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

113 lines
5.2 KiB
Markdown

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