ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WebClient 와 ChatClient 의 관계
    웹 개발 2024. 12. 19. 12:20
    반응형

    0. 문제 발생

    타른 서버와의 API 통신을 위해 WebClient 를 사용하는 과정에서 API 통신이 안되는 반복되는 현상으로 정상적인 서비스가 되지 않았다..

    내부 API 통신 실패!!
    로만 확인이 되는 상황에서
    이유를 알고 싶어 여러 로그를 찍어보다가
    API 통신이 요청을 보내는 Header 에

    토큰 값으로 sk-proj…. 와 같은 토큰도 아닌 요상한게 들어가있었다..

    이게 골치였는데..
    토큰 값으로 해당 값이 세팅되어있으니 인증시에 계속.. 에러가 발생..
    해당 값의 출처는

    openai 를 사용하기 위한 api-key 였고, key 가 세팅되어 넘어갔었다.
    서버간 api 통신을 위해 webClient 사용을 하는데 bear auth 에 엉뚱한 api-key 가 세팅이 되니 통신 장애가 계속 발생되는 상황……

     


     

    관계를 알기 위해서 spring ai 라이브러리를 하나하나 다 확인해본 후 내용 공유를 합니다.

    먼저 간략하게

    **ChatClientDefaultChatClientStreamingChatModelOpenAiChatModelOpenAiApiWebClient**

    의 연결 구조를 이해하여야 한다.

     

    의 연결 구조를 이해하여야 한다.

    1. 각 구성요소와 역할

    1. ChatClient (인터페이스)
      • 최상위 클라이언트 인터페이스로, AI 모델과의 대화를 수행하는 역할을 정의합니다.
      • 이 인터페이스는 다양한 구현체를 가질 수 있습니다.
    2. DefaultChatClient (ChatClient 구현체)
      • ChatModel 또는 StreamingChatModel 인터페이스를 사용하여 실제 AI 모델과 통신합니다.
      • call()stream() 메서드를 통해 동기/비동기 요청을 처리합니다.
    3. StreamingChatModel (인터페이스)
      • 비동기적으로 스트리밍 형태의 AI 응답을 반환하는 메서드를 정의합니다.
      • 핵심 메서드: Flux<ChatResponse> stream(Prompt prompt);
      • 구현체: OpenAiChatModel
    4. OpenAiChatModel (StreamingChatModel 구현체)
      • OpenAI API와 통신하는 역할을 수행합니다.
      • 내부적으로 OpenAiApi를 사용해 API를 호출하며 WebClient를 활용합니다.
    5. OpenAiApi
      • OpenAI와의 HTTP 통신을 담당합니다.
      • **WebClient**를 사용해 실제 HTTP 요청과 응답을 처리합니다.
    6. WebClient
      • Spring WebFlux에서 제공하는 비동기 HTTP 클라이언트입니다.
      • OpenAI API에 요청을 보내고 스트리밍 응답을 받는 데 사용됩니다.

    (출처 : 각 구성요소와 역할은 shout out to chatgpt)

     

    2. 흐름도 정리

    1. 사용자가 ChatClientprompt() 메서드를 호출합니다.

     

    2. prompt() 메서드 실행

    • ChatClientprompt()DefaultChatClientRequestSpec 객체를 생성합니다.
      • 이 객체는 Chat 요청 스펙을 설정합니다.

     

    3. call() 메서드 실행

    • call()DefaultCallResponseSpec 객체를 통해 채팅 응답을 가져옵니다.
    • 이 단계에서 doGetChatResponse()doGetObservableChatResponse() 메서드가 호출됩니다.

     

    4. Observation 설정

    • doGetObservableChatResponse() 메서드 내에서 Observation 설정이 이루어지고,
      **observation.observe()**가 실행됩니다.

     

    5. doGetChatResponse() 에서 AdvisedRequest (어드바이저 패턴 적용)

    • AdvisedRequest 객체가 생성되며 Advisor 패턴이 적용됩니다.
    • aroundAdvisorChainBuilder.build().nextAroundCall()를 통해
      어드바이저 체인의 다음 단계로 실행이 넘겨지고,
      요청이 처리됩니다.

     

    6. toAdvisedRequest() 호출

    • toAdvisedRequest() 내에서 chatModel 이 등록됩니다.

    7. 최종 chatModel 확인

    • 등록되는 chatModelStreamingChatModel 입니다.
      즉, StreamingChatModel이 최종적으로 사용됩니다.

    8. **DefaultChatClient**에서 **StreamingChatModel** 를 사용해 nextAroundCall()로 stream()을 호출합니다.

    9. **StreamingChatModel**을 구현한 **OpenAiChatModel**은 stream() 메서드 내부에서 **OpenAiApi**를 호출합니다.

    여기서 가장 궁금한!!!

    10. **OpenAiApi**는 **WebClient**를 사용해 OpenAI API에 HTTP 요청을 보냅니다.

     

    11. WebClient가 받은 응답을 OpenAiApi → OpenAiChatModel → DefaultChatClient를 거쳐 최종적으로 반환합니다.

     

     

    3. 흐름 요약

    1. DefaultChatClient → 사용자의 요청을 StreamingChatModel로 전달.
    2. OpenAiChatModelstream()을 통해 OpenAI API 호출 요청 준비.
    3. OpenAiApiWebClient를 사용해 OpenAI API에 HTTP 요청 전송 및 Flux 응답 반환.
    4. WebClient → OpenAI의 스트리밍 응답을 Flux 형태로 비동기 처리.
    5. 최종적으로 ChatResponse가 사용자에게 반환.

    4. 요약 도식

    User → ChatClient (DefaultChatClient)
           → StreamingChatModel (OpenAiChatModel)
               → OpenAiApi → WebClient → OpenAI API
                   ↳ Flux<ChatResponse> (Streamed Response)

     

    5. 결론

    현재 우리 프로젝트에서는

    //WebClientConfig.java
    
    @Configuration
    public class WebClientConfig {
        @Bean
        public WebClient webClient() {
            return webClientBuilder().build();
        }
    
        @Bean
        public WebClient.Builder webClientBuilder() {
            HttpClient client = createHttpClient();
            ExchangeStrategies strategies = getExchangeStrategies();
            var connector = new ReactorClientHttpConnector(client);
            return WebClient.builder()
                            .exchangeStrategies(strategies)
                            .clientConnector(connector);
        }
    
    ...
    }
    

    로 설정되어 WebClient.BuilderSpring Bean으로 싱글톤(singleton)으로 등록되고 있고, 이 WebClient.Builder를 기반으로 다양한 WebClient 인스턴스가 생성됩니다.

    openAI 사용을 위해

    //OpenAIConfig.java
    
    @Configuration
    @RequiredArgsConstructor
    public class OpenAiConfig {
    
        @Bean
        public ChatClient chatClient(ChatClient.Builder builder) {
            return builder.build();
        }
    }

    로 설정되어있고, ChatClient를 Bean으로 등록할 때 ChatClient.Builder를 사용합니다.

    해당 ChatClient 내부에서 OpenAiApi가 생성될 때, WebClient.Builder가 사용되며 헤더 설정(Authorization)을 추가합니다.

    WebClient.Builder는 상태를 가지는 객체입니다.

    Bean으로 등록된 WebClient.Builder싱글톤으로 관리되므로, 모든 클래스에서 동일한 인스턴스를 참조합니다.

    따라서, 특정 클래스에서 WebClient.Builder에 헤더나 다른 설정을 추가하면 이 설정이 WebClient.Builder 인스턴스에 반영되며, 이 빌더를 기반으로 새로 생성되는 모든 WebClient 인스턴스에 적용됩니다.

    OpenAiApi는 WebClient.Builder를 전달받아 WebClient를 생성하면서 Authorization 헤더를 추가합니다:

    미심쩍은 부분들이 있지만

    정리하면

    • WebClient.BuilderOpenAiApi에 전달된 이후 헤더 설정이 내부적으로 변했을 가능성이 있다.
    • WebClient.Builder 자체는 원칙적으로 불변 상태로 유지되어야 하지만,
      의도치 않게 상태가 공유되거나 커스터마이징이 적용되었을 경우 모든 build() 호출에서 해당 상태가 전파되었다.

    6. 해결방안

    WebClient 의 분리도 있을 수 있었지만, WebClient.Builder 를 빈으로 관리하여 설정이 덮어씌어지는걸 막는 것으로 해결하였다.

    WebClient.Builcer 는 상태를 유지하는 객체이고, Builder 에 설정된 상태를 기반으로 새로운 WebClient 를 생성하기 때문에 빈으로 관리하지 않는 것으로 생각하였다.

    //WebClientConfig.java
    
    @Configuration
    public class WebClientConfig {
        @Bean
        public WebClient webClient() {
            HttpClient client = createHttpClient();
            ExchangeStrategies strategies = getExchangeStrategies();
            var connector = new ReactorClientHttpConnector(client);
    
            WebClient.Builder builder = WebClient.builder()
                    .exchangeStrategies(strategies)
                    .clientConnector(connector);
    
            return builder.build();
        }
        ...
    }
    
    //OpenAiConfig.java
    
    @Configuration
    @RequiredArgsConstructor
    public class OpenAiConfig {
    
        @Value("${spring.ai.openai.api-key}")
        private String apiKey;
    
        @Bean
        public ChatClient chatClient() {
            ChatModel chatModel = new OpenAiChatModel(new OpenAiApi(apiKey));
            return ChatClient.builder(chatModel).build();
        }
    }
    
    

    아직 미심쩍고 의심되는 부분들이 더있지만.. 이후에 가설들을 검증해 나가며,, 더 내용을 개선해나가야겠다.

    반응형
Designed by Tistory.