runtime-role-matrix-live-20.../docs/adr/ADR-001-spring-architecture-boundaries.md

5.3 KiB

ADR-001: Spring Architecture Boundaries

Status

Accepted

Context

프로젝트 배경

runtime-role-matrix-live-202607141522 프로젝트는 역할 기반 접근 제어(RBAC) 매트릭스를 실시간으로 관리하는 시스템이다. Spring Boot 기반으로 구축되며, 다중 클라이언트 환경에서 일관된 아키텍처 패턴이 필요하다.

문제 정의

  1. Controller-Service-Repository 경계 모호: 각 계층의 책임이 명확하지 않아 코드의 응집도 감소 및 결합도 증가
  2. 오류 계약 부재: 예외 처리 전략이 통일되지 않아 일관되지 않은 API 응답 생성
  3. 트랜잭션 경계 불명확: 서비스 계층에서 트랜잭션 관리 방식이 표준화되지 않아 데이터 일관성 위험

기술 환경

  • Spring Boot 3.2.x / Java 17 / Jakarta EE 10
  • Spring Data JPA / Spring Web (REST API)

Decision

1. Controller-Service-Repository 경계 정의

계층 책임 금지 사항
Controller HTTP 요청/응답, 입력 검증(@Valid), ResponseEntity 반환 비즈니스 로직, DB 접근, @Transactional
Service 비즈니스 로직, @Transactional 관리, 도메인 객체 조작 HttpServletRequest/Response 접근, 응답 형식 직접 생성
Repository DB 접근, 쿼리 실행, Entity 관리 비즈니스 로직, Service 호출
// Controller 예시
@RestController @RequiredArgsConstructor
public class RoleMatrixController {
    private final RoleMatrixService service;
    @PostMapping @Valid @RequestBody RoleCreateRequest req
    public ResponseEntity<ApiResponse<RoleResponse>> createRole(req) {
        return ResponseEntity.status(CREATED).body(ApiResponse.success(service.createRole(req)));
    }
}

// Service 예시
@Service @RequiredArgsConstructor @Transactional(readOnly = true)
public class RoleMatrixService {
    private final RoleRepository roleRepository;
    @Transactional public RoleResponse createRole(RoleCreateRequest req) {
        Role role = Role.create(req.getName(), req.getDescription());
        return RoleResponse.from(roleRepository.save(role));
    }
}

// Repository 예시
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    Optional<Role> findByName(String name);
    @Query("SELECT r FROM Role r LEFT JOIN FETCH r.permissions WHERE r.id = :id")
    Optional<Role> findByIdWithPermissions(@Param("id") Long id);
}

2. 오류 계약 정의

예외 계층: BaseExceptionBusinessException / SystemException

오류 응답 표준 형식:

{"success": false, "error": {"code": "ROLE_NOT_FOUND", "message": "요청한 역할을 찾을 수 없습니다."}, "timestamp": "..."}
예외 유형 HTTP 상태 용도
BusinessException 4xx 클라이언트 오류 (not found, duplicate, denied)
SystemException 5xx 서버 오류 (database, external service)
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException ex) {
        return ResponseEntity.status(ex.getErrorCode().getHttpStatus())
                .body(ApiResponse.error(ex.getErrorCode()));
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
        return ResponseEntity.status(INTERNAL_SERVER_ERROR)
                .body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR));
    }
}

3. 트랜잭션 경계 정의

상황 설정 설명
기본 읽기 @Transactional(readOnly = true) 성능 최적화, Dirty Checking 비활성화
쓰기 작업 @Transactional 변경 감지 활성화
전파 정책 REQUIRED (기본) 기존 트랜잭션 참여 또는 신규 생성
격리 수준 READ_COMMITTED 기본값
롤백 조건 unchecked exception RuntimeException 자동 롤백

규칙: 트랜잭션 시작/종료점은 Service Layer public 메서드. 다중 Repository 호출 시同一 트랜잭션에서 실행.


Alternatives

대안 장점 단점 미선택 이유
Transaction Script 단순, 직관적 복잡도 증가 시 유지보수 어려움 확장성 부족
DDD 패턴 복잡 도메인 캡슐화 학습 곡선 높음, 과도한 추상화 현재 규모에서 과도한 복잡성
Checked Exception 컴파일 타임 강제 코드 복잡도 증가, 트랜잭션 충돌 Spring 예외 처리와 불일치
Repository DTO 반환 즉시 변환 가능 계층 결합, 테스트 어려움 책임 분리 위반

Consequences

긍정적 결과

  • 명확한 책임 분리: 계층별 집중으로 가독성 향상
  • 일관된 오류 처리: 표준화된 예외/응답으로 API 일관성 확보
  • 테스트 용이성: 계층별 Mock 가능
  • 트랜잭션 안전성: 명확한 경계로 데이터 일관성 보장

부정적 결과

  • 초기 설정 비용: 예외 계층, DTO 등 부가 코드 증가
  • 추가 추상화: 간단 CRUD도 Service 경유 필요

재검토 조건

  • 도메인 복잡도大幅 증가 시 DDD 패턴 도입 검토
  • 성능 병목 발생 시 쿼리 최적화