반응형

로깅

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) {
			//...
		}
	};
}
반응형

+ Recent posts