HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법에는 크게 3가지가 있다.
이제부터 각각의 방법에 대해 하나씩 알아보자
http://localhost:8080/request-param?username=hello&age=20
POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20
쿼리 파라미터 전송 방식과 HTML Form 전송 방식은
클라이언트가 보내는 방식이 다를 뿐
서버 입장에서는 username=hello&age=20
처럼 서로 같은 형식을 갖고 있다
즉, 서버 입장에서는 같은 형식이기 때문에,
구분 할 필요없이 같은 방법으로 조회할 수 있다.
이런 형식을 조회하는 방법을 요청 파라미터(request parameter) 조회
라 한다.
@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);
}
/** @RequestParam : 파라미터 이름으로 바인딩 **/
@RequestMapping("/request-param-v2")
public void requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", memberName, memberAge);
}
// HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
@RequestMapping("/request-param-v3")
public void requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
}
// String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
@RequestMapping("/request-param-v4")
public void requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
}
// 파라미터 필수 여부를 required 옵션으로 지정할 수 있다 (기본값 : true)
@RequestMapping("/request-param-required")
public void requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
log.info("username={}, age={}", username, age);
}
// 파라미터에 값이 없는 경우 defaultValue 를 사용하면 기본 값을 적용할 수 있다.
// 이미 기본 값이 있기 때문에 required 는 의미가 없다.
@RequestMapping("/request-param-default")
public void requestParamDefault(
@RequestParam(defaultValue = "guest") String username,
@RequestParam(defaultValue = "-1") int age) {
log.info("username={}, age={}", username, age);
}
@ResponseBody
@RequestMapping("/request-param-map")
public void requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
}
실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 다음과 같이 코드를 작성할 것이다.
@RequestMapping("/request-param")
public void requestParam(
@RequestParam String username,
@RequestParam int age) {
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
...
}
스프링은 이 과정을 자동화해주는 @ModelAttribute
를 제공한다.
이 기능을 사용하기 위해서는 먼저 요청 파라미터를 바인딩 받을 객체를 만들어야 한다.
@Data
public class HelloData {
private String username;
private int age;
}
스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
1. HelloData 객체를 생성한다.
2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
3. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.
/** @ModelAttribute : 요청 파라미터를 이용해 필요한 객체 만드는 과정 자동화 **/
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
}
// @ModelAttribute 는 생략할 수 있다.
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
}
참고) @ModelAttribute 는 생략할 수 있다. 그런데 @RequestParam 도 생략할 수 있으니 혼란이 발생할 수 있다.
=> 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
String , int , Integer 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute
요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없다. (물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)
HttpEntity
, @RequestBody
를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해준다.
@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");
}
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity 를 상속받은 다음 객체들도 같은 기능을 제공한다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
⚠️ 주의
HTTP 요청시에 content-type이 application/json인지 꼭! 확인해야 한다.
그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행된다.
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream(); // HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어온다
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); // 문자로 변환한다
log.info("messageBody={}", messageBody);
HelloData data = objectMapper.readValue(messageBody, HelloData.class); // 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 이용해 자바 객체로 변환
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
// 문자로 변환하고 다시 json으로 변환
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
// @ModelAttribute처럼 한번에 객체로 변환
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
@RequestBody 요청 : JSON 요청 -> HTTP 메시지 컨버터 ->객체
@ResponseBody 응답 : 객체 -> HTTP 메시지 컨버터 -> JSON 응답
요청 파라미터 vs HTTP 메시지 바디
@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)
){
return "ok";
}
}
참고 : MultiValueMap은 하나의 key에 여러 value를 가질 수 있는 map
인프런 김영한님의 스프링 MVC 1편의 [섹션6 - Spring MVC 기본기능]
스프링 공식문서의 Method Arguments