애플리케이션 외부 API 호출
현업에서 외부 API를 호출해야하는 일이 많다. 다른 회사의 서비스(휴대전화 인증, 결제시스템)를 이용할 때 필수적이다. 물론 클라이언트 단에서 외부 API를 호출한다면 스프링 서버에서 API를 호출할 일이 없겠지만, CORS 오류를 회피하기 위해 프록시 서버가 필요한 경우 스프링 서버가 프록시 서버의 역할을 해줘야 하기 때문에 스프링에서 외부 API를 대신 호출해줘야 한다.
또한 서버를 서비스 단위로 나눠 배포하는 경우, 내부 서버 컨테이너끼리 데이터를 교환해야할 경우가 생기는데 이럴 때 RabbitMQ와 같은 메시지 브로커를 사용해 데이터를 전달할 수 있지만, 컨테이너끼리 API를 호출을 하는 방식을 사용할 수 있다.
자바나 스프링에서는 HTTP 요청을 보내기 위한 다양한 도구를 지원한다. 자바에서 기본적으로 HttpURLConnection/URLConnection라는 Http Connection을 맺고 끊는 방식의 API를 제공하고 있지만 여러 단점(추상화 레벨이 낮다는 점, 동기적이라는 점)을 가지고 있기 때문에 개발자들은 더 나은 방식을 찾고자 했다. 자연스럽게 자바와 스프링이 버전이 올라감과 동시에 외부 API를 호출하는 방법도 발전했다.
외부 API를 호출하는 방법
1. HttpURLConnection/URLConnection
- HTTP 통신을 가능하게 해주는 클래스로 자바에서 제공하는 기본적인 API이다. (순수 자바로 HTTP 통신)
- URL을 이용해 외부 API에 연결하고 데이터를 전송한다.
- 데이터의 타입/길이에 거의 제한이 없다.
- 오래된 자바 버전에서 사용하는 클래스다. 즉, 동기적 통신을 기본으로 사용한다. 동기적이므로 요청을 보내고 응답을 받을 때까지 스레드가 대기 상태라는 점에서 성능에 안 좋은 영향을 미칠 수 있다.
- URLConnection은 상대적으로 저수준 API에 해당하기 때문에 기본적인 요청/응답 기능이 있지만 추가적인 기능들을 직접 구현해야 한다는 불편함도 존재한다.
HttpURLConnection 사용 예시
- 자바에서 기본으로 제공하는 API이기 때문에 따로 의존성을 추가할 필요가 없다.
- POST를 호출하는 코드 예시이다. http & https부터 Header 및 Method 설정과 외부 API와의 통신과 결과값 받기 그리고 그 결과값에 따른 반환값 및 예외 처리까지 전부 사용자가 처리해줘야 한다.
@Component
@RequiredArgsConstructor
public class HttpURLConnectionEx {
private final ObjectMapper objectMapper;
public String post(
String requestUrl,
Map<String, String> headers,
Object body) {
try {
URL obj = new URL(requestUrl);
HttpURLConnection con = requestUrl.startsWith("https") ? (HttpsURLConnection) obj.openConnection() : (HttpURLConnection) obj.openConnection();
con.setRequestProperty("charset", "utf-8");
for(Map.Entry<String, String> header : headers.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
con.setRequestMethod(HttpMethod.POST.name());
con.setDoOutput(true);
String bodyStr = objectMapper.writeValueAsString(body);
con.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8));
con.connect();
int resCode = con.getResponseCode();
if (resCode != 200) {
con.disconnect();
throw new MyHttpFailRuntimeException();
}
BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "euc-kr"));
StringBuffer response = new StringBuffer();
String inputLine;
while ((inputLine = br.readLine()) != null) {
response.append(inputLine);
}
return response.toString();
} catch(IOException e) {
throw new MyException();
} catch(MyHttpFailRuntimeException e) {
throw e;
}
}
}
2. Apache HttpClient
- HTTP 프로토콜을 쉽게 사용할 수 있게 도와주는 Apache HTTP 컴포넌트이다. 객체 생성이 쉽다는 장점이 있다.
- URLConnection 방식보다 코드가 간결해 졌지만, 반복적이고 코드가 길고 응답의 컨텐츠 타입에 따라 별도의 로직이 필요하다는 단점이 존재한다.
- HttpClient5부터는 CloseableHttpAsyncClient를 사용해 비동기 통신이 가능해졌다.
코드 예시
- 아래 의존성을 추가해주자.
implementation 'org.apache.httpcomponents.client5:httpclient5:5.3'
- HttpURLConnection/URLConnection보다 추상화 레벨이 높기 때문에 보다 다루기 쉬운 편이다.
@Component
@RequiredArgsConstructor
public class ApacheHttpClientEx {
private final ObjectMapper objectMapper;
public MyResponse post(
String requestUri,
MyRequest requestBody) {
try {
HttpPost httpPost = new HttpPost(requestUri);
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
HttpEntity entity = new StringEntity(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8);
httpPost.setEntity(entity);
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
CloseableHttpResponse response = httpClient.execute(httpPost);
if (response.getCode() != 200) {
throw new MyException();
}
BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler();
String body = handler.handleResponse(response);
MyResponse myResponse = objectMapper.readValue(body, MyResponse.class);
return myResponse;
} catch (IOException e) {
throw new MyException(e);
}
}
}
3. Java11 버전부터 출시된 Java.net.http의 HttpClient
- 위에서 소개한 apache의 HttpClient와는 다른 객체이다.
- java.net.http 패키지의 HttpClient는 자바 11 버전에 포함되어 있기 때문에 따로 의존성이 필요하지 않다.
- 비동기 통신을 사용할 수 있다는 장점이 있다.
HttpClient 사용 예시
- Java 11에서 포함하고 있는 API이기 때문에 따로 의존성을 추가할 필요가 없다.
- 아래 예시는 send 메소드를 사용해 동기적 통신을 하고 있지만 sendAsync 메소드를 사용해 비동기 통신을 할 수 있다.
@Component
@RequiredArgsConstructor
public class HttpClientEx {
private final ObjectMapper objectMapper;
public MyResponse post(
String requestUri,
MyRequest requestBody) {
try {
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUri))
.timeout(Duration.ofSeconds(30))
.setHeader("Content-Type", "application/json;charset=UTF-8")
.POST(BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody)))
.build();
HttpResponse<String> responseStr = client.send(request, BodyHandlers.ofString());
return objectMapper.convertValue(responseStr.body(), MyResponse.class);
} catch (IOException e) {
throw new MyException(e);
} catch (InterruptedException e) {
throw new MyException(e);
}
}
}
4. RestTemplate
- 스프링에서 제공하는 HTTP 통신 템플릿으로 스프링 3.0에서 추가됐다.
- Apache의 HttpClient를 추상화해서 제공한다. ClientHttpRequestFactory에 HttpClient를 넘겨서 활용할 수 있다.
- 다른 API를 호출할 때 RestTemplate를 사용해 호출한다.
- JSON, XML, String과 같은 응답을 받을 수 있다.
- Blocking 기반의 동기 방식 사용한다.
- HTTP 서버와의 통신을 단순화하고 RESTful 원칙 고수
- header, content-type등을 설정하며 외부 API를 호출
- 사용하기 편하고 직관적이라는 장점이 있다.
- 동기적인 HTTP 요청을 하기 때문에 성능에 영향을 미칠 수 있다는 점, Connection Pool을 사용하지 않기 때문에 연결할 때마다 로컬 포트를 열고 TCP Connection을 시도한다는 특징으로 인해 해당 로직을 따로 설정해줘야한다는 불편함 등의 단점이 존재한다.
RestTemplate의 동작 원리
- RestTemplate의 동작 원리는 아래와 같다.
- 애플리케이션이에서 API를 호출하기 위해 RestTemplate을 호출한다.
- HttpMessageConverter를 사용해 Object를 RequestBody에 담을 수 있는 형태로 변환한다.
- ClientHttpRequestFactory를 통해 ClientHttpRequest를 받아와 요청을 보낸다.
- ClientHttpRequest가 요청메시지를 만들어 HTTP 프로토콜을 통해 REST API를 호출한다.
- ResponseErrorHandler를 사용해 오류가 확인되면 RestTemplate에서 오류 로직을 실행한다.
- ResponseErrorHandler에 오류가 확인되면 ClientHttpResponse에서 응답데이터를 가져와 처리한다.
- HttpMessageConverter가 문자열로 되어 있는 응답메시지를 Object로 변환해준다.
- 애플리케이션은 자바 Object를 반환받는다.
RestTemplate 사용 예시
- RestTemplate을 사용하려면 스프링 의존성이 필요하다. (RestTemplate에서 Apache의 HttpClient의 기능을 활용하고 싶다면 Apache HttpClient 의존성을 따로 추가해주자.)
implementation 'org.springframework.boot:spring-boot-starter-web'
- RestTemplate를 사용하면 ObjectMapper를 주입받아 요청과 응답에 사용될 Java Object를 직접 변환해줄 필요가 없다.
public class RestTemplateEx {
public MyResponse post(
String requestUrl,
MyRequest requestBody) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-8"));
headers.setContentType(mediaType);
HttpEntity<MyRequest> requestHttpEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<MyResponse> response = restTemplate.postForEntity(requestUrl, requestHttpEntity, MyResponse.class);
return response.getBody();
}
}
5. WebClient
- 스프링 5.0부터 도입된 라이브러리이다.
- 비동기/논블로킹 방식으로 외부 API 호출 가능하다. 무엇보다 WebClient의 장점은 HttpInterface라는 강력한 도구와 함께 사용할 수 있다는 점이다.
- 리액티브 프로그래밍이 가능하며 데이터 스트림을 효과적으로 처리 가능하기 때문에 높은 처리량과 확장성을 확보할 수 있다.
- 대신 WebFlux 의존성을 설치해야하고, WebClient를 잘 사용하려면 WebFlux에 대한 이해도가 필요(진입 장벽이 높다)하다는 단점이 존재한다.
코드 예시
- 먼저 WebFlux 의존성을 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
- 메소드 체이닝만으로도 WebClient를 생성하거나 요청을 보낼 수 있다. 또한 WebClient의 Exception Handling도 훨씬 간편하게 할 수 있었다. 이와 같은 장점 때문에 이전의 다른 API 호출 방식보다 사용하기 수월했다.
public class WebClientEx {
public MyResponse post(
String requestUri,
MyRequest requestBody) {
WebClient webClient = WebClient.builder()
.baseUrl(requestUri)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultStatusHandler(HttpStatusCode::is4xxClientError, response -> {
throw new MyException();
}).build();
ResponseEntity<MyResponse> response = webClient.post()
.uri("/request")
.bodyValue(requestBody)
.retrieve()
.toEntity(MyResponse.class)
.block();
return response.getBody();
}
}
6. HttpInterface
- 스프링 6.0 버전 이후부터 사용할 수 있는 HTTP 통신 기능이다.
- 각 API 자원에 대한 인터페이스를 작성하고 ProxyFactory를 사용하면 인터페이스와 WebClient 혹은 RestClient를 통해 동적 프록시를 생성한다. 이를 Bean으로 등록해주면 서비스 단에서 인터페이스를 주입받아 메소드 하나로 API 호출을 쉽게 할 수 있다.
- @RequestHeader, @RequestBody 등의 어노테이션을 사용해 요청에 필요한 정보를 매개변수로 전달할 수 있다.
- WebClient가 있어야만 사용할 수 있어서 WebFlux 의존성이 필요했지만 스프링 6.1.2 이후로 RestClient라는 대안이 생겼다.
코드 예시
- 먼저 WebFlux 의존성을 추가해주자
implementation 'org.springframework.boot:spring-boot-starter-webflux'
- 외부 REST API의 스펙에 맞게 인터페이스를 작성해준다.
- 주목할 점은 Controller처럼 @PathVariable, @RequestBody 등의 어노테이션을 활용해 필요한 정보를 담아 보낼 수 있다는 점이다. RequestBody의 경우, Java Object를 그대로 내보내도 직렬화를 자동으로 해준다. 정말 편리하다.
public interface MyHttpInterface {
@PostExchange("/request")
MyResponse request(
@RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
@RequestBody MyRequest request);
}
- HttpInterface를 동적 프록시를 생성해주는 Config를 작성하자. HttpInterface를 Bean으로 등록해준다.
@Configuration
public class HttpInterfaceConfig {
@Bean
public MyHttpInterface myHttpInterface() {
WebClient webClient = WebClient.builder()
.baseUrl("uri")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultStatusHandler(HttpStatusCode::is4xxClientError, response -> {
throw new MyException();
}).build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient)).build();
return factory.createClient(MyHttpInterface.class);
}
}
- 이제 서비스 단에 HttpInterface를 주입받으면 동적 프록시로 생성된 객체를 사용할 수 있다.
@Service
@RequiredArgsConstructor
public class MyService {
private final MyHttpInterface myHttpInterface;
public MyResponse request(MyRequest request) {
MyResponse response = myHttpInterface.request(MediaType.APPLICATION_JSON_VALUE, request);
return response;
}
}
Spring 6.1.2 버전에 출시된 RestClient
RestClient란?
- Spring 6.1.2 버전부터 WebFlux 의존성 없이 사용할 수 있는 RestClient 기능이 출시되었다.
- WebClient와 유사한 HTTP 요청을 위한 객체이다. WebClient와는 다르게 동기식으로 동작되지만 사용방식은 WebClient와 거의 유사하다.
- 스프링 6.0 버전에 출시된 HttpInterface는 강력한 도구지만 해당 기능을 사용하기 위해 WebClient를 사용해야했고, WebFlux 의존성을 설치해야만 했지만 RestClient로 대체할 수 있다.
토스 페이먼츠 실전 예시
- 아래 코드는 토스페이먼츠의 환불 REST API를 호출하는 예시이다.
HttpInterface
- TossPayments HttpInterface이다. HttpServiceProxyFactory를 통해 RestClient를 달아주고 IoC 컨테이너에 Bean으로 등록해주면 동적 프록시를 통해 구현체를 자동으로 만들어준다.
- @RequestHeader, @PathVariable, @RequestBody 등의 어노테이션을 통해 요청에 필요한 정보를 담는다.
public interface TossPaymentsClient {
@PostExchange("/v1/payments/{paymentsKey}/cancel")
RefundPaymentResponse refund(
@RequestHeader(HttpHeaders.AUTHORIZATION) String secretKey,
@RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
@PathVariable String paymentsKey,
@RequestBody RefundPaymentRequest request);
}
HttpConfig
- Factory Interface
public interface HttpInterfaceFactory {
<S> S create(Class<S> clientClass, RestClient restClient);
}
- Factory 구현체이다. Config에서 HttpInterface를 쉽게 선언하기 위해 만든 것이다.
public class SimpleHttpInterfaceFactory implements HttpInterfaceFactory {
public <S> S create(Class<S> clientClass, RestClient restClient) {
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(clientClass);
}
}
- HttpConfig 코드이다. RestClient를 만들고 HttpInterface에 부여해 Bean으로 등록하는 코드이다.
- RestClient의 builder를 사용해 RestClient를 구성할 수 있다. RestClient에 TossPayments의 REST API URL을 baseUrl로 부여한다. 또한, defaultStatusHandler를 사용해 에러 핸들링을 할 수 있다.
@Slf4j
@Configuration
public class HttpInterfaceConfig {
private final String tossPaymentsUrl;
private final HttpInterfaceFactory httpInterfaceFactory;
public HttpInterfaceConfig(@Value("${external-api.toss.url}") String tossPaymentsUrl) {
this.httpInterfaceFactory = new SimpleHttpInterfaceFactory();
this.tossPaymentsUrl = tossPaymentsUrl;
}
@Bean
public TossPaymentsClient tossPaymentsClient() {
return httpInterfaceFactory.create(TossPaymentsClient.class, createRestClient(tossPaymentsUrl));
}
private RestClient createRestClient(String baseUrl) {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultStatusHandler(
HttpStatusCode::is4xxClientError,
(request, response) -> {
log.error("Client Error Code={}", response.getStatusCode());
log.error("Client Error Message={}", new String(response.getBody().readAllBytes()));
})
.defaultStatusHandler(
HttpStatusCode::is5xxServerError,
(request, response) -> {
log.error("Server Error Code={}", response.getStatusCode());
log.error("Server Error Message={}", new String(response.getBody().readAllBytes()));
})
.build();
}
}
Service
- 동적 프록시로 생성된 HttpInterface를 서비스 코드에서 주입받는다. 이제 비즈니스 로직에서 해당 서비스 메소드를 호출하면 REST API가 호출된다.
- 참고로 TossPaymentsUtils의 encodeAuthorizationByBase64 메소드는 시크릿키를 Base64로 변환해주는 로직이다. 자세한 내용은 토스 페이먼츠 연동 공식 문서를 참고하길 바란다.
@Service
public class TossPaymentsService {
private final String clientKey;
private final String secretKey;
private final TossPaymentsClient tossPaymentsClient;
public TossPaymentsService(
@Value("${external-api.toss.client-key}") String clientKey,
@Value("${external-api.toss.secret-key}") String secretKey,
TossPaymentsClient tossPaymentsClient) {
this.clientKey = clientKey;
this.secretKey = secretKey + ":";
this.tossPaymentsClient = tossPaymentsClient;
}
public void refundPayment(OrderPayment orderPayment) {
tossPaymentsClient.refund(
TossPaymentsUtils.encodeAuthorizationByBase64(secretKey),
TossPaymentsUtils.applicationJsonAndUtf8Set(),
orderPayment.getPaymentsKey(),
RefundPaymentRequest.of(orderPayment.getPayAmount().abs()));
}
}
결론
WebClient 또는 RestClient와 HttpInterface를 결합해 사용하면 RestTemplate, HttpClient, HttpURLConnection, URLConnection 등의 예전 기술과 비교해봤을 때 추상화 수준이 높기 때문에 매우 편리하다고 느꼈다. 코드 가독성도 훨씬 좋고 생산성도 높아진다. 외부 API를 호출하는 로직이 있다면 HttpInterface를 사용하는 것을 강력 추천하고 싶다.
RestClient와 WebClient의 기능이 비슷하지만 서버를 리액티브 프로그래밍으로 만들지 않았다면 아래의 이유 때문에 RestClient를 사용하는 것이 좋다고 생각한다.
- RestClient를 사용하면 WebFlux 의존성을 제거할 수 있다는 점에서 좋다. (막상 젠킨스에서 빌드를 여러차례 돌려봤으나 속도의 차이를 못 느낄 정도였다. WebFlux 의존성이 그렇게 무거운 의존성은 아니기 때문이다.)
- WebClient를 사용할 때는 Mono, Flux 등의 Reactor 객체에 대한 이해도가 있어야 하지만 RestClient는 동기식 Http 호출 도구이기 때문에 WebClient에 비해 사용하기 쉽다.
신규 프로젝트를 구성하거나 오래되지 않은 프로젝트를 진행하고 있다면, RestClient를 사용하기 위해 Spring Boot 버전을 올리는 것도 괜찮다고 생각한다. 하지만 기존 프로젝트에서 RestClient를 사용하기 위해 버전을 올려야 하고, 버전을 올렸을 때 프로젝트에 영향이 가는 상황이라면 WebClient + HttpInterface나 RestTemplate을 사용하거나 원래 사용하던 기술을 그대로 가져가는 것이 오히려 더 좋은 방법이라고 생각한다.
관련글
참고자료
- HttpURLConnection 통신: https://chwan.tistory.com/entry/HttpURLConnection-POST-%ED%86%B5%EC%8B%A0
- Apache HttpClient: https://linked2ev.github.io/java/2020/03/09/JAVA-3.-Apache-httpclient-Http-API-Request/
- nGrinder에 적용한 HttpCore5와 HttpClient5 살펴보기: https://d2.naver.com/helloworld/0881672
- Java11에 정식으로 추가된 HttpClient: https://brush-up.github.io/java/java11-http-client/
- HttpClient를 사용해 웹사이트 요청보내기: 링크
- RestTemplate 정의, 특징, 동작원리: https://sjh836.tistory.com/141
- Spring RestTemplate: https://dejavuhyo.github.io/posts/spring-resttemplate/
- HttpInterface: https://mangkyu.tistory.com/291
- Spring 6.0의 HTTPInterface 끄적이기: https://medium.com/@auburn0820/spring-6-0%EC%9D%98-httpinterface-%EB%81%84%EC%A0%81%EC%9D%B4%EA%B8%B0-f3653143a373
- RestClient 공식문서: https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
- RestClient: https://www.baeldung.com/spring-boot-restclient
- [Spring] Spring Boot3.2에 새롭게 추가될 RestClient: https://mangkyu.tistory.com/303