기존 Spring 환경에서 LLM/AI 기능을 추가하려면 Python 기반 환경을 따로 띄워야 했음. 싸피 프로젝트에서도 이렇게 AI를 직접 구현하고(내용은 잘 기억 안 남) 연동해서 띄웠던 걸로 기억함.
근데 이제는 이미 만들어진 AI를 불러다 쓸 수 있음. 또한 직접 모든 코드를 짜서 부르는 게 아니라, 어느 정도 그 과정 자체가 간편화됨. 그게 바로 Spring AI의 등장.
또 모델이 굉장히 많은데, 과거에는 각 모델의 API를 직접 호출해 기능을 만들어야 했음. 모델마다 엔드포인트가 다르고 여러 API를 쓰는 게 불편했음. ChatGPT / Claude / Gemini 등 각각 다르게 써야 했는데, Spring AI의 등장으로 하나의 규격으로 쓰는 게 편리해짐.
즉 이것도 나름 전략 패턴이라고 할 수 있지 않을까? ㅇㅇ
// src/main/java/.../post/service/search/EmbeddingService.java
private final EmbeddingModel embeddingModel; // Spring AI 인터페이스
public float[] embed(String text) { // 캐시활용
return queryEmbeddingCache.get(text, text2 -> embeddingModel.embed(text2));
}
public float[] embedWithoutCache(String text) { // 노캐시
return embeddingModel.embed(text);
}
@Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
@Transactional
public void generateAndSave(Long postId, String text) { // 임베딩한 데이터 DB에 저장
generationTimer.record(() -> {
float[] embedding = embedWithoutCache(text);
therapyPostRepository.updateContentEmbedding(postId, toVectorString(embedding));
});
successCounter.increment();
}
EmbeddingModel은 스프링이 AI를 자동 구성으로 만들어주는 빈. 직접 new하지 않고 생성자 주입을 받으면 됨.
그리고 만약 똑같은 검색을 자주 한다면 비용을 절약하기 위해 카페인캐시를 두었음.
설정은 이렇게:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY:}
embedding:
options:
model: text-embedding-3-small
model:
chat: none # OpenAI의 다른 기능(챗, 이미지 등)은 전부 꺼둠
embedding: none # 기본은 꺼두고, 검색 기능 프로필에서만 켜는 구조
image: none
moderation: none
Spring AI가 대신해주는 부분을 보여주기 위한 비교용 코드. (실제 저장소엔 없음, 가상 예시)
@Component
public class OpenAiEmbeddingClient {
private final RestClient restClient; // RestClient 빌드
private final ObjectMapper objectMapper;
private final String apiKey;
private final String model;
public OpenAiEmbeddingClient(@Value("${OPENAI_API_KEY}") String apiKey,
@Value("${openai.embedding.model:text-embedding-3-small}") String model) {
this.apiKey = apiKey;
this.model = model;
this.objectMapper = new ObjectMapper();
Duration timeout = Duration.ofSeconds(10); // 타임아웃 설정
this.restClient = RestClient.builder()
.baseUrl("https://api.openai.com/v1")
.requestFactory(ClientHttpRequestFactories.get(
ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(timeout)
.withReadTimeout(timeout)))
.build();
}
public float[] embed(String text) {
Map<String, Object> body = Map.of( // JSON 요청 바디 구성
"model", model,
"input", text
);
String response = restClient.post()
.uri("/embeddings")
.header("Authorization", "Bearer " + apiKey)
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(String.class);
return parseEmbedding(response); // 응답 파싱
}
private float[] parseEmbedding(String response) {
try {
JsonNode root = objectMapper.readTree(response);
JsonNode values = root.path("data").get(0).path("embedding");
float[] embedding = new float[values.size()];
for (int i = 0; i < values.size(); i++) {
embedding[i] = (float) values.get(i).asDouble();
}
return embedding;
} catch (Exception e) {
throw new RuntimeException("OpenAI embedding response parsing failed", e);
}
}
}
위의 코드가 아래 한 줄로 대체됨:
public float[] embedWithoutCache(String text) {
return embeddingModel.embed(text);
}
| 항목 | 직접 구현 시 | Spring AI 사용 시 |
|---|---|---|
| HTTP 클라이언트 생성/타임아웃 설정 | RestClient.builder()... 직접 작성 | 자동 구성이 내부적으로 처리 |
| 인증 헤더 붙이기 | "Authorization", "Bearer " + apiKey 직접 작성 | spring.ai.openai.api-key 설정만 하면 끝 |
| 요청 바디 JSON 스키마 | OpenAI 스펙(model, input 필드명 등)을 코드로 숙지·작성 | embeddingModel.embed(text) 한 줄 |
| 응답 파싱 | data[0].embedding 경로를 알고 JsonNode로 직접 파싱 | float[]를 그냥 리턴받음 |
| 에러 처리(4xx/5xx, 재시도 대상 판단) | 직접 예외 매핑 | 라이브러리가 표준화된 예외 제공 |
application.yaml을 수정하거나:
spring.ai.model.embedding: none -> openai
app.search.embedding.enabled: false -> true
app.search.strategy: gin -> vector
또는 deploy.yml을 변경하면 됨:
"aws secretsmanager get-secret-value --secret-id community/melonne/openai/staging --region ap-northeast-2 --query SecretString --output text >> /tmp/staging.env",
"echo SPRING_AI_MODEL_EMBEDDING=openai >> /tmp/staging.env",
"echo APP_SEARCH_EMBEDDING_ENABLED=true >> /tmp/staging.env",
"echo APP_SEARCH_STRATEGY=vector >> /tmp/staging.env",
근데 application.yaml에서 비밀 아닌 값(embedding on/off, strategy 등)은 그냥 처리하고, OpenAI API 키 어디서 가져올지만 deploy.yml에 명시하면 될 듯.