12.1 서버 간 통신 - RestTemplate

김찬미·2024년 6월 27일
0

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

이번 장에서는 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와주는 RestTemplateWebClient에 대해 살펴보도록 하겠다.


RestTemplate 이란?

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

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

  • HTTP 프로토콜의 메서드에 맞는 여러 메서드를 제공
  • RESTful 형식을 갖춘 템플릿
  • HTTP 요청 후 JSON, XML, 문자열 등의 다양한 형식으로 응답
  • 블로킹(blocking) I/O 기반의 동기 방식 사용
  • 다양한 HTTP 헤더 값을 설정 가능

🚨 주의
RestTemplate은 현업에서는 많이 쓰이나 지원 중단(deprecated) 상태이므로, 향후 WebClient 방식도 함께 알아둘 것을 권장한다.

RestTemplate의 동작 원리

RestTemplate의 동작을 도식화하면 아래와 같다.

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

  2. 외부 API로 요청을 보낸다.

  3. RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환한다.

  4. 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.

  5. 외부에서 요청에 대한 응답을 받는다.

  6. RestTemplateResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리한다.

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

RestTemplate의 대표적인 메서드

RestTemplate에서는 외부 API로 요청을 보낼 수 있도록 다양한 메서드를 제공한다.

메서드HTTP 형태설명
getForObjectGETGET 형식으로 요청한 결과를 객체로 반환
getForEntityGETGET 형식으로 요청한 결과를 ResponseEntity 결과로 반환
postForLocationPOSTPOST 형식으로 요청한 결과를 헤더에 저장된 URI로 반환
postForObjectPOSTPOST 형식으로 요청한 결과를 객체로 반환
postForEntityPOSTPOST 형식으로 요청한 결과를 ResponseEntity 형식으로 반환
deleteDELETEDELETE 형식으로 요청
putPUTPUT 형식으로 요청
patchForObjectPATCHPATCH 형식으로 요청한 결과를 객체로 반환
optionsForAllowOPTIONHTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용 가능
exchangeanyHTTP 헤더를 임의로 추가할 수 있고, 어떤 메서드 형식에서도 사용 가능
executeany요청과 응답에 대한 콜백을 수정

RestTemplate 사용하기

이제 RestTemplate을 사용해보겠다. 요청을 보낼 서버 용도로 별도의 프로젝트를 하나 생성하고 다른 프로젝트에서 RestTemplate을 통해 요청을 보내는 방식으로 실습을 진행할 예정이다.

서버 프로젝트 생성하기

먼저 RestTemplate의 동작을 확인하기 위해 서버 용도의 프로젝트를 생성하겠다. 실습 환경에서는 한 컴퓨터 안에서 두 개의 프로젝트를 가동시켜야 하기 때문에 톰캣의 포트를 변경해야 한다.

프로젝트에는 spring-boot-starter-web 모듈만 의존성으로 추가하며, 여기서는 serverBox라는 이름으로 프로젝트를 생성했다. 이 프로젝트의 구조는 아래와 같다.

server.port=9090

컨트롤러 생성

컨트롤러에서는 GETPOST 메서드 형식의 요청을 받기 위한 코드를 구성하겠다. 컨트롤러 클래스는 아래와 같다.

@RestController
@RequestMapping("api/v1/crud-api")
public class CrudController {

    @GetMapping
    public String getName(){
        return "Flature";
    }

    @GetMapping(value = "/{variable}")
    public String getVariable(@PathVariable String variable) {
        return variable;
    }

    @GetMapping("/param")
    public String getNameWithParam(@RequestParam String name){
        return "Hello. " + name + "!";
    }

    @PostMapping
    public ResponseEntity<MemberDto> getMember(
            @RequestBody MemerDto request,
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam String organization)
    {
        System.out.println(request.getName());
        System.out.println(request.getEmail());
        System.out.println(request.getOrganization());

        MemberDto memberDto = new MemberDto();
        memberDto.setName(name);
        memberDto.setEmail(email);
        memberDto.setOrganization(organization);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }

    @PostMapping(value = "/add-header")
    public ResponseEntity<MemberDto> addHeader(@RequestHeader("my-header") String header,
                                               @RequestBody MemberDto memberDto){

        System.out.println(header);

        return ResponseEntity.status(HttpStatus.OK).body(memberDto);
    }
}

PUT, DELETE 메서드는 GETPOST 형식과 각 구성 방식이 거의 비슷하기 때문에 생략했다. 위 코드의 5~18번 줄의 코드는 GET 형식의 요청이 들어오는 상황의 케이스를 구현한다. 첫 번째 메서드는 아무 파라미터가 없는 경우, 두 번째는 PathVariable을 사용하는 경우, 세 번째는 RequestParameter를 사용하는 경우이다.

20~46번 줄에는 POST 형식의 요청을 받기 위한 두 개의 메서드가 구현돼 있다. 첫번째 메서드는 예제의 간소화를 위해 요청 파라미터(Request Parameter)와 요청 바디(Request Body)를 함께 받도록 구현했고, 두 번째 메서드는 임의의 HTTP 헤더를 받도록 구현했다.

여기서 사용된 MemeberDto객체는 아래와 같다.

MemberDto 클래스

@Setter
@Getter
public class MemberDto {
    private String name;
    private String email;
    private String organization;
    
    @Override
    public String toString(){
        return "MemberDto{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", organization='" + organization + '\'' +
                '}';
    }
}

MemberDto클래스는 name, email, organization이라는 총 3개의 필드를 가지고 있다.


RestTemplate 구현하기

일반적으로 RestTemplate은 별도의 유틸리티 클래스로 생성하거나 서비스 또는 비즈니스 계층에 구현된다. 앞서 생성한 서버 프로젝트에 요청을 날리기 위해 서버의 역할을 수행하면서 다른 서버로 요청을 보내는 클라이언트의 역할도 수행하는 새로운 프로젝트를 생성한다.

위 그림에서 클라이언트는 서버를 대상으로 요청을 보내고 응답을 받는 역할을 하고, 앞에서 구현한 서버 프로젝트는 서버2가 된다.

지금부터 RestTemplate을 포함하는 프로젝트를 생성하겠다. 다음과 같이 설정해서 프로젝트를 생성한다. 스프링 부트 버전은 이전과 같은 2.5.6 버전으로 진행하며, 다음과 같은 내용을 설정한다.

  • groupId: com.springboot
  • artifactId: rest
  • name: rest
  • Developer Tools: Lombok, Spring Configuration Processor
  • Web: Spring Web

또한 클라이언트에서 요청하는 것처럼 실습하기 위해 SwaggerConfiguration 클래스와 의존성 추가를 해야 한다. RestTemplate은 이미 spring-boot-starter-web 모듈에 포함돼 있는 기능이므로 pom.xml에 별도로 의존성을 추가할 필요는 없다.

프로젝트의 구조로는 클라이언트로부터 요청을 받는 컨트롤러와, RestTemplate을 활용해 다른 서버에 통신 요청을 하는 서비스 계층으로 작성하겠다.

GET 형식의 RestTemplate 작성하기

먼저 GET 형식의 RestTemplate 예제를 살펴보겠다.

@Service
public class RestTemplateService {

    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();
    }

    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();
    }

    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();
    }
}

RestTemplate을 생성하고 사용하는 방법은 아주 다양하다. 그중 가장 보편적인 방법은 UriComponentsBuilder를 사용하는 방법이다. UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스로서 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 수행한다.

각 메서드는 정의된 컨트롤러 메서드와 비교해 코드를 확인할 수 있다. 4~16번 줄의 메서드는 PathVariable이나 파라미터를 사용하지 않는 호출 방법이다. UriComponentsBuilder는 빌더 형식으로 객체를 생성한다. fromUriString()메서드에서는 호출부의 URL을 입력하고, 이어서 path() 메서드에 세부 경로를 입력한다. encode() 메서드는 인코딩 문자셋을 설정할 수 있는데, 인자를 전달하지 않으면 기본적으로 UTF-9로 다음과 같은 코드가 실행된다.

public final UriComponentsBuilder encode() {
    return encode(StandardCharsets.UTF-8);
}

이후 builder() 메서드를 통해 빌더 생성을 종료하고 UriComponents 타입이 리턴된다. 예제에서는 toUri() 메서드를 통해 URI 타입으로 리턴받았다. 만약 URI 객체를 사용하지 않고 String 타입의 URI를 사용한다면 toUriString() 메서드로 대체해서 사용하면 된다.

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

18~31번 줄의 코드에서 눈여겨볼 내용은 path() 메서드와 expand() 메서드 내에 입력한 세부 URI 중 중괄호({}) 부분을 사용해 개발 단계에서 쉽게 이해할 수 있는 변수명을 입력하고 expand() 메서드에서는 순서대로 값을 입력하면 된다. 값을 여러 개 넣어야 하는 경우에는 콤마(,) 로 구분해서 나열한다.

33~46번 줄은 파라미터로 전달하는 예제이다. 예제에서 볼 수 있듯이 queryParam() 메서드를 사용해 (키, 값) 형식의 파라미터를 추가할 수 있다.

POST 형식의 RestTemplate 작성

POST 형식의 RestTemplate 사용법은 아래와 같다.

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("name", "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;
}

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

    MemberDto memberDto = new MemberDto();
    memberDto.setName("flature!!");
    memberDto.setEmail("flature@gmail.com");
    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;
}

예제에서 1~22번 줄은 POST 형식으로 외부 API에 요청할 때 Body 값과 파라미터 값을 담는 방법 두 가지를 모두 보여준다. 2~10번 줄에서는 파라미터에 값을 추가하는 작업이 수행되며, 12~19번 줄에서는 RequestBody에 값을 담는 작업이 수행된다. RequestBody에 값을 담기 위해서는 12~15번 줄과 같이 데이터 객체를 생성한다. postForEntity() 메서드를 사용할 경우에는 파라미터로 데이터 객체를 넣으면 된다.

postForEntity() 메서드로 서버 프로젝트의 API를 호출하면 서버 프로젝트의 콘솔 로그에는 RequestBody 값이 출력되고 파라미터 값은 결괏값으로 리턴된다. 앞에서 프로젝트를 생성하면서 설명했지만 이 프로젝트에서 쉽게 API를 호출할 수 있게 Swagger를 설정하겠다. pom.xml파일에 Swagger 의존성을 추가한 후 아래와 같이 Swagger 설정 코드를 작성한다.

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.springboot.rest"))
            .paths(PathSelectors.any())
            .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
            .title("Spring Boot Open API Test with Swagger")
            .description("설명 부분")
            .version("1.0.0")
            .build();
    }
}

그러고 나서 앞에서 작성한 서비스 코드를 연결하는 컨트롤러 코드를 아래와 같이 작성한다.

@RestController
@RequestMapping("/rest-template")
public class RestTemplateController {
    
    private final RestTemplateService restTemplateService;
    
    public RestTemplateController(RestTemplateService restTemplateService){
        this.restTemplateService = restTemplateService;
    }
    
    @GetMapping
    public String getName(){
        return restTemplateService.getName();
    }
    
    @GetMapping("/path-variable")
    public String getNameWithPathVariable(){
        return restTemplateService.getNameWithPathVariable();
    }
    
    @PostMapping
    public ResponseEntity<MemberDto> postDto(){
        return restTemplateService.postWithParamAndBody();
    }
    
    @PostMapping("/header")
    public ResponseEntity<MemberDto> postWithHeader(){
        return restTemplateService.postWithHeader();
    }
}

여기까지 진행했다면 애플리케이션을 실행하고 postDto() 메서드에 해당하는 POST API를 호출하면 아래의 결과가 출력된다. 참고로 이번 장에서 진행하는 실습은 앞서 생성한 2개의 프로젝트가 모두 가동돼 있는 상태에서 진행해야 한다.

flature!!
fature@gmail.com
Around Hub Studio

위 출력 결과는 서버 프로젝트가 파라미터의 값과 Body 값을 정상적으로 전달받았다는 것을 의미한다.

위에서 작성한 서비스 코드의 24~47번 줄의 메서드는 헤더를 추가하는 예제이다. 대부분의 외부 API는 토큰키를 받아 서비스 접근을 인증하는 방식으로 작동한다. 이때 토큰값을 헤더에 담아 전달하는 방식이 가장 많이 사용된다.

헤더를 설정하기 위해서는 RequestEntity 를 정의해서 사용하는 방법이 가장 편한 방법이다. 37~40번 줄은 RequestEntity를 생성하고 post() 메서드로 URI를 설정한 후 header() 메서드에서 헤더의 키 이름과 값을 설정하는 코드이다. 대체로 서버 프로젝트의 API 명세에는 헤더에 필요한 키 값을 요구하면서 키 이름을 함께 제시하기 때문에 그에 맞춰 헤더 값을 설정하면 된다.

마지막으로 43번 줄에는 RestTemplateexchange() 메서드를 사용했다. exchange() 메서드는 모든 형식의 HTTP 요청을 생성할 수 있다. RequestEntity의 설정에서 post() 메서드 대신 다른 형식의 메서드로 정의만 하면 exchange() 메서드로 쉽게 사용할 수 있기 때문에 대부분 exchange() 메서드를 사용하는 편이다.

지금까지 GET, POST 형식으로 RestTemplate을 사용하는 방법을 알아봤다.


RestTemplate 커스텀 설정

RestTemplate은 HTTPClient를 추상화하고 있다. HttpClient의 종류에 따라 기능에 차이가 다소 있는데, 가장 큰 차이는 커넥션 풀(Connection Poll)이다.

💡 Tip. 커넥션 풀이란?
커넥션 풀(Connection Poll)은 데이터베이스나 다른 리소스와의 연결을 효율적으로 관리하여 성능을 향상시키고, 동시 접속 처리를 지원하는 소프트웨어적 기법이다. 미리 생성된 연결을 재사용하고, 자원을 효율적으로 관리하여 애플리케이션의 부담을 줄인다.

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

먼저 아파치의 HttpClient를 사용하려면 아래와 같이 의존성을 추가해야 한다.

<dependencies>
    .. 생략 ..
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    .. 생략 ..
</dependencies>

의존성을 추가하면 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();
    this.setRequestFactory(requestFactory);
}

ClientHttpRequestFactory는 함수형 인터페이스(functional interface)로, 대표적인 구현체로서 SimpleClieentHttpRequestFactoryHttpComponentsClientHttpRequestFactory가 있다. 별도의 구현체를 설정해서 전달하지 않으면 HttpAccessor에 구현돼 있는 내용에 의해 SimpleClientHttpRequestFactory를 사용하게 된다.

별도의 HttpComponentsClientHttpRequestFactory 객체를 생성해서 ClientHttpRequestFactory를 사용하면 15~16번 줄과 같이 RestTemplateTimeout 설정을 할 수 있다.

그리고 HttpComponentsClientHttpRequestFactory는 커넥션 풀을 설정하기 위해 HttpClientHttpComponentsClientHttpRequestFactory에 설정할 수 있다. HttpClient를 생성하는 방법은 두 가지가 있는데 4~7번 줄의 HttpClientBuilder.create() 메서드를 사용하거나 9~12번 줄의 HttpClients.custom() 메서드를 사용하는 것이다.

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

4번 줄에서는 HttpClient 객체를 생성했고 9번 줄에서는 CloseableHttpClient를 생성했다. 두 객체는 비슷하면서도 기능 면에서 차이가 있다. 이 두 객체의 차이를 비교해보면 커넥션에 대한 기초 지식까지 늘릴 수 있는 기회가 될 것이다.

profile
백엔드 개발자

0개의 댓글

관련 채용 정보