# ADR-001: Spring 계층 경계, 오류 계약 및 트랜잭션 경계 정의 **날짜:** 2026-07-14 **상태:** 수락됨 **결정자:** TA 아키텍트 --- ## Context (맥락) `runtime-role-matrix-live-202607141522-v2` 프로젝트는 Spring Boot 기반 멀티 모듈 애플리케이션이다. 현재 계층 간 책임 범위가 모호하고, 예외 처리 정책이 불명확하며, 트랜잭션 전파 규칙이 문서화되지 않아 유지보수성과 일관성에 위험이 존재한다. --- ## Decision (결정) ### 1. Controller-Service-Repository 경계 | 계층 | 책임 | 허용 작업 | |------|------|-----------| | **Controller** | HTTP 요청/응답 변환, 입력 검증, HTTP 상태 코드 결정 | `@Valid`, `@RequestBody`, `@PathVariable` 파싱, Service 호출, `ResponseEntity` 반환 | | **Service** | 비즈니스 로직, 도메인 규칙, 트랜잭션 경계 | Repository 호출, 도메인 객체 조작, 다른 Service 호출 (필요시), `@Transactional` 적용 | | **Repository** | 데이터 접근 추상화, 쿼리 실행 | JPA `CrudRepository`/`JpaRepository` 메서드, `@Query`, `EntityManager` 직접 사용 | **금지 규칙:** - Controller는 `@Transactional`을 직접 적용하지 않는다. - Repository는 비즈니스 로직을 포함하지 않는다. - Service 계층을 건너뛰고 Controller가 Repository를 직접 호출하지 않는다. ### 2. 오류 계약 (Error Contract) 모든 계층에서 발생하는 예외는 다음 규칙을 따른다. | 예외 유형 | 발생 계층 | HTTP 상태 | 응답 본문 필드 | |----------|-----------|-----------|---------------| | `ValidationException` | Controller | 400 | `{ "code": "VALIDATION_ERROR", "message": "...", "field": "..." }` | | `ResourceNotFoundException` | Service/Repository | 404 | `{ "code": "NOT_FOUND", "message": "...", "resourceId": "..." }` | | `BusinessRuleViolationException` | Service | 409 | `{ "code": "BUSINESS_RULE_VIOLATION", "message": "...", "rule": "..." }` | | `DataAccessException` | Repository | 500 | `{ "code": "DATABASE_ERROR", "message": "Internal server error" }` | | `UnexpectedException` | Any | 500 | `{ "code": "INTERNAL_ERROR", "message": "Internal server error" }` | **오류 계약 규칙:** - ControllerAdvice가 모든 예외를 가로채고 일관된 JSON 구조로 변환한다. - 내부 예외 메시지는 클라이언트에 노출하지 않는다 (로그에만 기록). - 예외 계층 구조: `RuntimeException` → `BaseException` → 구체 예외 클래스. ### 3. 트랜잭션 경계 | 시나리오 | 전파 방식 | 격리 수준 | 롤백 규칙 | |---------|-----------|-----------|-----------| | Service → Repository 호출 | `REQUIRED` (기본) | `READ_COMMITTED` (기본) | Checked 예외는 롤백하지 않음, Unchecked 예외는 롤백 | | readOnly 트랜잭션 | `REQUIRED` + `readOnly=true` | `READ_COMMITTED` | 읽기 전용, 쓰기 시 예외 발생 | | 다중 Service 호출 (같은 트랜잭션) | `REQUIRED` | 상속됨 | 부모 트랜잭션과 동일하게 처리 | | 별도 트랜잭션 필요 | `REQUIRES_NEW` | 명시적 지정 | 독립 롤백 가능 | **트랜잭션 규칙:** - `@Transactional`은 public 메서드에만 적용한다. - 클래스 레벨 `@Transactional`보다 메서드 레벨이 우선한다. - `readOnly=true`는 SELECT 성능 최적화를 위해 명시적으로 설정한다. --- ## Alternatives (대안) ### 대안 1: DTO 직접 전달 방식 Controller가 Entity를 직접 Service에 전달하고 Service가 Entity를 직접 반환한다. - **단점:** 계층 간 결합도 증가, 테스트 어려움, 도메인 오염 위험. - **선택하지 않음** ### 대안 2: 전역 예외 처리 미사용 각 Controller에서 개별적으로 예외를 처리한다. - **단점:** 코드 중복, 응답 형식 불일치, 유지보수 비용 증가. - **선택하지 않음** ### 대안 3: Programmatic Transaction `TransactionTemplate`을 사용하여 명시적으로 트랜잭션을 관리한다. - **단점:** 선언적 트랜잭션 대비 코드 복잡, 실수 가능성 증가. - **선택하지 않음** --- ## Consequences (결과) ### 긍정적 결과 - 계층별 책임이 명확해져 개발자 간 협업 효율 향상 - 일관된 오류 응답으로 API 소비자가 예측 가능한 에러 처리 가능 - 선언적 트랜잭션으로 데이터 무결성 보장 ### 부정적 결과 - DTO 변환 코드가 추가됨 (Mapper 또는 수동 변환) - 예외 계층 구조 도입으로 초기 개발 시간 증가 - `readOnly` 설정 누락 시 성능 최적화 미적용 ### 모니터링 필요 항목 - Controller 레벨 HTTP 5xx 에러율 - Service 레벨 트랜잭션 롤백 빈도 - Repository 레벨 쿼리 성능 (N+1 문제)