Html 화면 오류 vs API 오류
html 사용하는 환경의 오류는 BasicErrorController를 사용하는게 편하다. 그냥 리소스의 템플릿에 /error 폴더 만들어서 코드명.html로 에러 페이지 만들어 놓으면 스프링이 자동으로 등록시키는 BasicErrorController에서 자동으로 인식한다.
근데 API는 시스템마다 응답의 모양도 다르고, 스펙도 다르다. 단순히 예외에 대한 정보를 보여주는 게 아니라, 특정 예외에 따라서 각각 다른 데이터를 보내줘야 할 때도 있다.
한마디로 세밀한 제어가 필요하다.
예를 들어 상품API와 주문 API 의 오류는 에러정보를 Json으로 내려줄 때 모양새가 완전히 다를 수 있다.
스프링은 이러 한 문제점을 해결하기 위해, @Exceptionhandler라는 매우 편리한 애노테이션을 제공한다.
이것이 바로 71번째 글의 우선순위에 잠깐 나왔던 ExceptionHandlerExceptionResolver이다.
실무에서 API예외처리는 대부분 이 기능을 사용한다.
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@GetMapping("/api2/members/{id}")
public ApiExceptionController.MemberDto getMember(@PathVariable("id") String id){
if(id.equals("ex")){
throw new RuntimeException("잘못된 사용자");
}
if(id.equals("bad")){
throw new IllegalArgumentException("잘못된 입력 값");
}
if(id.equals("user-ex")){
throw new UserException("사용자 오류");
}
return new ApiExceptionController.MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto{
private String memberId;
private String name;
}
}
그냥 오류가 생길 수 있는 컨트롤러에
@ExceptionHandler(IllegalArgumentException.class)
하고 컨트롤러에서 정의해 두면,
예외발생 시 일단 5번 ExceptionResolver로 갔다가,
에러에 대해 온 정보 중 해당 컨트롤러에 대한 정보까지 같이 받아서,
그 컨트롤러에 @ExceptionHandler가 있는지 조회해 본다.
다음은 실제 HandlerExceptionResolver 인터페이스의 일부 코드이다.
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
request, response, 에러 뿐만 아니라 핸들러까지 받는다. 저 핸들러 안에 @ExceptionHandler가 있는지 조회해 보는거다.
그래서 만약 있다면,
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
여기에 명시한 대로 처리를 하는 것이다.
단, 여기서 이렇게 깨끗하게 처리를 하였으므로, WAS는 이걸 정상적인 것으로 본다.(상태코드가 200으로 된다.)
그래서 상태코드를 @ResponseStatus(상태코드)해서 바꿔서 넣어주는 것이다.
ErrorResult는 API의 스펙? 간단하게 우리가 정의해 놓은 것이다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
위의 순서도 보면 알겠지만, 응답의 Exception이 WAS로 가기 전에 처리 한 것이라 WAS까지 정상적으로 쭉 간다.
추가로 더 @ExceptionHandler를 구현해 봤다.
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX",e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(UserException.class)랑 똑같다. 저렇게 좀 인자 받는 느낌으로 받게도 할 수 있다. 왠지 저게 더 보기 좋다.
이거는 아예 Return타입을 ResponseEntity로 Json으로 변환 후 반환하게 해놨다.
사실 상 거의 보통의 API용 컨트롤러 쓰는 느낌이다.
추가로 또 @ExceptionHandler를 구현해 봤다.
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부 오류");
}
아예 Exception을 받게 해 놨다.
위 처럼 더 구체적인? 더 신세대? 더 자세한? 쪽이 먼저 조회되는 것 같다.
신세대 쪽 부터 조회를 하다가, 맨 마지막을 Exception 쪽으로 조회하는 것 같다.
정리
사용법
예외가 발생할 여지가 있는 컨트롤러에,
@ResponseStatus(상태코드) <- 안 넣으면 200으로 정상처리라고 응답하기 때문
@ExceptionHandler(예외클래스.class) 혹은 @ExceptionHandler 하고 인자로 예외 받기
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
log.error("[exceptionHandler] ex", e);
return new ErrorResult("EX", "내부 오류");
}
이런 식으로 만들면, 그 컨트롤러 안에서 일어나는 예외는 이렇게 정의해 놓은 얘네가 처리를 하는 것이다.
ResponseEntity를 반환타입으로 하여, 다른 컨트롤러 처럼 반환하여도 된다.
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
log.error("[exceptionHandler] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX",e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(AException.class, BException.class) 이렇게 여러 예외를 묶어서 처리할 수도 있다. 아마도 둘 중 하나의 예외가 저리로 들어오면 처리되는 듯 하다.
이건 인자도 컨트롤러 처럼 받을 수 있다. 정말 하나의 컨트롤러라고 생각하면 된다. return 타입을 String으로 해 놓으면 똑같이 뷰템플릿을 호출할 수도 있다.
하긴 request, response 거쳐서 가는 거니까. 일종의 ModelAndView 반환하는 느낌도 있다.
하지만 받을 수 있는 인자나, 반환타입은 컨트롤러 보단 적다. 아마 예외처리할 때 필요한 정보들만 인자로 받거나 반환하거나 할 수 있는 듯 하다.
그래도, 컨트롤러와 유사 하므로 이 컨트롤러에서 예외가 발생하면 호출되는 컨트롤러 라고 생각하면 된다.
근데 이미 MVC는 BasicErrorController가 있어서, 이건 거의 API만 쓴다고 생각하면 된다.
'스프링 > 4. 스프링 MVC-2' 카테고리의 다른 글
75. 스프링의 자동 타입 변환 (0) | 2023.09.22 |
---|---|
74. ControllerAdviece (0) | 2023.09.17 |
72. 스프링 내부에서 발생하는 예외를 처리해주는 Resolver (0) | 2023.09.15 |
71. 스프링이 자동등록해주는 ExceptionResolver (0) | 2023.09.15 |
70. HandlerExceptionResolver로 API 예외 처리 (0) | 2023.09.13 |