Dispatcher Servlet
오버라이딩 된 메서드 service 호출 (frontcontroller)
doDispatch() 를 이전에 작성한 V5와 비교해보자.
Object handler = getHandler(request); // v5 핸들러 조회
MyHandlerAdapter adapter = getHandlerAdapter(handler); // 핸들러 어댑터 조회
ModelView mv = adapter.handle(request, response, handler);
// 핸들러 어댑터 실행, 핸들러 어댑터를 통해 핸들러 실행, ModelView 반환
MyView view = viewResolver(viewName); // 뷰 리졸버
view.render(mv.getModel(), request, response); // 뷰 렌더링 호출
---------------------------------------------------------------------------------------------
mappedHandler = getHandler(processedRequest); // Dispatcher 핸들러 조회
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 핸들러 어댑터 조회
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 핸들러 어댑터 실행, 핸들러 어댑터를 통해 핸들러 실행, ModelandView 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request) // 뷰 리졸버
view.render(mv.getModelInternal(), request, response); // 뷰 렌더링 호출
핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러) 조회
핸들러를 실행할 수 있는 핸들러 어댑터 조회
핸들러 어댑터 실행
핸들러 실행
ModelandView 반환
뷰 리졸버 호출
뷰 렌더링
핸들러 매핑 - 해당 URI에 맞는 컨트롤러를 찾음
핸들러 어댑터 - 핸들러 매핑을 통해서 찾은 핸들러를 종류와 상관없이 실행시킬 수 있게 하는 어댑터 조회( supports() )
핸들러 어댑터 실행 - 어댑터를 실행하면서 핸들러 정보도 함께 넘겨줌, 해당 어댑터는 핸들러를 내부에서 실행 후, 반환
정리하자면 매핑을 통해 컨트롤러를 찾은 후 이 컨트로를 실행시킬 수 있는 어댑터가 있는지 조회한 다음, 해당 어댑터를 실행시킨다(ex handle) 이 후 어댑터의 실행 메서드에서 컨트롤러에게 데이터를 넘긴 후 이를 반환 받고, ModelandView 타입으로 어댑터를 호출한 곳에 반환한다.
springmvc v1
@Controller : 스프링이 자동으로 스프링 빈으로 등록. 내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨) 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식.
@RequestMapping : 요청 정보를 매핑. 해당 URL이 호출되면 이 메서드가 호출. 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 작성.
ModelAndView : 모델과 뷰 정보를 담아서 반환.
스프링 부트 3.0 부터는 클래스 레벨에 @Controller만 @RequestMappingHandlerMapping에서 인식한다. 따라서 클래스 레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않는다.
springmvc v2
하지만 v1에서 @RequestMapping이 클래스 단위가 아니라 메서드 단위로 적용된 것을 알 수 있다 이를 기반으로 한 클래스로 유연하게 모든 메서드들을 통합할 수 있다.
이 때 중복되는 URI를 클래스의 @RequestMapping에 작성하고 변경되는 URI 즉, 논리 이름만 메서드에서 작성할 수 도 있다.
springmvc v3
v2까지는 매개변수로 ModelAndView 타입으로 반환했다면, 이제부터는 Model 없이 논리이름만 가지고 반환할 수 있다. 매개변수로 Model 파라미터를 받음으로써 더 이상 ModelAndView가 아닌 String 타입으로 뷰의 논리이름만 반환한다.
스프링은 HTTP의 요청 파라미터를 @RequestParam으로 받을 수 있다.
즉 기존의 request.getParameter("username")의 역할을 @RequestParam("username")가 수행하게 된다.
지난 버전들은 HTTP 메서드의 구분없이 모든 데이터를 요청 및 응답 받았다. 하지만 이제부터는
해당 HTTP 메서드 + Mapping(@GetMapping, @PostMapping)을 이용하여 HTTP 메서드를 명시함과 동시에 명확한 정보를 제공할 수 있다
그 동안 작성했던 코드의 최종 코드는 다음과 같다.
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private 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";
}
}
단 30줄에 가까운 코드로 여태까지 공부해온 코드를 줄이다니..... 스프링은 마법인가?
이전까지 단순히 MVC의 구조에 대해서 학습을 했다면 지금부터는 HTTP 요청,응답에 대해서 좀 더 자세하게 알아보자
크게 세 가지 방식으로 나누어서 이해하고자 한다.
1.쿼리 파라미터, HTML form을 기반으로한 요청
2.message body의 내용을 기반으로한 요청
3.응답
내용이 생각보다 꽤 많지만 차근차근 정리해보는 게 도움이 될 듯 하다.
우선 세 가지 방식에 대해서 정리하기 전에 HTTP의 메서드 매핑, 경로 변수 사용, 특정 헤더 조건 매핑, 미디어 타입 조건 매핑에 대해서 간단히 짚고 넘어가면 좋을 듯 하다.
1.HTTP 메서드 매핑 - GET,POST,PUT,DELETE와 같은 http메서드에 맞게 매핑된다 @http 메서드 Mapping으로 활용
2.경로변수 - 아래의 코드를 참고
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
log.info("mappingPath userId={}", data);
return "ok";
}
3.특정 헤더 조건 매핑
/**
* 특정 헤더로 추가 매핑
* 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";
}
4.미디어 타입 조건 매핑 - 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 = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
GET - 쿼리 파라미터 / POST HTML form
HTML form은 메세지 바디에 쿼리 파라미터 형식으로 전달한다 따라서 이 요청 데이터를 전달할 때는 두 방식이 같은 방법을 공유한다고 할 수 있다. (요청 파라미터)
1.HttpServletRequest, request.getParameter
@RequestMapping("/request-param")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
response.getWriter().write("ok");
}
2.@RequestParam
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge)
이 때, 변수 이름이 파라미터 이름과 같다면 @RequestParam의 괄호를 삭제를 할 수 있다.
public String requestParamV3(
@RequestParam String username,
@RequestParam int age)
3. 해당 파라미터를 받는 변수가 String,int,Integer 등의 단순 타입이면 @RequestParam도 생략 가능하다
public String requestParamV4(String username, int age)
참고로 @RequestParam을 생략하면 필수여부가 false로 설정된다. 필수여부랑 해당 파라미터의 값이 입력되어야 하는지에 대한 여부인데 만약 특정 파라미터의 필수여부가 required = false로 설정되면 해당 파라미터를 입력하지 않아도 클라이언트는 이동할 수 있다.
파라미터 이름만 있고 값이 없는 경우에는 빈 문자로 판단되어 통과된다
값을 입력하지 않으면 이는 null로 입력되는데, 만약 해당 파라미터를 받는 변수가 null을 받을 수 없는 primitive 타입일 경우 서버에 오류를 야기시켜 500예외가 발생한다.
이 때 @RequestParam의 괄호 안에 defaultValue를 정의해주면 값을 입력하지 않아도 해당 defaultValue가 입력된다.
4. Map
public String requestParamMap(@RequestParam Map<String, Object> paramMap)
key = value로 해당 파라미터는 username = kim , age = 27 과 같은 형태로 전달된다.
이전의 String으로 반환하는 방식과 달리 @ModelAttribute를 사용하면 객체에 해당 파라미터 값들을 바인딩할 수도 있다.
참고로 스프링은 String,int,Integer와 같은 단순 타입에선 해당 에너테이션이 생략시 @RequestParam로 판단하고, 나머지는 @ModelAttribute가 생략된 것으로 판별한다.
@Data
public class HelloData {
private String username;
private int age;
}
parameter의 값을 속성으로 가질 객체를 만들 클래스 생성
5.@ModelAttribute
public String modelAttributeV2(@ModleAttribute HelloData helloData) {
return "OK";
원래라면 @ModelAttribute도 생략 가능하지만, 이 경우 @RequestParam을 생략한건지 @ModelAttrubute를 생략한 건지 한 번에 파악하기 힘들기 때문에 신중하게 사용하는 것이 권장된다.
HTTP messageBody, 단순 텍스트
위에서 배운 요청파라미터와 다르게 HTTP 메세지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.(요청 메세지)
주로 HTTP API에서 사용되는 데이터 형식은 JSON이다. 따라서 이 JSON을 기반으로 messageBody가 넘어오게 되면 어떻게 처리하는지에 대해서 알아보자.
1.InputStream(Reader), OutputStream(Writer)
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
response.getWriter().write("OK");
}
이 때, Http에서 바디로 넘어온 내용헤 대한 문자기준을 명시적으로 정해주어야 한다.
위의 코드를 좀 더 간결하게 변경하면
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
와 같이 작성될 수 있다.
2.HttpEntity
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
HttpEntity
Http header, body 정보를 편리하게 직접 조회,
요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 관계가 없음
(요청 파라미터를 조회 = @RequestParam, @ModelAttribute // 메세지 바디를 직접 조회 = @RequestBody)
응답에도 사용될 수 있으며 view 조회는 할 수 없음
RequestEntity
HttpEntity를 상속받음, url 정보 추가, 요청에서 사용
ResponseEntity
Http 상태 코드 설정 가능, 응답에서 사용
3.@RequestBody
Http 바디 정보를 편리하게 조회
헤더 정보가 필요하다면 HttpEntity, @RequestHeader를 사용
이전까지는 단순히 메세지 바디를 직접 조회하고 이를 활용했다면 지금부터는 JSON 데이터 형식을 사용해서 http Body를 조회해보자
4.@RequestBody 문자 변환
private ObjectMapper objectMapper = new ObjectMapper();
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username = {}", helloData.getUsername());
log.info("age={}", helloData.getAge());
return "ok";
}
objectMapper - messageBody를 조회하고 문자로 된 JSON데이터를 Jakson라이브러리 objectMapper를 사용해서 해당 데이터를 자바 객체로 변환
하지만 이러한 과정은 문자로 변환하고, 다시 JSON으로 변환하고 객체로 변환하는 과정이 불편하다.
5.@RequestBody 객체 변환
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
return "ok";
}
@RequestBody에 직접 만든 객체를 지정할 수 있다.
HttpEntity, @RequestBody를 사용하면 HTTP 메세지 컨버터가 HTTP 메세지의 바디의 내용을 우리가 원하는 문자나 객체등으로 변환해준다.
HTTP 메세지 컨버터는 JSON도 객체로 변환해주는데 이에 관한 내용은 후술하겠다.
이 때 @RequestBody는 생략이 불가능하다. 만약 생략하게되면, 스프링은 생략된 에너테이션을 @ModelAttribute로 적용되어 HTTP 메세지 바디가 아닌 해당 객체의 파라미터를 처리하게 된다. 즉 원하는 messageBody의 데이터를 조회하는 게 아닌 객체 파라미터의 값을 조회하게 된다.(높은 확률로 null, 0)
6.HttpEntity
@RequestBody에 HttpEntity를 사용할 수도 있다.
7.객체 자체를 반환
public HelloData requestBodyJsonV4(@RequestBody HelloData helloData) {
return helloData;
}
@RequestBody 요청 : JSON 요청 - HTTP 메세지 컨버터 - 객체
@ResponseBody 응답 : 객체 - HTTP 메세지 컨버터 - JSON 응답
HTTP 응답
1. 정적 리소스 : 해당 파일을 변경없이 그대로 서비스
2.뷰 템플릿 사용 : 뷰 템플릿을 거처서 HTML이 생성되고 뷰가 응답을 만들어서 전달
@ResponseBody가 없으면 RequestMapping 안의 경로로 뷰 리졸버가 실행되어 뷰를 찾고 렌더링
@ResponseBody가 있으면 뷰 리졸버를 실행하지 않고 HTTP 바디에 직접 response/hello라는 문자가 입력되므로 주의
3.HTTP 메세지 사용
HTTP 응답 - HTTP API, 메세지 바디에 직접 입력
1.서블릿을 직접 다루기
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
HttpServletResponse를 이용
2.ResponseEntity
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("Ok", HttpStatus.OK);
}
추가로 HTTP 상태코드를 설정할 수 있다. 그리고 HTTP 메세지 컨버터를 통해서 HTTP 메세지 바디에 직접 내용을 입력한다
3.@ResponseBody
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "OK";
}
@ResponseBody도 HTTP 메세지 컨버터를 통해서 HTTP 메세지 바디에 직접 내용을 입력한다,
JSON 응답
4.ResponseEntity
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonv1() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
ResponseEntity를 반환 HTTP 메세지 컨버터가 반환 된 데이터를 JSON으로 변환하여 반환
5.@ResponseBody
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonv2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
@ResponseBody는 HTTP 상태 코드를 설정할 수 없다. 하지만 @ResponseStatus(HttpStatus.해당 상태) 에노테이션을 사용하면 상태코드를 설정할 수 있다. 물론 에너테이션이기 때문에 해당 상태 코드를 동적으로 변경할 수 없다. 프로그램의 조건에 따라서 상태코드를 변경하고 싶다면 ResponseEntity를 사용하면 된다.
추가적으로 중복되는 @ResponseBody가 있을 경우 해당 클래스에 @ResponseBody를 적어주면 된다. 이 경우 @Controller와 @ReseponseBody가 같이 있게 되는데 이 둘의 역할을 모두 수행할 수 있는 것이 @RestController이다.
HTTP 메세지 컨버터
JSON 데이터를 HTTP 메세지 바디에서 직접 읽거나 쓰는 경우 HTTP 메세지 컨버터는 이를 자동으로 변환해준다.
@ResponseBody를 사용하면 HTTP의 바디에 문자 내용을 직접 반환하는데 이 때 해당 문자 내용을 ViewResolver가 아닌 Http 메세지 컨버터가 처리한다.
기본 문자 처리는 StringHttpMessageConverter
기본 객체 처리는 MappingJackson2HttpMessageConverter
이 외에도 여러 Http 메세지 컨버터가 등록 되어 있다.
참고로 응답의 경우에는 클라이언트가 받고자 하는 형식을 기재한 Accept 헤더와 서버의 컨트롤러 반환 타입 정보를 조합해서 Http 메세지 컨버터가 선택된다.
요청 매핑 핸들러 구조
이러한 Http 메세지 컨버터는 핸들러 어댑터에서 컨트롤러를 호출하기 전에 작동한다
핸들러 어댑터는 컨트롤러의 파라미터, 에너테이션 정보를 기반으로 전달 데이터를 생성하는 ArgumentResolver에게 원본 데이터를 넘기고 이 ArgumentResolver는 해당 데이터를 적절한 파라미터로 변환한다.
이후 컨트롤러는 반환 값을 ReturnValueHandler에게 넘겨서 이를 적절하게 가공한다. 컨트롤러가 String으로 뷰 이름을 반환해도 동작하는 이유가 바로 이 때문이다.
ArgumentResolver와 ReturnValueHandler는 Http 메세지 컨버터를 이용하여 컨트롤러가 원하는 파라미터로 변환함을 알 수 있다.
'Spring > Spring MVC' 카테고리의 다른 글
메시지/국제화 (0) | 2024.04.22 |
---|---|
MVC 5 - (검증오류), 로그인, 필터, 인터셉터, 예외처리 (0) | 2023.06.27 |
MVC 4 - 검증 및 오류 처리 (0) | 2023.06.25 |
MVC 2 - FrontController과 프레임워크의 확장 (0) | 2023.06.22 |
MVC 1 - 서블릿, JSP (0) | 2023.06.21 |