From 927cc8b5d94ec3b4937a8006063dfc99b2437d4b Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 08:24:00 +0000 Subject: [PATCH] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84=20smoke=20(role-ta-live-v2-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/adr/ADR-001-spring-boundary-contract.md | 99 ++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/adr/ADR-001-spring-boundary-contract.md diff --git a/docs/adr/ADR-001-spring-boundary-contract.md b/docs/adr/ADR-001-spring-boundary-contract.md new file mode 100644 index 0000000..dc8def0 --- /dev/null +++ b/docs/adr/ADR-001-spring-boundary-contract.md @@ -0,0 +1,99 @@ +# 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 문제)