[Spring Boot] 서버 간 통신: RestTemplate과 WebClient
1. 서버 간 통신의 필요성
MSA(MicroService Architecture)의 등장
소프트웨어의 모든 구성요소가 한 프로젝트에 통합되어 있는 Monolithic Architecture는 서비스의 규모가 커질 수록 유지 보수 측면에서의 한계를 보인다.
이러한 문제를 보완하기 위해 하나의 애플리케이션이 하나의 기능만을 가지도록 구성한 형태인 MicroService Architecture가 등장하였다. 애플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버(애플리케이션)가 그 API를 호출하여 사용한다. 이 과정에서 다른 서버로 웹 요청을 보내고 응답을 받을 수 있게 도와주는 기술이 RestTemplate과 WebClient이다.
2. RestTemplate
개요
- RestTemplate은 Spring에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다. HTTP 서버와의 통신을 단순화한 이 템플릿을 이용하면 RESTful한 서비스를 편리하게 만들 수 있다.
- 기본적으로 동기 방식으로 처리된다. 비동기 방식으로 사용하려면 AsyncRestTemplate을 사용한다.
- 현업에서 많이 쓰이나 deprecated 상태라서 대체 기술인 WebClient 방식에 대한 학습도 필요하다.
RestTemplate의 특징
- HTTP 요청 메서드(GET, POST 등)에 맞는 여러 메서드를 제공한다.
- HTTP 요청 후 JSON, XML, 문자열 등 다양한 형식으로 응답을 받을 수 있다.
- Blocking I/O 기반의 동기 방식을 사용한다.
- 다른 API를 호출할 때 HTTP 헤더에 다양한 값을 설정할 수 있다.
* Bocking: 함수가 B 함수를 호출 할 때, B 함수가 자신의 작업이 종료되기 전까지 A 함수에게 제어권을 돌려주지 않는 것
* Non-Blocking: A 함수가 B 함수를 호출 할 때, B 함수가 제어권을 바로 A 함수에게 넘겨주면서, A 함수가 다른 일을 할 수 있도록 하는 것.
RestTemplate의 동작원리
위 그림과 같이 RestTemplate은 Spring Framework(애플리케이션)와 Rest API(외부 API) 사이에서 서버 간 데이터의 요청과 반환을 돕는다. 다음은 개략적인 동작 순서이다.
- 애플리케이션에서 RestTemplate을 선언하여 URI, HTTP 메서드, Body 등 요청에 대한 정보를 설정한다.
- 외부 API로 요청을 보내면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환한다.
- RestTemplate에서는 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientRequest로 가져온 후 외부 API로 요청을 보낸다.
- 외부에서 요청에 대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리한다.
- 받은 응답 데이터가 정상적이라면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환하여 애플리케이션으로 반환한다.
RestTemplate의 대표적인 메서드
RestTemplate은 편리하게 외부 API로 요청을 보낼 수 있도록 다음과 같은 다양한 메서드를 제공한다. 메서드의 이름에 접미사로 붙은 ForEntity의 경우 반환 타입이 ResponseEntity이며, ForObject의 경우 객체로 반환받는다.
RestTemplate 예제
1. GET 요청을 위한 메서드
@Service
public class RestTemplateService {
// 파라미터 없는 경우
public String getNumber() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080") // 호출부 URL
.path("/api/v1/crud-api") // 세부경로
.encode()
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
// PathVariable 방식
public String getNameWithPathVariable() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080")
.path("/api/v1/crud-api/{name}/{email}")
.encode()
.build()
.expand("Green", "green@gmail.com") // pathVariable로 들어갈 변수 입력
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
// RequestParam 방식
public String getNameWithParameter() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080")
.path("/api/v1/crud-api/param")
.queryParam("name", "green") // 키, 값 형식
.encode()
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
}
2. POST 요청을 위한 메서드
@Service
public class RestTemplateService {
// 파라미터와 body에 값을 담는 경우
public ResponseEntity<MemberDto> postWithParamAndBody() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080") // 호출부 URL
.path("/api/v1/crud-api") // 세부경로
.queryParam("name", "Green") // 파라미터에 담는 부분
.queryParam("emai", "green@gmail.com") // 파라미터에 담는 부분
.encode()
.build()
.toUri();
MemberDto memberDto = MemberDto.builder()
.name("Blue")
.email("blue@gmail.com")
.build();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<MemberDto> responseEntity = restTemplate.postForEntity(uri, memberDto, MemberDto.class); // 지정한 클래스의 객체로 반환받겠다고 알리는 부분
return responseEntity;
}
// PathVariable 방식
public ResponseEntity<MemberDto> postWithHeader() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080")
.path("/api/v1/crud-api/add-header")
.encode()
.build()
.toUri();
MemberDto memberDto = MemberDto.builder()
.name("Blue")
.email("blue@gmail.com")
.build();
RequestEntity<MemberDto> requestEntity = RequestEntity
.post(uri) // uri 주소로 POST 요청을 보내겠다
.header("TOKEN", "blahblahblah") // header 설정: 키, 값 형식으로 입력
.body(memberDto); // body에 값 입력
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<MemberDto> responseEntity = restTemplate.exchange(requestEntity, MemberDto.class); // exchange는 아무 Http 메서드에도 붙을 수 있다
return responseEntity;
}
}
3. WebClient
개요
- WebClient는 Spring WebFlux 라이브러리의 일부로서, HTTP 요청을 수행하는 클라이언트이다.
- WebClient는 Reactor 기반으로 동작하는 API이다. 따라서 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.
WebClient 특징
- Non-Blocking I/O를 지원한다.
- Reactive Streams의 Back Pressure를 지원한다.
- 적은 하드웨어 리소스로 동시성을 지원한다.
- 함수형 API를 지원한다.
- 동기, 비동기 상호작용을 지원한다.
- 스트리밍을 지원한다.
WebClient 예제
1. GET 요청을 위한 메서드
@Service
public class WebClientService {
public String getName() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080") // 기본 URL 설정
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // header 설정
.build();
return webClient
.get() // HTTP 메서드 설정
.uri("/api/v1/crud-api")
.retrieve() // 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법
.bodyToMono(String.class)
.block(); // blocking 방식으로 변환
}
public String getNameWithPathVariable() {
WebClient webClient = WebClient.create("http://localhost:8080");
ResponseEntity<String> responseEntity = webClient.get()
.uri("/api/v1/crud-api/{name}", "Green") // PathVariable 설정
.retrieve()
.toEntity(String.class).block();
return responseEntity.getBody();
}
public String getNameWithParameter() {
WebClient webClient = WebClient.create("http://localhost:8080");
return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
.queryParam("name", "Green") // 파라미터 설정
.build())
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(String.class);
} else {
return clientResponse.createException().flatMap(Mono::error);
}
})
.block();
}
}
2. POST 요청을 위한 메서드
@Service
public class WebClientService {
public ResponseEntity<MemberDto> postWithParamAndBody() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080") // 기본 URL 설정
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // header 설정
.build();
MemberDto memberDto = MemberDto.builder()
.name("Blue")
.email("blue@gmail.com")
.build();
return webClient
.post() // HTTP 메서드 설정
.uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
.queryParam("name", "green") // 파라미터 설정
.queryParam("email", "green@gmail.com")
.build())
.bodyValue(memberDto) // body 설정
.header("TOKEN", "blagblag") // 헤더 설정
.retrieve()
.toEntity(MemberDto.class)
.block();
}
}
[참고 자료] 『스프링 부트 핵심 가이드』 장정우 지음
위 책을 읽고 정리한 내용입니다.