RestControllerAdvice로 전 계층 예외 처리

YJ·2024년 5월 27일

Spring 에러 노트

목록 보기
5/5
post-thumbnail

상황

ProductService.class

    public Product findProduct(String name) {
        return productRepository.findByName(name)
                .orElseThrow(()
                        -> new NoSuchElementException("상품이 존재하지 않습니다."));
    }

해당 Service에서 Spring Data JPA의 findByName으로 데이터를 조회할 때, 데이터가 없을 경우에는 null값이 반환되어 NoSuchElementException이 발생한다.

해당 에러를 처리한 부분은 다음과 같다.

// 프로젝트의 모든 컨트롤러의 예외 처리를 전역으로 담당
// 다른 계층에서 터져도 throws 등을 통해 컨트롤러로 전달
// 결국 모든 프로젝트 코드 예외 처리를 도맡아 가능하다!
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiUtils.ApiResult<ApiUtils.ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
        return error( ApiUtils.ErrorResponse.of(ex), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(DuplicateMemberIdException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ApiUtils.ApiResult<String> handleDuplicateMemberIdException(DuplicateMemberIdException error) {
        String errorMessage = error.getMessage();
        return error(errorMessage, HttpStatus.CONFLICT);
    }

    @ExceptionHandler(DuplicateProductNameException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ApiUtils.ApiResult<String> handleDuplicateProductNameException(DuplicateProductNameException error) {
        String errorMessage = error.getMessage();
        return error(errorMessage, HttpStatus.CONFLICT);
    }

    @ExceptionHandler(PasswordNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiUtils.ApiResult<String> handlePasswordNotValidException(PasswordNotValidException error) {
        String errorMessage = error.getMessage();
        return error(errorMessage, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(NoSuchElementException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiUtils.ApiResult<String> handleNoSuchElementException(NoSuchElementException error) {
        String errorMessage = error.getMessage();
        return error(errorMessage, HttpStatus.NOT_FOUND);
    }


//    @ExceptionHandler(MethodArgumentNotValidException.class)
//    public ResponseEntity<ApiUtils.ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
//        System.out.println("여기까지는 출력됨!");
//        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiUtils.ErrorResponse.of(ex));
//    }
}

해당 클래스는 @RestControllerAdvice를 붙인 클래스로써
Controller에서 발생하는 에러를 처리해주는 클래스이다.

에러는 Service에서 발생했는데, 어떻게 Controller의 예외를 처리해주는 클래스에서 예외를 처리해줄까?

해결 방법

서비스 계층에서 발생한 예외가 컨트롤러로 전파되는 것은 자바와 스프링 프레임워크의 예외 전파 메커니즘 때문이다.
자바에서 예외는 호출 스택(call stack)을 따라 전파된다.
즉, 메서드에서 발생한 예외를 해당 메서드 내에서 처리하지 않을 경우, 그 메서드를 호출한 상위 메서드로 예외가 전달된다.
이러한 과정이 반복되어 예외를 처리할 코드를 만날 때까지 계속된다.

나의 코드의 경우
Service의 findProduct에서 예외가 발생하였다.
이 메서드에서는 NoSuchElementException을 처리해주지 않았다.
그러면 이 예외는 호출한 상위 메서드로 전달된다.
이 메서드를 호출한 메서드는 Controller에 있다.

@GetMapping("/products/{name}")
    public ApiUtils.ApiResult<Product> findProduct(@PathVariable("name") String name) {
        if(!Validator.isAlpha(name)){
            // Logger log = LoggerFactory.getLogger(ProductController.class);

            // 요청할 때, 값이 잘 못 전달 된 경우
            // 즉, 요청이 잘 못된 경우에는 BAD_REQUEST
            // TODO log INFO 레벨 id
            // type : generics
            return error(null, HttpStatus.BAD_REQUEST);
        }

        Product resultProduct = productService.findProduct(name);

        if(resultProduct == null) {
            return error( null, HttpStatus.NOT_FOUND);
        }
        return success(resultProduct);
    }

즉, 서비스 계층에서 예외가 발생하였는데 처리되지 않으면 이 예외를 호출한 컨트롤러 계층으로 전파한다.
현재 이 컨트롤러 메서드에서도 해당 에러를 처리하는 구문은 없다.
따라서, 컨트롤러에서도 이 예외를 처리하지 않았으므로, 스프링 프레임워크의 글로벌 예외 핸들러(@ControllerAdvice 어노테이션이 적용된 클래스)까지 예외가 전파된다.
글로벌 예외 핸들러에서는 다양한 타입의 예외를 처리할 수 있도록 @ExceptionHandler 어노테이션을 사용하여 예외 처리 메서드를 정의할 수 있다.

따라서, @RestControllerAdvice의 @ExceptionHandler(NoSuchElementException.class) 어노테이션이 붙은 예외 처리 메서드가 호출되는 것이다.

이런 방식으로, 스프링 프레임워크는 서비스 계층에서 발생한 예외를 효율적으로 컨트롤러 계층으로 전파하고, 최종적으로는 글로벌 예외 핸들러를 통해 애플리케이션 전체에서 일관된 방식으로 예외를 처리할 수 있게 한다.

참고 자료
https://help.acmicpc.net/judge/rte/NoSuchElement

profile
dev

0개의 댓글