공통 모듈 : 예외처리

hyezuu·2025년 3월 14일

시작하며

Spring Boot 예외 관련 공통 기능을 구현했다.
예외가 발생했을 때 일관된 응답 형식을 제공하고, 비즈니스 로직과 예외 처리를 깔끔하게 분리하는 방법을 정리해보쟈

1. 예외 처리 아키텍처 설계

먼저 예외 처리 아키텍처의 핵심 구성 요소부터 살펴보자:

  • ErrorCode: 모든 에러 코드의 인터페이스다
  • CommonErrorCode: 공통 에러 코드를 정의한 열거형이다
  • BusinessException: 비즈니스 예외를 표현하는 커스텀 예외 클래스다
  • ErrorResponse: 클라이언트에게 반환할 에러 응답 DTO다
  • GlobalExceptionHandler: 전역 예외 처리기다

이렇게 구성하면 일관된 에러 처리와 응답이 가능해진다.

2. ErrorCode 인터페이스

모든 에러 코드가 공통으로 가져야 할 속성을 정의한 인터페이스다.

public interface ErrorCode {
    String getCode();
    String getMessage();
    int getStatus();
}

이 인터페이스를 통해 다양한 도메인의 에러 코드를 일관되게 정의할 수 있다.

3. CommonErrorCode 열거형

공통으로 사용할 에러 코드를 정의한 열거형이다.

@Getter
public enum CommonErrorCode implements ErrorCode {
    INVALID_REQUEST("COM_001", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST),
    UNAUTHORIZED("COM_002", "인증되지 않은 요청입니다.", HttpStatus.UNAUTHORIZED),
    FORBIDDEN("COM_003", "접근이 금지 되었습니다.", HttpStatus.FORBIDDEN),
    NOT_FOUND("COM_004", "요청한 데이터를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
    INTERNAL_SERVER_ERROR("COM_005", "서버 에러 입니다.", HttpStatus.INTERNAL_SERVER_ERROR),
    // ... 기타 공통 에러 코드들
    
    private final String code;
    private final String message;
    private final int status;

    CommonErrorCode(String code, String message, HttpStatus status) {
        this.code = code;
        this.message = message;
        this.status = status.value();
    }
}

이렇게 하면 에러 코드, 메시지, HTTP 상태코드를 한 곳에서 관리할 수 있다.

4. BusinessException 클래스

비즈니스 로직에서 발생하는 예외를 표현하는 커스텀 예외 클래스다.

@Getter
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;

    public static BusinessException from(ErrorCode errorCode) {
        return new BusinessException(errorCode);
    }

    private BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

이 클래스는 ErrorCode를 캡슐화해서 예외를 좀 더 도메인적으로 표현할 수 있게 해준다.

5. ErrorResponse 클래스

클라이언트에게 반환할 에러 응답 형식을 정의한 DTO다.

public record ErrorResponse(String code, String message, int status, List<ValidationError> errors) {
    // 다양한 factory 메서드
    public static ErrorResponse of(ErrorCode errorCode) {
        return new ErrorResponse(
            errorCode.getCode(),
            errorCode.getMessage(),
            errorCode.getStatus(),
            Collections.emptyList()
        );
    }

    // 검증 에러를 위한 중첩 레코드
    public record ValidationError(
        String field,
        String rejectedValue,
        String reason
    ) {
        // FieldError, ConstraintViolation 변환 메서드들
    }
}

레코드를 사용해서 불변 객체로 만들었고, 다양한 타입의 예외에 대응할 수 있는 팩토리 메서드를 제공한다.

6. GlobalExceptionHandler 클래스

모든 예외를 한 곳에서 처리하는 전역 예외 처리기다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode());
        return ResponseEntity.status(errorResponse.status()).body(errorResponse);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
        MethodArgumentNotValidException e) {
        ErrorResponse errorResponse = ErrorResponse.of(e.getBindingResult());
        return ResponseEntity.status(errorResponse.status()).body(errorResponse);
    }

    // 기타 예외 처리 메서드들
}

@RestControllerAdvice를 사용해서 모든 컨트롤러에서 발생하는 예외를 일관되게 처리할 수 있다.

7. 실제 사용 예시

이제 실제 비즈니스 로직에서 어떻게 사용하는지 살펴보자.

// 도메인별 에러 코드 정의
@Getter
public enum StockErrorCode implements ErrorCode {
    INSUFFICIENT_STOCK("STK_001", "재고가 부족합니다", HttpStatus.BAD_REQUEST),
    STOCK_NOT_FOUND("STK_002", "재고 정보를 찾을 수 없습니다", HttpStatus.NOT_FOUND);
    
    // 필드 및 생성자
}

// 서비스 레이어에서 사용
@Service
public class StockService {
    public void decreaseStock(UUID productId, int quantity) {
        Stock stock = stockRepository.findById(productId)
            .orElseThrow(() -> BusinessException.from(StockErrorCode.STOCK_NOT_FOUND));
            
        if (stock.getQuantity() < quantity) {
            throw BusinessException.from(StockErrorCode.INSUFFICIENT_STOCK);
        }
        
        stock.decrease(quantity);
    }
}

8. 장점

이 방식의 주요 장점은:

  1. 일관된 예외 응답: 모든 예외가 같은 형식으로 응답된다
  2. 도메인별 에러 코드: 각 도메인마다 에러 코드를 정의할 수 있다
  3. 책임 분리: 예외 처리 로직과 비즈니스 로직이 깔끔하게 분리된다
  4. 확장성: 새로운 타입의 예외나 에러 코드를 쉽게 추가할 수 있다
  5. 검증 에러 처리: Bean Validation이나 ConstraintViolation 예외를 깔끔하게 처리한다

9. 결론

Spring Boot에서 이런 방식으로 예외 처리를 구현하면 일관성 있고 확장 가능한 에러 처리 전략을 만들 수 있다. 특히 마이크로서비스 환경에서는 여러 서비스 간에 일관된 에러 응답 형식을 유지하는 게 중요한데, 이 방식이 그런 요구사항을 잘 충족시켜준다.

비즈니스 로직에서는 단순히 적절한 에러 코드와 함께 BusinessException을 던지기만 하면 나머지는 GlobalExceptionHandler가 알아서 처리해주니까 개발자는 비즈니스 로직에만 집중할 수 있다.

profile
기록

0개의 댓글