5.2 KiB
5.2 KiB
ADR-001: Spring 경계 아키텍처 정의
날짜: 2026-07-14 상태: 수락됨 결정자: TA 아키텍트
Context (배경)
TA 역할 프로젝트는 역할(Role) 기반 접근 제어 시스템을 구현한다. 다중 계층 구조에서 Controller, Service, Repository 각 계층의 책임 범위, 오류 계약, 트랜잭션 경계를 명확히 정의하지 않으면 다음과 같은 문제가 발생한다.
- 책임 혼재: 비즈니스 로직이 Controller에 유출되거나, DB 접근 로직이 Service에 직접 작성
- 일관성 없는 오류 처리: 각 계층마다 다른 예외 타입과 HTTP 상태 코드를 반환
- 트랜잭션 누락/과다: 읽기 전용 쿼리에 불필요한 트랜잭션이 걸리거나, 다중 쓰기 작업이 원자성 없이 실행
- 테스트 어려움: 계층 간 결합으로 인해 단위 테스트가 불가능
Decision (결정)
1. 계층 책임 경계
| 계층 | 책임 | 포함 사항 | 금지 사항 |
|---|---|---|---|
| Controller | HTTP 요청/응답 변환, 입력 검증, 라우팅 | @RequestMapping, @Valid, @RequestBody 파싱, 응답 DTO 변환 |
비즈니스 로직 직접 실행, DB 접근, Service 메서드 직접 호출 없이 로직 처리 |
| Service | 비즈니스 로직, 트랜잭션 관리, 도메인 조율 | @Transactional, 도메인 객체 조작, 다중 Repository 호출, 정책 enforcement |
HTTP 요청/응답 직접 처리, SQL 직접 작성, @Entity 직접 매핑 반환 |
| Repository | 데이터 접근 추상화, 쿼리 실행 | JpaRepository 확장, @Query, EntityManager 직접 사용, Specification 패턴 |
비즈니스 로직, 트랜잭션 경계 설정, 응답 형식 결정 |
2. 오류 계약 (Error Contract)
모든 계층에서 발생하는 예외는 단일 예외 계층 구조로 변환되어 Controller에서 일관된 HTTP 응답을 생성한다.
BaseException (추상)
├── BusinessException → HTTP 400 (잘못된 요청)
│ ├── RoleNotFoundException
│ ├── DuplicateRoleException
│ └── InvalidRoleStateException
├── AuthorizationException → HTTP 403 (권한 없음)
└── SystemException → HTTP 500 (서버 오류)
├── DataAccessException
└── ExternalServiceException
오류 응답 형식 (RFC 7807 Problem Details):
{
"type": "https://api.example.com/errors/role-not-found",
"title": "Role Not Found",
"status": 404,
"detail": "Role with id '123' does not exist",
"instance": "/api/v1/roles/123",
"timestamp": "2026-07-14T10:17:23Z"
}
계약 규칙:
- Service 계층은
BusinessException하위 타입만 던진다 - Repository 계층 예외는 Service 계층에서
SystemException으로 래핑한다 - Controller는
@ControllerAdvice에서 전역 예외를 처리한다 - 예외 메시지는 외부 노출용으로 사용자 친화적이어야 한다
3. 트랜잭션 경계
| 시나리오 | 전파 방식 | 격리 수준 | 읽기 전용 |
|---|---|---|---|
| 단일 조회 (findById) | REQUIRED | DEFAULT | true |
| 목록 조회 (findAll) | REQUIRED | DEFAULT | true |
| 단일 생성 (save) | REQUIRED | DEFAULT | false |
| 벌크 업데이트 (bulk update) | REQUIRED | READ_COMMITTED | false |
| 다중 리포지토리 쓰기 | REQUIRED | READ_COMMITTED | false |
| 읽기 전용 조회 (통계/리포트) | REQUIRED_READ_ONLY | DEFAULT | true |
트랜잭션 롤백 규칙:
RuntimeException,DataAccessException은 자동 롤백- 검사 예외(
Checked Exception)는 명시적rollbackFor지정 필요 - 읽기 전용 트랜잭션에서 쓰기 시도 시 예외 발생
Alternatives (대안)
대안 1: Service에서 직접 예외 던지기 (현재 미선택)
- 각 Service 메서드가 다양한 예외 타입을 직접 던짐
- 단점: Controller에서 예외 타입별 분기 처리 필요, 일관성 유지 어려움
대안 2: 트랜잭션 없음 (수동 커밋)
TransactionTemplate을 수동으로 사용- 단점: 코드 복잡성 증가, 실수 가능성 높음
대안 3: Repository에서 비즈니스 로직 포함
- 단점: 데이터 접근과 비즈니스 로직 결합, 테스트 어려움, 재사용성 저하
Consequences (결과)
긍정적 결과
- 단위 테스트 용이: 각 계층이 명확히 분리되어 Mock 기반 테스트 가능
- 유지보수성: 오류 처리와 트랜잭션 정책이 한 곳에 집중
- 일관성: 모든 API가 동일한 오류 응답 형식 제공
- 확장성: 새 예외 타입 추가 시
BaseException하위 클래스만 생성
부정적 결과
- 초기 개발 시간: 예외 계층 구조와
@ControllerAdvice설정 필요 - 학습 곡선: 개발자가 계층 책임 경계와 트랜잭션 전파 규칙을 숙지해야 함
- 오버엔지니어링 위험: 소규모 프로젝트에서는 과한 추상화 가능성
모니터링 지표
- 예외 발생 시
type필드로 문제 유형 추적 - 트랜잭션 경과 시간 로깅으로 성능 병목 탐지
- 계층 간 호출 횟수 카운터로 불필요한 조회 감지