[트러블슈팅] open ai api 통신 문제 디버깅 과정

#study

open ai api 를 통해서 원하는 프롬프트 응답 기능을 만들어 배포하던 도중 발생한 에러를 해결하게된 과정에 대해서 정리하였습니다.

배포 서버 환경

Oracle Cloud EC2 instance OCPU 개수 : 1 네트워크 대역폭(Gbps):0.48 메모리(GB): 1 메모리 스왑 4GB

Canonical Ubuntu 22.04 Caddy Web Server

문제 상황

OpenAI API를 SpringAI 프레임워크를 통해 연동하여, 수업 공유 시스템 프로젝트의 욕설 감지 기능을 구현을 완료하고 배포하다가 발생한 문제였다. Local 환경에서 postman 테스트에서는 문제없이 잘 동작하였다. open ai api까지 요청, 응답이 잘 이루어졌고 문제없이 boolean을 반환 받았다. 하지만, 배포 환경으로 올려 api 테스트를 시도하자 에러가 발생하여 정상적인 응답을 받지 못하고 500 상태 번호가 반환되었다.

에러 내용

image

: 으로 시작하는 ErrorMessage가 null인 것이다!

api 요청을 할때, 요청 객체 자체에서 에러가 발생한 것인데 막막하였다.

원인을 향한 여정

원인이 무엇인지 파악하기 힘들었다. Exception Message는 null로 반환되었고, 비슷한 POST 요청의 에러 로그들을 구글링해보니 대부분 timeout 에러를 반환받은 분들이 많았다. 하지만 나는 timeout은 에러로그에서도 찾지 못했고(심지어 나는 null 이다....) 나와 다른 상황이라고 판단하여 다른 사례들을 찾아보았지만 끝내 찾지 못했다..

그래서 직접 하나씩 살펴보기로 했다.

DefaultRestClient 에러 발생

	at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:502) ~[spring-web-6.1.17.jar!/:6.1.17]

try {
    uri = initUri();
    HttpHeaders headers = initHeaders();
    ClientHttpRequest clientRequest = createRequest(uri);
    clientRequest.getHeaders().addAll(headers);
    ClientRequestObservationContext observationContext = new ClientRequestObservationContext(clientRequest);
    observationContext.setUriTemplate(this.uriTemplate);
    observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(observationConvention,
    DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry).start();
    if (this.body != null) {
        this.body.writeTo(clientRequest);
    }
    if (this.httpRequestConsumer != null) {
        this.httpRequestConsumer.accept(clientRequest);
    }
    clientResponse = clientRequest.execute();
    observationContext.setResponse(clientResponse);
    ConvertibleClientHttpResponse convertibleWrapper = new DefaultConvertibleClientHttpResponse(clientResponse);

      return exchangeFunction.exchange(clientRequest, convertibleWrapper);
    } catch (IOException ex) {
      ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
      if (observation != null) {
        observation.error(resourceAccessException);
      }
      throw resourceAccessException;
    }
  1. 요청할 URI 와 HTTP 헤더 초기화
  2. 클라이언트 요청 객체를 생성, 헤더 추가
  3. 생성된 요청 객체 기반으로 관찰(context) 객체 생성, URI 템플릿 설정
  4. HTTP 클라이언트 요청 및 응답 메트릭 수집, 로깅 시작
  5. 요청 본문이 존재한다면, 클라이언트 요청에 작성
  6. 추가적인 요청 consumer 가 존재한다면 consumer를 적용

내부적으로 어떤 요청을 진행했었고, 그 요청 내에서 특정 응답을 open ai에서 받아왔는지 확인할 수는 없었다. 하지만 위 과정을 진행 도중 어디선가 IOException이 발생했다.

다음으로는 ChatGPT 에게 요청할떄 사용되어지는 객체가 추적되었다. 해당 객체의 동작구조를 살펴본다.

	at org.springframework.ai.openai.api.OpenAiApi.chatCompletionEntity(OpenAiApi.java:256) ~[spring-ai-openai-1.0.0-M6.jar!/:1.0.0-M6]

OpenAiApi 의 chatCompletionEntity
public ResponseEntity<ChatCompletion> chatCompletionEntity(ChatCompletionRequest chatRequest,
MultiValueMap<String, String> additionalHttpHeader) {

Assert.notNull(chatRequest, "The request body can not be null.");
Assert.isTrue(!chatRequest.stream(), "Request must set the stream property to false.");
Assert.notNull(additionalHttpHeader, "The additional HTTP headers can not be null.");

    return this.restClient.post()
      .uri(this.completionsPath)
      .headers(headers -> headers.addAll(additionalHttpHeader))
      .body(chatRequest)
      .retrieve()
      .toEntity(ChatCompletion.class);
}
  1. ChatCompletion 타입의 응답을 ResponseEntity 로 반환
  2. ChatRequest, additionalHttpHeader 가 null이 아님을 검증
  3. 요청의 stream 속성이 false 여야 한다는 조건을 확인
  • restClient를 사용하여 지정된 URI(completionPath) 로 POST 요청을 보냄
  • 추가 HTTP 헤더를 요청에 포함시키고, chatRequest를 본문으로 설정
  • 응답을 ChatCompletion 타입의 엔티티로 변환하여 반환

OpenAiChatModel

ResponseEntity<ChatCompletion> completionEntity = this.retryTemplate
					.execute(ctx -> this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));

chatCompletionEntity가 완전한 응답을 반환하기 전에 에러가 발생하였다. 이후 하위의 에러들은 Net.java와 같은 RestTemplate로 HTTP 요청을 진행하는 객체들이었고, 요청과 응답이 정상적으로 진행되지 않았다는 것은 파악되었다.

하지만 요청에 문제가 있을지, 응답에 문제가 있을지에 대해서 확인을 위해 요청이 올바르게 진행되었는지 확인을 진행하였다.

open ai api에서 발급받은 고유 app key를 제대로 입력하지 않은 상태에서 요청을 진행하니

org.springframework.ai.retry.NonTransientAiException: 401 - {
    "error": {
        "message": "Incorrect API key provided: sk-proj-********************************************************************************************************************************************************KRsA. You can find your API key at https://platform.openai.com/account/api-keys.",
        "type": "invalid_request_error",
        "param": null,
        "code": "invalid_api_key"
    }
  }

401 에러를 반환하였고, 요청이 정상적으로 Open ai api에 도달하고 있음을 확인하였다.

응답이 정상적으로 반환되지 않고 있다는 것을 확인하였고, 이때부터 원인을 찾는데 시간이 많이 걸렸다...

"https://api.openai.com/v1/chat/completions": null

해당 예외 메시지를 반환하는 객체를 참조해보았다.

catch (IOException ex) {
				ResourceAccessException resourceAccessException = createResourceAccessException(uri, this.httpMethod, ex);
				if (observation != null) {
					observation.error(resourceAccessException);
				}
				throw resourceAccessException;
			}

위 catch 로직은 chatClient가 사용하는 요청 객체에 작성되어있었고, 해당 메시지는 createResourceAccessException 이 발생하며 해당 양식으로 로그에 생성된 것을 확인하였다.

createResourceAccessException 메서드

  StringBuilder msg = new StringBuilder("I/O error on ");
  msg.append(method.name());
  msg.append(" request for \"");
  String urlString = url.toString();
  int idx = urlString.indexOf('?');
  if (idx != -1) {
    msg.append(urlString, 0, idx);
  }
  else {
    msg.append(urlString);
  }
  msg.append("\": ");
  msg.append(ex.getMessage());
  return new ResourceAccessException(msg.toString(), ex);
  • "I/O error on " 라는 기본 메시지에 요청 메서드 이름을 추가
  • 쿼리 파라미터(?)를 제외한 URL을 포함
  • 마지막 부분에 원래 예외 메시지를 덧붙여 전체 오류 메시지 완성

message가 null로 생성되었고, 에러 메시지를 받지 못한 것이다.

더이상 응답 상황을 살펴볼 곳도 없었고, 이제는 배포 환경에 대해서 고민하게 되었다. 왜냐하면 현재 배포되었던 환경의 리전은 Osaka 지역이었다. 나는 Oracle Cloud의 EC2를 사용하고 있었고, 나의 OpenAI 계정은 한국 계정으로 등록되어있었다. 요청과 응답은 원활히 진행된 것을 보았을때, EC2내의 Caddy WebServer 의 프록싱까지 정상적으로 진행된 것으로 판단하였고, 이제는 인스턴스의 리전자체에 고민하게 되었다.

Oracle Cloud의 EC2를 한국 계정으로 변경하여 다시 배포를 진행하였다.

2025-02-27T11:44:05.192Z  INFO 1 --- [class-review-vilification] [nio-8080-exec-3] c.e.c.api.PromptRequestEndPoint          : ==== 요청 글 ===== {"postTitle":"좋은 강의","postContent":"아주좋고 자유로운 좋은 강의입니다. "} 
2025-02-27T11:44:05.787Z  INFO 1 --- [class-review-vilification] [nio-8080-exec-3] c.e.c.ChatClientProvider                 : ====== 결과 ===== ChatResponse [metadata={ id: chatcmpl-B5Wb7rh6Wx8AXedYmMDBInXZLLPhN, usage: DefaultUsage{promptTokens=203, completionTokens=2, totalTokens=205}, rateLimit: { @type: org.springframework.ai.openai.metadata.OpenAiRateLimit, requestsLimit: 10000, requestsRemaining: 9999, requestsReset: PT1M4S, tokensLimit: 200000; tokensRemaining: 199792; tokensReset: PT0.062S } }, generations=[Generation[assistantMessage=AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=false, metadata={refusal=, finishReason=STOP, index=0, id=chatcmpl-B5Wb7rh6Wx8AXedYmMDBInXZLLPhN, role=ASSISTANT, messageType=ASSISTANT}], chatGenerationMetadata=DefaultChatGenerationMetadata[finishReason='STOP', filters=0, metadata=0]]]] 

그러니 성공 메시지를 반환하였고, 정상적으로 AI가 작동하는 것을 확읺날 수 있었다. 인스턴스의 지역 문제가 open ai api의 연동에 문제를 일으켰다는 것으로 해결되었다.

나처럼 비슷한 과정을 겪은 분들이 있을까 싶기도 했고, 혹시나 SpringAI 에서 나와 비슷한 과정을 겪은 분이 있는지 궁금하여 SpringAI github issue를 뒤져보았지만, 찾지못했다. 디버깅한 과정들이 아깝기도해서 issue에 등록해두었다. (끝내 SpringAI 측이나 OpenAI 측에서는 제대로된 응답을 받지는 못했다..)