# ADR-001: Spring 전환 경계, 계약, 오류 및 트랜잭션 결정 **날짜:** 2025-07-14 **상태:** 수락됨 **결정자:** TA 아키텍트 --- ## 컨텍스트 runtime-role-matrix-live 프로젝트는 기존 레거시 런타임에서 Spring Boot 기반 런타임으로 전환 중이다. 전환 과정에서 다음 사항에 대한 명확한 경계와 계약이 필요하다: - Spring 컴포넌트와 비-Spring 레거시 간의 경계 - 컴포넌트 간 계약(인터페이스) 정의 - 예외 처리 정책 - 트랜잭션 전파 및 경계 관리 --- ## 결정 ### 1. Spring 경계 (Boundary) | 영역 | 경계 전략 | 설명 | |------|-----------|------| | **도입점** | `@SpringBootApplication` 메인 클래스 | 외부 요청의 단일 진입점 | | **내부 컴포넌트** | `@Configuration` + `@Bean` | Spring 컨테이너 관리 대상 | | **외부 의존성** | `Adapter` 패턴 | 비-Spring 라이브러리를 Spring Bean으로 감싸기 | | **레거시 통합** | `@Qualifier` 주입 | Bean 이름으로 명시적 의존성 해결 | **경계 원칙:** - Spring 컨테이너 내부에서만 `@Autowired`, `@Inject` 사용 - 컨테이너 외부 레거시 코드는 `ApplicationContext`에서 Bean 조회 후 전달 - 순환 참조 방지: 생성자 주입 우선, `@Lazy` 활용 ### 2. 계약 (Contract) **인터페이스 계층:** ``` ┌─────────────────────────────────────┐ │ Presentation Layer (@Controller) │ │ DTO / Request / Response │ └─────────────────┬───────────────────┘ │ ┌─────────────────▼───────────────────┐ │ Application Layer (@Service) │ │ Service Interface │ └─────────────────┬───────────────────┘ │ ┌─────────────────▼───────────────────┐ │ Domain Layer (Entity) │ │ Domain Model │ └─────────────────┬───────────────────┘ │ ┌─────────────────▼───────────────────┐ │ Infrastructure (@Repository) │ │ Repository Interface │ └─────────────────────────────────────┘ ``` **계약 규칙:** - 각 계층은 하위 계층 인터페이스에만 의존 (의존성 역전 원칙) - `Service` → `Repository` 계약: JPA `JpaRepository` 확장 또는 커스텀 인터페이스 - `Controller` → `Service` 계약: 명시적 서비스 인터페이스 정의 - DTO는 불변(immutable) 객체로 설계, `record` 또는 `final` 클래스 사용 ### 3. 오류 처리 (Error Handling) | 계층 | 처리 방식 | 구현 | |------|-----------|------| | **도메인** | 도메인 예외 (`DomainException`) | 비즈니스 규칙 위반 시 발생 | | **인프라** | 데이터 접근 예외 (`DataAccessException`) | Spring의 추상화 예외 활용 | | **애플리케이션** | 애플리케이션 예외 (`ApplicationException`) | 도메인 예외 감싸기, 로깅 | | **프레젠테이션** | `@ControllerAdvice` | 일관된 HTTP 응답 반환 | **예외 계층 구조:** ``` RuntimeException ├── DomainException (비즈니스 로직 오류) ├── ApplicationException (애플리케이션 수준 오류) │ └── ResourceNotFoundException │ └── InvalidStateException └── InfrastructureException (외부 시스템 오류) └── DataAccessException (Spring) └── ExternalServiceException ``` **@ControllerAdvice 규칙:** - `ErrorResponse` DTO 반환: `{ "code", "message", "timestamp", "path" }` - HTTP 상태 매핑: 400(Bad Request), 404(Not Found), 409(Conflict), 500(Internal) - 민감 정보 제외: 예외 스택 트레이스 클라이언트 노출 금지 - 로깅: `ERROR` 레벨, 요청 ID 포함 ### 4. 트랜잭션 (Transaction) | 시나리오 | 전파 정책 | 설명 | |----------|-----------|------| | **Service → Repository** | `REQUIRED` (기본) | 기존 트랜잭션 참여 또는 신규 생성 | | **Service → Service** | `REQUIRED` | 같은 트랜잭션 내에서 실행 | | **읽기 전용** | `readOnly=true` | SELECT 최적화 | | **다중 데이터소스** | `REQUIRES_NEW` | 독립 트랜잭션 보장 | | **비트랜잭션 작업** | `NOT_SUPPORTED` | 기존 트랜잭션 일시 중단 | **트랜잭션 경계 규칙:** - `@Transactional`은 public 메서드에만 적용 (프록시 제약) - 클래스 레벨 `@Transactional`보다 메서드 레벨 우선 - 롤백: `RuntimeException`, `Error` 자동 롤백, 체크 예외는 명시적 `rollbackFor` 필요 - 격리 수준: `DEFAULT`(데이터소스 기본값) 유지, 필요 시 명시적 지정 **트랜잭션 순서도:** ``` [요청 수신] │ ▼ [Controller] ──── 예외 ────▶ @ControllerAdvice ──▶ ErrorResponse │ ▼ [Service @Transactional] │ ├── 성공 ──▶ [Repository] ──▶ Commit │ ├── DomainException ──▶ Rollback │ └── ApplicationException ──▶ Rollback + 로깅 ``` --- ## 대안 검토 ### 대안 A: 모든 코드를 한 번에 Spring 전환 - **단점:** 리스크 높음, 점진적 검증 불가 - **선택 안 함** ### 대안 B: 경계 없이 자유 주입 - **단점:** 순환 참조, 테스트 어려움 - **선택 안 함** ### 대안 C: 체크 예외 기반 트랜잭션 - **단점:** 명시적 롤백 선언 필요, 실수 가능성 - **선택 안 함** --- ## 결과 (Consequences) ** positif:** - 명확한 계층 분리 → 유지보수성 향상 - 일관된 예외 처리 → 디버깅 용이 - 명시적 트랜잭션 경계 → 데이터 무결성 보장 - Spring 표준 패턴 적용 → 팀 역량 일관화 ** 부정적:** - 초기 학습 곡선 (ADR 숙지 필요) - 레거시 통합 시 어댑터 추가 개발 필요 **제한:** - 본 ADR은 `role-ta` 모듈 범위에 적용 - 향후 마이크로서비스 분할 시 조정 가능