Search
Duplicate

스프링 MVC - 기본 기능

목차

프로젝트 생성

사전 준비물

JDK 11 +
IDE: IntelliJ or Eclipse( or else ...)

프로젝트 생성

스프링 공식 홈페이지에서 프로젝트 구성 (https://start.spring.io)
IntelliJ Ultimate 이상이면 자체적으로 생성가능

프로젝트 구성

Progject: Gradle Project
Language: Java
Spring Boot: 2.4.x
Project Metadata
Group: hello
Artifact:springmvc
Name:springmvc
Package name: hello.springmvc
Packaging: Jar(주의!)
Java: 11

의존성 관리

Spring Web
Lombok
Thymeleaf

참고

Packaging을 War가 아니라 Jar를 선택해야 한다. JSP를 사용하지 않기에 Jar를 사용하는것이 좋고 스프링 부트에서는 주로 이 방식(Jar)를 사용하게 된다.
Jar를 선택하면 항상 내장 서버(톰캣)를 사용하고 webapp 경로도 사용하지 않는다. 내장 서버 사용에 최적화된 기능이다. War는 내장서버도 사용가능 하지만, 주로 외부 서버에 배포하는 목적으로 사용한다.

참고

롬복(Lombok)설정이나 기타 설정, 포스트맨 설치등에 대해서는 이전에 작성한 프로잭트 생성을 살펴보도록 한다.

참고: Git Repository

해당 프로젝트 생성법에 익숙하거나 따로 만드는게 귀찮다면 해당 git repository를 사용하자.

로깅 간단히 알아보기

지금까지는 콘솔창에 실행결과나 기대값을 System.out.println("") 을 통해 출력을 했다.
하지만 실제 운영을 할때는 시스템콘솔이 아닌 별도의 로깅 라이브러리를 사용해 출력을 한다.
여기서는 많은 로깅 라이브러리중 SLF4J, Logback정도만 알아보도록 한다.

로깅 라이브러리

스프링 부트 라이브러리를 사용할 경우 스프링 부트 로깅 라이브러리(spring-boot-starter-loggin)이 포함되는데 이 라이브러리는 내부에 다음 로깅 라이브러리가 사용된다.
로그 라이브러리는 Logback, Log4J, Log4J2 등 정말 많은 라이브러리가 있는데 이를 통합해서 제공하는게 SLF4J 이고 이 인터페이스의 구현체로 Logback과 같은 로그 라이브를 선택해서 사용한다.

사용법 - 선언

로깅을 사용하려면 우선 로깅 객체를 생성해야하는데, 다음과 같이 선언해서 사용한다.
1.
클래스 참조변수 선언
/* getClass()메서드를 통해 사용되는 클래스 타입 반환하여 삽입 */ private Logger log = LoggerFactory.getLogger(getClass()); /* 직접적으로 해당 클래스타입을 입력해줘도 된다. */ private static final Logger log = LoggerFactory.getLogger(Xxx.class);
Java
복사
2.
롬복(Lombok)사용
@Slf4j public class TestController { ... }
Java
복사
롬복라이브러리를 사용한다면 @Slf4j 애노테이션으로도 사용 가능하다.

코드

LogTestController
package hello.springmvc.basic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class LogTestController { private final Logger log = LoggerFactory.getLogger(getClass()); @RequestMapping("/log-test") public String logTest() { String name = "Spring"; log.trace("trace log={}", name); log.debug("debug log={}", name); log.info("info log={}", name); log.warn("warn log={}", name); log.error("error log={}", name); return "ok"; } }
Java
복사
@RestController
@Controller는 반환값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링된다.
@RestController는 반환 값으로 뷰를 찾는게 아니라 HTTP 메세지 바디에 바로 입력한다.
⇒클래스레벨이아닌 메서드레벨에서 @ResponseBody를 사용하면 @Controller를 사용하더라도 바로 HTTP 메세지 바디에 입력해서 반환을 해준다.
로그 출력 포맷
시간, 로그 레벨, 프로세스 ID(PID), 쓰레드 명, 클래스 명, 로그 메세지

실행결과는 5개의 로그가 다 나오지 않는 이유

해당 로깅 컨트롤러를 구현해서 테스트를 해본 뒤 실행결과를 보면 로그 출력 코드를 5개 작성했으니 5개가 나와야하지만 3개밖에 나오지 않았을 것이다. 왜그럴까?
로그에는 레벨이 있다. 그래서 로그레벨을 설정하면 그 로그 보다 우선순위가 높은 것만 출력이 되는데, 스프링 부트에서 기본으로 설정되어 있는 로그레벨은 info 이다. 그렇기에 info보다 우선순위가 낮은 debug, trace는 출력되지 않는다.
만약, 이런 로그를 임의로 내가 원하는대로 변경하고자 한다면 설정파일(application.properties)에서 레벨을 변경해주면 된다.
application.properties
#전체 로그 레벨 설정(기본 info) lolgging.level.root=info #hello.springmvc 패캐지와 그 하위 로그 레벨 설정 logging.level.hello.springmvc=[변경을 원하는 로그 레벨]
YAML
복사
로그 레벨
⇒ TRACE > DEBUG > INFO > WARN > ERROR
⇒ 개발서버는 debug 출력
⇒ 운영서버는 info 출력

올바른 로그 사용법

시스템 콘솔 출력을 작성할때 우리는 지금까지 주로 다음과 같이 작성을 했다.
기존의 문자열 결합을 이용한 출력문 사용
System.out.println(name + "님 안녕하세요."); /*로그도 위와같이 사용한다면?*/ log.debug(name + "님 안녕하세요.");
Java
복사
로그 레벨을 info로 설정해도 해당 코드에 있는 name + "님 안녕하세요." 는 평가되며 연산이 되버린다. 즉, 더하기 연산이 발생해버린다. 말인즉슨, 자바 컴파일 시점에서 사용하지도 않는 debug레벨에 있는 연산을 평가해버리니 리소스 낭비다.
새로운 방식의 로그 출력 방식
log.debug("{} 님 안녕하세요.", name);
Java
복사
로그 출력레벨이 debug 이상이면 debug내의 연산은 수행되지 않는다.

로그 사용시 장점

쓰레드 정보, 클래스 이름같은 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고 운영서버에선느 출력하지 않게끔 로그를 조절할 수 있다.
콘솔에만 출력하는게 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다.
특히 파일로 남길 때 일별, 특정 용량에 따라 로그를 분할하는것도 가능하다.
성능도 System.out보다 좋다.(내부 버퍼링, 멀티 쓰레드 등)

요청 매핑

요청이 왔을때 어떤 컨트롤러에서 매핑을 할지 조사해서 매핑을 진행하는 것.

코드

MappingController 완성본
기본 매핑(RequestMapping)
/** * 기본 요청 * 둘 다 허용한다 /hello-basic, /hello-basic/ * HTTP 메서드 모두 허용 GET, POST, HEAD, PUT, PATCH, DELETE */ @RequestMapping("/hello-basic") public String helloBasic() { log.info("helloBasic"); return "ok"; }
Java
복사
@RequestMapping("/hello-basic")
/hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다.
⇒ 대부분의 속성을 배열[]로 제공하기에 다중 설정도 가능하다.
(ex: {"/hello-basic", "/hello-go"})
⇒ method 속성으로 HTTP 메서드를 지정하지 않으면 모든 메서드에 무관하게 호출된다.
(GET, HEAD, POST, PATCH, DELETE)
method 특정 HTTP 허용 매핑
/** * method 특정 HTTP 메서드 요청만 허용한다. * GET, HEAD, POST, PUT, PATCH, DELETE */ @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET) public String mappingGetV1() { log.info("mappingGetV1"); return "ok"; }
Java
복사
method가 GET일 경우에만 매핑이되며 다른 방식으로 요청하면 HTTP 405(Method Not Allowd)가 반환된다.
HTTP 메서드 매핑 축약
/** * 편리한 축약 애노테이션 * * @GetMapping * @PostMapping * @PutMapping * @DeleteMapping * @PatchMapping */ @GetMapping(value = "/mapping-get-v2") public String mappingGetV2() { log.info("mapping-get-v2"); return "ok"; }
Java
복사
매번 method 속성을 설정해서 HTTP 메서드를 지정해주는게 번거롭고 가독성도 떨어지기에 전용 애노테이션을 만들어서 해결한다.
GetMapping, PostMapping, PatchMapping, DeleteMapping등 이름에 의미를 부여해 더 직관적이다.
⇒ 애노테이션 내부에는 @RequestMappingmethod를 미리 지정해놨다.
PathVariable(경로 변수)를 사용한 매핑
/** * PathVariable 사용 * 변수명이 같으면 생략 가능 * * @PathVariable("userId") String userid -> @PathVariable userId */ @GetMapping("/mapping/{userId}") public String mappingPath(@PathVariable String userId) { log.info("mappingPath userId={}", userId); return "ok"; }
Java
복사
최근 HTTP API는 위와 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다고 한다.
/mapping/userA
/users/1
@RequestMapping은 URL 경로를 템플릿화 할 수 있는데 @PathVariable 애노테이션을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
@PathVariable의 이름과 파라미터 이름이 같으면 생략 가능하다.
@PathVariable("username") String username → PathVariable String username
PathVariable 다중 사용
/** * PathVariable 다중 사용 */ @GetMapping("/mapping/users/{userId}/orders/{orderId}") public String mappingPath(@PathVariable String userId, @PathVariable String orderId) { log.info("mappingPath userId={}, orderId={}", userId, orderId); return "ok"; }
Java
복사
하나 이상의 PathVariable도 사용이 가능하다.
특정 파라미터 조건 매핑
/** * 파라미터로 추가 매핑 * params="mode", * params="!mode", * params="mode=debug", * params="mode!=debug"(!=) * params={"mode=debug", "data=good"} */ @GetMapping(value = "/mapping-param", params = "mode=debug") public String mappingParam() { log.info("mappingParam"); return "ok"; }
Java
복사
특정 파라미터를 조건식으로 매핑해서 매핑여부를 결정할 수 있다.
http://localhost:8080/mapping-param?mode=debug
잘 사용하지 않는다.
특정 헤더 조건 매핑
/** * 특정 헤더로 추가 매핑 * 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"; }
Java
복사
특정 파라미터 매핑과 동일하게 헤더 역시 조건매핑이 가능하다.
미디어 타입 조건 매핑 - HTTP 요청 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"; }
Java
복사
HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.
일치하지 않을 경우 HTTP 415(Unsupported Media Type)을 반환한다.
조건을 배열로 설정할수도 있고 상수로 제공하는 매직넘버를 사용해도 된다.
⇒ 사용 예시
consumes = "application/json" consumes = {"text/plain", "application/*"} consumes = MediaType.TEXT_PLAIN_VALUE
Java
복사
미디어 타입 조건 매핑 - HTTP 요청 Accept, produce
/** * Accept 헤더 기반 Media Type * produces="text/html", * produces="!text/html", * produces="text/*", * produces="*\/*" */ @PostMapping(value = "/mapping-produces", produces = "text/html") public String mappingProduces() { log.info("mappingProduces"); return "ok"; }
Java
복사
HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
만약 맞지 않으면 HTTP 406(Not Acceptable)을 반환한다.

요청 매핑 - API 예시

이제 회원관리 웹 애플리케이션에 필요한 HTTP API를 구조만 만들어보자.

회원 관리 API

회원 목록 조회 : GET /users
회원 등록 : POST /users
회원 조회 : GET /users/{userId}
회원 수정 : PATCH /users/{userId}
회원 삭제 : DELETE /users/{userId}

코드

MappingClassController
package hello.springmvc.basic.requestmapping; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/mapping/users") public class MappingClassController { @GetMapping public String user() { return "get users"; } @PostMapping public String addUser() { return "add user"; } @GetMapping("/{userId}") public String findUser(@PathVariable String userId) { return "get userId= " + userId; } @PatchMapping("/{userId}") public String updateUser(@PathVariable String userId) { return "update userId= " + userId; } @DeleteMapping("/{userId}") public String deleteUser(@PathVariable String userId) { return "delete userId= " + userId; } }
Java
복사
@RequestMapping("/mapping/users")
⇒ 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.

HTTP 요청 - 기본, 헤더 조회

애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.
스프링에서는 아주 유연하게 컨트롤러의 메서드가 요구하는 파라미터를 정말 대부분 지원을 해주는데, 코드를 통해 알아보자.

코드

RequestHeaderController
package hello.springmvc.basic.request; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; @Slf4j @RestController public class RequestHeaderController { @RequestMapping("/headers") 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("host = {}", host); log.info("cookie = {}", cookie); return "ok"; } }
Java
복사
HttpMethod
⇒ HTTP 메서드를 조회한다(org.springframework.http.HttpMethod)
Locale
⇒ Locale 정보를 조회한다.(ko-kr, euc-kr, kr ...)
@RequestHeader MultiValueMap<String, String> headerMap
⇒ 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
@RequestHeader("host")String host
⇒ 특정 HTTP 헤더를 조회한다.
⇒ 속성
필수 값 여부(required)
기본 값 속성(defaultValue)
@CookieValue(value = "myCookie", required = false)String cookie
⇒ 특정 쿠키를 조회한다.
⇒ 속성
필수 값 여부(required)
기본 값 속성(defaultValue)

참고: MultiValueMap

Map과 유사하지만 하나의 키에 여러 값을 받을 수 있다.
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
⇒ keyA=value1&keyA=value2
MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("keyA", "value1"); map.add("keyA", "value2"); //[value1, value2] List<String> values = map.get("keyA");
Java
복사

HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

HTTP 요청 메세지를 개발자가 사용하기 편하게 변환해 제공하는 것이 HttpServletRequest 객체다.
이 객체내의 getParameter()를 이용하면 요청 파라미터를 조회할 수 있는데, queryString으로 요청 메세지를 전달하는 것은 GET, 쿼리파라미터 전송과 POST HTML Form 전송방식이다.
GET 쿼리 파라미터 전송
http://localhost:8080/request-param?username=hello&age=20
POST, HTML Form 전송
POST /request-param ... content-type: application/x-www-form-urlencoded username=hello&age=20
Java
복사
위 두 방식은 모두 형식이 동일하기에 구분없이 getParameter() 메서드를 이용해 조회할 수 있는데 이를 요청 파라미터(request parameter)조회라 한다.

코드

Post Form 페이지 코드
RequestParamController
package hello.springmvc.basic.request; import hello.springmvc.basic.HelloData; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; @Slf4j @Controller public class RequestParamController { /** * 서블릿 시절 사용하던 쿼리 스트링 추출 방식 */ @RequestMapping("/request-param-v1") public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException { String username = request.getParameter("username"); int age = Integer.parseInt(request.getParameter("age")); log.info("username={}, age={}", username, age); response.getWriter().write("ok"); } }
Java
복사
반환 타입이 없으면서 응답에 값을 직접 입력("ok") 하면 view 조회를 할 수 없다.
이전 서블릿 코드를 구현하던 시절과 같이 HttpServletRequest에서 getParameter로 요청 파라미터를 조회했다.

참고

Jar를 사용하면 webapp 경로 사용을 못하기에 정적 리소스도 클래스 경로에 함께 포함해야 한다.

HTTP 요청 파라미터 - @RequestParam

스프링이 제공하는 애노테이션인 @RequestParam을 사용하면 위에서 사용한 getParameter 메소드를 통해 꺼내는 대신 매개변수 레벨에서 더 빠르게 바로 꺼내 사용할 수 있다.

코드

RequestParamController.requestParamV2
/** * RequestParam 애노테이션을 활용해 내부 속성으로 쿼리 스트링의 Key를 작성해서 해당 key 의 value 추출 */ @RequestMapping("/request-param-v2") @ResponseBody public String requestParamV2(@RequestParam("username") String memberName, @RequestParam("age") int memberAge) { log.info("username={}, age={}", memberName, memberAge); return "ok"; }
Java
복사
@RequestParam("username")
⇒ 파라미터 이름으로 바인딩
@ResponseBody
⇒ View 조회를 무시하고, HTTP message body에 직접 해당 내용을 입력
⇒ 클래스레벨에서 @Controller를 사용하는 경우 메서드레벨에서 해당 애노테이션을 사용해서 메세지 바디에 직접 내용입력하는게 가능하다.
RequestParamController.requestParamV3
/** * 매개변수의 이름이 쿼리파라미터의 key와 이름이 동일하면 속성을 빼도 동작한다. */ @RequestMapping("/request-param-v3") @ResponseBody public String requestParamV3(@RequestParam String username, @RequestParam int age) { log.info("username={}, age={}", username, age); return "ok"; }
Java
복사
HTTP 파라미터 이름이 변수 이름과 같을경우 파라미터 속성 생략이 가능하다.
@RequestParam("username") String username → @RequestParam String username
RequestParamController.requestParamV4
/** * 쿼리 파라미터의 Key가 일치하면 애노테이션을 제거해도 동작한다. */ @RequestMapping("/request-param-v4") @ResponseBody public String requestParamV4(String username, int age) { log.info("username={}, age={}", username, age); return "ok"; }
Java
복사
String, int, Integer 등의 단순 타입이면 @RequestParam도 생략이 가능하다.
지금까지는 @RequestParam의 속성중 바인딩을 위한 요청 파라미터 이름만 사용했는데, 그 외에도 여러 속성이 있다. 다음 소개할 속성은 required 라는 속성으로 해당 파라미터의 필수 여부를 설정할 수 있다.
RequestParamController.requestParamRequired
@RequestMapping("/request-param-required") @ResponseBody public String requestParamRequired(@RequestParam(required = true) String username, @RequestParam(required = false) Integer age) { log.info("username={}, age={}", username, age); return "ok"; }
Java
복사
@RequestParam.required
⇒ 파라미터 필수 여부
⇒ 기본값은 파라미터 필수 (true)이다.
해당 파라미터를 공백(ex: username=)으로 전송하면 빈 문자로 통과된다.
requiredtrue인 파라미터를 보내주지 않으면 400 예외(BAD_REQUEST)가 발생한다.
원시타입은 null이 들어갈 수 없어서 requiredfalse여도 500에러가 발생한다.
int형으로 에러가 발생하면 Integer같은 wrapper 타입을 사용해야 한다.
⇒ 혹은 기본값을 설정해주는 defaultValue를 사용하면 된다.
속성중 필수 여부 속성(required)를 설정할수 있다. 그럼 필수인데 값을 매번 공통 초기값을 넣거나 기본값이 필요한경우에는 어떻게 해야할까? 속성중에는 defaultValue라는 기본값 속성이 있다.
RequestParamController.requestParamDefault
@RequestMapping("/request-param-default") @ResponseBody public String requestParamDefault(@RequestParam(defaultValue = "catsbi") String username, @RequestParam(defaultValue = "20") int age) { log.info("username={}, age={}", username, age); return "ok"; }
Java
복사
파라미터가 없는 경우 기본값으로 설정한 값이 적용된다.
이미 기본값이 있기에 required는 의미가 없어 빼도 된다.
빈 문자("")의 경우에도 설정한 기본 값이 적용 된다.
⇒ 요청(?age=) 을 공백으로 하면 int 기본형이라 null을 받아들일 수 없어 에러가 나야하지만 defaultValue로 설정한 값이 적용되어 age에 20이 주입된다.
지금까지 요청 파라미터를 하나하나씩 받고 있는데 Map을 이용해 한 번에 받을 수도 있다.
RequestParamController.requestParamMap
@RequestMapping("/request-param-map") @ResponseBody public String requestParamMap(@RequestParam Map<String, Object> paramMap) { log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age")); return "ok"; }
Java
복사
파라미터를 Map, MultiValueMap으로 조회할 수 있다
@RequestParam Map
→Map(key=value)
@RequestParam MultiValueMap
→ MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])
파라미터의 값이 1개가 확실하면 Map을 써도 되지만 그렇지 않다면 MultiValueMap을 사용하자.

HTTP 요청 파라미터 - @ModelAttribute

위에서 @RequestParam을 이용해 요청 파라미터를 하나하나 받아줬다. 애노테이션을 이용해 최대한 편하게 받아줬지만, 요청 파라미터가 하나의 객체가 되야하는 경우 각각 파라미터 요청을 조회해서 객체에 값을 넣어서 생성해주는 작업을 해야하기 때문에 번거롭다. 예를들어 username과 age 필드가 있는 HelloData라는 객체가 있다면 지금까지 배운 지식만 가지고 코드를 구현함녀 다음과 같다.
@RequestParam을 이용한 객체 HelloData객체 생성
@RequestMapping("/model-attribute-v1") @ResponseBody public String modelAttributeV1(@RequestParam(defaultValue = "catsbi") String username, @RequestParam(defaultValue = "20") int age) { HelloData helloData = new HelloData(); helloData.setUsername(username); helloData.setAge(age); log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return helloData.toString(); }
Java
복사
위 예제에서는 username과 age밖에 없지만 이 요청 파라미터가 많아질수록 코드는 길어지고 오류의 확률은 올라간다.
이런 번거로운 부분을 자동화해주는 애노테이션도 스프링에서는 제공해주는데 그게 @ModelAttribute이다.
코드를 통해 바로 학습해보자.

코드

HelloData
요청 파라미터를 바인딩할 객체 HelloData
package hello.springmvc.basic; import lombok.Data; @Data public class HelloData { private String username; private int age; }
Java
복사
롬복을 통해 Getter, Setter 뿐아니라 toString, EqualsAndHashCode, 와 생성자까지 자동생성되는데 @Data 애노테이션은 이 모든 애노테이션을 한 번에 모아서 제공한다.
RequestParamController.modelAttributeV1
@ModelAttribute 애노테이션을 이용한 요청 파라미터 객체 바인딩
@RequestMapping("/model-attribute-v1") @ResponseBody public String modelAttributeV1(@ModelAttribute HelloData helloData) { log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return helloData.toString(); }
Java
복사
?username=catsbi&age=20 이라는 쿼리스트링을 담아서 요청을하면 바로 HelloData 객체에 담겨서 사용할 수 있는 걸 확인할 수 있다.
스프링MVC 는 @ModelAttribute가 있으면 다음을 수행한다.
1.
HelloData 객체를 생성한다.
2.
요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 바인딩한다.
⇒ 파라미터 이름이 username 이면 setUsername() 메서드를 찾아 호출한다.
만약 나이(age) 필드에 숫자가 아닌 문자(age=hello)를 넣으려하면 BindException 이 발생하는데 이런 바인딩 오류를 처리하는부분은 검증 부분에서 다룬다.
RequestParamController.modelAttributeV2
생략 가능한 @ModelAttribute 애노테이션
@RequestMapping("/model-attribute-v2") @ResponseBody public String modelAttributeV2(HelloData helloData) { log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); return helloData.toString(); }
Java
복사
@ModelAttribute는 생략할 수 있는데 @RequestParam도 생략이 가능하다. 그럼 이 HelloData는 어느 애노테이션이 생략되었는지 어떻게 판단해야 할까? 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
String, int, Integer 같은 단순 타입 = @RequestParam
⇒ 나머지 = @ModelAttribute (argument resolver로 지정해둔 타입은 제외)
⇒ argument resolver는 뒤에서 학습한다.

HTTP 요청 메시지 - 단순 텍스트

지금까지는 쿼리스트링을 이용해서 요청 파라미터를 전송하는 학습을 했는데, 그외에도 HTTP message body에 데이터를 직접 담아서 요청하는 방법도 있다.
HTTP API에서 주로 사용하며 JSON, XML, TEXT... 거의 모든 데이터를 전송할 수 있다. 주로 JSON 형식의 데이터를 주고받을때 많이 사용한다.
주의할점은 요청 파라미터와는 다르게 HTTP 메세지 바디를 통해 데이터가 직접 넘어오는 경우는 HTML Form 방식을 제외하고는 @RequestParam, @ModelAttribute를 사용할 수 없다.
먼저, 요청 메세지로 단순 텍스트를 담아서 전송하고 읽어보며 학습을 해보자.
HTTP 메세지 바디의 데이터를 InputStream을 사용해서 직접 읽는게 가능하다

코드

RequestBodyStringController 완성본
RequestBodyStringController.requestBodyString
HttpServletRequst에서 getInputStream()으로 읽어와서 문자열로 변환해서 읽을 수 있다.
@PostMapping("/request-body-string-v1") public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException { ServletInputStream inputStream = request.getInputStream(); String string = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", string); response.getWriter().write("ok"); }
Java
복사
Postman을 사용해 테스트를 해보자.
⇒ 테스트 접속 URL: http://localhost:8080/request-body-string-v1
⇒ BODY ⇒ row, Text 선택
RequestBodyStringController.requestBodyStringV2 매개변수에서 바로 inputStream 과 writer를 받을수도 있다.
@PostMapping("/request-body-string-v2") public void requestBodyStringV2(InputStream inputStream, Writer writer) throws IOException { String string = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", string); writer.write("ok"); }
Java
복사
InputStream(Reader): HTTP 요청 메세지 바디의 내용을 직접 조회
OutputStream(Writer): HTTP 응답 메세지의 바디에 직접 결과 출력
RequestBodyStringController.requestBodyStringV3
HttpEntity를 사용해서 더 편리하게 조회가 가능하다.
@PostMapping("/request-body-string-v3") public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity){ log.info("messageBody={}", httpEntity.getBody()); return new HttpEntity<>("ok"); }
Java
복사
HttpEntity: HTTP header, body 정보를 편리하게 조회할 수 있게 해준다.
⇒ 메세지 바디 정보를 직접 조회 가능(getBody())
⇒ 요청 파라미터를 조회하는 기능과 관계 없다.(@RequestParam, @ModelAttribute)
⇒ 응답에서도 사용할 수 있다.
→ 헤더 정보포함도 가능하지만, View 조회는 안된다.
참고
스프링MVC 내부에서 HTTP 메세지 바디를 읽어 문자나 객체로 변환해서 전달해주는데, 이때 HTTP메세지 커너터(HttpMessageConverter) 라는 기능을 사용한다. 이는 뒤에 자세히 알아보자.
RequestBodyStringController.requestBodyStringV4
@RequestBody라는 애노테이션을 이용해 더 간편하게 요청 메세지 바디를 받을수도 있다.
@ResponseBody @PostMapping("/request-body-string-v4") public String requestBodyStringV4(@RequestBody String body){ log.info("messageBody={}", body); return "ok"; }
Java
복사
@RequestBody
⇒ HTTP 메세지 바디 정보를 편리하게 조회하게 해주는 애노테이션으로 만약 바디가 아니라 헤더정보가 필요하면 HttpEntity@RequestHeader 애노테이션을 사용하면 된다.
⇒ 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 관계가 없다.
참고: 요청 파라미터 vs HTTP 메세지 바디
요청 파라미터를 조회하는 기능은 @RequestParam, @ModelAttribute 를 사용하고 HTTP 메세지 바디를 직접 조회하는 기능 애노테이션은 @RequestBody를 사용한다.

HTTP 요청 메시지 - JSON

위에서도 말했지만, HTTP 요청 메세지 바디에는 JSON이 주로 사용된다. JSON은 다음과 같은 구조인데, 이를 객체로 변환화는 과정에 대해서 학습해보자.
{ "username":"catsbi", "age":20 }
JSON
복사
JSON
@Data public class HelloData { private String username; private int age; }
Java
복사
Object

코드

RequestBodyJsonController 완성본
RequestBodyJsonController.requestBodyJsonV1
예전 방식의 HttpServletRequest, HttpServletResponse 객체에서 메세지 바디를 읽어와 ObjectMapper로 객체 바인딩을 하는 코드
@PostMapping("/request-body-json-v1") public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException { ServletInputStream inputStream = request.getInputStream(); String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); log.info("messageBody={}", messageBody); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); log.info("helloDate = {}", helloData.toString()); response.getWriter().write("ok"); }
Java
복사
HttpServletRequest 를 사용해서 직접 HTTP 메세지 바디에서 데이터를 읽어와, 문자로 변환한다.
문자로 된 JSON 데이터를 Jackson 라이브러리인 ObjectMapper를 사용해 객체변환을 한다.
예전 방식으로 JSON을 받아서 처리하려니 처리해야할 기본 로직이 상당히 많다. StreamUtil부터 ObjectMapper까지 꺼내서 사용을해야하는데 JSON을 받아서 사용해야 할 컨트롤러가 많을수록 코드 중복이 많아질수밖에 없다.
RequestBodyJsonController.requestBodyJsonV2
위에서 배운 @RequestBody 애노테이션을 이용해 메세지 바디를 바로 받아서 사용하면 InputStream을 꺼내 서 StreamUtili로 변환해줄 필요가 없이 바로 ObjectMapper로 객체 변환을 해줄 수 있다.
@PostMapping("/request-body-json-v2") @ResponseBody public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException { log.info("messageBody={}", messageBody); HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); log.info("helloDate = {}", helloData.toString()); return "ok"; }
Java
복사
ObjectMapper를 통해 객체 변환하는 과정도 번거롭긴 매한가지다. @ModelAttribute처럼 애노테이션으로 한번에 객체 변환은 불가능할까?
RequestBodyJsonController.requestBodyJsonV3
: @RequestBody 를 사용하면 객체를 직접 지정해서 매핑해 줄 수도 있다.
@PostMapping("/request-body-json-v3") @ResponseBody public String requestBodyJsonV3(@RequestBody HelloData helloData){ log.info("helloDate = {}", helloData.toString()); return "ok"; }
Java
복사
HttpEntity, @RequestBody 를 사용하면 HTTP 메세지 컨버터가 HTTP 메세지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
HTTP 메세지 컨버터는 문자 뿐 아니라 JSON도 변환해주며 우리가 위에서 수동으로 진행했던 과정을 대신 처리해준다.
@RequestBody는 생략이 불가능하다.
⇒ 기본타입과, 나머지는 모두 RequestParam, ModelAttribute가 매핑하기 때문
⇒ 그래서 위 코드에서 해당 애노테이션을 제거하면 @ModelAttribute가 적용되어 버린다.

주의: Content-type은 application/json이어야 한다.

HTTP 요청시에 content-type이 application/json인지 확인해야 한다. 그래야 JSON을 처리할 수 있는 HTTP 메세지 컨버터가 실행된다.
RequestBodyJsonController.requestBodyJsonV4 ~ V5
@PostMapping("/request-body-json-v4") @ResponseBody public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity){ HelloData helloData = httpEntity.getBody(); log.info("helloDate = {}", helloData.toString()); return "ok"; } @PostMapping("/request-body-json-v5") @ResponseBody public HelloData requestBodyJsonV5(@RequestBody HelloData helloData){ log.info("helloDate = {}", helloData.toString()); return helloData; }
Java
복사
HttpEntity를 이용해서 지네릭스 타입으로 객체 매핑도 가능하다.
V5를 보면 애노테이션을 최대한 활용해서 V1보다 훨씬 간결해진 코드가 완성된다.
@RequestBody 요청
⇒ JSON 요청 → HTTP 메세지 컨버터 → 객체
@ResponseBody 응답
⇒ 객체 → HTTP 메세지 컨버터 → JSON 응답

HTTP 응답 - 정적 리소스, 뷰 템플릿

HTTP 요청에 대해서 서버에서 비즈니스 로직이 다 수행된 다음 이제 응답을 해야하는데 스프링(서버)에서 응답 데이터를 반드는 방식은 크게 세 가지가 있다.
1.
정적 리소스
⇒ 웹 브라우저에 정적인 HTML, css, js를 제공할 때 정적 리소스를 사용한다.
2.
뷰 템플릿 사용
⇒ 웹 프라우저에 동적인 HTML을 제공할 때 뷰 템플릿을 사용한다.
3.
HTTP 메세지 사용
⇒ HTTP API 를 제공하는 경우 HTML이 아니라 데이터를 전달해야 하기에 HTTP 메세지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

정적 리소스

스프링 부트는 클래스패스에 다음 디렉토리에 있는 정적 리소스를 제공한다.
/static
/public
/resources
/META-INF/resources
src/main/resources는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다.
따라서 다음 디렉토리에 리소르를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
예를 들어, 정적 리소스 경로 src/main/resources/static/baisc/hello-form.html
에 해당 파일이 있다면 웹 브라우저에서는 컨트롤러를 통하지않고 정적리소스 경로 뒤의 경로를 입력해 바로 가져올 수 있다.
정적 경로 하위에 있는 html파일을 컨트롤러를 통하지 않고 바로 가져올 수 있다.

뷰 템플릿

뷰 템플릿을 거쳐 HTML이 생성되고 뷰가 응답을 만들어 전달하는데, 일반적으로HTML을 동적으로 생성하는 용도로 사용하지만 다른 것들도 가능하다. 뷰 템플릿이 만들 수 있다면 뭐든지 가능하다.
실제로 타임리프 뷰 템플릿을 이용해 간단한 페이지를 만들어보자.

코드

뷰 템플릿 생성
경로: src/main/resources/templates/response/hello.html
스프링에서는 뷰 템플릿은 기본으로 src/main/resources/templates 경로를 제공한다.
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p th:text="${data}">empty</p> </body> </html>
HTML
복사
ResponseViewController
위에 작성한 hello 라는 뷰를 호출하는 컨트롤러
package hello.springmvc.basic.response; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller public class ResponseViewController { @RequestMapping("/response-view-v1") public ModelAndView responseViewV1() { ModelAndView mv = new ModelAndView("/response/hello") .addObject("data", "hello"); return mv; } @RequestMapping("/response-view-v2") public String responseViewV2(Model model) { model.addAttribute("data", "hello"); return "response/hello"; } @RequestMapping("/response/hello") public void responseViewV3(Model model) { model.addAttribute("data", "hello!!"); } }
Java
복사
보면 모두 반환 타입이 다르다(ModelAndView, String, void) 각각의 반환 타입별 로직은 달라진다.
ModelAndView 를 반환하는 경우(responseViewV1)
⇒ 객체에서 View를 꺼내어 물리적인 뷰 이름으로 완성한 뒤 뷰를 찾아 렌더링을 한다.
String을 반환하는 경우(responseViewV2)
⇒ @ResponseBody(혹은 클래스레벨에서 @RestController)가 없으면 response/hello라는 문자가 뷰 리졸버로 전달되어 실행되서 뷰를 찾고 렌더링을 한다.
⇒ @ResponseBody(혹은 클래스레벨에서 @RestController)가 있으면 뷰 리졸버를 실행하지 않고 HTTP 메세지 바디에 직접 response/hello 라는 문자가 입력된다.
⇒ 위 코드에서는 /response/hello를 반환하는데 뷰 리졸버는 물리적 이름을 찾아서 렌더링을 실행한다.
→ 실행: templates/response/hello.html
void를 반환하는 경우(responseViewV3)
⇒ @Controller를 사용하고 HttpServletResponse, OutputStream(Writer)같은 HTTP 메세지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용한다.
⇒ 위 코드에서 경로는 요청 URL인 (/response/hello)를 사용한다.
⇒ 이 방식은 명시성이 너무 떨어지고 이런 케이스가 나오는 경우도 거의 없어 권장하지 않는다.

Thymeleaf 스프링 부트 설정

우리는 이미 프로젝트 생성시점에서 Thymeleaf 라이브러리를 추가해놨지만, 만약 추가를 하지 않았다면
build.gradle 소스에 다음 코드를 추가한다.
build.gralde
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
Java
복사
스프링부트는 자동으로 ThymeleafViewResolver와 필요한 스프링 빈들을 등록하는데, 스프링 설정을 통해 해당 뷰 리졸버에서 뷰 템플릿의 물리적 경로를 완성할때 접두사나 접미사를 변경할수도 있다.
#아래 설정은 기본값이기에 변경이 필요할때만 사용한다. spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html
YAML
복사

HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

HTTP API를 제공하는 경우 응답 메세지로 HTML이 아니라 데이터를 전달해야 한다. 그리고 이 때 HTTP 메세지 바디에는 JSON같은 형식으로 데이터를 실어서 보낸다.

참고

HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메세지 바디에 데이터를 담아서 전달한다. 하지만 이번 섹션에서 말하는 내용은 정적 리소스나, 뷰 템플릿이 아니라 직접 HTTP 응답 메세지를 전달하는 경우를 말한다.

코드

ReponseBodyController
package hello.springmvc.basic.response; import hello.springmvc.basic.HelloData; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Controller public class ResponseBodyController { @GetMapping("/response-body-string-v1") public void responseBodyV1(HttpServletResponse response) throws IOException { response.getWriter().write("ok"); } @GetMapping("/response-body-string-v2") public ResponseEntity<String> responseBodyV2() { return new ResponseEntity<>("ok", HttpStatus.OK); } @GetMapping("/response-body-string-v3") @ResponseBody public String responseBodyV3() { return "ok"; } @GetMapping("/response-body-json-v1") public ResponseEntity<HelloData> responseBodyJsonV1() { HelloData helloData = new HelloData(); helloData.setUsername("catsbi"); helloData.setAge(20); return new ResponseEntity<>(helloData, HttpStatus.OK); } @ResponseStatus(HttpStatus.OK) @ResponseBody @GetMapping("/response-body-json-v2") public HelloData responseBodyJsonV2() { HelloData helloData = new HelloData(); helloData.setUsername("catsbi"); helloData.setAge(20); return helloData; } }
Java
복사
responseBodyV1
⇒ 서블릿을 직접 다룰 때와 같이 코드가 구현되있다.
HttpServletResponse 객체를 통해 HTTP 메세지 바디에 직접 OK 응답 메세지를 전달한다.
response.getWriter().write("ok")
responseBodyV2
⇒ ResponseEntity 엔티티는 HttpEntity를 상속받았는데, HttpEntity는 HTTP메세지의 헤더, 바디 정보를 가지고 있다면 ResponseEntity는 HTTP 응답코드가 추가되었다고 생각하면 된다.
→return new ResponseEntity<>(helloData, HttpStatus.OK);
responseBodyV3
⇒ @ResponseBody 애노테이션을 사용하면 view 를 사용하지 않고 HTTP 메세지 컨버터를 통해 HTTP 메세지를 직접 입력할 수 있다 ResponseEntity도 동일한 방식으로 동작한다.
responseBodyJsonV1
⇒ ResponseEntity를 반환한다. HTTP 메세지 컨버터를 통해서 객체는 JSON으로 변환되어 반환된다.
responseBodyJsonV2
ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데 @ResponseBody를 사용하면 설정하기가 까다롭다. 그래서 이런 경우에는 @ResponseStatus 애노테이션을 이용하여 상태코드를 설정할 수 있다.
⇒ 정적으로 상태코드를 작성한 것이기에 유연하지는 못하다. 그렇기에 동적으로 상태코드가 변경되야하는 상황이라면 ResponseEntity를 사용하면 된다.

HTTP 메시지 컨버터

지금까지 여러 애노테이션을 이용해서 JSON, queryString등을 @RequestBody, @ModelAttribute 등으로 편하게 객체로 변환해서 사용했다. 그런데 이쯤에서 어떻게 스프링이 객체로 변환을 해주는지에 대해 의문을 가질 필요가 있다.
HTTP 메세지 컨버터를 설명하기 전 @ResponseBody의 사용 원리를 살펴보자.
@ResponseBody를 사용하니 HTTP의 BODY에 문자 내용을 직접 반환하는데 그림을 보면 viewResolver 대신HttpMessageConverter가 동작한다.
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
byte 처리등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.
여기서 응답할 때 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 HttpMessageConverter가 선택된다.
정리하면, 스프링 MVC는 다음의 경우에 HTTP 메세지 컨버터를 적용한다.
HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)

HTTP 메세지 컨버터 인터페이스

org.springframework.http.converter.HttpMessageConverter
package org.springframework.http.converter; public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
Java
복사
HTTP 메세지 컨버터는 요청 및 응답 둘 다 사용되야 합니다.
⇒ 요청시 JSON → 객체
⇒ 응답시 객체 → JSON
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
⇒ 메세지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크하는 메서드
T read()
void write()
⇒ 메세지 컨버터를 통해 메세지를 실제로 변환하는 메서드

스프링 부트 기본 메세지 컨버터

0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter
Java
복사
실제로는 기본 등록되는 컨버터가 더 많지만 생략한다.
대상 클래스 타입과 미디어 타입 둘을 체크한 뒤 사용여부를 결정한다. 등록된 메세지 컨버터들이 순회하며 만족한다면 멈추고 해당 컨버터를 사용하고 조건을 만족하지 않으면 다음 컨버터로 우선순위가 넘어간다.
주로 쓰이는 컨버터 세가지(위에서 작성한)에 대해 알아보자.
1.
ByteArrayHttpMessageConverter: byte[] 데이터를 처리한다.
클래스 타입: byte[], 미디어타입: */*
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[] 미디어 타입 application/octet-stream
2.
StringHttpMessageConverter: String 문자로 데이터를 처리한다.
클래스 타입: String, 미디어 타입: */*
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok" 미디어 타입 text/plain
3.
MappingJackson2HttpMessageConverter: application/json
클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
요청 예) @RequestBody HelloData data
응답 예) @ResponseBody return helloData 미디어 타입 application/json 관련

HTTP 요청/ 응답 데이터 읽기/쓰기

HTTP 요청 데이터 읽기

HTTP 요청이 오면 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용한다.
메세지 컨버터가 메세지를 읽을 수 있는지 확인하기 위해 canRead()로 지원 여부를 확인한다.
⇒ 대상 클래스 타입을 지원하는가 ( Ex: @RequestBody의 대상 클래스(byte[], String, HelloData)
⇒ HTTP 요청의 Content-Type 미디어 타입을 지원하는가(Ex: text/plain, application/json, */*)
canRead() 조건을 만족하면 read() 메서드를 호출해 객체를 생성및 반환한다.

HTTP 응답 데이터 생성

컨트롤러에서 @ResponseBody, HttpEntity 로 값이 반환된다.
메세지 컨버터가 메세지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
⇒ 대상 클래스 타입을 지원하는가 (return 의 대상 클래스(byte[], String, HelloData))
⇒ HTTP 요청의 Accept 미디어 타입을 지원하는가(정확히는 @RequestMappingproduces)
(text/plain, application/json, */*)
canWrite() 를 만족하면 write() 메서드를 호출해 HTTP 응답 메세지 바디에 데이터를 생성한다.

요청 매핑 핸들러 어댑터 구조

스프링MVC의 구조를 보면 HTTP요청부터 응답까지 여러 과정을 거쳐서 결과가 응답되는데, 여기서 HTTP메세지 컨버터는 어디서 사용되는 것일지 의문을 가질 수 있다.
이 의문의 키워드는 @RequestMapping에 있다.

RequestMappingHandlerAdapter

@RequestMapping 애노테이션을 처리해주는 핸들러 어댑터가 RequestMappingHandlerAdapter인데, 이 어댑터의 동작 방식은 다음과 같다.
RequestMappingHandlerAdapter 동작 방식
⇒ 스프링 MVC 를 학습하면서 HttpServletRequestModel부터 @RequestParam, @ModelAttribute같은 애노테이션이나 @RequestBody, HttpEntity까지 정말 많은 요청 파라미터를 처리하는데 해당 어댑터에서는 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터의 값을 생성한다. 그리고 모든 파라미터의 값이 준비되면 컨트롤러를 호출하면서 값을 넘겨준다. 참고로 스프링은 30개가 넘는 ArgumentResolver를 기본으로 제공한다.
참고: 가능한 파라미터 목록은 다음 메뉴얼에서 확인이 가능하다.

ArgumentResolver 인터페이스

정확히는 HandlerMethodArgumentResolver지만 줄여서 ArgumentResolver라 한다.
public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
Java
복사

동작 방식

1.
ArgumentResolver의 supportsParameter() 메서드를 호출해 해당 파라미터를 지원하는지 체크
a.
(지원할 경우) resolveArgument() 메서드를 호출해서 실제 객체를 생성한다.
b.
(지원안할경우) 다음 ArgumentResolver로 우선순위가 넘어간다.

ReturnValueHandler

HandlerMethodReturnValueHandler를 줄여서 ReturnValueHandler라 부른다.
이 또한 ArgumentResolver와 비슷한에 이것은 요청이 아닌 응답 값을 변환하고 처리한다.
컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 이 ReturnValueHandler 덕분이다.
스프링은 10개가 넘는 ReturnValueHandler를 지원한다.
예) ModelAndView, @ResponseBody, HttpEntity, String
참고: 가능한 응답 값 목록은 다음 메뉴얼에서 확인이 가능하다.

HTTP 메세지 컨버터

그래서 HTTP 메세지 컨버터는 도대체 어디에 있는건가? 해당 컨버터가 필요한 상황은 대표적으로 @RequestBody같은 애노테이션일텐데 다음 구조를 확인해보자.
HTTP 메세지 컨버터 위치
위 그림처럼 @RequestBody 를 사용하는 컨트롤러가 필요로 하는 파라미터 값에 사용된다.
⇒ 요청의 경우 @RequestBody를 처리하는 ArgumentResolver가 있고 HttpEntity를 처리하는 ArgumentResolver가 있는데, 이 ArgumentResolver들이 HTTP 메세지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
응답의 경우에도 @ResponseBody가 사용되며 컨트롤러의 반환값을 이용하기에 그 시점에서 HTTP 메세지 컨버터는 이용된다.
⇒ 응답의 경우 @ResponseBodyHttpEntity를 처리하는 ReturnValueHandler가 있는데 여기서 HTTP 메세지 컨버터를 호출해서 응답 결과를 얻는다.

확장

스프링은 이런 ArgumentReolver나 ReturnValueHandler, MessageConverter를 모두 인터페이스로 제공하기에 다음과 같은 인터페이스를 언제든지 확장해서 사용할 수 있다.
HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter
대부분은 이미 스프링에서 구현되어 제공되기에 실제로 확장할 일이 많지는 않다.
만약 기능 확장을 할 때는 WebMvcConfigurer를 상속받아 스프링 빈으로 등록하면 된다.
WebMvcConfigure 확장
@Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers){ //... } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { //... } }; }
Java
복사

이전 챕터로

다음 챕터로