RestTemplate은 스프링에서 HTTP 통신 기능을 손쉽게 사용하도록 설계된 템플릿이다. RestTemplate은 기본적으로 동기 방식으로 처리되며, 비동기 방식으로 사용하고 싶을 경우 AsyncRestTemplate을 사용하면 된다. RestTemplate은 현업에서 많이 사용되나 deprecated된 상태라서 WebClient 방식도 함께 알아두면 좋다.
12.1.1 RestTemplate의 동작 원리
서버 용도 프로젝트 생성하고 하나의 컴퓨터에서 두 개의 프로젝트를 실행시켜야 하므로 포트를 변경해준다.
// 서버용도 프로젝트 컨트롤러
@RestController
@RequestMapping("/rest-template/test")
public class CrudController {
@GetMapping
public String getName() {
return "통신 성공";
}
@GetMapping(value = "/{variable}")
public String getVariable(@PathVariable String variable) {
return variable;
}
@GetMapping("/param")
public String getNameWithParam(@RequestParam String name) {
return "통신 성공" + name + "!";
}
@PostMapping
public ResponseEntity<PartnerDto> getPartner(
@RequestBody PartnerDto 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());
PartnerDto partnerDto = new PartnerDto();
partnerDto.setName(name);
partnerDto.setEmail(email);
partnerDto.setOrganization(organization);
return ResponseEntity.status(HttpStatus.OK).body(partnerDto);
}
@PostMapping(value = "/add-header")
public ResponseEntity<PartnerDto> addHeader(
@RequestHeader("my-header") String header,
@RequestBody PartnerDto partnerDto) {
System.out.println(header);
return ResponseEntity.status(HttpStatus.OK).body(partnerDto);
}
}
// 서버용도 프로젝트 PartnerDto
@Getter
@Setter
public class PartnerDto {
private String name;
private String email;
private String organization;
}
12.2.2 RestTemplate 구현하기
RestTemplate을 사용하기 위해 보통 서비스 단에서 URI를 구성하여 사용한다. Body에 객체를 담아 호출을 할 경우에는 객체를 RestTemplate의 메서드에 파라미터로 넣어 호출할 수 있다. Header에 정보를 담아 호출할 경우에는 RequestEntity를 구성하여 RestTemplate의 exchange() 메서드를 사용하여 호출할 수 있다.
// RestTemplate service 구현 예제
@Service
public class RestTemplateService {
// GET형식의 RestTemplate 작성
public String getName() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/rest-template/test")
.encode() // 기본적으로 utf-8
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity =
restTemplate.getForEntity(uri, String.class); // uri와 응답받는 타입
return responseEntity.getBody();
}
public String getNameWithPathVariable() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/rest-template/test/{name}")
.encode()
.build()
.expand("원빈") // pathVariable에 넣을 값 : 값이 여러 개일 경우 , 로 구분
.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("/rest-template/test/param")
.queryParam("name", "원빈") // 파라미터 전달
.encode()
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity =
restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
// POST 형식의 RestTemplate 작성
public ResponseEntity<PartnerDto> postWithParamAndBody() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/rest-template/test")
.queryParam("name", "원빈")
.queryParam("email", "aaa@aaaa.com")
.queryParam("organization", "Happy")
.encode()
.build()
.toUri();
PartnerDto partnerDto = new PartnerDto();
partnerDto.setName("강호동");
partnerDto.setEmail("bbbb@bbbbb.com");
partnerDto.setOrganization("Rich");
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<PartnerDto> responseEntity =
restTemplate.postForEntity(uri, partnerDto, PartnerDto.class);
return responseEntity;
}
public ResponseEntity<PartnerDto> postWithHeader() {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/rest-template/test/add-header")
.encode()
.build()
.toUri();
PartnerDto partnerDto = new PartnerDto();
partnerDto.setName("강호동");
partnerDto.setEmail("bbbb@bbbbb.com");
partnerDto.setOrganization("Rich");
RequestEntity<PartnerDto> requestEntity = RequestEntity
.post(uri)
.header("my-header", "Happy API")
.body(partnerDto);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<PartnerDto> responseEntity =
restTemplate.exchange(requestEntity, PartnerDto.class);
return responseEntity;
}
}
// RestTemplate 사용하기 위한 controller 작성 예제
@RestController
@RequiredArgsConstructor
@RequestMapping("/rest-template")
public class RestTemplateController {
private final 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<PartnerDto> postDto() {
return restTemplateService.postWithParamAndBody();
}
@PostMapping("/header")
public ResponseEntity<PartnerDto> postWithHeader() {
return restTemplateService.postWithHeader();
}
}
참고 : spring boot 3.x.x 부터는 swagger 적용을 위해 아래의 의존성을 주입해야 한다.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
12.2.3 RestTemplate 커스텀 설정
RestTemplate은 HttpClient를 추상화하고 있다. RestTemplate은 커넥션 풀을 지원하지 않는다. 커넥션 풀이란 미리 connection을 해놓은 객체들을 pool에 저장해두었다가, 클라이언트 요청이 오면 connection 객체를 빌려주고 처리가 끝나면 다시 connection 객체를 반납받아 pool에 저장하는 방식을 말한다. 커넥션 풀을 지원하지 않으면 매번 호출할 때 마다 포트를 열어 커넥션을 생성하게 되는데, TIME_WAIT 상태가 된 소켓*을 다시 사용하려고 접근한다면 재사용하지 못하게 된다.
RestTemplate에서 커넥션 풀 기능을 활성화하는 대표적인 방법은 아파치에서 제공하는 HttpClient로 대체해서 사용하는 방식이다. 아파치 HttpClient를 사용하려면 아래와 같이 의존성을 주입한다. spring boot 3.x.x 에서는 아래와 같이 httpclient5를 사용해야 한다.
implementation 'org.apache.httpcomponents.client5:httpclient5-win:5.2.1'
아래와 같이 커스텀 RestTemplate 객체를 생성해 줄 수 있다.
// 커스텀 RestTemplate 객체 생성 메서드
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
PoolingHttpClientConnectionManager connectionManager =
PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(500)
.setMaxConnPerRoute(500)
.build();
// HttpClient client = HttpClientBuilder.create()
// .setConnectionManager(connectionManager)
// .build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
factory.setHttpClient(httpClient);
factory.setConnectTimeout(2000);
// factory.setReadTimeout(5000);
return new RestTemplate(factory);
}
spring boot 3.x.x 부터 httpclient5를 사용하는데 위에서 RequestFactory에서 setReadTimeout() 메서드가 사용할 수 없게 되었다. 이를 위한 대응방법을 아직 찾지 못 해서, 좀 더 공부가 필요할 듯 하다.
실제 운영환경에서는 RestTemplate을 많이 사용하고 있다. 하지만 최신 버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용할 것을 권고하고 있다.
Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다. WebClient는 리액터(Reactor)* 기반으로 동작하는 API다.
12.3.1 WebClient 구성
WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 아래와 같이 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
12.4.1 WebClient 구현
// WebClient를 활용한 GET 요청 예제
@Service
public class WebClientService {
public String getName() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090")
.defaultHeader(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.build();
// WebClient는 get(), post(), put(), delete() 등 명확하게 HTTP 메서드를 설정할 수 있다.
return webClient.get()
.uri("/rest-template/test")
.retrieve() // 요청에 대한 응답을 받았을 대 그 값을 추출하는 방법 중 하나이다.
.bodyToMono(String.class) // 리턴타입을 설정해서 문자열 객체를 받아오게 함
.block(); // WebClinet는 논블로킹 방식으로 동작하기 때문에 블로킹 구조로 별도 설정함.
}
public String getNameWithPathVariable() {
WebClient webClient = WebClient.create("http://localhost:9090");
ResponseEntity<String> responseEntity = webClient.get()
.uri(uriBuilder -> uriBuilder.path("rest-template/test/{name}")
.build("원빈"))
.retrieve().toEntity(String.class).block();
assert responseEntity != null;
return responseEntity.getBody();
}
public String getNameWithParameter() {
WebClient webClient = WebClient.create("http://localhost:9090");
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("rest-template/test")
.queryParam("name", "원빈").build())
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.OK)) {
return clientResponse.bodyToMono(String.class);
} else {
return clientResponse.createException().flatMap(Mono::error);
}
})
.block();
}
}
일반적으로 WebClient 객체를 이용할 때는 WebClient 객체를 생성한 후 재사용하는 방식으로 구현하는 것이 좋다. builder()를 사용할 경우 확장할 수 있는 메서드는 다음과 같다.
일단 빌드된 WebClient는 변경할 수 없지만, 다음과 같이 복사해서 사용할 수는 있다.
WebClient webClient = WebClient.create("http://localhost:9090")
WebClient clone = webClient.mutate().build();
Mono와 Flux는 리액터의 핵심 타입으로 둘 다 리액티브 스트림의 Publisher 인터페이스를 구현한 것이다.
// WebClient를 활용한 POST 요청 예제
public ResponseEntity<PartnerDto> postWithParamAndBody() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090")
.defaultHeader(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.build();
PartnerDto partnerDto = PartnerDto.builder()
.name("강호동!")
.email("aaaa@aaaaa.com")
.organization("행복한 ~")
.build();
return webClient.post()
.uri(uriBuilder -> uriBuilder.path("rest-template/test")
.queryParam("name", "원빈")
.queryParam("email", "bbbb@bbb.com")
.queryParam("organization", "재미있는").build())
.bodyValue(partnerDto)
.retrieve()
.toEntity(PartnerDto.class)
.block();
}
public ResponseEntity<PartnerDto> postWithHeader() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:9090")
.defaultHeader(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.build();
PartnerDto partnerDto = PartnerDto.builder()
.name("강호동!")
.email("aaaa@aaaaa.com")
.organization("행복한 ~")
.build();
return webClient.post()
.uri(uriBuilder -> uriBuilder.path("rest-template/test/add-header")
.build())
.bodyValue(partnerDto)
.header("my-header", "TEST API")
.retrieve()
.toEntity(PartnerDto.class)
.block();
}