diff --git a/docs/adr/ADR-001-spring-boundary-contracts-error-tx.md b/docs/adr/ADR-001-spring-boundary-contracts-error-tx.md new file mode 100644 index 0000000..2bc5f0e --- /dev/null +++ b/docs/adr/ADR-001-spring-boundary-contracts-error-tx.md @@ -0,0 +1,233 @@ +# ADR-001: Spring 전환 경계, 계약, 오류 및 트랜잭션 결정 + +## Context + +runtime-role-matrix-live 프로젝트는 Java 기반 레거시 시스템에서 Spring Boot로의 전환을 계획하고 있다. 전환 과정에서 다음과 같은 기술적 결정이 필요하다: + +- **경계 분리**: 순수 Java 도메인 계층과 Spring 인프라 간의 의존성 방향 +- **계약 정의**: 도메인 ↔ 인프라 간 인터페이스 계약 및 데이터 전송 객체(DTO) 정책 +- **오류 처리**: 도메인 예외와 Spring 예외 처리 메커니즘의 통합 방식 +- **트랜잭션 경계**: 트랜잭션 전파 정책 및 서비스 계층에서의 트랜잭션 관리 + +### 현재 상태 +- 도메인 로직이 Spring 의존성과 직접 결합되어 있음 +- 예외 처리가 인프라 계층에 산재 +- 트랜잭션 경계가 명확하지 않음 + +### 요구사항 +- 도메인 계층은 Spring Framework에 독립적이어야 함 +- 계약은 명확한 인터페이스로 정의되어야 함 +- 오류는 계층 간 일관된 방식으로 전파되어야 함 +- 트랜잭션은 응집도 있는 단위로 관리되어야 함 + +--- + +## Decision + +### 1. 경계 분리: 도메인-인프라 분리 원칙 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Presentation Layer │ +│ (Spring MVC / WebFlux Controllers) │ +└─────────────────────────┬───────────────────────────────┘ + │ DTO +┌─────────────────────────▼───────────────────────────────┐ +│ Application Layer │ +│ (Spring @Service + @Transactional) │ +└─────────────────────────┬───────────────────────────────┘ + │ Domain Interface (Port) +┌─────────────────────────▼───────────────────────────────┐ +│ Domain Layer │ +│ (Pure Java - No Spring Dependencies) │ +│ - Entities, Value Objects, Domain Services │ +│ - Domain Exceptions │ +│ - Domain Ports (Interfaces) │ +└─────────────────────────┬───────────────────────────────┘ + │ Infrastructure Implementation +┌─────────────────────────▼───────────────────────────────┐ +│ Infrastructure Layer │ +│ (Spring Data JPA, Repository Implementations) │ +└─────────────────────────────────────────────────────────┘ +``` + +**결정 사항:** +- 도메인 계층은 `org.example.domain.*` 패키지에 위치하며 Spring 의존성 없음 +- 포트(인터페이스)는 `domain.ports` 패키지에 정의 +- 어댑터(구현체)는 `infrastructure.adapters.*` 패키지에 위치 + +### 2. 계약 정의: Ports and Adapters 패턴 + +**도메인 포트 인터페이스:** +```java +package com.example.domain.ports.inbound; + +public interface RoleManagementUseCase { + RoleDto createRole(CreateRoleCommand command); + RoleDto findById(Long id); + List findAll(); +} +``` + +```java +package com.example.domain.ports.outbound; + +public interface RoleRepository { + Role save(Role role); + Optional findById(Long id); + List findAll(); + void deleteById(Long id); +} +``` + +**결정 사항:** +- 인바운드 포트: 유스케이스 인터페이스 (도메인 사용) +- 아웃바운드 포트: 리포지토리/외부 서비스 인터페이스 (도메인 정의) +- DTO는 `application.dto` 패키지에 위치, 도메인 엔티티와 분리 +- Mapper는 `application.mapper` 패키지에 위치 + +### 3. 오류 처리: 도메인 예외 → Spring 예외 변환 + +**도메인 예외 계층:** +```java +package com.example.domain.exceptions; + +public abstract class DomainException extends RuntimeException { + private final ErrorCode errorCode; + + public DomainException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { return errorCode; } +} + +public class RoleNotFoundException extends DomainException { + public RoleNotFoundException(Long id) { + super(ErrorCode.ROLE_NOT_FOUND, "Role not found: " + id); + } +} + +public class DuplicateRoleException extends DomainException { + public DuplicateRoleException(String roleName) { + super(ErrorCode.DUPLICATE_ROLE, "Duplicate role: " + roleName); + } +} +``` + +**Spring 예외 처리:** +```java +package com.example.application.exception; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(RoleNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleRoleNotFound(RoleNotFoundException ex) { + return new ErrorResponse(ex.getErrorCode(), ex.getMessage()); + } + + @ExceptionHandler(DuplicateRoleException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ErrorResponse handleDuplicateRole(DuplicateRoleException ex) { + return new ErrorResponse(ex.getErrorCode(), ex.getMessage()); + } + + @ExceptionHandler(DomainException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleDomainException(DomainException ex) { + return new ErrorResponse(ex.getErrorCode(), ex.getMessage()); + } +} +``` + +**결정 사항:** +- 도메인 예외는 `ErrorCode` enum으로 코드 체계化管理 +- `@ControllerAdvice`에서 도메인 예외를 HTTP 상태码로 변환 +- 인프라 예외(SQLException 등)는 도메인 예외로 래핑 + +### 4. 트랜잭션 경계: Application Service 단위 + +```java +package com.example.application.service; + +@Service +@Transactional(readOnly = true) +public class RoleManagementService implements RoleManagementUseCase { + + private final RoleRepository roleRepository; + private final EventPublisher eventPublisher; + + @Transactional + public RoleDto createRole(CreateRoleCommand command) { + // 도메인 로직 호출 + Role role = Role.create(command.name(), command.description()); + Role saved = roleRepository.save(role); + eventPublisher.publish(new RoleCreatedEvent(saved)); + return toDto(saved); + } + + @Transactional(readOnly = true) + public RoleDto findById(Long id) { + return roleRepository.findById(id) + .map(this::toDto) + .orElseThrow(() -> new RoleNotFoundException(id)); + } +} +``` + +**결정 사항:** +- 트랜잭션 경계는 Application Service 레벨 +- `@Transactional`은 메서드 단위로 명시적 지정 +- 읽기 전용 쿼리는 `readOnly = true` 사용 +- 도메인 서비스는 트랜잭션 어노테이션 없음 (Application Service가 관리) +- 트랜잭션 전파: `REQUIRED` (기본값) 사용 + +--- + +## Alternatives + +### 대안 1: 도메인 계층에 Spring Data JPA 직접 사용 +- **장점**: 단순한 설정, 빠른 개발 +- **단점**: 도메인이 인프라에 강결합, 테스트 어려움 +- **채택 안 함**: 전환 목표에 부합하지 않음 + +### 대안 2: Checked Exception 기반 오류 처리 +- **장점**: 명시적인 예외 선언 +- **단점**: 호출자 코드 복잡성 증가, 트랜잭션 롤백과 통합 어려움 +- **채택 안 함**: Spring 기본 런타임 예외 전략 채택 + +### 대안 3: 도메인 주도 설계(DDD) 애그리거트 단위 트랜잭션 +- **장점**: 일관성 경계 명확 +- **단점**: 높은 학습 곡선, 초기 개발 속도 저하 +- **미래 고려 사항**: 복잡도 증가 시 마이그레이션 가능 + +--- + +## Consequences + +### 긍정적 결과 +- **테스트 용이성**: 도메인 계층은 순수 Java로 단위 테스트 가능, Spring 의존성 없음 +- **유지보수성**: 경계가 명확하여 변경 영향 범위 파악 용이 +- **확장성**: 포트/어댑터 패턴으로 인프라 교체 용이 (예: JPA → MongoDB) +- **일관된 오류 처리**: 전 계층에서统一的 예외 처리 + +### 부정적 결과 +- **초기 개발 시간**: 기존 코드 대비 포트/어댑터 패턴 도입으로 초기 개발 시간 증가 +- **복잡도 증가**: 다중 계층으로 인한 파일 수 증가 +- **학습 곡선**: 팀원의 DDD/헥사고날 아키텍처 이해 필요 + +### 해결 방안 +- 단계적 마이그레이션: 도메인 계층부터 순차 전환 +- 문서화: 각 패키지 책임 및 의존성 규칙 명시 +- 코드 리뷰 가이드라인: 경계 위반 체크 + +--- + +## 참고 자료 + +- [Ports and Adapters Architecture](https://alistair.cockburn.us/hexagonal-architecture/) +- [Spring Boot Transaction Management](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction) +- [ErrorCode Enum Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/_index)