컨트롤러에서 예외가 발생하면 WAS까지 거슬러올라감 (500 에러 발생)
WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 컨트롤러로 다시 돌아오면서 필터, 서블릿, 인터셉터가 모두 다시 호출된다. 너무 비효율적이므로 뒤에서 처리방법을 배운다
text/html
인 경우에는 Html을 호출해서 view를 제공하고,application/json
인 경우에는 BasicErrorController가 제공하는 기본 정보들을 활용해서 오류 API를 생성해 json데이터로 반환해준다.@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, // 오류코드 변경
ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
@ExceptionHandler
을 처리한다. API 예외 처리는 대부분 이 기능으로 해결한다. @ExceptionHandler를 가진 모든 메서드는 해당 타입의 예외를 파라미터로 전달받을 수 있다.@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
// IllegalArgumentException를 파라미터로 전달받음
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 ExceptionResolver가 예외처리가 일치하는 @ExceptionHandler를 찾아서 실행한다. (그 자식예외까지 처리)@ControllerAdvice, @RestControllerAdvice
를 사용하면 정상코드와 예외처리코드를 분리할 수 있다. 따로 클래스를 만들어서 @ExceptionHandler메서드를 모아놓으면 됨return값도 JSON으로 바꾸고 정상흐름인 HTTP상태코드 200코드로 반환하고 끝냄, 다른 상태코드를 반환하고 싶으면 @ResponseStatus 사용(클래스, 메서드레벨)
@ResponseStatus
(value = HttpStatus.NOT_FOUND)@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
// throw new BadRequestException(); 하면 HttpStatus.BAD_REQUEST와 메시지가 나옴,
reason은 MessageSource기능도 적용가능
@ResponseStatus 는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다. 또한 어노테이션을 사용하기 때문에, 조건에 따라 동적으로 변경하는 것도 힘들다.
이럴때는 ResponseStatusException
또는 ResponseEntity<>
를 사용한다
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
IllegalArgumentException());
}
@ExceptionHandler // 예외를 생략하면 메서드 파라미터의 예외가 지정된다
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
DefaultHandlerExceptionResolver (우선 순위가 가장 낮다.)
DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다. 대표적으로는 파라미터 바인딩 시에 발생하는 TypeMismatchException
파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로, 500오류가 아닌 400오류로 변경해서 WAS에서 오류페이지를 호출하도록 설계되어 있다.
- HTTP 요청 파라미터는 모두 문자로 처리된다. 따라서 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하고 싶으면 타입을 변환하는 과정을 거쳐야 한다.
@RequestParam
@ModelAttribute
@PathVariable
th:field
모두 자동으로 형변환을 해준다.
만약 개발자가 새로운 타입을 만들어서 변환하고 싶으면 컨버터 인터페이스를 사용하면 된다. ex) Json문자열을 객체에 바로 담고 싶은 경우
@Slf4j
public class StringToIntegerConverter implements Converter<String, Stuendent> {
@Override
public Integer convert(String source) {
log.info("convert source={}", source);
return ObjectMapper.readValue(source, Student.class)
}
}
@Configuration // 만든 컨버터를 포메터에 등록
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIntegerConverter());
registry.addConverter(new IntegerToStringConverter());
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
//스프링에서 사용자가 직접 정의한 타입 외의 거의 모든 타입에 대한 컨버터를 제공하고 있기 때문에,
이런걸 만들 필요는 없음
}
@GetMapping("/converter-view")
public String converterView(Model model) {
model.addAttribute("number", 10000);
model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
return "converter-view";
}
<li>${number}: <span th:text="${number}" ></span></li>
<li>${{number}}: <span th:text="${{number}}" ></span></li>
<li>${ipPort}: <span th:text="${ipPort}" ></span></li>
<li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li>
• ${number}: 10000 (문자) -> 컨버터 표현식을 쓰지 않았지만, 스프링이 자동으로 컨버터를 적용
• ${{number}}: 10000 (문자)
• ${ipPort}: hello.typeconverter.type.IpPort@59cb0946
• ${{ipPort}}: 127.0.0.1:8080
타임리프는 ${{...}}
를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력해준다. -> 문자로 자동 컨버트해서 출력 , Formatter와 함께 사용할 경우 원하는 포맷의 형식으로 컨버팅해서 출력
Formatter는 문자에 특화(객체 -> 문자, 문자 -> 객체) + 현지화(Locale)
예)화면에 숫자를 출력해야 하는데, 숫자 1000을 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력 또는 "1,000" 라는 문자를 1000 이라는 숫자로 변경
날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황
@NumberFormat
: 숫자 관련 형식 지정 포맷터 사용
@DateTimeFormat
: 날짜 관련 형식 지정 포맷터 사용
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
<li>${form.number}: <span th:text="${form.number}" ></span></li>
<li>${{form.number}}: <span th:text="${{form.number}}" ></li>
<li>${form.localDateTime}: <span th:text="${form.localDateTime}"</li>
<li>${{form.localDateTime}}: <span th:text="${{form.localDateTime}}"
• ${form.number}: 10000 -> 자동 형변환으로 숫자가 문자로 바뀌었지만 @NumberFormat(pattern = "###,###")는
적용되지않았음
• ${{form.number}}: 10,000 -> Formatter까지 적용됨
• ${form.localDateTime}: 2021-01-01T00:00:00 -> 형변환은 됐지만 Formatter 적용안됨
• ${{form.localDateTime}}: 2021-01-01 00:00:00 -> Formatter 적용됨