# 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)**: ```json { "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` 필드로 문제 유형 추적 - 트랜잭션 경과 시간 로깅으로 성능 병목 탐지 - 계층 간 호출 횟수 카운터로 불필요한 조회 감지