PGVector 설치 및 설정
PGVector는 PostgreSQL의 오픈 소스 확장 기능으로, 벡터 데이터를 컬럼 타입으로 지원한다.
- docker-compose.yml
services:
pgvector:
image: pgvector/pgvector:pg17
container_name: pgvector
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgress
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- pg-network
volumes:
pgdata:
networks:
pg-network:
driver: bridge

- build.gradle
implementation 'org.springframework.ai:spring-ai-starter-model-vertex-ai-embedding'
implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector'
- application.yml
spring:
# PGVector
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres
# 정적 리소스 캐시 사용 안함 설정
web:
resources:
cache:
cachecontrol:
no-cache: true
no-store: true
must-revalidate: true
ai:
vertex:
ai:
embedding:
project-id: ${GCP_PROJECT_ID}
location: asia-northeast3
text:
options:
model: gemini-embedding-001
dimensions: 1536
gemini:
# ...
vectorstore:
pgvector:
initialize-schema: true # 테이블 자동 구성 여부
schema-name: public # 사용할 DB 내부 스키마 이름
table-name: vector_store # 사용할 테이블 이름
dimensions: 1536 # 저장할 Vector 차원 수
- 앱 실행 시 vector_store 테이블이 생성된다.
| 컬럼명 | 타입 | 설명 |
| id | uuid | 기본키 (PK), 자동으로 UUID 생성 |
| content | text | 원문 텍스트 데이터 |
| metadata | json | 메타데이터 (출처, 페이지 번호 등) |
| embedding | vector(1536) | 임베딩 모델을 통해 생성된 벡터 데이터 |

- VertexAiConfig
@Configuration
public class VertexAiConfig {
// ...
@Value("${spring.ai.vertex.ai.embedding.project-id}")
private String embeddingProjectId;
@Value("${spring.ai.vertex.ai.embedding.location}")
private String embeddingLocation;
// ...
@Bean
public VertexAiEmbeddingConnectionDetails connectionDetails() throws IOException {
byte[] decodedKey = Base64.getDecoder().decode(base64Credentials.trim());
GoogleCredentials credentials = GoogleCredentials.fromStream(new ByteArrayInputStream(decodedKey))
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"));
PredictionServiceSettings settings = PredictionServiceSettings.newBuilder()
.setEndpoint(embeddingLocation + "-aiplatform.googleapis.com:443")
.setCredentialsProvider(FixedCredentialsProvider.create(credentials))
.build();
return new VertexAiEmbeddingConnectionDetails(embeddingProjectId, embeddingLocation, "google", settings);
}
}
임베딩 테스트
Gemini 2.5 Flash 모델의 Cut-Off는 2025년 1월이므로, 검색 기능을 활성화하지 않는 한 Gemini는 2026년 2월 말에 시작된 미국-이란 전쟁에 대해 알지 못한다.
- EmbeddingController
더보기
@RestController
public class EmbeddingController {
private final VectorStore vectorStore;
public EmbeddingController(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@GetMapping(value = "/embed")
void embed() {
List<Document> documents = List.of(
new Document(
"2026년 미국-이란 전쟁 개요: 2026년 2월 28일, 미국 트럼프 대통령의 명령으로 'Operation Epic Fury' 작전이 개시되었다. " +
"미국과 이스라엘은 이란의 핵무기 개발 저지와 반정부 세력 지원을 명분으로 선전포고 없이 이란에 대한 기습 선제 타격을 감행했다. " +
"CENTCOM은 UTC 기준 06:35에 이란에 대한 공습을 시작했다고 발표했으며, " +
"미 해군 함정이 토마호크 미사일을 발사하고 B-2 스텔스 폭격기, B-1 랜서, B-52 등이 이란의 강화된 탄도미사일 시설을 타격했다.",
Map.of("category", "전쟁개요", "date", "2026-02-28", "source", "wikipedia")
),
new Document(
"2026년 이란 전쟁 이스라엘 작전 및 이란 지도부 제거: 2026년 2월 28일 UTC 06:45, " +
"이스라엘 공군은 전례 없는 참수 작전을 개시했다. 이란 최고지도자 알리 하메네이가 자신의 거주 단지에서 회의 중 사망했으며, " +
"여러 고위 관리들과 그의 가족들도 함께 사망했다. " +
"또한 미국 잠수함이 스리랑카 갈레 해안에서 이란 군함 IRIS Dena를 격침시켜 승조원 87명이 사망했다. " +
"해당 함정은 인도 비사카파트남에서 개최된 국제 함대 사열 2026에서 귀환하던 중이었다.",
Map.of("category", "군사작전", "date", "2026-02-28", "source", "wikipedia")
),
new Document(
"2026년 이란 전쟁 이란의 반격: 이란은 이스라엘 본토와 미군이 주둔한 바레인, 아랍에미리트, 쿠웨이트 등에 대한 반격을 개시했다. " +
"2026년 3월 5일, 이란 군사 소식통은 파르스 통신을 통해 2월 28일 이후 500발 이상의 탄도미사일 및 순항미사일과 " +
"약 2,000대의 드론을 발사했다고 밝혔다. " +
"이란은 또한 호르무즈 해협을 봉쇄하여 국제 원유 수송에 큰 차질을 빚었으며, 서아시아 전역이 전쟁에 연루되었다.",
Map.of("category", "이란반격", "date", "2026-03-05", "source", "wikipedia")
),
new Document(
"2026년 이란 전쟁 민간인 피해 및 인프라 타격: 2026년 3월 3일, 이란 적신월사는 600명 이상의 민간인이 사망했다고 발표했으며, " +
"이란 인권 활동가들은 742명의 민간인이 사망한 것으로 추정했다. " +
"이란의 한 학교 건물이 미사일 공격을 받아 초등학생 100명 이상과 50~100명의 인원이 사망했다. " +
"3월 7일, 적신월사는 미국-이스라엘 공격으로 6,668개 이상의 민간 시설이 표적이 되었다고 보고했다. " +
"사이버 공격으로 이란 내 인터넷이 거의 완전히 마비되어 60시간 이상 연결률이 평소의 1~4% 수준으로 떨어졌으며, " +
"정부 통신, 국영 미디어, 공공 서비스가 중단되었다.",
Map.of("category", "민간인피해", "date", "2026-03-07", "source", "적신월사")
),
new Document(
"2026년 이란 전쟁 휴전 합의: 약 5주간의 전쟁 끝에 2026년 4월 7일 조건부 휴전이 극적으로 합의되었다. " +
"이란은 호르무즈 해협을 재개방하기로 합의했다. " +
"4월 21일, 트럼프 대통령은 휴전을 무기한 연장했으나 이후에도 양측의 휴전 위반 사례가 지속되었다. " +
"이 휴전은 단순한 외교적 타협이 아닌 국제 구조, 국내 제도, 중견국 중재가 복합적으로 작용한 결과였다.",
Map.of("category", "휴전협상", "date", "2026-04-07", "source", "wikipedia")
),
new Document(
"2026년 이란 전쟁 이후 핵 협상 및 평화 협상: 2026년 5월 28일, 미국과 이란 협상단은 " +
"휴전 연장 및 이란 핵 프로그램 협상을 위한 60일 양해각서(MOU) 합의에 도달했으나 트럼프 대통령의 최종 승인은 미정이었다. " +
"MOU에는 호르무즈 해협의 무제한 통항 보장, 이란의 30일 내 해협 내 기뢰 제거, " +
"이란의 핵 농축 모라토리엄 및 고농축 우라늄 처리 방안 등이 포함되었다. " +
"그러나 이란 측은 호르무즈 해협 지위, 핵 프로그램, 레바논 친이란 무장세력 관련 핵심 쟁점에서 " +
"여전히 큰 이견이 남아 있다고 밝혔다.",
Map.of("category", "핵협상", "date", "2026-05-28", "source", "al-jazeera")
)
);
vectorStore.add(documents);
System.out.println("총 " + documents.size() + "개의 문서가 VectorStore에 임베딩되었습니다.");
}
}

RAG 파이프라인 구축하기
- build.gradle
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
- 기본 요청 DTO
public record ChatRequest(String message) {
}
- 기본 응답 DTO
public record ChatResponse(String answer) {
}
- RagController
@RestController
@RequestMapping("/api/rag")
public class RagController {
private final RagChatService ragChatService;
public RagController(RagChatService ragChatService) {
this.ragChatService = ragChatService;
}
@PostMapping
public ChatResponse ask(@RequestBody ChatRequest request) {
return new ChatResponse(ragChatService.askWithDocuments(request.message()));
}
}
- RagChatService
@Service
public class RagChatService {
private final ChatClient chatClient;
public RagChatService(ChatClient.Builder builder, VectorStore vectorStore) {
QuestionAnswerAdvisor questionAnswerAdvisor = QuestionAnswerAdvisor
.builder(vectorStore)
.build();
this.chatClient = builder
.defaultSystem("""
제공된 문서 컨텍스트를 우선하여 답변하세요.
문서에서 근거를 찾을 수 없으면 모른다고 답하세요.
답변 마지막에는 참고한 근거를 짧게 요약하세요.
""")
.defaultAdvisors(questionAnswerAdvisor)
.build();
}
public String askWithDocuments(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
RAG 테스트
- RAG 적용하지 않은 경우: 2026년의 데이터가 없기 때문에 과거의 이야기를 하고 있다.

- RAG를 적용한 경우: 임베딩한 문서를 바탕으로 2026년의 정보를 제공한다.

GitHub - qkrwns1478/ai-demo: Spring AI 실습
Spring AI 실습. Contribute to qkrwns1478/ai-demo development by creating an account on GitHub.
github.com
'프레임워크 > Spring' 카테고리의 다른 글
| [Spring Boot] Spring AI와 GCP Vertex API 연동하기 (0) | 2026.06.09 |
|---|---|
| [Spring Boot] Github Actions CI Workflow 파일 (0) | 2026.05.12 |
| [Spring Boot] Github Actions를 사용한 GCP 배포 가이드 (0) | 2026.05.06 |
| [Spring Boot] Spring CRUD 구현 - Day 5 : 댓글 기능 구현 (0) | 2026.03.16 |
| [Spring Boot] Spring CRUD 구현 - Day 4 : Spring Security + JWT (0) | 2026.03.13 |