Spring MVC 핵심 개념: 구조와 동작 원리 완벽 이해
김영한님의 Spring MVC 1편을 정리한 내용입니다.
서블릿
@ServletComponentScan
스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 어노테이션을 지원한다
@WebServlet
서블릿 어노테이션
- name : 서블릿 이름
- urlPatterns : URL 맵핑
HttpServletRequest
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 파싱을 대신한다. 응답도 마찬가지로 편리하게 사용할 수 있도록 HttpServeltResponse
를 제공한다.
startline
, header
등을 쉽게 데이터를 꺼낼 수 있도록 기본 기능들도 제공된다.
HTTP 요청 데이터
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 주요 3가지 방법이 있다.
- GET - 쿼리 파라미터
/url?username=hello&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
- POST - HTML Form
content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
- 예) 회원 가입, 상품 주문, HTML Form 사용
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH
복수 파라미터에서 단일 파라미터 조회?
username=hello&username=kim
request.getParameter()
는 하나의 파라미터 이름에 대해서 단 하나의 값만 있을 때 사용 해야 하기 때문에 위와 같은 경우는 request.getParameterValues()
를 사용한다. 2개 이상일 때 getParameter
를 사용 하면 getParameterValues
의 첫 번째 값만 반환 한다.
JSON 결과를 Object에 맵핑
JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 Jackson, Gson 등 JSON 변환 라이브러리르 추가해서 사용해야 한다. 스프링 부트 +
Spring MVC
를 사용할 경우엔 Jackson 라이브러리ObjectMapper
를 함께 제공한다.
서블릿, JSP, MVC 패턴
동적인 응답을 만들기 위해 서블릿으로 자바코드와 HTML을 섞어 사용하여 코드를 작성하는 것은 복잡하고 비효율적이다.
동적으로 변하는 부분만 자바코드를 넣어 더 편리하게 사용하고자 템플릿 엔진이 탄생하였다. 템플릿 엔진을 사용하면 HTML에서 필요한 곳만 코드를 적용해 동적으로 변경할 수 있다. 템플릿 엔진의 종류로는 JSP, Thymeleaf 등이 있다.
JSP는 성능과 기능면에서 다른 템플릿 엔진에 밀려 안쓰는 추세이다. 스프링과 잘 통합되는 Thymeleaf를 많이 사용한다.
하지만, JSP 코드도 파일 내에 비지니스 로직과 결과를 보여주기 위한 HTML 로직이 섞여 있는 구조이다. 코드가 길어질 수록 유지보수는 어려우며, 화면을 변경하기 위해 서비스 로직 포함되어 있는 파일을 수정해야하는 문제가 남아 있다.
이 문제를 해결하기 위해 비지니스 로직과 화면을 그리는 부분을 나누는 MVC 패턴이 등장했다.
- 모델 : 뷰에 출력할 데이터를 담는다.
- 뷰 : 모델에 담겨 있는 데이터를 화면에 그린다 (HTML 생성)
- 컨트롤러 : HTTP 요청을 받아 파라미터를 검증하고, 뷰에 전달할 결과 데이터를 조회해 모델에 담는다
컨트롤러에 비지니스 로직을 담아 둘 수는 있지만, 역할이 커지기 때문에 비지니스 로직은 서비스 계층을 만들어서 처리한다.
컨트롤러에서 다른 서블릿이나 JSP로 이동할 수 있는 dispatcher.foward()
를 활용한다. /WEB-INF
경로안에 JSP파일을 두어 외부에서 직접 호출하지 못하도록 막고, 항상 컨트롤러를 통해 JSP 호출하도록 한다.
하지만 이런 MVC 패턴에도 한계가 존재한다.
컨트롤러 마다 뷰로 이동하는 코드나 Path를 지정하는 중복된 코드가 많기 때문이다. 또한, 기능이 추가될 수록 컨트롤러에서 공통으로 처리해야하는 부분이 점점 증가하는데 이런 부분을 메소드로 뽑아도 되지만 메소드를 항상 호출해야하고, 실수로 호출하지 않으면 문제가 발생할 수 있는 가능성도 존재하게 된다. (+ 메소드를 호출하는 것 자체도 중복이다.)
이런 문제를 해결하기 위해 컨트롤러 호출 전 공통 기능을 처리하는 프론트 컨트롤러 패턴
이 필요하고, 프론트 컨트롤러 패턴을 적용하게 된다면 스프링 MVC 와 유사한 구조가 된다.
MVC 프레임워크 만들기
스프링 웹 MVC의 핵심인 DispatcherServlet
이 FrontController
패턴으로 구현되어 있다.
프론트 컨트롤러 패턴은 아래와 같은 특징을 가지고 있다.
- 프론트 컨트롤러 서블릿 하나로만 클라이언트의 요청을 받는다
- 요청에 맞는 컨트롤러를 찾아서 호출한다
- 이런 구조를 가지므로써 공통적으로 필요한 로직을 처리할 수 있게 된다.
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용할 필요가 없어지게 된다.
강의에서는 v1부터 v5 버전까지 프론트 컨트롤러를 구현해보면서 Spring MVC와 유사한 프론트 컨트롤러를 만들어 나간다.
- v1: 프론트 컨트롤러를 도입
- 기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
- v2: View 분류
- 단순 반복 되는 뷰 로직 분리
- v3: Model 추가
- 서블릿 종속성 제거
- 뷰 이름 중복 제거
- v4: 단순하고 실용적인 컨트롤러
- 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
- v5: 유연한 컨트롤러
- 어댑터 도입
- 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
Spring MVC와 유사한 구조의 프론트 컨트롤러 구조는 아래처럼 생겼다. 위 구조에서 핸들러 어댑터는 다양한 종류의 어댑터 목록에서 핸들러를 처리할 수 있는 핸들러 어댑터가 무엇인지를 찾는 역할을 수행한다.
다음은 ControllerV3 버전을 지원하는 어댑터를 구현한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV3 controller = (ControllerV3) handler;
Map < String, String > paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private Map < String, String > createParamMap(HttpServletRequest request) {
Map < String, String > paramMap = new HashMap < > ();
request.getParameterNames().asIterator()
.forEachRemaining(paramName - > paramMap.put(paramName,
request.getParameter(paramName)));
return paramMap;
}
}
support 메소드는 객체의 타입이 controllerV3
이면 true를 반환한다.
handle
메소드에서는 V3
로 handler
를 타입 캐스팅하고, ControllerV3
버전은 ModelView
를 사용하므로 ModelView
를 반환해 준다.
이번에는 프론트컨트롤러 V5에 대해 살펴본다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map < String, Object > handlerMappingMap = new HashMap < > ();
private final List < MyHandlerAdapter > handlerAdapters = new ArrayList < > ();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter: handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
컨트롤러 뿐만 아니라 어댑터가 지원하면 어떤 것이라도 URL 맵핑을 하여 사용할 수 있기 때문에 어댑터 라는 이름으로 변경해준다.
생성자에서는 핸들러 맵핑과 어댑터를 초기화한다.
맵핑 정보는 ControllerV3, ControllerV4 와 같은 인터페이스에서 아무 값이나 모두 받을 수 있는 Object
로 변경한다. private final Map<String, Object> handlerMappingMap = new HashMap<>();
핸들러 맵핑은 handlerMappingMap
에서 URL에 맵핑된 핸들러 객체를 찾아서 반환하는 역할을 수행한다.
1
2
3
4
5
6
7
Object handler = getHandler(request)
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
getHandlerAdapter
메소드는 핸들러를 처리할 수 있는 어댑터를 조회하는 메소드이다.
이제 어댑터 핸들러에서 handle 메소드를 실행하여 실제 어댑터를 호출한다. ModelView mv = adapter.handle(request, response, handler);
다음으로 V4 컨트롤러도 사용할 수 있도록 FrontControllerServletV5에 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 추가
}
이제 ControllerV4HandlerAdapter를 작성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV4 controller = (ControllerV4) handler;
Map < String, String > paramMap = createParamMap(request);
Map < String, Object > model = new HashMap < > ();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map < String, String > createParamMap(HttpServletRequest request) {
Map < String, String > paramMap = new HashMap < > ();
request.getParameterNames().asIterator()
.forEachRemaining(paramName - > paramMap.put(paramName,
request.getParameter(paramName)));
return paramMap;
}
}
중요한 부분이 있는데 handle 메소드에서 어댑터 뷰의 이름이 아닌 ModelView
를 반환한다. 왜냐하면 형식에 맞추어서 반환해야하기 때문이다.
뷰 리졸버
스프링부트는 InternalResourceViewResolver
라는 뷰 리졸버를 자동으로 등록하는데 applciation.properties
에 등록한 spring.mvc.prefix
, spring.mvc.view.suffix
설정 정보를 사용해 등록한다.
권장하지는 않지만 설정없이 전체 경로를 명시하여 동작 시킬 수도 있다. `return new ModelAndView(“/WEB-INF/views/new-form.jsp”);
스프링부트가 자동으로 등록하는 뷰 리졸버가 있다.
- BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환
- InternalResourceViewResolver : JSP를 처리할 수 있는 뷰 반환
동작방식을 차례대로 살펴보면 아래와 같다.
1. 핸들러 어댑터 호출
핸들러 어댑터를 통해 new-form
이라는 논리 뷰 이름을 획득한다.
2. ViewResolver 호출
new-form
이라는 뷰 이름으로 viewResolver
를 순서대로 호출한다.
BeanNameViewResolver
는 new-form
이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없기 때문에 다음 순서인 InternalResourceViewResolver
가 호출된다.
3. InternalResourceViewResolver
이 뷰 리졸버는 InternalResourceView
를 반환한다.
4. InternalResourceView
InternalResourceView
는 JSP처럼 포워드 forward()
를 호출해서 처리할 수 있는 경우에 사용한다.
5. view.render()
view.render()
가 호출되고 InternalResourceView
는 forward()
를 사용해서 JSP를 실행한다.
스프링 MVC 시작하기
가장 우선순위가 높은 핸들러 맵핑과 핸들러 어댑터는 RequestMappingHandlerMapping
과 RequestMappingHandlerAdapter
이다.
@Controller
: 내부에 @Component
어노테이션이 있어 스프링이 자동으로 스프링 빈으로 등록한다. 또한, 스프링 MVC에서 어노테이션 기반 컨트롤러로 인식한다. 또한 RequestMappingHandlerMapping
에서 핸들러 정보로 인식하고 꺼낼 수 있도록 어노테이션 기반 컨트롤러로 인식시켜준다.
@RequestMapping
: 요청 정보를 맵핑하고, 해당 URL로 요청하면 어노테이션이 달린 메소드가 실행된다 어노테이션 기반으로 동작하기 때문에 메서드 이름은 자유롭게 사용할 수 있다.
RequestMappingHandlerMapping
은 스프링 빈 중 @Controller
또는 @RequestMapping
가 클래스 레벨에 적용되어 있으면 해당 클래스를 맵핑정보로 인식한다.
Spring MVC 실무적인 방식
- 파라미터를
@RequestParam
으로 받을 수 있다. - 파라미터에
model
을 전달할 수 있는데 비지니스 로직에서 필요한 데이터를 담아 줄 수 있다. - 반환을 String 타입으로 변환하고, 뷰네임을 반환한다
@RequestMapping
에서method
로 특정 http method 타입에만 호출하도록 제한을 둘 수 있다. 더 나아가@GetMapping
,@PostMapping
어노테이션으로 사용할 수 있다.
Spring MVC 기본 기능
lombok 셋팅 : 컴파일러 > 어노테이션 프로세서 > 어노테이션 프로세싱 활성화
로깅
스프링부트 라이브러리에는 spring-boot-starter-logging 라이브러리가 포함된다.
- SLF4J - 인터페이스
- logback - 구현체
@Controller
는 반환값이 String이면 뷰 이름으로 인식하여 뷰를 찾고 렌더링한다. 반면 @RestController
는 리턴되는 문자열을 HTTP 바디에 입력을 한다.
로컬에서 개발 시 모든 로그를 보고 싶으면 application.properties 에서 로깅 레벨을 설정 할 수 있다. logging.level.hello.springmvc=trace
기본은 logging.level.root=info
이다.
1
private final Logger log = LoggerFactory.getLogger(LogTestController.class);
위 코드는 로거는 @Slf4j
어노테이션으로 대체하여 사용할 수 있다. 로그 레벨이 info일 때 아래 두 로그는 찍히지 않는다. 하지만 첫 번째 로그를 출력하는 방식은 불필요하게 + 연산을 실행하게 된다. 따라서 로그를 출력할 땐 {} 오 파라미터를 이용하여 출력을 해야 한다.
log.trace("log = " + log);
log.trace("log = {}", log);
요청 맵핑
@RequestMapping
은 배열형식으로 @RequestMapping({"hello-basic", "hello-go"})
와 같이 사용도 가능하다. /hello-basic 과 /hello-baisc/은 다른 URL 이지만 스프링은 같은 요청으로 맵핑을 하고, method 속성을 명시해주지 않으면 HTTP 메서드와 무관하게 호출된다.
@PathVariable
의 이름과 파라미터 이름이 같으면 생략할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping(value = "/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
log.info("mappingPath = {}", data);
return "ok";
}
@GetMapping(value = "/mapping/{userId}")
public String mappingPath(@PathVariable String data){
log.info("mappingPath = {}", data);
return "ok";
}
@GetMapping(value = "/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable String orderId){
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
url 경로뿐만 아니라 params 로 특정 파라미터가 있으면 메소드가 호출되도록 제한을 걸 수도 있다.
또한 아래와 같이 특정 헤더와 미디어 타입을 조건을 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader(){
log.info("mappingHeader");
return "ok";
}
@PostMapping(value = "/mapping-header", consumes = "aplication/json")
public String mappingHeader(){
log.info("mappingHeader");
return "ok";
}
consume
은 요청 헤더의 Content-type
, produces
는 요청 헤더의 Accept
와 미디어 타입과 동일해야한다.
HTTP 요청 - 기본, 헤더 조회
어노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.
1
2
3
4
5
6
7
8
9
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = true) String cookie
)
{
@RequestHeader MultiValueMap<String, String> headerMap
- 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
MultiValueMap
를 사용하면 하나의 헤더(Key)에 여러가지 값을 담을 수 있다
HTTP 요청 파라미터 - 쿼리파라미터, HTML Form
클라이언트에서 서버로 요청 데이터를 전달할 때는 3가지 방법 중 하나를 사용한다
- GET - 쿼리 파라미터
- /url?username=hello&age=20 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20 예) 회원 가입, 상품 주문, HTML Form 사용
- content-type: application/x-www-form-urlencoded
GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있고 요청 파라미터 조회라고 칭한다.
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT 데이터 형식은 주로 JSON 사용 (POST, PUT, PATCH)
/rousrce/static
아래에 두면 스프링 부트가 자동으로 인식한다.
HTTP 요청 파라미터 - @RequestParam
@RequestParam
: 파라미터 이름으로 바인딩한다.
@ResponseBody
: View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력한다.
String, int 등의 단순 타입이면 @RequestParam도
생략이 가능하다.
1
2
3
4
5
6
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
그러나 너무 생략하면 코드가 한눈에 파악하기 어려울 수 있다.
필수파라미터 지정도 가능하다.
1
@RequestParam(required = true)
만약 해당 파라미터를 전달해주지 않으면 Bad Request 400 예외가 발생한다.
parameter이름만 있고 값이 없는 username= 와 같은 경우엔 빈문자로 통과가 된다. 추가로, 기본형에 null 입력하는 아래와 같은 경우엔 500에러가 발생한다. /request-param
1
@RequestParam(required = false) int age
이 때는 null을 받을 수 있도록 int → Intger형으로 변경하거나 defaultValue
를 사용한다. @RequestParam(required = false, defaultValue = "-1") int age)
defulatValue
는 빈 문자의 경우에도 설정한 기본 값이 들어간다. 사실 defulatValue가 들어가면 required가 의미가 없어진다.
파라미터를 Map으로 조회도 가능하다.
1
2
3
4
5
6
7
8
9
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"),
paramMap.get("age"));
return "ok";
}
파라미터 값이 1개가 확실하면 Map을 사용해도 되지만, 2개 이상이라면 MultiValueMap
을 사용하도록 해야한다.
@ModelAttribute
스프링MVC는 @ModelAttribute
가 있으면 다음을 수행한다
HelloData
객체를 생성- 요청 파라미터의 이름으로
HelloData
객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
@ModalAttribute
는 생량할 수 있으나 @RequestParam
도 생략 가능 하기 때문에 둘 중 어느 것인지 헷갈릴 수 있다.
String, int 와 같은 단순타입은 @RequestParam을 사용하고 ,나머지는 @ModelAttribute를 사용한다. 단, Argument Resolver는 예외다.
HTTP 요청 메시지 - 단순 텍스트
가장 간단하게는 Body 데이터를 InputStream
을 사용해서 직접 읽을 수 있다. byteCode를 문자로 받을 때는 인코딩 방식을 항상 지정해 줘야한다. 코드를 개선한다면 HttpServletRequest
을 InputStream
타입으로 직접 받도록 변경이 가능하다. 더 나아가서 메시지 컨버터를 활용한 HttpEntity<String> httpEntity
파라미터로 받아서 사용할 수 있다.
어노테이션으로도 제공이 되는데 @RequestBody
를 사용하면 된다. 응답 어노테이션도 존재하는데 @ResponseBody
라고 상요한다.
HTTP 요청 메시지 - Json
문자로 된 JSON 데이터는 Jackjson 라이브러리인 objectMapper
를 사용해서 자바 객체로 변환해도 되지만, @RequestBody
에서 객체를 바로 맵핑 해 줄 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request,
HttpServletResponse response) throws IOException {
...
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
Http Entity
, @RequestBody
를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 원하는 문자 또는 객체등으로 변환해준다. (+ json도 자동으로 변환을 해준다)
@RequestBody
는 생략이 불가능하다. 만약 어노테이션을 생략하게 되면, String
, int
, Integer
와 같은 단순타입은 @RequestParam
으로 맵핑하고, 나머지는 @ModelAtrribute
로 맵핑을 하기 때문에 HTTP 메시지 바디가 아니라 요청 파라미터로 인식하고 처리한다.
JSON 요청 시 content-type이 application/json 인지 꼭 확인한다.
추가로 HttpEntity<HelloData>
로도 값을 받을 수 있다.
반환의 경우 @ResponseBody를 사용하면 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있고, 이 경우에도 HttpEntity를 사용해도 된다.
HTTP 응답 - 정적리소스, 뷰 템플릿
스프링에서 응답 데이터를 만드는 방법은 크게 3가지이다.
- 정적 리소스
- 뷰 템플릿
- 동적인 HTML을 제공할 수 있다.
- HTTP 메시지 사용
- HTML이 아니라 HTTP 메시지 바디에 JSON 와 같은 형식의 데이터를 전달한다.
정적 리소스
스프링 부트는 classpath의 다음 디렉토리에 있는 정적 리소스를 제공한다. /static
, /public
, /resources
, /META-INF/resources
src/main/resources는 리소스를 보관하는 곳이고, classpath의 시작 경로이다. 따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
정적 리소스 경로 : src/main/resources/static
src/main/resources/static/basic/hello-form.html
에 파일이 있으면 웹 브라우저에서 다음과 같이 접근 할 수 있다. http://localhost:8080/basic/hello-form.html
뷰 템플릿
스프링 부트는 기본 뷰 템플릿 경로를 제공한다. src/main/resources/templates
뷰 템플릿을 호출하는 컨트롤러에서 String을 반환하는 경우@ResponseBody
가 없으면 response/hello
로 뷰 리졸버가 실행되어서 뷰를 찾고 렌더링한다. 만약 @ResponseBody
가 있으면 뷰 리졸버를 실행하지 않고 HTTP 메시지 바디에 직접 response/hello
라는 문자가 입력된다.
여기서 뷰의 논리 이름인 response/hello
를 반환하면 다음 경로의 뷰 템플릿이 렌더링 되는 것을 확인 할 수 있다.
타임리프 스프링부트 설정
타임리프 라이브러리를 추가하면 스프링부트가 ThymeleafViewResolver와 필요한 스프링 빈들을 등록한다. 추가로 다음 설정을 사용한다.
1
2
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
필요하면 applciation.properties에 명시하여 변경할 수 있다.
HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
ResponseEntity
은 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody
만으로는 응답 코드를 설정할 수 없다. 이때는 @ResponseStatus(HttpStatus.OK)
와 같이 어노테이션을 추가하여 응답 코드를 설정할 수 있다. 다만 어노테이션이기 때문에 동적으로는 상태를 변경하는 것은 불가하다. 동적으로 변경이 필요하다면 ResponseEntity
를 사용한다.
클래스 레벨에 @Controller
대신 @RestController
를 붙이면 컨트롤러 맵핑에 모두 @ResponseBody
가 적용된다.
HTTP 메시지 컨버터
HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.
@ResponseBody
를 사용하게 되면 아래 순서대로 동작한다.
- HTTP Body에 문자 내용을 직접 반환한다.
- ViewResolver 대신
HttpMessageConverter
가 동작한다. - 기본 문자 처리는
StringHttpMessageConverter
로하고, 기본 객체 처리는MappingJason2HttpMessageConverter
로 한다.
스프링 MVC는 @RequestBody
, @ResponseBody
가 있으면 RequestResponseBodyMethodProcessor()
을 사용하고 HttpEntity가
있으면 HttpEntityMethodProcessor()
를 사용한다.
스프링은 다음을 모두 인터페이스로 제공하기 때문에 필요 시 기능을 확장할 수 있다.
HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter
기능 확장은 WebMvcConfigurer
를 상속받아서 스프링 빈으로 등록하면 된다.