본문으로 건너뛰기
Advertisement

7.3 커스텀 비즈니스 예외 클래스 설계

어플리케이션을 개발하다보면 자바 계열의 표준 예외(IllegalArgumentException, NullPointerException)만으로는 애플리케이션의 특정 비즈니스 정책 위반 상황을 표현하기가 애매할 때가 많습니다.

“사용자 계정을 찾을 수 없다”, “상품 재고가 부족하다”, “이미 삭제된 게시글이다” 같은 상황을 명확하게 표현하고 처리하기 위해 Custom Business Exception 을 만듭니다.

커스텀 예외 클래스 만들기

스프링 백엔드에서 예외를 만들 때 권장되는 기본 원칙은 RuntimeException (Unchecked Exception)을 상속받는 것입니다. 컴파일러가 강제하는 Exception 클래스는 서비스/컨트롤러 로직 전체에 걸쳐 지저분한 throws 선언을 요구하며, 트랜잭션 롤백 기본 설정에서도 문제를 일으킬 수 있기 때문입니다.

public class UserNotFoundException extends RuntimeException {

private final String email;

public UserNotFoundException(String email) {
super("해당 이메일로 가입된 사용자를 찾을 수 없습니다: " + email);
this.email = email;
}

public String getEmail() {
return email;
}
}

만약 프로젝트 내의 수많은 비즈니스 예외들을 한 묶음으로 묶어 처리하고 싶다면 최상위 커스텀 예외 클래스를 두는 구조도 설계할 수 있습니다.

// 모든 비즈니스 예외의 부모 클래스
public abstract class BusinessException extends RuntimeException {
private final ErrorCode errorCode;

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

public ErrorCode getErrorCode() {
return errorCode;
}
}

Enum을 활용한 ErrorCode 정의

에러 코드(400, 404)와 일관된 애플리케이션 에러 코드(USER_001), 응답 메시지를 하나로 묶어 관리하기 위해 Error Code Enum을 자주 사용합니다.

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum ErrorCode {

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U-001", "사용자를 찾을 수 없습니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "U-002", "이미 존재하는 이메일입니다."),
OUT_OF_STOCK(HttpStatus.BAD_REQUEST, "I-001", "상품 재고가 부족합니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

ErrorCode(HttpStatus httpStatus, String code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}

ControllerAdvice 연동

이제 우리가 만든 비즈니스 예외 계층을 @RestControllerAdvice에서 한 번에 낚아채어 클라이언트에게 예쁘게 반환할 수 있게 됩니다.

@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
ErrorCode errorCode = e.getErrorCode();
ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage());
return ResponseEntity.status(errorCode.getHttpStatus()).body(response);
}

이 접근 방식은 오류를 유형화하고, 협업하는 프론트엔드 팀에게 명확하고 일관된 형태의 오류 응답 규격을 제공할 수 있어서 실무에서 매우 사랑받는 패턴입니다.

추천되는 예외 계층 구조

실무에서는 다음과 같이 예외를 계층화하여 관리하는 것이 유지보수에 유리합니다.

  1. BusinessException(최상위 추상 클래스)
    • EntityNotFoundException(데이터 없음 계열)
      • UserNotFoundException
      • PostNotFoundException
    • InvalidValueException(잘못된 값 계열)
      • DuplicateEmailException
    • AccessDeniedException(권한 없음 계열)

이렇게 계층화하면 @RestControllerAdvice에서 부모 타입인 BusinessException 하나만 핸들링해도 하위 모든 커스텀 예외를 공통된 로직으로 처리할 수 있습니다.

🎯 핵심 요점

  • 커스텀 예외는 비즈니스 의도 를 명확히 전달하기 위해 사용합니다.
  • RuntimeException 을 상속받아 불필요한 예외 전파 선언을 방지하세요.
  • ErrorCode Enum 과 조합하면 에러 관리가 매우 체계적으로 변합니다.
Advertisement