1. 세팅
다음 값들을 하드 코딩하지 않고, application.yml
파일에 작성해둔다.
// yml 파일
openai:
secret-key: $OPENAI_API_KEY
model: gpt-4o-mini
max-tokens: 250
temperature: 0.5
2. 흐름
OpenAi Api
와 통신을 하기 위해서는 보통RESTful
서비스를 소비하기 위한 동기식 HTTP 클라이언트인RestTemplate
을 빈으로 등록하여 사용한다. 하지만 여기서는WebClient
를 사용하여 동기식 통신을 진행하였다. 다음은WebClient
를 빈으로 등록하는 과정이다.OpenAiConfig
클래스를 통해 openai secret key 값과 기본 url을 갖고 공통 헤더를 작성한다. 그 이유는 OpenAi 공식 문서를 확인하면 알 수 있다.
@Configuration
public class OpenAiConfig {
@Value("${openai.secret-key}")
private String secretKey;
// OpenAI API의 기본 URL
private static final String OPENAI_BASE_URL = "https://api.openai.com/v1";
// JSON 형태의 메타데이터와 secretKey 값을 넣은 공통 헤더
@Bean
public WebClient webClient(WebClient.Builder webClientBuilder) {
return webClientBuilder
.baseUrl(OPENAI_BASE_URL)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + secretKey) // jwt 토큰으로 Bearer 토큰 값을 입력하여 전송
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
- 이제 이 빈으로 등록된
WebClient
를 아래OpenAiApiClient
클래스에서 사용하는데, 이 클래스는OpenAI API
와 통신하기 위한 클라이언트 역할을 수행한다. Spring의WebClient
를 활용하여OpenAI API
에 요청을 보내고, 응답을 받아 처리하는 구조를 갖는다. 이 코드에서 작성한대로https://api.openai.com/v1//chat/completions
로 통신이 가능하다.
@Slf4j
@Service
@RequiredArgsConstructor
public class OpenAiApiClient {
private final WebClient webClient;
public ChatGPTResponse sendRequestToModel(String model, List<Message> messages, int maxTokens, double temperature) {
ChatGPTRequest request = new ChatGPTRequest(model, messages, maxTokens, temperature);
return webClient.post()
.uri("/chat/completions")
.bodyValue(request)
.retrieve()
.bodyToMono(ChatGPTResponse.class)
.block();
}
}
여기서 ChatGPTRequest
와 ChatGPTResponse
클래스는 OpenAi
에 요청을 보내고 응답을 받는 DTO이다.
@Data
@AllArgsConstructor
public class ChatGPTRequest {
private String model;
private List<Message> messages;
private int max_tokens;
private double temperature;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatGPTResponse {
private List<Choice> choices;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Choice {
private int index;
private Message message;
}
}
그리고
OpenAi Api
와 통신을 할 때는 프롬프트가 필요한데, 프롬프트를 직접 타이핑으로 치는 것이 아닌PromptManager
,PromptTemplate
을 통해 관리하였다.public class PromptTemplate { private static final String BASE_TEMPLATE = """ ### 요청하고 싶은 것 {{request}} ### 응답 값 형식 {{responseFormat}} """; private final String template; public PromptTemplate() { this.template = BASE_TEMPLATE; } public String fillTemplate(String request, String responseFormat) { return template.replace("{{request}}", request) .replace("{{responseFormat}}", responseFormat); } }
이를 통해 어떻게 요청을 할 것이고, 어떻게 응답을 받을지에 대한 프롬프트를 쉽게 작성할 수 있다.
이제 가장 중요한
OpenAi Api
와 통신을 하는 서비스 계층 코드이다.@Slf4j @Service @RequiredArgsConstructor public final class ChatGptService { private final OpenAiApiClient openAiApiClient; // ChatGPT API와의 통신을 담당 private final PromptManager promptManager; // 프롬프트 생성 private final ObjectMapper objectMapper = new ObjectMapper(); // JSON 응답 변환 @Value("${openai.model}") private String model; // OpenAI 모델 @Value("${openai.max-tokens}") private int maxTokens; // 최대 토큰 수 @Value("${openai.temperature}") private double temperature; // 생성된 응답의 창의성 정도 private final String systemRole = "system"; // 대화에서의 역할(시스템 메시지) private final String userRole = "user"; // 대화에서의 역할(사용자 메시지) // 체형 분석 public BodyTypeAnalysisResponse analyzeBodyType(String name, String gender, String answers) { // 템플릿 생성 String prompt = promptManager.createBodyTypeAnalysisPrompt(name, gender, answers); // OpenAI API 호출 ChatGPTResponse response = openAiApiClient.sendRequestToModel( model, List.of( new Message(systemRole, prompt) ), maxTokens, temperature ); // response에서 content 추출 String content = response.getChoices().get(0).getMessage().getContent().trim(); log.info("content : " + content); // 백틱과 "json" 등을 제거 if (content.startsWith("```")) { content = content.replaceAll("```[a-z]*", "").trim(); } log.info("content after removing backticks: " + content); try { // content -> BodyTypeAnalysisResponse 객체로 변환 return objectMapper.readValue(content, BodyTypeAnalysisResponse.class); } catch (JsonMappingException e) { throw new RestApiException(AnalysisErrorStatus._GPT_ERROR); } catch (JsonProcessingException e) { throw new RestApiException(AnalysisErrorStatus._GPT_ERROR); } } }
PromptManager
를 통해 프롬프트를 생성하고,OpenAiApiClient
를 통해OpenAi Api
를 호출하고,ChatGPTResponse
의 형식에 맞게 응답을 받는다. 그리고 이 response에서 우리가 필요한 content 값만 추출한다. 이 content 값을ObjectMapper
를 사용하여 우리가 응답 DTO로 사용하고자 하는BodyTypeAnalysisResponse
로 변환한다.마지막으로 컨트롤러 계층이다.
@Tag(name = "체형 분석", description = "체형 분석 API") @RestController @RequestMapping("/body-analysis") @RequiredArgsConstructor public class BodyTypeController { private final ChatGptService chatGptService; @PostMapping("/result") @Operation(summary = "체형 분석 API", description = "OpenAI를 사용해서 사용자의 체형을 분석하는 API입니다. Request Body에는 질문에 맞는 답변 목록을 보내주세요.") public BaseResponse<BodyTypeAnalysisResponse> analyzeBodyType( @AuthenticationPrincipal CustomUserDetails customUserDetails, @Valid @RequestBody BodyTypeAnalysisRequest request ) { return BaseResponse.onSuccess(memberBodyTypeCommandService.analyzeBodyType(customUserDetails.getMember(), request.getAnswer())); } }
/body-analysis/result
로 Post 요청이 오면OpenAi Api
를 사용하여 체형 분석을 해주고 반환한다.
3. 겪었던 문제와 해결 방법
content에는 로그가
2025-01-12T20:03:58.963+09:00 INFO 31648 --- [mody-server-local] [nio-8080-exec-4] c.e.m.d.chatgpt.service.ChatGptService : content : ```json { "name": "차은우", "bodyTypeAnalysis": { "type": "스트레이트", "description": "스트레이트 체형은 상대적으로 균형 잡힌 비율을 가지고 있으며, 어깨와 엉덩이의 너비가 비슷한 특징을 보입니다. 근육이 탄탄하고 뼈대가 두드러지며, 허리의 굴곡이 적어 직선적인 실루엣을 형성합니다.", "featureBasedSuggestions": { "emphasize": "어깨선과 쇄골을 강조하여 상체의 강한 인상을 주는 스타일을 선택하세요.", "enhance": "허리 라인을 부각시킬 수 있는 디자인의 의상을 선택하여 전체적인 균형을 맞추는 것이 좋습니다." } } }
이렇게 잘 찍히는데
스웨거에는
그래서 에러 로그를 찍어보니
Unexpected character ('`' (code 96)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
-> 이는 백틱 때문이라는데, OpenAI API 응답에 JSON 코드 블록 표시용 백틱(```json`)이 포함되어 content와 DTO 형식이 맞아도 ObjectMapper가 변환을 못 해주던 것이다.
2. 해결 방법 : 서비스 계층에서 백틱 제거 로직 추가
if (content.startsWith("```")) {
content = content.replaceAll("```[a-z]*", "").trim(); // 백틱과 "json" 등을 제거
}
log.info("content after removing backticks: " + content);
'mody' 카테고리의 다른 글
도메인 구매하여 Certbot을 통해 HTTPS 배포하기 (0) | 2025.03.09 |
---|---|
스프링에서 핀터레스트 이미지 크롤링하기 (0) | 2025.03.09 |
S3 파일 업로드 CORS 해결 (0) | 2025.03.09 |
스프링에서 S3 이미지 삭제 deleteObject() 403 Access Denied - AWSCompromisedKeyQuarantineV3 정책 (0) | 2025.03.09 |
mody - 당신의 AI 스타일 친구 (0) | 2025.03.09 |