OpenFeign, 혹은 Feign Client를 통해 OpenAI API를 연동해 ChatGPT 기능을 구현해보았다. 이에 대한 코드 설명이다.
0. 개발 전, 필요한 작업
1. OpenAI 회원가입
2. 카드 등록
3. API Key 발급: API 호출시에 사용할 것이고, 추후 다시 확인할 수 없으니, 잊어버리지 말고 공개하지도 않는다.
모두 OpenAI 사이트에서 할 수 있다.
1. 외부 API 연동 기술 선택 후 적용
지난 글에서 각 연동 기술을 비교해보고, OpenFeign을 선택했다. 이를 적용해보겠다.
먼저, FeignClient를 사용하기 위해 인터페이스를 만든다.
<OpenAiClient>
package com.example.spinlog.ai.service;
import com.example.spinlog.ai.dto.CommentRequest;
import com.example.spinlog.ai.dto.CommentResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@FeignClient(name = "OpenAiClient", url = "${openai.url}")
public interface OpenAiClient {
@PostMapping(consumes = APPLICATION_JSON_VALUE)
CommentResponse getAiComment(@RequestHeader("Authorization") String authorization, @RequestBody CommentRequest commentRequest);
}
@FeignClient: FeignClient를 사용하기 위한 어노테이션이다. 이름과 호출 URL을 넣어준다. url은 추후 관리 및 테스트 용이성을 위해 properties에 넣어주었다.
<applicaton.properties>
openai.url= https://api.openai.com/v1/chat/completions
@PostMapping: GPT를 통해 받은 데이터를 Entity에 저장해야했기 때문에 Post로 요청했다. consumes는 Json타입의 데이터가 필요했기 때문에 JSON_VALUE으로 지정해주었다.
클라이언트에서 getAiComment()를 호출하면서 Header에 기존에 발급받은 API Key를 Authorization에 넣어주고, Body에 요청할 내용을 넣어준다.
<AiServiceImpl>
package com.example.spinlog.ai.service;
import com.example.spinlog.ai.dto.*;
import com.example.spinlog.article.entity.Article;
import com.example.spinlog.article.service.ArticleService;
import com.example.spinlog.global.error.exception.ai.EmptyCommentException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class AiServiceImpl implements AiService {
private final OpenAiClient openAiClient;
private final ArticleService articleService;
private final ModelMapper modelMapper;
private final static String AI_MODEL = "gpt-3.5-turbo";
private final static String AI_ROLE = "system";
private final static String USER_ROLE = "user";
private final static String MESSAGE_TO_AI = "Your identity is as an advice giver.\n" +
"This data shows that people in their 20s and 30s made consumption due to emotional expression. Could you give me some advice on the connection between emotions and consumption?\n" +
"===Please answer by referring to the rules below==\n" +
"First of all, empathize with the user. At this time, mention emotions, events, and purchase details.\n" +
"Please tell us 3 areas for improvement along with reasons.\n" +
"Please use a total of 50 Korean words.\n" +
"Speak in a friendly manner, as if you were talking to a friend.";
@Value("${apiKey}")
private String apiKey;
/**
* AI 코멘트를 요청하고, 결과 반환
*
* @param requestDto 요청 DTO
* @return AI 응답 DTO
*/
@Override
@Transactional
public AiResponseDto requestAiComment(AiRequestDto requestDto) {
List<Message> messages = prepareMessages(requestDto);
CommentRequest commentRequest = createCommentRequest(messages);
String aiComment = getAiComment(commentRequest);
return AiResponseDto.from(aiComment, modelMapper);
}
}
FeignClient를 통해 구현한 OpenAiClient를 주입받는다.
apiKey는 원래 application.properties에 작성해두어야 하는데, 공개가 되면 안되기 때문에 배포 서버에 환경 변수로 등록해둬서 @Value()를 사용해 가져다 쓰도록 했다.
구현 로직을 보면,
- prepareMessages()를 호출해 OpenAI의 API에 요청할 메시지 리스트를 전달한다. AI에게 어떤 역할을 해야하는지 세팅하는 부분이라고 이해된다. 작성법은 OpenAI의 API 명세서에 자세히 나와있기 때문에 그대로 작성하면 된다.
/**
* 메시지 리스트 준비
*
* @param requestDto 요청 DTO
* @return 메시지 리스트
*/
private List<Message> prepareMessages(AiRequestDto requestDto) {
Message message1 = Message.builder()
.role(AI_ROLE)
.content(MESSAGE_TO_AI)
.build();
Message message2 = Message.builder()
.role(USER_ROLE)
.content(requestDto.toString())
.build();
return Arrays.asList(message1, message2);
}
여기서 요청 스펙을 맞춰서 클래스를 생성해줘야 한다.
<CommentRequest>
package com.example.spinlog.ai.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CommentRequest implements Serializable {
private String model;
private List<Message> messages;
}
<Message>
package com.example.spinlog.ai.dto;
import lombok.Builder;
import lombok.Getter;
import java.io.Serializable;
@Getter
@Builder
public class Message implements Serializable {
private String role;
private String content;
}
<Choice>
package com.example.spinlog.ai.dto;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class Choice implements Serializable {
private Integer index;
private Message message;
}
<Usage>
package com.example.spinlog.ai.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
public class Usage implements Serializable {
@JsonProperty("prompt_tokens")
private String promptTokens;
@JsonProperty("completion_tokens")
private String completionTokens;
@JsonProperty("total_tokens")
private String totalTokens;
}
<CommentResponse>
package com.example.spinlog.ai.dto;
import lombok.Getter;
import java.io.Serializable;
import java.util.List;
@Getter
public class CommentResponse implements Serializable {
private List<Choice> choices;
}
여기서 실질적으로 사용한 부분은 CommentRequest, Message, 그리고 CommentResponse이나 데이터를 받기 위해서 다른 클래스들도 생성해둬야 한다.
2. 위에서 세팅한 메시지와 함께 사용할 AI 모델을 지정해주고 요청 정보를 생성하면 된다. 현재 무료 버전의 가장 최신 버전인 "gpt-3.5-turbo"를 이용해 세팅한 메시지와 같이 요청 정보를 생성했다.
/**
* CommentRequest 생성
*
* @param messages 메시지 리스트
* @return CommentRequest 객체
*/
private CommentRequest createCommentRequest(List<Message> messages) {
return CommentRequest.builder()
.model(AI_MODEL)
.messages(messages)
.build();
}
3. openAiClient의 getAiComment()를 호출해 apiKey와 위에서 만들어둔 요청 정보를 보낸다.
/**
* AI 코멘트를 요청하고, 결과 반환
*
* @param commentRequest 요청 객체
* @return AI 코멘트
*/
private String getAiComment(CommentRequest commentRequest) {
String aiComment = openAiClient
.getAiComment(apiKey, commentRequest)
.getChoices()
.stream()
.findFirst()
.map(choice -> choice.getMessage().getContent())
.orElseThrow(() -> new EmptyCommentException("fail to get ai comment"));
log.debug("AI 한마디를 성공적으로 요청했습니다.");
return aiComment;
}
<응답 정보 예시>
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-3.5-turbo-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "This is a test!"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 13,
"completion_tokens": 7,
"total_tokens": 20
}
}
그러면 ChatGPT는 위와 같이 Json으로 된 여러가지 정보를 전달해준다. 이 중 필요한 정보인 ChatGPT의 대답만 추출하고자 한다.
위의 코드와 같이 choice를 stream()으로 열어 message의 content를 가져오면 된다.
이를 클라이언트에 반환하면 된다. 끝!
어려울 줄 알았으나, 비교적 간단히 연동 작업은 끝낼 수 있었다.
다음 글은 외부 API에서 오류가 났을때 어떻게 대처해야하는지에 대해 알아보겠다.
'개발 활동 > 스위프' 카테고리의 다른 글
외부 API 연동 - WireMock으로 테스트하기 (0) | 2024.05.31 |
---|---|
외부 API 연동 - 외부 API에 문제가 있으면? Circuit Breaker Pattern & Resilience4j (0) | 2024.05.30 |
외부 API 연동하기 - 종류 (0) | 2024.05.30 |
[Test] 애플리케이션 테스트 방법 (0) | 2024.04.29 |
AWS Elastic Beanstalk로 간단 배포 (0) | 2024.04.22 |