HTTP 요청 - 기본, 헤더 조회
- HttpServletReqeust, HttpServletResponse
- HttpMethod : HTTP 메서드 조회
- Locale : Locale 정보 조회 (언어)
- @RequestHeader MultiValueMap<String, String> headerMap
- 모든 HTTP 헤더를 MultiValueMap으로 조회한다.
- @RequestHeader(“host”) String host
- 특정 HTTP 헤더를 조회한다.
- @CookieValue(value = “myCookie”, required = false) String cookie
- 특정 쿠키를 조회한다.
MultiValueMap
MAP과 유사하지만 하나의 키에 여러 값을 받을 수 있다.
- HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
- keyA=value1&keyA=value2
1
List<String> values = map.get("keyA");
HTTP 요청 파라미터
서블릿에서 학습했던 HTTP 요청 데이터를 조회하는 방법을 다시 떠올려보자. 서블릿으로 학습했던 내용을 스프링이 얼마나 깔끔하고 효율적으로 바꾸어주는지 알아본다.
HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법
- GET - 쿼리파라미터
- /url?username=hello&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해 전달
- ex) 검색 필터, 페이징 등에서 많이 사용
- POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 (username=kim&age=20)
- ex) 회원가입, 상품주문, HTML Form 사용
- HTTP message body에 데이터를 직접 담아 요청
- HTTP API에서 주로 사용. JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH
요청 파라미터 조회 (쿼리 파라미터, HTML Form)
GET 쿼리 파라미터든 POST HTML Form 전송 방식이든 둘 다 형식이 같으므로 구분없이 조회할 수 있다.
이것을 간단히 요청 파라미터 조회라고 한다.
우선 서블릿처럼 request 를 이용해 request.getParameter(“username”)로 받을 수 있다.
다음을 보자.
@RequestParam
- @RequestParam
- 파라미터 이름으로 바인딩
- 파라미터 이름이 변수명과 같다면 (“username”) 생략 가능
- @ResponseBody
- 위를 @RestController가 아닌 @Controller로 했을 때 뷰를 찾게 되는데 @RestController 처럼 VIew 조회를 무시하고 HTTP message body에 직접 해당 내용을 입력한다.
@RequestParam마저 생략 가능하다.
- String, int 등의 단순 타입일 때 가능하다.
애노테이션이 이렇게 생략은 되지만 명확하게 요청 파라미터에서 데이터를 읽는다는 것을 한눈에 알기는 어려움으로 생략하는 것은 고민해봐야 할 문제.
참고 (required=true, false)
- 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false를 적용한다.
- 파라미터가 필수가 아니라는 뜻이다.
- 반대로 true라고 되어 있으면 파라미터가 없으면 400 예외를 발생시킨다.
- 기본값은 true이다.
- /request-param?username=kim 과 같이 주어지고 age에 false로 한다면?
- 잘 되어야 할 것 같은데 500오류가 발생한다.
- int age인데 int에는 null 값이 들어갈 수 없다. 따라서 Integer로 바꿔주어야 한다.
- /request-param?username= 과 같이 빈칸으로 둔다면?
- null이 아닌 ““로 처리된다.
- @RequestParam( required = true, defalutValue = “guest”) 와 같이 기본값을 줘서 해결할 수 있다.
- 이 경우 사실 required의 값은 무의미하다.
Map으로 받기
파라미터를 Map으로 한 번에 받을 수 있다.
파라미터의 값이 1개가 확실하다면 Map을 사용하지만 그렇지 않다면 MultiValueMap을 사용한다. (보통은 하나를 사용하긴 한다.)
@ModelAttribute
실제 개발을 하면 요청 파라미터를 받아 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 아래와 같이 코드를 작성할 것이다.
1
2
3
4
5
6
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
스프링은 이런 과정을 자동화해주는 @ModelAttribute 기능을 제공한다.
먼저 요청 파라미터를 받을 바인딩 객체를 만들어본다.
1
2
3
4
5
@Data
public class HelloData{
private String username;
private int age;
}
- @Data (롬복)
- @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동 적용 해준다.
v1
HelloData 객체가 생성되고 요청 파라미터의 값도 모두 들어가 있다.
스프링 MVC는 @ModelAttribute가 있으면 아래를 실행한다.
- HelloData 객체를 생성한다.
- 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾는다.
- 해당 프로퍼티의 setter를 호출해 파라미터의 값을 입력 한다.
- ex) 파라미터의 이름이 ‘username’이면 setUsername() 메서드를 찾아 호출하면서 값을 입력한다.
바인딩 오류
‘age = abc’ 처럼 숫자가 들어갈 곳에 문자를 넣으면 BindException이 발생한다. 이러한 바인딩 오류를 처리하는 방법은 검증에서 다룬다.
v2
@ModelAttribute의 생략도 가능하다. 그런데 @RequestParam도 생략할 수 있어 혼란이 발생할 수 있다.
그래서 스프링은 해당 생략시 아래와 같은 규칙을 적용한다.
- ‘String’, ‘int’, ‘Integer’ 같은 단순 타입 = @RequestParam
- 이 외 나머지 = @ModelAttribute (argument resolver로 지정해둔 타입 제외)
- argument resolver는 추후 다룬다.
HTTP 요청 메시지
요청 파라미터와 다르게 HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.
이런 경우에 대해 알아보자.
단순 텍스트
위의 방법 2가지는 스프링을 어느정도 활용하지 않고 가져올 수 있는 방법이다. inputStream 등등 사용하기 번거롭다.
단순 텍스트를 가져오는 방법 아래를 보자.
HttpEntity
- HttpEntity: HTTP header, body 정보를 편리하게 조회한다.
- 메시지 바디 정보를 직접 조회
- 요청 파라미터를 조회하는 기능과 관계 없음 (@RequestParam, @ModelAttribute)
- HttpEntity는 응답에도 사용 가능하다.
- 메시지 바디 정보 직접 반환
- 헤더 정보 포함 가능
- 당연히 view 조회 X
HttpEntity를 상속받은 아래의 객체들도 같은 기능을 제공한다.
- RequestEntity
- HttpMethod, url 정보가 추가
- 요청에서 사용된다.
- ResponseEntity
- HTTP 상태 코드 설정 가능
- 응답에서 사용된다.
1
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED)
@RequestBody
얘가 가장 많이 쓰인다.
- @RequestBody
- 메시지 바디를 바로 조회가능.
- 헤더정보가 필요하다면 @RequestHeader를 이용하면 된다.
- @RequestParam, @ModelAttribute와 관계없다.
- @ResponseBody
- 응답 결과를 HTTP 메시지 바디에 직접 담아 전달한다.
- 이 경우도 당연히 view를 사용하지 않는다.
JSON 데이터
서블릿에서는 이렇게 받아왔었다.
스프링을 이용해 어떻게 하는지 보자.
@RequestBody 이용
단순히 바디를 @RequestBody로 가져오고 똑같이 매핑해줄 수 있다. 하지만 이것도 번거롭다.
@RequestBody 이용 v2
@ModelAttribute 처럼 바로 객체를 대입해 setUsername()과 같은 작업을 자동으로 하게 할 수 있다.
만약 여기에 JSON이 아닌 일반 TEXT를 POST하면 415 오류가 발생한다. username과 age를 매핑할 수 없기 때문이다.
- @RequestBody에 직접 만든 객체를 지정할 수 있다.
- HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
- HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해준다.
참고
@RequestBody는 @RequestParam이나 @ModelAttribute처럼 생략하면 안된다.
만약 생략하면 @ModelAttribute가 붙는 것과 같기 때문에 HTTP 메시지 바디가 아닌 요청 파라미터를 처리하게 된다.
객체를 JSON으로 응답 (@ResponseBody)
이렇게 객체를 @ResponseBody를 통해 반환하게 되면 JSON 형태로 응답하게 된다.
- @RequestBody
- JSON 요청 -> HTTP 메시지 컨버터 -> 객체
- @ResponseBody 응답
- 객체 -> HTTP 메시지 컨버터 -> JSON 응답
HTTP 응답
스프링에서 응답 데이터를 만드는 방법은 크게 3가지이다.
- 정적 리소스
- ex) 웹 브라우저에 정적인 HTML, css, js 등을 제공할 때는 정적 리소스를 사용한다.
- 뷰 템플릿 사용
- ex) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
- HTTP 메시지 사용
- HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
정적 리소스
스프링 부트는 클래스 패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
- /static
- /public
- /resources
- /META-INF/resources
정적 리소스 주소 src/main/resources/static
src/main/resources/static/basic/hello-form.html 경로에 파일이 있다고 하면
http://localhost:8080/basic/hello-form.html 로 실행하면 된다.
정적 리소스는 파일을 변경 없이 그대로 서비스 하는 것이다.
뷰 템플릿
스프링 부트의 기본 뷰 템플릿 경로는 아래와 같다.
- src/main/resources/templates
위 경로에 위와 같은 파일을 넣는다.
th:text=의 data를 사용하려면 컨트롤러가 필요하다. 만들어보자.
v1
data에 hello!를 넣었고 반환해 호출하면 hello!를 출력하게 된다.
v2
String으로 뷰의 논리적 이름을 반환해 뷰를 찾는다.
데이터는 Model 객체에 넣어 addAttribute를 해주면 보낼 수 있다.
이 형태를 주로 사용하게 된다.
복습
@ResponseBody가 없으면 response/hello를 뷰 리졸버가 실행되어 뷰를 찾고, 렌더링 한다.
있으면 메시지 바디에 직접 문자를 입력한다.
HTTP API, 메시지 바디에 직접 입력
이는 위에서 다루었다. 아래 코드들을 보고 복습한다.
@ResponseBody와 String을 반환해 문자를 직접 전달할 수 있었고,
@RequestBody로 JSON 데이터를 객체로 받고, @ResponseBody를 이용해 객체를 반환하면 JSON으로 다시 바꾸어 보여줄 수 있다.
참고
@RestController를 붙이면 @ResponseBody를 안붙여도 되는 것. 이름 그대로 Rest API(HTTP API)를 만드는 데 사용된다.
이를 이용해 보통 HTTP API를 작성할 때는 아래와 같은 구조로 사용하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class APIController{
@ResponseStatus(HttpStatus.OK)
@GetMapping("/httpapi")
public HelloData httpApi(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
HTTP 메시지 컨버터
뷰 템플릿으로 HTML을 생성해 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.
- @ResponseBody를 사용
- viewResolver 대신 HttpMessageConverter가 동작
- 기본 문자 처리 : StringHttpMessageConverter
- 기본 객체 처리 : MappingJackson2HttpMessageConverter
- byte 처리 등등 기타 여러 HttpMessageConverter가 등록되어 있다.
- viewResolver 대신 HttpMessageConverter가 동작
HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 이 둘을 조합해 HttpMessageConverter가 선택된다.
HTTP 메시지 컨버터 인터페이스
HTTP 요청, 응답 둘 다 사용된다.
- canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
- read(), write() : 메시지 컨버터를 통해 메시지를 읽고 쓰는 기능
컨버터 종류
1
2
3
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
대략 위가 기본 메시지 컨버터이고
대상 클래스 타입과 미디어 타입 둘을 체크해 사용 여부를 결정한다. 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
- ByteArrayHttpMessageConverter
- byte[] 데이터를 처리한다.
- 클래스 타입: ‘byte[]’, 미디어타입: ‘전체’
- 요청 예시) @RequestBody byte[] data
- 응답 예시) @ResponseBody return byte[]
- StringHttpMessageConverter
- String 문자로 데이터를 처리한다.
- 클래스 타입: String, 미디어타입: ‘전체’
- 요청 예시) @RequestBody String data
- 응답 예시) @ResponseBody return “ok”
- MappingJackson2HttpMessageConverter
- JSON 관련 컨버터
- 클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
- 요청 예시) @RequestBody HelloData data
- 응답 예시) @ResponseBody return data
HTTP 요청 데이터 읽기 과정
- HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용한다.
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
- 대상 클래스 타입을 지원하는가
- ex) @RequestBody의 대상 클래스(‘byte[]’, String, ‘HelloData’)
- HTTP 요청의 Content-Type 미디어 타입을 지원하는가
- ex) ‘text/plain’, ‘application/json’
- 대상 클래스 타입을 지원하는가
- canRead() 조건을 만족하면 read()를 호출해 객체를 생성하고 반환한다.
HTTP 응답 데이터 생성 과정
- 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환된다.
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
- 대상 클래스 타입을 지원하는가
- ex) return 대상의 클래스(byte[], String, HelloData)
- HTTP 요청의 Accept 미디어 타입을 지원하는가 (@RequestMapping의 produces)
- ex) text/plain, application/json 등
- 대상 클래스 타입을 지원하는가
- canWrite() 조건을 만족하면 write()를 호출해 HTTP 응답 메시지 바디에 데이터를 생성한다.
예시
1
2
3
4
content-type: application/json
@RequestMapping
void hello(@RequestBody String data) {}
이면?
ByteArrayhttpMessageConverter은 클래스 타입이 맞지 않으니 패스
StringHttpMessageConverter는 클래스 타입 일치하고, 미디어타입은 전체이니까 가능.
얘를 쓰게 된다.
1
2
3
4
content-type: application/json
@RequestMapping
void hello(@RequestBody HelloData data) {}
이면?
같은 과정으로 MappingJackson2HttpMessageConverter를 사용하게 된다.
여기서 만약 content-type: text/html 이라면?
MappingJackson2HttpMessageConverter도 탈락.
요청 매핑 핸들러 어댑터(RequestMappingHandlerAdapter) 구조
HTTP 메시지 컨버터는 스프링 MVC 어디쯤에서 사용되는 것인가.
비밀은 애노테이션 기반의 컨트롤러, 즉 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에 있다.
동작 방식
- ArgumentResolver
- 애노테이션 기반 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
- HttpServletRequest, Model, @RequestParam, @RequestBody 등등..
- 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 ArgumentResolver이다.
- RequestMappingHandlerAdapter는 얘를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값(객체)을 생성한다. 그리고 이렇게 파라미터의 값이 모두 준비되면 컨트롤러를 호출하며 값을 넘겨준다.
- supportsParameter()를 호출해 해당 파라미터를 지원하는지 체크하고,
- 지원한다면 resolveArgument()를 호출해 실제 객체를 생성한다.
- 그리고 컨트롤러 호출 시 객체를 넘긴다.
- ReturnValueHandler
- ArgumentResolver와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
- 컨트롤러에서 String으로 뷰 이름을 반환해도 동작하는 이유가 얘 덕분이다.
메시지 컨버터의 위치
HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.
@ResponseBody도 컨트롤러의 반환 값을 이용한다.
- 요청
- @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있다.
- 이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해 필요한 객체를 생성하는 것이다.
- 응답
- 마찬가지로 RetrunValueHandler에서 HTTP 메시지 컨버터를 호출해 응답 결과를 만든다.
확장
스프링은 아래를 모두 인터페이스로 제공한다. 따라서 확장이 가능하다.
- HandlerMethodArgumentResolver
- HandlerMethodReturnValueHandler
- HttpMessageConverter
스프링이 필요한 대부분의 기능을 제공하기 때문에 확장할 일은 별로 없다. 기능 확장은 WebMvcConfigurer를 상속받아 스프링 빈으로 등록하면 된다. 실제 필요할 때 WebMvcConfigurer를 검색해보자.