자바로 OpenAI API를 보내보려한다.
API키를 발급하려면 https://deepdaive.com/openai-api-key/
OpenAI API Key 발급 방법 (2025년) - DeepdAive
이번 포스팅에서는 OpenAI API Key를 발급 받는 방법을 소개하겠습니다.
deepdaive.com
이걸 참고하면된다. API 키를 발급받으면 결제창이 뜨는데 나중에 결제한다고하고 넘어갈 수 있다.
문제는 결제를하지 않고 넘어가면 요청을 보낼 수 없다. 최소 5달러는 결제해놓고 거기서 보내는 메시지에 따라 돈을 좀 차감하는 식으로 사용해야한다. 요즘 달러환율이 올라서 5달러에 세금 0.5달러해서 5.5달러, 8600원인가가 나갔다. 나 머플로 사야하는데
공식웹페이지의 코드를 보면서 치려고했는데, 버전도 안맞고 약간씩 오류가 있어서 블로그 몇 개를 참고해가며 다시 새로 쳤다.
원래는 이전에 간단하게나마 써봤던 WebClient로 요청을 보낼까했는데, RestTemplate로 요청을 보내는게 더 간편해서 그걸 사용하려한다.
package com.jamongsalguclub.RFR.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class OpenAiConfig {
@Value("${openai.api.key}")
private String apiKey;
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add((request, body, execution) -> {
request.getHeaders().add("Authorization", "Bearer " + apiKey);
return execution.execute(request, body);
});
return restTemplate;
}
}
그러므로 앞으로 보낼 RestTemplate을 사전설정하고 빈에 등록했다.
앞으로 생성자를 통한 호출이 아니라 스프링 컨테이너에서 RestTemplate를 주입받으면 이 RestTemplate를 주입받게되어 직접 하나하나 불편하게 설정파일을 만지지 않아도 된다.
저 코드가 실행되면 앞으로 스프링에서 주입받은 RestTemplate로 요청을 보내면 헤더에
Authorization="Bearer {{apikey}}"
이 추가되어 요청이 보내질 것이다. Bearer 뒤에 띄어쓰기 하나 추가해야하는거 중요!
요청보내기
package com.jamongsalguclub.RFR.ChatSendTest;
import com.jamongsalguclub.RFR.DTO.GPTRequest;
import com.jamongsalguclub.RFR.DTO.GPTResponse;
import com.jamongsalguclub.RFR.DTO.MessageDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ChatSendTest {
@Value("${openai.bef.prompt}")
private String befPrompt;
@Value("${openai.model}")
private String model;
@Value("${openai.api.url}")
private String apiUrl;
@Autowired
private RestTemplate restTemplate;
@GetMapping("chat")
public String chat(@RequestParam(name="prompt") String prompt) {
GPTRequest request = new GPTRequest(model, prompt, befPrompt);
GPTResponse response = restTemplate.postForObject(apiUrl, request, GPTResponse.class);
return response.getChoices().get(0).getMessage().getContent();
}
}
그리고 요청을 보내는 부분인데,
보낼 메시지 정리하기
한 줄씩 뜯어보면
GPTRequest request = new GPTRequest(model, prompt, befPrompt)
GPTRequest는 DTO이름이다.
package com.jamongsalguclub.RFR.DTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import java.util.ArrayList;
import java.util.List;
@Data
public class GPTRequest {
private String model;
private List<MessageDTO> messages;
public GPTRequest(String model, String prompt, String befPrompt) {
this.model = model;
this.messages = new ArrayList<>();
this.messages.add(new MessageDTO("system", befPrompt));
this.messages.add(new MessageDTO("user", prompt));
}
}
이렇게 모델이름과 보낼 메시지들을 정리한다.
befPrompt가 내가 application.properties에 저장하고있는 프롬프트고, prompt 변수가 실제 유저가 입력한 값이다.
package com.jamongsalguclub.RFR.DTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageDTO {
private String role;
private String content;
}
그리고 보낼 메시지의 실제 값은 MessageDTO에 저장되어있다.
content가 진짜 유저가 보낸 메시지고, role은 Chat-GPT에게 이 메시지가 어떤 특성을 지니고 있는지 알려준다.
GPT의 답변을 받는 일반메시지는 "user",
프롬프트 같이 사전설정 메시지는 "system",
GPT가 반환해주는 메시지는 "assistant" (아닐수도있음. 기억안남) 로 설정된다.
GPTRequest에서 메시지들이 리스트에 담아서 이동하는데, 여러 메시지를 받아도 GPT가 알아서 role 값과 메시지를 구분하여 답변한다.
public GPTRequest(String model, String prompt, String befPrompt) {
this.model = model;
this.messages = new ArrayList<>();
this.messages.add(new MessageDTO("system", befPrompt)); //사전 프롬프트 메시지를 넘김
this.messages.add(new MessageDTO("user", prompt)); //실제 유저메시지
}
그래서 생성자 부분을 보면 인스턴스 변수 model과 messages를 설정해 넘기는데, model은 어떤 모델을 사용할지 정하고, (아직 어느 모델을 사용할지 안정해져서 application.properties에서 받아서 사용 중이다.) messages에 MessageDTO를 여러 개 넘긴다.
요청 받기
GPTResponse response = restTemplate.postForObject(apiUrl, request, GPTResponse.class);
RestTemplate으로 요청을 보내고 요청을 받아온다. GPTResponse는 DTO이름이다.
package com.jamongsalguclub.RFR.DTO;
import lombok.*;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GPTResponse {
private List<Choice> choices;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Choice {
private int index;
private MessageDTO message;
}
}
{
"id": "chatcmpl-xxxx",
"object": "chat.completion",
"model": "gpt-4o-mini",
"created": 123456789,
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "GPT가 반환하는 내용"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 123,
"completion_tokens": 456,
"total_tokens": 579
}
}
GPT에게 물어본 OpenAI API의 리턴예시이다. GPT가 메시지를 반환하면 여러 메타데이터와 함께 choices 부분에 반환메시지를 담아서 반환한다.
그래서 여기서 반환값을 받아 GPTResponse(DTO)에 저장하면 JSON 중 GPTResponse에서 정의하고있는 choice 부분만 JSON에서 GPTResponse로 매핑되게되고 나머지 부분은 버려진다.
이거는 Jackson(ObjectMapper)와 RestTemplate 덕분이라고한다. 나중에 이 둘 좀 파헤쳐봐야겠다.
중요한 것은 이 메시지를 받는 GPTResponse에 있는데, 만약 실제로 전해주는 변수의 이름과 GPTResponse에 저장된 변수의 이름이 다르다면 매핑에 실패하여 NPE가 발생하게된다. 주의할 것
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Choice {
private int index;
private MessageDTO message;
}
처음에 MessageDTO의 변수명을 message가 아닌 messageDTO로 설정했다가 한 번 에러가 발생했다.
최종반환
return response.getChoices().get(0).getMessage().getContent();
그래서 이거는 DTO에서 게터를 통해서 메시지만 가져오는 과정이다.
Response DTO 내부에 Choices가 있고 그 배열의 0번째요소에서 Message를 받아와 그 Content를 가져와서 반환하도록한다.
이렇게 GPT에게 요청도 보내고 반환도 받으며 값을 공유할 수 있다.
이게 간단하게 GPT와 텍스트를 주고받는 방식이다.


이렇게 실제로 메시지를 보내보고 답도 받을 수 있다.
원래는 실제 GPT처럼 대화하듯이 반환메시지를 전해주는데, 프롬프팅을 통해 JSON만 반환하도록 설정해놨다.
프롬프팅.. 처음해보니까 재밌긴한데 약간 자연어로 코딩하는 느낌이라 아주 새롭다.
다만 유효성검사나 할루시네이션 검증은 개발자가 담당해야한다.
또 지금은 RequestParam 어노테이션을 통해 쿼리파라미터로 검색어를 받고 있지만 나중에는 HTTP 요청의 body에 검색어가 들어오게 할 수도 있고
지금 생각해보고 있는 프로젝트가 하나 있는데 백단은 하루이틀이면 다 완성할 것 같은데 이제 다시 시험기간이라..
자바의 정석도 읽어보고 읽고싶은 책도 생기고 RestTemplate 관련된 스프링인액션도 좀 읽어야하고 할 게 많다.
열심히해보자
끝
'CS > 백엔드' 카테고리의 다른 글
| CSRF 공격이란 무엇인가? (0) | 2025.11.25 |
|---|---|
| AI를 이용할 때 할루시네이션 막아보기 (0) | 2025.11.21 |
| WebSocketConfig로 사전 세팅하기 (0) | 2025.11.04 |
| HttpSession을 통한 세션로그인 구현해보기 (0) | 2025.11.03 |
| [백엔드] slf4j로 로그 남기기 (0) | 2025.10.27 |
