본문 바로가기
Spring

RestTemplate _GET에 body를 넣어서 보내고 싶지만

by 개발하는 호빗 2022. 7. 7.

조회 API 설계 중 HttpMethod GET에 body를 넣어서 개발을 하고 싶었다.

 

WebClient를 사용하고싶지만

기존에 개발되어 있는 관련 API 코드들이 RestTemplate을 사용하고 있어 일단은 동일하게 RestTemplate으로 개발을 하기로 했다.

그러다보니 RestTemplate이 GET에 body 넣을 수 있도록 지원을 하고 있는지 궁금했다.

전에 봤는데 ... 기억이 가물가물..

 

아니나 다를까 exception 발생했다.

org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 : [{
  "code" : -1,
  "message" : "Required request body is missing: public org.springframework.http.ResponseEntity<java.util.List<xxx.xxx.xxxx.xxx.dto.XXXXDto>> xxx. XXXController.getXXX(xxx.xxx.xxxx.xxx.dto.XXXXDto) throws java.lang.Exception"
}]

body를 받게 만들 방법이 없을지 궁금해서 디버깅이랑 검색을 한 후 정리해봤다.

 


URLConnection과 HttpURLConnection

Java에서 URL을 통해 서버와 통신할 때 사용되는 주요 클래스로는 URLConnection과 HttpURLConnection이 있다.

이 두 클래스는 모두 추상 클래스로서 URL 통신과 관련된 기능들을 제공해준다.

 

URLConnection의 주석을 읽어보면

URL Connection은 애플리케이션과 URL 간의 통신 연결을 나타내는 모든 클래스의 수퍼클래스이다.
URL Connection의 인스턴스를 사용하여 URL에서 참조하는 resource를 읽고 쓸 수 있다.

라고 나와 있다.

 

일반적으로 클라이언트는 서버와 URL 통신을 할 때 아래와 같은 과정을 거친다.

1. URL 객체 만들기
2. URL에서 URLConnection 객체 얻기
3. URL 연결 구성
4. Header 읽기
5. 입력 스트림 가져오기 및 데이터 읽기
6. 출력 스트림 가져오기 및 데이터 쓰기
7. 연결 닫기

※ 3~6 단계는 선택사항이다.
※ 5~6단계는 클라이언트와 서버가 서로 입장을 바꿔서 설정할 수 있다.

 

URLConnection 클래스는 위의 URL 통신 과정에 필요한 설정들을 다루는 API를 제공하고 있다.

HttpURLConnection는 URLConnection 클래스를 확장한 클래스로, HTTP 고유의 기능에 대한 추가 기능을 제공하고 있다.

 

▸URLConnection과 HttpURLConnection가 제공하는 API에 대한 글

 

URLConnection의 doOutput

GET 요청에 body를 쓸 수 있는지 없는지는

URLConnection의 필드 중 하나인 doOutput이 결정한다.

/**
  * This variable is set by the {@code setDoOutput} method. Its
  * value is returned by the {@code getDoOutput} method.
  * <p>
  * A URL connection can be used for input and/or output. Setting the
  * {@code doOutput} flag to {@code true} indicates
  * that the application intends to write data to the URL connection.
  * <p>
  * The default value of this field is {@code false}.
  *
  * @see     java.net.URLConnection#getDoOutput()
  * @see     java.net.URLConnection#setDoOutput(boolean)
  */
 protected boolean doOutput = false;

URL 연결은 입력 또는 출력에 사용할 수 있는데,

doOutput이 true냐 false냐에 따라서 URL 연결을 출력용으로 사용할지 입력용으로 사용할지 여부가 결정된다.

다르게 표현하면 데이터를 OutputStream으로 넘겨줄지 말지를 결정한다고도 표현할 수 있다.

 

그렇다면, RestTemplate에선 언제 doOutput을 설정할까?

 


RestTemplate 와 요청 핸들러 Factory 주입

RestTemplate의 생성자 중엔 ClientHttpRequestFactory를 주입받는 생성자가 하나 있다.

/**
 * Create a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}.
 * @param requestFactory the HTTP request factory to use
 * @see org.springframework.http.client.SimpleClientHttpRequestFactory
 * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory
 */
public RestTemplate(ClientHttpRequestFactory requestFactory) {
   this();
   setRequestFactory(requestFactory);
}

RestTemplate의 상속 관계를 따라가보면  HttpAccessor 추상 클래스가 나온다.

HttpAccessor은 HTTP Access gateway helper 역할을 하는 기본 클래스이다.

HttpAccesor은 setRequestFactory() 메서드를 통해서, 클라이언트의 요청 핸들러를 구성해 줄 Factory를 지정한다.

 

위의 생성자는 그 Factory 클래스를 지정해주는 생성자이다.

 

Factory를 통한 요청 생성

URLConnection의 doOutput을 어디에서 설정하는지 보기 위해 디버깅을 마저 해보면, 

public class BufferingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {

   /**
    * Create a buffering wrapper for the given {@link ClientHttpRequestFactory}.
    * @param requestFactory the target request factory to wrap
    */
   public BufferingClientHttpRequestFactory(ClientHttpRequestFactory requestFactory) {
      super(requestFactory);
   }


   @Override
   protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory)
         throws IOException {

      ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod);
      if (shouldBuffer(uri, httpMethod)) {
         return new BufferingClientHttpRequestWrapper(request);
      }
      else {
         return request;
      }
   }

 

BufferingClientHttpRequestFactory에서 

uri, HttpMethod 그리고 ClientHttpRequestFactory를 받아서 요청을 생성하는 것을 볼 수 있다.

 

이 때, createRequest는

ClientHttpRequestFactory의 대표적인 구현체 중 하나인 SimpleClientHttpRequestFactory의 오버라이딩한 메서드가 호출되는 것을 알 수 있는데

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
   HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
   prepareConnection(connection, httpMethod.name());

   if (this.bufferRequestBody) {
      return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
   }
   else {
      return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
   }
}

여기에서 

openConnection() : Connection을 열고

prepareConnection() : Connection을 구성하고

new SimpleBufferingClientHttpRequest() : 버퍼링된 요청을 실행할 ClientHttpRequest 구현체의 인스턴스를 생성

하는 것을 볼 수 있다.

 

그리고 바로 SimpleClientHttpRequestFactory의 prepareConnection() 메서드에서

/**
 * Template method for preparing the given {@link HttpURLConnection}.
 * <p>The default implementation prepares the connection for input and output, and sets the HTTP method.
 * @param connection the connection to prepare
 * @param httpMethod the HTTP request method ({@code GET}, {@code POST}, etc.)
 * @throws IOException in case of I/O errors
 */
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
   if (this.connectTimeout >= 0) {
      connection.setConnectTimeout(this.connectTimeout);
   }
   if (this.readTimeout >= 0) {
      connection.setReadTimeout(this.readTimeout);
   }

   connection.setDoInput(true);

   if ("GET".equals(httpMethod)) {
      connection.setInstanceFollowRedirects(true);
   }
   else {
      connection.setInstanceFollowRedirects(false);
   }

   if ("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
         "PATCH".equals(httpMethod) || "DELETE".equals(httpMethod)) {
      connection.setDoOutput(true);
   }
   else {
      connection.setDoOutput(false);
   }

   connection.setRequestMethod(httpMethod);
}

HttpMethod가 GET일 경우

connection.setDoOutput(false); 로 URL 연결을 입력용으로 사용하겠다고 설정하는 것을 볼 수 있다.

 

위 doOutput 설정에 따라서,

SimpleBufferingClientHttpRequest의 헤더와 내용을 구성해서 요청을 수행하는 메서드 executeInternal() 안에서

Output과 관련된 어떤 작업도 거치지 않은 채

응답 코드를 받고

클라이언트 측의 HTTP Response 인스턴스를 생성하는 것을 확인해 볼 수 있다.

 

 

@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
   addHeaders(this.connection, headers);
   // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
   if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {
      this.connection.setDoOutput(false);
   }
   if (this.connection.getDoOutput() && this.outputStreaming) {
      this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
   }
   this.connection.connect();
   if (this.connection.getDoOutput()) {
      FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
   }
   else {
      // Immediately trigger the request in a no-output scenario as well
      this.connection.getResponseCode();
   }
   return new SimpleClientHttpResponse(this.connection);
}

 


결론

RestTemplate은 GET 방식의 요청에선 Output을 제공하지 않는다.

 


참고

[1] https://brunch.co.kr/@kd4/158

[2] https://blueyikim.tistory.com/2199

[3] https://stackoverflow.com/questions/37987040/how-to-send-http-options-request-with-body-using-spring-rest-template

'Spring' 카테고리의 다른 글

Spring의 Transaction  (0) 2024.01.12