스프링 MVC 2
메시지, 국제화
- 스프링에서는 다양한 메시지를 한 곳에서 관리하는 기능인 메시지 기능과 메시지의 국제화를 할 수 있는 기능을 제공한다.
메시지
우리가 어떤 개발한 애플리케이션에서 상품 이름과 관련된 문구를 상품명이라고 정했다고 가정하자.
근데 요구사항이 변경되어 상품명 -> 상품이름으로 변경해야 되고, 이에 따라 변경해야되는 파일들이 수십개 이상이라면 하드코딩된 상품명을 모두 고쳐야하는 상황이 발생한다.
이런 상황을
messages.properties
에 key-value로 값을 설정한 뒤 그 값을 사용함으로써 해결 가능하다.1 2 3
item=상품 item.id=상품 ID item.itemName=상품명
국제화
- 메시지 기능은 국제화 기능 또한 제공한다.
- 만약, 영어와 한글을 지원하는 애플리케이션을 만들었다면, messages_en.properties(영어)와 messages.properties(한글) 두 파일을 만들어 관리하면 된다.
- 국제화 기능은 HTTP accept-language 헤더 값을 이용하는 등의 방법으로 사용자의 선호 언어에 맞춰 기능한다.
스프링 메시지 소스
스프링에서 메시지 관리 기능을 사용하기 위해선
MessageSource
를 스프링 빈에 등록시켜야된다.스프링 부트에서는 기본적으로 등록해주기 때문에 개발자가 따로 MessageSource를 구현할 필요는 없다. 별도의 설정을 하지 않으면 messages라는 이름으로 기본 등록된다.
MessageSource 사용 방법
1 2 3
//messages.properties hello=안녕 hello.name=안녕 {0}
정상 작동
1 2 3 4 5 6 7 8 9 10
@SpringBootTest public class MessageSourceTest { @Autowired MessageSource ms; @Test void helloMessage() { Stirng result = ms.getMessage("hello", null, null); assertThat(result).isEqualto("안녕"); } }
메시지가 없는 경우, 기본 메시지 설정
1 2 3 4 5 6 7 8 9 10
@Test void notFoundMessageCode() { assertThatThrownBy(() -> ms.getMessage("no_code", null, null)) .isInstanceOf(NoSuchMessageException.class); } @Test void notFoundMessageCodeDefaultMessage() { String result = ms.getMessage("no_code", null, "기본 메시지", null); assertThat(result).isEqualTo("기본 메시지"); }
메시지가 없는 경우 NoSuchMessageException이 발생하고, 메시지가 없어도 defaultMessage를 사용하면 기본 메시지가 반환된다.
매개변수 사용
1 2 3 4 5
@Test void argumentMessage() { String result = ms.getMessage("hello.name", new Object[]{"Spring"}, null); assertThat(result).isEqualTo("안녕 Spring"); }
매개변수를 사용해 치환이 가능하다. hello.name=안녕 {0} -> 안녕 Spring
타임리프에서의 사용법
1 2 3 4
<div th:text="#{hello}"/> //파라미터 hello.name=안녕 {0} //() 안에 원하는 값을 넣으면 치환이 가능 <p th:text="#{hello.name(...)}"></p>
#{…}으로 메시지를 편리하게 조회할 수 있다.
스프링 국제화
- 스프링에서 제공하는 MessageSource는 Locale 정보를 가지고 언어를 한다.
- 스프링은 Locale 선택 방식을 변경할 수 있도록
LocaleResolver 인터페이스
를 제공한다. - 스프링 부트는 기본으로 HTTP의 Accept-Language를 활용하는
AcceptHeaderLocaleResolver
를 사용하는데 만약 Locale 선택 방식을 바꾸고 싶다면 직접 LocaleResolver를 구현하면 된다.
Bean Validation
- 스프링에서는 요청 시 들어온 데이터를 검증할 수 있는 Validation을 제공한다.
- @Validated와 BindingResult를 사용해 검증을 할 수 있다.
- BindingResult를 통해 validation과 표시할 메시지는 errors.properties에 따로 정의해 스프링 메시지와 함께 사용 가능하다.
- 주의할 점은 API 설계시 @RequestBody를 사용할 때 주의해야한다.
- Form으로 넘어온 데이터의 경우 Type이 맞지 않을 때 TypeDismatch 오류가 BindingResult의 FieldError로 생성된다.
- 하지만 JSON으로 넘어온 데이터의 Type이 맞지 않을 시 HttpMessageConverter에서 요청 JSON으로 객체를 생성하지 못하기 때문에 BindingResult로 가기 전에 예외를 던진다.
- 이럴 때 HttpMessageConverter 단계에서 예외 발생 시 우리가 원하는 모양으로 만들기 위해 @ExceptionHandler을 사용해야한다.
쿠키와 세션
- 로그인 유지를 위해 보통 세션을 사용한다.
- 세션이 어떻게 동작하는지 알아보자.
- 세션에 대해 얘기 하기 전 쿠키에 대해 먼저 알 필요가 있다.
쿠키
로그인 시 쿠키를 사용할 시 Response에 쿠키 생성해 응답해주면 된다.
1 2 3 4 5 6 7
@PostMapping("/login") public String login(@ModelAttribute LoginForm from, HttpServlcetResponse response) { // member의 id를 찾는 로직 생략 Cookie idCooki = new Cookie("memberId", String.valueOf(loginMember.getId())); // 쿠키는 <String, String> response.addCookie(idCookie); return "redirect:/"; }
이와 같이 쿠키를 생성하면 각 url 요청시 헤더에 Cookie로 member의 id를 받아 어떤 유저가 로그인했는지 식별이 가능하다.
문제점 : 쿠키로도 충분히 로그인 유지가 가능하지만 심각한 보안 문제가 존재한다.
- 쿠키 값은 임의로 변경 가능
- 쿠키에 보관된 정보도 훔칠 수 있고, 해커가 쿠리를 한번 훔쳐가면 평생 사용 가능
해결책
- 쿠키에 중요한 값을 노출 X, 사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 Id를 매핑해서 인식. 그리고 서버에서 토큰을 관리
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 해야함(UUID 사용)
세션
위의 해결책을 모두 충족하는 것이 보통 우리가 사용하는 세션이다.
사실 세션은 Cookie를 기반으로 동작한다.
세션은 요청이 들어오면 사용자를 검증한 뒤 세션 저장소(Map 사용)에 UUID로 생성한 sessionId와 객체를 저장해준다.
그 다음 sessionId를 쿠키에 담아서 클라이언트에 보내준다.
결국 클라이언트와 서버는
쿠키로 연결
되어야 하고, 중요한 포인트는회원과 관련된 정보는 클라이언트에 전달하지 않는다
는 것이다.로그인 후 요청시에는 sessionId 쿠키를 항상 전달한다.
서버에서는 클라이언트가 전달한 sessionId 쿠키 정보로 세션 저장소에서 보관한 화원 정보를 사용하게 된다.
이로써 보완과 관련된 문제가 해결되었다.
세션의 생존 시간은 최근에 요청한 시간을 기준 업데이트 된다. HttpSession도 이런 방식으로 동작한다.
서블릿 필터와 스프링 인터셉터
- Spring은 공통적으로 필요한(공통 관심사) 여러 처리들을 도와줄 수 있는 기능들을 지원한다.
- 그 중 서블릿이 지원해주는 서블릿 필터와 스프링이 제공하는 스프링 인터셉터에 대해 알아보자.
- 이 둘의 특징으로는 Request 객체와 Response 객체를 제공해 웹과 관련된 처리를 편리하게 할 수 있다. (AOP와 다른 점)
Servlet Filter
서블릿 필터는 서블릿 컨테이너에서 제공하는 공통 처리 기능이다.
필터 흐름
1 2 3
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //필터에 걸릴 시 HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출 X)
필터 체인
1
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
필터 인터페이스
1 2 3 4 5 6 7
public interface Filter { public default void init(FilterConfig filterConfig) throws ServletException {}; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; public default void destroy() {}; }
- init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
- doFilter() : 요청이 올 때 마다 해당 메서드가 호출. 핵심 로직
- destroy() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출
필터 구현
1 2 3 4 5 6 7 8 9 10 11 12 13
public MyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 로직 생략 try { chain.doFilter(request, response); } catch (Exception e) { throw e; } finally { } } }
- 필터를 구현할 때 doFilter()를 제외한 메서드는 default 메서드로 구현하지 않아도 된다.
- doFilter() 에서는 필터링 로직을 구현한 뒤 chain.doFilter로 다음 필터가 있을 시 호출 해줘야 한다.
- 만약 필터가 하나일 때도 chain,doFilter()를 꼭 호출 해줘야 한다. 호출 하지 않는다면 다음으로 넘어가지 않는다. 즉, 서블릿이 띄워지지 않는다.
필터 설정
1 2 3 4 5 6 7 8 9 10 11
@Configuration public class WebConfig { @Bean public FilterRegistrationBean logFilter() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new LogFilter()); filterRegistrationBean.setOrder(1); filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } }
- 필터는 위와 같이 FilterRegistrationBean을 사용해 등록하면 된다.
- setFilter : 등록할 필터를 지정, setOrder : 필터 체인 순서 지정, addUrlPatterns : 필터를 적용한 URL 패턴 지정 등 다양한 메서드를 지원한다.
서블릿 필터 VS 스프링 시큐리티
필터와 시큐리티는 사용자
인증 및 인가 기능
을 구현하는데 쓰인다.- 스프링 시큐리티도 결국 서블릿 필터를 기반으로 동작한다.
- 하지만 서블릿 필터의 경우 설정과 구현에 다소 번거롭고, 구체적인 인증 및 인가 로직은 직접 구현해야된다. 또 다른 스프링 모듈과의 통합이 복잡할 수 있다.
- 이런 점을 편리하게 사용할 수 있게 스프링에서 인증 및 인가를 위한 스프링 시큐리티를 제공한다.
- 스프링 시큐리티는 더욱 강력한 보안 기능(CSRF 방지 등)을 제공하고 보다 높은 추상화를 제공해 설정이 편리하다.
스프링 인터셉터
스프링 인터셉터도 필터와 같이 공통 관심 사항 처리를 도와주는 기능이다.
둘 다 웹과 관련된 공통 관심 사항 처리를 도와주지만, 적용되는 순서와 범위, 사용 방법에 차이가 있다.
필터 VS 인터셉터
필터
: 공통된 보안 및 인증/인가, 모든 요청에 대한 로깅 또는 감사 등인터셉터
: 세부적인 보안 및 인증/인가 작업, API 호출에 대한 로깅 또는 감사, Controller로 넘겨주는 데이터의 가공 등
스프링 인터셉터 흐름
1
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터 제한
1 2
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링인터셉터 -> 컨트롤러 //로그인 사용자 HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
스프링 인터셉터 체인
1
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
스프링 인터셉터 인터페이스
1 2 3 4 5 6 7 8 9 10 11
public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception; default void afterCompletion(HttpServletRequest request, HttpServletResponse response, javaObject handler, @Nullable Exception ex) throws Exception; }
스프링 인터셉터 호출 흐름
예외 발생 시 흐름
- 컨트롤러에서 예외 발생 시 postHandle는 호출 되지 않음
- 하지만 afterCompletion은 예외와 상관 없이 호출 됨
- 예외와 무관하게 공통 처리를 하려면 afterCompletion을 사용하면 됨
구현
preHandle를 구현 시 우리가 작성한 핸들러들의 정보를 받아올 수 있게 handler를 파라미터로 제공한다.
하지만 다양한 핸들러들을 지원하기 위해 Object 클래스로 되어있어 HandlerMethod로 캐스팅 해주면 해당 컨트롤러의 다양한 정보들을 사용 가능하다.
1 2 3
if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다. }
HandlerMethod
: @RequestMapping이 붙은 메소드의 정보를 추상화한 객체로 해당 메소드의 여러 메타 정보들을 가지고 있다 (파라미터 정보, 리턴 값 등)
인터셉터 등록
1 2 3 4 5 6 7 8 9 10
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()) .order(1) .addPathPatterns("/**") .excludePathPatterns("/css/**", "/*.ico", "/error"); } }
- addInterceptors를 통해 등록하면 된다.
- 서블릿 필터와 다르게 url 패턴을 더욱 상세히 적용할 수 있다.
서블릿 예외 처리
서블릿 예외 처리에는 두 가지 종류가 있다.
Exception : 예외
웹 애플리케이션에서 예외가 발생 시 중간에 예외를 처리하는 로직이 없다면 WAS까지 예외가 던져지게 된다.
1
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
이럴 시 WAS는 기본으로 제공하는 해당 오류에 맞는 오류 페이지를 제공해준다.
response.sendError(HTTP 상태코드, 오류 메시지)
오류가 발생 시 예외를 던지지 않고 response.sendError() 메서드를 사용해도 된다.
이 메소드를 호출하면 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
1 2
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError())
response 내부에 오류가 발생했다는 상태를 저장한 뒤 WAS에서 sendError가 호출 되었다면 오류 코드에 맞춰 기본 오류 페이지를 제공한다.
하지만 이런 방식은 서블릿이 기본 제공하는 오류페이지가 나오기 때문에 우리가 직접 제공하는 오류 페이지를 설정할 필요가 있다.
서블릿 예외 처리 페이지
서블릿에 직접 오류페이지를 등록(ErrorPage 사용)하고 예외 발생 시 해당 페이지를 출력하는 기능을 제공한다
예외 발생 흐름
1
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
이렇게 WAS까지 예외가 전파되면 설정한 ErrorPage 정보를 확인 후 해당 에러 페이지를 응답하기 위해 해당 컨트롤러까지 다시 요청을 보낸다.
1 2 3
1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생) 2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/ 500) -> View
이런 방식으로 클라이언트는 서버 내부에서 이런 흐름이 일어난다는 사실을 전혀 모르게 할 수 있고, 오류 페이지를 응답 받을 수 있다.
위와 같은 방식 덕분에 에러에 대한 정보를 request 객체에 담아서 보낼 수 있다. (새로운 요청을 보내기 때문에 가능)
- javax.servlet.error.exception : 예외
- javax.servlet.error.exception_type : 예외 타입
- javax.servlet.error.message : 오류 메시지
- javax.servlet.error.request_uri : 클라이언트 요청 URI
- javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
- javax.servlet.error.status_code : HTTP 상태 코드
서블릿 예외 처리 - 필터
오류가 발생하면 오류 페이지 출력을 위해 WAS가 새로운 요청을 보낸다.
이런 과정에서 필터, 서블릿, 인터셉터도 모두 다시 호출된다. 이런 방식은 매우 비효율적이다.
결국 오류를 위한 요청인지, 클라이언트가 보낸 정상 요청인지 구분할 수 있어야 한다.
이런 문제를 해결하기 위해 서블릿은
DispatcherType
이라는 추가 정보를 제공한다.DispatcherType
1 2 3 4 5 6 7 8
//javax.servlet.DispatcherType public enum DispatcherType { FORWARD, //FORWARD 시 INCLUDE, //INCLUDE 시 REQUEST, // 클라이언트 요청 시 ASYNC, //서블릿 비동기 호출 시 ERROR //오류 요청 시 }
- 고객이 처음 요청하면 dispatcherType = REQUEST이고, 오류 페이지 요청 시는 dispatcherType = ERROR로 설정된다.
필터를 특정 DispatcherType에만 적용하고 싶으면 WebConfig에서 필터를 적용할 때 설정할 수 있다.
1 2 3 4 5 6 7 8
@Configuration public class WebConfig implements WebMvcConfigurer { @Bean public FilterRegistrationBean logFilter() { //... 기존 로직 filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR); //... }
- 이와 같이 설정하면 REQUEST와 ERROR 일 시 필터가 적용된다.
- 설정을 따로 하지 않으면 디폴트가 REQUEST이기 때문에 상황에 맞게 설정하면 된다.
서블릿 예외 처리 - 인터셉터
- 필터의 경우 등록 시 DispatcherType 필터링이 가능했다. 하지만 인터셉터의 경우 서블릿이 아니라 스프링이 제공하는 기능이다. 따라서 DispatcherType과 무관하게 항상 호출된다.
- 대신 인터셉터는 요청 경로에 따라서 추가나 제외가 가능하기 때문에 excludePathPatterns을 이용해 “/error”, “/error-page/**” 등과 같이 설정해 오류 발생 시 호출이 안되게 설정할 수 있다.
스프링 부트 예외 페이지
- 스프링 부트 이전에는 직접 오류 페이지를 등록하고 컨트롤러를 만드는 등 복잡한 과정을 거쳤다.
- 하지만 스프링 부트에서는 이런 과정들을 모두 기본으로 제공한다.
- BasicErrorController라는 클래스가 자동으로 빈에 등록되어 오류 요청들을 처리해준다.
- 뷰 선택 우선순위
- 뷰 템플릿
- resources/templates/error/500.html
- resources/templates/error/5xx.html 2. 정적 리소스( static , public )
- resources/static/error/400.html
- resources/static/error/404.html
- resources/static/error/4xx.html
- 적용 대상이 없을 때 뷰 이름( error )
- resources/templates/error.html
- 뷰 템플릿
- 위의 경로에 HTTP 상태 코드 이름의 뷰 파일들을 넣어두면 스프링 부트에서 알아서 처리해준다.
API 예외 처리
API는 특별한 예외 처리가 필요하다
API 요청의 경우 예외가 발생할 때 각각의 컨트롤러나 리소스에 따라 예외의 결과가 바뀌어야 하는 경우가 많이 발생한다.
그렇기 때문에 API 예외에는 세밀하고 복잡한 처리가 필요하다.
1 2
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
Controller에 위와 같은 produces = MediaType.APPLICATION_JSON_VALUE 를 붙이면 HTTP Header의 Accept 값이 application/json 일 때 이 메서드가 호출된다는 뜻이다.
이와 같은 방법으로 우리가 위에서 봤던 BasicErrorController도 API 예외 처리를 지원한다.
하지만 BasicErrorController에서 생성한 예외의 경우 API 예외를 처리하기에는 세밀한 처리를 지원하지는 않는다.
물론 BasicErrorController를 확장해서 JSON 메시지를 처리할 수도 있다.
하지만 BasicErrorController는 HTML 화면 처리할 때 더 적합하고, API 의 경우
@ExceptionHandler
를 사용하면 더 편리하게 처리가 가능하다.그렇다면 복잡한 API 오류는 어떻게 처리해야되는지 단계별로 알아보자.
HandlerExceptionResolver
스프링 MVC는 핸들러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.
그것이 바로
HandlerExceptionResolver
이다. 줄여서 ExceptionResolver라고 부른다.HandlerExceptionResolver 인터페이스
1 2 3
public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
HandlerExceptionResolver 예제
1 2 3 4 5 6 7 8 9 10 11 12
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; }
- ExceptionResolver가 ModelAndView를 반환하는 이유는 마치 try,catch 처럼 Exception을 처리해서 정상 흐름으로 변경하는 목적이 있기 때문이다.
반환 값에 따른 동작 방식
- 빈 ModelAndView : new ModelAndView와 같이 빈 ModelAndView를 반환하면 뷰를 랜더링하지 않고, 정상 흐름으로 서블릿이 리턴. 위의 예제에서는 sendError()를 사용해 오류 페이지를 요청
- ModelAndView 지정 : 리턴한 뷰와 모델을 기반으로 랜더링
- null : 다음 ExceptionResolver를 찾아서 실행. 많약 없으면 예외 처리가 안되고 기존에 발생한 예외를 서블릿 밖으로 던짐
ExceptionResolver 등록
1 2 3 4 5
@Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { resolvers.add(new MyHandlerExceptionResolver()); }
ExceptionResolver 활용
- 예외 상태 코드 변환 : response.sendError()를 호출해 상태코드를 변경, 이후 WAS에서는 해당 코드에 맞는 오류 페이지 호출
- 뷰 템플릿 처리 : ModelAndView에 값을 채워서 예외에 따른 새로운 뷰 랜더링
- API 응답 처리 : response.getWriter().println();와 같이 HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능. JSON으로 응답하면 API 응답 처리를 할 수 있다.
ExceptionResolver를 활용하면 WAS에 예외가 올라가기 전에 Resolver에서 예외를 정상 흐름으로 돌리는 등 다양한 기능이 제공된다.
- 하지만 직접 구현하기에는 너무 복잡한 부분이 많다. 스프링에서는 쉽게 사용 가능한 ExceptionResolver들을 제공해준다.
스프링이 제공하는 ExceptionResolver
ExceptionHandlerExceptionResolver
: @ExceptionHandler 처리, API 예외 처리는 대부분 이 기능으로 해결, 따로 파트를 나눠서 설명ResponseStatusExceptionResolver
: HTTP 상태 코드 지정@ResponseStatus가 달려 있는 예외 처리
1
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
예외 예외 클래스 위에 @ResponseStatus 어노테이션이 붙은 예외의 상태 코드와 메시지(reason)을 변경해준다.
reason의 값은 messages.properties의 있는 값으로 설정도 가능하다.
1 2 3
reason = "error.bad" //messages.properties error.bad=잘못된 요청입니다.
ResponseStatusException 예외 처리
만약 라이브러리에 포함된 예외 등 @ResponseStatus를 못 붙이는 상황에는 ResponseStatusException을 사용하면 된다.
1 2 3
public String responseStatusEx2() { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException()); }
DefaultHandlerExceptionResolver
: 스프링 내부 기본 예외 처리- DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 예외들을 처리한다.
- 만약 파라미터 바인딩 시점에서 TypeDismatchException이 발생하는데 이런 예외를 DefaultHandlerExceptionResolver 내부에서 처리해준다.
- DefaultHandlerExceptionResolver를 직접 찾아가보면 수많은 스프링 내부 예외를 처리해주는 것을 확인할 수 있다.
API 예외 처리 - @ExceptionHandler
웹 브라우저에서 HTML 화면을 제공할 때 오류가 발생하면 BasicErrorController를 사용하는게 편함
하지만 API 예외는 고려해야되는 사항이 많고 세밀한 처리가 필요하다.
지금까지 살펴본 BasicErrorController와 HandlerExceptionResolver를 직접 구현해서 API 예외를 다루기 쉽지 않다.
API 예외처리의 어려움 점
- HandlerExceptionResolver은 ModelAndView를 반환한다. API는 필요 X
- 같은 예외라도 다른 방식으로 처리하고 싶다면?
@ExceptionHandler
스프링은 이러한 API 예외 처리 문제를 해결해주는 어노테이션을 지원해준다.
1 2 3 4 5 6 7
//예외 발생 시 응답으로 사용할 객체 @Data @AllArgsConstructor public class ErrorResult { private String code; private String message; }
1 2 3 4 5 6
@ResponseStatus(HttpStatus.BAD_REQUEST) //상태 코드 지정 - 없을 시 200으로 설정 @ExceptionHandler(IllegalArgumentException.class) // 처리할 예외 지정 public ErrorResult illegalExHandle(IllegalArgumentException e) { log.error("[exceptionHandle] ex", e); return new ErrorResult("BAD", e.getMessage()); }
다음과 같이 예외를 처리할 각 컨트롤러에 정의하면 된다.
만약 해당 컨트롤러에서 예외가 발생 시 ExceptionHandlerExceptionResolver에서 컨트롤러 안에 @ExceptionHandler 어노테이션이 붙은 메소드를 찾고 처리가 가능한 예외라면 해당 메소드를 호출해준다.
REST 컨트롤로의 응답과 비슷하게 동작하므로 ResponseEntity 등으로 구현해도 된다.
1 2 3 4 5 6
@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); }
####
@ControllerAdvice
위의 코드 @ExceptionHandler를 적용시키려면 예외 처리할 컨트롤러 안에 설정하면 된다고 했다.
하지만 이런 방식은
정상 코드와 예외 처리 코드가 한 컨트롤러에 있는 문제
가 있다.(관심사 분리)이럴 때 사용하는 것이 @ControllerAdvice이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
@RestControllerAdvice public class ExControllerAdvice { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResult illegalExHandle(IllegalArgumentException e) { log.error("[exceptionHandle] ex", e); return new ErrorResult("BAD", e.getMessage()); } @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); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler public ErrorResult exHandle(Exception e) { log.error("[exceptionHandle] ex", e); return new ErrorResult("EX", "내부 오류"); } }
위 코드와 같이 분리가 가능하다.
@ControllerAdvice
- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능을 부여해주는 역할을 한다.
- @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.
스프링 타입 컨버터
HTTP 요청 시 넘어오는 데이터는 모두 String 타입이다. 하지만 우리가 @ModelAttribute나, @RequestParam 등을 사용할 땐 Integer나 여러 타입으로 사용이 가능했다. 스프링은 이런 부분을 어떻게 처리할까?
정답은 스프링은 타입 컨버터라는 컨버터 인터페이스를 제공하고 이를 통해 타입을 변환해준다.
1 2 3
public interface Converter<S, T> { T convert(S source); }
제네릭으로 된 부분에 S는 변환할 값의 타입, T는 변환 결과의 타입을 지정해주면 된다.
ex) String -> Integer : Converter<String, Integer>
스프링은 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다.
하지만 이렇게 타입 컨버터를 하나하나 직접 찾아서 적용하기는 불편하다. 그래서 스프링은 컨버터들을 모아두고 편리하게 사용할 수 있는 기능을 제공한다. 바로 컨버전 서비스(ConversionService)이다.
ConversionService
ConversionService 인터페이스
1 2 3 4 5 6 7
public interface ConversionService { boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType); boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType); <T> T convert(@Nullable Object source, Class<T> targetType); Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); }
- 컨버팅이 가능한지 확인하는 기능과 컨버팅 기능을 제공한다.
ConversionService에 직접 구현한 Converter 등록
Config 파일에서 스프링 내부에 있는 ConversionService에 등록이 가능하다
1 2 3 4 5 6 7
@Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToIntegerConverter()); registry.addConverter(new IntegerToStringConverter()); registry.addConverter(new StringToIpPortConverter()); registry.addConverter(new IpPortToStringConverter()); }
ConversionService 처리 과정
- Config 파일에 등록하기만 했을 뿐인데 Converter는 스프링 내부에서 어떻게 처리될까?
- 스프링은 ArgumentResolver를 처리하는 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.
ConversionService 뷰 템플릿
뷰 템플릿에서도 랜더링 시 ConversionService를 사용해서 컨버팅이 가능하다
1 2
일반 변수 표현식 : ${...} 컨버전 서비스 적용 : $
포맷터 - Formatter
Converter는 범용 타입 변환 기능을 제공한다
하지만 웹 애플리케이션 환경에서는 단순히 객체를 변환하는 것보다 객체를 문자로, 문자를 객체로 바꾸는 일이 대부분이다. 예를 들어 다음과 같다.
- Integer -> String 출력 시점에서 숫자 1000 -> 문자 1,000 과 같이 쉼표를 넣어서 출력하거나 반대인 상황
- 날짜 객체를 문자인 “2024-05-31 xx:xx:xx”와 같이 출력하거나 반대인 상황
이렇게 객체를 특정한 포맷에 맞춰 컨버팅해주는 것이 포맷터이다. 포맷터는 컨버터의 특별한 버전으로 이해하면 된다.
Converter VS Formatter
- Converter : 범용 (객체 -> 객체)
- Formatter : 문자에 특화 (객체 -> 문자, 문자 -> 객체) + 현지화 (Locale)
Formatter 인터페이스
1 2 3 4 5 6 7 8 9 10
public interface Printer<T> { String print(T object, Locale locale); } public interface Parser<T> { T parse(String text, Locale locale) throws ParseException; } public interface Formatter<T> extends Printer<T>, Parser<T> { }
Formatter 등록
1 2 3 4 5 6 7 8 9 10
@Override public void addFormatters(FormatterRegistry registry) { //registry.addConverter(new StringToIntegerConverter()); //registry.addConverter(new IntegerToStringConverter()); registry.addConverter(new StringToIpPortConverter()); registry.addConverter(new IpPortToStringConverter()); //포맷터 추가 registry.addFormatter(new NumberFormatter); }
- StringToIntegerConverter와 IntegerToStringConverter는 주석 처리가 필요하다. 만약 Converter와 Formatter가 같은 타입을 지원하면 우선순위 상 컨버터가 우선되어 포맷터가 적용되지 않는다.
스프링 기본 포맷터 사용
스프링에서는 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 제공한다.
근데 위에서 알아봤듯이 컨버터와 포맷터가 같은 타입을 지원하면 컨버터가 적용된다고 했다.
그럼 특정 변수에 컨버터 대신 포맷터를 적용하기 싶다면 어떻게 해야될까?
스프링은 이런 문제를 해결하기 위해 어노테이션으로 원하는 형식을 지정할 수 있게 지원한다.
대표 어노테이션
- @NumberFormat : 숫자 관련 형식 지정 포맷터
- @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용
예)
1 2 3 4 5 6 7 8
@Data public class Member { @NumberFormat(pattern = "###,###") private Integer number; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime dateTime; }
주의!!!!
- 이런 컨버터나 포맷터는 메시지 컨버터(HttpMessageConverter)에는 적용되지 않는다.
- 즉, 객체를 JSON으로 파싱하는데 사용되는 메시지 컨버터는 HTTP 메시지 바디의 내용을 객체로 변환하거나 그 반대로 바꾸는 역할을 하고 그 내부에서 Jackson 같은 라이브러리를 사용한다.
- 때문에 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶다면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야한다.
- 결과적으로 이것은 컨버전 서비스와 전혀 관계가 없고, 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다
출처)
[스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 | 김영한 - 인프런 (inflearn.com)](https://www.inflearn.com/course/스프링-mvc-2) |