글로벌 예외 처리(@ExceptionHandler)

대영·2024년 7월 18일
2

Spring

목록 보기
15/16

🙏내용에 대한 피드백은 언제나 환영입니다!!🙏

우선 내가 @ExceptionHandler를 통해서 글로벌 예외처리를 한 이유는 아래와 같다.

  • 일관적이지 않은 예외처리 문구
  • 직접 작성의 복잡함
  • 반환되는 예외에 대한 key, value값을 명시화
  • 상황에 맞는 상태코드 작성

이제 @ExceptionHandler에 대해서 알아보겠다.

❓ @ExceptionHandler

@ExceptionHandler는 스프링 프레임워크에서 사용되는 예외 처리 어노테이션이다. 컨트롤러 클래스 내에서 특정 예외가 발생했을 때 해당 예외를 처리할 메서드를 지정하는 데 사용된다. 그래서, 예외가 발생하면 사용자가 정의한 로직을 실행하거나 적절한 응답을 반환할 수 있습니다.

('컨트롤러 클래스 내에서 특정 예외가 발생했을 때' => 컨트롤러에 작성되어 있는 메서드에서 호출하는 다른 클래스의 메서드에 대해서도 @ExceptionHandler가 적용된다.
why? 컨트롤러 메서드에서 호출되기에 다른 클래스에서 예외가 발생하면 그것이 컨트롤러까지 전파되기 때문이다.)

📌 구현 방법

아래는 내가 작성한 글로벌 예외 처리 과정을 작성하겠다.

👉 1. ErrorCode.java 작성

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

    // 인증이 되어 있지 않을 때.
    UNAUTHORIZED(401, "접근 권한이 없습니다."),

	// 존재하지 않는 값을 보낼 때.
    USER_NOT_FOUND(404, "존재하지 않는 회원입니다."),
    POST_NOT_FOUND(404, "존재하지 않는 글입니다."),

    private final int status;
    private final String message;
}

몇가지만 가지고 왔고, UNAUTHORIZED 예외처리는 다음 글과 관련있기 때문에 남겨두었다.
enum class를 이용하여
첫번째 인자에는 http 상태코드
두번째 인자에는 에러에 대한 메시지를 담아두었다.

👉 2. ErrorDto.java 작성

@Getter
@AllArgsConstructor
public class ErrorDto {

    private final int status;
    private final String message;
}

에러 내용을 담기 위해서 작성하였다.

👉 3. CustomException.java

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {

    private final ErrorCode errorCode;
}

RuntimeException을 상속하여 CustomException에서 사용할 수 있도록 하였다.

👉 4. GlobalExceptionHandler.java

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /* CustomHandler 에러 처리 */
    @ExceptionHandler(CustomException.class)
    protected ResponseEntity<?> customExceptionHandler(CustomException ex) {

        ErrorCode errorCode = ex.getErrorCode();
        ErrorDto errorDto = new ErrorDto(errorCode.getStatus(), errorCode.getMessage());
        log.error("Error occurred : {}, Stack trace: {}", ex.getMessage(), getCustomStackTrace(ex));
        return new ResponseEntity<>(errorDto, HttpStatusCode.valueOf(errorDto.getStatus()));
    }
    
    /* MethodArgumentNotValidException 처리 */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        log.error("Error occurred : {}", errors);
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
    
    /* 일반 예외 처리 */
    @ExceptionHandler
    protected ResponseEntity<?> customServerException(Exception ex) {
        ErrorDto error = new ErrorDto(INTERNAL_SERVER_ERROR.getStatus(), INTERNAL_SERVER_ERROR.getMessage());
        log.error("Error occurred : {}, Stack trace: {}", ex.getMessage(), getCustomStackTrace(ex));
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

우선 class에 어노테이션을 달아줘야 하는데 2가지 어노테이션이 있다.

  • @ControllerAdvice는 전통적인 MVC 컨트롤러와 함께 사용.
  • @RestControllerAdvice는 RESTful 컨트롤러와 함께 사용.
  • @RestControllerAdvice는 @ControllerAdvice에 @ResponseBody를 추가한 것이다.

나는 RESTful한 @RestControllerAdvice를 작성하였다.

그리고, 예외 처리를 할 메서드에는 @ExceptionHandler를 추가해주어야 하고, 위 예시처럼 @ExceptionHandler(CustomException.class)이렇게 명시적으로 예외 처리 할 class를 지정해줘도 되지만, 안해줘도 된다.

  • 단, 예외 처리 메서드 인자값에 예외 반환 값이 있어야함.
    ex - protected ResponseEntity<?> customExceptionHandler(CustomException ex)

다음으로 예외 처리를 위해서 메서드를 작성하여야 한다.

나는 3가지 예외 처리 메서드를 작성하였다.
첫 번째 메서드는 내가 작성한 CustomException과 관련된 예외 처리이다.
두 번째 메서드는 유효성 검사 중 오류가 발생한 부분에 대해서 예외를 처리해주는 것이다. (아래에 자세히 적어두겠다.)
세 번째 메서드는 작성하지 못한 예외 문구 or 잘못된 api 연결 등에 대해서 처리해준다.

두 번째 예외 처리 메서드는 다음과 같이 작동한다.
<SignUpDto에 대한 유효성 검사 문구들>
<Controller에서 SignUpDto에 대해서 유효성 검사를 위해 @Valid 어노테이션 추가>
<조건 만족 못할 시 반환값>


위와 같이 설정하면 아래와 같이 예외 문구를 처리할 수 있다. (첫 번째 예외 처리 메서드 문구이다.)

User foundUser = userRepository.findByUsername(username).orElseThrow(() ->
                new CustomException(ErrorCode.USER_NOT_FOUND));

💡 느낀점

개인 프로젝트에서도 글로벌 예외 처리는 편리하고 중요하겠지만, 팀 프로젝트를 할 때 특히나 중요하다고 느꼈다. 사람마다 생각하는 예외 처리 문구가 다를 수 있고, 보내는 상태코드 값이 다를 수 있기 때문에 일관성이 없어질 수 있다.
팀원과 처음에 시작할 때 이것을 고려하면 예외 처리에 대한 일관성을 유지할 수 있다고 생각한다.

profile
Better than yesterday.

0개의 댓글