구조
전 강의에서 만들었던 FrontController가 스프링 MVC에서는 DispatcherServlet이다
(제일 중요함)
DispatcherServelt도 부모 클래스에서 HttpServlet을 상속받아 사용하고 서블릿으로 동작한다.
스프링부트가 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로에 대해 매핑한다
(urlPatterns="/"
)
DispatcherServlet의 부모인 FrameworkServlet에서 service()
를 오버라이딩 해뒀다.
service
호출에 의해 여러 메서드가 호출되면서 최종적으로 DispatcherServlet.doDispatch()
가 호출된다.
핸들러 매핑, 핸들러 어댑터, 뷰 리졸버, 뷰 등이 모두 인터페이스로 선언되어 있다. 따라서 DispatcherServlet의 코드 변경 없이 원하는 기능을 변경하거나 확장할 수 있다.
동작 순서
1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(=컨트롤러) 조회
2. 핸들러 어댑터 조회 : 매핑된 핸들러를 실행할 수 있는 핸들러 어댑터 조회
3. 핸들러 어댑터 실행
4. 핸들러 실행 : 핸들러 어댑터가 핸들러를 실행한다
5. ModelAndView 반환 : 핸들러 어댑터는 핸들러의 반환 정보를 ModelAndView로 변환해서 반환한다
6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다
7. view 반환 : 뷰 리졸버가 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
8. 뷰 렌더링 : 뷰를 통해 뷰를 렌더링 한다
핸들러 매핑과 핸들러 어댑터
특정 컨트롤러가 호출되기 위해서는 이러한 과정을 거친다
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터 대부분을 구현해두었다.
우선순위대로 찾고 없을 경우 다음으로 넘어간다.
뷰 리졸버
스프링부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동 등록한다.
이 때 application.properties에 등록한 정보를 토대로 등록한다.
스프링부트가 자동 등록하는 뷰 리졸버는 아래와 같다
뷰 리졸버의 동작방식은 이러하다.
1. 핸들러 어댑터 호출 : 논리 뷰 이름 획득
2. 뷰리졸버 호출 : 우선순위에 따라 뷰 리졸버를 호출한다
3. 뷰리졸버가 뷰 반환
4. 뷰를 렌더링한다
앞에서 봤듯이 가장 우선순위가 높은 핸들러 매핑과 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다.
실무에서는 대부분 애노테이션 기반의 컨트롤러를 사용한다.
실용적인 방식은 아래와 같다
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
MemberRepository memberRepository = MemberRepository.getInstance();
@GetMapping("/new-form")
public String newForm(){
return "new-form";
}
@PostMapping("/save")
public String save(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model
) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member",member);
return "save-result";
}
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members",members);
return "members";
}
}
model.addAttribute("member",member)
로 뷰에 데이터를 전달할 수 있다@RequestParam
사용 : HTTP 요청 파라미터를 받는다✔ @Controller
: 반환 값이 String이면 View 이름으로 인식한다. 뷰를 찾고 뷰가 렌더링 된다.
✔ @RestController
: 반환 값으로 View를 찾는 것이 아니라, HTTP 메세지 바디에 바로 입력한다.
✔ @GetMapping
, @PostMapping
등 HTTP 메서드 구분해서 매핑 가능하다.
✔ @RequestMapping
으로 사용할 경우 모든 메서드를 허용한다.
✔ @PathVariable
으로 URL 경로를 템플릿화 할 수 있다.
✔ 특정 헤더 조건으로 매핑
/**
* 특정 헤더로 추가 매핑
* headers="mode",
* headers="!mode"
* headers="mode=debug"
* headers="mode!=debug" (! = )
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
✔ 미디어 타입 조건 매핑 Content-Type, Consume
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
HTTP 요청의 content-type 헤더를 기반으로 미디어 타입으로 매핑한다.
맞지 않을 경우 415(Unsupported Media Type)을 반환한다.
✔ 미디어 타입 조건 매핑 Accept, produce
/**
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
HTTP 요청의 Accept 헤더를 기반으로 매핑한다. 맞지 않으면 406(Not Acceptable)을 반환한다.
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/hedaers")
public String headers(
HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String,String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value="myCookie", required = false) String cookie
){
log.info("request={}", request);
log.info("response={}", response);
log.info("httpMethod={}", httpMethod);
log.info("locale={}", locale);
log.info("headerMap={}", headerMap);
log.info("header host={}", host);
log.info("myCookie={}", cookie);
return "ok";
}
}
✔ @RequestHeader MultiValueMap<String,String> headerMap
모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다. MultiValueMap은 하나의 키에 여러 값을 받을 수 있다
✔ @RequestHeader("host") String host
특정 HTTP 헤더를 조회한다.
✔ @CookieValue(value="myCookie", required = false) String cookie
특정 쿠키를 조회한다
속
헤더와 쿠키에 모두 required
, defaultValue
속성 사용 가능하다.
클라이언트에서 서버로 요청 데이터를 전달하는 방법은 크게 3가지다.
/url?username=hello&age=20
✅ 요청 파라미터
1,2번 방식 모두 형식이 요청 파라미터 형식으로 같아서 구분없이 조회할 수 있다.
둘 다 요청 파라미터라 한다.
요청 파라미터를 조회하는 다양한 방법이 있다.
단순히 HttpServletRequest가 제공하는 request.getParameter("username")
과 같이 조회할 수 있다.
✅ RequestParam
스프링에서 제공하는 @RequestParam
을 사용해서 요청 파라미터를 편리하게 조회할 수 있다.
@RequestParam("username") String memberName
과 같이 사용하면 된다. 컨트롤러에서 파라미터로 바로 값을 사용할 수 있다.
그리고 @RequestParam("username") String memberName
과 같은 경우 실제 쿼리파라미터 명은 username
이 된다. 즉 요청할 때 api/v1?username=jidam
과 같이 요청이 들어오는 것이다.
만약 HTTP 파라미터 이름이 변수 이름과 같다면 @RequestParam String name
과 같이 생략할 수 있다.
✔ @RequestParam(required=true) String name
파라미터 필수 여부를 결정한다. 기본값이 true
이다
✔ @RequestParam(defaultValue="guest") String name
파라미터에 값이 없을 때 defaultValue 속성을 활용해 기본 값을 적용할 수 있다. 이때는 required 속성은 의미가 없어진다.
파라미터가 여러개인 경우 Map이나 MultiValueMap을 활용할 수 있다.
파라미터가 여러개라면 MultiValueMap을 사용하자.
✅ ModelAttribute
일반적으로 파라미터를 통해 값을 입력받고 그 값으로 객체를 생성한다
예를 들자면 아래와 같다
public Member test(@RequestParam String name){
Member member = new Member(1L, name);
return member;
}
스프링에서는 @ModelAttribute
를 통해 이 과정을 자동화 할 수 있다
public Member test2(@ModelAttribute Member member){
return member;
}
위와 같이 @ModelAttribute
가 있으면 자동으로 쿼리 파라미터에서 Member
에 필드에 해당하는 값을 찾아서 값을 넣어준다.
이 때, age=abc
처럼 숫자가 들어가야 할 곳에 문자를 넣으면 바인딩 오류가 발생한다.
요청 파라미터 (쿼리파라미터 또는 html form) 이 아닌 HTTP 요청 바디에 직접 데이터가 넘어오는 경우는 @RequestParam
이나 @ModelAttribute
를 사용할 수 없다.
✅ HttpEntity
@PostMapping("api/v1")
public HttpEntity<String> httpEntity(HttpEntity<String> httpEntity){
HttpHeaders headers = httpEntity.getHeaders();
String body = httpEntity.getBody();
return new HttpEntity<>("ok");
}
HttpEntity를 사용하면 HTTP header, body 정보를 편리하게 조회할 수 있다. 응답에도 사용할 수 있다. 메세지 바디 정보를 직접 반환하며 헤더 정보도 포함할 수 있다.
✅ RequestBody
@ResponseBody
@PostMapping("api/v2")
public String requestBody(@RequestBody String messageBody){
log.info("messageBody = {}",messageBody);
return "OK";
}
RequestBody를 통해 메세지 바디 정보를 편리하게 조회할 수 있다.
헤더 정보가 필요하면 RequestHeader를 사용하면 된다
또한 ResponseBody를 사용하면 응답 결과를 Http 메세지 바디에 직접 담아서 전달할 수도 있다.
@ResponseBody
@PostMapping("api/v3")
public String requestBodyJson(@RequestBody Member member){
log.info("member.getName = {}",member.getName());
return member.getName();
}
RequestBody를 사용하면 Json 요청도 객체로 바로 변환할 수 있다.
이 때, Http 메세지 컨버터가 HTTP 바디의 내용을 우리가 원하는 문자, 객체의 내용으로 변환해준다. 또한 JSON도 객체로 변환해준다.
JSON으로 요청할 때는 HTTP 요청 content-type이 application/json이어야 한다.
스프링에서 HTTP 응답을 하는 방법은 크게 3가지이다
1. 정적 리소스
웹 브라우저에 정적인 HTML,css,js 제공
2. 뷰 템플릿
웹 브라우저에 동적인 HTML 제공
3. HTTP 메세지
HTTP API를 제공하는 경우 HTML이 아니라 데이터를 제공하므로 HTTP 메세지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다
✅ 정적 리소스
정적 리소스는 파일을 변경 없이 그대로 서비스하는 것
src/main/resources/static
경로에 정적 리소스를 넣을 수 있다.
src/main/resources/static/basic/hello.html
파일이 있다면
http:://{HOST}:8080/basic/hello.html
과 같이 요청하면 된다.
✅ 뷰 템플릿
뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
일반적으로 HTML을 동적으로 만드는 데 사용한다.
스프링부트 기본 뷰 템플릿 경로
src/main/resources/templates
@RequestMapping("/view")
public String view(Model model){
model.addAttribute("data","hello");
return "response/hello";
}
위와 같이 String을 담아서 응답하고 @ResponseBody
가 없을 때는
src/main/resources/templates/response/hello
에 있는 뷰를 반환한다.
@ResponseBody
가 있다면 Http 메세지 바디에 직접 response/hello
라는 문자가 입력된다.
✅ 메세지 바디에 직접 입력
HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메세지 바디에 HTML 데이터가 담겨서 전달된다.
@GetMapping("/responseEntity")
public ResponseEntity<Member> responseEntity(){
return ResponseEntity.ok().body(new Member(1L,"jidam"));
}
ResponseEntity는 HttpEntity를 상속받는다. Http응답의 헤더, 바디, 응답 코드를 설정할 수 있다.
return new ResponseEntity<>(member,HttpStatus.OK)
와 같이 응답할 수도 있다.
HTTP 메세지 컨버터에 의해 JSON 형식으로 변환되어서 반환한다.
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/responseBody")
public Member responseBody(){
return new Member(1L,"jidam");
}
ResponseEntity를 사용하지 않고 @ResponseBody
를 사용할 수도 있다.
이 때는 HTTP 상태코드는 @ResponseStatus(HttpStatus.OK)
와 같이 지정한다.
@RestController
애노테이션은 @Controller
+ @ResponseBody
이다.