본문 바로가기

Development/Spring

[Spring] 서버 간 통신하기: RestTemplate

스프링에서 다른 서버로 웹 요청 보내기: RestTemplate과 WebClient

최근에 개발되는 서비스들은 마이크로서비스 아키텍처(MSA) 를 주로 채택하고 있습니다. MSA는 말 그대로 애플리케이션이 가지고 있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태입니다. 각 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버가 그러한 API를 호출해서 사용할 수 있게 구성되므로 각 서버가 다른 서버의 클라이언트가 되는 경우도 많습니다.

스프링에서는 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와주는 RestTemplateWebClient가 있습니다.

이 글에선 RestTemplate에 대해 살펴보겠습니다.

RestTemplate

RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿입니다.
HTTP 서버와의 통신을 단순화한 이 템플릿을 이용하면 RESTful 원칙을 따르는 서비스를 편리하게 만들 수 있습니다.

RestTemplate은 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶을 경우 AsyncRestTemplate을 사용하면 됩니다.다만 RestTemplate은 지원이 중단된(deprecated) 상태여서, WebCLient를 사용한 방식도 함께 알아둘 것을 권장합니다.

RestTemplate은 다음과 같은 특징을 가지고 있습니다.

  • HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공합니다.
  • RESTful 형식을 갖춘 템플릿입니다.
  • HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답을 받을 수 있습니다.
  • 블로킹(Blocking) I/O 기반의 동기 방식을 사용합니다.
  • 다른 API를 호출할 때 HTTP 헤더에 다양한 값을 설정할 수 있습니다.

RestTemplate의 동작 원리

애플리케이션에서는 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등을 설정합니다.

그리고 외부 API로 요청을 보내게 되면 RestTemplate 에서 HttpMessageConverter를 통해 RequestEntity 를 요청 메시지로 변환합니다.

RestTemplate에서는 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보냅니다.

외부에서 요청에 대한 응답을 받으면 RestTemplateResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리합니다.

받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 애플리케이션으로 반환합니다.

RestTemplate의 대표적인 메서드

1. GET 메서드:

  • getForObject(url,responseType): 지정된 URL로 GET 요청을 보내고 응답 결과를 지정된 responseType 객체로 변환하여 반환합니다.
  • getForEntity(url,responseType): 지정된 URL로 GET 요청을 보내고 응답 결과를 ResponseEntity 객체로 반환합니다. ResponseEntity 객체는 응답 코드, 헤더 정보, 응답 본문을 포함합니다.

2. POST 메서드:

  • postForObject(url, request, responseType): 지정된 URL로 POST 요청을 보내고 request 객체를 JSON 또는 XML 형식으로 변환하여 전송합니다. 응답 결과를 지정된 responseType 객체로 변환하여 반환합니다.
  • postForEntity(url, request, responseType): 지정된 URL로 POST 요청을 보내고 request 객체를 JSON 또는 XML 형식으로 변환하여 전송합니다. 응답 결과를 ResponseEntity 객체로 반환합니다.

3. PUT 메서드:

  • put(url, request): 지정된 URL로 PUT 요청을 보내고 request 객체를 JSON 또는 XML 형식으로 변환하여 전송합니다.
  • put(url, request, responseType): 지정된 URL로 PUT 요청을 보내고 request 객체를 JSON 또는 XML 형식으로 변환하여 전송합니다. 응답 결과를 지정된 responseType 객체로 변환하여 반환합니다.

4. DELETE 메서드:

  • delete(url): 지정된 URL로 DELETE 요청을 보냅니다.
  • delete(url, request): 지정된 URL로 DELETE 요청을 보내고 request 객체를 JSON 또는 XML 형식으로 변환하여 전송합니다.

5. Exchange 메서드:

  • exchange(url, method, requestEntity, responseType): 지정된 URL로 HTTP 요청을 보내고 응답 결과를 ResponseEntity 객체로 반환합니다. method는 GET, POST, PUT, DELETE 등의 HTTP 메서드를 지정합니다. requestEntity는 요청 헤더와 본문 정보를 담고 있으며, responseType은 응답 본문을 변환할 객체를 지정합니다.

RestTemplate 구현하기

일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현됩니다.
RestTemplatespring-boot-starter-web모듈에 포함돼 있는 기능이므로 별도로 의존성을 추가할 필요는 없습니다.

위 그림에서 클라이언트는 서브를 대상으로 요청을 보내고 응답을 받는 역할을 합니다.

아래의 예시는 서버 1 사이드에 해당하는 코드입니다.

GET 형식의 RestTemplate 작성하기

PathVariable이나 파라미터를 사용하지 않는 호출 방법

@Service
public class RestTemplateService {

    // 예제 12.3
    public String getName() {
        URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api")
            .encode()
            .build()
            .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

위 예제는 http://localhost:9090/api/v1/crud-api 라는 URI로 요청을 보내고 결괏값을 받는 코드입니다.

여기서 사용하는 UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로서 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행합니다.
위 코드에서 fromUriString() 메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력합니다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-8로 다음과 같은 코드가 실행됩니다.

참고로, fromUriString()를 사용할 때 유효하지 않은 URL 형식인지 유효성 검사 로직을 추가하는 것이 바람직합니다.
특히 사용자로부터 입력받은 URL의 경우 신뢰할 수 없으므로 보안 취약성 문제가 발생할 수 있습니다.

public final UriComponentsBuilder encode() {
    return encode(StandartCharsets.UTF_8);
}

이후 build() 메서드를 통해 빌더 생성을 종료하고 UriComponents타입이 리턴됩니다.
위 코드에서는 toUri() 메서드를 통해 URI 타입으로 리턴받았습니다. 만약 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체해서 사용하면 됩니다.

이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는 데 사용되며, getForEntity()에 파라미터로 전달됩니다.
getForEntity()는 URI와 응답받는 타입을 매개변수로 사용합니다.

PathVariable을 사용하는 호출 방법

public String getNameWithPathVariable() {
        URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api/{name}")
            .encode()
            .build()
            .expand("Flature") // 복수의 값을 넣어야할 경우 , 를 추가하여 구분
            .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

위 예제는 http://localhost:9090/api/v1/crud-api/Flature 라는 URI로 Flature라는 PathVariable를 포함하여 요청을 보내고 결괏 값을 받는 코드입니다.

여기서 눈여겨볼 코드는 path()expand() 메서드 입니다.
path() 메서드 내에 입력한 세부 URI 중 중괄호 '{}' 부분을 사용해 변수명을 입력하고, expand() 메서드에서는 순서대로 변수 값을 입력하면 됩니다. 값을 여러 개 넣어야 하는 경우에는 콤마(,)로 구분해서 나열합니다.

파라미터를 사용한 호출 방법

public String getNameWithParameter() {
        URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api/param")
            .queryParam("name", "Flature")
            .encode()
            .build()
            .toUri();

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        return responseEntity.getBody();
    }

위 예제는 http://localhost:9090/api/v1/crud-api/param 라는 URI로

{"name": "Flature"} 라는 데이터 Body를 포함하여 요청을 보내고 결괏 값을 받는 코드입니다.

queryParam() 메서드를 사용해 (키, 값) 형식으로 파라미터를 추가할 수 있습니다.

POST 형식의 RestTemplate 작성

파라미터와 Body를 포함한 요청

public ResponseEntity<MemberDto> postWithParamAndBody() {
        URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api")
            .queryParam("name", "Flature")
            .queryParam("email", "flature@wikibooks.co.kr")
            .queryParam("organization", "Wikibooks")
            .encode()
            .build()
            .toUri();

        MemberDto memberDto = new MemberDto();
        memberDto.setName("flature!!");
        memberDto.setEmail("flature@gmail.com");
        memberDto.setOrganization("Around Hub Studio");

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto,
            MemberDto.class);

        return responseEntity;
    }

위 코드는 POST형식으로 외부 API에 요청할 때 Body 값과 파라미터 값을 담는 방법 두 가지를 모두 보여줍니다.

queryParam() 메서드를 통해 파라미터에 값을 추가하는 작업이 수행되며,
responseEntity 객체에 값을 담기 위해서 postForEntity() 메서드를 사용하였습니다.
이 메서드엔 값을 보낼 데이터 객체(예제에서 MemberDto)를 넣어야 합니다.

postForEntity() 메서드로 API를 호출하면 서버의 콘솔 로그에는 RequestBody 값이 출력되고 파라미터 값은 결과값으로 리턴됩니다.

Header를 포함한 요청

public ResponseEntity<MemberDto> postWithHeader() {
        URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:9090")
            .path("/api/v1/crud-api/add-header")
            .encode()
            .build()
            .toUri();

        MemberDto memberDTO = new MemberDto();
        memberDTO.setName("flature");
        memberDTO.setEmail("flature@wikibooks.co.kr");
        memberDTO.setOrganization("Around Hub Studio");

        RequestEntity<MemberDto> requestEntity = RequestEntity
            .post(uri)
            .header("my-header", "Wikibooks API")
            .body(memberDTO);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity,
            MemberDto.class);

        return responseEntity;
    }

대부분의 외부 API는 토큰키를 받아 서비스 접근을 인증하는 방식으로 작동합니다. 이때 토큰값을 헤더에 담아 전달하는 방식이 가장 많이 사용됩니다. 헤더를 설정하기 위해서는 RequestEntity를 정의해서 사용하는 방법이 가장 편한 방법입니다.
post() 메서드로 URI를 설정한 후 header() 메서드에서 헤더의 키 이름과 값을 설정하는 코드입니다.

exchange() 메서드는 모든 형식의 HTTP 요청을 생성할 수 있습니다.
post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 쉽게 사용할 수 있기 때문에 대부분 이 메서드를 사용합니다.

RestTemplate 커스텀 설정

RestTemplateHTTPClient를 추상화하고 있습니다. HttpClient의 종류에 따라 기능에 차이가 다소 있는데, 가장 큰 차이는 커넥션 풀(Connection Pool) 입니다.

RestTemplate은 기본적으로 커넥션 풀을 지원하지 않기 때문에, 매번 호출할 때 마다 포트를 열어 커넥션을 생성하게 됩니다.
이때 TIME_WAIT 상태가 된 소켓을 다시 사용하려고 접근한다면 재사용하지 못하게 됩니다.
이를 방지하기 위해 커넥션 풀 기능을 활성화해서 재사용할 수 있게 하는 것이 좋습니다.
이 기능을 활성화 하는 가장 대표적인 방법은 아파치에서 제공하는 HttpClient로 대체해서 사용하는 방식입니다.

먼저 아파치의 HttpClient를 사용하려면 다음과 같이 의존성을 추가해야 합니다.

Maven

<dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>

Gradle

dependencies {
    // ... other dependencies

    compile "org.apache.httpcomponents:httpclient"
}

커스텀 RestTemplate 객체 생성 메서드

 public RestTemplate restTemplate() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();

        HttpClient client = HttpClientBuilder.create()
            .setMaxConnTotal(500)
            .setMaxConnPerRoute(500)
            .build();

        CloseableHttpClient httpClient = HttpClients.custom()
            .setMaxConnTotal(500)
            .setMaxConnPerRoute(500)
            .build();

        factory.setHttpClient(httpClient);
        factory.setConnectTimeout(2000);
        factory.setReadTimeout(5000);

        RestTemplate restTemplate = new RestTemplate(factory);

        return restTemplate;
    }

RestTemplate의 생성자를 보면 다음과 같이 ClientHttpRequestFactory를 매개변수로 받는 생성자가 존재합니다.

public RestTemplate(ClientHttpRequestFactory requestFactory) {
    this();
    setRequestFactory(requestFactory);
}

ClientHttpRequestFactory는 함수형 인터페이스(functional interface)로, 대표적인 구현체로서 SimpleClientHttpRequestFactoryHttpComponentsClientHttpRequestFactory가 있습니다.

별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleCLientHttpRequestFactory를 사용하게 됩니다.

별도의 HttpComponentsClientHttpRequestFactory객체를 생성해서 ClientHttpRequestFactory를 사용하면 RestTemplate의 Timeout 설정을 할 수 있습니다.

그리고 HttpComponentsClientHttpRequestFactory의 커넥션 풀을 설정하기 위해 HttpClientHttpComponentsClientHttpRequestFactory에 설정할 수 있습니다.

HttpClient를 생성하는 방법은 두 가지가 있는데 HttpClientBuilder.create() 메서드를 사용하거나 HttpClient.custom()를 사용하는 것 입니다.

생성한 HttpClient는 factory의 setHttpClient() 메서드를 통해 인자로 전달해서 설정할 수 있습니다. 이렇게 설정된 factory 객체를 RestTemplate을 초기화하는 과정에서 인자로 전달하면 됩니다.

HttpClient vs CloseableHttpClient

HttpClient와 CloseableHttpClient는 둘 다 Apache HttpClient 라이브러리에서 제공하는 클래스이며 HTTP 요청을 수행하는 데 사용됩니다. 하지만 몇 가지 중요한 차이점이 있습니다.

HttpClient

  • 인터페이스(Interface): HttpClient는 HTTP 요청을 수행하는 클래스를 위한 인터페이스입니다. 구체적인 구현 방법에 대한 제약이 없으며 인증, 리다이렉트 등과 같은 세부 사항은 개별 클라이언트 구현에 따라 다릅니다.
  • 리소스 관리(Resource Management): HttpClient 자체는 리소스 관리를 제공하지 않습니다. 따라서 사용자가 직접 클라이언트 연결을 닫아야 합니다.

CloseableHttpClient

  • 추상 클래스(Abstract Class): CloseableHttpClient는 HttpClient 인터페이스를 구현하는 추상 클래스입니다. 기본적인 HTTP 요청 실행 기능을 제공하며 close() 메서드를 통해 연결을 닫을 수 있습니다. 또한 AutoCloseable 인터페이스를 구현하여 try-with-resources 문에서 자동으로 연결을 닫을 수 있습니다.
  • 리소스 관리(Resource Management): CloseableHttpClient는 연결을 닫는 기능을 제공하여 리소스 누수(leak)를 방지합니다.

일반적으로 CloseableHttpClient를 사용하는 것이 좋습니다. 연결 닫기를 자동으로 처리하므로 코드가 더 간결하고 안전합니다.
특별한 이유가 없으면 HttpClient 인터페이스를 직접 구현하는 것은 권장하지 않습니다.

참고