최근에 개발되는 서비스들은 마이크로서비스 아키텍처(MSA)를 주로 채택하고 있다. MSA는 말 그래도 어플리케이션이 가지고 있는 기능(서비스)이 하나의 비즈니스 범위만 가지는 형태이다. 각 어플리케이션은 자신이 가진 기능을 API로 외부에 노출하고, 다른 서버가의 API를 호출해서 사용할 수 있게 구성된다. 그래서 각 서버가 클라이언트가 되기도 한다. 이런 서버간 요청을 도와주는 RestTemplate, WebClient를 글을 작성하며 공부해 보았다.
RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다.
RESTful 원칙을 따르는 서비스를 편리하게 만들수 있도록 도와준다.
기본적으로 동기 방식으로 처리되지만 비동기 방식으로도 사용할 수 있다. 비동기 방식으로 사용하기 위해서는 AsyncRestTemplate을 사용하면 된다.
하지만 현재 RestTemaplate은 depreated된 상태라서 WebClient 방식을 사용하는 것이 권장되고 있는 상황임을 알고 있어야한다.

Application에서 RestTemplate을 선언하고 URI와 HTTP 메서드, Body 등을 설정
외부 API로 요청을 보내게 되면 RestTemplate에서 HttpMessageConverter를 통해 RequestEntity를 요청 메시지로 변환
RestTemaplate에서는 변환된 요청 메시지를 ClientHttpRequestFactory를 통해 ClientHttpRequest로 가져온 후 외부 API로 요청을 보낸다.
외부에서 요청에대한 응답을 받으면 RestTemplate은 ResponseErrorHandler로 오류를 확인하고, 오류가 있다면 ClientHttpResponse에서 응답 데이터를 처리
받은 응답 데이터가 정삭적이면 다시 한번 HttpMessageConverter를 거쳐 자바 객체로 변환해서 어플리케이션으로 반환
공부하면서 내가 이해한 RestTemplate의 흐름을 그려본 것이다.

그림을 그리면서 결국 Application에서는 사용하기 좋은 자바 객체로 다시 응답 값을 받는 것을 알 수 있었다.
RestTempalte을 사요해보기 위해서는 두개의 서버가 필요하다. 그래서 간단하게 두개의 서버를 만들었다. 서버 하나는 8080포트에서 실행시켰고 다른 하나는 9090포트에서 실행시켜 RestTemplate을 통해 소통하도록 했다.
Controller
// file -> controller/CrudController.java
@RestController
@RequestMapping("/api/v1/crud-api")
public class CrudController {
@GetMapping
public String getName(){
return "Falture";
}
@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 MemberDto 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("/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);
}
}
MemberDto
// file -> dto/MemberDto.java
public class MemberDto {
private String name;
private String email;
private String organization;
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getOrganization() {
return organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
@Override
public String toString() {
return "MemberDto{" +
"name='" + name + '\'' +
", email='" + email + '\'' +
", organization='" + organization + '\'' +
'}';
}
}
Service
@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")
.queryParam("name", "Flature")
.path("/api/v1/crud-api")
.encode()
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
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;
}
public ResponseEntity<MemberDto> postWithHeader(){
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/v1/add-header")
.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");
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;
}
}
UriComponentsBuilder를 사용해서 RestTemplate을 사용하였다. UriComponentsBuilder는 스프링 프레임워크에서 제공하는 클래스다. 이 클래스는 여러 파라미터를 연결해서 URI 형식으로 만드는 기능을 제공해준다.
fromUriString() 메서드에는 호출부의 URL을 입력하면 된다.
path() 메서드에는 세부 경로를 입력하면된다.
build() 메서드를 통해 빌터 생성을 종료시키고 UriComponents 타입을 리턴한다.
이렇게 생성된 uri는 restTemplate이 외부 API를 요청하는 데 사용된다.
getForEntity() 메서드를 통해 요청을 보낸다. URI와 응답 받는 타입이 매개변수로 사용된다.
path()와 expand() 메서드는 변수명을 입력하는 것이다.
예를들어서, 이전에 Controller에서 컨트롤러 주소를 기입할 때 "/api/user/{id}" 이런식으로 적은 적이 있다. 이때 {id}에 변수 값이 들어가고 요청되는 곳에서 이 위치에 들어온 값을 @PathVariable을 사용해서 변수로 받아 사용한다. 이와 동일한 것이다. path에는 {}를 사용해서 들어갈 위치의 변수명이 들어가도록 작성ㅇ하고 expand()에서 그 위치에 어떤 값이 들어갈지 적어주는 것이다.
Controller
@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();
}
@GetMapping("/parameter")
public String getNameWithParameter(){
return restTemplateService.getNameWithParameter();
}
@PostMapping
public ResponseEntity<MemberDto> postDto(){
return restTemplateService.postWithParamAndBody();
}
@PostMapping("/header")
public ResponseEntity<MemberDto> postWithHeader(){
return restTemplateService.postWithHeader();
}
}
DTO
실행결과
결과는 Swagger를 사용해서 확인해 보았다.

Swagger에 들어가 POST /rest-template으로 진행해 보았다.

정상적으로 응답을 받은것을 볼 수 있다.
실제 운영환경에서는 RestTemplate이 지원 중단되었기 때문에 WebClient를 사용할 것을 권고하고 있다.
Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터(Reactor)를 기반으로 동작하는 API이다.
Spring WebFlux에 대해서는 자세한 설명이 없어서 이후에 추가적인 공부가 필요할 것 같다.
@Service
public class WebClientService {
public String getName() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
return webClient.get()
.uri("/api/v1/crud-api")
.retrieve()
.bodyToMono(String.class)
.block();
}
public String getNameWithPathVariable() {
WebClient webClient = WebClient.create("http://localhost:9090");
ResponseEntity<String> responseEntity = webClient.get()
.uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")
.build("Flature"))
.retrieve().toEntity(String.class).block();
return responseEntity.getBody();
}
public String getNameWithParameter() {
WebClient webClient = WebClient.create("http://localhost:9090");
return webClient.get().uri(uriBuilder -> uriBuilder.path("api/v1/crud-api")
.queryParam("name", "Flature")
.build())
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(String.class);
} else {
return clientResponse.createException().flatMap(e -> Mono.error(e));
}
})
.block();
}
}
getName() 메서드에서는 builder를 사용해서 WebClient를 만들었고 나머지 두 메서드는 create() 메서드를 사용했다.
WebClient는 객체를 생성 후 요청을 전달하는 방식으로 동작한다.
defaultHeader() 메서드로 헤더의 가밧을 설정한다.
retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출하는 방법 중 하나이다.
기본저긍로 WebClient는 논블로킹 방식으로 동작하기 때문에 코드를 블로킹 구조로 바꿔줄 필요가 있다. block()메서드를 사용해서 바꿔준다.
getNameWithPathVariable() 메서드는 PathVariable 값을 추가해 요청을 보내는 예제이다. uri()메서드 내부에서 uriBuilder를 사용해 path를 설정하고 build() 메서드를 추가해서 값을 넣는 것으로 pathVariable을 추가할 수 있다.
bodyToMono()메서드가 아닌 toEntity()를 사용하면 ResponseEntity타입으로 응답을 받을 수 있다.
exchangeToMono()를 사용해서 if문을 통해 clientResponse 상태값에 따라 결과값을 다르게 전달할 수 있게 했다.
public ResponseEntity<MemberDto> postWithHeader(){
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
MemberDto memberDto = new MemberDto();
memberDto.setName("flature!!");
memberDto.setEmail("flature@gmail.com");
memberDto.setOrganization("Around Hub Studio");
return webClient
.post()
.uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/add-header")
.build())
.bodyValue(memberDto)
.header("my-header", "Wikibooks API")
.retrieve()
.toEntity(MemberDto.class)
.block();
}
이번에는 WebClient로 POST방식을 작성한 것이다.
GET 방식과 크게 다르지 않지만 body값을 추가한 것과 header를 추가한 점은 차이가 있다.
bodyValue() 메서드를 통해서 HTTP body값을 설정했고 header() 메서드를 통해서 header에 값을 추가했다.
일반적으로 추가한 헤더에는 API를 사용하기 위한 토큰값을 담아 전달한다.
이번에는 RestTemplate과 WebClient를 사용해서 서버간 통신 하는 방법을 알아보았다. 통신을 하기 위해서는 프론트 부분에서 요청을 보내는 방법이 유일하다생각했다. 이전에 React를 사용하면서 axios를 통해 요청을 보내는 코드를 작성한 적이 있다. 그래서 그런지 이 방법이 유일한 통신 방법이라고 생각했다. 하지만 서버간에도 이렇게 통신을 하고 데이터를 주고받는다는 점이 신기하고 재미있었다.
공부를 하면서 Spring WebFlux에 대한 정보가 없던 부분이 아쉬웠다. 요즘 회사들의 취업 공고를 보면 백엔드 부분에서 webflux에 대한 요구를 하는 경우가 많아진것을 보았다. 처음 보는 단어라는 이유로 위축되었었는데 결국 서버간 통신을 경험해 보았는지를 물어보는 것이었다. 책에 설명이 없어서 궁금한것도 있고 실제 실무에서도 자주 사용할것 같아 추가적인 공부를 해서 다음에 포스팅을 하면 좋을것 같다.