개발/BE

Spring에서 외부 API 호출하기(WebClient 사용)

kkap999 2024. 3. 12. 23:59
728x90

이번에 클라이언트와 내부 서버들의 통신을 위한 중간 서버를 만들기 위해 Spring기반의 서버 내부에서 타 서버로의 API 호출이 필요했다.

가장 최근에 도입된 WebClient 활용
그거 말고는 아래 방식들이 있음

1. HttpURLConnection
스프링이나 외부 라이브러리가 아닌 Java 표준에서 제공하는 HTTP Connection 클래스이다.
URLConnection을 상속받고있음
옛날옛적에 사용하던거

2. RestTemplate
Spring 3.0부터 도입된 클래스로, 동기적인 HTTP 통신을 위해 쓰인다.

3. WebClient
비동기/논블로킹 방식을 지원
리액티브 프로그래밍이 가능하며 데이터 스트림을 효과적으로 처리할 수 있음 등등...

 

프로젝트 구조는 다음과 같이 잡아보았다

common
   ㄴ webclient
       ㄴ DataProviderClient : webClient에서 HTTP 요청 시 사용할 기본 템플릿. 이 객체를 상속받아 다른 외부 객체들을 구현한다.
   ㄴ config
       ㄴ WebClientConfig : 각각의 외부 상속 객체들의 빈을 관리해줄 Config 클래스

Config에 등록된 빈들을 서비스 레이어에서 불러와서 사용한다.

chatGPT와 karlo API를 요청하기 위한 객체만,, 코드 예시

DataProviderClient

import lombok.*;
import org.springframework.http.*;
import org.springframework.util.*;
import org.springframework.web.reactive.function.*;
import org.springframework.web.reactive.function.client.*;
import org.springframework.web.util.*;
import reactor.core.publisher.*;

import java.net.*;

@Data
@AllArgsConstructor
public class DataProviderClient {

    private final WebClient.Builder webClientBuilder;
    private final String BASE_URL;

    public <T> Mono<ResponseEntity<T>> post(String request_url,
                                            String mediaType,
                                            Object requestBody,
                                            Class<T> responseType) {
        return webClientBuilder.build()
                .method(HttpMethod.POST)
                .uri(getWebClientUri(request_url))
                .header(HttpHeaders.CONTENT_TYPE, mediaType)
                .body(BodyInserters.fromValue(requestBody))
                .retrieve()
                .toEntity(responseType);
    }

    public <T> Mono<ResponseEntity<T>> get(String request_url, Class<T> responseType) {
        return webClientBuilder.build()
                .method(HttpMethod.GET)
                .uri(getWebClientUri(request_url))
                .retrieve()
                .toEntity(responseType);
    }

    // url
    public URI getWebClientUri(String request_url) {
        return new DefaultUriBuilderFactory(BASE_URL)
                .uriString(request_url)
                .build();
    }

    // url + port
    public URI getWebClientUri(String request_url, int port) {
        return new DefaultUriBuilderFactory(BASE_URL)
                .uriString(request_url)
                .port(port)
                .build();
    }

    // url + port + query
    public URI getWebClientUri(String request_url, int port, MultiValueMap<String, String> queryParams) {
        return new DefaultUriBuilderFactory(BASE_URL)
                .uriString(request_url)
                .port(port)
                .queryParams(queryParams)
                .build();
    }
}

 

WebClientConfig

import org.springframework.beans.factory.annotation.*;
import org.springframework.context.annotation.*;
import org.springframework.web.reactive.function.client.*;

@Configuration
public class WebClientConfig {
    // KARLO property
    @Value("${model-server.karlo.url}")
    String KARLO_BASE_URL;
    @Value("${model-server.karlo.api-key}")
    String KARLO_API_KEY;
    @Value("${model-server.gpt.url}")
    String GPT_BASE_URL;
    @Value("${model-server.gpt.api-key}")
    String GPT_API_KEY;

    WebClient.Builder webClientBuilder() {
        return WebClient.builder()
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)).build());
    }

    @Bean
    KarloClient karloClient() {
        return new KarloClient(webClientBuilder().defaultHeader("Authorization", "KakaoAK " + KARLO_API_KEY), KARLO_BASE_URL);
    }

    @Bean
    GptClient gptClient() {
        return new GptClient(webClientBuilder().defaultHeader("Authorization", "Bearer " + GPT_API_KEY), GPT_BASE_URL);
    }
}

GPTClient

import org.springframework.web.reactive.function.client.*;

public class GptClient extends DataProviderClient {

    public GptClient(WebClient.Builder webClientBuilder, String BASE_URL) {
        super(webClientBuilder, BASE_URL);
    }
}

URI에 쿼리스트링이 들어가야 하는 친구가 있어서 DataProviderClient의 uri 구성하는 메서드를 오버로딩한건데 구현할때는 이렇게 해주었다.

ExampleClient

    public class ExampleClient extends DataProviderClient {
        private ExampleClient(WebClient.Builder webClientBuilder, String BASE_URL) {
            super(webClientBuilder, BASE_URL);
        }

        @Override
        public URI getWebClientUri(String request_url) {
            return super.getWebClientUri(request_url, PORT, {MultiValueMap 객체});
        }
    }

 

코드 재사용을 줄이고, SOLID 원칙을 최대한 준수할 수 있도록 짜보고싶었는데 잘 됐는지 모르겠다.
역시 코드 구조 짜는건 참 어려운 것 같다.
아키텍처 공부좀 열심히 해야겠다.