일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 객체지향 쿼리 언어
- 이펙티브자바
- jdbc
- cicd
- 아이템31
- 아이템 23
- java
- 아이템 25
- 큐시즘
- criteriaquery
- Spring Batch
- chapter4. 리포지터리와 모델 구현
- Domain Driven Design
- chapter5. 스프링 데이터 jpa를 이용한 조회 기능
- 도메인 주도 개발 시작하기
- 최범균
- 일ㅊ
- chatgpt 연동
- JPQL
- ddd
- 기업프로젝트
- 아이템 28
- 아이템29
- 아이템 24
- 아이템 27
- GitHub Actions
- JPA
- 아이템30
- 아이템 26
- 자바 ORM 표준 JPA 프로그래밍
- Today
- Total
코딩은 마라톤
[SpringBoot + OpenAI(ChatGPT)] SpringBoot에서 OpenAI API를 이용해 연동하기 본문
[SpringBoot + OpenAI(ChatGPT)] SpringBoot에서 OpenAI API를 이용해 연동하기
anxi 2024. 11. 10. 20:21진행 중인 프로젝트에서 최근 ChatGPT를 연동해서 응답을 받아와야할 상황이 생겼습니다.
OpenAI Java SpringBoot 라이브러리도 있기는 하지만 (https://github.com/TheoKanning/openai-java?tab=readme-ov-file)
라이브러리보다 직접 구현하는게 나중에 API 명세가 바뀌더라도 변경하는데 용이하지 않을까 해서 구현해보았습니다!
RestClient와 WebClient를 사용하여 구현할 수 있는데 저는 WebClient를 사용하였습니다!
* 추후 RestClient와 WebClient에 관한 블로그도 올릴게요!
1. OpenAI API 요청과 응답 확인하기
Create Chat Completion API를 사용하였습니다! (참고: https://platform.openai.com/docs/api-reference/chat/create)
요청
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
}
]
}'
예시 요청은 위와 같습니다.
여기서 우리가 요청할 때 보내야하는 값은
- Authorization : OpenAI에서 제공받은 Secret Key
- model : 사용할 GPT 모델명 (gpt-4o, gpt-4o-mini, gpt-4, gpt-3.5-turbo 등)
- messages
- role
- system : 모델이 질문에 답하는 방식을 지정
- user : 사용자의 질의
- assistant : user 질의에 대응되는 응답
- content : gpt에 제공할 프롬프트
- role
이렇게 존재합니다.
Authorization Key 얻기
https://platform.openai.com/api-keys
위 페이지에서 회원가입을 진행합니다.
create new secret key 버튼을 누르면 name과 permission을 지정하고 생성할 수 있습니다!
그러면 이와 같이 secret key가 나옵니다!
여기서 Done을 누를 시 이제 secret key를 확인할 수 없게 되므로, secret key는 꼭!!! 메모장이나 private한 공간에 저장하시길 바랍니다. (공유도 하면 안돼요!)
이외 model과 messages는 개발하면서 설정하면 됩니다!
응답
{
"id": "chatcmpl-123456",
"object": "chat.completion",
"created": 1728933352,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hi there! How can I assist you today?",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 19,
"completion_tokens": 10,
"total_tokens": 29,
"prompt_tokens_details": {
"cached_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"system_fingerprint": "fp_6b68a8204b"
}
응답은 여러 필드가 존재합니다.
여기서 저희가 봐야할 필드는 "choices"와 "usage" 입니다!
- choices
- messages
- content : 사용자의 질의 요청에 대한 응답을 반환합니다.
- messages
- usage : 사용된 토큰의 개수를 알 수 있습니다. 이를 통해 가격을 계산할 수 있습니다.
- prompt_tokens: 요청 시 작성한 프롬프트에 관한 토큰 개수입니다.
- completion_tokens: 응답의 토큰 개수입니다.
2. SpringBoot OpenAI API 연동하기
SpringBoot 기본 세팅은 되어있다는 가정 하에 시작하겠습니다.
개발 환경
Java 17, SpringBoot 3.3.4, Gradle
2-1. build.gradle 의존성 추가하기
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
우선 WebClient를 사용하기 위해 webflux 의존성을 추가해줍니다.
만약 동기식으로 처리할거면 의존성 추가가 필요 없는 RestClient를 사용하셔도 무방합니다!
2-2. 프로퍼티 설정하기(application.yml, properties)
Secret Key는 유출되면 안되는 값이라 프로퍼티에 추가해서 사용합니다!
application.yml
openai:
model: {model 명} ex) gpt-4o-mini
secret-key: {위에서 발급받은 secret key}
chat-completions-url: https://api.openai.com/v1/chat/completions
model과 chat-completions-url도 프로퍼티에 추가해서 사용합니다! (model과 url도 변경될 수 있어 프로퍼티에서 관리합니다.)
2-3 요청, 응답 DTO 만들기
우리는 OpenAI에 요청을 보내고 응답을 받기 때문에 이를 전달할 수 있는 DTO를 만들려고 합니다!
요청
저희는 요청에서 필요한 값 중 Authorization과 model은 프로퍼티에서 가져오기 때문에 Message DTO만 생성하면 됩니다.
@Builder
public record Message(String role, String content) {
public static List<Message> createMessages() {
List<Message> messages = new ArrayList<>();
messages.add(createSystemMessage());
messages.add(createUserMessage());
return messages;
}
public static Message createSystemMessage() {
return Message.builder()
.role(SYSTEM.getDescription())
// content에는 프롬프트를 작성하면 됩니다!
.content("따뜻한 분위기로 글을 작성해줘")
.build();
}
public static Message createUserMessage() {
return Message.builder()
.role(USER.getDescription())
// content에는 프롬프트를 작성하면 됩니다!
.content("오늘 나 맛있는 거 먹었다!")
.build();
}
@Getter
@RequiredArgsConstructor
public enum Role {
SYSTEM("system"),
USER("user");
private final String description;
}
}
- SYSTEM과 USER의 role로 Message를 생성하는 메서드를 만들었습니다.
- 요청에서 사용되는 messages는 리스트 형태로 들어가기 때문에 createMessages()를 통해 리스트로 만들어줍니다.
응답
public record OpenAIResponse(
String id,
String object,
int created,
String model,
@JsonProperty("system_fingerprint")
String systemFingerprint,
List<Choice> choices,
Usage usage
) {
public record Choice(
int index,
Message message,
Boolean logprobs,
@JsonProperty("finish_reason")
String finishReason
) {
}
public record Usage(
@JsonProperty("prompt_tokens")
int promptTokens,
@JsonProperty("completion_tokens")
int completionTokens,
@JsonProperty("total_tokens")
int totalTokens,
@JsonProperty("completion_tokens_details")
CompletionTokensDetails completionTokensDetails
) {
}
public record CompletionTokensDetails(
@JsonProperty("reasoning_tokens")
int reasoningTokens,
@JsonProperty("accepted_prediction_tokens")
int acceptedPredictionTokens,
@JsonProperty("rejected_prediction_tokens")
int rejectedPredictionTokens
) {
}
}
- 응답 DTO는 위와 같이 생성하면 됩니다!
- camelCase로 변수명을 사용하기 위해 @JsonProperty를 사용하였습니다.
- DTO에서 사용할 값을 서비스 레이어에서 조회, 수정하면 됩니다.
2-4. WebClient로 OpenAI 연동하기
WebClient를 사용한 이유는 요청하고 받아오는 시간이 꽤 걸리기도 해서 동기식으로 처리하는 것보다 비동기로 처리하고 응답 받아오기 전까지 다른 작업을 수행할 수 있으므로 WebClient를 사용하였습니다!
1) 프로퍼티에 추가한 값 가져오기
@Component
@RequiredArgsConstructor
@Log4j2
public class OpenAIRecommendationProvider {
@Value("${openai.model}")
private String model;
@Value("${openai.secret-key}")
private String secretKey;
@Value("${openai.chat-completions-url}")
private String chatCompletionsUrl;
- @Value 어노테이션을 사용해 프로퍼티의 값을 가지고 옵니다.
2) WebClient로 연동하기
public Mono<OpenAIResponse> getRecommendation() {
return WebClient.create()
.post()
.uri(chatCompletionsUrl)
.headers(header -> {
header.setContentType(MediaType.APPLICATION_JSON);
header.setBearerAuth(secretKey);
})
.bodyValue(createRecommendationRequestBody())
.retrieve()
.bodyToMono(OpenAIResponse.class)
.onErrorResume(WebClientException.class, e -> {
log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e);
return Mono.error(GlobalErrorCode.WEB_CLIENT_ERROR.toException());
})
.onErrorResume(Exception.class, e -> {
log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e);
return Mono.error(GlobalErrorCode.INTERNAL_ERROR.toException());
});
}
private Map<String, Object> createRecommendationRequestBody() {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model);
requestBody.put("messages", Message.createMessages());
return requestBody;
}
- 요청 시 POST 메서드를 이용해 가지고 옵니다!
- headers
- APPLICATION_JSON 형식으로 content type을 지정합니다.
- secretKey도 header에 지정합니다.
- bodyValue
- 요청시 보낼 값을 넣습니다!
- 우리는 model, messages를 넣어 보냅니다.
- createRecommendationRequestBody() 메서드를 이용해 requestBody를 생성합니다.
- retrieve, mono
- 비동기 처리를 위해 retrieve를 사용하고 mono 타입으로 반환합니다.
- 만약 동기 식으로 처리하고 싶으면 block()을 사용하면 됩니다.
WebClient 전체 코드는 다음과 같습니다!
@Component
@RequiredArgsConstructor
@Log4j2
public class OpenAIRecommendationProvider {
@Value("${openai.model}")
private String model;
@Value("${openai.secret-key}")
private String secretKey;
@Value("${openai.chat-completions-url}")
private String chatCompletionsUrl;
public Mono<OpenAIResponse> getRecommendation() {
return WebClient.create()
.post()
.uri(chatCompletionsUrl)
.headers(header -> {
header.setContentType(MediaType.APPLICATION_JSON);
header.setBearerAuth(secretKey);
})
.bodyValue(createRecommendationRequestBody())
.retrieve()
.bodyToMono(OpenAIResponse.class)
.onErrorResume(WebClientException.class, e -> {
log.error("WebClient 에러 발생 - 에러 메시지: {}", e.getMessage(), e);
return Mono.error(GlobalErrorCode.WEB_CLIENT_ERROR.toException());
})
.onErrorResume(Exception.class, e -> {
log.error("알 수 없는 내부 오류 발생 - 에러 메시지: {}", e.getMessage(), e);
return Mono.error(GlobalErrorCode.INTERNAL_ERROR.toException());
});
}
private Map<String, Object> createRecommendationRequestBody() {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model);
requestBody.put("messages", Message.createMessages());
return requestBody;
}
}
2-5. 결과 확인하기
Controller에서 OpenAIRecommendationProvider를 통해 응답을 받아봅시다!
@RestController
@RequiredArgsConstructor
public class RecommendationController {
private final OpenAIRecommendationProvider openAIRecommendationProvider;
@GetMapping("/api/v1/recommendations")
public Mono<OpenAIResponse> getRecommendation() {
return openAIRecommendationProvider.getRecommendation();
}
}
- 만약 ChatGPT에게 프롬프트를 동적으로 처리하고 싶다면 아래의 방식을 추가해주시면 됩니다!
- PostMapping을 통해 RequestBody로 프롬프트를 넘겨주기
- Message에 Content에 동적으로 값 설정해주기 (createUserMessage() 변경)
결과
{
"id": "chatcmpl-123456",
"object": "chat.completion",
"created": 1731236284,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "와, 정말 기쁘네요! 어떤 맛있는 음식을 드셨나요? 맛있게 먹은 순간은 언제나 특별하죠. 함께 나누고 싶은 음식이나 그 경험에 대한 이야기도 들려주시면 좋을 것 같아요. 그 음식을 드시면서 느꼈던 감정이나, 친구들과 함께한 즐거운 시간은 또 어떤지요?🌼✨"
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 29,
"completion_tokens": 83,
"total_tokens": 112,
"completion_tokens_details": {
"reasoning_tokens": 0,
"accepted_prediction_tokens": 0,
"rejected_prediction_tokens": 0
}
},
"system_fingerprint": "fp_6b68a8204b"
}
이렇게 나오게 됩니다!
그래서 나중에 내용만 필요할 경우에는
response.choices().get(0).message().content()
이런 식으로 추출해올 수 있어요!
+ 503 Service Temporarily Unavailable가 발생할 경우
저도 처음에 curl 요청을 보냈을 때 위의 에러가 발생했는데요!
# Secret Key를 발급받으시고 curl 요청을 보내보세요! (터미널에서 복붙하시면 돼요!)
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \ // 발급 받은 secret key 넣기
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
}
]
}'
위 에러는 OpenAI에 충전된 금액이 없어서 발생하더라고요!
https://platform.openai.com/settings/organization/billing/overview
위 페이지에서 카드 등록 후, Add to credit balance를 통해 충전하시면 됩니다!
5$ 부터 가능하니 참고해주세요!
GPT 비싸다는데 얼마인가요..??
저희가 반환받은 응답의 토큰 수로 계산을 한 번 해보겠습니다!
"model": "gpt-4o-mini-2024-07-18",
"prompt_tokens": 29, "completion_tokens": 83, "total_tokens": 112,
모델은 gpt-4o-mini-2024-07-18를 사용하였고,
입력 29토큰, 출력 83토큰 총 112토큰을 사용하였습니다!
gpt한테 계산해달라고 부탁했습니다.
0.00005415달러???
네. 0.076원입니다..!
저렴하다고 생각할 수 있지만, 지금은 프롬프트가 매우 짧은 문장이라 그렇고 문장의 길이가 길어지면 길어질 수록 가격은 기하급수적으로 올라갈 수 있기 때문에 매번 사용하고 대시보드에서 사용량과 비용을 꼭 확인하세요!!!!!
끝!
'Backend > SpringBoot' 카테고리의 다른 글
[SpringBoot] 서블릿과 서블릿 컨테이너 (0) | 2024.06.28 |
---|---|
[Springboot] Filter와 Interceptor (0) | 2024.05.08 |
Request DTO에서 @Getter를 쓰는 이유 (바인딩) (0) | 2024.04.08 |