@RestControllerAdvice를 이용한 예외처리

wooSim·2023년 7월 10일
1

Spring 프레임워크에서 다양한 예외 처리 방법 중 권장되는 @RestControllerAdvice(@ControllerAdvice)에 대해 알아보고자 합니다.


1. @ExceptionHandler란


@RestControllerAdvice(@ControllerAdvice)를 알아보기 전에 먼저 @ExceptionHandler 에 대해 알아보겠습니다. 아래와 같이 일부러 Unchecked Exception을 만들어보겠습니다.

@Controller
public class IndexController {
	...
    @GetMapping("/posts")
    public String posts(){
        int exception = 4/0;
        return "posts";
    }
}

위 코드는 ArithmeticException 예외를 발생할 것입니다. 어플리케이션을 실행해 확인해보면 스프링부트에서 제공하는 기본 에러 페이지를 확인할 수있습니다.

만약 실제 운영하고 있는 시스템에서 에러가 발생해서 위와같은 화면이 나오고 앱이 죽는다면 이용자들이 앱을 사용하는데 상당한 불편함을 겪을 것 입니다. 앱을 죽지 않게 할려면 try-catch 구문을 작성해야 하지만 Spring에서는 @ExceptionHandler 어노테이션을 통해 매우 유연하고 간단하게 예외처리를 할 수 있게 해줍니다.

@GetMapping("/posts")
public String posts(){
	int exception = 4/0;
	return "posts";
}

@ExceptionHandler(ArithmeticException.class)
public ResponseEntity<String> handleNoSuchElementFoundException(ArithmeticException exception) {
	return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exception.getMessage());
}

위와같이 @ExceptionHandler 어노테이션이 붙은 메서드를 추가해서 에러를 처리하고 발생한 예외는 ExceptionHandlerExceptionResolver에 의해 처리가 됩니다.

@ExceptionHandler 사용함으로서 장점은를 사용하면 HttpServletRequest나 WebRequest 등을 얻을 수 있으며 반환 타입으로는 ResponseEntity, String, void 등 자유롭게 활용할 수 있습니다.

하지만 @ExceptionHandler는 특정 컨트롤러에서만 발생하는 예외만 처리하기 때문에 여러 Controller에서 발생하는 에러 처리 코드가 중복될 수 있으며, 사용자의 요청과 응답을 처리하는 Controller의 기능에 예외처리 코드가 섞이며 단일 책임 원칙(SRP)가 위배되게 됩니다.



2. @ControllerAdvice와 @RestControllerAdvice


두 어노테이션은 각각 @Controller 어노테이션, @RestController 어노테이션이 붙은 컨트롤러에서 발생하는 예외(@ExceptionHandler)를 AOP를 적용해 예외를 전역적으로 처리할 수 있는 어노테이션 입니다. 당연한 얘기지만 @RestControllerAdvice @ResponseBody 어노테이션을 포함하기 때문에 Json으로 응답을 해줍니다.

그럼 아까 @ExceptionHandler를 @RestController에 옮겨보도록 하겠습니다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> handleArithmeticException(ArithmeticException e) {

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }

}

그리고 다시 어플리케이션을 실행해보면 같은 Response 응답을 받는 것을 볼 수 있습니다.

이제 다른 예외들도 @RestControllerAdvice 어노테이션이 붙은 클래스에서 처리하여 예외처리 전용 클래스를 만들면 됩니다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> handleArithmeticException(ArithmeticException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
    
    
    ...
}

이렇게 @RestController를 구현해보았습니다. 이제 여기에서 Custom Exception도 추가할 수 있도록 더 구현해보도록 하겠습니다.



3. Custom Exception 적용하기


○ Error Code 정의하기

Custom Exception을 추가하기 위해서 먼저 ErrorCode를 만들도록 하겠습니다.

ErrorCode에는 에러이름, HTTP 상태, 메시지를 담고 다양한 Error Code에서 사용할 수 있도록 인터페이스로 만들어 추상화하도록 하겠습니다.

public interface ErrorCode {
    String name();
    HttpStatus getHttpStatus();
    String getMessage();
}

그리고 enum 클래스로 ErrorCode를 구현하도록 하겠습니다.

@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode{
    INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter..."),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error..."),
    ;

    private final HttpStatus httpStatus;
    private final String message;

}

@Getter
@RequiredArgsConstructor
public enum PostErrorCode implements ErrorCode{

    DUPLICATED_POST_REGISTER(HttpStatus.BAD_REQUEST, "Duplicated post register..."),
    ;

    private final HttpStatus httpStatus;
    private final String message;
}

CommonErrorCode에는 잘못된 파라미터가 넘어왔을때 사용할 INVALID_PARAMETER와 그 외의 런타임 exception에 사용할 INTERNAL_SERVER_ERROR를 만들고 PostErrorCode에는 똑같은 게시글이 저장했을 때 사용할 DUPLICATED_POST_REGISTER를 구현하였습니다.

RuntimeException을 상속받는 Custom 예외클래스를 만들어 줍니다.

@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException{

    private final ErrorCode errorCode;
}

이제 클라이언트에 응답으로 던져줄 Error 포맷을 만들기 위해 DTO 클래스를 만들어 줍니다.

@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {

    private final String code;
    private final String message;
}

이제 마지막 으로 @RestControllerAdvice를 다시 구현해보겠습니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class) // ①
    public ResponseEntity<Object> handleCustomException(CustomException e) {
        ErrorCode errorCode = e.getErrorCode();
        return handleExceptionInternal(errorCode);
    }

    @ExceptionHandler({Exception.class}) // ②
    public ResponseEntity<Object> handleIllegalArgument(Exception e) {
        ErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
        return handleExceptionInternal(errorCode);
    }

    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeErrorResponse(errorCode));
    }

    private ErrorResponse makeErrorResponse(ErrorCode errorCode) {
        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(errorCode.getMessage())
                .build();
    }


    @ExceptionHandler(ArithmeticException.class) // ③
    public ResponseEntity<Object> handleArithmeticException(ArithmeticException e) {
        ErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR;
        return handleExceptionInternal(errorCode, e.getMessage());
    }


    private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, String message) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(makeErrorResponse(errorCode, message));
    }

    private ErrorResponse makeErrorResponse(ErrorCode errorCode, String message) {
        return ErrorResponse.builder()
                .code(errorCode.name())
                .message(message)
                .build();
    }
}

우리가 만든 CustomException이 발생할 경우 ①에서 예외처리를 하게 되고 따로 만들어준 ArithmeticException 이 발생할 경우 ③ 에서 예외처리 응답을 하게 됩니다. 마지막으로 예외가 발생했는데 별다른@ExceptionHandler에서 처리되지 않으면 최상위 Exception인 ②에서 처리 되어 CommonErrorCode.INTERNAL_SERVER_ERROR 에러 코드를 응답하게 됩니다.

테스트를 위해 Controller 강제로 exception을 발생 시키면 각각 어느 @ExceptionHandler에서 처리되는지 자세히 확인할 수있습니다.

GetMapping("/posts")
public String posts(){ //@ExceptionHandler(ArithmeticException.class)
	int exception = 4/0;
	return "posts";
}

@GetMapping("/posts/exception")
public void exception() { // @ExceptionHandler({Exception.class})
	throw new IllegalStateException("State Exception...");
}

@GetMapping("/posts/custom/common")
public void exception1(){//@ExceptionHandler(RestApiException.class)
        throw new CustomException(CommonErrorCode.INVALID_PARAMETER);
}

@GetMapping("/posts/custom/post")
public void exception2(){//@ExceptionHandler(RestApiException.class)
	throw new CustomException(PostErrorCode.DUPLICATED_POST_REGISTER);
}

이렇게 구현한 RestControllerAdvice은 무분별한 try-catch가 없어 가독성에 좋다. 하나의 클래스로 모든 Controller에 대한 전역적인 예외처리가 가능해서 훨씬 깔끔하게 예외처리를 할 수 있습니다.



profile
daily study

0개의 댓글