Spring RestTemplate 까보기

kindof

·

2024. 1. 11. 22:19

RestTemplate은 Spring에서 동기적인 HTTP 요청을 수행하기 위해 정말 많이 사용되고 있는 클래스입니다.
 
Spring 5.0부터 소개된 WebClient는 HTTP 요청에 대한 동기, 비동기적인 처리 방식을 모두 제공하는 조금 더 최신의 클라이언트지만, 과거 버전의 스프링 프로젝트 + 전통적인 MVC 패턴 안에서 개발을 시작한 프로젝트라면 RestTemplate이 당연히 더 익숙할 수밖에 없습니다.
 
그래서 이번 글에서는 RestTemplate에 대한 공식 문서와 여러 레퍼런스를 참고해서 RestTemplate을 어떻게 이해하고 사용해야 하는지에 대한 나름의 정리를 해보고자 합니다.
 
 

1. RestTemplate은 Deprecated 되나?

구글에 RestTemplate에 대해 검색해보면 아래와 같이 연관 검색어로 RestTemplate deprecated 라는 내용이 보입니다.
 
많은 블로그에서도 WebClient를 사용해야 하는 근거 중의 하나로 RestTemplate의 Deprecation을 이야기하는데요. 
 

RestTemplate deprecated?

 
하지만 결론부터 말하자면, RestTemplate은 deprecated 되지 않았으며 여전히 잘 사용할 수 있는 클라이언트입니다.
 
아래 영상에서 이에 대한 자세한 이야기를 하고 있습니다.

 

https://www.youtube.com/watch?v=S4W3cJOuLrU

 
요약하자면 Spring 개발 팀에서 RestTemplate 클래스에 남긴 한 커밋에서 RestTemplate은 deprecated 될 예정이며 WebClient를 사용하라는 문구를 실제로 남긴 적이 있지만, 이후에 deprecated에 대한 내용을 삭제하고 maintenance 모드로 관리될 것이라고 명시적으로 말했다는 내용입니다.
 
아래는 Spring 공식 프로젝트의 #24503 이슈입니다.

https://github.com/spring-projects/spring-framework/issues/24503

 
 
따라서, RestTemplate은 이제 버릴 대상이 아니라 여전히 우리가 잘 알고 사용해야 하는 대상이겠습니다.
 
 

2. RestTemplate 코드

RestTemplate에 대한 spring 공식 문서를 보면 아래와 같은 계층 구조, 그리고 Implemented Interfaces에 대한 내용이 있습니다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html

 
그리고 RestOperations 인터페이스를 확인해보면 GET, HEAD, POST, PUT 등에 대한 메서드가 정의되어 있습니다.

public interface RestOperations {

   // GET
   // .. 생략

   <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;


   // HEAD
   // .. 생략

   HttpHeaders headForHeaders(String url, Object... uriVariables) throws RestClientException;


   // POST
   // .. 생략
   URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;

   // .. 생략
   
}

 
이제 RestTemplate의 구현 코드를 살펴보겠습니다.
 
먼저, RestTemplate의 필드 선언과 생성자 부분입니다.

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {

   private static final boolean romePresent =
         ClassUtils.isPresent("com.rometools.rome.feed.WireFeed",
               RestTemplate.class.getClassLoader());

   private static final boolean jaxb2Present =
         ClassUtils.isPresent("javax.xml.bind.Binder",
               RestTemplate.class.getClassLoader());

   private static final boolean jackson2Present =
         ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
               RestTemplate.class.getClassLoader()) &&
         ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
               RestTemplate.class.getClassLoader());

   private static final boolean jackson2XmlPresent =
         ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper",
               RestTemplate.class.getClassLoader());

   private static final boolean gsonPresent =
         ClassUtils.isPresent("com.google.gson.Gson",
               RestTemplate.class.getClassLoader());


   private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();

   private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();

   private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();

   private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();


   /**
    * Create a new instance of the {@link RestTemplate} using default settings.
    * Default {@link HttpMessageConverter}s are initialized.
    */
   public RestTemplate() {
      this.messageConverters.add(new ByteArrayHttpMessageConverter());
      this.messageConverters.add(new StringHttpMessageConverter());
      this.messageConverters.add(new ResourceHttpMessageConverter());
      this.messageConverters.add(new SourceHttpMessageConverter<Source>());
      this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

      if (romePresent) {
         this.messageConverters.add(new AtomFeedHttpMessageConverter());
         this.messageConverters.add(new RssChannelHttpMessageConverter());
      }

      if (jackson2XmlPresent) {
         this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
      }
      else if (jaxb2Present) {
         this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
      }

      if (jackson2Present) {
         this.messageConverters.add(new MappingJackson2HttpMessageConverter());
      }
      else if (gsonPresent) {
         this.messageConverters.add(new GsonHttpMessageConverter());
      }
   }

 
RestTemplate은 ByteArrayHttpMessageConverter, StringHttpMessageConverter 등 여러 메시지 컨버터를 messageConverters 리스트에 추가해두고, 사용하고 있는 의존성에 따라 Jackson, Gson 등의 메시지 컨버터들을 추가해둡니다.
 
예를 들어, 제가 연습으로 사용하고 있는 프로젝트에서 "com.rometools.rome.feed.WireFeed" 클래스를 주입하는 의존성은 없는 상태인데요.

 
이 상태에서 테스트 코드를 작성해서 RestTemplate이 초기화될 때 MessageConverters 안의 컨버터들을 확인해보겠습니다.

@Test
public void restTemplate_init_messageConverters() {
    List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();

    for (HttpMessageConverter<?> messageConverter : messageConverters) {
        System.out.println("messageConverter.getClass() = " + messageConverter.getClass());

    }
}

 

 

예상했던 것처럼 "com.rometools.rome.feed.WireFeed" 클래스에 대한 의존성이 존재하지 않기 때문에, 메시지 컨버터 내역에도 해당 컨버터가 들어가있지 않습니다.

테스트 결과

 
반면, MappingJackson2HttpMessageConverter는 아래와 같이 해당 클래스가 프로젝트 내에 존재하기 때문에 메시지 컨버터로 포함된 것을 알 수 있습니다.

jackson2Present

 
여기까지는 RestTemplate의 MessageConverter 초기화에 대한 간단한 내용이었습니다. 이제 GET, POST 등의 요청에 대해 살펴보겠습니다.
 


1. GET

RestTemplate의 GET 요청 메서드 시그니쳐를 보면 getForObject, getForEntity를 오버로딩한 형태입니다.

<T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
<T> T getForObject(URI url, Class<T> responseType)

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)

 
getForObject는 요청을 보내고 서버 응답을 지정한 클래스 형식으로 변환하여 반환하고, getForEntity는 응답 데이터를 ResponseEntity로 반환한다는 차이가 있습니다. 그리고 ResponseEntity는 HTTP 응답 상태, 헤더 및 본문 데이터를 포함하게 됩니다.
 
 

2. POST

POST 요청은 크게 postForLocation, postForObject, postForEntity 세 가지 메서드가 존재합니다.

URI postForLocation(String url, Object request, Object... uriVariables)
URI postForLocation(String url, Object request, Map<String, ?> uriVariables)
URI postForLocation(URI url, Object request)

<T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)
<T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
<T> T postForObject(URI url, Object request, Class<T> responseType)

<T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
<T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
<T> ResponseEntity<T> postForEntity(URI url, Object request, Class<T> responseType)

 
postForLocation은 요청을 보낸 뒤 응답에서 생성된 리소스의 위치(URI)를 반환하고, postForObject와 postForEntity는 GET 요청과 마찬가지로 각각 응답의 결과를 지정한 클래스 혹은 ResponseEntity로 반환합니다.
 
 

3. DELETE

void delete(String url, Object... uriVariables)
void delete(String url, Map<String, ?> uriVariables) 
void delete(URI url)

DELETE 요청은 URL에 대한 DELETE 요청을 보내고, 응답을 리턴하지는 않습니다.
 
...
 
나머지 HEAD, OPTIONS, PUT 등의 메서드는 위와 비슷하기 때문에 생략합니다.
 
 


 

4. exchange()

위에서 소개한 RestTemplate의 REST API 메서드 요청을 보면 실제로 거의 대부분은 execute() 메서드를 호출하면서 끝이 납니다. 

// GET
@Override
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
   RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
   HttpMessageConverterExtractor<T> responseExtractor =
         new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
   return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
}

// POST
@Override
public <T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
        throws RestClientException {

    RequestCallback requestCallback = httpEntityCallback(request, responseType);
    ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
    return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}

// PUT
@Override
public void put(String url, Object request, Object... uriVariables) throws RestClientException {
    RequestCallback requestCallback = httpEntityCallback(request);
    execute(url, HttpMethod.PUT, requestCallback, null, uriVariables);
}

// .. 생략

하지만 실제 RestTemplate 소스 코드를 보면 execute() 메서드 이전에 exchange() 메서드가 존재하는데요.
 
exchange() 메서드는 매개변수로 대상 URL, HTTP 메서드, 요청에 대한 추가 정보인 HttpEntity, ResponseType, uriVariables를 받아 헤더나 여러 요청 파라미터를 추가할 수 있고, 제네릭을 메서드로 선언함으로써 GET, POST 등의 요청을 구분하지 않고 HttpMethod 파라미터에 넘겨 정교하고 유연한 HTTP 요청을 보낼 수 있게 합니다.

<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException

 

/**
 * exchange() 사용예시
 * https://www.baeldung.com/spring-resttemplate-exchange-postforentity-execute
**/

ResponseEntity<Book> response = restTemplate.exchange(
  "https://api.bookstore.com",
  HttpMethod.POST,
  new HttpEntity<>(book, headers),
  Book.class);

 
 

5. execute()

마지막으로 execute() 메서드입니다. execute() 메서드 코드를 보면 아래와 같습니다.

// general execution

@Override
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
      ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

   URI expanded = getUriTemplateHandler().expand(url, uriVariables);
   return doExecute(expanded, method, requestCallback, responseExtractor);
}

 
주석으로도 // general execution 이라는 설명이 있는데요.
 
execute() 메서드는 RequestCallback, ResponseExtractor 매개변수를 받아 HTTP 요청에 대한 요청과 응답에 대한 제어를 할 수 있게 합니다.
 
예를 들어, 위에서 살펴봤던 postForEntity 메서드를 다시 한 번 보겠습니다.

@Override
public <T> ResponseEntity<T> postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
      throws RestClientException {

   RequestCallback requestCallback = httpEntityCallback(request, responseType);
   ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
   return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}

 
이 코드를 다시 보면 RequestCallback, ResponseExtractor를 정의해서 execute() 메서드에 넘겨주고 있음을 확인할 수 있습니다.
 
물론 여기서는 가장 기본적인 RequestCallback, ResponseExtractor을 정의해서 넘긴 것인데요.
 
우리는 이 부분을 Callback 함수로 생성해서 HTTP 요청을 보낼 수 있다는 것입니다.

/**
 * execute() 사용예시
 * https://www.baeldung.com/spring-resttemplate-exchange-postforentity-execute
**/

ResponseEntity<Book> response = restTemplate.execute(
    "https://api.bookstore.com",
    HttpMethod.POST,
    new RequestCallback() {
        @Override
        public void doWithRequest(ClientHttpRequest request) throws IOException {
            // manipulate request headers and body
        }
    },
    new ResponseExtractor<ResponseEntity<Book>>() {
        @Override
        public ResponseEntity<Book> extractData(ClientHttpResponse response) throws IOException {
            // manipulate response and return ResponseEntity
        }
    }
);

 
따라서, RestTemplate의 모든 HTTP 요청 중에 가장 유연하게 사용할 수 있는 메서드라는 결론을 내릴 수 있습니다.
 
 

3. RestTemplate이 던지는 예외

RestTemplate이 HTTP 요청에서 던지는 모든 예외는 RestClientException 이며 아래와 같은 계층 구조를 가집니다.

RestClientException Diagram &amp;amp;amp;amp;amp; Implementations

 
다이어그램에서 알 수 있듯이 RestClientException은 RuntimeException의 하위 클래스이며 이를 구현한 HttpClientErrorException, HttpServerErrorException, ResourceAccessException 등이 존재합니다.
 
HttpClientErrorException은 HTTP 요청에서 클라이언트의 에러(4xx)가 발생한 경우 던져지게 되며, HttpServerErrorException은 서버의 에러(5xx)가 발생한 경우, ResourceAccessException은 리소스에 접근할 수 없는 경우(네트워크 연결 실패, 타임아웃) 등에서 발생합니다.
 
RestClientException의 하위 구현체들을 이렇게 나누어 놓은만큼 저희는 RestClientException을 통해 예외의 경중을 따지고 다른 조치를 취해야 하는데요.
 
가령, 4xx 에러가 발생하는 예외일 경우 일반적으로는 호출하는 쪽(나)의 요청 방식에 대한 실수를 검토해봐야 할 것이고, 5xx 에러가 발생하는 경우 호출당하는 쪽(상대)에서 혹시 예외 케이스에 대해 오류를 내는 것은 아닌지에 대해 같이 검토해봐야 하지 않을까 생각할 수 있습니다. 또한 Timeout이나 Connection 관련 문제라면 네트워크 상태나 인프라적인 문제에 대해서 살펴봐야 할 수도 있고, 간헐적으로 발생한다면 이 예외를 어떤 식으로 관리할 것인지(retry, timeout 시간 조정 등)에 대해서도 다시 고민해볼 수 있습니다.
 
물론 이러한 내용을 더 단순화시켜서 관리할수도, 더 구체화시켜 관리할 수도 있습니다. 팀이나 서비스의 관점에서 정책적으로 정할 수 있는 부분일 것 같습니다.
 
 

4. 정리

이번 글에서는 RestTemplate의 개요부터 시작해서 RestTemplate의 각 메서드가 어떤 역할을 하는지, 어떻게 추상화되어있고 다른 메서드들과의 차이점은 무엇인지 등 코드를 보며 RestTemplate의 동작에 대해 살펴봤습니다.
 
그리고 RestTemplate이 던지는 RestClientException에 대해서도 간략히 살펴봄으로써 전반적으로 RestTemplate을 어떤 상황에서 어떻게 사용하는지, 그 결과를 어떻게 처리해야 하는지까지 고민해본 것 같습니다.
 
글에서 설명한 한 대목을 가지고서도 조금 더 깊고 다양한 주제로 확장할 수 있을 것 같은데요. 이번 정리를 통해 RestTemplate의 전반적인 동작과 흐름에 대해 이해했다면 나중에 문제가 생겼을 때 혹은 문제를 개선해야 할 때 도움이 될 것 같습니다.
 
 

5. Reference

- https://docs.spring.io/spring-framework/docs/6.0.0/reference/html/integration.html#rest-client-access
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html
- https://www.baeldung.com/spring-resttemplate-exchange-postforentity-execute