스프링부트에서 예외처리하기

코코코딩을 합시다·2024년 4월 11일

배포도 하지 않는 소규모 프로젝트에선 예외처리를 꼼꼼하게 할 필요가 없었다.
그냥 테스트 하면서 뜨는 오류 정의하려고 오류 메시지 날리는게 전부였다.
이번 프로젝트는 로직이 복잡하지 않은만큼 예외처리도 꼼꼼하게 해보려 한다.
따라서 이번 포스팅에선 훗날 두고두고 써먹기 위해 스프링부트에서 예외처리 하는 방법을 정리해보려한다.

우선 스프링부트에선 디폴트 예외처리 컨트롤러인 BasicErrorController 가 존재한다.
따라서 예외처리를 하지 않으면 스프링이 기본적으로 /error로 에러 요청을 다시 전달하도록 WAS 설정해두었기 때문에 BasicErrorController로 에러 처리 요청이 전달된다.
에러 발생시 요청 흐름은 다음과 같다.

WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
-> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(BasicErrorController)

결론적으로 기본적인 예외처리는 컨트롤러와 필터, 인터셉터가 두 번씩 호출되며 응답코드 500만 날리기 때문에 클라이언트가 무슨 오류인지 알 방법이 없다.

현재 가장 많이 쓰이는 예외처리 방법은 @ExceptionHandler@RestControllerAdvice 이다.

@ExceptionHandler

Java에서 예외처리는 try-catch문을 활용하지만 이를 일일히 작성하는 것은 매우 번거로운 일이다. 스프링에선 ExceptionHandler으로 예외처리를 유연하고 간단하게 할 수 있도록 한다. @ExceptionHandler는 Exception 클래스를 속성으로 받아 처리할 예외를 지정할 수 있다. 이때 기본 Http Status 외에도 에러코드 enum을 만들어 직접 에러코드를 커스텀할 수도 있다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorCode errorCode = ErrorCode.INTER_SERVER_ERROR; // 예시로 사용
        ErrorResponse errorResponse = new ErrorResponse(errorCode);
        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorCode.getStatus()));
    }
}

ExceptionHandler을 사용할 때 주의점은 @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일해야 한다는 것이다.
또 ExceptionHandler은 컨트롤러에서 구현되므로 특정 컨트롤러에서 발생하는 예외만을 다룬다.
오류를 전역적으로 처리하기 위해 스프링은 ControllerAdvice를 제공한다.

@RestControllerAdvice

우선 ControllerAdvice와 RestControllerAdvice의 차이점은 Controller와 RestController처럼 ResponseBody의 유무 차이이다. ControllerAdvice는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다.
따라서 ControllerAdvice를 붙이면 해당 클래스는 에러를 전역으로 처리하게 된다.
혹시 전역이 아닌 패키지 단위 등으로 범위를 제한하고 싶다면 basePackages 등을 설정해 조정할 수 있다.


스프링부트에서 예외처리하기

간단한 설명을 마쳤으니 본격적으로 예외처리 로직을 작성해보겠다.
우선 예외처리에서 작성해야 할 클래스는 다음과 같다.

  • ErrorCode enum 정의
    • 발생할 수 있는 모든 에러를 status, code, message를 포함한 enum으로 정리해놓은 클래스이다.
  • ErrorResponse 클래스 정의
    • ErrorCode 도메인을 받아와 DTO를 생성하는 클래스이다.
  • GlobalExceptionHandler 정의
    • 모든 예외를 한 곳에서 처리하는 예외처리컨트롤러이다. ControllerAdvice와 ExceptionHandler로 구현한다.

ErrorCode 정의

    // project
    MEMBER_NOT_IN_PROJECT(40603,"P005","해당 프로젝트에 소속되지 않은 멤버입니다."),
    NON_PROJECT_MEMBER_ERROR(40100,"P006","탈퇴된 멤버입니다."),
    // record
    DELETED_RECORD_CODE(40402,"R001","이미 삭제된 회의록입니다."),
    // tag
    DELETED_TAG_CODE(40403,"T001","이미 삭제된 태그입니다."),
    // memberRecord
    DELETED_MR_CODE(40404,"MR001","이미 삭제된 MemberRecord 매핑입니다."),
    // recordTag
    DELETED_RT_CODE(40405,"RT001","이미 삭제된 RecordTag 매핑입니다.");

ErrorResponse 정의

public class ErrorResponse {
    private String message;
    private String code;
    private int statusCode;
    private String detail;

    public ErrorResponse(ErrorCode code) {
        this.message = code.getMessage();
        this.statusCode = code.getStatus();
        this.code = code.getCode();
        this.detail = code.getDetail();
    }

    public static ErrorResponse of(ErrorCode code) {
        return new ErrorResponse(code);
    }
}

Errorcode 필드를 받아와 ErrorResponse 객체를 만들고 예외 발생시 리턴해줄거당

GlobalExceptionHandler 정의

@RestControllerAdvice //컨트롤러에서 발생하는 예외처리를 해당 클래스안에서 해결
public class ErrorControllerAdvice {
    @ExceptionHandler(value = NoSuchElementException.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorResponse response = ErrorResponse.of(ErrorCode.RESOURCE_NOT_FOUND);
        response.setDetail(e.getMessage());
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(value = CustomException.class)
    protected ResponseEntity<ErrorResponse> customException(CustomException e) {
        ErrorResponse response = ErrorResponse.of(e.getErrorCode());
        response.setDetail(e.getMessage());
        return new ResponseEntity<>(response, e.getHttpStatus());
    }
}

Exception을 받아와 ResponsEntity로 변환해서 리턴해줄거당


이러면 예외 발생시 에러코드가 아닌 예외 객체가 반환돼 어떤 예외인지 정확하게 알 수 있다 !!

profile
좋아하는 걸로 밥 벌어먹기

0개의 댓글