예외처리, Converter, Fommater

Seung jun Cha·2022년 6월 23일
0
post-thumbnail

1. 예외처리 흐름(HTML)

  1. 컨트롤러에서 예외가 발생하면 WAS까지 거슬러올라감 (500 에러 발생)

  2. WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 컨트롤러로 다시 돌아오면서 필터, 서블릿, 인터셉터가 모두 다시 호출된다. 너무 비효율적이므로 뒤에서 처리방법을 배운다

2. 스프링부트 오류페이지

2-1 개념

  • 스프링부트는 ErrorPage 를 자동으로 등록한다. 이때 /error 라는 경로로 기본 오류 페이지를 설정한다.
  • 스프링부트는 ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러인 BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록한다.
    즉, 오류가 발생했을 때 BasicErrorController가 작동하여 /error 폴더에서 해당하는 status오류의 파일을 자동으로 가져온다.
    1. 뷰 템플릿
    • resources/templates/error/500.html
    • resources/templates/error/5xx.html
    1. 정적 리소스( static , public )
    • resources/static/error/400.html
    • resources/static/error/404.html
    • resources/static/error/4xx.html
    1. 적용 대상이 없을 때 뷰 이름( error )
    • resources/templates/error.html

3. API 예외처리

3-1 개념

  • Accept 해더 값text/html 인 경우에는 Html을 호출해서 view를 제공하고,
    application/json 인 경우에는 BasicErrorController가 제공하는 기본 정보들을 활용해서 오류 API를 생성해 json데이터로 반환해준다.
    (API 오류는 상황에 따라 엄청 복잡하기 때문에, BasicErrorController를 사용하지 말자)

3-2 HandlerExceptionResolver

  • 컨트롤러에서 예외가 발생하면 ExceptionResolver에서 예외해결을 위한 시도를 하고, 해결되면 정상호출 됨. ExceptionResolver로 예외를 해결해도 postHandle() 은 호출되지 않는다.
@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;
 	}
}
  • 반환 값에 따른 동작 방식
    HandlerExceptionResolver 의 반환 값에 따른 DispatcherServlet 의 동작 방식
    • 빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지않고, 정상 흐름으로 서블릿이 리턴된다. WAS가 변경된 오류코드에 맞는 /error 페이지를 찾아서 호출
    • ModelAndView 지정: ModelAndView에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링한다.
    • null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.

3-3 스프링이 제공하는 HandlerExceptionResolver

  1. ExceptionHandlerExceptionResolver : @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메서드를 모아놓으면 됨
    @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)
  • return값도 JSON으로 바꾸고 정상흐름인 HTTP상태코드 200코드로 반환하고 끝냄, 다른 상태코드를 반환하고 싶으면 @ResponseStatus 사용(클래스, 메서드레벨)

    1. ResponseStatusExceptionResolver : HTTP 상태 코드를 지정해준다.
      예) @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);
}
  1. DefaultHandlerExceptionResolver (우선 순위가 가장 낮다.)
    DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다. 대표적으로는 파라미터 바인딩 시에 발생하는 TypeMismatchException

    파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이므로, 500오류가 아닌 400오류로 변경해서 WAS에서 오류페이지를 호출하도록 설계되어 있다.

4. 타입 컨버터, Formatter

4-1 컨버터 생성, 등록

- 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());

 //스프링에서 사용자가 직접 정의한 타입 외의 거의 모든 타입에 대한 컨버터를 제공하고 있기 때문에, 
 이런걸 만들 필요는 없음
 }

4-2 뷰템플릿에 컨버터 적용

  • 타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다. (view는 기본적으로 문자를 출력)
    이번에는 객체를 문자로 변환하는 작업을 확인할 수 있다.
@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와 함께 사용할 경우 원하는 포맷의 형식으로 컨버팅해서 출력

4-3 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 적용됨
  • 메시지 컨버터( HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는다. 즉, 객체와 JSON의 변환과정에서는 형변환이 자동으로 이루어 지지않는다. JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.

0개의 댓글