runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-boundary-contracts-error-tx.md
forge-bot d7f96ebe4a
All checks were successful
ci / test (pull_request) Successful in 1m31s
TA 역할 Spring 경계 smoke (role-ta-live-v5-001)
2026-07-14 09:29:07 +00:00

9.5 KiB

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 패턴

도메인 포트 인터페이스:

package com.example.domain.ports.inbound;

public interface RoleManagementUseCase {
    RoleDto createRole(CreateRoleCommand command);
    RoleDto findById(Long id);
    List<RoleDto> findAll();
}
package com.example.domain.ports.outbound;

public interface RoleRepository {
    Role save(Role role);
    Optional<Role> findById(Long id);
    List<Role> findAll();
    void deleteById(Long id);
}

결정 사항:

  • 인바운드 포트: 유스케이스 인터페이스 (도메인 사용)
  • 아웃바운드 포트: 리포지토리/외부 서비스 인터페이스 (도메인 정의)
  • DTO는 application.dto 패키지에 위치, 도메인 엔티티와 분리
  • Mapper는 application.mapper 패키지에 위치

3. 오류 처리: 도메인 예외 → Spring 예외 변환

도메인 예외 계층:

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 예외 처리:

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 단위

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/헥사고날 아키텍처 이해 필요

해결 방안

  • 단계적 마이그레이션: 도메인 계층부터 순차 전환
  • 문서화: 각 패키지 책임 및 의존성 규칙 명시
  • 코드 리뷰 가이드라인: 경계 위반 체크

참고 자료