실무형 패키지 구조와 핵심 코드 설계
1. 전체 패키지 구조
com.lemuel.ai
├── config
│ ├── AiConfig.java
│ └── VectorStoreConfig.java
│
├── rag
│ ├── controller
│ │ └── RagController.java
│ │
│ ├── service
│ │ ├── RagService.java
│ │ ├── QueryAnalyzer.java
│ │ ├── RetrievalOrchestrator.java
│ │ └── AnswerGenerator.java
│ │
│ ├── domain
│ │ ├── QueryIntent.java
│ │ └── RagContext.java
│ │
│ ├── embedding
│ │ └── EmbeddingService.java
│ │
│ ├── retrieval
│ │ └── VectorSearchService.java
│ │
│ ├── prompt
│ │ └── PromptBuilder.java
│ │
│ └── guard
│ └── AnswerGuard.java
├── config
│ ├── AiConfig.java
│ └── VectorStoreConfig.java
│
├── rag
│ ├── controller
│ │ └── RagController.java
│ │
│ ├── service
│ │ ├── RagService.java
│ │ ├── QueryAnalyzer.java
│ │ ├── RetrievalOrchestrator.java
│ │ └── AnswerGenerator.java
│ │
│ ├── domain
│ │ ├── QueryIntent.java
│ │ └── RagContext.java
│ │
│ ├── embedding
│ │ └── EmbeddingService.java
│ │
│ ├── retrieval
│ │ └── VectorSearchService.java
│ │
│ ├── prompt
│ │ └── PromptBuilder.java
│ │
│ └── guard
│ └── AnswerGuard.java
👉 핵심 포인트
- “LLM 호출 클래스” 하나로 끝내지 않음
- 의도 분석 / 검색 / 프롬프트 / 가드레일 분리
2. 핵심 흐름 (Service 레벨)
@Service
@RequiredArgsConstructor
public class RagService {
private final QueryAnalyzer queryAnalyzer;
private final RetrievalOrchestrator retrievalOrchestrator;
private final AnswerGenerator answerGenerator;
private final AnswerGuard answerGuard;
public String ask(String question) {
// 1. 의도 분석
QueryIntent intent = queryAnalyzer.analyze(question);
// 2. 컨텍스트 생성
RagContext context = retrievalOrchestrator.retrieve(question, intent);
// 3. 답변 생성
String answer = answerGenerator.generate(question, context);
// 4. 가드레일 적용
return answerGuard.validate(answer, context);
}
}
@RequiredArgsConstructor
public class RagService {
private final QueryAnalyzer queryAnalyzer;
private final RetrievalOrchestrator retrievalOrchestrator;
private final AnswerGenerator answerGenerator;
private final AnswerGuard answerGuard;
public String ask(String question) {
// 1. 의도 분석
QueryIntent intent = queryAnalyzer.analyze(question);
// 2. 컨텍스트 생성
RagContext context = retrievalOrchestrator.retrieve(question, intent);
// 3. 답변 생성
String answer = answerGenerator.generate(question, context);
// 4. 가드레일 적용
return answerGuard.validate(answer, context);
}
}
👉 이 구조 하나로
**“아키텍처 설계 가능한 개발자”**로 보입니다
3. QueryIntent (질의 분류)
public enum QueryIntent {
ORDER,
REFUND,
PRODUCT,
DELIVERY,
FAQ,
UNKNOWN
}
ORDER,
REFUND,
PRODUCT,
DELIVERY,
FAQ,
UNKNOWN
}
4. QueryAnalyzer (의도 분석)
@Component
public class QueryAnalyzer {
public QueryIntent analyze(String query) {
if (query.contains("환불")) return QueryIntent.REFUND;
if (query.contains("주문")) return QueryIntent.ORDER;
if (query.contains("배송")) return QueryIntent.DELIVERY;
return QueryIntent.UNKNOWN;
}
}
public class QueryAnalyzer {
public QueryIntent analyze(String query) {
if (query.contains("환불")) return QueryIntent.REFUND;
if (query.contains("주문")) return QueryIntent.ORDER;
if (query.contains("배송")) return QueryIntent.DELIVERY;
return QueryIntent.UNKNOWN;
}
}
👉 실제 실무에서는
- LLM 기반 분류 or
- 규칙 + ML 혼합 구조로 확장 가능
5. RetrievalOrchestrator (핵심 로직)
@Service
@RequiredArgsConstructor
public class RetrievalOrchestrator {
private final VectorSearchService vectorSearchService;
private final OrderService orderService;
public RagContext retrieve(String query, QueryIntent intent) {
List<String> documents = new ArrayList<>();
switch (intent) {
case REFUND:
documents.addAll(orderService.findRefundData(query));
documents.addAll(vectorSearchService.search(query));
break;
case PRODUCT:
documents.addAll(vectorSearchService.search(query));
break;
default:
documents.addAll(vectorSearchService.search(query));
}
return new RagContext(documents);
}
}
@RequiredArgsConstructor
public class RetrievalOrchestrator {
private final VectorSearchService vectorSearchService;
private final OrderService orderService;
public RagContext retrieve(String query, QueryIntent intent) {
List<String> documents = new ArrayList<>();
switch (intent) {
case REFUND:
documents.addAll(orderService.findRefundData(query));
documents.addAll(vectorSearchService.search(query));
break;
case PRODUCT:
documents.addAll(vectorSearchService.search(query));
break;
default:
documents.addAll(vectorSearchService.search(query));
}
return new RagContext(documents);
}
}
👉 핵심 포인트
- “무조건 벡터검색” X
- 정형 + 비정형 데이터 혼합
6. VectorSearchService
@Service
@RequiredArgsConstructor
public class VectorSearchService {
private final VectorStore vectorStore;
public List<String> search(String query) {
return vectorStore.similaritySearch(query)
.stream()
.map(Document::getContent)
.toList();
}
}
@RequiredArgsConstructor
public class VectorSearchService {
private final VectorStore vectorStore;
public List<String> search(String query) {
return vectorStore.similaritySearch(query)
.stream()
.map(Document::getContent)
.toList();
}
}
👉 pgvector / Qdrant / OpenSearch 다 여기로 추상화
7. PromptBuilder
@Component
public class PromptBuilder {
public String build(String question, List<String> docs) {
return """
당신은 전문 상담 AI입니다.
아래 문서를 기반으로만 답변하세요.
질문:
%s
문서:
%s
규칙:
- 모르면 모른다고 말할 것
- 추측하지 말 것
""".formatted(question, String.join("\n", docs));
}
}
public class PromptBuilder {
public String build(String question, List<String> docs) {
return """
당신은 전문 상담 AI입니다.
아래 문서를 기반으로만 답변하세요.
질문:
%s
문서:
%s
규칙:
- 모르면 모른다고 말할 것
- 추측하지 말 것
""".formatted(question, String.join("\n", docs));
}
}
8. AnswerGenerator (Spring AI 핵심)
@Service
@RequiredArgsConstructor
public class AnswerGenerator {
private final ChatClient chatClient;
private final PromptBuilder promptBuilder;
public String generate(String question, RagContext context) {
String prompt = promptBuilder.build(question, context.getDocuments());
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
@RequiredArgsConstructor
public class AnswerGenerator {
private final ChatClient chatClient;
private final PromptBuilder promptBuilder;
public String generate(String question, RagContext context) {
String prompt = promptBuilder.build(question, context.getDocuments());
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
9. AnswerGuard (가드레일)
@Component
public class AnswerGuard {
public String validate(String answer, RagContext context) {
if (answer.contains("추측")) {
return "정확한 정보를 찾을 수 없습니다.";
}
return answer;
}
}
public class AnswerGuard {
public String validate(String answer, RagContext context) {
if (answer.contains("추측")) {
return "정확한 정보를 찾을 수 없습니다.";
}
return answer;
}
}
👉 실제 확장:
- 금지어 필터
- PII 마스킹
- hallucination 검증
10. Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/rag")
public class RagController {
private final RagService ragService;
@PostMapping("/ask")
public String ask(@RequestBody String question) {
return ragService.ask(question);
}
}
@RequiredArgsConstructor
@RequestMapping("/api/rag")
public class RagController {
private final RagService ragService;
@PostMapping("/ask")
public String ask(@RequestBody String question) {
return ragService.ask(question);
}
}
11. 실무 확장 포인트
1️⃣ 캐싱
@Cacheable("rag-response")
public String ask(String question)
2️⃣ 비동기 처리
@Async
public CompletableFuture<List<String>> searchAsync(...)
public CompletableFuture<List<String>> searchAsync(...)
3️⃣ Observability
- /actuator/prometheus
- RAG latency metrics
- vector search hit rate
4️⃣ Batch Embedding
@Scheduled(cron = "0 0 * * * *")
public void reindex() {
embeddingService.rebuild();
}
public void reindex() {
embeddingService.rebuild();
}
5️⃣ Agent 확장
Tool: getOrderStatus()
Tool: calculateRefund()
Tool: fetchDeliveryInfo()
Tool: calculateRefund()
Tool: fetchDeliveryInfo()
👉 단순 QA → Action 가능한 AI
LIST
'Software > Maker(Spring & Python & node)' 카테고리의 다른 글
| Claude Design의 등장, 그리고 Figma와 Canva의 엇갈린 운명 (2) | 2026.04.21 |
|---|---|
| 지기지우(知己之友) — 한 사람의 마법을 알아보는 눈 (0) | 2026.04.21 |
| Spring AI 기반 RAG 아키텍처 설계기 (0) | 2026.04.17 |
| AI 시대에도 Spring은 죽지 않는다: 백엔드 개발자의 진화 방향 (0) | 2026.04.17 |
| Spring Boot 3.5.x 무료 지원 종료 후 유료 지원의 모든 것 — 가격, 기간, 패치 방식 비교 (0) | 2026.04.13 |