1. HTTP 요청
기본, 헤더 조회
RequestMappingRequestHandler가 동작하는 애노테이션 기반의 스프링 Controller는 다양한 파라미터를 지원한다. 기본값과 헤더값을 꺼내는 방법을 알아보자.
- Locale
- 우선순위가 가장 높은 Locale 정보(현재 시스템에 설정된 국가 및 언어 설정)를 조회한다.
- 우선순위나 처리 방법은 LocaleResolver 참고
- @RequestHeader MultiValueMap<String, String> headerMap
- 모든 HTTP header를 MultiValueMap 형식으로 조회한다.
- @RequestHeader("host") String host
- 특정 HTTP 헤더를 조회한다.
- 속성
- 필수 값 여부: required / 기본 값 속성: defaultValue
- @CookieValue(value = "myCookie", required = false) String cookie
- 특정 쿠키를 조회한다.
- 속성
- 필수 값 여부: required / 기본 값 속성: defaultValue
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request, HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie) {
log.info("request={}", request);
log.info("response={}", response);
log.info("httpMethod={}", httpMethod);
log.info("locale={}", locale);
log.info("headerMap={}", headerMap);
log.info("header host={}", host);
log.info("myCookie={}", cookie);
return "ok";
}
}
MultiValueMap
- Map과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
- HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
- keyA=value&keyA=value2
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");
List<String> values = map.get("keyA"); // [value1, value2]
참고
@Controller의 사용 가능한 파라미터 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
@Controller의 사용 가능한 응답 값 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
2. HTTP 요청 파라미터
서블릿에서 HTTP 요청 데이터를 조회하는 방법을 학습했었다. 스프링은 그 복잡했던 내용을 깔끔하고 효율적으로 바꿔준다.
우선, HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법을 알아보자. 주로 다음 3가지 방법을 사용한다.
- GET - 쿼리 파라미터
- /url?username=hello&age=20
- message body 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달한다.
- ex. 검색, 필터, 페이징 등에서 주로 사용하는 방식
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
- message body에 쿼리 파라미터 형식으로 전달한다. (username=hello&age=20)
- ex. 회원가입, 상품 주문, HTML Form 사용
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용한다.
- 데이터 형식엔 JSON, XML, TEXT 등이 있으며, 주로 JSON을 사용한다.
- HTTP method로는 POST, PUT, PATCH가 다 가능하다.
아래에서 하나씩 알아보자.
쿼리 파라미터, HTML Form
HttpServletRequest의 request.getParameter()를 사용하면 다음 두 가지 요청 파라미터를 조회할 수 있다. 아래 두 전송 방식은 데이터 형식이 같기 때문에 구분 없이 조회할 수 있다. 이것을 간단히 요청 파라미터(request parameter) 조회라고 한다.
- GET - 쿼리 파라미터 전송
- ex. http://localhost:8080/request-param?username=hello&age=20
- POST - HTML Form 전송
스프링은 아래 코드의 메서드처럼, 반환 타입이 없으면서(void) response에 값을 직접 집어넣는 경우엔 View를 조회하지 않는다.
/**
* 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회 X
*/
@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");
}
참고
Packaging으로 Jar를 사용하면 webapp 경로를 사용할 수 없다. 이제부터 정적 리소스도 class 경로에 함께 포함해야 한다.
/resources/static 경로 아래에 HTML 파일을 두면 스프링 부트가 자동으로 인식한다. (자동 공개)
@RequestParam
스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 편리하게 사용할 수 있다.
a. 파라미터 이름 속성
@RequestParam의 name(value) 속성이 파라미터 이름으로 사용된다.
- @RequestParam("username") String memberName → request.getParameter("username")
/**
* @RequestParam 사용
* - 파라미터 이름으로 바인딩
* @ResponseBody 추가
* - View 조회를 무시하고, HTTP message body에 직접 해당 내용("ok") 입력
*/
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
b. 파라미터 이름 속성 생략
HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam의 name(value) 속성을 생략할 수 있다.
/**
* @RequestParam 사용
*/
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(@RequestParam String username, @RequestParam int age) {}
c. @RequestParam 애노테이션 생략
직접 만든 class 객체가 아니라 String, int, Integer 등의 단순 타입이면 @RequestParam도 생략할 수 있다.
- 애노테이션 자체를 생략하면, 스프링 MVC는 내부에서 required=false를 적용한다.
- @RequestParam이 있으면 명확하게 요청 파라미터에서 값을 읽어온다는 것을 알 수 있다. 따라서 생략하지 않는 게 직관적으로 볼 수 있다는 장점이 있다.
/**
* @RequestParam 사용
*/
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {}
d. 파라미터 필수 여부 속성 - required
파라미터 중 username 값은 requied = true이므로 null 값이 들어올 수 없어 무조건 입력해야 한다.
- 입력하지 않으면 400 에러가 뜨게 된다.
- 빈 문자를 넘기면 에러가 뜨지 않고 통과된다. (빈 문자 ≠ null)
age 값은 required = false이므로 null 값이 들어올 수 있다.
- 이때 int엔 null을 입력할 수 없으므로 age의 타입을 Integer로 변경하거나 기본값을 설정할 수 있는 defaultValue를 사용해야 한다.
- int로 설정해 두면 서버에 500 에러가 뜨게 된다.
/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 예외
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*/
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {}
e. 파라미터 기본값 속성 - defaultValue
파라미터에 값이 없는 경우 defalutValue 속성을 사용하면 기본값을 적용할 수 있다. 이미 기본값이 정해지기 때문에 required 속성은 의미가 없어지게 된다.
- 파라미터가 빈 문자로 들어오더라도 설정해 둔 기본값이 적용된다. (빈 문자 ≠ null)
- ex. /request-param-default?username=
/**
* @RequestParam
* - defaultValue 사용
*/
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age) {}
f. 파라미터를 Map으로 조회하기
파라미터를 Map, MultiValueMap으로 조회할 수 있다. 파라미터의 값이 1개가 확실한 경우에만 Map을 사용하고, 그렇지 않다면 MultiValueMap을 사용해야 한다.
/**
* @RequestParam Map, MultiValueMap
* Map(key=value)
* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
*/
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
@ModelAttribute
실제 개발을 하면 요청 파라미터를 받아 필요한 객체를 만들고, 그 객체에 값을 넣어줘야 한다. 보통 아래와 같이 코드를 작성하게 된다.
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
- 스프링은 이 과정을 완전히 자동화해 주는 @ModelAttribute 기능을 제공한다.
참고
Lombok의 @Data 애노테이션을 사용하면 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 애노테이션을 자동으로 적용해 준다.
a. @ModelAttribute 적용
스프링 MVC는 @ModelAttribute 애노테이션이 있으면 다음을 실행한다. 아래 코드를 예시로 살펴보자.
- HelloData 객체를 생성한다.
- 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다
- 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다.
- ex. 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출한 뒤 값을 입력한다.
/**
* @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";
}
log.info("helloData={}", helloData);
- 실행 시 helloData 객체에 @ToString 애노테이션이 있기 때문에 helloData.toString()이 자동으로 적용돼 이쁘게 출력된다.
- ex. helloData=HelloData(username=a, age=10)
프로퍼티
객체에 getUsername(), setUsername() 메서드가 있으면, 이 객체는 username이라는 프로퍼티를 갖고 있다. username 프로퍼티의 값을 변경하면 setUsername()이 호출되고, 조회하면 getUsername()이 호출된다.
참고
age=abc처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException이 발생한다. 이런 바인딩 오류를 처리하는 방법은 검증 부분에서 다룬다.
b. @ModelAttribute 생략
@ModelAttribute 애노테이션도 생략할 수 있다. 그런데 @RequestParam 애노테이션도 생략할 수 있기 때문에 둘 다 없는 경우엔 혼란이 발생할 수 있다.
/**
* @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로 지정해 둔 타입 외; 뒤에서 학습)
3. HTTP 요청 메시지
단순 텍스트
요청 파라미터와는 다르게, HTTP message body를 통해 데이터가 직접 넘어오는 경우엔 @RequestParam이나 @ModelAttribute를 사용할 수 없다.
- 물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.
- 테스트는 Postman으로 진행해야 한다. (Body로 row, Text 선택)
a. InputStream을 사용해서 직접 읽기
먼저 가장 단순한 텍스트 메시지를 HTTP message body에 담아서 전송하고 읽어보자. InputStream을 사용해서 직접 읽을 수 있다.
@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");
}
b. InputStream과 OutputStream을 파라미터로 받기
스프링 MVC는 다음 파라미터를 지원한다. 서블릿과 관련된 코드 대신 사용할 수 있다.
- 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");
}
c. HttpEntity 사용하기
스프링 MVC는 다음 파라미터를 지원한다.
- HttpEntity: HTTP header, body 정보를 편리하게 조회할 수 있다.
- 메시지 바디 정보를 직접 조회한다.
- 요청 파라미터를 조회하는 기능과는 관계가 없다. (@RequestParam X, @ModelAttribute X)
- HttpMessageConverter를 사용하며, HttpEntity<String>이기 때문에 StringHttpMessageConverter가 적용된다.
- HttpEntity는 응답에도 사용할 수 있다.
- 메시지 바디 정보를 직접 반환한다.
- header 정보를 포함할 수 있지만, View를 조회하지 않는다.
@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를 상속받은 다음 객체들도 같은 기능을 제공한다.
- RequestEntity
- HttpMethod, URL 정보가 추가됐고, 요청에서 사용한다.
- ResponseEntity
- HTTP 상태 코드를 설정할 수 있고, 응답에서 사용한다.
- ex. return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
참고
스프링 MVC 내부에서 HTTP message body를 읽어서 문자나 객체로 변환한 뒤 전달해 주는데, 이때 HttpMessageConverter라는 기능을 사용한다. 자세한 내용은 다음에 설명한다.
d. @RequestBody 사용하기
@RequestBody를 사용하면 HTTP message body 정보를 편리하게 조회할 수 있다. 참고로 header 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.
- 이렇게 메시지 바디를 직접 조회하는 기능(@RequestBody)은 요청 파라미터를 조회하는 @RequestParam이나 @ModelAttribute와는 전혀 관계가 없다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
@ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다. 물론 이 경우에도 View를 사용하지 않는다.
JSON
이번에는 HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해 보자. 기존 서블릿에서 사용했던 방식과 비슷하게 시작한다.
- Postman으로 테스트해야 한다.
- POST http://localhost:8080/request-body-json-v1
- raw, JSON, content-type: application/json
- {"username":"hello", "age":20}
a. HttpServletRequest 사용하기
직접 HTTP message body에서 데이터를 읽어와서 문자로 변환한다.
- 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다.
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");
}
b. @RequestBody 사용하기
@RequestBody를 사용해 HTTP message에서 데이터를 꺼내고, messageBody에 저장한다.
- 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다.
- 이때, 문자로 변환하고 다시 JSON으로 변환하는 과정이 조금 불편하다. @ModelAttribute 애노테이션처럼 한 번에 객체로 변환할 수는 없을까?
@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";
}
c. @RequestBody로 객체로 변환하기
@RequestBody에 직접 만든 객체를 지정할 수 있다.
- 이때, @RequestBody를 생략하면 @ModelAttribute가 적용되어 HTTP message body 대신 요청 파라미터를 처리하게 되기 때문에 생략할 수 없다.
- HttpEntity, @RequestBody를 사용하면 HTTP message Converter(MappingJackson2HttpMessageConverter)가 HTTP message body의 내용을 우리가 원하는 문자나 객체 등으로 변환해 준다. v2에서 한 작업도 HTTP message Converter가 대신 처리해 준다.
/**
* @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";
}
참고
HTTP 요청 시 content-type이 application/json인지 꼭 확인해야 한다. 그래야 JSON을 처리할 수 있는 HTTP message Converter가 실행된다.
d. HttpEntity 사용하기
위의 단순 텍스트에서 배운 것과 같이 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";
}
e. @ResponseBody 사용하기
응답의 경우에도 @ResponseBody를 사용하면 해당 객체를 HTTP message body에 직접 넣을 수 있다. 물론 이 경우에도 HttpEntity를 사용해도 된다.
- @RequestBody 요청
- JSON 요청 → HTTP message Converter → 객체
- @ResponseBody 응답
- 객체 → HTTP message Converter → JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}