runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-boundary-architecture.md
2026-07-14 10:18:28 +00:00

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