Spring MVC는 MVC2 모델의 Controller( Servlet ) - View ( JSP ) - Model와 유사한 구조를 띄며, 몇가지 핵심적인 개념이 추가된다
Spring Web MVC의 핵심은 DispatcherServlet
, ViewResolver
, HandlerMapping
,HandlerAdapter
다
위 이미지에서 FrontController
는 DispatherServlet
을 의미하는데 간략하게 설명하면 다음과 같다.
- Client에게 받은 요청이
DispatherServlet
에 도착 ( 이 사이에Filter
동작 )DispatherServlet
에서HandlerMapping
을 통해 해당 작업을 수행할HandlerAdapter
탐색
- Controller로 넘어가기 전에
Interceptor
동작HandlerAdapter(Controller)
로 로직을 수행하고model
( 데이터 ) 을 반환DispatherServlet
이ViewResolver
를 통해 해당하는View
를 탐색하고model
정보적용- Client에게 응답
이때 DispatcherServlet
이 WebApplicationContext
라는 Bean Container를 이용하여
Handlermapping
이나 ViewResolver
, Controller
등을 사용하는 구조를 가진다
- Tomcat에 있는
server.xml
에 실행시킬 프로젝트 설정- Tomcat에 있는
context.xml
에 프로젝트 로드시 읽어들일 설정파일 지정web.xml
에 프로젝트 load시 먼저 읽어들일 bean파일 지정 (root-context.xml
)
- web과 관련없는 DB, service, aop 설정
ContextLoaderListener
를 이용해서 WAS가 뜨는지 확인- WAS가 프로젝트를 load하면
root-context.xml
을 읽음
RootApplicationContext
에bean
설정- Client가 request을 보냄 ( DNS 등을 거쳐서 )
DispatcherServlet
에 도착하기 전후에Filter
동작DispatcherServlet
생성 ⇒servlet-context.xml
읽음
WebApplicationContext
에 web과 관련있는 ViewResolver, Controller, Interceptor 등의bean
설정- 이때
RootApplicationContext
에 등록된 Bean과 동일한 이름이 있으면 덮어씀DispatcherServlet
이HandlerMapping
을 통해서 url에 해당하는Controller
를 찾아줌
- 이 과정에서
HandlerAdaptor
를 사용해서Controller
를 실행- 이때
interceptor
동작Controller
에서 비즈니스 로직을 처리하고 반환된 데이터를ModelAndView
객체로 리턴
- 비즈니스 로직 처리과정에서
AOP
가 동작DispatherServlet
이ViewResolver
를 통해 응답페이지 선정
- FE 프레임워크 사용 시, JSON이나 XML형식의 데이터만 보내줌
- Client 응답 수신
먼저 HTTP request에 사용되는 요청메서드를 알아보자
이제 Spring에서 앞서 소개한 HTTP Method들을 매핑하는 방법을 알아보자
Spring에서 요청을 mapping하는데는 @RequestMapping
애노테이션이 사용된다
내부적으로@RequestMapping
을 사용하는@GetMapping
이나 @PostMapping
등이 주로 사용된다
RequestMapping의 경우 Method를 지정해주지 않으면 모든 요청 Method에 매핑되어 처리된다
@Test
void helloTest() throws Exception {
this.mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
this.mockMvc.perform(post("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
this.mockMvc.perform(put("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
this.mockMvc.perform(delete("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
모든 /hello 요청이 수행된다
@Controller
@RequestMapping("/hello")
public class SampleController {
@RequestMapping("/ddings")
public @ResponseBody String hello(){
return "hello";
}
}
메서드뿐만 아니라 클래스에도 @RequestMapping
을 달 수 있으며 클래스에 달았을 경우 클래스내의 모든 Mapping uri의 앞에 붙는다
즉, 위 코드에서 hello()
메서드는 /hello/ddings
요청을 수행한다
@RequestMapping("/hello?") // => /hello1, /hello2, 한 글자를 의미
@RequestMapping("/hello*") // => /helloddings, /hellomobile, 여러 글자
@RequestMapping("/hello**") // => /hello/ddings/modile, /를 포함한 아무런 글자들
@RequestMapping
은 위의 패턴들을 지원한다
@RequestMapping("/hello/{name:[a-z]+}") // name PathVariable로 소문자 아무거나 와도 된다
이 외에도 정규표현식또한 지원하며 만일 중복되는 mapping이 있다면 가장 구체적으로 명시된 handler에 mapping된다
💡 URI 확장자 매핑
Spring MVC에서는
/hello
handler만 있더라도/hello.json
과 같이 확장자가 포함된 mapping을 처리했었다
문제는 이때.zip
과.bat
같은 확장자를 붙히면 파일을 다운로드받게 되는데, 이를 이용한 RFD 공격이 가능했다고 한다
그렇기 떄문에 Spring Boot에서는 기본적으로 확장자 매핑을 지원하지않는다
@Controller
@RequestMapping("/hello")
public class SampleController {
@RequestMapping(value="/ddings", consumes = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody String hello(){
return "hello";
}
}
consume 속성을 이용해서 특정 MediaType이 왔을때만 처리하는 Handler를 만들 수도 있다
@Test
void helloTest() throws Exception {
this.mockMvc.perform(get("/hello/ddings"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
위 테스트에서 Content-Type으로 json을 지정하지않아서 415 Unsupported Media Type 에러가 발생하며 테스트가 실패한다
@Test
void helloTest() throws Exception {
this.mockMvc.perform(get("/hello/ddings")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
Content-Type을 json으로 명시해주면 테스트가 통과한다
@Controller
@RequestMapping("/hello")
public class SampleController {
@RequestMapping(value="/ddings"
, consumes = MediaType.APPLICATION_JSON_VALUE
, produces = MediaType.TEXT_PLAIN_VALUE)
public @ResponseBody String hello(){
return "hello";
}
}
produces 를 이용해서 response 시의 MediaType을 지정하는 handler를 만드는 것도 가능하다
이건 request시에 accept헤더를 통해서 필터링을 할 수 있는데.. accept헤더를 설정하지 않으면 그냥 mapping이 된다
이 부분은 살짝 개발자의 의도와 다르게 동작한다고 생각될 수 있는 부분으로 생각한다
@RequestMapping(value="/ddings"
,headers = HttpHeaders.FROM)
public @ResponseBody String hello(){
return "hello";
}
headers를 이용해서 특정 헤더가 포함된 요청을 처리하는 handler를 만들 수 있다
@Test
void helloTest() throws Exception {
this.mockMvc.perform(get("/ddings")
.header(HttpHeaders.AUTHORIZATION,"zamong"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
Header에 FROM을 추가하지않은 위 테스트는 실패하게 된다
headers = "!" + HttpHeaders.FROM
!를 붙혀서 not을 처리하는 것도 가능하고
headers = HttpHeaders.AUTHORIZATION + "=" + "zamong"
특정 헤더값을 지정하여 mapping하는 것도 가능하다
@RequestMapping(value="/ddings" ,params = "name") public @ResponseBody String hello(){ return "hello"; }
params를 이용하여 특정 파라미터가 있는 경우에만 동작하는 handler를 만들 수도 있다