Study/Spring

[Spring] @ResponseBody 무쓸모 아님?(HttpMessageConverter, ResponseEntity)

dev_kong 2023. 4. 14. 13:55
728x90
728x90

이번에 웹 자동차경주 미션 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가지의 구현체는 다음과 같다.

 

  1. byte[] 의 처리를 위한 ByteMessageConvert
  2. String 문자열의 처리를 위한 StringHttpMessageConverter
  3. 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 에도 단점이 있다.

 

  1. ResponseHeader를 유연하게 설정하지 못한다.
  2. 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);  
    }
}

 

대충 봐도 종단에는 생성자를 사용하는 모습이나, 다른 특별한 동작이 진행되지 않는 것으로 보아

그냥 자기가 생각하기에 읽기 편한것이나, 혹은 팀 컨벤션에 맞게 취사선택 하여 사용하면 될 것 같다.

728x90
728x90