MockHttpServletRequest: HTTP Method = GET Request URI = /hello/1234 Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {}
Handler: Type = me.nimkoes.MyHelloController Method = public me.nimkoes.UserInfo me.nimkoes.MyHelloController.hello(int)
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/json;charset=UTF-8]} Content type = application/json;charset=UTF-8 Body = {"id":1234,"name":null} Forwarded URL = null Redirected URL = null Cookies = []
URI 정보에서 argument 를 추출해내는 것과 관련해서 @PathVariable 이외에 @MatrixVariable 이 있다.
MatrixVariable api 문서를 보면 이것 역시 @RequestMapping annotation 이 붙은 handler method 에 사용할 수 있고, URI path 상에 name-value 쌍으로 이루어진 내용을 참고해서 argument 로 사용할 수 있다.
자세한 내용은 문서를 보는게 가장 좋겠지만 쉽지 않은 일일 수 있기 때문에, 자주 사용하는 것 위주로 정리를 할 계획이다.
개인적으로 활용하는 파트도 중요하지만 그 앞의 원리와 설정에 대해 아는게 더 중요하다고 생각한다.
handler method 의 argument 와 return type 을 실제로 활용하는 방법에 대해 정리하기 앞서 누가 어디서 뭘 어떻게 했길래 가능한건지부터 살펴보려 한다.
디버그 모드로 서버를 실행한 다음 한줄씩 실행 과정을 따라가 보면서 확인해보기 위해 가장 기본적인 형태의 handler method 를 만들고 DispatcherServlet 클래스의 doService 메소드에 break point 를 걸어두었다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hi";
}
}
HTTP GET 요청으로 /hello 를 보내면 "hi" 라는 문자열을 HTTP response body 에 실어 보내는 handler method 이다.
DispatcherServlet 의 doService 메소드에 있는 doDispatch 안으로 들어간다.
하는 일을 보면 크게
1. 요청을 처리 할 handler method 찾기 :: getHandler
2. 요청을 처리할 수 있는 adapter 찾기 :: getHandlerAdapter
3. 실제로 요청을 처리하기 :: ha.handle
4. 응답으로 보낼 결과 처리하기 :: processDispatchResult
정도인 것으로 보인다.
지금은 실제로 handler method 를 처리할 때 argument 를 어떻게 처리하는지 알아보는 것을 목적으로 하고 있기 때문에
// Actually invoke the handler. 라고 되어있는 ha.handle 의 안쪽으로 들어가 봤다.
한줄씩 실행하다보니 관련된 것 같아 보이는 부분을 발견 하였다.
argument 를 처리할 수 있는 argumentResolver 와 return value 를 처리하는 returnValueHandlers 를 등록해 주는 부분이 있었다.
한가지 의문은 등록 해준적도 없는 저 구현체들은 어디서 나왔냐는 건데, 실제로 handler method 를 실행하는 invokeHandlerMethod 메소드는 RequestMappingHandlerAdapter 클래스에 정의되어 있는데, 이 클래스에 getDefaultArgumentResolvers 메소드와 getDefaultReturnValueHandlers 메소드에서 등록해주고 있다.
그리고 parameterNameDiscoverer 를 등록하는 부분이 있는데, 아무래도 paramter 의 이름을 찾는 일을 하는 것 같다.
실제로 등록되는 클래스를 보면 Reflection 이나 Local 변수와 관련된 Table 정보를 참고해서 parameter name 을 찾는 것 같아 보인다.
이렇게 전처리?를 다 하고나면, 정말로 handler method 를 실행하는 부분인 invocableMethod.invokeAndHandle 메소드 에서 요청 처리를 시작 한다.
invokeAndHandle 은 요청을 처리한 다음 앞서 등록한 returnValueHandlers 로 return value 를 처리하도록 되어 있다.
getMethodArgumentValues 로 args 들을 추출해낸 다음 main 메소드 실행하 듯 doInvoke 를 호출하는 것을 볼 수 있다.
this.resolvers 에는 앞서 default 로 자동 등록해준 26개의 argument 를 처리할 수 있는 구현체들의 LinkedList 이다.
지금 만든 handler method 는 parameter 가 없기 때문에 return EMPTY_ARGS 를 실행 하겠지만, 만약 그렇지 않았다면 처리 가능한 parameter 인지 검사하고, 처리 가능한 경우 arg 배열에 하나씩 그 값을 추가 한다.
각 구현체가 어떻게 argument 를 처리하는지 까지는 다루지 않고 어떻게 사용하는지에 대해서만 정리할 예정이다.
드디어 모든 준비가 끝나면 doInvoke(args) 를 실행해서 handler method 를 실행 한다.
java Reflection 을 사용해서 method 의 modifier 가 (접근 제한자) public 이 아닌 경우 public 접근이 가능하도록 수정한 다음 handler method 를 실행 한다.
테스트 해보기 위해 만든 handler method 의 반환 값인 "hi" 문자열이 returnValue 에 담겼고, 이제 returnValueHandlers 의 handleReturnValue 로 이 값을 처리한다.
이후 처리되는 과정도 한줄씩 실행해보면서 보았지만 더 깊은 내용을 정리하는건 좋지 않다고 생각했다.
지금은 DispatcherServlet 의 doService 메소드를 시작점으로 break point 를 잡았지만, 사실 DispatcherServlet 은 단지 하나의 Servlet 일 뿐이라는 것을 생각해보면 filter, interceptor 등 전후에 실행하는 Servlet lifecycle 을 따라야 한다는 것 정도만 기억하고 넘어가도 괜찮다는 생각이다.
아무튼, 기나긴 여정을 지나 실행 결과를 받아볼 수 있었다.
본 글을 마무리 하자면, 맨 위에 목록으로 정리 한 handler method 의 Argument 와 Return value 를 별다른 설정 없이 너무나 당연한 권리 찾듯 사용하는게 어떻게 가능했는지에 대해 간략하게 살펴 보았다.
어떻게 해서 사용할 수 있었는지 이제는 알기 때문에 어떻게 활용할 수 있는지에 대해 정리해 보려 한다.
MyHelloController 를 제외한 Person, PersonFormatter, GreetingInterceptor, AnotherInterceptor 를 삭제하고 WebConfig 의 Interceptor, ResourceHandler, Jaxb 설정도 삭제했다.
MyHelloControllerTest 클래스의 내용도 모두 삭제 했다.
MyHelloController 에 다음과 같이 handler method 를 하나 등록 하였다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}
@Controller annotation 을 사용해서 component scan 대상으로 만들고, 그 안에 @RequestMapping annotation 을 사용해서 uri 를 지정해주면 handler method 가 된다.
만약 11라인의 @ResponseBody 를 사용하지 않으면 return 하는 문자열에 해당하는 view 를 찾고, 사용 할 경우 반환하는 값을 HTTP response body 에 실어 응답 데이터로 보내게 된다.
추가로 @RequestMapping 을 사용해서 uri 만 지정할 경우 모든 종류의 HTTP method 를 허용하게 된다.
HTTP method 의 종류로는 GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 등이 있다.
만약 특정한 요청에 대해서만 허용하게 하려면 @RequestMapping annotation 에 값을 추가하면 된다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello() {
return "hello";
}
}
RequestMethod 는 enum 타입으로 HTTP request method 를 정의하고 있다.
/*
* Copyright 2002-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.bind.annotation;
/**
* Java 5 enumeration of HTTP request methods. Intended for use with the
* {@link RequestMapping#method()} attribute of the {@link RequestMapping} annotation.
*
* <p>Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet}
* supports GET, HEAD, POST, PUT, PATCH and DELETE only. DispatcherServlet will
* process TRACE and OPTIONS with the default HttpServlet behavior unless explicitly
* told to dispatch those request types as well: Check out the "dispatchOptionsRequest"
* and "dispatchTraceRequest" properties, switching them to "true" if necessary.
*
* @author Juergen Hoeller
* @since 2.5
* @see RequestMapping
* @see org.springframework.web.servlet.DispatcherServlet#setDispatchOptionsRequest
* @see org.springframework.web.servlet.DispatcherServlet#setDispatchTraceRequest
*/
public enum RequestMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
}
지금은 @RequestMapping annotation 을 사용해서 HTTP 의 어떤 타입의 요청을 처리할지 mehotd 값을 지정해 주었지만 한가지 타입의 요청만 처리하도록 한다면 @GetMapping 과 같은 다른 annotation 을 사용해서 코드를 간결하게 작성할 수 있다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
}
@GetMapping 을 포함하여 @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 등이 있다.
그리고 이 모든 HTTP request method 에 매칭하는 annotation 은 @RequestMapping annotation 을 meta annotation 으로 사용하고 있다.
@GetMapping 을 대표로 문서를 보면 @RequestMapping(method = RequestMethod.GET) 의 축약 표현이라고 정의되어 있다.
추가로 @RequstMapping 은 method 가 아닌 class 레벨에 작성할 수도 있다.
package me.nimkoes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(method = RequestMethod.GET)
public class MyHelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello() {
return "hello";
}
@PostMapping
@ResponseBody
public String hi() {
return "hi";
}
}
class 레벨에 @ReauestMapping 으로 GET 요청만 받을 수 있도록 정의 했기 때문에 이 안에 작성된 모든 handler 는 GET 요청만 처리할 수 있게 된다.
실제로 그런지 궁금해서 일부러 @PostMapping 을 추가하고 테스트 코드를 작성해봤다.
사용하기에 따라 굉장히 복잡한 규칙을 만들 수 있겠지만 그런 경우가 얼마나 있을까 싶은 생각이다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello?")
public String hello_case_1() { return "hello case 1"; }
@GetMapping("/hello/?")
public String hello_case_2() { return "hello case 2"; }
@GetMapping("/hello/??")
public String hello_case_3() { return "hello case 3"; }
@GetMapping("/hello/?/?")
public String hello_case_4() { return "hello case 4"; }
@GetMapping("/hello*")
public String hello_case_5() { return "hello case 5"; }
@GetMapping("/hello/*/test")
public String hello_case_6() { return "hello case 6"; }
@GetMapping("/hello/*/te*st")
public String hello_case_7() { return "hello case 7"; }
@GetMapping("/hello/**")
public String hello_case_8() { return "hello case 8"; }
@GetMapping("/hello/**/test")
public String hello_case_9() { return "hello case 9"; }
}
어떻게 사용할 수 있는지 테스트 해보기 위해 몇가지 경우만 만들어 보았다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello1")) .andExpect(content().string("hello case 1"));
mockMvc.perform(get("/hello!")) .andExpect(content().string("hello case 1"));
mockMvc.perform(get("/hello/z")) .andExpect(content().string("hello case 2"));
mockMvc.perform(get("/hello/nk")) .andExpect(content().string("hello case 3"));
mockMvc.perform(get("/hello/n/k")) .andExpect(content().string("hello case 4"));
mockMvc.perform(get("/hello")) .andExpect(content().string("hello case 5"));
mockMvc.perform(get("/helloTest")) .andExpect(content().string("hello case 5"));
mockMvc.perform(get("/hello/nimkoes/test")) .andExpect(content().string("hello case 6"));
mockMvc.perform(get("/hello/nk/te_some_st")) .andExpect(content().string("hello case 7"));
mockMvc.perform(get("/hello/nk/some")) .andExpect(content().string("hello case 8"));
mockMvc.perform(get("/hello/nk/some/another")) .andExpect(content().string("hello case 8"));
mockMvc.perform(get("/hello/nk/some/another/test")).andExpect(content().string("hello case 9"));
}
}
uri 패턴 매핑 방법 중에 확장자를 매핑하는 방법도 있다.
이 방법은 요청 uri 가 특정 파일의 확장자를 포함하는 경우인데 /hello.* 와 같은 uri 패턴을 사용할 경우 /hello.json, /hello.zip 등의 uri 를 사용할 수 있는 것을 뜻한다.
하지만 확장자를 사용한 uri 패턴을 사용할 경우 RFD 공격과 같은 보안 이슈가 있기 때문에 권장하는 방법이 아니며, Spring Boot 에서는 이런 uri 의 요청을 사용하지 못하도록 막고 있다.
@RequestMapping 또는 @RequestMapping 을 meta annotation 으로 사용하는 @GetMapping 과 같은 annotation 을 조금 더 자세히 보면 consumes 와 produces 라는 값을 설정할 수 있다.
// 생략...
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
// 생략...
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
// 생략...
/**
* The consumable media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Content-Type} matches one of these media types.
* Examples:
* <pre class="code">
* consumes = "text/plain"
* consumes = {"text/plain", "application/*"}
* </pre>
* Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Content-Type} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this consumes restriction.
* @see org.springframework.http.MediaType
* @see javax.servlet.http.HttpServletRequest#getContentType()
*/
String[] consumes() default {};
/**
* The producible media types of the mapped request, narrowing the primary mapping.
* <p>The format is a single media type or a sequence of media types,
* with a request only mapped if the {@code Accept} matches one of these media types.
* Examples:
* <pre class="code">
* produces = "text/plain"
* produces = {"text/plain", "application/*"}
* produces = MediaType.APPLICATION_JSON_UTF8_VALUE
* </pre>
* <p>It affects the actual content type written, for example to produce a JSON response
* with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
* all requests with a {@code Accept} other than "text/plain".
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings override
* this produces restriction.
* @see org.springframework.http.MediaType
*/
String[] produces() default {};
}
consumes 와 produces 모두 header 의 Mediatype 을 참조해서 handler 를 매핑하는데 사용한다.
차이가 있다면 consumes 는 HTTP 요청의 Content-Tpye 을 사용하고 produces 는 Accept 를 사용 한다.
package me.nimkoes;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public String hello() {
return "hello";
}
}
HTTP GET 요청을 받는 /hello handler method 는 consumes 로 APPLICATION_JSON_UTF8_VALUE 를 사용하고 있다.
사실 MediaType 타입의 값 대신 문자열을 사용해도 되지만 오타가 있을 경우 찾아내기 매우 어려울 수 있기 때문에 되도록 정의 된 상수를 사용하는걸 권장한다.
APPLICATION_JSON_UTF8_VALUE 는 "application/json;charset=UTF-8" 값이 할당되어 있다.
그래서 이 handler 는 HTTP GET 방식의 uri 가 /hello 여야 하기도 하지만, Content-Type 이 json 형식이라고 명시되어 있어야 매핑할 수 있다.
28라인에서 차라리 accept 를 설정하지 않았다면 테스트는 오히려 성공 했을 텐데, 이 요청에 대한 응답 결과 json 형식의 데이터를 기대한다고 명시했기 때문에 406 Not Acceptable 오류가 발생 한다.
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = {Content-Type=[application/json], Accept=[application/json]} Body = <no character encoding set> Session Attrs = {}
생략...
MockHttpServletResponse: Status = 406 Error message = null Headers = {} Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = []
생략...
java.lang.AssertionError: Status Expected :200 Actual :406
정리하면 @RequestMapping 또는 이 annotation 을 meta annotation 으로 사용하는 @GetMapping, @PostMapping 등을 사용할 때 consumes 와 produces 값을 추가로 설정할 수 있는데,
consumes 는 HTTP request header 의 Content-Type 을 참조하고, produces 는 HTTP request header 의 Accept 값을 참조 한다.
Content-Type 이 일치하지 않는 경우 415 Unsupported Media Type 오류가 발생하고
Accept 값이 일치하지 않는 경우 406 Not Acceptable 오류가 발생 한다.
@RequestMapping 에 설정할 수 있는 값으로 headers 가 있다.
// 생략...
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
// 생략...
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
// 생략...
/**
* The headers of the mapped request, narrowing the primary mapping.
* <p>Same format for any environment: a sequence of "My-Header=myValue" style
* expressions, with a request only mapped if each such header is found
* to have the given value. Expressions can be negated by using the "!=" operator,
* as in "My-Header!=myValue". "My-Header" style expressions are also supported,
* with such headers having to be present in the request (allowed to have
* any value). Finally, "!My-Header" style expressions indicate that the
* specified header is <i>not</i> supposed to be present in the request.
* <p>Also supports media type wildcards (*), for headers such as Accept
* and Content-Type. For instance,
* <pre class="code">
* @RequestMapping(value = "/something", headers = "content-type=text/*")
* </pre>
* will match requests with a Content-Type of "text/html", "text/plain", etc.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this header restriction (i.e. the type-level restriction
* gets checked before the handler method is even resolved).
* @see org.springframework.http.MediaType
*/
String[] headers() default {};
// 생략...
}
이 값은 HTTP request 에 특정한 header 가 있는 경우 또는 그 header 의 값이 특정한 값인 경우에만 handler 를 매핑해주고 싶을 때 사용할 수 있다.
package me.nimkoes;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@GetMapping(value = "/hello", headers = HttpHeaders.AUTHORIZATION)
@ResponseBody
public String hello() {
return "hello";
}
}
이 경우 HTTP request header 에 AUTHORIZATION 이 있는 경우 매핑될 수 있다.
MockHttpServletRequest: HTTP Method = HEAD Request URI = /hello Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {}
Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello()
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[5]} Content type = text/plain;charset=UTF-8 Body = Forwarded URL = null Redirected URL = null Cookies = []
MockHttpServletRequest: HTTP Method = OPTIONS Request URI = /hello Parameters = {} Headers = {} Body = <no character encoding set> Session Attrs = {}
Handler: Type = org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler Method = public org.springframework.http.HttpHeaders org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler.handle()
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = {Allow=[GET,HEAD,POST,OPTIONS]} Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = []
이것 역시 WebMvcConfigurer 인터페이스를 오버라이딩 해서 핸들러를 추가할 수 있다.
package me.nimkoes;
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor())
.addPathPatterns("/hi")
.order(0);
registry.addInterceptor(new AnotherInterceptor()).order(-1);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/m/**") // 어떤 패턴의 요청을 처리할지 정의
.addResourceLocations("classpath:/m/") // 리소스를 찾을 위치
.setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES)); // 리소스가 변경되지 않은 동안 캐싱을 유지할 시간
}
}
테스트 내용이 의도한 대로 동작 하였으며, 아까와 달리 Header 에 추가 등록한 Cache-Control 에 대한 max-age 값도 600초 (10분) 으로 정상적으로 설정된 것을 볼 수 있었다.
추가로 addResourceHandler 로 리소스 핸들러를 등록할 때, 리소스를 찾는 경로로 classpath 뿐만 아니라 file system 을 줄 수도 있다.
그리고 classpath 또는 file 과 같은 값을 주지 않고 그냥 경로만 입력할 경우 src/main/webapp 경로를 사용 한다.
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/m/**") // 어떤 패턴의 요청을 처리할지 정의
.addResourceLocations("classpath:/m/", "file:/Users/nimkoes/files/", "/myStatic/") // 리소스를 찾을 위치
.setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES)); // 리소스가 변경되지 않은 동안 캐싱을 유지할 시간
}
위와 같이 작성한 경우 src/main/webapp/myStatic/ 에서도 /m/** 패턴의 요청에 대해 리소스를 찾는 경로로 사용할 수 있다.
문서에 따르면 handler method 의 구현 내용을 수정하지 않고 공통으로 처리 할 작업들을 정의할 수 있다고 되어 있다.
이 인터페이스는 세가지 default method 를 가지고 있는 것으로 보아 굳이 구현하지 않아도 괜찮아 보인다.
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
public interface HandlerInterceptor {
/**
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can decide to abort the execution chain,
* typically sending a HTTP error or writing a custom response.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation returns {@code true}.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return {@code true} if the execution chain should proceed with the
* next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself.
* @throws Exception in case of errors
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
* Intercept the execution of a handler. Called after HandlerAdapter actually
* invoked the handler, but before the DispatcherServlet renders the view.
* Can expose additional model objects to the view via the given ModelAndView.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can post-process an execution,
* getting applied in inverse order of the execution chain.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param modelAndView the {@code ModelAndView} that the handler returned
* (can also be {@code null})
* @throws Exception in case of errors
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
* Callback after completion of request processing, that is, after rendering
* the view. Will be called on any outcome of handler execution, thus allows
* for proper resource cleanup.
* <p>Note: Will only be called if this interceptor's {@code preHandle}
* method has successfully completed and returned {@code true}!
* <p>As with the {@code postHandle} method, the method will be invoked on each
* interceptor in the chain in reverse order, so the first interceptor will be
* the last to be invoked.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param ex exception thrown on handler execution, if any
* @throws Exception in case of errors
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
전체적인 실행 흐름을 다음과 같이 그림을 그려볼 수 있다.
preHandle 에서는 보통 Servlet 에서 사용하던 Filter 와 비슷하지만 보다 구체적인 작업을 할 수 있다.
예를 들어 preHandler 의 메소드 파라미터 중에 Object handler 가 있는 것을 볼 수 있다.
즉, Filter 와 다르게 어떤 class 의 어떤 handler method 가 호출 될 것인지에 대한 정보를 전달 받기 때문에 요청에 대해 보다 구체적인 처리가 가능하다.
preHandle 에서 한가지 더 눈여겨 볼 점은 return type 이 boolean 이라는 것인데, 이 메소드 실행 결과 true 를 반환하면 다음 interceptor 의 preHandler 등 처리를 진행하지만, 그렇지 않고 false 를 반환하면 요청 처리를 더 이상 진행하지 않고 멈춘다.
handler method 까지 실행을 마치고 나서 view 를 렌더링 하기 직전에 postHandle 을 실행 한다.
이 postHandle 은 ModelAndView 타입의 객체를 매개변수로 받는다.
그렇기 때문에 이 곳에서는 model 객체에 추가 정보를 넣거나 view 를 변경하는 등의 작업을 할 수 있다.
afterCompletion 은 veiw 렌더링을 마친 다음으로 모든 요청 처리가 끝난 다음에 호출 된다.
그리고 preHandler 의 반환 값이 true 인 경우 postHandle 실행과 상관 없이 실행 된다.
어떠한 요청 처리 전 후에 필요한 작업을 한다는 점에서 Servlet 의 Filter 와 Spring 의 HandlerInterceptor 는 별다른 차이가 없어 보일 수 있다.
하지만 앞서 정리했던 것처럼 Filter 는 HandlerInterceptor 만큼의 정보를 알고 있지 않다.
그렇다고 해서 무조건 HandlerInterceptor 를 쓰는게 좋은것은 아니다.
만약 전후처리 내용이 Spring 과 밀접한 관련이 있다면 당연히 HandlerInterceptor 를 사용하는게 좋고 또 그래야 하겠지만, 그렇지 않은 성격의 전후처리라면 여전히 Servlet 의 Filter 를 사용하는게 문맥상 더 자연스럽다고 생각한다.
예를 들어 XSS 공격은 특정 handler 에 대한 것도 아니고 Spring 과 무관한 내용이므로 HandlerInterceptor 가 아닌 Servlet Filter 에서 처리하도록 구현 되어야 한다.
그럼 마지막으로 HandlerInterceptor 를 구현해보자.
구현하는 방법은 이 인터페이스를 구현하는 클래스를 만들고 Interceptor 로 등록해주면 된다.
이 요청을 의도한 대로 처리할 수 있도록 handler method 를 다음과 같이 수정할 수 있다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "hello " + name;
}
}
@PathVariable annotation 을 사용해서 url path 의 값을 가져와 사용할 수 있다.
handler method 를 위와 같이 수정하면 테스트가 성공하지만 Formatter 를 사용하도록 고쳐보자.
Formatter 는 서두에 설명해 두었지만, 문자열을 객체로, 객체를 문자열로 변환할 수 있는 인터페이스이다.
즉, 문자열인 name 에 대해 다음과 같이 객체로 변환해서 입력받을 수 있다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello/{name}")
public String hello(@PathVariable("name") Person person) {
return "hello " + person.getName();
}
}
변환에 사용 할 Person 클래스를 정의 한다.
package me.nimkoes;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Person {
String name;
}
현재 이 상태에서는 name 이 Person 타입의 객체에 들어갈 수 없다.
왜냐하면 컴퓨터는 이걸 어떻게 집어 넣어야 하는지 알 수 없기 때문이다.
그래서 Formatter 를 만들어서 어떻게 변환해야 하는지 알려주기 위해 Formatter<Person> 인터페이스를 구현하는 PersonFormatter 클래스를 새로 정의 했다.
package me.nimkoes;
import java.text.ParseException;
import java.util.Locale;
import org.springframework.format.Formatter;
public class PersonFormatter implements Formatter<Person> {
@Override
public Person parse(String text, Locale locale) throws ParseException {
Person person = new Person();
person.setName(text);
return person;
}
// 현재 사용하고 있지 않지만 임시로 toString 을 반환하도록 구현
@Override
public String print(Person object, Locale locale) {
return object.toString();
}
}
이렇게 만든 Formatter 를 등록하는 방법은 크게 두 가지가 있다.
하나는 WebMvcConfigurer 를 사용해서 addFormatter 로 추가해 주는 방법과
Spring Boot 를 사용할 경우 bean 으로 등록해서 Formatter 를 자동 등록하는 방법이다.
우선 WebMvcConfigurer 를 사용해서 Formatter 를 등록해 보자.
WebMvcConfigurer 인터페이스를 구현하는 설정 파일을 하나 만들어 준다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new PersonFormatter());
}
}
addFormatter 에는 Formatter 뿐만 아니라 Converter 도 추가로 등록할 수 있다.
Converter 는 Formatter 보다 일반적인 경우에 사용할 수 있다.
예를 들면 변환 대상이 굳이 문자열이 아니어도 사용할 수 있다. Formatter 에 비해 더 좋아 보이는 듯 해보이기도 하지만 Web 과 관련해서는 문자열을 객체로 변환하는 경우가 더 많기 때문에 Formatter 를 더 많이 사용하게 된다.
Formatter 를 추가했기 때문에 이제는 Spring 은 문자열을 Person 타입의 객체로 어떻게 변환해야 하는지 알게 된다.
다시 한 번 테스트 코드를 실행해보면 이번엔 성공하는 것을 볼 수 있다.
MockHttpServletRequest: HTTP Method = GET Request URI = /hello/nimkoes Parameters = {} Headers = [] Body = null Session Attrs = {}
Handler: Type = me.nimkoes.MyHelloController Method = me.nimkoes.MyHelloController#hello(Person)
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"13"] Content type = text/plain;charset=UTF-8 Body = hello nimkoes Forwarded URL = null Redirected URL = null Cookies = []
또한 Formatter 는 PathVariable 이 아닌 RequestParam 으로 전달해도 사용 할 수 있다.
RequestParam 은 query string 이라고 생각하면 된다.
테스트 코드의 GET 요청을 query string 을 사용하도록 수정한다.
package me.nimkoes;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@RunWith(SpringRunner.class)
@WebMvcTest
public class MyHelloControllerTest {
// @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
this.mockMvc.perform(get("/hello")
.param("name", "nimkoes"))
.andDo(print())
.andExpect(content().string("hello nimkoes"));
}
}
handler method 도 @PathVariable 대신 @RequestParam 을 사용하도록 수정 한다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello")
public String hello(@RequestParam("name") Person person) {
return "hello " + person.getName();
}
}
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {name=[nimkoes]} Headers = [] Body = null Session Attrs = {}
Handler: Type = me.nimkoes.MyHelloController Method = me.nimkoes.MyHelloController#hello(Person)
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"13"] Content type = text/plain;charset=UTF-8 Body = hello nimkoes Forwarded URL = null Redirected URL = null Cookies = []
의도한 대로 동작했고 테스트도 성공 했다.
마지막으로 Spring Boot 를 사용할 때 Formatter 를 등록할 수 있는 방법을 사용해 본다.
무조건 이렇게 할 필요는 없다. 왜냐면 지금 사용중인 프로젝트도 Spring Boot 프로젝트라는 사실을 잊지 말아야 한다.
Spring Boot 프로젝트면 사용할 수 있는 방법이 하나 더 있을 뿐이다.
이 방법은 Formatter 를 bean 으로 등록하면 자동으로 Formatter 에 등록이 되는 방법이다.
그래서 WebMvcConfigurer 를 구현해서 addFormetter 에 추가해 주지 않아도 된다.
WebConfig 파일을 삭제 해버려도 되지만 내용만 지우기로 했다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
}
그리고 PersonFormatter 를 @Component annotation 을 사용해서 Component Scan 의 대상이 될 수 있도록 하여 bean 으로 등록한다.
package me.nimkoes;
import java.text.ParseException;
import java.util.Locale;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
@Component
public class PersonFormatter implements Formatter<Person> {
@Override
public Person parse(String text, Locale locale) throws ParseException {
Person person = new Person();
person.setName(text);
return person;
}
// 현재 사용하고 있지 않지만 임시로 toString 을 반환하도록 구현
@Override
public String print(Person object, Locale locale) {
return object.toString();
}
}
Spring Boot 버전이 올라가면서 구현이 조금 바뀐것 같긴 하지만, bean 으로만 등록 되어 있어도 자동으로 Formatter 로 등록해 주는 부분을 찾아 보았다.
Spring Boot 2.1.1.RELEASE 버전 기준으로 org.springframework.boot:spring-boot-autoconfigure 의 META-INF 하위 spring.factories 파일에 정의 된 자동 설정 중 WebMvcAutoConfiguration 클래스가 있다.
이 클래스는 @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) 가 정의되어 있다.
이 annotation 의 의미는 WebMvcConfigurationSupport 타입의 bean 이 없으면 자동설정을 한다는 것을 의미한다.
그리고 이 클래스는 @EnableWebMvc annotation 을 작성하면 @Import 하는 DelegatingWebMvcConfiguration.class 클래스가 상속 받은 클래스이다.
정리하면 @EnableWebMvc annotation 을 사용하지 않고 Spring Boot 의 Web 자동 설정을 사용하면 WebMvcAutoconfiguration 클래스를 실행해서 Web 관련 자동 설정을 해준다는 것을 의미한다.
beanFactory 에 등록 된 bean 의 타입 중 Formatter 타입의 bean 을 FormatterRegistry 에 자동으로 추가해주는 부분이다.
이런 내용이 있어서 @Component 로 Formatter<Person> 을 구현 한 PersonFormatter 클래스가 Formatter 로 자동 등록 될 수 있는 것이다.
다시 테스트 코드로 돌아가서 실행해보면 예상과 달리 테스트가 실패 한다.
하지만 애플리케이션을 구동해서 브라우저를 통해 실행해보면 의도한 대로 잘 동작하는 것을 확인할 수 있다.
그 이유는 테스트 코드에 @WebMvcTest annotation 을 사용했기 때문이다.
이 annotation 은 slice 테스트를 위한 것으로 Web 과 관련 된 bean 만 등록해서 테스트를 진행 한다.
@Component 는 Web 과 관련 된 bean 으로 인식하지 않기 때문에 의도한 결과를 받아보지 못한 것이다.
slice 테스트 관련된 대표적인 annotation 으로는 @WebMvcTest, @WebFluxTest, @DataJpaTest, @JsonTest, @RestClientTest 등이 있다.
그리고 @WebMvcTest 는 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 등에 대해서만 bean 으로 등록 한다.
직접 PersonFormatter 를 테스트에 사용 할 bean 이라고 명시해주는 방법도 있지만 모든 bean 을 등록해서 테스트를 진행하도록 수정해 보았다.
@WebMvcTest 대신 @SpringBootTest annotation 을 사용 했고, 이렇게 하면 더이상 MockMvc 에 대한 의존성을 주입받을 수 없기 때문에 @AutoConfigureMockMvc annotation 을 추가로 작성해 주었다.
package me.nimkoes;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyHelloControllerTest {
// @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
@Autowired
MockMvc mockMvc;
@Test
public void hello() throws Exception {
this.mockMvc.perform(get("/hello")
.param("name", "nimkoes"))
.andDo(print())
.andExpect(content().string("hello nimkoes"));
}
}
MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {name=[nimkoes]} Headers = {} Body = null Session Attrs = {}
Handler: Type = me.nimkoes.MyHelloController Method = public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person)
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[text/plain;charset=UTF-8], Content-Length=[13]} Content type = text/plain;charset=UTF-8 Body = hello nimkoes Forwarded URL = null Redirected URL = null Cookies = []
모든 bean 을 등록 했기 때문에 @Component annoation 에 대해서도 bean 으로 등록 되었고, @EnableWebMvc 를 사용하지 않은 Spring Boot 자동 설정 내용을 기준으로 PersonFormatter 를 FormatterRegistry 에 등록해 주어 정상적으로 테스트가 성공하는 것을 확인할 수 있다.
An opinionated WebApplicationInitializer to run a SpringApplication from a traditional WAR deployment. Binds Servlet, Filter and ServletContextInitializer beans from the application context to the server.
To configure the application either override the configure(SpringApplicationBuilder) method (calling SpringApplicationBuilder.sources(Class...)) or make the initializer itself a @Configuration. If you are using SpringBootServletInitializer in combination with other WebApplicationInitializers you might also want to add an @Ordered annotation to configure a specific startup order.
Note that a WebApplicationInitializer is only needed if you are building a war file and deploying it. If you prefer to run an embedded web server then you won't need this at all.
이 클래스에 대한 마지막 설명을 보면 embedded web server 를 사용하려 한다면 굳이 이 클래스를 사용 할 필요가 없다고 하고 있다.
왜냐하면 war 포맷으로 packaging 한다는 것은 웹 애플리케이션 외부에 있는 WAS 에서 실행한다는 의미가 강하고, jar 포맷으로 packaging 한다는 것은 웹 애플리케이션 내부에 있는 WAS 에서 실행한다는 의미이기 때문이다.
IDE 설정을 local 에 설치한 tomcat 으로 맞춰준 다음 실행하면 이번에는 다음과 같이 외부 WAS 에 war 파일이 배포되어 실행되는 것을 확인할 수 있다.
조금 잘려 나오긴 했지만, 자바 애플리케이션을 구동하여 실행했을 때와 로그를 비교해보면 독립 실행 했을 때와 WAS 위에 배포했을 때의 다른 점을 로그를 통해 알 수 있다.
독립 실행 했을 때는 'Spring' 배너가 로깅 되는 것을 시작으로 WAS 에 대한 port 정보 등이 로깅 되었었는데, WAS 에 war 를 배포했을 때는 WAS 가 구동하는 로그가 먼저 출력 되고 Sping boot 애플리케이션을 구동하고 나서는 WAS 에 대한 port 정보가 출력되지 않고 있는것을 볼 수 있다.
마지막에 다룬 war 배포 관련하여 maven 에 대해 정리할 때 관련있는 내용을 조금 다룬적이 있었다.
링크로 남겨둔 게시물의 아래쪽에 정리 된 'war packaging custom' 내용을 참고해도 좋을것 같다.
library 는 사용자가 능동적으로 어떤 도구를 사용할지 고르고 골라 필요한걸 선택해서 사용한다.
framework 는 이미 구조가 짜여져 있고, 고정된 틀과 흐름이 있고 사용자는 그 흐름에 맞춰 원하는 결과가 나올 수 있도록 재료들을 제공해줘야 한다.
좀 더 쉽게 정리하면 library 는 사용자가 골라서 사용하는 거고, framework 는 사용자가 맞춰줘야 한다.
갑자기 library 와 framework 의 차이를 정리한 이유는 Spring 은 library 가 아니고 framework 이기 때문이다.
즉, framework 의 생명주기(lifecycle)에 대해 알고 언제 무슨 일이 일어나며 그 일이 일어날 때 사용자가 무엇을 제공해 주어야 원하는 결과를 받아볼 수 있기 때문이다.
그런 의미에서 지금부터 Spring Web MVC 설정하는 방법에 대해 정리한다.
앞선 내용에서 자주 언급 했지만, Servlet WebApplicationContext 를 만드는 DispatcherServlet 을 사용해서 Spring Web MVC 를 사용하면 DispatcherServlet.properties 파일을 참고하여 기본 전략이 설정 된다.
만약 이 기본 설정을 바꾸고 싶으면 그 타입의 bean 을 등록하여 덮어 쓰거나 추가할 수 있다.
그 예로 ViewResolver 타입의 InternalResourceViewResolver 클래스를 bean 으로 등록하여 prefix 와 suffix 설정을 추가한 ViewResolver 를 사용하도록 했었다.
@Bean
public ViewResolver myCustomViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
하지만 일반적으로 이렇게 사용 할 bean을 설정하는 일은 거의 없다.
왜냐하면 보편적으로 많이 사용하는 Web 설정에 대해 Spring 에서 @EnableWebMvc annotation 을 지원하기 때문이다.
package me.nimkoes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@ComponentScan
@EnableWebMvc
public class MyWebConfig {
@Bean
public ViewResolver myCustomViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
@EnableWebMvc annotation 을 열어보면 DelegatingWebMvcConfiguration.class 를 import 하고 있다.
DelegatingWebMvcConfiguration.class 파일을 열어보면 WebMvcConfigurationSupport.class 를 상속 받고 있는것을 볼 수 있다.
package org.springframework.web.servlet.config.annotation;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
/**
* A subclass of {@code WebMvcConfigurationSupport} that detects and delegates
* to all beans of type {@link WebMvcConfigurer} allowing them to customize the
* configuration provided by {@code WebMvcConfigurationSupport}. This is the
* class actually imported by {@link EnableWebMvc @EnableWebMvc}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// 생략...
}
WebMvcConfigurationSupport.class 파일을 열어보면 실제로 등록하는 bean 들을 볼 수 있다.
This is the main class providing the configuration behind the MVC Java config. It is typically imported by adding @EnableWebMvc to an application @Configuration class. An alternative more advanced option is to extend directly from this class and override methods as necessary, remembering to add @Configuration to the subclass and @Bean to overridden @Bean methods. For more details see the javadoc of @EnableWebMvc.
This class registers the following HandlerMappings:
- RequestMappingHandlerMapping ordered at 0 for mapping requests to annotated controller methods. - HandlerMapping ordered at 1 to map URL paths directly to view names. - BeanNameUrlHandlerMapping ordered at 2 to map URL paths to controller bean names. - RouterFunctionMapping ordered at 3 to map router functions. - HandlerMapping ordered at Integer.MAX_VALUE-1 to serve static resource requests. - HandlerMapping ordered at Integer.MAX_VALUE to forward requests to the default servlet.
Registers these HandlerAdapters:
- RequestMappingHandlerAdapter for processing requests with annotated controller methods. - HttpRequestHandlerAdapter for processing requests with HttpRequestHandlers. - SimpleControllerHandlerAdapter for processing requests with interface-based Controllers. - HandlerFunctionAdapter for processing requests with router functions.
Registers a HandlerExceptionResolverComposite with this chain of exception resolvers:
- ExceptionHandlerExceptionResolver for handling exceptions through ExceptionHandler methods. - ResponseStatusExceptionResolver for exceptions annotated with ResponseStatus. - DefaultHandlerExceptionResolver for resolving known Spring exception types
Registers an AntPathMatcher and a UrlPathHelper to be used by:
- the RequestMappingHandlerMapping, - the HandlerMapping for ViewControllers - and the HandlerMapping for serving resources
Note that those beans can be configured with a PathMatchConfigurer.
Both the RequestMappingHandlerAdapter and the ExceptionHandlerExceptionResolver are configured with default instances of the following by default:
- a ContentNegotiationManager - a DefaultFormattingConversionService - an OptionalValidatorFactoryBean if a JSR-303 implementation is available on the classpath - a range of HttpMessageConverters depending on the third-party libraries available on the classpath.
WebMvcConfigurationSupport.class 에서 눈여겨 볼 만한 부분이 있다.
/**
* Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with.
* <p>This method cannot be overridden; use {@link #addInterceptors} instead.
*/
protected final Object[] getInterceptors() {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
/**
* Override this method to add Spring MVC interceptors for
* pre- and post-processing of controller invocation.
* @see InterceptorRegistry
*/
protected void addInterceptors(InterceptorRegistry registry) {
}
전통적인 servlet 생명주기에서 보았던 interceptor 를 java 코드로 추가할 수 있도록 장치가 마련되어 있다.
InterceptorRegistry 클래스를 열어보니 ORDER 정보로 정렬 한 ArrayList 타입의 값을 사용하도록 되어 있었다.
@EnableWebMvc annotation 을 사용하기 전과 후에 어떤 bean 이 등록 되는지 비교해 보았다.
실제로 어떤 bean 이 등록 되는지 보기 위해 DispatcherServlet 클래스의 initStrategis 메소드에 debgger 를 설정했다.
@EnableWebMvc annotation 을 사용할 때 주의할 점이 있는데, ServletContext 를 반드시 설정해 주어야 한다.
그렇지 않으면 bean 설정이 제대로 되지 않고 오류가 발생 한다.
package me.nimkoes;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
/*
* @EnableWebMvc annotation 을 사용하면
* DispatcherServlet 이 ServletContext 를 참조하기 때문에 반드시 설정해 주어야 정상적으로 동작 한다.
*/
context.setServletContext(servletContext);
context.register(MyWebConfig.class);
context.refresh();
// DispatcherServlet 을 만들 때 설정 정보를 가지고 있는 context 를 넘겨 준다.
DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
// ServletContext 에 DispatcherServlet 을 등록 한다.
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
왼쪽은 @EnableWebMvc annotation 을 사용하지 않았을 때, 오른쪽은 사용했을 경우이다.
handlerMappings, handlerAdapters, viewResolvers 세 항목 위주로 다른 점을 확인 해보았다.
handlerMappings 와 handlerAdapters 를 보면 등록 된 baen 은 같은데 순서가 달라졌다.
작은 차이이지만 이 순서도 마냥 무시할 수 있는것은 아닌데, 처리 가능한 handler 를 찾기 위해 반복문을 수행 하는데, 더 자주 사용하는 bean 을 배열 인덱스의 앞쪽에 위치 함으로 반복 횟수를 조금이라도 줄일 수 있다.
viewResolvers 는 @EnableWebMvc annotation 을 사용했을 때 ViewResolverComposite bean 이 하나 더 등록 되는것을 볼 수 있었다.
마지막으로 확인해볼 내용은 DelegatingWebMvcConfiguration.class 이다.
이 클래스는 @EnableWebMvc annotation 이 import 하고 있는 클래스이다.
본문 앞쪽에서는 이 클래스에 대해 WebMvcConfigurationSupport.class 클래스를 상속 받는다고 단순하게 정리하고 넘어갔다.
하지만 이 클래스는 Spring Web MVC 를 사용할 때 기능을 손쉽게 확장할 수 있도록 해주는 중요한 클래스이다.
이 delegation 구조를 활용해서 기능을 확장하는 방법은 WebMvcConfigurer 인터페이스를 구현하는 것이다.
인터페이스를 구현 한다고 해서 확장하지도 않을 기능에 대한 추상 메소드를 구현해야만 하는 일은 없다.
왜냐하면 이 인터페이스는 전부 default 메소드를 사용하기 때문이다.
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.config.annotation;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
* Defines callback methods to customize the Java-based configuration for
* Spring MVC enabled via {@code @EnableWebMvc}.
*
* <p>{@code @EnableWebMvc}-annotated configuration classes may implement
* this interface to be called back and given a chance to customize the
* default configuration.
*
* @author Rossen Stoyanchev
* @author Keith Donald
* @author David Syer
* @since 3.1
*/
public interface WebMvcConfigurer {
/**
* Helps with configuring HandlerMappings path matching options such as trailing slash match,
* suffix registration, path matcher and path helper.
* Configured path matcher and path helper instances are shared for:
* <ul>
* <li>RequestMappings</li>
* <li>ViewControllerMappings</li>
* <li>ResourcesMappings</li>
* </ul>
* @since 4.0.3
*/
default void configurePathMatch(PathMatchConfigurer configurer) {
}
/**
* Configure content negotiation options.
*/
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
/**
* Configure asynchronous request handling options.
*/
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* Configure a handler to delegate unhandled requests by forwarding to the
* Servlet container's "default" servlet. A common use case for this is when
* the {@link DispatcherServlet} is mapped to "/" thus overriding the
* Servlet container's default handling of static resources.
*/
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
/**
* Add {@link Converter Converters} and {@link Formatter Formatters} in addition to the ones
* registered by default.
*/
default void addFormatters(FormatterRegistry registry) {
}
/**
* Add Spring MVC lifecycle interceptors for pre- and post-processing of
* controller method invocations. Interceptors can be registered to apply
* to all requests or be limited to a subset of URL patterns.
* <p><strong>Note</strong> that interceptors registered here only apply to
* controllers and not to resource handler requests. To intercept requests for
* static resources either declare a
* {@link org.springframework.web.servlet.handler.MappedInterceptor MappedInterceptor}
* bean or switch to advanced configuration mode by extending
* {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
* WebMvcConfigurationSupport} and then override {@code resourceHandlerMapping}.
*/
default void addInterceptors(InterceptorRegistry registry) {
}
/**
* Add handlers to serve static resources such as images, js, and, css
* files from specific locations under web application root, the classpath,
* and others.
*/
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
/**
* Configure cross origin requests processing.
* @since 4.2
*/
default void addCorsMappings(CorsRegistry registry) {
}
/**
* Configure simple automated controllers pre-configured with the response
* status code and/or a view to render the response body. This is useful in
* cases where there is no need for custom controller logic -- e.g. render a
* home page, perform simple site URL redirects, return a 404 status with
* HTML content, a 204 with no content, and more.
*/
default void addViewControllers(ViewControllerRegistry registry) {
}
/**
* Configure view resolvers to translate String-based view names returned from
* controllers into concrete {@link org.springframework.web.servlet.View}
* implementations to perform rendering with.
* @since 4.1
*/
default void configureViewResolvers(ViewResolverRegistry registry) {
}
/**
* Add resolvers to support custom controller method argument types.
* <p>This does not override the built-in support for resolving handler
* method arguments. To customize the built-in support for argument
* resolution, configure {@link RequestMappingHandlerAdapter} directly.
* @param resolvers initially an empty list
*/
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
/**
* Add handlers to support custom controller method return value types.
* <p>Using this option does not override the built-in support for handling
* return values. To customize the built-in support for handling return
* values, configure RequestMappingHandlerAdapter directly.
* @param handlers initially an empty list
*/
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
/**
* Configure the {@link HttpMessageConverter HttpMessageConverters} to use for reading or writing
* to the body of the request or response. If no converters are added, a
* default list of converters is registered.
* <p><strong>Note</strong> that adding converters to the list, turns off
* default converter registration. To simply add a converter without impacting
* default registration, consider using the method
* {@link #extendMessageConverters(java.util.List)} instead.
* @param converters initially an empty list of converters
*/
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* A hook for extending or modifying the list of converters after it has been
* configured. This may be useful for example to allow default converters to
* be registered and then insert a custom converter through this method.
* @param converters the list of configured converters to extend.
* @since 4.1.3
*/
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* Configure exception resolvers.
* <p>The given list starts out empty. If it is left empty, the framework
* configures a default set of resolvers, see
* {@link WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)}.
* Or if any exception resolvers are added to the list, then the application
* effectively takes over and must provide, fully initialized, exception
* resolvers.
* <p>Alternatively you can use
* {@link #extendHandlerExceptionResolvers(List)} which allows you to extend
* or modify the list of exception resolvers configured by default.
* @param resolvers initially an empty list
* @see #extendHandlerExceptionResolvers(List)
* @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
*/
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
/**
* Extending or modify the list of exception resolvers configured by default.
* This can be useful for inserting a custom exception resolver without
* interfering with default ones.
* @param resolvers the list of configured resolvers to extend
* @since 4.3
* @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
*/
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
/**
* Provide a custom {@link Validator} instead of the one created by default.
* The default implementation, assuming JSR-303 is on the classpath, is:
* {@link org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean}.
* Leave the return value as {@code null} to keep the default.
*/
@Nullable
default Validator getValidator() {
return null;
}
/**
* Provide a custom {@link MessageCodesResolver} for building message codes
* from data binding and validation error codes. Leave the return value as
* {@code null} to keep the default.
*/
@Nullable
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
View 를 반환할 때 사용했던 ViewResolver 타입의 InternalResourceViewResolver 객체를 직접 만들어 bean 으로 등록하는 대신 이 인터페이스를 사용해서 다음과 같이 동일하게 동작하도록 할 수 있다.