HTML vs Api 오류
전에 언급한 Spring mvc 패턴과 Restful Api의 차이가 여기서 한 번 더 나온다. 이 둘의 차이가 헷갈리다면 아래 글을 참고하기 바란다.
2023.08.16 - [Spring/Spring Exception] - Spring Exception 4. Api 예외 처리 (Spring mvc VS RestfulApi)
Spring Exception 4. Api 예외 처리 (Spring mvc VS RestfulApi)
Spring mvc 패턴 VS RestfulApi 이전 글에도 언급했지만 mvc 패턴과 Api의 차이를 명확히 알아야 이해할 수 있다. 이 둘의 가장 큰 차이점만 언급하겠다. Spring mvc 패턴 View 지향: Spring MVC 패턴은 주로 웹 애
toychip.tistory.com
둘의 차이를 이해해야 이해할 수 있으므로 꼭 이해하고 읽기를 바란다.
웹 브라우저에 HTML 화면을 제공할 때 오류 발생 시에 BasicController를 사용하면 4xx, 5xx 이런 식으로 오류를 통합할 수 있으며 매우 편리하다.
하지만 Api의 경우 상황에 따라 다른 오류 메시지를 전달해줘야 한다.
이 전글에서 보면 HandlerExceptionResolver는 빈 ModelAndView를 반환해서 오류를 삼키고 WAS에게 정상 응답을 내려주었었다.
하지만 Api에서는 이러한 로직이 필요하지 않다. Api 응답을 위해 예전 SpringException 7번 글에서 errorResult.put을 사용하여 어떤 오류인지 직접 내려주었다.
이렇게 하나하나 하는 것은 매우 불편하다. 또한 어떠한 컨트롤러에서만 발생하는 예외를 처리하고 싶을 때 커스텀하기가 어렵다.
예를 들어 상품의 컨트롤러와 회원 컨트롤러의 동일한 RuntimeException 예외를 서로 다른 방식으로 내려주고 싶다면 어떻게 해야 할까?
@ExceptionHandler
스프링은 @ExceptionHandler를 제공하는데, 이것이 ExceptionHandlerExceptionResolver이다. 이름 한 번 길다.
스프링은 이를 기본으로 제공하고, ExceptionResolver 중에 우선순위도 가장 높다. 실무에서 Api 예외처리는 대부분 이 기능을 사용한다.
Api 예외처리를 하기 위해 이전과 같은 코드를 만들고 엔드포인트만 수정한다.
@RestController
public class ApiExceptionV2Controller {
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if (id.equals("user-except")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "나의 이름은! " + id);
}
}
여기서, http://localhost:8080/api2/members/bad 요청 시 아래와 같은 결과가 이전에도 나왔었다.
실행 결과1
BasicErrorController에서 500 코드를 400으로 바꿔주었다. 하지만 우리는 예외를 커스텀 하고 싶다.
@ExceptionHandler를 사용해 보자.
ApiExceptionV2Controller에 아래의 내용을 추가한다.
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
이렇게 ApiExceptionV2Controller에다가 @ExceptionHandler(IllergalArgumentException.class)를 선언해 놓을 경우, 해당 컨트롤러에서 IllergalArgumentException 이 발생할 경우 illegalExGandler 메서드가 실행된다.
RestController이기 때문에, ErrorResult가 json으로 반환되어 나간다.
실행결과 2
우리가 정한 대로 code의 값은 "BAD"가, message의 값은 컨트롤러에서 정해놓은 "잘못된 입력 값" json을 볼 수 있다.
진행 순서를 보자. IllegalArgumentException이 발생했을 때 바로 @ExceptionHandler가 있는 illegalExHandler가 호출되는 게 아니다.
이전에 학습했던 그림으로 다시 한번 상기시켜 보자.
컨트롤러에서 예외가 발생했을 때 ExceptionResolver에다가 예외를 처리할 수 있는지 물어본다.
그 후 ExceptionResolver이 @ExceptionHandler가 있는지 찾아본다. 그리고 호출이 되는 것이다.
결과가 커스텀 되었지만 지금 상태코드는? 200으로 되어있다. 200은 정상일 경우에만 보이는 상태코드이므로 이를 바꿔줘야 한다.
이를 controller에서도 붙일 수 있는 @ResponseStatus를 사용하여 상태코드를 지정하는 것을 illegalExHandler에 붙여주면 된다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
이렇게 해결할 경우. 이 전에 학습했던 servlerContainner까지 올라가고 WAS에서 다시 내려오는 것이 아닌, 여기서 정상 흐름으로 끝난 것이다! 상태코드를 바꿔준 정상 흐름이라고 생각하면 된다.
또 다른 예제로 응답 형식을 ResponseEntity로 해서 반환해 보자.
@ExceptionHandler(UserException.class)
public ResponseEntity<ErrorResult> userHandler(UserException e){
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}
여기서 메서드 매개변수 에러와 @ExceptionHandler의 예외가 같다면, @ExceptionHandler() 매개변수는 생략할 수 있다!
역시나 잘 호출된 것을 볼 수 있다.
다른 예제를 하나 더 보자
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부 오류");
}
같은 클래스에 다른 예외처리를 하나 더 만들었다.
예외에 있어 가장 최상위 계층인 Exception을 잡도록 했다. 이렇게 선언하고 500 상태코드를 보이게 만들었다.
지금까지 작성한 예외들을 보면, illegalExHandler 메서드는 IllegalArgumentException과 그 자식들의 예외를 처리한다. userHandler 메서드는 UserHandlerException과 그 자식들의 예외를 처리한다.
여기서 방금 작성한 exHandler 메서드에서 Exception을 처리하지만 userException과 IllegalArgumentException은 Exception의 자식들이다. Exception은 예외에 있어 가장 높은 부모이기 때문이다.
처음에는 겹치기에 어떻게 처리하는지 헷갈렸는데 알고 보면 간단하다. Spring은 항상 더 자세한 것에 대한 것이 우선 처리이다. 그러므로 UserException과 IllegalArgumentException이 아닌 다른 예외들만 잡히게 된다.
ex로 들어올 경우 우리는 RunTimeException 예외처리를 했다. 근데 RunTimeException에 대한 예외 처리가 없으므로 RunTimeException의 부모인 Exception 처리를 통해 해결되고, 방금 작성한 "내부 오류"와 "500 Internal Server Error"가 나타난 것을 볼 수 있다.
이것이 거의 마지막 단계인 것 같고 가장 완벽한 것 같다.
하지만 단점이 딱 하나 있는데, 한 컨트롤러 안에서 예외를 우리가 작성하여 사용해 봤다.
하지만 다른 컨트롤러에서도 이와 똑같은 예외를 적용하고 싶다면? 코드를 전부 다 복사 붙여 넣기를 해야 할까?
그것을 해결할 수 있는 것이 정말 마지막인 @ControllerAdvice이다. 다음 글에서 @ControllerAdvice를 알아본다.
코드링크
https://github.com/toychip/StudySpring/commit/192573077ad5a27b5b8aa3b26f391c3d90b6d86b
Feat: Spring-mvc2-exception @ExceptionHandler 시작 · toychip/StudySpring@1925730
toychip committed Aug 31, 2023
github.com
https://github.com/toychip/StudySpring/commit/690acf15fcb0c1c0ee5340d73bc19b54826de70e
Feat: Spring-mvc2-exception @ExceptionHandler 완료 · toychip/StudySpring@690acf1
toychip committed Sep 4, 2023
github.com
김영한님 mvc2 - Exception 강의로 학습하였습니다. 공부한 내용을 상기시키고 해당 내용을 몰랐을 때의 입장에서 쉽게 배울 수 있지 않을까 해서 작성한 글입니다. 잘못된 내용이 있을 경우 말씀해 주세요. 피드백 환영합니다.
'Spring > Spring Exception' 카테고리의 다른 글
Spring Exception 10. @ControllerAdvice (0) | 2023.08.26 |
---|---|
Spring Exception 8. Spring이 제공하는 @ResponseStatus, DefaultHandler (0) | 2023.08.19 |
Spring Exception 7. Api에서 ExceptionResolver 활용 (0) | 2023.08.18 |
Spring Exception 6. ExceptionResolver (HandlerExceptionResolver) (0) | 2023.08.17 |
Spring Exception 5. 스프링 부트 Api 오류처리 (BasicErrorController) (0) | 2023.08.16 |