TA 역할 Spring 경계 smoke #3
1 changed files with 140 additions and 0 deletions
140
docs/adr/ADR-001-spring-architecture-boundaries.md
Normal file
140
docs/adr/ADR-001-spring-architecture-boundaries.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# ADR-001: Spring 계층 경계, 오류 계약 및 트랜잭션 경계 정의
|
||||
|
||||
## Context
|
||||
|
||||
runtime-role-matrix-live-202607141522-v3 프로젝트에서 TA(Tech Architect) 역할의 역할 기반 접근 제어(RBAC)가 Spring Boot 기반으로 구현된다. Controller, Service, Repository 계층 간 책임 범위, 오류 처리 규약, 트랜잭션 전파 전략을 명확히 정의하지 않으면 다음과 같은 문제가 발생한다.
|
||||
|
||||
- **책임 혼재**: Controller에서 비즈니스 로직 수행 또는 Repository 직접 호출로 결합도 증가
|
||||
- **일관성 없는 오류 처리**: 각 계층마다 다른 예외 타입/메시지 반환으로 클라이언트 혼란
|
||||
- **트랜잭션 경계 불명확**: 읽기 전용 쿼리에 불필요한 트랜잭션 오버헤드 또는 데이터 정합성 손실
|
||||
- **테스트 어려움**: 계층 간 경계 모호 시 단위 테스트 mocking 전략 수립 곤란
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Controller-Service-Repository 경계
|
||||
|
||||
| 계층 | 책임 | 금지 사항 |
|
||||
|------|------|-----------|
|
||||
| **Controller** | HTTP 요청/응답 변환, 입력 검증(Bean Validation), 라우팅, 예외 매핑 | 비즈니스 로직 직접 구현, Repository 직접 호출, @Transactional 선언 |
|
||||
| **Service** | 비즈니스 로직 수행, 도메인 객체 조합, 트랜잭션 경계 설정, 오류 변환 | HTTP 관련 코드(HttpServletRequest 등), 직접 JDBC/ORM 쿼리 실행 |
|
||||
| **Repository** | 영속성 작업(DB CRUD), 쿼리 실행, JPA Entity 관리 | 비즈니스 로직, 다른 Repository 직접 호출, @Transactional 선언(상위 계층 위임) |
|
||||
|
||||
### 2. 오류 계약 (Error Contract)
|
||||
|
||||
모든 계층에서 발생하는 오류는 `RuntimeException` 계층 구조로 표준화한다.
|
||||
|
||||
```
|
||||
BaseException (RuntimeException)
|
||||
├── BusinessException → 사용자에게 의미 있는 메시지, HTTP 4xx 매핑
|
||||
│ ├── RoleNotFoundException
|
||||
│ ├── DuplicateRoleException
|
||||
│ └── InsufficientPermissionException
|
||||
└── SystemException → 내부 오류, HTTP 5xx 매핑
|
||||
├── DatabaseException
|
||||
└── ExternalServiceException
|
||||
```
|
||||
|
||||
**오류 응답 형식 (RFC 7807 Problem Details)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://api.example.com/errors/role-not-found",
|
||||
"title": "Role Not Found",
|
||||
"status": 404,
|
||||
"detail": "ID가 'admin'인 역할이 존재하지 않습니다.",
|
||||
"instance": "/api/v1/roles/admin",
|
||||
"timestamp": "2026-07-14T15:22:00Z",
|
||||
"traceId": "abc123"
|
||||
}
|
||||
```
|
||||
|
||||
**@ControllerAdvice 예외 매핑 규칙**:
|
||||
|
||||
| 예외 타입 | HTTP 상태 코드 | 로깅 레벨 |
|
||||
|----------|---------------|----------|
|
||||
| BusinessException | 4xx (설정값) | WARN |
|
||||
| SystemException | 500 | ERROR |
|
||||
| ValidationException | 400 | WARN |
|
||||
| 기타 예외 | 500 | ERROR |
|
||||
|
||||
### 3. 트랜잭션 경계
|
||||
|
||||
| 작업 유형 | @Transactional 설정 | 전파 방식 |
|
||||
|----------|---------------------|----------|
|
||||
| **읽기 전용 조회** | `readOnly = true` | REQUIRED |
|
||||
| **단일 쓰기 작업** | `readOnly = false` (기본) | REQUIRED |
|
||||
| **복합 쓰기 작업** | `readOnly = false` | REQUIRED |
|
||||
| **네스티드 읽기** | `readOnly = true` | NESTED (Savepoint) |
|
||||
| **독립 읽기** | `readOnly = true` | REQUIRES_NEW |
|
||||
|
||||
**트랜잭션 경계 위치**: Service 계층의 public 메서드에 선언한다. Controller에서 @Transactional 사용을 금지한다.
|
||||
|
||||
**격리 수준 (Isolation Level)**:
|
||||
|
||||
| 시나리오 | 격리 수준 | 선택 이유 |
|
||||
|---------|----------|-----------|
|
||||
| 역할 목록 조회 | READ_COMMITTED | 기본값, 동시성 성능 |
|
||||
| 역할 할당/해제 | REPEATABLE_READ | 데이터 정합성 보장 |
|
||||
| 설정 변경 | SERIALIZABLE | 극단적 정합성 필요 시 |
|
||||
|
||||
**롤백 규칙**: Checked Exception은 기본적으로 롤백되지 않으므로, 명시적 롤백이 필요한 BusinessException에는 `@Transactional(rollbackFor = BusinessException.class)`를 적용한다.
|
||||
|
||||
### 4. 패키지 구조
|
||||
|
||||
```
|
||||
com.example.runtimematrix
|
||||
├── controller # REST API 엔드포인트
|
||||
│ └── RoleController.java
|
||||
├── service # 비즈니스 로직 + 트랜잭션
|
||||
│ ├── RoleService.java
|
||||
│ └── impl/
|
||||
├── repository # 영속성 접근
|
||||
│ ├── RoleRepository.java
|
||||
│ └── custom/
|
||||
├── domain # 엔티티, 밸류 오브젝트
|
||||
│ ├── entity/
|
||||
│ └── vo/
|
||||
├── exception # 예외 계층 구조
|
||||
│ ├── BaseException.java
|
||||
│ ├── BusinessException.java
|
||||
│ └── SystemException.java
|
||||
├── dto # 요청/응답 DTO
|
||||
│ ├── request/
|
||||
│ └── response/
|
||||
└── config # 설정 클래스
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
### 대안 1: Controller-Service-Repository 외에 DTO 변환 계층 추가
|
||||
|
||||
**선택하지 않은 이유**: 소규모 프로젝트에서 과도한 추상화로 복잡성 증가. DTO 변환은 Mapper 라이브러리(MapStruct) 또는 수동 변환으로 Service 내에서 처리한다.
|
||||
|
||||
### 대안 2: 모든 예외를 RuntimeException으로 통일
|
||||
|
||||
**선택하지 않은 이유**: 예외 유형 구분 없이는 @ControllerAdvice에서 HTTP 상태 코드 매핑이 어려우며, 클라이언트에게 의미 있는 오류 피드백 제공 곤란.
|
||||
|
||||
### 대안 3: 트랜잭션을 Controller에 선언
|
||||
|
||||
**선택하지 않은 이유**: HTTP 요청/응답 처리와 트랜잭션 관점 분리 필요. AOP 프록시 기반 트랜잭션은 public 메서드에만 적용되므로 Controller의 메서드 레벨 제어가 불완전하다.
|
||||
|
||||
## Consequences
|
||||
|
||||
### 긍정적 영향
|
||||
|
||||
- **단위 테스트 용이성**: Service를 MockRepository로 교체하여 독립 테스트 가능
|
||||
- **일관된 오류 처리**: 클라이언트가 예측 가능한 오류 응답 형식 수신
|
||||
- **트랜잭션 최적화**: 읽기 전용 쿼리에서 readOnly=true로 불필요한 쓰기 잠금 해제
|
||||
- **유지보수성**: 계층별 책임 명확화로 개발자 간 협업 효율 향상
|
||||
|
||||
### 부정적 영향
|
||||
|
||||
- **추가 클래스 생성**: 예외 계층 구조로 인한 다수의 예외 클래스 필요
|
||||
- **DTO 변환 코드**: Service 계층에서 Entity ↔ DTO 변환 로직 추가
|
||||
- **트랜잭션 경계 설계 필요**: 각 Service 메서드별 트랜잭션 설정 의사결정 필요
|
||||
|
||||
### 모니터링 및 검증
|
||||
|
||||
- 트랜잭션 경계 위반 시 `TransactionException` 발생으로 조기 감지
|
||||
- @ControllerAdvice 전역 예외 처리 로깅으로 오류 패턴 분석 가능
|
||||
- Integration Test에서 @Transactional(readOnly = true) 조회 후 수정 시도 시 예외 검증
|
||||
Loading…
Add table
Add a link
Reference in a new issue