runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-architecture-boundaries.md
forge-bot 59a16ba145
All checks were successful
ci / test (pull_request) Successful in 5s
TA 역할 Spring 경계 smoke (role-ta-live-v3-001)
2026-07-14 08:29:21 +00:00

6.4 KiB

ADR-001: Spring 계층 경계, 오류 계약 및 트랜잭션 경계 정의

Context

runtime-role-matrix-live-202607141522-v3 프로젝트에서 TA(Tech Architect) 역할의 역할 기반 접근 제어(RBAC)가 Spring Boot 기반으로 구현된다. Controller, Service, Repository 계층 간 책임 범위, 오류 처리 규약, 트랜잭션 전파 전략을 명확히 정의하지 않으면 다음과 같은 문제가 발생한다.

  • 책임 혼재: Controller에서 비즈니스 로직 수행 또는 Repository 직접 호출로 결합도 증가
  • 일관성 없는 오류 처리: 각 계층마다 다른 예외 타입/메시지 반환으로 클라이언트 혼란
  • 트랜잭션 경계 불명확: 읽기 전용 쿼리에 불필요한 트랜잭션 오버헤드 또는 데이터 정합성 손실
  • 테스트 어려움: 계층 간 경계 모호 시 단위 테스트 mocking 전략 수립 곤란

Decision

1. Controller-Service-Repository 경계

계층 책임 금지 사항
Controller HTTP 요청/응답 변환, 입력 검증(Bean Validation), 라우팅, 예외 매핑 비즈니스 로직 직접 구현, Repository 직접 호출, @Transactional 선언
Service 비즈니스 로직 수행, 도메인 객체 조합, 트랜잭션 경계 설정, 오류 변환 HTTP 관련 코드(HttpServletRequest 등), 직접 JDBC/ORM 쿼리 실행
Repository 영속성 작업(DB CRUD), 쿼리 실행, JPA Entity 관리 비즈니스 로직, 다른 Repository 직접 호출, @Transactional 선언(상위 계층 위임)

2. 오류 계약 (Error Contract)

모든 계층에서 발생하는 오류는 RuntimeException 계층 구조로 표준화한다.

BaseException (RuntimeException)
├── BusinessException        → 사용자에게 의미 있는 메시지, HTTP 4xx 매핑
│   ├── RoleNotFoundException
│   ├── DuplicateRoleException
│   └── InsufficientPermissionException
└── SystemException          → 내부 오류, HTTP 5xx 매핑
    ├── DatabaseException
    └── ExternalServiceException

오류 응답 형식 (RFC 7807 Problem Details):

{
  "type": "https://api.example.com/errors/role-not-found",
  "title": "Role Not Found",
  "status": 404,
  "detail": "ID가 'admin'인 역할이 존재하지 않습니다.",
  "instance": "/api/v1/roles/admin",
  "timestamp": "2026-07-14T15:22:00Z",
  "traceId": "abc123"
}

@ControllerAdvice 예외 매핑 규칙:

예외 타입 HTTP 상태 코드 로깅 레벨
BusinessException 4xx (설정값) WARN
SystemException 500 ERROR
ValidationException 400 WARN
기타 예외 500 ERROR

3. 트랜잭션 경계

작업 유형 @Transactional 설정 전파 방식
읽기 전용 조회 readOnly = true REQUIRED
단일 쓰기 작업 readOnly = false (기본) REQUIRED
복합 쓰기 작업 readOnly = false REQUIRED
네스티드 읽기 readOnly = true NESTED (Savepoint)
독립 읽기 readOnly = true REQUIRES_NEW

트랜잭션 경계 위치: Service 계층의 public 메서드에 선언한다. Controller에서 @Transactional 사용을 금지한다.

격리 수준 (Isolation Level):

시나리오 격리 수준 선택 이유
역할 목록 조회 READ_COMMITTED 기본값, 동시성 성능
역할 할당/해제 REPEATABLE_READ 데이터 정합성 보장
설정 변경 SERIALIZABLE 극단적 정합성 필요 시

롤백 규칙: Checked Exception은 기본적으로 롤백되지 않으므로, 명시적 롤백이 필요한 BusinessException에는 @Transactional(rollbackFor = BusinessException.class)를 적용한다.

4. 패키지 구조

com.example.runtimematrix
├── controller          # REST API 엔드포인트
│   └── RoleController.java
├── service             # 비즈니스 로직 + 트랜잭션
│   ├── RoleService.java
│   └── impl/
├── repository          # 영속성 접근
│   ├── RoleRepository.java
│   └── custom/
├── domain              # 엔티티, 밸류 오브젝트
│   ├── entity/
│   └── vo/
├── exception           # 예외 계층 구조
│   ├── BaseException.java
│   ├── BusinessException.java
│   └── SystemException.java
├── dto                 # 요청/응답 DTO
│   ├── request/
│   └── response/
└── config              # 설정 클래스

Alternatives

대안 1: Controller-Service-Repository 외에 DTO 변환 계층 추가

선택하지 않은 이유: 소규모 프로젝트에서 과도한 추상화로 복잡성 증가. DTO 변환은 Mapper 라이브러리(MapStruct) 또는 수동 변환으로 Service 내에서 처리한다.

대안 2: 모든 예외를 RuntimeException으로 통일

선택하지 않은 이유: 예외 유형 구분 없이는 @ControllerAdvice에서 HTTP 상태 코드 매핑이 어려우며, 클라이언트에게 의미 있는 오류 피드백 제공 곤란.

대안 3: 트랜잭션을 Controller에 선언

선택하지 않은 이유: HTTP 요청/응답 처리와 트랜잭션 관점 분리 필요. AOP 프록시 기반 트랜잭션은 public 메서드에만 적용되므로 Controller의 메서드 레벨 제어가 불완전하다.

Consequences

긍정적 영향

  • 단위 테스트 용이성: Service를 MockRepository로 교체하여 독립 테스트 가능
  • 일관된 오류 처리: 클라이언트가 예측 가능한 오류 응답 형식 수신
  • 트랜잭션 최적화: 읽기 전용 쿼리에서 readOnly=true로 불필요한 쓰기 잠금 해제
  • 유지보수성: 계층별 책임 명확화로 개발자 간 협업 효율 향상

부정적 영향

  • 추가 클래스 생성: 예외 계층 구조로 인한 다수의 예외 클래스 필요
  • DTO 변환 코드: Service 계층에서 Entity ↔ DTO 변환 로직 추가
  • 트랜잭션 경계 설계 필요: 각 Service 메서드별 트랜잭션 설정 의사결정 필요

모니터링 및 검증

  • 트랜잭션 경계 위반 시 TransactionException 발생으로 조기 감지
  • @ControllerAdvice 전역 예외 처리 로깅으로 오류 패턴 분석 가능
  • Integration Test에서 @Transactional(readOnly = true) 조회 후 수정 시도 시 예외 검증