SpringBoot

[Spring] 커스텀 에러 만들기

ssddo 2025. 1. 4. 16:59

저는 현재 회사에서 WebFlux 를 활용한 PG 도메인 프로젝트를 진행하며, 커스텀 에러를 사용하고 있습니다.

이번 글에서는 WebFlux 뿐만 아니라 MVC 환경에서도 적용 가능한 커스텀 에러 설계 및 사용 방법을 공유하고자 합니다.

 

중간에 WebFlux 문법이 등장할 수 있지만, 사용된 어노테이션은 MVC 환경에서도 적용 가능하니 참고해 주세요 ☺️


🪴 들어가기 전

커스텀 에러에 대해 소개하기 전에 이 글에서 다뤄질 에러 종류에 대해 정리하고 가겠습니다.

이 글에서 언급하는 에러는 크게 HTTP 통신 에러와 애플리케이션 단계의 에러 두 가지로 나눌 수 있습니다.

  • HTTP 통신 에러는 클라이언트가 직접적으로 받게 되는 에러로, 서버에서 응답한 HTTP 상태 코드와 메시지를 통해 전달됩니다.
  • 애플리케이션 단계의 에러는 서버 내부 로직에서 발생하는 에러로, 주로 서버에서 확인하고 처리해야 하는 에러입니다.

 

🪴 커스텀 에러

커스텀 에러는 애플리케이션 내부 로직에서 발생하는 특정 예외를 처리하기 위해 개발자가 정의한 사용자 맞춤형 예외입니다.

이를 통해 클라이언트에게 더 정확한 에러 원인을 제공할 수 있어서 에러 처리의 효율성이 높아집니다.

 

예를 들어 이미 가입된 회원에 대해 단순히 HTTP 400 코드와 "잘못된 요청" 메시지를 반환하는 대신에

아래와 같이 클라이언트에게 더 구체적인 에러 메시지를 전달할 수 있습니다

// 400 Bad Request
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다.")

 

🪴 전체적인 코드

 

1. ErrorCodes

  • 커스텀 에러 코드와 메시지를 정의하는 곳입니다.
public enum ErrorCodes {
    //400
    MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST.value(), "이미 존재하는 회원입니다."),
    MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 회원입니다. 회원가입 진행해주세요."),
    INVALID_MEMBER_LOGIN_INFO(HttpStatus.BAD_REQUEST.value(), "아이디 또는 비밀번호가 올바르지 않습니다."),
    //403
    FORBIDDEN(HttpStatus.FORBIDDEN.value(), "접근할 수 없습니다."),
    //500
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다.");
    
    private final int httpStatusCode;
    private final String message;

    ErrorCodes(int httpStatusCode, String message) {
        this.httpStatusCode = httpStatusCode;
        this.message = message;
    }

    public int httpStatusCode() {
        return httpStatusCode;
    }

    public String message() {
        return message;
    }

    @Override
    public String toString() {
        return message;
    }
}

 

2. CustomException

  • 커스텀 에러를 던질 수 있도록 RuntimeException을 확장한 예외 클래스입니다.
public class CustomException extends RuntimeException{
    private final int httpStatusCode;
    private final String body;
    private final String message;

    public CustomException(ErrorCodes errorCode){
        super(errorCode.message());
        this.httpStatusCode = errorCode.httpStatusCode();
        this.body = errorCode.name();
        this.message = errorCode.message();
    }

    public CustomException(ErrorCodes errorCode, String message){
        super(errorCode.message());
        this.httpStatusCode = errorCode.httpStatusCode();
        this.body = errorCode.name();
        this.message = message;
    }

    public CustomException(int httpStatusCode, String body, String message) {
        this.httpStatusCode = httpStatusCode;
        this.body = body;
        this.message = message;
    }

    public int httpStatusCode() {
        return httpStatusCode;
    }

    public String body() {
        return body;
    }

    public String message() {
        return message;
    }
}

 

3. GlobalExceptionHandler

  • 애플리케이션 전역에서 발생한 커스텀 예외를 처리하는 핸들러 클래스입니다.
@ControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    protected ResponseEntity<ResultData<String>> handleCustomException(CustomException e){
        return ResponseEntity.status(e.httpStatusCode())
                .body(ResultData.<String>builder()
                        .body(e.body())
                        .message(e.message())
                        .build()
                );
    }
}
  • @ControllerAdvice
    • @ControllerAdvice는 전역적으로 예외를 처리할 수 있도록 하는 어노테이션입니다. 이를 사용하면 특정 예외를 애플리케이션 전역에서 처리할 수 있으며 여러 컨트롤러에서 발생하는 예외를 하나의 클래스로 처리할 수 있습니다.
  • @ExceptionHandler
    • @ExceptionHandler는 지정된 예외가 발생했을 때 해당 예외를 처리하는 메서드를 정의하는데 사용합니다.

 

4. 사용예시

//비밀번호 재설정 예시코드
    public Mono<ResultData<ResultCodes>> resetPassword(
            PasswordResetRequestDto requestDto,
            ServerWebExchange exchange
    ) {
        return memberRepository.updateMemberPassword(requestDto.memberId(), bCryptPasswordEncoder.encode(requestDto.newPassword()), exchange)
                .switchIfEmpty(Mono.error(new CustomException(ErrorCodes.MEMBER_NOT_FOUND)))
                .map(result -> new ResultData<>(ResultCodes.SUCCESS, "비밀번호가 재설정되었습니다."));
    }

 

🪴 에러 처리 흐름

 

위의 코드에서 GlobalExceptionHandler 클래스가 존재하지 않는다면 모든 에러는 500 에러로 통신하게 됩니다.

핸들러가 존재하지 않으면 Spring 에서는 예외 발생 시, 자동으로 500 Internal Server Error를 반환하기 때문입니다.

따라서 커스텀 에러를 만들고자 할 때에는, @ControllerAdvice@ExceptionHandler 를 이용해서 예외 처리에 대한 핸들러 작업을 필수로 해주어야합니다.

 

스프링 프레임워크에서의 에러 흐름을 정리하면 아래와 같습니다.

 

  1. HTTP Client 에서 요청 시작
  2. DispatcherServlet 을 통한 요청 라우팅
  3. Controller 와 Service 처리
  4. 예외 발생 시 Global ExceptionHandler 에서 처리
  5. 커스텀 또는 기본 예외에 따른 응답 처리
  6. 최종 응답 HTTP Client 로 반환