4.8 KiB
4.8 KiB
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 |
표준 오류 응답 형식:
{
"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 |