실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 아래와 같이 코드를 작성할 것이다!
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
⬆️ 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute
기능을 제공한다!!
✔️ HelloData
- 요청 파라미터를 바인딩 받을 객체 생성
package hello.springmvc.basic;
import lombok.Data;
@Data
public class HelloData {
private String username;
private int age;
}
✔️ @ModelAttribute 적용 - modelAttributeV1
/**
* @ModelAttribute 사용
* 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때 자세히 설명
*/
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ModelAttribute
가 있으면 다음을 실행한다.HelloData
객체를 생성한다.setUsername()
메서드를 찾아서 호출하면서 값을 입력한다.✔️ @ModelAttribute 생략 - modelAttributeV2
/**
* @ModelAttribute 생략 가능
* String, int 같은 단순 타입 = @RequestParam
* argument resolver 로 지정해둔 타입 외 = @ModelAttribute *
*/
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
@ModelAttribute
는 생략할 수 있다.@RequestParam
도 생략할 수 있으므로 이럴 때 혼란이 발생할 수 있다!따라서 스프링은 해당 생략시 다음과 같은 규칙을 적용한다❗️
String
, int
, Integer
와 같은 단순 타입 = @RequestParam
@ModelAttribute
(argument resolver 로 지정해둔 타입 외)서블릿에서 학습한 내용을 떠올려보자!
HTTP message body에 데이터를 직접 담아서 요청
✔️ RequestBodyStringController
생성
package hello.springmvc.basic.request;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
@Slf4j
@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");
}
/**
* InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
* OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
*/
@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");
}
/**
* HttpEntity: HTTP header, body 정보를 편라하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회 X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
}
📌 요청 파라미터 vs HTTP 메시지 바디
- 요청 파라미터를 조회하는 기능 :
@RequestParam
,@ModelAttribute
- HTTP 메시지 바디를 직접 조회하는 기능 :
@RequestBody
이번에는 HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해보자.
기존 서블릿에서 사용했던 방식과 비슷하게 시작할 것이다🙂
✔️ RequestBodyJsonController
생성
package hello.springmvc.basic.request;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* {"username":"hello", "age":20}
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new 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 data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
}
✔️ Version 2 @RequestBody 문자 변환
/**
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 *
* @ResponseBody
* - 모든 메서드에 @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@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";
}
➡️ 문자로 변환하고 다시 json으로 변환하는 과정이 불편하다🤔 @ModelAttribute
처럼 한번에 객체로 변환할 수는 없을까?
✔️ Version 3 @RequestBody 객체 변환
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@RequestBody
객체 파라미터
@RequestBody HelloData data
@RequestBody
에 직접 만든 객체를 지정할 수 있다.HttpEntity
, @RequestBody
를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.🤚🏻
@RequestBody는
생략 불가능
✔️ Version 4 HttpEntity
@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";
}
✔️ Version 5
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-
type: application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
(Accept: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
@ResponseBody
@ResponseBody
를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.HttpEntity
를 사용해도 된다.스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지이다❗️
1️⃣ 정적 리소스
2️⃣ 뷰 템플릿 사용
3️⃣ HTTP 메시지 사용
스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다!!
/static , /public , /resources , /META-INF/resources
src/main/resources
는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다.
따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
정적 리소스 경로
src/main/resources/static
⬇️아래 경로에 파일이 들어있으면⬇️
src/main/resources/static/basic/hello-form.html
웹 브라우저에서 이렇게 실행하면 된다!
http://localhost:8080/basic/hello-form.html
정적 리소스는 해당 파일을 변경 없이 그대로 서비스하는 것❗️
뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다!
📌 스프링 부트는 기본 뷰 템플릿 경로를 제공한다.
src/main/resources/templates
src/main/resources/templates/response/hello.html
라는 뷰 템플릿을 생성하고,
뷰 템플릿을 호출하는 컨트롤러도 만들어 보자!
✔️ ResponseViewController
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 mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
@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!!");
}
}
@ResponseBody
가 없으면 response/hello
로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링 한다.@ResponseBody
가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello
라는 문자가 입력된다.@ResponseBody
, HttpEntity
를 사용하면, 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 응답 데이터를 출력할 수 있다.HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어보낸다!
HTTP 요청에서 응답까지 대부분 다루었으므로 이번시간에는 정리를 해보자☺️
✔️ResponseBodyController
생성
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.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Controller
//@RestController
public class ResponseBodyController {
// 서블릿을 직접 다룰 때 처럼
// HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
/**
* HttpEntity, ResponseEntity(Http Status 추가)
* @return
*/
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
// ResponseEntity 엔티티는 HttpEntity 를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다.
// ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.
return new ResponseEntity<>("ok", HttpStatus.OK);
}
// @ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다.
// ResponseEntity 도 동일한 방식으로 동작한다.
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
// ResponseEntity 를 반환한다.
// HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.
@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);
}
// @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다.
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
📌
@RestController
@Controller
대신에@RestController
애노테이션을 사용하면, 해당 컨트롤러에 모두@ResponseBody
가 적용되는 효과가 있다.- 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다.
- 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러!
참고로@ResponseBody
는 클래스 레벨에 두면 전체에 메서드에 적용되는데,@RestController
에노테이션 안에 @ResponseBody 가 적용되어 있다.
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라,
HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다👍🏻
⬆️ 스프링 입문 강의 내용을 다시 살펴보고 진행하자!
스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다❗️
@RequestBody
, HttpEntity(RequestEntity)
@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;
}
HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다❗️
canRead()
, canWrite()
: 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크 read()
, write()
: 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능📌 스프링 부트 기본 메시지 컨버터 (일부 생략)
- 스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다.
- 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다!
스프링 너, 내가 우스워?
스프링 : 네
안녕하세요! 스프링 부트 관련 글 잘 보고 있습니다! 다름이 아니라 현재 프로젝트 하나를 진행 중인데, 막히는 부분이 하나 있어서. 혹시 질문도 받아주실 수 있으신가요?