로깅
1. 로깅 라이브러리
1.1 로깅 라이브러리 종류
• 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging )가 함께 포함됨.
• 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용.
SLF4J - http://www.slf4j.org
Logback - http://logback.qos.ch
1.2 사용법
• 클래스 참조 변수 선언
/* getClass()메서드를 통해 사용되는 클래스 타입 반환하여 삽입 */
private Logger log = LoggerFactory.getLogger(getClass());
/* 직접적으로 해당 클래스타입을 입력해줘도 된다. */
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
• 롬복 사용
@Slf4j
public class TestController {
...
}
1.3 코드
package hello.springmvc.basic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
//@Controller // 반환값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링된다.
//@Slf4j // private final Logger log = LoggerFactory.getLogger(getClass()); 안써도됨 lombok에서 제공
@RestController // return 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(LogTestController.class);
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
System.out.println("name = "+ name);
// 이렇게 사용하면 안됨
// argument에 +를 먼저 연산하고 메모리를 할당함, 쓸데없는 리소스를 낭비하게됨
log.trace("trace log="+ name);
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";
}
}
• @RestController
- @Controller는 반환값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링됨
- @RestController는 반환 값으로 뷰를 찾는게 아니라 HTTP 메세지 바디에 바로 입력함
- 클래스레벨이아닌 메서드레벨에서 @ResponseBody를 사용하면 클래스레벨에 @Controller를 사용하더라도 바로 HTTP 메세지 바디에 입력해서 반환을 해줌
• 로그 출력 포맷
시간, 로그 레벨, 프로세스 ID(PID), 쓰레드 명, 클래스 명, 로그 메세지
1.4 로그 레벨
• 작성한 로그 출력코드는 5개인데 로그는 3개가 출력되는 이유
- 로그의 출력 레벨이 있음.
- 로그레벨을 설정하면 그 로그 보다 우선순위가 높은 것만 출력
- 스프링 부트에서 기본으로 설정되어 있는 로그레벨은 info
- 그렇기에 info보다 우선순위가 낮은 debug, trace는 출력되지 않음.
• 로그 레벨
- TRACE > DEBUG > INFO > WARN > ERROR
- 개발서버는 debug까지 출력가능
- 운영서버는 통상적으로 info만 출력
• 로그를 임의로 내가 원하는대로 변경하는법
설정파일(application.properties)에서 레벨을 변경
#전체 로그 레벨 설정(기본 info)
lolgging.level.root=info
#hello.springmvc 패캐지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=[변경을 원하는 로그 레벨]
1.5 올바른 로그 사용법
• 기존의 문자열 결합을 이용한 출력문 사용
System.out.println(name + "님 안녕하세요.");
/*로그도 위와같이 사용한다면?*/
log.debug(name + "님 안녕하세요.");
※ 로그 레벨을 info로 설정해도 해당 코드에 있는 name + "님 안녕하세요."는 자바 특성상 실행이 안되어도 연산이 되버어림.
자바 컴파일 시점에서 사용하지도 않는 debug레벨에 있는 연산을 평가해버리니 리소스 낭비.
• 새로운 방식의 로그 출력 방식
log.debug("{} 님 안녕하세요.", name);
※ 로그 출력레벨이 debug 이상이면 debug내의 연산은 수행되지 않음.
1.6 로그 사용시 장점
• 쓰레드 정보, 클래스 이름같은 정보를 함께 볼 수 있고, 출력 모양을 조정 가능
• 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고 운영서버에서는 출력하지 않게끔 로그를 조절 가능
• 콘솔에만 출력하는게 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있음
• 특히 파일로 남길 때 일별, 특정 용량에 따라 로그를 분할하는것도 가능
• 성능도 System.out보다 좋음 (내부 버퍼링, 멀티 쓰레드 등)
요청매핑(RequestMapping)
1. 기본매핑
/**
* 기본 요청
* 둘 다 허용한다 /hello-basic, /hello-basic/
* 스프링 부트 3.0 부터는 /hello-basic , /hello-basic/ 는 서로 다른 URL 요청으로 인식함
* HTTP 메서드 모두 허용 GET, POST, HEAD, PUT, PATCH, DELETE
*/
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
• @RequestMapping("/hello-basic")
- /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑
- 대부분의 속성을 배열[]로 제공하기에 다중 설정도 가능
ex) @RequestMapping(value = {"/hello-basic", "/hello-go"})
- method 속성으로 HTTP 메서드를 지정하지 않으면 모든 메서드에 무관하게 호출
(GET, HEAD, POST, PATCH, DELETE)
2. HTTP 특정 method 허용 매핑
/**
* method 특정 HTTP 메서드 요청만 허용한다.
* GET, HEAD, POST, PUT, PATCH, DELETE
*/
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
• method가 GET일 경우에만 매핑이되며 다른 방식으로 요청하면 HTTP 405(Method Not Allowd)가 반환
3. HTTP 특정 method 허용 매핑 축약
/**
* 편리한 축약 애노테이션
*
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
• 매번 method 속성을 설정해서 HTTP 메서드를 지정해주는게 번거롭고 가독성도 떨어지기에 전용 애노테이션을 만들어서 해결
• GetMapping, PostMapping, PatchMapping, DeleteMapping등 이름에 의미를 부여해 더 직관적임
- 애노테이션 내부에는 @RequestMapping과 method를 미리 지정해놓음
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
...
}
4. PahVariable(경로 변수)를 사용한 매핑
/**
* PathVariable 사용
* 변수명이 같으면 아래와 같이 생략 가능
* @PathVariable("userId") String userid -> @PathVariable String userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
log.info("mappingPath userId={}", userId);
return "/mapping/{userId} ok";
}
• 최근 HTTP API는 위와 같이 리소스 경로에 식별자를 넣는 스타일을 선호
ex) /mapping/userA
ex) /users/1
• @RequestMapping은 URL 경로를 템플릿화 할 수 있는데 @PathVariable 애노테이션을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있음
• @PathVariable의 이름과 파라미터 이름이 같으면 생략 가능하다.
⇒ @PathVariable("username") String username → PathVariable String username
5. 다수의 PahVariable(경로 변수)를 사용한 매핑
/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "/mapping/users/{userId}/orders/{orderId} ok";
}
• 하나 이상의 PathVariable도 사용이 가능
6. 특정 파라미터 조건 매핑
/**
* 파라미터로 추가 매핑
* params="mode", // 파라미터 이름만 있어도됨
* params="!mode" // 특정 파라미터이름이 안 들어가있어야 함
* params="mode=debug" // 특정 파라미터이름과 값이여야함
* params="mode!=debug" // 특정 파라미터이름과 값이면 안됨
* params = {"mode=debug","data=good"} // 여러개의 특정 파라미터이름과 값이여야함
*/
// 특정 파라미터 정보가 있을때만 호출 됨(url경로 뿐아니라 파라미터 정보까지 추가로 더 매핑한것)
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "/mapping-param ok";
}
• 특정 파라미터를 조건식으로 매핑해서 매핑여부를 결정할 수 있음
ex) http://localhost:8080/mapping-param?mode=debug
• 잘 사용하지 않음
7. 특정 헤더 조건 매핑
/**
* 특정 헤더로 추가 매핑
* headers="mode" // 헤더에 특정 헤더명이 들어가있어야함
* headers="!mode" // 헤더에 특정 헤더명이 안 들어가있어야함
* headers="mode=debug" // 헤더에 특정 헤더명과 값이여야함
* headers="mode!=debug" // 헤더에 특정 헤더명과 값이면 안됨
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "/mapping-header ok";
}
• 특정 파라미터 매핑과 동일하게 헤더 역시 조건매핑이 가능
8. 미디어 타입 조건 매핑 1 - HTTP 요청 Content-Type, consume
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json" // Content-Type 헤더에 Media Type이 "application/json"이여야함
* consumes="!application/json" // Content-Type 헤더에 Media Type이 "application/json"이면 안됨
* consumes="application/*" // Content-Type 헤더에 Media Type이 "application/*"이여야함
* consumes="*\/*" // Content-Type 헤더에 Media Type이 "*\/*"이여야함
* MediaType.APPLICATION_JSON_VALUE = "application/json"
*/
// 요청 컨텐트 타입이 json이 여야함
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "/mapping-consume ok";
}
• HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑
• 일치하지 않을 경우 HTTP 415(Unsupported Media Type)을 반환
• 조건을 배열로 설정할수도 있고 상수로 제공하는 매직넘버를 사용해도 됨
• 사용 예시
consumes = "application/json"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE
9. 미디어 타입 조건 매핑 2 - HTTP 요청 Accept, produce
/**
* Accept 헤더 기반 Media Type
* produces = "text/html" // Accept 헤더에 Media Type이 "text/html"이여야함
* produces = "!text/html" // Accept 헤더에 Media Type이 "text/html"이면 안됨
* produces = "text/*" // Accept 헤더에 Media Type이 "text/*"이여야함
* produces = "*\/*" // Accept 헤더에 Media Type이 "*\/*" 이여야함
* MediaType.MediaType.TEXT_PLAIN_VALUE = "text/plain"
*/
// 클라이언트가(브라우저가) text/html만 받아들일수 있다는 뜻
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "/mapping-produce ok";
}
• HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑
• 만약 맞지 않으면 HTTP 406(Not Acceptable)을 반환
요청 매핑(Request Mapping) - API 예시
1. API 명세 예시
기능 | HTTP Method | URI |
회원 목록 조회 | GET | /mapping/users |
회원 등록 | POST | /mapping/users |
회원 조회 | GET | /mapping/users/{userId} |
회원 수정 | PATCH | /mapping/users/{userId} |
회원 삭제 | DELETE | /mapping/users/{userId} |
2. 코드 예시
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/mapping/users")
@RestController
public class MappingClassController {
@GetMapping
public String users() {
return "get users";
}
@PostMapping
public String addUser() {
return "post users";
}
@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;
}
}
HTTP 요청
1. 기본, 헤더 조회 예제
package hello.springmvc.basic.request;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
// MultiValueMap : Map과 유사한데 하나의 키에 여라값을 받을 수 있음(keyA=value1&keyA=value2)
@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";
}
}
• HttpMethod httpMethod 객체
- HTTP 메서드를 조회(org.springframework.http.HttpMethod)
• Locale 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 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
ex) 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");
2. HTTP 요청 파라미터 - 쿼리 파라미터와 HTML Form
2.1 HttpServletRequest객체
• HTTP 요청 메세지를 개발자가 사용하기 편하게 변환해 제공하는 것이 HttpServletRequest 객체
• 이 객체내의 getParameter()를 이용하면 요청 파라미터를 조회할 수 있음
• queryString으로 요청 메세지를 전달하는 것은 GET 전송 방식
• HTML Form에서 요청 메세지 바디에 전달 하는 것은 POST 전송방식
• GET 쿼리 파라미터 전송의 URL 예시
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
※ 위 GET쿼리 파라미터 전송 방식과 POST, HTML Form 전송의 요청 메세지 방식은 모두 형식이 동일 하기때문에
구분없이 getParameter() 메서드를 이용해 조회할 수 있음 이를 요청 파라미터(request parameter)조회라고함.
2.2 request.getParameter()메서드로 데이터 사용 예제 코드
@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");
}
}
2.3 @RequestParam어노테이션으로 데이터 사용 예제 코드
@Controller
public class RequestParamController {
@ResponseBody // @RestController랑 같은 효과 바로 html body로 String을 뿌려줌 viewResolver를 안탐
@RequestMapping("/request-param-v2")
public String reqeustParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}",memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String reqeustParamV3(
// 파라미터 명과 변수명이 같으면 파라미터 명 생략가능
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}",username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4")
// 파라미터 명과 변수명이 같고 단순타입(String int Integer) 파라미터 명 @RequestParam 생략가능
// 과한 생략이라는 생각이듬
public String reqeustParamV4(String username, int age) {
log.info("username={}, age={}",username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-required")
public String reqeustParamRequired(
// required = true가 기본 값 파라미터가 없으면 오류남
// required = false일 경우 bad request 400 에러남
// /request-param-required?username=
// 위와 같은 url로 paramter없이 입력 시 null 아닌 ""로 인식하여 required를 통과함
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
// java에 int에 null을 넣을수 없음
//int a = null;
// Integer는 참조변수형이라 null이 가능
//Integer b =null;
log.info("username={}, age={}",username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-default")
public String reqeustParamDefault(
// 파라미터가 없을 경우 기본 값 세팅
// defaultValue를 사용하면 required가 의미가 없음
// /request-param-required?username=
// 빈 파라미터 값도 설정한 defaultValue 값으로 처리함
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age) {
log.info("username={}, age={}",username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-map")
// 파라미터를 Map, MultiValueMap으로 받을 수 있음(대부분 파라미터 값은 한 개임)
// @RequestParam Map : Map(key=value)
// @RequestParam MultiValueMap : MultivalueMap(key=[value1,value2,...])
public String reqeustParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}",paramMap.get("username"), paramMap.get("age"));
return "ok";
}
}
• @ResponseBody
- View 조회를 무시하고, HTTP message body에 직접 해당 내용을 입력
- 클래스레벨에서 @Controller를 사용하는 경우 메서드레벨에서 해당 애노테이션을 사용해서 메세지 바디에 직접 내용입력하는게 가능
• @RequestParam("username") String usernam
- 파라미터 이름으로 바인딩
• @RequestParam String username
- HTTP 파라미터 이름이 변수 이름과 같을경우 파라미터 속성 생략이 가능
ex) @RequestParam("username") String username → @RequestParam String username
• String username, int age
String, int, Integer 등의 단순 타입이면 @RequestParam도 생략이 가능
• RequestParam의 속성 2가지
① required : 파라미터 필수 여부 속성
- 사용 방법
@RequestParam(required = true)
- 기본값은 파라미터 필수 (required = true)
- 해당 파라미터를 공백(ex: username=)으로 전송하면 빈 문자로 통과가 됨
- required가 true인 파라미터를 보내주지 않으면 400 예외(BAD_REQUEST)가 발생
- 원시타입은 null이 들어갈 수 없어서 required가 false여도 500에러가 발생
int형으로 에러가 발생하면 Integer같은 wrapper 타입을 사용해야 함
혹은 기본값을 설정해주는 defaultValue를 사용하면 됨
② defaultValue : 파라미터가 없는 경우 기본값으로 설정한 값이 적용
- 사용 방법
@RequestParam(defaultValue = "20")
- 이미 기본값이 있기에 required는 의미가 없어 빼도 됨
- 빈 문자("")의 경우에도 설정한 기본 값이 적용
• @RequestParam Map
- Map을 이용해 한 번에 받을 수도 있음
- Map(key=value) 형식
• @RequestParam MultiValueMap
- 파라미터의 값이 1개가 확실하면 Map을 써도 되지만 그렇지 않다면 MultiValueMap을 사용
- MultiValueMap(key=[value1, value2, ...] 형식
ex) (key=userIds, value=[id1, id2])
2.3 @ModelAttribute어노테이션으로 데이터 사용 예제 코드
• HelloData 코드
package hello.springmvc.basic;
import lombok.Data;
// 요청 파라미터를 바인딩할 객체 HelloData
// @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용해줌
@Data
public class HelloData {
private String username;
private int age;
}
• @ModelAttribute어노테이션으로 데이터 사용
@Controller
public class RequestParamController {
@ResponseBody
@RequestMapping("/model-attribute-v1")
// 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾아서 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("helloData={}", helloData);
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
// @ModelAttribute 생략가능
// @RequestParam도 생략 가능함 그럼 Spring은 어떤 걸 바인딩 할까
// String int Integer같은 단순 타입은 @RequestParam로 바인딩
// 나머지는 @ModelAttribute로 바인딩
public String modelAttributeV2( HelloData helloData) {
log.info("helloData={}", helloData);
return "ok";
}
}
• ?username=spring&age=20 이라는 쿼리스트링을 담아서 요청을하면 바로 HelloData 객체에 담겨서 사용할 수 있음
• 스프링MVC는 @ModelAttribute가 있으면 다음을 수행
① HelloData 객체를 생성
② 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾음
③ 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 바인딩
※ 파라미터 이름이 username 이면 setUsername() 메서드를 찾아 호출
• @ModelAttribute는 생략가능
- @ModelAttribute 생략 vs @RequestParam 생략
- 스프링은 해당 생략시 다음과 같은 규칙을 적용
String, int, Integer 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute (argument resolver로 지정해둔 타입은 제외)
3. HTTP 요청 파라미터 - 단순 텍스트
※ HTTP 메세지 바디를 통해 데이터가 직접 넘어오는 경우는 HTML Form 방식을 제외하고 @RequestParam, @ModelAttribute를 사용할 수 없음
3.1 예제 v1
@Controller
public class RequestBodyStringController {
@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);
log.info("messageBody={}",messageBody);
response.getWriter().write("ok");
}
}
• HttpServletRequst에서 getInputStream()으로 읽어와서 문자열로 변환해서 읽을 수 있음
3.2 예제 v2
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v2")
// InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
// OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}",messageBody);
responseWriter.write("ok");
}
}
• 매개변수에서 바로 inputStream과 writer를 받을수도 있음
- InputStream(Reader): HTTP 요청 메세지 바디의 내용을 직접 조회
- OutputStream(Writer): HTTP 응답 메세지의 바디에 직접 결과 출력
3.3 예제 v3
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v3")
// 메시지 바디 정보를 직접 조회, 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity)throws IOException {
// HttpEntity: HTTP header, body 정보를 편리하게 조회
String messageBody = httpEntity.getBody();
httpEntity.getHeaders();
log.info("messageBody={}",messageBody);
// HttpEntity는 응답에도 사용 가능 메시지 바디 정보 직접 반환
// 헤더 정보 포함 가능
// view 조회X
return new HttpEntity<>("ok");
}
}
• HttpEntity: HTTP header, body 정보를 편리하게 조회할 수 있게 해줌
- 메세지 바디 정보를 직접 조회 가능(getBody())
- 요청 파라미터를 조회하는 기능과 관계 없다.(@RequestParam, @ModelAttribute)
• 응답에서도 사용할 수 있음
- 메세지 바디 정보 직접 반환
- 헤더 정보포함도 가능
- View 조회는 안됨
3.3 예제 v3 다른방식
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v3-another")
// RequestEntity : HttpMethod, url 정보가 추가, 요청에서 사용
public HttpEntity<String> requestBodyStringV3Another(RequestEntity<String> httpEntity)throws IOException {
String messageBody = httpEntity.getBody();
httpEntity.getHeaders();
log.info("messageBody={}",messageBody);
// ResponseEntity : HTTP 상태 코드 설정 가능, 응답에서 사용
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
}
• HttpEntity를 상속받은 RequestEntity, ResponseEntity 객체들도 같은 기능을 제공
- RequestEntity : HttpMethod, url 정보가 추가, 요청에서 사용
- ResponseEntity : HTTP 상태 코드 설정 가능, 응답에서 사용
ex) return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED)
3.4 예제 v4
@Controller
public class RequestBodyStringController {
// @ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다.(view를 사용하지 않음)
@ResponseBody
@PostMapping("/request-body-string-v4")
// @RequestBody : HTTP 메시지 바디 정보를 편리하게 조회할 수 있다.
// 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.
// 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 전혀 관계가 없다.
public String requestBodyStringV4(@RequestBody String messageBody)throws IOException {
log.info("messageBody={}",messageBody);
return "ok";
}
// 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
// HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody
}
• @RequestBody
- HTTP 메세지 바디 정보를 편리하게 조회하게 해주는 애노테이션
- 만약 바디가 아니라 헤더정보가 필요하면 HttpEntity나 @RequestHeader 애노테이션을 사용하면 됨
- 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 관계가 없다.
4. HTTP 요청 파라미터 - Json
※ HTTP 요청 메세지 바디에는 JSON이 주로 사용됨
※ JSON은 다음과 같은 구조인데, 이를 객체로 변환하여 로직을 수행
4.1 예제 v1
@Controller
public class RequestBodyJsonController {
// Jackson 라이브러리인 objectMapper
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException{
// HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
// 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
}
• HttpServletRequest를 사용해서 직접 HTTP 메세지 바디에서 데이터를 읽어와, 문자로 변환
• 문자로 된 JSON 데이터를 Jackson 라이브러리인 ObjectMapper를 사용해 자바 객체변환
4.2 예제 v2
@Controller
public class RequestBodyJsonController {
// Jackson 라이브러리인 objectMapper
private ObjectMapper objectMapper = new ObjectMapper();
@ResponseBody
@PostMapping("/request-body-json-v2")
// @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException{
log.info("messageBody={}", messageBody);
// 문자로 된 JSON 데이터인 messageBody를 objectMapper를 통해서 자바 객체로 변환
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
• @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에 저장
• 문자로 된 JSON 데이터인 messageBody 를 objectMapper 를 통해서 자바 객체로 변환
4.3 예제 v3
@Controller
public class RequestBodyJsonController {
@ResponseBody
@PostMapping("/request-body-json-v3")
// @RequestBody에 직접 만든 객체 HelloData를 지정할 수 있다.
// HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줌
// HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데,
// V2의 HelloData helloData = objectMapper.readValue(messageBody, HelloData.class)를 처리해줌
// @RequestBody는 생략 불가능 (HTTP 메시지 바디가 아니라 요청 파라미터 @ModelAttribute로 처리함)
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException{
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
}
• @RequestBody 객체 파라미터에 직접 만든 객체를 지정할 수 있음
• @RequestBody는 생략 불가능
- @ModelAttribute , @RequestParam 과 같은 해당 애노테이션을 생략시 다음과 같은 규칙을 적용
String , int , Integer 같은 단순 타입 = @RequestParam 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외) |
- @RequestBody를 생략하면 @ModelAttribute 가 적용되어버림
※ HelloData data → @ModelAttribute HelloData data
- 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됨
• 주의
- HTTP 요청시에 content-type이 application/json인지 꼭! 확인해야 함.
- 그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행됨
4.4 예제 v4, v5
@Controller
public class RequestBodyJsonController {
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity)throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}",data.getUsername(), data.getAge());
return "ok";
}
// @ResponseBody : 객체 → HTTP 메시지 컨버터 → JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
// @RequestBody : JSON 요청 → HTTP 메시지 컨버터 → 객체
public HelloData requestBodyJsonV5(@RequestBody HelloData data) throws IOException{
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
• HttpEntity, @ResponseBody로 data 객체로 변환
※ 응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음
• @RequestBody 요청
JSON 요청 → HTTP 메시지 컨버터 → 객체
• @ResponseBody
응답 객체 → HTTP 메시지 컨버터 → JSON 응답
• HTTP 메시지 컨버터
- @RequestBody, HttpEntity를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해줌.
- HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데, 우리가 방금 V2에서 했던 수동 작업을 대신 처리해줌
HTTP 응답
1. 정적 리소스, 뷰 템플릿, HTTP 메세지 사용
※ HTTP 요청에 대해서 서버에서 비즈니스 로직이 다 수행된 다음 이제 응답을 해야하는데 스프링(서버)에서 응답 데이터를 반드는 방식은 크게 세 가지가 있음
• 정적 리소스
• 뷰 템플릿 사용
• HTTP 메세지 사용
1.1 정적 리소스
• 스프링 부트는 클래스패스에 다음 디렉토리에 있는 정적 리소스를 제공함.
- /static, /public, /resources, /META-INF/resources
• src/main/resources는 리소스를 보관하는 곳이고, 클래스패스의 시작 경로임
• 위의 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공
• 예를 들어, 정적 리소스 경로 src/main/resources/static/baisc/hello-form.html에 해당 파일이 있다면 웹 브라우저에서는 컨트롤러를 통하지않고 정적리소스 경로 뒤의 경로를 입력해 바로 가져올 수 있음
1.2 뷰 템플릿
• 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달
• 일반적으로 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>
• 뷰 템플릿 호출 예제 코드
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 {
// 정적 리소스
// 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
// src/main/resources/static , src/main/resources/public , src/main/resources , /META-INF/resources
// src/main/resources는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로
// 다음 경로에 파일이 들어있으면 src/main/resources/static/basic/hello-form.html
// 웹 브라우저에서 다음과 같이 실행하면 된다. http://localhost:8080/basic/hello-form.html
// view 템플릿 경로 src/main/resources/templates
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello").addObject("data","hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data","hello!");
// @Controller와 @RequestMapping가 있으면 view의 논리 이름으로 viewResolver가 가져가서 render해줌
return "response/hello";
}
// controller경로의 이름과 view의 논리적 이름이 같으면 return값이 없어도 viewResolver가 가져가서 render해줌
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data","hello!");
}
}
※ 뷰 템플릿 호출 예제의 반환 타입이 다 다름 (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
뷰 경로 : templates/response/hello.html
- 이 방식은 명시성이 너무 떨어지고 이런 케이스가 나오는 경우도 거의 없어 권장하지 않음
• Thymeleaf 스프링 부트 설정
- build.gradle 파일에 하기 라이브러리를 추가하면 스프링 부트가 자동으로 ThymeleafViewResolver 와 필요한 스프링 빈들을 등록
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' |
- 뷰 리졸버 설정 방법.
application.properties spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html |
※ 이 설정은 기본 값 이기 때문에 변경이 필요할 때만 설정하면 됨
1.3 HTTP 메세지 사용(HTTP API, 메세지 바디에 직접 입력)
• 예제 코드
package hello.springmvc.basic.response;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
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 hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
// @RestController
// @RestController = @Controller + @ResponseBody
// 뷰 템플릿을 사용하는 것이 아니라, Rest API(HTTP API)를 만들 때 사용할때 사용하는 컨트롤러에 붙여주는 어노테이션
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);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3(){
return "ok";
}
@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);
}
// 프로그램 조건에 따라서 동적으로 http 상태코드를 변경하려면 ResponseEntity를 사용
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
• 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를 사용
• @RestController
- @Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있음
- 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력
- Rest API(HTTP API)를 만들 때 사용하는 컨트롤러
- @ResponseBody는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 에노테이션 안에 @ResponseBody + @Controller가 적용되어 있음
HTTP 메시지 컨버터
1. HTTP 메세지 컨버터란
※ 지금까지 여러 애노테이션을 이용해서 JSON, queryString등을 @RequestBody, @ModelAttribute 등으로 편하게 객체로 변환해서 사용했다. 그런데 이쯤에서 어떻게 스프링이 객체로 변환을 해주는지 의문을 가질 필요가 있다.
1.1 @ResponseBody의 사용원리
• @ResponseBody를 사용하니 HTTP의 BODY에 문자 내용을 직접 반환하는데 그림을 보면 viewResolver 대신HttpMessageConverter가 동작한다.
- 기본 문자처리: StringHttpMessageConverter
- 기본 객체처리: MappingJackson2HttpMessageConverter
- byte 처리등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
• 응답 시 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해 HttpMessageConverter가 선택됨.
• 스프링 MVC는 다음의 경우에 HTTP 메세지 컨버터를 적용
- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
2. 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;
}
• HTTP 메세지 컨버터는 요청 및 응답 둘 다 사용됨.
- 요청시 JSON → 객체
- 응답시 객체 → JSON
• canRead(), canWrite()
- 메세지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크하는 메서드
• T read(), void write()
- 메세지 컨버터를 통해 메세지를 실제로 변환하는 메서드
3. 스프링 부트 기본 메세지 컨버터
3.1 컨버터의 종류
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter ... 일부 생략 ... |
3.2 컨버터 사용여부 및 우선순위
• 컨버터를 사용할 대상의 클래스 타입과 미디어 타입을 체크한 뒤 사용여부를 결정
• 등록된 메세지 컨버터들이 순회하며 만족한다면 해당 컨버터를 사용하고 조건을 만족하지 않으면 다음 컨버터로 우선순위가 넘어감.
3.3 주요 컨버터 정리
• ByteArrayHttpMessageConverter : byte[] 데이터를 처리
클래스 타입: byte[], 미디어타입: */*
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[] 미디어 타입 application/octet-stream
• StringHttpMessageConverter : String 문자로 데이터를 처리
클래스 타입: String, 미디어 타입: */*
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok" 미디어 타입 text/plain
• MappingJackson2HttpMessageConverte r: application/json
클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
요청 예) @RequestBody HelloData data
응답 예) @ResponseBody return helloData 미디어 타입 application/json 관련
4. HTTP 요청 데이터 읽기 / 응답 데이터 생성
4.1 HTTP 요청 데이터 읽기
• HTTP 요청이 오면 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용함.
• 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출함.
- @RequestBody의 대상 클래스 타입(byte[] , String , HelloData)을 지원하는지 여부 체크
- HTTP 요청의 Content-Type 미디어 타입(text/plain , application/json , */*)을 지원하는지 여부 체크
• canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환
4.2 HTTP 응답 데이터 생성
• 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환됨.
• 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출함.
- return의 대상 클래스 타입( byte[] , String , HelloData )을 지원하는지 여부 체크
- HTTP 요청의 Accept 미디어 타입(text/plain , application/json , */*)을 지원하는지 여부 체크
(더 정확히는 @RequestMapping 의 produces)
• canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성
요청 매핑 핸들러 어댑터(RequestMappingHandlerAdapter)의 구조
1. RequestMappingHandlerAdapter
1.1 매핑 핸들러가 동작하는 곳
※ 애노테이션 기반의 컨트롤러인 @RequestMapping을 처리하는 핸들러 어댑터 RequestMappingHandlerAdapter에서 매핑 핸들러가 동작
1.2 RequestMappingHandlerAdapter의 동작 방식
• 애노테이션 기반의 컨트롤러(@RequestMapping)는 매우 다양한 파라미터를 사용할 수 있었음
• HttpServletRequest나 Model부터 @RequestParam, @ModelAttribute 같은 애노테이션 그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었음
• 이렇게 많은 요청 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분
• 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 이 ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성함.
• 그리고 이렇게 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨줌
• 스프링은 30개가 넘는 ArgumentResolver를 기본으로 제공함
※ ArgumentResolver에서 지원하는 파라미터 목록메뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annarguments
2. 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;
}
2.1 동작 방식
• ArgumentResolver의 supportsParameter() 메서드를 호출해 해당 파라미터를 지원하는지 체크
- (지원할 경우) resolveArgument() 메서드를 호출해서 실제 객체를 생성
- (지원안할경우) 다음 ArgumentResolver로 우선순위가 넘어감.
3. ReturnValueHandler
※ 정확히는 HandlerMethodReturnValueHandler인데 줄여서ReturnValueHandler라 부름
• 이 또한 ArgumentResolver와 비슷, 요청이 아닌 응답 값을 변환하고 처리
• 컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 이 ReturnValueHandler 덕분
• 스프링은 10개가 넘는 ReturnValueHandler를 지원
ex) ModelAndView, @ResponseBody, HttpEntity, String
※ ReturnValueHandler에서 지원하는 응답값 목록메뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types
4. HTTP 메세지 컨버터
4.1 HTTP 메시지 컨버터 위치
• 위 그림처럼 요청의 경우 컨트롤러가 파라미터로 @RequestBody, HttpEntity를 사용할때 HTTP 메세지 컨버터를 사용
- @RequestBody, HttpEntity를 처리하는 각각의 ArgumentResolver가 있는데,
이 ArgumentResolver들이 HTTP 메세지 컨버터를 사용해서 필요한 객체를 생성
• 응답의 경우에도 컨트롤러의 반환값으로 @ResponseBody, HttpEntity가 사용되는 시점에서 HTTP 메세지 컨버터를 사용
- @ResponseBody와 HttpEntity를 처리하는 각각의 ReturnValueHandler가 있는데,
이 ReturnValueHandler들이 HTTP 메세지 컨버터를 호출해서 응답 결과를 얻음.
4.2 확장
• 스프링은 ArgumentReolver나 ReturnValueHandler, MessageConverter를 모두 인터페이스로 제공하기에 다음과 같은 인터페이스를 언제든지 확장해서 사용 가능
- HandlerMethodArgumentResolver - HandlerMethodReturnValueHandler - HttpMessageConverter |
※ 대부분은 이미 스프링에서 구현되어 제공되기에 실제로 확장할 일이 많지는 않음.
※ 만약 기능 확장을 할 때는 WebMvcConfigurer를 상속받아 스프링 빈으로 등록하면 됨.
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers){
//...
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//...
}
};
}
'일상의 흔적 > Study' 카테고리의 다른 글
인프런 스프링 MVC 2 (백엔드 웹개발 활용 기술) : Thyemleaf 기본기능 - 1 (0) | 2023.03.18 |
---|---|
인프런 스프링 MVC 1 (웹개발 핵심 기술) : 스프링 MVC 웹페이지 만들기 - 7 (0) | 2023.03.18 |
인프런 스프링 MVC 1 (웹개발 핵심 기술) : 스프링 MVC 구조 - 5 (0) | 2023.03.17 |
인프런 스프링 MVC 1 (웹개발 핵심 기술) : MVC 프레임워크 만들기 - 4 (0) | 2023.03.16 |
인프런 스프링 MVC 1 (웹개발 핵심 기술) : 서블릿, JSP, MVC 패턴 - 3 (0) | 2023.03.16 |