runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-layered-architecture-boundaries.md
2026-07-14 10:34:29 +00:00

6.1 KiB

ADR-001: Spring 계층형 아키텍처 경계 정의

Context

본 프로젝트(runtime-role-matrix-live)는 Spring Boot 기반 마이크로서비스로, 역할 기반 접근 제어(RBAC) 기능을 제공한다. 현재 계층 간 책임 분담이 명확하지 않아 다음 문제가 발생한다.

  • Controller: 요청 검증과 응답 형식화만 담당해야 하지만, 비즈니스 로직이 직접 포함됨
  • Service: 트랜잭션 경계가 불명확하여 데이터 일관성 문제 발생 가능
  • Repository: 도메인 로직과 데이터 접근 로직이 혼재됨
  • 오류 처리: 각 계층에서 중구난방式的 예외 처리, 일관된 오류 계약 부재

기술 스택

  • Java 17+
  • Spring Boot 3.x
  • Spring Data JPA
  • Spring Web (REST API)

Decision

1. Controller-Service-Repository 경계 정의

계층 책임 포함 사항 미포함 사항
Controller HTTP 요청/응답 변환, 입력 검증, 라우팅 @RestController, @RequestMapping, @Valid, DTO 변환, HTTP 상태 코드 결정 비즈니스 로직, DB 접근, 트랜잭션 관리
Service 비즈니스 로직, 트랜잭션 경계, 도메인 조율 @Service, @Transactional, 도메인 객체 조작, 다중 Repository 호출, 오류 계약 정의 HTTP 프로토콜 이해, 직접 HTTP 응답
Repository 데이터 접근 추상화, 쿼리 실행 @Repository, @JpaRepository, 커스텀 쿼리, 엔티티 매핑 비즈니스 로직, 서비스 호출

2. 오류 계약 (Error Contract)

예외 계층 구조

BaseException (추상)
├── BusinessException      → 사용자에게 의미 있는 오류 (400 Bad Request)
│   ├── RoleNotFoundException
│   ├── DuplicateRoleException
│   └── PermissionDeniedException
└── SystemException        → 시스템 내부 오류 (500 Internal Server Error)
    ├── DatabaseException
    └── ExternalServiceException

오류 응답 형식 (RFC 7807 Problem Details)

{
  "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:30:00Z",
  "traceId": "abc123"
}

계층별 오류 처리 규칙

계층 예외 발생 시 처리 방식
Repository DB 오류 발생 DataAccessException 래핑하여 Service에 전달
Service 비즈니스 규칙 위반 BusinessException 발생, 트랜잭션 롤백
Controller Service 예외 포착 @ControllerAdvice에서 ProblemDetail 응답 생성

3. 트랜잭션 경계

트랜잭션 전파 정책

시나리오 전파 방식 설명
Service → Repository REQUIRED (기본값) 기존 트랜잭션 참여 또는 새 트랜잭션 생성
읽기 전용 연산 readOnly = true 성능 최적화, Hibernate flush mode AUTO
다중 데이터 소스 REQUIRES_NEW 독립 트랜잭션 필요 시

트랜잭션 경계 설정 규칙

// Service 계층에서 트랜잭션 시작
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public RoleResponse createRole(CreateRoleRequest request) {
    // 트랜잭션 경계 내: 모든 DB 연산 포함
    validateRoleName(request.getName());
    Role role = roleRepository.save(toEntity(request));
    permissionRepository.saveAll(toPermissions(role, request.getPermissions()));
    return toResponse(role);
}

// 읽기 전용 트랜잭션
@Transactional(readOnly = true)
public RoleResponse getRole(Long id) {
    return roleRepository.findById(id)
        .map(this::toResponse)
        .orElseThrow(() -> new RoleNotFoundException(id));
}

트랜잭션 격리 수준

격리 수준 사용 시나리오 주의사항
READ_COMMITTED 기본값, 대부분의 경우 Dirty Read 방지
REPEATABLE_READ 동일 트랜잭션 내 일관성 필요 시 성능 저하 고려
SERIALIZABLE 극단적 일관성 필요 시 동시성 심각히 저하, 피해야 함

Alternatives

대안 1: Controller에서 직접 Service 호출, Service에서 직접 예외 변환

장점:

  • 단순한 구조, 소규모 프로젝트에 적합

단점:

  • Service가 HTTP 상태 코드에 종속됨 (관심사 분리 위반)
  • 오류 처리 로직 중복 가능성 높음
  • 테스트 어려움

대안 2: 모든 예외를 RuntimeException으로 통일

장점:

  • 예외 타입 단순화

단점:

  • 오류 유형 구분 불가, 적절한 HTTP 상태 코드 매핑 어려움
  • 클라이언트에게 의미 있는 오류 정보 제공 불가

대안 3: CQRS 패턴 적용

장점:

  • 읽기/쓰기 분리による 성능 최적화
  • 복잡한 도메인에 적합

단점:

  • 초기 구축 비용 높음
  • 본 프로젝트 규모에는 과도한 설계

Consequences

긍정적 결과

  • 단일 책임 원칙 준수: 각 계층이 명확한 책임만 담당
  • 테스트 용이성: Mock을 통한 단위 테스트 간결화
  • 일관된 오류 처리: API 소비자에게 예측 가능한 오류 응답
  • 트랜잭션 보장: 데이터 일관성 확보, 롤백 규칙 명확
  • 유지보수성 향상: 변경 영향 범위 제한적

부정적 결과

  • 추가 코드 작성: DTO, Mapper, Exception 클래스 증가
  • 학습 곡선: 팀원들의 계층 경계 규칙 숙지 필요
  • 성능 오버헤드: 트랜잭션 관리, AOP 프록시 생성 비용 (미미)

모니터링 필요 사항

  • 트랜잭션 롤백 빈도
  • BusinessException 발생 패턴
  • API 응답 시간 (Controller → Service 경계)

참고 자료