ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Error Handling: Spring에서 Exception 대응하기
    각종 학습 요약/Spring 2022. 6. 17. 20:51

    Error Handling: Spring에서 IllegalArgumentException 대응하기

    스프링부트 프로젝트를 빠르게 훑어보고자 하는 요구가 있어서, 최근 이동욱 님의 스프링 부트와 AWS로 혼자 구현하는 웹서비스를 보고 있습니다. 간단한 생성/조회/수정/삭제 기능을 구현해보는 파트를 진행하다가 마지막 삭제 파트를 따라서 구현해놓고 궁금증이 생겼습니다.

    다 따라서 만들긴 했는데...


    (삭제는 이미 잘 되는 상황)
    삭제는 잘 작동하는데.... 만약에 다시 삭제를 시도하면 어떻게 될까?

    해당 부분을 위해서 Service 계층에서 삭제하기 전에 먼저 조회로 리포지토리.findById(id).orElseThrow(() -> new IllegalArgumentException(id + "번 게시글이 없어요"));와 대강 비슷한 코드를 작성해놓긴 했었지만 실제로 어떻게 동작되는지가 궁금했어요.
    그래서 삭제완료 페이지에서 뒤로 가기를 한 다음 다시 삭제버튼을 눌러봤습니다.

    그랬더니 아래 이미지와 같은 상황이 벌어졌습니다.

    이것을 보고 네 가지 생각이 들었는데요.
    1. 실제 상황이 아니어서 정말 다행이다.😰
    2. 사용자에게 response message를 그대로 보여주는 건 부적절하다.
    3. 이 상황에서 500 에러를 던지는 것은 부적절하다. 식별할 리소스가 존재하지 않았으니 404가 적절하지 않을까?
    4. 책에서 HTTP DELETE는 멱등이라고 했는데, 삭제 전에 삭제 할 수 있는지 조회를 해본다면 결국 멱등하지 않은 거 아닐까? (삭제 1회차: 조회성공, 조회한 데이터 삭제되고 삭제된 리소스 아이디를 리턴 / 삭제 2회차: 조회실패, 삭제 시도 안하고 오류를 리턴)
    일단 정말 안도했고요(1).
    제 생각에는 404를 던지고 싶은데(3) 스프링에서 에러핸들링을 어떻게 하고 있는지(2)를 모르고 있다는 생각이 들었습니다. ('포스팅으로 정리하면서 공부해야겠다!')
    그리고 끝으로, '어쩌면 DELETE를 멱등하게 구성하는 건 좀 이상한 걸지도?... 삭제하지도 않은 걸 삭제했다고 할 수는 없으니... 그렇다고 정상적으로 삭제를 시도했습니다 라고 하는 것도 이상하고...' 라는 생각을 했습니다.

    사실 글로 옮기려고 네 가지만 적은거지, 오만가지 생각이 들었네요. 다 이야기 할 수는 없으니 다음 파트로 넘어가 보겠습니당.

    일단 이렇게 임시방편



    응답을 받는 js fallback에서 이런 코드를 넣었습니다. (실제 서비스라면 어떻게 했을까.... 궁금..)

    그래서 이렇게 되었습니다.

    하지만 이 해결 방법이 진짜 진짜 진짜로 마음에 들지 않았기 때문에 해결 방안을 찾아 떠나게 되었어요.

    Spring에서 어떻게 처리하고 있지?


    스프링에서 에러 처리가 어떻게 되었는지를 알면, 첫째로 적절한 상태코드를 돌려줄 수 있을 것이고, 둘째로 적절한 에러메시지를 만들어줄 수 있을 거라고 생각했어요.
    그래서 해당 키워드를 찾아나섰는데...

    이렇게 많은 내용이 있을 줄 몰랐는데 일단 찾은 것들 정리 좀 해볼게요...

    일반적인 예외와 오류에 대한 처리는 위와 같이 찍먹해보았습니다. 다시 원점으로 돌아와서...
    스프링에서는 어떻게 처리하고 있는 걸까요?
    일단 Spring Web MVC는 Exception이 발생하면(그리고 코드가 아무 관여도 하지 않으면) 응답 코드를 HttpStatus.INTERNAL_SERVER_ERROR로 내려줍니다(잡았다 요놈!!!!!).
    휴... 그대로 둬서는 안되겠죠. 적절한 처리 방법에 대해 알아봐야겠습니다.

    오류/예외 처리는 공통 관심사(cross-cutting concern)


    소제목과 같이 오류/예외의 처리는 공통 관심 로직이기 때문에 Spring에서는 AOP 개념을 적용하기 위해 HandlerExceptionResolver라는 인터페이스를 구현해두었습니다(Spring docs - HandlerExceptionResolver). 그리고 구현체로는 네 가지가 있는데, 그 중 직접 처리에 관여하는 것은 대개 세 가지입니다.
    1. ExceptionHandlerExceptionResolver: @Controller나 @RestController의 @ExceptionHandler를 따라 예외처리합니다.
    2. ResponseStatusExceptionResolver: @ResponseStatus나 ResponseStatusException의 예외를 따라 처리합니다.
    3. DefaultHandlerExceptionResolver: 스프링의 예외를 처리합니다.

    이 글에서는 위의 처리방법 중에서 1, 2번에 대해서만 설명을 하도록 하겠습니다.

    1. ExceptionHandlerExceptionResolver


    1. 사용처: (Rest)Controller의 메소드나 @(Rest)ControllerAdvice 클래스의 메소드에 @ExceptionHandler 애노테이션을 추가할 수 있습니다.
    2. 명시방법: 처리할 exception class명을 애노테이션의 attribute로 지정하고, 파라미터 타입으로 지정하여 사용합니다.
    3. 동작: status와 payload(body())를 한 번에 직접 커스텀할 수 있다는 장점이 있습니다(적절한 메시지를 내려준다면 장점이겠죠?!). 구체 익셉션 클래스의 핸들러부터 상위의 익셉션 클래스 핸들러 순으로 찾습니다. 개인적으로는 가장 유용할 것 같아요.
    4. 예시 코드
    @Controller 
    @RequiredArgsConstructor
    public class UserController {
    
        private final UserService userService;
    
        @GetMapping("/users/{id}")
        public Response findUser(@PathVariable Long id) {
            return userService.findUser(id);
        }
    
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException exception) { //파라미터 타입은 어트리뷰트와 동일
            return ResponseEntity.status(HttpStatus.NOT_FOUND) // 상태코드 변경
                    .body(exception.getMessage()); // payload 변경
        }
    }

    2. ResponseStatusExceptionResolver


    1. 사용처: 익셉션 클래스 자체 || 컨트롤러 메소드 || @RestControllerAdvice에 함께
    2. 명시방법: @ResponseStatus 애노테이션을 명시하고 code attribute로 HttpStatus 상태코드를 지정합니다.
    3. 동작: 상태코드를 기준으로 작동하기 때문에(지정한 상태코드가 응답에 담김), payload도 변경해야할 경우 적절치 않을 수 있습니다. 혹은 '1.ExceptionHandlerExceptionResolver'와 함께 혼용해야 합니다(그러나 굳이..?).
    4. 예시 코드
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public class CustomException extends IllegalArgumentException {
        public CustomException(String message) {
            super(message);
        }
    }
    // 특정 예외별로 처리를 따로 해주어야 하고, 묶인 예외와의 결합이 너무 강해 좋지 않습니다(하나의 예외는 무조건 동일한 응답을 주게 됨).

    마무리 - 설명하지 않은 방식들


    덜 중요해서 설명하지 않은 것은 아니고요. 이해가 부족해서 설명을 덧붙이지 않았습니다. 다만, 공부는 해야하니까요! 키워드는 남겨두고자 본 항목을 만들었습니다.

    • @ControllerAdvice 또는 @RestControllerAdvice를 통한 전역적인 컨트롤러 예외 처리. (본래는 대개 이 방법으로 예외를 처리하는 것이 가장 좋다고 하네요.)
    • Spring 예외를 처리해둔 추상클래스: ResponseEntityExceptionHandler
    • ResponseStatusException(@ResponseStatus의 대체제. Spring 5부터 가능. unchecked기 때문에 명시적인 처리 불필요.)
    • 처리 순서(스프링의 기본적인 처리순서 || @Order)와 처리 흐름에 대한 좀 더 깊은 이해.

    댓글

Designed by Tistory.