프로젝트에서 필요한 기능 중 ChatGPT와의 연동을 해야하는 기능이 있었다.
어떻게 구현해야할까 고민하던 중, 가장 보편적으로 사용하는 RestTemplate으로 구현해야겠다고 생각했다.(사실 내가 알던 유일한 방식이었다)
구현을 하면서 테스트를 해보니 잘 작동해서 이렇게 가면 되겠다 싶었으나, 내가 생각하는 멘토이자 존경하는 개발자인 분에게 이러이러한 방식으로 구현 중이다고 하니 다른 방식을 고민해보라고 했다.
잘 되는데 왜?라는 생각이 1초 정도 들었으나, 여러 가지 방법이 있다고 한번 공부해보라고 하셔서 다른 방법은 어떤게 있는지 궁금해졌다. 그래서 다른 방식에 대해 스터디한 내용을 정리해보고자 한다.
서치를 해보니 클라이언트단에서 외부 API를 호출하는 것 보다, 스프링에서 외부 API를 호출하는 이유가 있다고 한다.
클라이언트단에서 외부 API를 호출하면 CORS 오류를 회피하기 위해 프록시 서버가 필요한 경우 스프링 서버가 프록시 서버의 역할을 해줘야 하기 때문에 스프링에서 외부 API를 대신 호출해줘야 한다고 한다.
1. RestTemplate
스프링 3에서 추가된 외부 API 연동 방법 중 가장 간단하고, Spring이 간편하게 Rest 방식으로 API를 호출할 수 있는 Spring 내장 클래스이다.
Apache의 HttpClient를 추상화해서 제공한다.
<특징>
- Multi-Thread, Blocking 방식을 사용
- Restful 형식에 맞추어진 템플릿
- Header, Content-Type등 설정하여 외부 API 호출
- Http 요청 후 json, xml, String 같은 응답을 받을 수 있음
<장점>
- 사용하기 편하고 직관적이다.
<단점>
- 동기적인 HTTP 요청을 하기 때문에 성능에 영향을 미칠 수 있음
- Connection Pool을 사용하지 않기 때문에 연결할 때마다 TCP Connection을 시도. 스레드가 다 차는 경우, 가용 가능한 스레드 수가 줄어들어 병목 현상이 나타나 서비스 성능을 저하시킬 수 있음
- Deprecate되었다는 이야기가 있는데, 어떤 곳에서는 되었다고도 하고 어떤 곳에서는 아니라고도 한다.. 일단 IntelliJ는 Deprecate되었다고 나오니 사용하기 껄끄럽다.
<사용 예시>
스프링 의존성만 추가해주면 된다.
implementation 'org.springframework.boot:spring-boot-starter-web'
public class RestTemplateEx {
public MyResponse post(
String requestUrl,
MyRequest requestBody) {
RestTemplate restemplate = 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();
}
}
1. Header 만들고,
2. MediaType 만들어서 Header에 넣어주고,
3. Request 만들고,
4. RestTemplate에 url, request와 response 클래스를 넣어주고,
5. response를 반환한다.
2. WebClient
스프링 5에서 추가된 인터페이스로 Single-thread, Non-Blocking 방식을 사용한다.
<장점>
- HttpInterface라는 강력한 도구와 함께 사용할 수 있다.
- Reactive 프로그래밍이 가능하며 데이터 스트림을 효과적으로 처리 가능하기 때문에 높은 처리량과 확장성을 확보할 수 있다.
<단점>
- WebFlux 의존성을 설치해야한다.
- WebClient를 잘 사용하려면 WebFlux에 대한 이해도가 필요하다.(학습 곡선 존재)
<사용 예시>
WebFlux 의존성을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
public class WebClientEx {
public MyResponse post(
String requestUri,
MyRequest requestBody) {
WebClient webClient = WebClient.builder()
.baseUrl(requestUri)
.defaultHeader(HttpHeader.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();
}
}
메소드 체이닝만으로도 WebClient를 사용할 수 있다.
또한 WebClient의 Exception Handling도 훨씬 간편하게 할 수 있었다.
1. WebClient를 빌더로 생성하는데, (빌더 외에, 정적 팩토리 메서드 방식도 지원한다고 한다.)
2. uri와 header, statusHandler를 넣어준다.
2-1. Exception을 처리할 수 있도록 설정한다.
3. response를 생성하는데,
4. Webclient에 uri, requestbody를 넣고, .retrieve()를 통해 어떻게 결과를 추출할 것인지 정한다.
5. response의 결과를 받을 수 있게 넣어주고,
6. 기본적으로 비동기로 작동하는 WebClient가 동기로 작동할 수 있게 .block()을 넣는다.
3. RestClient
스프링 6.1.2 버전에서 추가된 WebFlux 의존성 없이 사용할 수 있는 기능이다.
Webclient와는 비슷하지만, 동기식으로 동작된다.
스프링 6.0 버전에 출시된 HttpInterface는 강력한 도구지만, 해당 기능을 사용하기 위해서는 WebClient를 사용해야했고, WebFlux 의존성을 설치해야했다. 하지만 RestClient는 WebFlux의 의존성 없이 사용할 수 있다.
RestClient와 WebClient의 기능이 비슷하지만 서버를 리액티브 프로그래밍으로 만들지 않았다면 RestClient를 사용하는 것이 좋다고 한다.
RestClient를 사용하면 WebFlux 의존성을 제거할 수 있다는 점에서 좋다. (의존성으로 인한 속도의 차이는 거의 없다고 한다.)
WebClient를 사용할 때는 Mono, Flux 등의 Reactor 객체에 대한 이해도가 있어야 하지만, RestClient는 동기식 Http 호출 도구이기 때문에 WebClient에 비해 사용하기 쉽다.
+ 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'
public interface MyHttpInterface {
@PostExchange("/request")
MyResponse request(
@RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
@RequestBody MyRequest request);
}
외부 REST API의 스펙에 맞게 인터페이스 작성한다.
Controller처럼 @PathVariable, @RequestBody 등 어노테이션을 활용해 필요한 정보를 담아 보낼 수 있다.
@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를 동적 프록시를 생성해주는 Config를 작성한다. HttpInterface를 Bean으로 등록해준다.
@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;
}
}
서비스단에 HttpInterface를 주입받으면 동적 프록시로 생성된 객체를 사용할 수 있다.
4. OpenFeign
Netflix에서 개발된 Http client binder.
선언적 웹서비스 클라이언트.
인터페이스에 어노테이션들만 붙혀주면 구현이 끝난다.(Spring Data JPA와 유사)
<장점>
- 인터페이스, 어노테이션 기반으로 코드 작성이 쉽다.
- REST API를 사용하는데 필요한 설정이 간편해진다.(개발자는 비즈니스 로직에 더 집중할 수 있다)
- 학습하기 쉽다
- 다른 Spring Cloud 기술들과 통합이 쉽다.
<단점>
- Spring Cloud 의존성이 추가된다.
- HttpClient가 Http2를 지원하지 않는다.(Http Client에 대한 추가 설정이 필요하다)
- 공식적으로 Reactive 모델을 지원하지 않는다.
- 테스트 도구를 제공하지 않는다.
RestTemplate으로 API 호출시, API 호출이 잦은 MSA시대에선 코드를 반복하는 일이 자주 발생하고 번거로운 일이다.(boiler code)
@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;
}
}
이를 OpenFeign으로 바꿔보면, 다음과 같이 코드양이 확 줄어든다.
@FeignClient(name = "ExchangeRateOpenFeign", url = "${exchange.currency.api.uri}")
public interface ExchangeRateOpenFeign {
@GetMapping
ExchangeRateResponse call(
@RequestHeader String apiKey,
@RequestParam Currency source,
@RequestParam Currency currencies);
}
나는 이중 OpenFeign을 이용해 OpenAI의 API를 연동하려고 한다. 그 이유는
1. 사용자가 그리 많지 않을 것으로 예상되어 아직 비동기 방식의 필요성을 느끼지 못한다.
2. 기한 내에 개발을 위해서 비교적 적용이 쉬운 방법이다.
다음 글은 OpenFeign을 이용한 ChatGPT와 우리 서비스의 실제 연동 코드를 리뷰해보겠다.
출처:
https://myvelop.tistory.com/217
https://jie0025.tistory.com/531
'개발 활동 > 스위프' 카테고리의 다른 글
외부 API 연동 - WireMock으로 테스트하기 (0) | 2024.05.31 |
---|---|
외부 API 연동 - 외부 API에 문제가 있으면? Circuit Breaker Pattern & Resilience4j (0) | 2024.05.30 |
외부 API 연동하기 - OpenFeign을 이용한 ChatGPT 연동 (0) | 2024.05.30 |
[Test] 애플리케이션 테스트 방법 (0) | 2024.04.29 |
AWS Elastic Beanstalk로 간단 배포 (0) | 2024.04.22 |