-
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
- 사용처: (Rest)Controller의 메소드나 @(Rest)ControllerAdvice 클래스의 메소드에 @ExceptionHandler 애노테이션을 추가할 수 있습니다.
- 명시방법: 처리할 exception class명을 애노테이션의 attribute로 지정하고, 파라미터 타입으로 지정하여 사용합니다.
- 동작: status와 payload(
body()
)를 한 번에 직접 커스텀할 수 있다는 장점이 있습니다(적절한 메시지를 내려준다면 장점이겠죠?!). 구체 익셉션 클래스의 핸들러부터 상위의 익셉션 클래스 핸들러 순으로 찾습니다. 개인적으로는 가장 유용할 것 같아요. - 예시 코드
@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
- 사용처: 익셉션 클래스 자체 || 컨트롤러 메소드 || @RestControllerAdvice에 함께
- 명시방법: @ResponseStatus 애노테이션을 명시하고 code attribute로 HttpStatus 상태코드를 지정합니다.
- 동작: 상태코드를 기준으로 작동하기 때문에(지정한 상태코드가 응답에 담김), payload도 변경해야할 경우 적절치 않을 수 있습니다. 혹은 '1.ExceptionHandlerExceptionResolver'와 함께 혼용해야 합니다(그러나 굳이..?).
- 예시 코드
@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)와 처리 흐름에 대한 좀 더 깊은 이해.
'각종 학습 요약 > Spring' 카테고리의 다른 글
Spring : 이벤트 리스너 기본 개념 (@EventListener, @TransactionalEventListener) (2) 2022.07.11 Spring: Proxy를 통한 Spring Data Repository 초기화와 설정 (2) 2022.07.01 Spring: AOP의 기본적인 개념 & SpringAOP 훑어보기 (2) 2022.06.20 Spring: 의존관계 주입(DI, Dependency Injection) 방식 네 가지 요약 (0) 2022.06.16 Spring: IoC와 DI를 예시로 쉽게 이해해보자 (0) 2022.06.16 Spring: Spring Container와 Bean (0) 2022.06.16