MVC 패턴
하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것
- 모델(Model) : 뷰에 출력할 데이터를 담아두는 영역으로 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근없이, 화면을 렌더링 하는 일에 집중 가능
- 뷰(View) : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중하는 역할
- 컨트롤러(Controller) : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행하는 역할을 수행하며 뷰에 전달할 결과 데이터를 조회해서 모델에 담음
- 비즈니스 로직은 주로 서비스 계층에 있다. 즉 컨트롤러는 서비스를 호출하는 역할
Spring MVC 구조
동작 순서
- HTTP 요청이 오면 서블릿 컨테이너에 있는 DispatcherServlet의 service() 호출
- (url 패턴이 /모든경로이므로 모든 요청을 받을 수 있음.
- 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회.
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회.
- 핸들러 어댑터 실행 : 핸들러 어댑터를 실행.
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행.
- ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환.
- viewResolver 호출 : 뷰 리졸버를 찾고 실행.
- View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환.
- 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링.
- 응답을 클라이언트에게 반환.
예를 들어 다음과 같은 컨트롤러가 있다고 하자.
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
}
해당 컨트롤러는 Controller 인터페이스를 상속받았고, “/springmvc/old-controller” 라는 이름으로 스프링 빈에 등록하였다.
이 컨트롤러가 호출되려면 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑과 이를 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
HandlerMapping 우선 순위
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
HandlerAdapter 우선 순위
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
위의 경우 스프링 빈으로 등록되어있으므로 2번째 우선 순위인 BeanNameUrlHandlerMapping 가 실행에 성공하고 핸들러인 OldController가 반환된다. 이 후 HandlerAdapter 의 supports()를 순서대로 호출한다. 예시에선 Controller 인터페이스를 상속 받았으므로 3번째 우선 순위인 SimpleControllerHandlerAdapter가 실행되고, 그 결과인 new-form 이름의 ModelAndView를 반환하게 된다.
뷰 리졸버
뷰 리졸버 우선 순위
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환.
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환.
…
호출 순서
- 핸들러 어댑터 호출
- 핸들러 어댑터를 통해 new-form 이라는 뷰의 논리 이름을 획득.
- ViewResolver 호출
- new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출.
- BeanNameViewResolver 는 new-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없으므로 InternalResourceViewResolver 가 호출된다.
- InternalResourceViewResolver
- 이 뷰 리졸버는 InternalResourceView 를 반환.
- 뷰 - InternalResourceView
- InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용.
- view.render()
- view.render() 가 호출되고 InternalResourceView 는 forward()를 사용해서 JSP를 실행.
요청 매핑
consumes, produces의 차이
- consumes : 들어오는 데이터 타입을 정의한다. (클라이언트 → 서버, 데이터 타입 정의)
- HTTP 요청의 Content-Type 헤더를 기반하여 해당 미디어 타입으로 매핑
- 아래와 같이 consumes="application/json"로 정의되엉 있다면 해당 메서드는 content-type: "application/json" 인 요청만 처리 가능
@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
- produces : 반환하는 데이터 타입을 정의한다. (서버 → 클라이언트, 데이터 타입 정의)
- HTTP 요청의 Accept 헤더를 기반하여 해당 미디어 타입으로 매핑
- Accept 헤더는 클라이언트 입장에서 자신이 받을 수 있는 content-type을 나타낸 것
- 즉 아래와 같을 때 클라이언트의 요청 헤더 Accpet를 체크해서 text/html이 아니라면 에러를 발생
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
HTTP 요청 파라미터 조회
클라이언트 → 서버로 요청 데이터를 전달할 때는 3가지 방법을 사용
- GET - 쿼리 파라미터 ( /url?username=hello&age=20)
- 메시지 바디 없이, URL에 쿼리 파라미터에 데이터를 포함해서 전달하는 방법이다.
- POST - HTML Form
- 메시지 바디에 쿼리 파라미터 형식으로 전달하는 방법이다.
- POST /request-param
- content-type: application/x-www-form-urlencoded
- username=hello&age=20
- 메시지 바디에 쿼리 파라미터 형식으로 전달하는 방법이다.
- HTTP message body에 데이터를 직접 담아서 요청
요청 파라미터 조회 (1, 2번 방식)
@RequestParam
스프링은 요청 파라미터를 쉽게 사용할 수 있도록 다음과 같은 어노테이션을 지원한다.
(HttpServletRequest 의 request.getParameter() 기능)
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@RequestParam(”username”) String username과 같이 요청 파라미터의 이름으로 바인딩하여 변수에 지정할 수 있지만 요청 파라미터 이름과 변수의 이름이 같다면 위와 같이 생략 가능하다.
또한 String, int 등과 같이 단순 타입이면 @RequestParam 도 생략이 가능하다.
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@RequestParam.required (파라미터 필수 여부, default = true)
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
log.info("username={}, age={}", username, age);
return "ok";
}
위와 같이 username이 필수이고, age는 필수가 아닌 요청이 있다고 하자.
- 발생할 수 있는 경우
- username이 없다면? → 400 exception 발생
- 파라미터 이름만 있고 값이 없다면? (/request-param-required?username= ) → 빈 문자로 통과
- int 타입에 null이 들어가는 것은 불가능! → Integer를 사용하거나 defaultValue로 기본 값을 지정.
@ModelAttribute
만약 파라미터가 아닌 객체로 값을 받고 싶다면 @ModelAttribute을 사용하면 된다.
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
@Data
static class HelloData {
private String username;
private int age;
}
ModelAttribute 작동 순서
- HelloData 객체를 생성
- 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
- 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력한다.
@ModelAttribute 역시 생략이 가능하다.
@ModelAttribute와 @RequestParam 생략 시 규칙.
String , int , Integer 같은 단순 타입 = @RequestParam 나머지 = @ModelAttribute (argument resolver로 지정해둔 타입 외)
@RequestParam → 요청 파라미터를 받는 작업을 수행
@ModelAttribute → 요청 파라미터를 받고 이를 이용하여 객체 생성, 값 바인딩, Model에 담아주는 작업 수행
요청 파라미터 조회 (3번 방식, Http body에 데이터를 담아 직접 전송)
@RequestBody
스프링은 @RequestBody로 HTTP 메시지 바디 정보를 편리하게 조회할 수 있는 기능을 제공한다.
(content-type : application/json)
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@RequestBody 역시 body 정보를 한 번에 객체로 전환할 수 있다.
(HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 문자 뿐만 아니라 JSON도 객체로 변환 해줌)
- @RequestBody 요청
- JSON 요청 → HTTP 메시지 컨버터 → 객체
- @ResponseBody 응답
- 객체 → HTTP 메시지 컨버터 → JSON 응답
HTTP 메시지 컨버터
위에서 객체를 JSON으로 응답하거나 JSON으로 받은 정보를 객체로 변환하는 과정에는 메시지 컨버터라는 것이 동작한다는 것을 배웠다.
(정확히는 요청이나 응답에 @RequestBody, @ResponseBody, HttpEntity를 사용할 때 HTTP 메세지 컨버터 동작)
그렇다면 메시지 컨버터가 정확히 어떤 역할을 하고 어떻게 하는 지 알아보자.
먼저 클라이언트에서 요청이 오고 해당 요청 메서드에 @ResponseBody가 존재하면 viewResolver 대신에 HttpMessageConverter가 동작하고 해당 타입에 맞는 메시지 컨버터가 처리되어 해당 타입의 내용을 HTTP의 BODY에 직접 반환하게 된다.
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ? getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
위의 메시지 컨버터의 canRead와 canWrite를 보면 스프링 부트는 대상 클래스 타입과, 미디어 타입을 확인 후 메시지 컨버터 사용 여부를 판단한다.
- 메시지 컨버터의 종류
- ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
- 클래스 타입: byte[] , 미디어 타입: */*
- 요청 예) @RequestBody byte[] data
- 응답 예) @ResponseBody return byte[] 쓰기, 미디어 타입 application/octet-stream
- StringHttpMessageConverter : String 문자로 데이터를 처리한다.
- 클래스 타입: String , 미디어 타입 :*/*
- 요청 예) @RequestBody String data
- 응답 예) @ResponseBody return "ok", 쓰기, 미디어 타입 text/plain
- MappingJackson2HttpMessageConverter : application/json
- 클래스 타입: 객체 또는 HashMap , 미디어 타입 application/json 관련
- 요청 예) @RequestBody HelloData data
- 응답 예) @ResponseBody return helloData 쓰기, 미디어 타입 application/json 관련
이렇게 canRead, canWrite()의 조건을 만족하는 컨버터를 찾으면 read, write를 호출하여 객체를 생성, 반환하거나 http 응답 메시지 바디에 데이터를 생성한다.
그렇다면 이러한 Http Message Converter는 어디에서 사용이 될까?
처음에 있던 Spring MVC 구조에서 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter(요청 매핑 핸들러 어댑터)에 있다.
애노테이션 기반의 컨트롤러는 다양한 타입의 파라미터를 인자로 받을 수 있었다.(@RequestParam, @ModelAttribute, HttpServletRequest, Model 등등) 이렇게 파라미터를 받을 수 있는 이유는 ArgumentResolver가 애노테이션 정보를 기반으로 전달 데이터를 생성, 컨트롤러에 전달해주기 때문이다. Http 메시지 컨버터 역시 이러한 ArgumentResolver에 위치하고 있다.
즉 요청의 경우에는 @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있어서 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이고, 응답의 경우에는 @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
따라서 정리를 해보자면 아래와 같은 코드가 있다고 하자.
@ResponseBody
@RequestMapping("/test")
public Member test(@RequestBody Member member) {
System.out.println("member = " + member);
return member;
}
- 클라이언트가 /test 요청을 하면 DispatcherServlet이 요청을 받음
- URL과 매핑된 핸들러 메서드를 찾음 (위 메서드 발견)
- 핸들러 어댑터가 해당 핸들러 메서드 실행하려는데 파라미터가 Member 타입인 것을 확인 → ArgumentResover가 요청으로 온 JSON을 member 객체에 바인딩
- 핸들러 메서드가 실행되면서 위 로직 수행
- 응답이 Member 타입인 것을 확인하고 ReturnValueHandler가MappingJackson2HttpMessageConverter을 통하여 member 객체를 JSON 형태로 만듦
- 응답에 해당 JSON을 기록 후 DispatcherServlet으로 전달
- 이 경우 ViewResolver는 거치지 않고 바로 클라이언트에게 응답 전송.
만약 HTTP Body에 응답 전송이 아니라 뷰 논리 이름을 반환하는 경우라면?
- HandlerAdapter는 해당 ReturnValueHandler의 handleReturnValue()를 호출하고 Model과 뷰 이름을 가지고 ModelView를 생성. (메세지 컨버터는 사용 X)
- 생성한 ModelView를 HandlerAdapter → DispatcherServlet 순서로 반환
- DispatcherServlet은 ModelView를 ViewResolver에 전달.
- ViewResolver가 논리 이름을 가지고 뷰를 반환한 후 DispatcherServlet가 뷰의 render()를 호출하면 HTML이 생성, 클라이언트에게 응답.
JSON으로 리턴하는 경우에도 MVC인가?
공부를 하다보니 JSON으로 응답을 주는 경우에도 MVC 패턴인지 궁금증이 생겼다.
왜냐하면 MVC는 뷰에 필요한 모델, 뷰, 로직을 수행하는 컨트롤러를 의미하는데, JSON으로 반환하는 경우에는 뷰가 필요없기 때문이다.
결론부터 이야기하면 이 경우에도 MVC 패턴이라고 하는거 같다.
- Model : 데이터와 비즈니스 로직 처리
- View : 사용자 인터페이스를 담당 → 시각적인 뷰가 아닌 클라이언트에게 전송되는 데이터 형식을 의미(JSON)
- Controller : 연결부로 기존과 동일한 의미
참고
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습
www.inflearn.com
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com
'Spring' 카테고리의 다른 글
[Spring] Bean Validation (0) | 2024.12.27 |
---|---|
[Spring] 메시지, 국제화 (0) | 2024.12.27 |
[Spring] 서블릿과 컨트롤러 (0) | 2024.12.27 |
[Spring] @Qualifier, @Primary (0) | 2024.12.27 |
[Spring] 스프링 컨테이너, 빈 (0) | 2024.12.27 |