BindProject

[Backend] exception(2) ( 예외 계층화 구현)

dding-shark 2025. 6. 17. 11:17
728x90

예외 계층화 설계 및 처리 전략

Bind 프로젝트에서는 각 도메인 서비스가 독립적이면서도 일관된 방식으로 예외를 처리할 수 있도록 예외 모듈을 설계했습니다.
기존의 단일 예외 구조에서 확장 가능한 계층형 예외 구조로 리팩터링한 과정을 정리합니다.


1. 도입 및 배경

초기 구조에서는 BusinessException 하나로 모든 예외를 처리했으나 다음과 같은 문제가 있었습니다:

  • 비즈니스 로직, 입력 검증, 외부 시스템 오류를 구분할 수 없음
  • 에러 응답의 상태 코드와 메시지 일관성이 떨어짐
  • 예외별 로깅 전략이나 알림 방식 적용이 어려움

이를 해결하기 위해 예외의 성격에 따라 계층형 예외 구조를 설계하였습니다.


2. 설계 목표

  • 예외를 비즈니스, 입력 검증, 외부 시스템, 인증 등으로 계층화
  • BindErrorCode 기반의 통합 인터페이스를 유지
  • 전역 핸들러에서 예외 타입별로 처리 방식 분기
  • 로그, 알림, 응답 메시지를 예외 성격에 맞게 다르게 처리

3. 설계 고려사항

  • Spring 의존성을 퍼블릭 모듈에 최소화할 것
  • RestControllerAdvice로 API 응답 예외를 공통 처리하되, 각 계층을 분리할 것
  • Kafka, Redis 등 비동기 시스템의 예외도 동일하게 포용할 것
  • 도메인 서비스마다 자체적인 에러 코드를 정의할 수 있게 확장할 것

4. 계층 구조

exception/
├── BindBaseException.java        # 최상위 추상 예외
├── BusinessException.java        # 비즈니스 예외
├── InvalidRequestException.java  # 유효성 검증 실패
├── ExternalServiceException.java # 외부 시스템 연동 실패
├── AuthException.java            # 인증/인가 실패
├── GlobalExceptionHandler.java   # 전역 핸들러 (타입별 분기)

🔧 5. 핵심 구현

// 공통 추상 예외
public abstract class BindBaseException extends RuntimeException {
    private final BindErrorCode errorCode;
    ...
}

// 도메인별 예외들
public class BusinessException extends BindBaseException { ... }
public class InvalidRequestException extends BindBaseException { ... }
public class ExternalServiceException extends BindBaseException { ... }
public class AuthException extends BindBaseException { ... }
// 전역 핸들러 분기
@ExceptionHandler(BusinessException.class)
public ResponseEntity<?> handleBusiness(BusinessException ex) { ... }

@ExceptionHandler(ExternalServiceException.class)
public ResponseEntity<?> handleExternal(ExternalServiceException ex) {
    log.error("External failure: {}", ex.getErrorCode().getMessage());
    return toResponse(GlobalErrorCode.INTERNAL_ERROR);
}

6. 테스트 전략

  • MockMvc 기반 통합 테스트
  • 각 예외 타입별로 엔드포인트 생성
  • 상태 코드, 메시지, 로깅 여부 검증
@Test
void handleBusinessException() throws Exception {
    mockMvc.perform(get("/exception/business"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.code").value("USER-001"));
}

퍼블릭 모듈에서는 @SpringBootTest(classes = ...)로 테스트 대상 명시 필요


7. 회고 및 확장 가능성

  • 예외 타입별로 응답 처리 방식, 상태 코드, 로깅 정책을 세분화할 수 있어 유연함
  • Kafka, Slack 연동 등 외부 시스템 예외 알림으로 확장하기 용이함
  • 도메인별 에러 코드 enum (UserErrorCode, OrderErrorCode 등) 정의를 통해 분산된 팀도 협업 가능
  • 공통 인터페이스(BindErrorCode)를 통해 핸들러 재사용성 확보

마무리

이번 구조 리팩터링을 통해 예외 처리를 단순히 에러 응답 처리에서
설계적 책임 분리, 확장성 확보, 운영 편의성까지 고려할 수 있게 되었습니다.
다음 글에서는 event, outbox, logging 모듈 설계로 이어질 예정입니다.

728x90