Converter

 

HTTP 요청 파라미터는 모두 문자로 처리되어서 다른 타입으로 받고 싶은 경우 직접 변환을 해야 되는데 스프링은 용도에 따라 다양한 방식의 타입 컨버팅을 제공한다. 하지만 수많은 타입 컨버터를 하나하나 찾는 것은 너무 불편하기 때문에 스프링은 추가로 컨버터를 편리하게 사용할 수 있는 ConversionService를 제공한다.

 

ConversionService 인터페이스는 단순하게 canConvert(), convert()로 되어 있고 직접 만든 컨버터를 등록할 수도 있는데

WebMvcConfigurer가 제공하는 addFormatters()를 통해 직접 만든 컨버터를 등록하면 스프링이 내부에서 사용하는 ConversionService에 추가가 되는데 직접 등록한 컨버터의 경우 스프링 기본 컨버터들보다 높은 우선 순위를 가진다.

 

 

ConversionService 적용 예시

 

1) 스프링MVC가 제공하는 @RequestParam 의 경우 컨트롤러의 파라미터를 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.

 

2) 타임리프는 변수 표현식에 괄호를 하나 더 넣어서 ${{...}}를 사용하면 ConversionService를 적용해서 변환된 결과를 출력해주는데 th:field의 경우 자동으로 ConversionService를 적용해준다.

 

 

Formatter

 

Converter는 타입을 변환하는 범용 기능을 제공하는데 실제로는 단순 범용적인 변환보다 숫자 1000을 문자 "1,000"으로 쉼표를 넣어 출력하거나 날짜 객체를 "22-06-17" 같이 출력하는 등의 처리를 해야 되는 경우가 많다. 이렇게 객체를 특정 문자 포멧에 맞추어 출력하는데 특화된 기능을 하는 것이 Formatter이다. (문자 변환에 특화)

 

하지만 Formatter도 크게 보면 객체를 문자로, 문자를 객체로 변환하는 정교한 컨버터일 뿐인데 포맷터를 지원하는 CovnersionService를 사용하면 포맷터도 컨버전 서비스에 추가할 수 있다.

 

스프링 부트는 포맷터를 추가할 수 있는 DefaultFormattingConversionService 를 상속 받은 WebConversionService를 내부적으로 사용한다. (Web)

 

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수많은 포맷터를 기본으로 제공하는데 포맷터는 기본 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기가 어려워서 애노테이션 기반의 매우 유용한 포맷터 2개를 기본으로 제공한다.

 

  • @NumberFormat(pattern = "###,###") : 숫자 관련 형식 지정 포맷터 사용
  • @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") : 날짜 관련 형식 지정 포맷터 사용

 

주의)

메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는데 특히 객체를 JSON으로 변환할 때 쓰이는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용하는데 이때 객체를 JSON으로 변환하는 것은 이 라이브러리에 달린 것이기 때문에 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶은 경우 Jackson 라이브러리가 제공하는 설정에 따라 포맷을 지정해야 한다.

 

컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용 가능

 

 

HTML은 HTTP 상태 코드에 따른 오류 페이지로 대부분 해결이 되지만 API의 경우에는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 반환해야 하기 때문에 더 복잡하다.

 

BasicErrorController

스프링부트가 제공하는 BasicErrorController에는 errorHtml(), error() 두개의 메서드가 있는데 클라이언트 요청이 Accept 헤더 값이 text/html인 경우에는 errorHtml()이 호출이 되고 그 외에는 error()가 호출이 되어 ResponseEntity로 HTTP Body에 JSON 데이터를 반환한다. 하지만 API 오류 처리는 각가의 경우에 따라 응답 결과를 다양하게 출력해야 되기 때문에 BasicErrorController보다는 @ExceptionHandler를 사용하는 것이 좋다.

 

HandlerExceptionResolver -> ExceptionResolver -> @ExceptionHandler

 

스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다.

DispatcherServlet에서 예외를 받아서 처리를 하고 정답 응답으로 바꿔 놓을 수 있다. (인터셉터의 postHandle()은 호출되지 않음)

 

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
    public ModelAndView resolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
             }
         } catch (IOException e) {
             ...
         }
         return null;
     }
}

 

ModelAndView를 반환하는 이유는 예외를 처리해서 정상 흐름처럼 변경하는 것이 목적이기 때문이다.

DispatcherServlet은 HandlerExceptionResolver의 반환 값(ModelAndView)에 따라 다음과 같이 동작한다.

 

  • 빈 ModelAndView : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴한다.
  • ModelAndView 지정 : ModelAndView에 View, Model 등의 정보를 담아서 반환하고 뷰를 렌더링한다.
  • null : 다음 ExceptionResolver를 찾아서 실행하고, 없을 경우 기존에 발생한 예외를 서블릿 밖으로 던진다

 

WAS까지 예외가 전달이 되고, WAS에서 오류 페이지 정보를 찾아서 다시 오류 페이지를 호출하는 과정은 너무 복잡한데 HandlerExceptionResolver를 통해 DispatcherServlet에서 예외를 처리하면 서블릿 컨테이너까지 예외가 전달되지 않아 WAS 입장에서는 정상 처리가 된 것처럼 만들 수 있다. 서블릿 컨테이너까지 예외가 올라가면 추가 프로세스가 복잡하게 실행되기 때문에 성능상 이점도 있는 것이다.

이제 RuntimeException을 상속 받은 예외 클래스를 생성하고 해당 예외를 처리하는 HandlerExceptionResolver를 만든 다음 WebMvcConfigurer에서 extendHandlerExceptionResolvers를 Override해서 resolver.add로 등록할 수 있다.

 

// WebMvcConfigurer 를 통해 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
}

 


 

스프링 ExceptionResolver

 

위에서처럼 HandlerExceptionResolver를 직접 만들어 response에 데이터를 넣고, ModelAndView를 반환하는 것은 너무 불편하다.

스프링부트가 제공하는 ExceptionResolver는 HandlerExceptionResolverComposite에 다음과 같이 우선 순위 순으로 ExceptionResolver를 등록한다.

 

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

 

스프링이 제공하는 ExceptionResolver를 통해 @ControllerAdvice, @RestControllerAdvice로 예외 처리를 하나의 클래스로 묶고 @ExceptionHandler와 @ResponseStatus를 통해 예외 처리를 메서드로 편리하게 관리할 수 있다.

 

 

1. ExceptionHandlerExceptionResolver

스프링은 @ExceptionHandler를 이용한 편리한 예외 처리 기능을 제공하는데 웹 화면에 HTML 오류 화면을 제공하는 경우는 BasicErrorController를 사용하는 것이 편하지만 API 예외의 경우는 복잡하기 때문에 세밀하게 설정할 필요가 있다.

 

@ExceptionHandler 애노테이션을 선언하고 처리하고 싶은 예외를 지정하면 해당 컨트롤러에서 예외 발생 시 ExceptionHanlderExceptionResolver는 해당 예외를 처리할 수 있는 @ExceptionHandler가 있는지 확인하고 실행하여 예외를 처리한다. ErrorResult를 반환하며 @ResponseStatus와 같이 사용할 수 있다. @ExceptionHanlder에 예외를 지정하지 않으면 해당 메서드의 파라미터 예외를 사용하며 @ResponseStatus 대신 ErrorResult를 ResponseEntity로 감싸서 HTTP 응답 코드를 동적으로 변경하여 줄 수도 있다.

컨트롤러에 기존 메서드와 예외 처리 @ExceptionHanlder 메서드가 섞여있는데 @ControllerAdvice, @RestControllerAdvice를 사용하면 이를 분리할 수 있다. @ExceptionHandler를 적용할 컨트롤러 또는 패키지를 지정하거나 아무 것도 지정하지 않으면 모든 컨트롤러에 글로벌ㄹ 적용할 수 있다.

 

2. ResponseStatusExceptionResolver

@ResponseStatus 애노테이션으로 상태 코드와 reason을 적용하면 해당 예외가 발생하여 컨트롤러 밖으로 넘어갈 때, ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인하고 오류 코드, 메시지를 적용한다. 결국 내부적으로 response.sendError() 를 호출하기 때문에 WAS에서 다시 오류 페이지(/error)를 내부 요청한다.

@GetMapping("/")
public String responseStatusEx() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

 reason은 MessageSource에서 찾는 기능도 제공하기 때문에 다음과 같이 messages.properties를 통해 사용할 수 있다.

error.bad=잘못된 요청 오류입니다. 메시지 사용

 

@ResponseStatus 애노테이션은 코드를 수정할 수 없는 라이브러리 예외 코드나 조건에 따라 동적으로 변경하고 싶은 경우 사용하기 힘들기 때문에 이런 경우는 ResponseStatusException을 직접 throw 하는 것이 좋다.

 

 

3. DefaultHandlerExceptionResolver

 

스프링 내부에서 발생하는 스프링 예외를 해결하는데 파라미터 바인딩 시점에 타입이 맞지 않는 경우 내부적으로 발생하는 TypeMismatchException의 경우 서블릿 컨테이너로 전달되어 500 오류가 발생하지만 파라미터 바인딩은 대부분 클라이언트에서 잘못 호출해서 발생하기 때문에 400오류가 더 적절하다. 이런 경우에 DefaultHandlerExceptionResolver는 이것을 400 오류로 변경한다. DefaultHandlerExceptionResolver도 내부적으로 sendError를 사용하며 스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있다.

 

 

 

 

자바의 main 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행되고 실행 도중 예외를 잡지 못해서 main 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 쓰레드가 종료된다.

 

웹어플리케이션은 사용자 요쳥별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 만약에 예외가 발생했는데 컨트롤러에서 예외를 잡지 못하고 WAS까지 예외가 전달이 되면 서블릿 컨테이너가 기본으로 제공하는 오류 화면이 호출된다.

 

서블릿과 서블릿 컨테이너

더보기

 CGI 웹 서버와 웹 애플리케이션 서버(WAS) 간에 데이터를 주고 받기 위한 규악으로 서버 프로그램은 CGI 규약에 따라 클라이언트의 데이터를 환경변수로 전달하고, 프로그램의 표준 출력 결과를 클라이언트에게 전송한다.

 서블릿은 자바 웹 서버가 동적인 페이지를 생성할 수 있도록 클라이언트의 요청을 처리하고, 결과를 반환하는 자바 웹 프로그래밍 기술로 흔히 자바로 구현된 CGI 라고 한다. 서블릿은 독립적으로 실행되지 않으며 서블릿 컨테이너에 의해 생명주기 관리가 되는데 대표적인 오픈 소스 서블릿 컨테이너로 톰캣(WAS)이 있다.

 

 

 

 서블릿 컨테이너는 클라이언트와 서버 간의 소켓 통신에 필요한 TCP/IP 연결, HTTP 프로토콜 해석 등의 네트워크 기반 작업을 추상화해 API로 제공하여 복잡한 과정을 생략하고 개발자가 비즈니스 로직에 집중하게 도와준다.

 

 서블릿 컨테이너는 서블릿 인터페이스(javax.servlet.Servlet)를 구현하여 GenericServlet 이라는 추상 클래스를 제공하는데 이는 service() 메서드를 제외하고 대부분의 서블릿에 필요한 메서드를 구현한 일종의 서블릿 어댑터이다.

 

 일반적으로 알고 있는 서블릿인 HttpServlet은 GenericServlet을 상속 받아 추상 메서드인 service()를 HTTP 프로토콜 요청 메서드에 적합하게 구현한 서블릿으로 서블릿 컨테이너는 웹 서버로부터 요청을 받으면 HTTP 프로토콜로 request, response 객체를 생성하여 서블릿을 호출하고 service()를 실행한다.

 

 

참고)

https://yangbongsoo.gitbook.io/study/servlet_container 

https://mangkyu.tistory.com/14

https://12bme.tistory.com/555

 

 

오류가 발생했을 때, HttpServletResponse가 제공하는 response.sendError(HTTP StatusCode, ErrorMessage)를 사용해서 서블릿 컨테이너에게 오류가 발생했다는 것을 전달할 수도 있다. 서블릿 컨테이너는 클라이언트에게 응답을 보내기 전 response 객체에 sendError()가 호출되었는지 확인하고 오류 페이지를 보여준다.


 

서블릿 컨테이너가 제공하는 기본 예외 처리 화면 대신 서블릿이 제공하는 오류 화면 기능을 사용하면 고객 친화적으로 오류 화면을 보여줄 수 있다. WebServerFactoryCustomizer의 customize를 오버라이드해서 오류 페이지를 등록할 수 있는데 HTTP 상태 코드와 오류 페이지 url로 ErrorPage를 생성해서 등록하고 오류 페이지를 처리할 컨트롤러를 생성하면 된다. (스프링 부트 자동화)

 

WAS는 ErrorPage를 확인하고 오류 정보를 request의 attribute에 담아서 오류 페이지 url로 다시 요청을 하는데 서버 내부에서 추가적인 호출을 하는 것이기 때문에 클라이언트는 알 수가 없다. 오류 페이지를 요청할 때 필터, 서블릿, 인터셉터가 전부 다시 호출되는데 서블릿은 해당 요청이 정상 요청인지, 오류 요청인지 알 수 있는 추가 정보로 DispatcherType(ENUM)을 제공한다.

(DispatcherType = FORWARD, REQUEST, ERROR ..)

// 필터를 등록할때 추가로 DispatcherType 설정 가능
// 기본 값이 REQUEST라서, 별도의 설정이 없으면 정상 요청시에만 필터를 적용한다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);

필터는 위와 같이 등록 시 설정을 해주면 되지만 기본 설정이 오류 요청에는 필터를 적용하지 않는 것이라 문제가 없다. 인터셉터는 서블릿이 아닌 스프링이 제공하는 기능이기 때문에 DispatcherType을 이용할 수 없고 excludePathPatterns에 오류 페이지 경로를 넣어주면 된다.

 

스프링 부트는 ErrorPage를 '/error' 라는 기본 경로로 자동으로 등록하고 BasicErrorController라는 스프링 컨트롤러를 생성한다.

BasicErrorController는 기본 로직이 전부 등록되어 있기 때문에 해당 컨트롤러가 제공하는 규칙에 따라 오류 페이지를 등록만 해두면 된다. 그리고 오류 관련 정보를 model에 담아서 전달하기 때문에 뷰 템플릿은 이 값을 활용해서 출력할 수 있다.

옵션으로 오류 관련 정보를 보낼지 설정이 가능한데 실무에서는 오류 정보를 전송하는 것은 위험하기 때문에 사용하지 않는 것이 좋다.

<ul>
	<li>오류 정보</li>
	<ul>
        <li th:text="|timestamp: ${timestamp}|"></li>
        <li th:text="|path: ${path}|"></li>
        <li th:text="|status: ${status}|"></li>
        <li th:text="|message: ${message}|"></li>
        <li th:text="|error: ${error}|"></li>
        <li th:text="|exception: ${exception}|"></li>
        <li th:text="|errors: ${errors}|"></li>
        <li th:text="|trace: ${trace}|"></li>
	</ul>
	</li>
</ul>
server.error.include-exception=true
// never : 사용 x, always: 항상 사용, on_param : 파라미터가 있을 때 사용
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

 

 

  • 정적 리소스
    • resources/static/error/404.html (구체적인 것이 우선 순위)
    • resources/static/error/4xx.html (400대 오류 디폴트 처리)

 

  • 뷰 템플릿 (정적 리소스보다 우선 순위가 높다.)
    • resources/templates/error/

'Spring' 카테고리의 다른 글

[스프링 MVC2] 스프링 타입 컨버터  (0) 2022.06.17
[스프링 MVC2] API 예외 처리  (0) 2022.06.17
[스프링 MVC2] 스프링 인터셉터  (0) 2022.06.16
[스프링 MVC2] 서블릿 필터  (0) 2022.06.16
[스프링 MVC2] 쿠키와 세션  (0) 2022.06.16

인터셉터

 

서블릿 필터에 이어 웹과 관련된 공통 관심사를 해결할 수 있는 방법으로 스프링 MVC가 제공하는 인터셉터가 있다.

인터셉터는 컨트롤러 호출 직전에 호출이 되며 필터처럼 체인으로 구성이 되는 등 비슷한 부분이 많지만 필터에 비해 좀 더 편리하고 정교한 기능을 지원한다.

 

 

 

먼저, 인터셉터를 사용하려면. HandlerIntercepter 인터페이스를 구현해야 한다.

  • preHandle : 컨트롤러 호출 전 동작
  • postHandle : 컨트롤러 호출 후 동작, 컨트롤러 예외 발생 시 호출되지 않는다.
  • afterCompletion : 마지막에 항상 호출, 예외를 받아서 처리할 수 있다.

DispatcherServlet -> preHandle -> handle() -> Controller -> postHandle -> render() -> View -> afterCompletion

 

public class LogInterceptor implements HandlerInterceptor {
    public static final String LOG_ID = "logId";

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
    	Object handler) throws Exception {

        HttpSession session = request.getSession(false);

        //@Controller, @RequestMapping으로 핸들러 매핑을 할 경우, 핸들러 정보로 HandlerMethod가 넘어온다.
        if (handler instanceof HandlerMethod) {
            //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
            HandlerMethod hm = (HandlerMethod) handler;
        }
       
      	if (session == null || session.getAttribute("loginMember") == null) {
            redirect response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }

	@Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
    	Object handler, ModelAndView modelAndView) throws Exception {
        //ModelAndView를 받을 수 있다.
    }

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
    	Object handler, Exception ex) throws Exception {
      	// Exception을 받아서 처리하거나 넘길 수 있다.
        
	}
}

 

인터셉터는 WebMvcConfigurer가 제공하는 addInterceptors() 를 사용해서 등록할 수 있다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LogInterceptor())
                .order(1) // 필터와 마찬가지로 호출 우선 순위
                .addPathPatterns("/**") // URL 패턴
                .excludePathPatterns("/css/**", "/*.ico", "/error"); // 인터셉터 제외 패턴
            
            // 2번째 인터셉터 등록            
            registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
              	.excludePathPatterns("/", "/members/add", "/login", "/logout",
                	"/css/**", "/*.ico", "/error");
    }
}

 

필터보다 URL 패턴을 더욱 세밀하게 설정할 수 있다.

(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html)

 


 

ArgumentResolver를 활용한 로그인 회원 조회

 

직접 만든 ArgumentResolver를 추가해서 로그인 회원을 편리하게 조회할 수 있다.

ArgumentResolver는 핸들러 어댑터에서 핸들러(컨트롤러)를 호출할 때 컨트롤러의 파라미터, 애노테이션 정보를 기반으로 전달 데이터를 생성하는 역할을 한다.

(ArgumentResolver는 요청 값을 처리하고 응답 값을 변환하고 처리하는 것은 ReturnValueHandler가 한다.)

 

@Login 애노테이션을 새로 만들어서 컨트롤러 파라미터에 입력하고 직접 만든 ArgumentResolver에서 이를 이용할 수 있다.

 

//@GetMapping을 제거하고 @Login 애노테이션을 선언
//@GetMapping("/")
public String homeLogin(@Login Member loginMember, Model model) {
	...
}

//@Login 애노테이션 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {

}

 

스프링은 30개가 넘는 ArgumentResovler를 기본으로 제공하는데 여기에 직접 만든 ArgumentResolver를 추가할 수 있다.

HandlerMethodArgumentResolver의 supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()를 호출해서 실제 객체를 생성하고 컨트롤러 호출 시 전달할 수 있다.

 

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
      	boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

	@Override
	public Object resolveArgument(MethodParameter parameter, 
    	ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
        WebDataBinderFactory binderFactory) throws Exception {
		
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
		
        if (session == null) {
            return null;
        }
        // 컨트롤러 파라미터에 선언된 @Login Member loginMember에 member 객체를 전달
        return session.getAttribute("loginMember");
    }
}

 

생성한 ArgumentResolver는 다음과 같이 등록할 수 있다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
    ...
}

+ Recent posts