ADR-001: Spring 경계 아키텍처 정의
Context
runtime-role-matrix-live 프로젝트는 역할(Role) 기반 접근 제어 시스템을 구현한다. 현재 계층화 아키텍처의 명확한 경계가 정의되어 있지 않아 다음과 같은 문제가 발생한다.
- 응집도 부족: Controller에서 비즈니스 로직 직접 수행
- 결합도 증가: Service 간 직접 의존으로 단위 테스트 어려움
- 트랜잭션 범위 모호: Repository 호출 시 트랜잭션 전파 정책 불명확
- 오류 처리 불일치: 각 계층별 예외 처리 방식 상이
현재 시스템 범위
| 계층 |
책임 |
| Controller |
HTTP 요청/응답 변환, 입력 검증, 라우팅 |
| Service |
비즈니스 로직, 트랜잭션 경계, 도메인 조율 |
| Repository |
데이터 접근 추상화, 쿼리 실행 |
Decision
1. Controller-Service-Repository 경계 정의
┌─────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ - HTTP 요청 파라미터 바인딩 및 검증 (@Valid) │
│ - HTTP 응답 변환 (DTO → ResponseEntity) │
│ - 예외 → HTTP 상태码 매핑 │
│ - 트랜잭션 경계에 참여하지 않음 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Layer │
│ - @Transactional 메서드 단위 트랜잭션 경계 │
│ - 비즈니스 규칙 및 도메인 로직 실행 │
│ - 다중 Repository 조율 │
│ - 도메인 객체 생성 및 상태 관리 │
│ -Checked Exception → Unchecked Exception 변환 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Repository Layer │
│ - JPA Repository (JpaRepository<T, ID>) 상속 │
│ - @Query 기반 커스텀 쿼리 │
│ - 도메인 엔티티 직접 반환 │
│ - 트랜잭션 읽기 전용 (readOnly=true) 활용 │
└─────────────────────────────────────────────────────────────┘
2. 오류 계약 (Error Contract)
예외 계층 구조
RuntimeException (java.lang)
│
├── RoleNotFoundException → HTTP 404
├── RoleAlreadyExistsException → HTTP 409
├── InvalidRoleStateException → HTTP 400
└── PermissionDeniedException → HTTP 403
오류 응답 형식
{
"timestamp": "2026-07-14T11:23:01Z",
"status": 404,
"error": "Not Found",
"code": "ROLE_NOT_FOUND",
"message": "Role with id '123' does not exist",
"path": "/api/v1/roles/123"
}
전역 예외 처리 규칙
| 예외 유형 |
HTTP 상태 |
응답 코드 |
| RoleNotFoundException |
404 |
ROLE_NOT_FOUND |
| RoleAlreadyExistsException |
409 |
ROLE_ALREADY_EXISTS |
| InvalidRoleStateException |
400 |
INVALID_ROLE_STATE |
| PermissionDeniedException |
403 |
PERMISSION_DENIED |
| MethodArgumentNotValidException |
400 |
VALIDATION_ERROR |
| 기타 RuntimeException |
500 |
INTERNAL_ERROR |
3. 트랜잭션 경계 정책
기본 원칙
| 작업 유형 |
전파 정책 |
readOnly |
| 조회 (Read) |
REQUIRED |
true |
| 생성 (Create) |
REQUIRED |
false |
| 수정 (Update) |
REQUIRED |
false |
| 삭제 (Delete) |
REQUIRED |
false |
Service 클래스 설계
@Service
@Transactional(readOnly = true)
public class RoleService {
@Transactional(readOnly = false)
public Role createRole(CreateRoleRequest request) {
// 비즈니스 로직
}
@Transactional(readOnly = false)
public Role updateRole(Long id, UpdateRoleRequest request) {
// 비즈니스 로직
}
public Role findById(Long id) {
// readOnly=true 상속
}
}
격리 수준
- 기본값: READ_COMMITTED
- 필요 시: @Transactional(isolation = Isolation.SERIALIZABLE)
Alternatives
대안 1: Transactional死在 Controller
@RestController
@Transactional
public class RoleController { ... }
| 항목 |
결함 |
| 문제점 |
HTTP 요청/응답 스레드와 트랜잭션 결합 |
| 결과 |
롤백 시 응답 불가 상태 발생 가능 |
대안 2: Service 계층 생략 (Transaction Script)
@RestController
public class RoleController {
@Autowired RoleRepository repository;
public Role create(...) { ... }
}
| 항목 |
결함 |
| 문제점 |
복잡한 도메인 로직 축적 시 재사용 어려움 |
| 결과 |
Controller 비대화, 테스트 어려움 |
대안 3: Checked Exception 직접 전파
| 항목 |
결함 |
| 문제점 |
호출자에게 예외 처리 강제, 결합도 증가 |
| 결과 |
Service 교체 시 Caller 코드 수정 필요 |
Consequences
긍정적 결과
- 단위 테스트 용이성: Service를 순수 Java로 테스트 가능
- 일관된 오류 처리: 전역 @ControllerAdvice로 중앙화
- 트랜잭션 명확성: 메서드 단위 경계로 디버깅 용이
- 유지보수성: 계층별 책임 분리
부정적 결과
- 추가 코드 작성: DTO, Exception, Mapper 클래스 증가
- 학습 곡선: 개발자별 아키텍처 이해 필요
- 성능 오버헤드: Proxy 기반 AOP 약간의 지연 (미미)
모니터링 필요 항목
- 트랜잭션 롤백 빈도
- 예외 발생 패턴 (ROLE_NOT_FOUND 등)
- Service 메서드 응답 시간
참고
- Java: 17+
- Spring Boot: 3.2.x
- JPA: Hibernate 6.x
- 빌드 도구: Maven