최근 Spring을 사용한 프로젝트를 진행하면서 외부 API를 호출해 데이터를 받고, 이 중 필요한 것만 뽑아서 데이터베이스에 저장하는 작업을 해야했다.
Java의 HttpURLConnection/URLConnection부터 Apache의 HttpClient, Spring의 RestTemplate, WebClient, OpenFeign까지 외부 API를 호출하는 다양한 도구가 존재한다.
이중 Spring 기반의 RestTemplate, WebClient, OpenFeign 의 특성에 대해 간단하게 정리해본 후, 각각의 경우 어떻게 코드가 작성되는지 비교해보도록 하겠다.
이제부터는 각각을 사용해서 같은 기능을 하는 코드를 짜보면서 어떤 차이가 있는지 알아보겠다.
멀티스레드, 동기, blocking 방식이기 때문에 응답까지 대기하며 스레드를 차단한다. 대량의 요청을 처리해야 하거나 비동기 처리가 필요한 경우 성능 이슈가 발생할 수 있다.
@Configuration
@Slf4j
@RequiredArgsConstructor
public class RestTemplateConfig {
private final VCFConfiguration vcfConfiguration;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
.setConnectTimeout(Duration.ofMillis(5000)) // connection-timeout
.setReadTimeout(Duration.ofMillis(5000)) // read-timeout
.additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")))
.rootUri(String.format("호출할 API 주소"))
.errorHandler(new RestTemplateResponseErrorHandler())
.build();
}
}
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode() == HttpStatus.BAD_REQUEST
|| response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
@Service
@RequiredArgsConstructor
public class Service{
private final RestTemplate restTemplate;
public SampleDto useResTemplate(){
HttpHeaders headers = new HttpHeaders();
MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
map.add("data1", "test");
map.add("file", file);
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(map, headers);
ResponseEntity<SampleDto> response = restTemplate.exchange(
"/sample",
HttpMethod.POST,
request,
SampleDto.class
);
ampleDto sampleDto = response.getBody();
return sampleDto;
}
}
request를 빼면 GET 방식의 request를 보낼 수 있다.
MultiValueMap<String, Object>를 이용해서 다양한 객체를 requestBody에 담을 수 있으며 그것을 HttpEntity<MultiValueMap<String, Object>>에 담아서 request를 보낸다.
@Data
public class SampleDto{
@JsonProperty("test_value")
private Integer testValue;
@JsonProperty("readValue")
private Integer readValue;
@JsonProperty("number")
private Integer strNumber;
}
SampleDto sampleDto = response.getBody();
이렇게 response Body 형식을 정할 수 있는데 만약 responseBody로 가져오는 형식과 내가 만든 DTO의 형식이 다르면 @JSonProperty로 매핑이 가능하다.
윗 줄의 코드로 객체로 변환이 가능하지만 이렇게 하려면 매핑시킨 객체에 setter와 기본생성자가 모두 있어줘야 한다!!!!!!!!
비즈니스 로직 상 에러를 처리할 일이 있는 게 아니라면 핸들러에서 Exception을 발생시키자.
Spring 5부터는 이것을 사용하는 것을 권장한다.
하지만 webFlux를 사용해서 진입장벽이 좀 있다.
reactive, non-blocking 솔루션이지만 동기, 비동기를 모두 지원한다.
반응형에는 권장하지 않는다.
WebFlux란
Spring에서 reactive-programming을 돕는 모듈이다.
비동기, 동기 처리가 모두 가능하다.
RestTemplate보다 확장성과 성능이 높다.
리액티브 스트림을 활용하여 대량의 요청을 효과적으로 처리할 수 있다.
비동기나 리액티브 처리가 필요하다면 유용
gradle에 아래 코드 추가 후
compile 'org.springframework.boot:spring-boot-starter-webflux'
@Configuration
public class WebClientConfig {
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
return factory;
}
@Bean
public WebClient webClient(){
return WebClient.builder()
.baseUrl("http://localhost:8080/api/")
.build();
}
}
@Service
@RequiredArgsConstructor
public class Service{
private final WebClient webClient;
public SampleDto useWebClient(){
SampleDto sampleDto = webClient.get()
.uri(uriBuilder -> uriBuilder
.path(String.format("/sample/sample2/sample3"))
.queryParam("x", 2)
.queryParam("y", 3)
.build())
.retrieve()
.bodyToMono(SampleDto.class)
.block();
return sampleDto;
}
}
http://localhost:8080/api/sample/sample2/sample3?x=2&y=2에 get 방식으로 api를 호출하고, 그 응답이 SampleDTO에 담긴다.
retrieve말고 exchange도 사용할 수 있는데 후자는 응답을 받아 다른 가공하고 객체 처리가 가능하다. 전자는 그런 처리없이 바로 객체로 처리한다.
일반적으로 전자를 leak을 막기 위해 권장한다.
@Service
@RequiredArgsConstructor
public class Service{
private final WebClient webClient;
public SampleDto useWebClient(){
SampleDto sampleDto = webClient.post() //post로 변경
.uri("/sample") //query parameter이 없으면 단순 String으로 넣어도 무방하다.
.body(BodyInserters.fromFormData("id", "admin"))
.retrieve()
.bodyToMono(SampleDto.class)
.block();
return sampleDto;
}
}
이렇게 하면 POST로 사용 가능하다.
requestBody에 필요 정보를 담을 때 BodyInserters.fromFormData().with()... 를 반복해서 넣어주기만 해도 request가 잘 가게된다.
@Service
@RequiredArgsConstructor
public class Service{
private final WebClient webClient;
public SampleDto useWebClient(){
SampleDto sampleDto = webClient.get()
.uri(uriBuilder -> uriBuilder
.path(String.format("/sample/%s/%s", sample2, sample3))
.queryParam("x", pcaRequest.getScreenWidth())
.queryParam("y", pcaRequest.getScreenHeight())
.build())
.retrieve()
.bodyToMono(SampleDto.class)
.onStatus(status -> status.is4xxClientError()
|| status.is5xxServerError()
, clientResponse ->
clientResponse.bodyToMono(String.class)
.map(body -> new RuntimeException(body)))
.block();
return sampleDto;
}
}
onStatus를 사용해서 각 요청마다 에러 처리가 가능하다.
작성할 코드가 매우 줄어든다는 장점을 가지고 있다.
요즘 같이 API 호출이 잦은 MSA의 시대에서 이런 코드를 반복한다는 것은 번거롭다.
@Component
@RequiredArgsConstructor
class ExchangeRateRestTemplate {
private final RestTemplate restTemplate;
private final ExchangeRateProperties properties;
private static final String API_KEY = "apikey";
public ExchangeRateResponse call(final Currency source, final Currency target) {
return restTemplate.exchange(
createApiUri(source, target),
HttpMethod.GET,
new HttpEntity<>(createHttpHeaders()),
ExchangeRateResponse.class)
.getBody();
}
private String createApiUri(final Currency source, final Currency target) {
return UriComponentsBuilder.fromHttpUrl(properties.getUri())
.queryParam("source", source.name())
.queryParam("currencies", target.name())
.encode()
.toUriString();
}
private HttpHeaders createHttpHeaders() {
final HttpHeaders headers = new HttpHeaders();
headers.add(API_KEY, properties.getKey());
return headers;
}
}
RestTemplate을 쓰면 이렇게 쓰는 코드를 OpenFeign을 사용하면
@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.currency.api.uri}")
public interface ExchangeRateOpenFeign {
@GetMapping
ExchangeRateResponse call(
@RequestHeader String apiKey,
@RequestParam Currency source,
@RequestParam Currency currencies);
}
이렇게 짧게 줄일 수 있다.
하지만 단점 또한 있다.
공식적으로 reactive 모델을 지원하지 않고
테스트 도구를 제공하지 않는다.
그 외에도 여러 단점이 있지만 모두 추가 설정을 통해 보완이 가능하긴 하다.