서론
최근 Chat GPT가 굉장히 많은 플랫폼에서 상용화가 되고 있고 핫하다는 걸 체감을 많이 느꼈다.
Chat GPT를 활용하여 현재 내가 하는 서비스 혹은 이후의 프로젝트들에서 이를 활용해 볼 수 있는 레퍼런스를 만들어보고 싶단 생각이 들어 보일러 플레이트를 한번 직접 만들어볼까?라는 생각으로 작업에 착수하게 되었다.
물론, Open AI 공식 홈페이지에 들어가보면 해당 Open API를 어떻게 사용하면 되는지 친절하게 설명들이 나와있긴 하지만 해당 부분 외로 레퍼런스를 찾았을 땐 마땅히 그렇다 할 레퍼런스가 부족했고 무엇보다 한국어로 된 레퍼런스는 찾기 힘들다는 생각이 들어 작게나마라도 한번 작성해 보자!라는 포부를 가지고 글을 작성하게 되었다.
해당 레퍼런스에서는 2023.04.16 기준으로 Chat GPT의 핵심이라고 생각하는 Text 채팅을 우선 구현하였다.
추후 더 추가해나갈 계획이며 발전시켜보고자 한다.
어쩌면 부족한 글일수도 있지만 처음 보는 분들이나 사용을 예정하시는 분들에게 조금이나마 도움이 되길 바라며 글을 시작해보려고 한다.
관점 포인트
Open AI의 경우 최근 깃허브에서도 2.5K 스타를 받고 있는 해당 외부 라이브러리를 이용했다.
- Open AI 적용
- https://github.com/TheoKanning/openai-java
- 프로젝트 내 Open AI를 어떻게 적용할 수 있는지 라이브러리를 하나씩 열어보며 살펴보자
- 내부 동작원리
- 현재 외부 라이브러리에서 어떻게 서비스를 제공할 수 있는지 내부 동작원리를 분석해 보자
- Latency 비교
- REST API로 제공했을 때와 Streaming으로 제공했을 때 각각의 Latency에 대해 비교해 보자
- 개인적인 나의 생각
레퍼런스를 구현해 보며 작게나마 들었던 생각들을 나열해 보았다.
Open API API 키 등록
Open API를 사용하는 방법은 아래 주소를 통해 공식 홈페이지에서 자세하게 알 수 있다.
https://platform.openai.com/docs/api-reference/authentication
Open API를 사용하기 위해선 공식 레퍼런스에서 제공하는 것과 같이 현재 자신 계정의 API Key를 함께 요청 시 보내줘야 한다.
먼저 위에서 언급했듯이 외부 라이브러리를 사용했기에, build.gradle에 라이브러리를 implementation을 넣어주자
구현 당시 기준으로 0.12.0이 최신버전이다. 해당 버전을 받도록 하자 추후 GPT 4 버전이 업데이트가 되었을 때 업데이트를 해야 한다… ( 외부 라이브러리의 단점 )
API 키 등록은 아래의 주소를 통해 생성 후 발급을 받을 수 있다.
https://platform.openai.com/account/api-keys
해당 부분은 Chat GPT에서 발급받은 API Token을 주입시켜주는 Config문이다.
여기서 마지막 return 부분에 Duration.ofSeconds를 통해 wait time을 정해놨는데 정한 이유는 다음과 같다.
- REST API로 구현했을 때 Response가 전체 답변이 입력된 후 호출이 되기에 wait time을 걸어두지 않으면 time out 이슈가 발생한다.
한번 내부를 더 살펴보도록 하겠다.
내부를 살펴보면 위에서 설정한 것과 같이 token, timeout을 파라미터로 받고 있습니다.
해당 내부에서는 Retrofit을 활용해 API와의 통신을 하고 있음을 확인할 수 있었습니다.
내부에 대해 하나씩 살펴보겠습니다.
- defaultObjectMapper
해당 부분의 실행 순서에 대해서는 다음과 같은 구조로 이뤄짐을 알 수 있었습니다.
- 새로운 ObjectMapper 인스턴스를 생성합니다.
- 생성된 ObjectMapper 인스턴스의 DeserializationFeature 설정 중 UNKNOWN_PROPERTIES를 무시하도록 설정합니다.
- 이 설정은 JSON 객체에서 알 수 없는 속성이 발견될 때 예외를 던지지 않도록 합니다.
- ObjectMapper 인스턴스의 SerializationInclusion 설정을 NON_NULL로 설정합니다.
- 이 설정은 JSON 직렬화 시 null 값을 갖는 속성을 무시하도록 합니다.
- ObjectMapper 인스턴스의 PropertyNamingStrategy를 SNAKE_CASE로 설정합니다. 이 설정은 객체의 속성 이름을 snake_case로 변환하도록 합니다.
- 설정이 완료된 ObjectMapper 인스턴스를 반환합니다.
- defaultClient
해당 코드의 실행 구조는 다음과 같습니다.
- OkHttpClient.Builder()를 생성하여 OkHttpClient 인스턴스를 구성합니다.
- 생성된 OkHttpClient.Builder 인스턴스에 AuthenticationInterceptor를 추가합니다.
- 이 인터셉터는 HTTP 요청에 헤더를 추가하여 OpenAI API의 인증 요구사항을 충족시키도록 합니다.
- 토큰을 사용하여 인증하는 인터셉터가 추가됩니다.
- connectionPool() 메서드를 사용하여 클라이언트와 서버 간에 유지되는 TCP 연결의 최대 수를 5개로 설정하고, 이 연결의 유지 시간을 1초로 설정합니다.
- readTimeout() 메서드를 사용하여 읽기 시간제한을 설정합니다.
- timeout 파라미터는 Duration 객체로 전달되며, 이를 밀리초 단위로 변환하여 읽기 시간 제한을 설정합니다.
- Duration.ZERO를 전달하면 읽기 시간제한이 없도록 설정됩니다.
- 설정이 완료된 OkHttpClient 인스턴스를 반환합니다.
- defaultRetrofit
- Retrofit.Builder()를 생성하여 Retrofit 인스턴스를 구성합니다.
- baseUrl() 메서드를 사용하여 OpenAI API의 기본 URL을 설정합니다.
- 이 URL은 OpenAI API의 기본 URL인 BASE_URL 상수로 정의됩니다.
- client() 메서드를 사용하여 OkHttpClient 인스턴스를 설정합니다.
- 이전 메서드 defaultClient()에서 생성한 OkHttpClient 객체를 전달합니다.
- addConverterFactory() 메서드를 사용하여 JacksonConverterFactory를 추가합니다.
- JacksonConverterFactory는 Jackson JSON 라이브러리를 사용하여 JSON 데이터를 자바 객체로 변환하는 데 사용됩니다.
- 이전 메서드 defaultObjectMapper()에서 생성한 ObjectMapper 객체를 전달합니다.
- addCallAdapterFactory() 메서드를 사용하여 RxJava2 CallAdapterFactory를 추가합니다.
- 이 어댑터는 Retrofit의 Call 객체를 RxJava2의 Observable 객체로 변환하는 데 사용됩니다.
- build() 메서드를 호출하여 Retrofit 인스턴스를 생성합니다.
- 설정이 완료된 Retrofit 인스턴스를 반환합니다.
위에서 생성된 항목들을 토대로 OpenAiApi의 인터페이스를 구현한 클래스의 인스턴스를 생성합니다.
그 후, Client의 Dispatcher의 ExecutorService를 저장합니다.
- 여기서 ExecutorService란 백 그라운드 스레드 풀을 관리하는 인터페이스입니다.
이렇게 생성자 주입 시 아래와 같은 구조로 이뤄짐을 확인할 수 있었습니다.
Completion vs ChatCompletion
텍스트를 통한 Chat GPT활용에서는 두 가지의 서비스를 제공하고 있습니다.
- Completion
출처 : https://platform.openai.com/docs/api-reference/completions/create
해당 부분은 prompt ( 본문 )을 제공하게 되었을 때 해당 본문에 대한 답변을 내려주고 있습니다.
하지만 다음 서비스를 이용했을 때는 현재 상용화되어있는 서비스와는 다르게 이전 답변에 대한 내용을 학습하지 않기에 채팅처럼 이용할 수 없다는 단점을 가지고 있습니다.
- Completion Chat
출처 : https://platform.openai.com/docs/api-reference/chat/create
해당 부분은 위와 다르게 해당 모델에게 messaage를 발송하게 될 때 채팅처럼 활용할 수 있는 서비스입니다.
위 Completion과는 다르게 이전 내용에 대한 기록이 되어 “이어서 답변해 줘”와 같은 채팅과 같은 기능을 제공합니다.
핵심 요약하여 차이점을 둔다면 다음과 같습니다.
- 각 서비스별 사용하는 Model이 다르다.
- Message 포맷이 다르다. ( Chat 의경우 role 설정이 필요하다. )
- Chat은 이전 내용을 기록, Completion의 경우는 그렇지 못하다.
모델의 종류는 아래 링크를 통해 더욱 자세하게 알 수 있습니다.
https://platform.openai.com/docs/models/overview
OpenAiService 내부 요청 파헤치기
해당 예제에서는 두 가지의 서비스를 다루고 있습니다.
두 가지 서비스를 구현하게 된 계기는 다음과 같습니다.
- 구현하는 입장에서 각 서비스별 얼마큼의 Latency가 발생되는지?
- Open API에서는 stream 서비스를 제공하는데 이를 활용해 보기 위해
completion과 chat completion의 경우 Request요청 값 외 구성 요소는 겹치는 게 많아 한번에 설명하고자 합니다.
Rest API을 구현했을 당시 Service 로직은 다음과 같습니다.
여기서 핵심은 해당 서비스에서는 openAiService.createCompletion을 활용해 해당 서비스를 이용할 수 있습니다.
- createCompletion 내부
- createChatCompletion 내부
해당 내부의 모습은 다음과 같이 이뤄짐을 알 수 있었습니다.
각 항목들에 대해서 더 자세히 알아보도록 하겠습니다.
- createCompletion
- createChatCompletion
openAiService 내부의 createCompletions, createChatCompletion에서는 실제로 서비스를 제공하고 있는 도메인 주소로 요청을 보내도록 설정이 되어있음을 확인할 수 있었습니다.
- execute
해당 부분은 Open AI Api를 호출하고 요청이 실패할 경우 에러 메시지를 파싱 하여 반환하는 메서드임을 확인할 수 있었습니다.
내부의 실행순서는 다음과 같습니다.
- execute 메서드는 apiCall 매개변수를 받아서 동기적으로 실행합니다.
- 만약 apiCall 호출에 실패하면 catch 블록이 실행됩니다.
- catch 블록은 먼저 HttpException이 null이 아니고, **errorBody()**도 null이 아닌 경우 에러 메시지를 파싱 하여 OpenAiHttpException 예외를 던집니다.
- OpenAiHttpException 예외 객체는 OpenAI API 요청에 대한 실패와 함께 파싱 된 에러 메시지를 포함합니다.
- 만약 HttpException이나 OpenAiHttpException 생성 중에 발생한 예외가 IOException이라면, catch 블록은 예외를 던집니다.
Stream Service
Chat GPT 서비스 중 우리가 이용하는 서비스처럼 한 글자씩 값을 내려주는 스트리밍 서비스를 제공하고 있습니다.
현재 상황에서 사용하는 방법은 두 가지가 있습니다.
- Request 요청 시, Body값 내에stream을 true로 값을 요청하는 경우
- 현재 사용 중인 라이브러리에서는 stream 메서드를 활용하는 방법
저는 두 번째 방법을 채택하여 사용하였습니다.
- Stream Completion 구현항목
다음은 stream service를 사용했을 때의 서비스 로직 내부 중 일부입니다.
stream 서비스를 사용할 때는 WebSocket을 활용하여 구현을 진행했습니다.
해당 서비스가 제공하는 내부에 대해 한번 살펴보겠습니다.
- streamCompletion 내부
내부의 모습은 다음과 같이 구성이 되어있습니다.
위에서 언급한 것과 같이 stream 서비스를 이용하는 방법은 두 가지가 있는데 해당 메서드를 활용하게 되면 내부에서 set을 통해 stream을 true로 요청을 보내고 있음을 확인할 수 있었습니다.
요청을 보낸 후 각 값들에 대해 Flowable로 값을 내려주고 있음을 확인할 수 있었고 이때 값을 내려주는 시점에서 content-type이 Application / Json 포맷이 아닌 text/event-stream으로 값을 내려줌을 확인할 수 있었습니다.
여기서 현재 제공하고 있는 stream의 내부를 더 살펴보도록 하겠습니다.
- stream 내부
해당 내부는 다음과 같은 구조로 이뤄짐을 확인할 수 있었습니다.
- Open AI API를 호출하고 데이터를 스트리밍 하는 메서드이다.
- apiCall매개 변수와 반환할 데이터 타입 cl을 인자로 받습니다.
- apiCall은 Retrofit 라이브러리의 Call인스턴스로, Open AI API를 호출하는 데 사용됩니다.
- cl은 반환할 데이터의 클래스 타입을 지정합니다.
Latency 비교 ( REST API vs Streaming )
지금까지 내부가 어떻게 이뤄져 있는지를 한번 살펴보았습니다.
한번 그럼 실질적인 구현을 통해 한번 Latency가 얼마나 걸리는 질 측정 해 보았습니다.
- Completion ( 10.82 s )
- Completion Chat ( 18.68 s )
간단한 질문임에도 생각보다… Response까지 오래 걸림을 확인할 수 있었다.
- Stream ( 15 s )
비슷한 Latency가 발생하지만 실시간으로 계속 값을 내주기에 클라이언트입장에서는 그래도 전부 작성하기까지의 이벤트를 제공해 줄 수 있어 기다리는 것에 대한 지루함을 덜어낼 수 있는 요소로 만들 수 있는 장점이 있지 않을까? 란 생각이 들었다.
아마 Chat GPT에서도 Stream 서비스를 활용해 현재 서비스를 제공하고 있지 않을까 라는 생각이 어렴풋 들었다.
마치며
이번 Open AI API가 공개되고 많은 플랫폼에서 채택할 만큼 정말 획기적인 아이템이 나왔다는 생각이 들었다.
지금까지의 공식 레퍼런스, 소스들을 공부해 보며 구현까지 진행해 보았을 때 내가 생각한 건 다음과 같았다.
- 생각보다 아직까지 Latency가 너무나 길다…
- 만약 공통적으로 답변을 내놓아야 하는 서비스라면 해당 답변들을 캐싱처리를 한 이후 제공하면 어떨까?
- Text 기능 외적으로도 한번 구현을 진행해 보며 개선할 방법이 있다면 개선해보고 싶다.
아직 공개된 지 많은 시간이 지나지 않았기에 개선점이라던지 여러 이슈가 있겠지만 나름 공부해 보고 외부 라이브러리까지 사용해 보며 재미있게 공부를 한 것 같았다.
아마 현재까지 공개된 서비스 중 나라면 Streaming 서비스를 제일 많이 이용하지 않을까란 생각이 들었다.
구현 항목
구현한 코드는 아래 깃허브에 있습니다!
https://github.com/JoeCP17/chatGPT_example
REF.
'Spring' 카테고리의 다른 글
실시간 코인시세 어디까지 알아봤니? part 1 (1) | 2023.08.14 |
---|---|
빗썸 API를 활용한 매수 / 매도 데이터 적재 (1) | 2023.06.11 |
분산락과 네임드락 그리고... 동시성 (1) | 2023.02.02 |
Throttling과 debounce에 대해 알아보자 (0) | 2022.12.11 |
[Design Pattern] Strategy Pattern 전략패턴에 대해 알아보자! (0) | 2022.10.30 |