์ด๋ฒ ์คํ๋ฆฐํธ(1.22 ~ 1.25)์์ OpenAI API๋ฅผ ์ฐ๋ํ๋ฉฐ ๊ฒช์๋ ์ฃผ์ ์๋ฌ 3๊ฐ์ง์ ๊ทธ ํด๊ฒฐ ๊ณผ์ ์ ์์ธํ ๊ธฐ๋กํฉ๋๋ค.
IllegalArgumentException: The template string is not valid๐จ ๋ฌธ์ ์ํฉ
Spring AI์ ChatClient๋ฅผ ์ฌ์ฉํ์ฌ AI์๊ฒ ์์ฒญ์ ๋ณด๋์ผ๋, ์๋ฒ ๋ก๊ทธ์ IllegalArgumentException์ด ๋ฐ์ํ๋ฉฐ ์์ฒญ์ด ์คํจํ์ต๋๋ค.
java.lang.IllegalArgumentException: The template string is not valid.
at org.springframework.ai.chat.prompt.PromptTemplate.<init>(PromptTemplate.java:85)
...
๐ ์์ธ ๋ถ์
Spring AI์ ๊ธฐ๋ณธ PromptTemplate ํด๋์ค๋ ๋ฌธ์์ด ๋ด๋ถ์ ์ค๊ดํธ { }๋ฅผ ๋ณ์ ์นํ์(Placeholder)๋ก ์ธ์.
ํ์ง๋ง ์ ๊ฐ ์์ฑํ ์์คํ
ํ๋กฌํํธ์๋ AI์๊ฒ JSON ์๋ต ํ์์ ์์๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์ค๊ดํธ๊ฐ ํฌํจ๋ JSON ๋ฌธ์์ด์ด ๋ค์ด ์์์ต๋๋ค.
// ๋ฌธ์ ๊ฐ ๋ ํ๋กฌํํธ ์์
String systemPrompt = """
...
๋ฐ๋์ ์๋ ํ์์ผ๋ก ์๋ตํ์ธ์:
{ <-- Spring AI๋ ์ด๊ฑธ ๋ณ์์ ์์์ผ๋ก ์ฐฉ๊ฐํจ
"score": ...
}
""";
PromptTemplate์ ์ด ์ค๊ดํธ๋ฅผ ๋ณด๊ณ "์ฌ๊ธฐ์ ์ด๋ค ๊ฐ์ ์ฑ์๋ฃ์ด์ผ ํ๋๋ฐ, ๋งค์นญ๋๋ ๋ณ์๊ฐ ์๋ค"๊ณ ํ๋จํ์ฌ ์๋ฌ๋ฅผ ๋ฐ์์ํจ ๊ฒ.
โ
ํด๊ฒฐ ๋ฐฉ๋ฒ
PromptTemplate์ ๊ฑฐ์น์ง ์๊ณ , SystemMessage์ UserMessage ๊ฐ์ฒด๋ฅผ ์ง์ ์์ฑํ์ฌ ๋ฉ์์ง ๋ฆฌ์คํธ๋ก ์ ๋ฌํ๋ ๋ฐฉ์์ ์ฌ์ฉํ์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ํ
ํ๋ฆฟ ํ์ฑ ๊ณผ์ ์ ๊ฑด๋๋ฐ๊ณ ๋ฌธ์์ด ๊ทธ๋๋ก AI์๊ฒ ์ ์ก๋ฉ๋๋ค.
// ํ
ํ๋ฆฟ ํ์ฑ์ ์๋ํ๋ค๊ฐ JSON ์ค๊ดํธ ๋๋ฌธ์ ์คํจํจ
chatClient.prompt()
.system(systemPrompt)
.user(userMessage)
.call();
// Message ๊ฐ์ฒด๋ก ์ง์ ๊ฐ์ธ์ ์ ๋ฌ (ํ
ํ๋ฆฟ ํ์ฑ ์ฐํ)
chatClient.prompt()
.messages(new SystemMessage(systemPrompt), new UserMessage(userMessage))
.call();
429 insufficient_quota (OpenAI Quota Exceeded)๐จ ๋ฌธ์ ์ํฉ
์ฝ๋๋ฅผ ์๋ฒฝํ๊ฒ ์์ ํ์์๋ ๋ถ๊ตฌํ๊ณ , AI ์์ฒญ ์ 429 ์ํ ์ฝ๋์ ํจ๊ป ์๋์ ๊ฐ์ ์๋ฌ ๋ฉ์์ง๊ฐ ๋ฐํ๋์์ต๋๋ค.
{
"error": {
"code": "insufficient_quota",
"message": "You exceeded your current quota, please check your plan and billing details."
}
}
๐ ์์ธ ๋ถ์
์ด ์๋ฌ๋ "API ์ฌ์ฉ ํ๋ ์ด๊ณผ" ๋๋ "์์ก ๋ถ์กฑ"์ ์๋ฏธํฉ๋๋ค.
โ
ํด๊ฒฐ ๋ฐฉ๋ฒ
OpenAI Platform์ Billing Settings ํ์ด์ง์์ Credit์ ์ถฉ์ ํ์ต๋๋ค.
(AI_INVALID_RESPONSE)๐จ ๋ฌธ์ ์ํฉ
AI๊ฐ ์๋ต์ ์ฃผ๊ธด ํ๋๋ฐ, ๊ฐํน ObjectMapper๊ฐ ์ด๋ฅผ DTO๋ก ๋ณํํ์ง ๋ชปํ๊ณ ์๋ฌ๋ฅผ ๋ฑ๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ์
๐ ์์ธ ๋ถ์
LLM(๊ฑฐ๋์ธ์ด๋ชจ๋ธ)์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ
์คํธ ์์ฑ๊ธฐ์ด๊ธฐ ๋๋ฌธ์, JSON ํ์์ผ๋ก ๋ฌ๋ผ๊ณ ์์ฒญํด๋ ์น์ ํ๊ฒ(?) ๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก์ ์์์ ์ฃผ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
// AI๊ฐ ์ค์ ๋ก ์ค ์๋ต (Raw String)
{
"score": { ... },
"feedback": [ ... ]
}
``` <-- ๋ค์ ๋ฐฑํฑ(```)์ด ๋ถ์ด์์
Java์ ObjectMapper๋ ์์ํ JSON ๋ฌธ์์ด๋ง ํ์ฑํ ์ ์๊ธฐ ๋๋ฌธ์, ์๋ค์ ๋ถ์ \```json ํ๊ทธ ๋๋ฌธ์ ํ์ฑ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ.
โ
ํด๊ฒฐ ๋ฐฉ๋ฒ
AI์ ์๋ต(Raw String)์ ํ์ฑํ๊ธฐ ์ ์ ์ ์ฒ๋ฆฌ(Pre-processing) ๊ณผ์ ์ ์ถ๊ฐํ๊ณ , ํ์ฑ ์คํจ ์ ํด๋ผ์ด์ธํธ์๊ฒ ๋ช
ํํ ์๋ฌ๋ฅผ ์ ๋ฌํ๋๋ก ์์ธ ์ฒ๋ฆฌ๋ฅผ ๊ฐํํ์ต๋๋ค.
1. ๋ฌธ์์ด ์ ์ฒ๋ฆฌ (replace)
// AiClient.java
String cleanJson = aiResponseRaw
.replace("```json", "") // ๋งํฌ๋ค์ด ์์ ํ๊ทธ ์ ๊ฑฐ
.replace("```", "") // ๋งํฌ๋ค์ด ๋ ํ๊ทธ ์ ๊ฑฐ
.trim(); // ๊ณต๋ฐฑ ์ ๊ฑฐ
2. ์ปค์คํ
์์ธ ๋์ง๊ธฐ
ํ์ฑ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์์คํ
๋ด๋ถ ์๋ฌ(500)๋ฅผ ๊ทธ๋๋ก ๋ณด์ฌ์ฃผ๋ ๋์ , ์ ์ํด๋ ๋น์ฆ๋์ค ์์ธ๋ฅผ ๋์ง๋๋ค.
try {
return objectMapper.readValue(cleanJson, ResumeResponse.class);
} catch (Exception e) {
log.error("AI ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: ", e);
// "AI ์๋ต์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค"๋ผ๋ ์๋ฏธ์ ์ปค์คํ
์์ธ
throw new BusinessException(ErrorCode.AI_INVALID_RESPONSE);
}
3. ์ ์ญ ์์ธ ์ฒ๋ฆฌ(GlobalExceptionHandler)
BusinessException์ ์ก์์ ํ๋ก ํธ์๋๊ฐ ์ดํดํ๊ธฐ ์ฌ์ด ํ์ค ํฌ๋งท์ผ๋ก ๋ณํํ์ฌ ๋ฐํํฉ๋๋ค.
// GlobalExceptionHandler.java
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException e) {
return ApiResponse.error(e.getErrorCode().name(), e.getMessage());
}