TA 역할 Spring 경계 smoke #3
2 changed files with 227 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
||||||
|
# runtime-role-matrix-live-20260714105818-v9-ta-001-attempt-1-run-a48b5fc922fc
|
||||||
|
|
||||||
|
Forge 이슈 작업 브랜치 `forge/runtime-role-matrix-live-20260714105818-v9-ta-001-attempt-1-run-a48b5fc922fc`.
|
||||||
224
docs/adr/ADR-001-spring-boundary-architecture.md
Normal file
224
docs/adr/ADR-001-spring-boundary-architecture.md
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
# ADR-001: Spring 경계 아키텍처 정의
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
runtime-role-matrix-live 프로젝트는 역할(Role) 기반 접근 제어 시스템을 구현한다. 다중 계층 구조에서 Controller, Service, Repository 간의 책임 분리와 오류 처리, 트랜잭션 관리가 명확히 정의되지 않아 다음 문제가 발생한다.
|
||||||
|
|
||||||
|
| 문제점 | 영향 |
|
||||||
|
|--------|------|
|
||||||
|
| Controller에서 비즈니스 로직 직접 실행 | 단일 책임 원칙 위반, 테스트 어려움 |
|
||||||
|
| Service에서 unchecked exception 무분별한 전파 | 일관된 오류 응답 불가 |
|
||||||
|
| Repository에서 트랜잭션 경계 불분명 | 데이터 정합성 위험 |
|
||||||
|
| 각 계층 간 계약(contract) 부재 | API 스펙 변경 시 파급 효과 예측 불가 |
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Controller-Service-Repository 경계
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Controller Layer │
|
||||||
|
│ - HTTP 요청/응답 처리 │
|
||||||
|
│ - 입력 검증 (DTO 변환, Bean Validation) │
|
||||||
|
│ - HTTP 상태 코드 결정 │
|
||||||
|
│ - Service 호출 및 결과 매핑 │
|
||||||
|
│ - 예외를 HTTP 응답으로 변환 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Service Layer │
|
||||||
|
│ - 비즈니스 로직 수행 │
|
||||||
|
│ - 도메인 객체 조작 │
|
||||||
|
│ - 트랜잭션 경계 설정 (@Transactional) │
|
||||||
|
│ - Repository 호출 │
|
||||||
|
│ - 도메인 예외을 ServiceException으로 변환 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Repository Layer │
|
||||||
|
│ - 데이터 접근 (JPA Repository) │
|
||||||
|
│ - 엔티티 ↔ 도메인 객체 변환 │
|
||||||
|
│ - 순수 데이터 조작만 담당 │
|
||||||
|
│ - 예외는 그대로 전파 (DataAccessException) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**책임 매트릭스**
|
||||||
|
|
||||||
|
| 책임 | Controller | Service | Repository |
|
||||||
|
|------|:----------:|:-------:|:----------:|
|
||||||
|
| HTTP 파라미터 바인딩 | ✅ | ❌ | ❌ |
|
||||||
|
| Bean Validation | ✅ | △ (도메인 검증) | ❌ |
|
||||||
|
| 비즈니스 로직 | ❌ | ✅ | ❌ |
|
||||||
|
| 트랜잭션 관리 | ❌ | ✅ | ❌ |
|
||||||
|
| 데이터 접근 | ❌ | ❌ | ✅ |
|
||||||
|
| DTO ↔ Entity 변환 | ✅ | △ (도메인 변환) | ✅ |
|
||||||
|
| 예외 → HTTP 응답 | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
### 2. 오류 계약 (Error Contract)
|
||||||
|
|
||||||
|
**예외 계층 구조**
|
||||||
|
|
||||||
|
```
|
||||||
|
RuntimeException
|
||||||
|
├── ServiceException (체크 예외, 비즈니스 오류)
|
||||||
|
│ ├── RoleNotFoundException
|
||||||
|
│ ├── RoleAlreadyExistsException
|
||||||
|
│ └── PermissionDeniedException
|
||||||
|
└── DataAccessException (Spring, unchecked)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ServiceException 스펙**
|
||||||
|
|
||||||
|
| 필드 | 타입 | 필수 | 설명 |
|
||||||
|
|------|------|:----:|------|
|
||||||
|
| code | String | ✅ | 오류 코드 (e.g., "ROLE_NOT_FOUND") |
|
||||||
|
| message | String | ✅ | 사용자에게 표시할 메시지 |
|
||||||
|
| details | Map | ❌ | 추가 메타데이터 |
|
||||||
|
| timestamp | Instant | ✅ | 발생 시각 |
|
||||||
|
|
||||||
|
**HTTP 상태 코드 매핑**
|
||||||
|
|
||||||
|
| 예외 | HTTP 상태 | 이유 |
|
||||||
|
|------|:---------:|------|
|
||||||
|
| RoleNotFoundException | 404 | 리소스 없음 |
|
||||||
|
| RoleAlreadyExistsException | 409 | 리소스 충돌 |
|
||||||
|
| PermissionDeniedException | 403 | 권한 없음 |
|
||||||
|
| ValidationException | 400 | 잘못된 요청 |
|
||||||
|
| ServiceException (기타) | 500 | 내부 서버 오류 |
|
||||||
|
|
||||||
|
**오류 응답 형식 (RFC 7807 Problem Details)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "https://api.example.com/errors/role-not-found",
|
||||||
|
"title": "Role Not Found",
|
||||||
|
"status": 404,
|
||||||
|
"code": "ROLE_NOT_FOUND",
|
||||||
|
"message": "ID가 'admin'인 역할을 찾을 수 없습니다.",
|
||||||
|
"details": {
|
||||||
|
"roleId": "admin",
|
||||||
|
"timestamp": "2026-07-14T10:58:18Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 트랜잭션 경계
|
||||||
|
|
||||||
|
**트랜잭션 전파 정책**
|
||||||
|
|
||||||
|
| 시나리오 | 전파 방식 | 설명 |
|
||||||
|
|---------|:---------:|------|
|
||||||
|
| Service → Repository | REQUIRED (기본) | 기존 트랜잭션 참여 또는 신규 생성 |
|
||||||
|
| Service → Service (내부 호출) | REQUIRED | 같은 트랜잭션 내에서 실행 |
|
||||||
|
| readOnly 조회 | readOnly=true | 성능 최적화,Dirty checking 비활성화 |
|
||||||
|
| 쓰기 작업 | readOnly=false (기본) | 기본값, 명시적 지정 불필요 |
|
||||||
|
|
||||||
|
**트랜잭션 경계 설정 규칙**
|
||||||
|
|
||||||
|
1. **트랜잭션 시작점**: Service 계층의 public 메서드
|
||||||
|
2. **트랜잭션 종료점**: Service 메서드 종료 시 commit, 예외 발생 시 rollback
|
||||||
|
3. **Controller에서 @Transactional 금지**: HTTP 요청 스레드와 트랜잭션 바인딩 분리
|
||||||
|
4. **Repository에서 @Transactional 금지**: 데이터 접근만 담당
|
||||||
|
|
||||||
|
**트랜잭션 시퀀스 다이어그램**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Controller
|
||||||
|
participant S as Service
|
||||||
|
participant R as Repository
|
||||||
|
participant DB as Database
|
||||||
|
|
||||||
|
C->>+S: createRole(dto)
|
||||||
|
S->>S: @Transactional 시작
|
||||||
|
S->>+R: existsByRoleId(id)
|
||||||
|
R->>+DB: SELECT
|
||||||
|
DB-->-R: 결과
|
||||||
|
R-->-S: false
|
||||||
|
alt 역할 존재 시
|
||||||
|
S-->>C: 예외 발생 (RoleAlreadyExistsException)
|
||||||
|
else 역할 미존재 시
|
||||||
|
S->>+R: save(entity)
|
||||||
|
R->>+DB: INSERT
|
||||||
|
DB-->-R: 저장된 엔티티
|
||||||
|
R-->-S: 저장된 엔티티
|
||||||
|
S-->>S: @Transactional 커밋
|
||||||
|
S-->>-C: 생성된 역할 DTO
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over S,DB: 예외 발생 시 자동 Rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
**외부 시스템 연동 시 트랜잭션 처리**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ @Transactional │
|
||||||
|
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
||||||
|
│ │ DB 저장 │ │ 메시지 발송 │ │ 외부 API 호출 │ │
|
||||||
|
│ │ (트랜잭션 참여)│ │ (로컬 트랜잭션)│ │ (비트랜잭션) │ │
|
||||||
|
│ └────────────────┘ └────────────────┘ └────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 실패 시: DB 저장만 롤백, 메시지/외부API는 별도 보상 처리 필요 │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
### 대안 1: Controller에서 직접 Repository 호출
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 장점 | 간단한 CRUD에 코드량 감소 |
|
||||||
|
| 단점 | 비즈니스 로직 분산, 테스트 어려움, 트랜잭션 관리 불가 |
|
||||||
|
| 채택 여부 | ❌ 불채택 - 확장성 및 유지보수성 저하 |
|
||||||
|
|
||||||
|
### 대안 2: Service 계층 없이 도메인 객체에 비즈니스 로직 포함
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 장점 | 도메인 주도 설계(DDD) 접근, 객체지향적 |
|
||||||
|
| 단점 | 도메인 객체가 프레임워크 의존성 발생, 테스트 복잡 |
|
||||||
|
| 채택 여부 | ❌ 불채택 - 현재 프로젝트 규모에서 과도한 복잡성 |
|
||||||
|
|
||||||
|
### 대안 3: 전역 예외 처리 (@ControllerAdvice)만 사용, 계층별 예외 변환 없음
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 장점 | 구현 단순화 |
|
||||||
|
| 단점 | 예외 처리 로직 중앙화되어 단일 책임 위반, 테스트 어려움 |
|
||||||
|
| 채택 여부 | ❌ 불채택 - 계층별 명확한 오류 계약 필요 |
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### 긍정적 결과
|
||||||
|
|
||||||
|
- **단일 책임 원칙 준수**: 각 계층이 명확한 역할을 담당
|
||||||
|
- **테스트 용이성**: Mock을 통한 단위 테스트 가능
|
||||||
|
- **일관된 오류 처리**: RFC 7807 표준 준수, 예측 가능한 API 응답
|
||||||
|
- **트랜잭션 관리 명확성**: Service 계층에서 트랜잭션 경계 집중 관리
|
||||||
|
- **유지보수성 향상**: 변경 시 파급 효과 최소화
|
||||||
|
|
||||||
|
### 부정적 결과 (적용 부담)
|
||||||
|
|
||||||
|
- **코드량 증가**: DTO 변환, 예외 변환 로직 추가
|
||||||
|
- **학습 곡선**: 개발자 역량 요구사항 상승
|
||||||
|
- **추가 의존성**: 예외 계층 구조 관리 필요
|
||||||
|
|
||||||
|
### 모니터링 및 검증
|
||||||
|
|
||||||
|
| 지표 | 측정 방법 |
|
||||||
|
|------|----------|
|
||||||
|
| 계층 분리 준수율 | 코드 리뷰 시 Controller에 비즈니스 로직 존재 여부 체크 |
|
||||||
|
| 예외 처리 일관성 | @ControllerAdvice 로그 분석 |
|
||||||
|
| 트랜잭션 커밋/롤백 비율 | 트랜잭션 로그 모니터링 |
|
||||||
|
|
||||||
|
### 마이그레이션 계획
|
||||||
|
|
||||||
|
1. 기존 코드를 점진적으로 리팩토링 (하위 호환 유지)
|
||||||
|
2. 새로운 기능은 ADR 규칙 즉시 적용
|
||||||
|
3. 공통 예외 클래스를 `exception` 패키지에 배치
|
||||||
|
4. DTO 클래스를 `dto` 패키지에 배치
|
||||||
Loading…
Add table
Add a link
Reference in a new issue