[Spring] @ResponseBody 무쓸모 아님?(HttpMessageConverter, ResponseEntity)
이번에 웹 자동차경주 미션 1단계를 진행 하면서 프론트에서 보낸 요청에 대해 응답을 주기 위해,
@ResponseBody
어노테이션을 method에 붙여주었다.
@PostMapping("/plays")
@ResponseBody
public ResultDto play(@RequestBody GameInfoDto gameInfoDto) {
...
}
그런데 페어인 이리내가 @ResponseBody
어노테이션을 제거하면 어떻게 되냐고 물어보길래,
"글쎄... 해보쉴?" 하고 어노테이션을 제거하고 실행해봤다.
그런데 예상과 달리 어노테이션의 유무와 상관없이 똑같이 동작하더라.
@ResponseBody
는 사실 간지용이 아닐까..?
@ResponseBody
간지용인지 아닌지 확인하기 위해 뭐하는 놈인지 확인을 해봤다.
이 친구의 역할은 스프링의 ViewResolver
를 사용하지 않게끔 해주고.
대신에 HttpMessageConverter
를 사용하여,
자바객체를 HttpResponse의 본문 즉, Response Body의 내용으로 매핑해주는 역할을 한다.
(@RequestBody
역시 마찬가지로 VIewResolver
를 사용 하지 않고 HttpMessageConverter
를 사용하여 HttpRequest의 body 내용을 자바객체로 매핑한다.)
예시를 통해 확인을 해보면 아래와 같이 작성된 api에 요청이 들어오면,
public class UserInfo {
private final String id;
private final String name;
public UserInfo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
@GetMapping("/test")
@ResponseBody
public UserInfo test() {
return new UserInfo("test", "polo");
}
Reponse Body의 형태는 다음과 같다.
// start line
HTTP/1.1 200 OK
// header
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 12 Apr 2023 10:41:33 GMT
Keep-Alive: timeout=60
Connection: keep-alive
// body(JSON 형식)
{
"id" : "test",
"name" : "polo"
}
HttpMessageCoverter
최대한 이해해보려고 했으나 좀 많이 어려워서 여기저기서 검색해보고 이해한 내용만 간략하게 정리해보려 한다.
HttpMessageConverter
의 interface 이다.
canRead
, canWrite
, read
, write
네가지 메서드의 역할만 간신히 이해할 수 있었다.
- canRead, canWrite : 메시지 컨버타가 해당 클래스 혹은 미디어 타입을 지원하지는 체크한다.
- read, write : 메시지 컨버터를 통해 메시지를 읽거나 쓰는 기능을 수행한다.
읽고, 쓰는 두가지의 기능을 지원 하는 이유는 메시지컨버터는 요청과 응답 모두에 사용 되기 때문이다.
해당 인터페이스를 구현한 구현체는 정말 많지만 그 중 주로 사용된다는 3가지의 구현체는 다음과 같다.
- byte[] 의 처리를 위한
ByteMessageConvert
- String 문자열의 처리를 위한
StringHttpMessageConverter
- JSON을 처리하기 위한
MappingJackson2HttpMessageConverter
byte와 String 의 경우는 미디어타입을 가리지 않는다. (*/*)
하지만 Mapping~는 application/json
만 지원한다.
그렇다고 한다... 사실 잘 모르겠다. ^^
@RestController
다시 본론으로 돌아와서,
무튼간에 @ResponseBody
는 상당히 중요한 역할을 하고있다.
그런데 위에서 말했듯, @ResponseBody
어노테이션을 사용하지 않았음에도
정상적으로 동작했다.
다시 말하자면, @ResponseBody
어노테이션이 없음에도 불구하고,
ViewResolver를 사용하지 않고 HttpMessageConverter를 사용했다.
원인은 Controller에 붙였던 @RestController
였다.
@RestController
public class WebController {
...
@PostMapping("/plays")
public ResultDto play(@RequestBody GameInfoDto gameInfoDto) {
...
}
}
왜인지는 모르겠는데, @RestController
를 사용했다.
아마 대충 IntelliJ가 추천해주는거 쓴 듯 싶다;;
하핳
@RestController
의 코드를 까보면
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody // 요기잉네
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/ @AliasFor(annotation = Controller.class)
String value() default "";
}
@ResponseBody
를 포함하고 있는 것을 확인 할 수 있다.
그니까 @RestController
를 사용하면 ViewResolver 대신 HttpMessageConvert를 사용하는 컨트롤러가 된다.
컨트롤러에서 경우에 따라 ViewResolver와 HttpMessaveCoverter를 모두 사용하고 싶다면,@Controller
어노테이션과, @ResponseBody
어노테이션을 사용하면 된다.
ResponseEntity
@ResponseBody
에도 단점이 있다.
- ResponseHeader를 유연하게 설정하지 못한다.
- Status를 메서드 밖에서 어노테이션을 사용하여 따로 설정해주어야 한다.
@GetMapping("/test")
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public User findUser(@RequestBody UserInfo userInfo) {
...
}
좀 불편한 감이 없지않아 있다.
나처럼 불편하다고 생각한 사람이 많았는지,@ResponseBody
의 상위호환쯤 되보이는 ResponseEntity라른 애가 생겼다.
마찬가지로 ViewResolver
대신 HttpMessageConverter
를 사용하는 친구다.
이 친구는 좀전에 얘기한 @ResponseBody
의 단점들을 모두 해결해준다.
ResponseEntity를 사용하는 코드는 다음과 같다.
@GetMapping("/test")
public ResponseEntity<ResultDto> test(@RequestBody Test test) {
...
return new ResponseEntity<>(resultDto, HttpStatus.OK);
}
ResponseBody를 생성자에 responseBody에 담길 내용과, status코드를 담아주면 된다.
@ResponseStatus 없이 status code를 포함하여 응답을 처리할 수 있다.
만약, responseHead에 내용을 추가하거나 수정하고 싶다면 header를 생성해서 인자로 넘겨주면된다.
@GetMapping("/test")
public ResponseEntity<ResultDto> test(@RequestBody Test test) {
...
HttpHeaders headers = new HttpHeaders();
headers.add("Test-Header", "test");
return new ResponseEntity<>(resultDto, headers, HttpStatus.OK);
}
또한, 시나리오 마다 다른 상태코드를 반환하는 것 역시 가능하다.
@GetMapping("/test")
public ResponseEntity<ResultDto> test(@RequestBody Test test) {
...
if(분기처리) {
return new ResponseEntity<>(resultDto, HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(resultDto, HttpStatus.OK);
}
ResponseEntity 는 생성자를 제외하고도 builder 릍 통한 생성도 지원한다.
@GetMapping("/test")
public ResponseEntity<ResultDto> test(@RequestBody Test test) {
...
return ResponseEntity.ok().header(header).body(dto);
}
header의 수정을 원치 않는다면, body() 만 사용해도 된다.
@GetMapping("/test")
public ResponseEntity<ResultDto> test(@RequestBody Test test) {
...
return ResponseEntity.ok().body(dto);
}
두가지 방법은 그냥 선호도의 차이인 듯 하다.
내부에서도 뭔가 별 다른 동작이 존재하지 않는다.
간단하게 ResponseEntity의 ok() method를 살펴보면, DefaultBodyBuilder를 반환하고.
DefaultBodyBuilder의 header() 는 DefaultBodyBuilder의 필드에 있는 HttpHeaders에 내용변경해주고 this를 반환한다.
DefaultBodyBuilder의 body() method는 생성자를 통해 ResponseEntity를 반환한다.
public class ResponseEntity<T> extends HttpEntity<T> {
...
public static BodyBuilder ok() {
return status(HttpStatus.OK);
}
...
public static BodyBuilder status(HttpStatus status) {
Assert.notNull(status, "HttpStatus must not be null");
return new DefaultBuilder(status);
}
...
}
private static class DefaultBuilder implements BodyBuilder {
private final Object statusCode;
private final HttpHeaders headers = new HttpHeaders();
public DefaultBuilder(Object statusCode) {
this.statusCode = statusCode;
}
...
@Override
public BodyBuilder header(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
this.headers.add(headerName, headerValue);
}
return this;
}
@Override
public BodyBuilder headers(@Nullable HttpHeaders headers) {
if (headers != null) {
this.headers.putAll(headers);
} return this;
}
@Override
public BodyBuilder headers(Consumer<HttpHeaders> headersConsumer) {
headersConsumer.accept(this.headers);
return this;
}
...
@Override
public <T> ResponseEntity<T> body(@Nullable T body) {
return new ResponseEntity<>(body, this.headers, this.statusCode);
}
}
대충 봐도 종단에는 생성자를 사용하는 모습이나, 다른 특별한 동작이 진행되지 않는 것으로 보아
그냥 자기가 생각하기에 읽기 편한것이나, 혹은 팀 컨벤션에 맞게 취사선택 하여 사용하면 될 것 같다.