RequestMappingHandlerAdapter
가 동작한다고 하였다.@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";
}
}
참고로 locale에서 localeResolver라는게 또 따로 있는데 locale을 쿠키나 세션에 저장해두고 여러 처리를 또 할 수 있다.
또 MultiValueMap은 이름 그대로 여러개의 value, 하나의 키에 여러개의 값을 가질 수 있는 Map 객체이다.
그럼 되는거 안 되는거를 확인하고 싶을 껀데 아래 홈페이지를 들어가면 확인할 수 있다.
컨트롤러에서 사용 가능한 파라미터 목록 공식 docs
컨트롤러에서 응답 값 목록 공식 docs
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
String age = request.getParameter("age");
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
혹은 query string이라고도 불렸던 GET 방식의 요청방식은
HttpServelet의 request.parameter으로 조회가 가능했는데 이는
POST, HTML Form이든 모두 조회가 가능했다.
참고로 .jar로 빌드할 시에는 내장톰캣을 사용하기 때문에 webapp 경로를 사용할 수 없기 때문에 정적리소스들을 static 경로에다 넣어주면 된다.
그러면 spring boot가 알아서 다 서빙해준다.
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String userName, @RequestParam("age") int userAge) {
log.info("userName={}, userAge={}", userName, userAge);
return "ok";
}
이것역시 앞서 했던 것인데 문제는 이 전체 클래스다 @RestController가 아닌 일반 @Controller일 때 반환타입이 String이라면 view를 찾는다고 하였는데 @ResponseBody를 넣으면 http response body로 반환되기 때문에 마치 @RestController를 넣은것과 같은 효과를 볼 수 있다.
또 만약 요청 파라미터의 key값과 변수이름을 일치시키면 모두 생략할 수 있다고도 배웠다.
그런데 추가로 원시타입일때 이때는 @RequestParam을 생략할 수도 있다.
참조타입일 땐 요청 파라미터의 key값과 변수 이름이 같아도 생략 불가
다만 너무 생략하게 되면 추후 이게 어디서 넘어오는 것인지 직관적이지 않기 때문에 @RequestParam정도는 넣어주는게 좋을 것 같다.
/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 예외
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*
* 주의!
* /request-param-required
* int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용)
*/
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
log.info("username={}, age={}", username, age);
return "ok";
}
이렇게 생겼고 default는 required = true이다.
즉, 위에 변수로 들어갔다면 값이 없으면 400 error bad request 오류가 난다는 것이다.
이때 또 주의해야할 점이
바로 타입이다. Integer로 한 이유는 만약 클라이언트 개발자가 값을 required가 false라 안 보냈다면 null로 들어올 텐데 그렇게 되면 int는 null을 받을 수 없기 때문에 500 에러가 난다. 그래서 이땐 반드시 Integer로 설정해줘야한다.
또 두번째로 조심해야할 점이
바로 빈문자열이다. "" != null 이기 때문에 로직상 이상없이 통과해버린다.
따라서 이것에 대한 로직을 클라이언트가 처리하고 또 서버에서도 처리해주면 좋다.
/**
* @RequestParam - defaultValue 사용
* <p>
* 참고: defaultValue는 빈 문자의 경우에도 적용
* /request-param-default?username=
*/
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age
) {
log.info("username={}, age={}", username, age);
return "ok";
}
참고로 어차피 값이 없으면 설정한 기본값이 들어가기 때문에 required 속성이 의미가 없어진다.
그리고 이게 좋은게 required과는 다르게 빈 문자열에도 적용이 되어 빈문자열이 넘어오면 자동으로 defaultValue가 들어간다.
/**
* @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";
}
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
class HelloData {
getUsername();
setUsername();
}
참고로 RequestParam처럼 얘도 생략이 가능한데 그럼 둘다 생략해버리면 뭐가 적용되는 어캐앎?
스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
String , int , Integer 같은 단순 타입 생략 = @RequestParam
나머지 타입 생략 = @ModelAttribute (argument resolver "HttpRequest 처럼 예약어들" 로 지정해둔 타입 외)
HTTP message body에 데이터를 직접 담아서 요청
요청 파라미터(get, post, HTML Form)와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute 를 사용할 수 없다.
@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");
}
InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
그런데 사실 너무 귀찮다 개발자가 일일이 직접 HTTP message에 들어가서 지정한 캐릭터 셋으로 변환하는 과정이 말이 안 된다. 그래서 나온 방법이 바로 HttpEntity이다.
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity<>는 안에 타입을 받는데 String으로 해두면 Spring이 HttpBody에 있는 값을 문자형으로 바꿔서 넘겨준다.
그래서 HttpMessageConverter가 동작을 한다.
특징은 앞서 언급했듯 message body 정보를 직접조회, 따라서 @RequestPararm, @ModelAttribute와는 전혀 관계가 없다.
HttpEntity는 응답에도 사용가능하다. message body 정보를 직접 반환할 수 있고 헤더 정보도 포함하여 가능하다.
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
요청 파라미터를 조회한다: @RequestParam, @ModelAttribute
HTTP message body를 직접 조회한다: @RequestBody
/**
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 모든 메서드에 @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:
application/json)
*
*/
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
이렇게 json은 더 간단하게 개선할 수 있는데 대충 원리는 HttpEntity , @RequestBody 를 사용했을 때 HTTPMessageConverter가 동작하여
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
이 코드를 대신 실행해준다고 생각하면 된다.
또 @RequestBody 생략하면 안 되는데 @ModelAttribute가 디폴트로 적용되어 버리기 때문이다.
그래서 message body를 조회해야하는데 request param을 찾고있게 된다.
@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";
}
/**
* @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 helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return helloData;
}
HttpMessageConverter가 @ResponseBody가 붙어있다면 자동으로 적용된다.
전에 String으로 반환할 때도 HttpMessageBody에 들어갔었다. 마찬가지로 반환타입을 지정하면 해당 타입의 객체를 생성하여 이게 HttpMessageConverter를 타고 json으로 값이 바뀐다. 그리고 이 json 문자열이 http message 응답에 들어가서 응답한다.
이때 헤더에 Accept:appliction/json이 명시되어있어야한다.