runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-boundary-contract.md
forge-bot 927cc8b5d9
All checks were successful
ci / test (pull_request) Successful in 1m42s
TA 역할 Spring 경계 smoke (role-ta-live-v2-001)
2026-07-14 08:24:00 +00:00

4.6 KiB

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 구조로 변환한다.
  • 내부 예외 메시지는 클라이언트에 노출하지 않는다 (로그에만 기록).
  • 예외 계층 구조: RuntimeExceptionBaseException → 구체 예외 클래스.

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 문제)