TA 역할 Spring 경계 smoke #3

Merged
2 changed files with 140 additions and 0 deletions

View file

@ -0,0 +1,3 @@
# runtime-role-matrix-live-20260714111146-v10-ta-001-attempt-1-run-67f0ca150a2e
Forge 이슈 작업 브랜치 `forge/runtime-role-matrix-live-20260714111146-v10-ta-001-attempt-1-run-67f0ca150a2e`.

View file

@ -0,0 +1,137 @@
# ADR-001: Spring 경계 아키텍처 및 오류 계약
## Context
본 프로젝트(runtime-role-matrix-live)는 Spring Boot 기반의 REST API 서버로, 역할(Role) 기반 접근 제어 및 매트릭스 관리 기능을 제공한다. 현재 Controller, Service, Repository 계층 간 책임 분담이 명확하지 않고, 예외 처리 및 트랜잭션 경계가 일관되지 않아 유지보수성과 테스트 가능성이 저하되고 있다.
### 현재 문제점
- Controller에서 비즈니스 로직 직접 수행
- Service 계층의 트랜잭션 경계 불명확
- 예외 처리 방식이 계층마다 상이
- Repository 호출 시 구체적 예외가 Service까지 전파
## Decision
### 1. 계층별 책임 정의
| 계층 | 책임 |
|------|------|
| **Controller** | HTTP 요청/응답 변환, 입력 검증, HTTP 상태 코드 결정, Service 호출 |
| **Service** | 비즈니스 로직 수행, 트랜잭션 경계 관리, 도메인 객체 조작, 예외 변환 |
| **Repository** | 데이터 접근 추상화, JPA Entity 관리, 쿼리 실행 |
### 2. 오류 계약 (Error Contract)
```
Client Request
┌─────────────┐
│ Controller │ ← @Valid, BindingResult 검증
└──────┬──────┘
│ BusinessException 또는 도메인 예외
┌─────────────┐
│ Service │ ← 트랜잭션 경계, 예외 변환
└──────┬──────┘
│ DataAccessException (RuntimeException 래핑)
┌─────────────┐
│ Repository │ ← JPA/DB 접근
└─────────────┘
```
**예외 계층 구조:**
| 예외 유형 | 발생 계층 | 처리 방식 |
|-----------|-----------|-----------|
| `MethodArgumentNotValidException` | Controller | 400 Bad Request, 필드 오류 목록 반환 |
| `BusinessException` | Service | 409 Conflict 또는 404 Not Found |
| `DataAccessException` | Repository | Service에서 `PersistenceException`으로 변환 → 500 Internal Server Error |
| `EntityNotFoundException` | Repository/Service | 404 Not Found |
**표준 오류 응답 형식:**
```json
{
"timestamp": "2026-07-14T11:11:46Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"path": "/api/v1/roles",
"details": [
{ "field": "name", "message": "must not be blank" }
]
}
```
### 3. 트랜잭션 경계
| 작업 유형 | 트랜잭션 속성 | 전파 방식 |
|-----------|--------------|-----------|
| 조회 (Read) | `readOnly = true` | `REQUIRED` |
| 단일 생성/수정/삭제 | `readOnly = false` | `REQUIRED` |
| 다중 변경 (Batch) | `readOnly = false` | `REQUIRES_NEW` |
**규칙:**
- Service 메서드가 트랜잭션 경계의 시작점
- Controller에서 `@Transactional` 사용 금지
- Repository는 항상 트랜잭션 내 실행
### 4. 의존성 방향
```
Controller ──► Service ──► Repository
│ │
└──► DTO/VO ◄──┘
```
- Controller는 Service 인터페이스에만 의존
- Service는 Repository 인터페이스에만 의존
- Entity는 Repository → Service 방향으로만 이동
- DTO/VO는 Controller ↔ Service 간 통신에 사용
## Alternatives
### 대안 1: 모든 계층에서 예외 처리
| 장점 | 단점 |
|------|------|
| 세밀한 오류 제어 가능 | 예외 처리 코드 중복 |
| 계층별 맞춤 응답 가능 | 일관성 유지 어려움 |
### 대안 2: @ControllerAdvice 단일화
| 장점 | 단점 |
|------|------|
| 예외 처리 중앙화 | 너무 많은 예외 유형 매핑 필요 |
| 응답 형식 일관성 | 디버깅 복잡도 증가 |
### 선택: 계층별 예외 변환 + @ControllerAdvice
Service 계층에서 도메인 예외로 변환하고, `@ControllerAdvice`에서 HTTP 응답으로 매핑하는 하이브리드 방식 채택.
## Consequences
### 긍정적 결과
- **단일 책임 원칙 준수**: 각 계층이 명확한 역할 수행
- **테스트 용이성**: Mock 기반 단위 테스트 가능
- **일관된 오류 응답**: API 소비자가 예측 가능한 에러 형식 수신
- **트랜잭션 관리 용이**: Service 메서드 수준에서 트랜잭션 제어
### 부정적 결과
- **추가 코드 작성**: 예외 변환 클래스와 DTO 증가
- **학습 곡선**: 개발자별 아키텍처 이해 필요
- **트랜잭션 경계 설계 주의**: 잘못된 전파 설정 시 데이터 불일치 위험
### 추적 항목
| 항목 | 상태 | 비고 |
|------|------|------|
| 예외 계층 구조 구현 | Pending | BusinessException, PersistenceException 정의 |
| @ControllerAdvice 구성 | Pending | GlobalExceptionHandler |
| Service 트랜잭션 어노테이션 | Pending | @Transactional 적용 |
| 오류 응답 DTO 정의 | Pending | ErrorResponse, FieldErrorResponse |