๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Project ESG+AI/[์‚ผ์ •KPMG]ESG ๋ฐ์ดํ„ฐ ํ™œ์šฉ ํ’€์Šคํ… ๊ฐœ๋ฐœ

32์ผ์ฐจ. ๋„๋ฉ”์ธ ์—ฐ๊ฒฐํ•˜๊ธฐ & ๋ฏธ๋“ค์›จ์–ด

by GreenJin_S2 2025. 11. 21.


๋ฏธ๋“ค์›จ์–ด๋ฅผ ์–ด๋–ป๊ฒŒ ํ• ๊ฒƒ์ธ์ง€

 

[React Component]
    ↓
[Zustand Store]       ← action์—์„œ api.* ํ˜ธ์ถœ
    ↓
[Frontend API Layer]  ← fetch ๋ž˜ํผ, ํ—ค๋”/ํ† ํฐ/์—๋Ÿฌ ๊ณตํ†ต ์ฒ˜๋ฆฌ
    ↓
[API Gateway]
    โ”œ /api/esg/** → ESG Service      (๋น„์ฆˆ๋‹ˆ์Šค ํŒŒ์ดํ”„๋ผ์ธ)
    โ”” /api/ai/**  → AI Orchestrator  (AI ๋ฏธ๋“ค์›จ์–ด ํŒŒ์ดํ”„๋ผ์ธ)
                          ↓
                   [AI ๋ฏธ๋“ค์›จ์–ด ์ฒด์ธ]
                     - LangChain
                     - MCP Tools
                     - LoRA Model ์„ ํƒ
                     - RAG ๊ฒ€์ƒ‰
                     - ๋กœ๊ทธ/ํ”„๋กœ๋น„๋„Œ์Šค

 

๊ฒฐ๊ตญ์— ai ๋ฏธ๋“ค์›จ์–ด๋Š” gateway์ดํ›„์— ai server๋กœ ๊ฐ€๋ฉด์„œ ์“ฐ๋Š” ๊ฒƒ์ด๊ณ 
ํ•œ๋งˆ๋””๋กœ ๋งํ•˜๋ฉด ๊ณต์žฅ์ž…๋‹ˆ๋‹ค
์–ด๋–ค ๊ณต์ •์„ ๋„ฃ์„ ๊ฒƒ์ธ๊ฐ€
์–ด๋–ป ๊ฒฝ๋กœ๋ฅผ ์žก์„ ๊ฒƒ์ธ๊ฐ€
์ด๊ฒƒ๋„ ์„ค๊ณ„์—์š”

 

 

Request
 → Input Validate
 → Context Load(organization, report...)
 → RAG Retrieval
 → MCP Tool ํ˜ธ์ถœ
 → Prompt ์กฐ๋ฆฝ
 → LoRA ๋ชจ๋ธ ์„ ํƒ
 → LLM ํ˜ธ์ถœ
 → Output Parsing
 → Citation ์ •๋ฆฌ
 → ์ €์žฅ(Log, Provenance)
 → Response // ๋Œ€์ถฉ ai ๋ฏธ๋“ค์›จ์–ด์˜ ๊ตฌ์กฐ๋Š” ์ด๋ ‡๋‹ค๋„ค์š”
์ผ๋‹จ ์ €ํฌ ํ™”๋ฉดํ•˜๊ณ  db์—ฐ๊ฒฐ์€ ๋ฌ๊ฑฐ๋“ ์š”.
๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด db์—์„œ ์ฟผ๋ฆฌ๋ฌธ ์‹คํ–‰ํ•จ.
์ €ํฌ๋Š” ์ค‘๊ฐ„์— ๋ฏธ๋“ค์›จ์–ด๋งŒ ๋„ฃ์–ด์ฃผ๋ฉด๋จ์š”.

 


(์•ž์—์„œ ์žก์•„๋‘” ๊ตฌ์กฐ๋Š” ์œ ์ง€ํ•˜๋ฉด์„œ “์„œ๋ฒ„ ์ƒํƒœ๋Š” React Query”, “์บ์‹œ·์„ธ์…˜์€ Redis”๋ผ๋Š” ์›์น™๋งŒ ์ถ”๊ฐ€ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.)
 
1. ์ „์ฒด ๊ทธ๋ฆผ (์—…๋ฐ์ดํŠธ ๋ฒ„์ „)
[React Component] ↓ [Store Context] - Zustand (UI/๋„๋ฉ”์ธ ์ƒํƒœ: ํ˜„์žฌ ํƒญ, ํŽธ์ง‘ ์ค‘ ์„น์…˜, ์„ ํƒ๋œ ์กฐ์ง ๋“ฑ) - React Query (์„œ๋ฒ„ ์ƒํƒœ: ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก, ๋ฆฌํฌํŠธ ๋ฆฌ์ŠคํŠธ, AI ์‘๋‹ต ์บ์‹œ ๋“ฑ) ↓ [Frontend API Layer] - apiFetch (fetch ๋ž˜ํผ, ํ† ํฐ/์—๋Ÿฌ/๊ณตํ†ต ํ—ค๋”) - esgApi / aiApi (๋„๋ฉ”์ธ๋ณ„ ํด๋ผ์ด์–ธํŠธ) ↓ [API Gateway (Spring Cloud Gateway)] ↓ [Backend Services] - ESG Service (PostgreSQL) - AI Orchestrator (LangChain + MCP + LoRA + VectorDB) ↓ [Infra] - PostgreSQL (์ •์‹ ์˜๊ตฌ ๋ฐ์ดํ„ฐ) - VectorDB / pgvector (RAG ์ธ๋ฑ์Šค) - Redis (์บ์‹œ, ์„ธ์…˜, Job ํ)
2. ํ”„๋ก ํŠธ์—”๋“œ: React Query + Zustand ๊ฐ™์ด ์“ฐ๋Š” ๋ฒ•
2-1. QueryClientProvider ์„ธํŒ…
main.tsx ๋˜๋Š” App.tsx ์ตœ์ƒ๋‹จ์— ๊ฐ์‹ธ ์ค๋‹ˆ๋‹ค.
// main.tsx (์˜ˆ์‹œ) import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import App from './App'; const queryClient = new QueryClient(); export function Root() { return ( <QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }
์ด๊ฑธ๋กœ React Query Context๊ฐ€ ์—ด๋ฆฌ๊ณ , ์–ด๋””์„œ๋“  useQuery, useMutation์„ ์“ธ ์ˆ˜ ์žˆ์Œ.
2-2. apiFetch ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ React Query ์–น๊ธฐ
์ด๋ฏธ ์žˆ๋Š” apiFetch๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ณ ,
 React Query๋Š” ๋‹จ์ง€ **“์–ธ์ œ/์–ด๋–ป๊ฒŒ ๋‹ค์‹œ ๋ถ€๋ฅผ์ง€ + ์บ์‹ฑ”**๋งŒ ๊ด€๋ฆฌํ•˜๊ฒŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
// lib/api/esg.ts import { apiFetch } from './base'; export const esgApi = { getChecklistItems: (params?: { category?: string; status?: string }) => { const search = new URLSearchParams(); if (params?.category) search.append('category', params.category); if (params?.status) search.append('status', params.status); return apiFetch(`/api/esg/checklists${search.toString() ? `?${search}` : ''}`); }, // ... };
์ด์ œ ์ด๊ฑธ React Query ํ›…์œผ๋กœ ๊ฐ์‹ธ๊ธฐ:
// hooks/useChecklistItems.ts import { useQuery } from '@tanstack/react-query'; import { esgApi } from '@/lib/api/esg'; export function useChecklistItems(params?: { category?: string; status?: string }) { return useQuery({ queryKey: ['checklists', params], queryFn: () => esgApi.getChecklistItems(params), staleTime: 1000 * 60, // 1๋ถ„ ๋™์•ˆ์€ ์‹ ์„ ํ•˜๋‹ค๊ณ  ๋ด„ }); }
์ปดํฌ๋„ŒํŠธ์—์„œ:
const ChecklistPage = () => { const { data, isLoading, error } = useChecklistItems({ category, status }); if (isLoading) return <Spinner />; if (error) return <ErrorBox message="์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค." />; return <ChecklistTable items={data} />; };
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ESG ์ฒดํฌ๋ฆฌ์ŠคํŠธ ๊ฐ™์ด “์„œ๋ฒ„์—์„œ ๋“ค๊ณ  ์˜ค๋Š” ๋ฆฌ์ŠคํŠธ๋“ค”์€ React Query๋กœ,
 ํ˜„์žฌ ํƒญ, ์„ ํƒ๋œ ํ–‰, ๋ชจ๋‹ฌ ์—ด๋ฆผ ์—ฌ๋ถ€ ๊ฐ™์€ ๊ฑด ๊ทธ๋Œ€๋กœ Zustand์— ๋‘๋ฉด ๋จ.
2-3. React Query 
 Zustand ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
์˜ˆ๋ฅผ ๋“ค์–ด, AI๊ฐ€ ์ƒ์„ฑํ•œ ์„น์…˜์„ Store์— ๋„ฃ๊ณ  ์‹ถ๋‹ค๋ฉด
 React Query onSuccess์—์„œ Zustand action๋งŒ ํ•œ ๋ฒˆ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.
// hooks/useGenerateSection.ts import { useMutation } from '@tanstack/react-query'; import { aiApi } from '@/lib/api/ai'; import { useReportStore } from '@/store/reportStore'; export function useGenerateSection() { const addSection = useReportStore((s) => s.addSection); return useMutation({ mutationFn: (payload: GenerateSectionPayload) => aiApi.generateSection(payload), onSuccess: (data, variables) => { // data.sectionText, data.metadata ๋“ฑ ๋ฐฑ์—”๋“œ ์‘๋‹ต ๊ตฌ์กฐ์— ๋งž๊ฒŒ addSection({ sectionKey: variables.sectionKey, content: data.sectionText, citations: data.citations, }); }, }); }
์ปดํฌ๋„ŒํŠธ:
const { mutate: generateSection, isPending } = useGenerateSection(); const handleClick = () => { generateSection({ organizationId, standard: 'IFRS_S2', sectionKey: 'governance', inputs: formValues, }); };
ํŒจํ„ด ์š”์•ฝ
์„œ๋ฒ„์—์„œ ์˜ค๋Š” ๋ฐ์ดํ„ฐ ๋ฆฌ์ŠคํŠธ/๋””ํ…Œ์ผ → React Query
ํŽ˜์ด์ง€ ๊ฐ„ ๊ณต์œ , ํŽธ์ง‘ ์ค‘ ์ƒํƒœ, ์ž„์‹œ UI ์ƒํƒœ → Zustand
ํ•„์š”ํ•  ๋•Œ onSuccess์—์„œ Store ์—…๋ฐ์ดํŠธ
3. AI/์ฑ—์— React Query ์“ฐ๋Š” ํŒจํ„ด
์ฑ„ํŒ…์€ ์ŠคํŠธ๋ฆฌ๋ฐ·์‹ค์‹œ๊ฐ„์„ฑ์ด ์žˆ์–ด์„œ
  • ์งง์€ ์š”์ฒญ/์‘๋‹ต → React Query useMutation
  • ์ง€์†์ ์ธ ๋Œ€ํ™” ์ƒํƒœ → Zustand messages ๋ฐฐ์—ด
์กฐํ•ฉ ์ถ”์ฒœ:
// store/chatSlice.ts export const createChatSlice: StateCreator<ChatState> = (set, get) => ({ messages: [], appendMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })), }); // hooks/useChatMutation.ts import { useMutation } from '@tanstack/react-query'; import { aiApi } from '@/lib/api/ai'; import { useChatStore } from '@/store/chatStore'; export function useChatMutation() { const appendMessage = useChatStore((s) => s.appendMessage); return useMutation({ mutationFn: (message: string) => aiApi.askConsultant({ organizationId: 'org-123', message }), onMutate: (message) => { appendMessage({ role: 'user', content: message }); }, onSuccess: (data) => { appendMessage({ role: 'assistant', content: data.answer }); }, }); }
4. ๋ฐฑ์—”๋“œ: Redis DB ์–ด๋””์— ๊ฝ‚์„์ง€
์ด์ œ ๋ ˆ๋””์Šค(๋ ˆ์ง€์Šค) DB๋ฅผ ๊ตฌ์กฐ์— ์ถ”๊ฐ€ํ•ด๋ด…์‹œ๋‹ค.
 ์—ญํ• ์„ ์• ๋งคํ•˜๊ฒŒ ์„ž์ง€ ๋ง๊ณ , ์•„์˜ˆ ์šฉ๋„๋ฅผ 3๊ฐœ๋กœ ๋‚˜๋ˆ„๋ฉด ๊น”๋”ํ•ด์š”.
4-1. Redis ์ฃผ์š” ์šฉ๋„ 3๊ฐ€์ง€
  1. AI ์‘๋‹ต ์บ์‹œ
    • ๊ฐ™์€ ์งˆ๋ฌธ/๊ฐ™์€ ์ปจํ…์ŠคํŠธ๋กœ ๋ฐ˜๋ณต ํ˜ธ์ถœ ์‹œ LoRA+RAG ๋น„์šฉ ์ ˆ๊ฐ
    • key ์˜ˆ: ai:cache:consult:{orgId}:{hash(prompt+ctx)}
  2. ์„ธ์…˜/๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ
  3. AI Orchestrator๊ฐ€ ์Šค์ผ€์ผ ์•„์›ƒ๋  ๋•Œ,
    •  ํŠน์ • ์œ ์ €์˜ ์ฑ„ํŒ… ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์–ด๋””์„œ๋‚˜ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
    • key ์˜ˆ: ai:session:{sessionId} → list(JSON messages)
  4. ๊ธด ์ž‘์—…(๋ณด๊ณ ์„œ ์ƒ์„ฑ) Job ํ
  5. “IFRS S2 ์ „์ฒด ๋ณด๊ณ ์„œ ์ƒ์„ฑ” ๊ฐ™์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„
    •  ๋น„๋™๊ธฐ Job์œผ๋กœ ๋˜์ง€๊ณ , ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ํด๋ง
4-2. AI Orchestrator์—์„œ Redis ์‚ฌ์šฉํ•˜๋Š” ์œ„์น˜
์•„๊นŒ ๋งŒ๋“  ๋ฏธ๋“ค์›จ์–ด ์ฒด์ธ์— Redis๋ฅผ ๋ผ์›Œ๋„ฃ์œผ๋ฉด ์ด๋ ‡๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:
[Controller] /api/ai/chat/consultant ↓ [AI Service] ↓ [Middleware Chain] 1) load_session_from_redis 2) check_answer_cache_in_redis 3) retrieve_docs_from_vector_db (RAG) 4) build_prompt 5) select_model_with_lora 6) call_llm 7) save_session_to_redis 8) save_cache_to_redis 9) log_provenance_to_postgres ↓ [Response]
(1) ์„ธ์…˜ ๋กœ๋“œ/์ €์žฅ
def load_session_from_redis(ctx): key = f"ai:session:{ctx.session_id}" messages = redis.lrange(key, 0, -1) # JSON ๋ฌธ์ž์—ด๋“ค ctx.history = [json.loads(m) for m in messages] return ctx def save_session_to_redis(ctx): key = f"ai:session:{ctx.session_id}" redis.rpush(key, json.dumps(ctx.new_user_message)) redis.rpush(key, json.dumps(ctx.new_assistant_message)) redis.expire(key, 60 * 60 * 24) # 1์ผ TTL return ctx
(2) ์บ์‹œ
def check_answer_cache_in_redis(ctx): cache_key = f"ai:cache:consult:{ctx.org_id}:{ctx.prompt_hash}" cached = redis.get(cache_key) if cached: ctx.answer = json.loads(cached) ctx.from_cache = True return ctx def save_cache_to_redis(ctx): if not ctx.from_cache: cache_key = f"ai:cache:consult:{ctx.org_id}:{ctx.prompt_hash}" redis.setex(cache_key, 60 * 10, json.dumps(ctx.answer)) # 10๋ถ„ ์บ์‹œ return ctx
์ด๋ ‡๊ฒŒ ํ•ด๋†“์œผ๋ฉด
Redis๋Š” ์ˆœ์ˆ˜ํ•˜๊ฒŒ “๋น ๋ฅธ ํœ˜๋ฐœ์„ฑ ๋ฐ์ดํ„ฐ”์šฉ,
Postgres๋Š” “๊ฒ€์ฆ ๊ฐ€๋Šฅํ•œ ์˜๊ตฌ ๋ฐ์ดํ„ฐ”์šฉ์œผ๋กœ ์—ญํ• ์ด ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
4-3. ESG Service ์ชฝ Redis ํ™œ์šฉ (์„ ํƒ)
ESG Service(Spring) ์ž…์žฅ์—์„œ๋„:
  • ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๊ธฐ์ค€/์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
  •  → Redis ์บ์‹œ
์˜ˆ:
// pseudo-code public ChecklistItem getChecklistItem(Long id) { String key = "esg:checklist:" + id; ChecklistItem cached = redisTemplate.opsForValue().get(key); if (cached != null) return cached; ChecklistItem item = repository.findById(id).orElseThrow(); redisTemplate.opsForValue().set(key, item, Duration.ofMinutes(5)); return item; }
5. ์ตœ์ข… ๊ตฌ์กฐ ์š”์•ฝ (์งง๊ฒŒ)
  1. ํ”„๋ก ํŠธ
  • React Query: ์„œ๋ฒ„ ์ƒํƒœ·AI ์‘๋‹ต·์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋“ฑ “๋ฐฑ์—”๋“œ์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ์ดํ„ฐ”
  • Zustand: ํ˜„์žฌ ํƒญ, ํŽธ์ง‘ ์ƒํƒœ, ๋กœ์ปฌ UI ์ƒํƒœ, ์„ ํƒ๋œ ์„น์…˜ ๋“ฑ
  • ๋‘˜ ์‚ฌ์ด ์—ฐ๊ฒฐ์€ onSuccess์—์„œ Store ์—…๋ฐ์ดํŠธ ๋ฐฉ์‹
  1. ๋ฐฑ์—”๋“œ
  • AI Orchestrator ๋‚ด๋ถ€:
  • LangChain + MCP + LoRA๋Š” ์—ฌ์ „ํžˆ ๋ฏธ๋“ค์›จ์–ด ์ฒด์ธ ์•ˆ
  • Redis:
  • ์„ธ์…˜(๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ)
  • AI ์‘๋‹ต/์ปจํ…์ŠคํŠธ ์บ์‹œ
  • (์„ ํƒ) Job ํ
  • ESG Service:
  • ๊ธฐ์ค€/์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋“ฑ ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ์—”ํ‹ฐํ‹ฐ ์บ์‹œ ์šฉ๋„๋กœ Redis ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  1. API ๊ณ„์•ฝ
  • ํ”„๋ก ํŠธ๋Š” ๊ณ„์† /api/esg/..., /api/ai/...๋งŒ ๋ด„
  • React Query๋Š” ์ด ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ฐ์‹ธ๊ณ ,
  •  Redis·LangChain·LoRA·MCP๋Š” ์ „๋ถ€ ๋ฐฑ์—”๋“œ ๋‚ด๋ถ€ ๊ตฌํ˜„ ๋””ํ…Œ์ผ๋กœ ์ˆจ๊น€
์›ํ•˜๋ฉด ๋‹ค์Œ ๋‹จ๊ณ„๋กœ๋Š”:
  • ์‹ค์ œ package.json์— @tanstack/react-query ์ถ”๊ฐ€ํ•˜๋Š” ๋ถ€๋ถ„ + ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ
  • Zustand slice + React Query hook ์„ธํŠธ๋ฅผ
  •  chat, report, checklist ๊ฐ๊ฐ์— ๋Œ€ํ•ด ๊ตฌ์ฒด์ ์œผ๋กœ ์งœ์ค„๊ฒŒ.
  •  

  • React Query + Zustand + Redis๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•œ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜ (2025๋…„ ๋ฒ„์ „)

    ์ตœ๊ทผ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์— React Query๋ฅผ ๋ณธ๊ฒฉ ๋„์ž…ํ•˜๊ณ , Redis๋ฅผ ๋ช…ํ™•ํ•œ ์—ญํ• ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด์— ์ž˜ ์“ฐ๋˜ Zustand์™€ ์ถฉ๋Œ ์—†์ด ๊ณต์กด์‹œํ‚ค๋Š” ํŒจํ„ด + Redis ์šฉ๋„๋ฅผ ๋ช…ํ™•ํžˆ ๋‚˜๋ˆˆ ์ตœ์ข… ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    text
     
    [React Component]
          ↓
    [Store Context]
       โ”œโ”€ Zustand          → UI/๋„๋ฉ”์ธ ์ƒํƒœ (ํ˜„์žฌ ํƒญ, ํŽธ์ง‘ ์ค‘ ์„น์…˜, ์„ ํƒ๋œ ์กฐ์ง, ๋ชจ๋‹ฌ ์ƒํƒœ ๋“ฑ)
       โ””โ”€ React Query       → ์„œ๋ฒ„ ์ƒํƒœ (์ฒดํฌ๋ฆฌ์ŠคํŠธ ๋ชฉ๋ก, ๋ฆฌํฌํŠธ ๋ฆฌ์ŠคํŠธ, AI ์‘๋‹ต ๋“ฑ)
          ↓
    [Frontend API Layer]
       โ”œโ”€ apiFetch (ํ† ํฐ/์—๋Ÿฌ/ํ—ค๋” ๊ณตํ†ต ์ฒ˜๋ฆฌ)
       โ””โ”€ esgApi / aiApi (๋„๋ฉ”์ธ๋ณ„ ํด๋ผ์ด์–ธํŠธ)
          ↓
    [API Gateway - Spring Cloud Gateway]
          ↓
    [Backend Services]
       โ”œโ”€ ESG Service → PostgreSQL
       โ””โ”€ AI Orchestrator → LangChain + MCP + LoRA + VectorDB
          ↓
    [Infra]
       โ”œโ”€ PostgreSQL      → ์˜๊ตฌ ๋ฐ์ดํ„ฐ, ๊ฐ์‚ฌ ๊ฐ€๋Šฅ ๋ฐ์ดํ„ฐ
       โ”œโ”€ VectorDB/pgvector → RAG ์ธ๋ฑ์Šค
       โ””โ”€ Redis           → ์บ์‹œ + ์„ธ์…˜ + Job ํ (ํœ˜๋ฐœ์„ฑ ๋น ๋ฅธ ๋ฐ์ดํ„ฐ ์ „์šฉ)
    ํ•ต์‹ฌ ์›์น™
    • ์„œ๋ฒ„ ์ƒํƒœ๋Š” React Query
    • ์บ์‹œ·์„ธ์…˜·Job ํ๋Š” Redis
    2. ํ”„๋ก ํŠธ์—”๋“œ: React Query + Zustand ์™„๋ฒฝ ์กฐํ•ฉ ํŒจํ„ด
    tsx
     
    // main.tsx
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
    import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: 1,
          refetchOnWindowFocus: false,
        },
      },
    });
    
    export function Root() {
      return (
        <QueryClientProvider client={queryClient}>
          <App />
          <ReactQueryDevtools initialIsOpen={false} />
        </QueryClientProvider>
      );
    }
    2-2. ๊ธฐ์กด apiFetch ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด์„œ React Query ์–น๊ธฐ
    TypeScript
     
    // lib/api/esg.ts (๊ธฐ์กด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ)
    export const esgApi = {
      getChecklistItems: (params?) => apiFetch(`/api/esg/checklists?...`),
    };
    
    // hooks/useChecklistItems.ts (React Query๋กœ ๊ฐ์‹ธ๊ธฐ)
    export function useChecklistItems(params?) {
      return useQuery({
        queryKey: ['checklists', params],
        queryFn: () => esgApi.getChecklistItems(params),
        staleTime: 1000 * 60, // 1๋ถ„
      });
    }
    → ์„œ๋ฒ„์—์„œ ์˜ค๋Š” ๋ฆฌ์ŠคํŠธ๋Š” ์ „๋ถ€ React Query → ํƒญ ์ƒํƒœ, ์„ ํƒ๋œ ํ–‰, ๋ชจ๋‹ฌ ์˜คํ”ˆ ์—ฌ๋ถ€ ๋“ฑ์€ ๊ทธ๋Œ€๋กœ ZustandAI๊ฐ€ ์„น์…˜ ์ƒ์„ฑ → ๋ฐ”๋กœ ๋ฆฌํฌํŠธ ์Šคํ† ์–ด์— ๋„ฃ๊ณ  ์‹ถ์„ ๋•ŒํŒจํ„ด ์š”์•ฝ
    • ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ → React Query
    • UI/ํŽธ์ง‘ ์ƒํƒœ → Zustand
    • ํ•„์š” ์‹œ onSuccess์—์„œ Store ์—…๋ฐ์ดํŠธ
    3. AI ์ฑ„ํŒ…์— React Query ์“ฐ๋Š” ์ถ”์ฒœ ํŒจํ„ด
    TypeScript
     
    // store/chatStore.ts
    messages: Message[];
    appendMessage: (msg) => set(state => ({ messages: [...state.messages, msg] }));
    
    // hooks/useChatMutation.ts
    export function useChatMutation() {
      const appendMessage = useChatStore(s => s.appendMessage);
    
      return useMutation({
        mutationFn: (message: string) => aiApi.askConsultant({ message }),
        onMutate: (message) => {
          appendMessage({ role: 'user', content: message });
        },
        onSuccess: (data) => {
          appendMessage({ role: 'assistant', content: data.answer });
        },
      });
    }
    → ๋ฉ”์‹œ์ง€ ๋ฐฐ์—ด์€ Zustand, ์š”์ฒญ·์‘๋‹ต์€ React Query mutation
     
    ์šฉ๋„ํ‚ค ์˜ˆ์‹œTTL์„ค๋ช…
    1. AI ์‘๋‹ต ์บ์‹œ ai:cache:consult:{orgId}:{promptHash} 10๋ถ„ ๊ฐ™์€ ์งˆ๋ฌธ ๋ฐ˜๋ณต ์‹œ LoRA+RAG ๋น„์šฉ ์ ˆ๊ฐ
    2. ์„ธ์…˜/๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ai:session:{sessionId} 24์‹œ๊ฐ„ ์Šค์ผ€์ผ ์•„์›ƒ ์‹œ ์–ด๋””์„œ๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ
    3. Job ํ (๋ณด๊ณ ์„œ ์ƒ์„ฑ ๋“ฑ) ai:job:report:{jobId} ์ž‘์—… ์ข…๋ฃŒ ์‹œ ์‚ญ์ œ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—… ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
    AI Orchestrator ๋ฏธ๋“ค์›จ์–ด ์ฒด์ธ์— Redis ๋ผ์šฐ๊ธฐRedis ์บ์‹œ ์˜ˆ์‹œ ์ฝ”๋“œ (Python)5. ์ตœ์ข… ์š”์•ฝ (ํ•œ๋ˆˆ์— ๋ณด๋Š” ํ•ต์‹ฌ ์›์น™)์ด์ œ ํ”„๋ก ํŠธ๋Š” /api/esg/..., /api/ai/...๋งŒ ๋ณด๋ฉด ๋˜๊ณ , Redis·LangChain·LoRA·MCP๋Š” ์ „๋ถ€ ๋ฐฑ์—”๋“œ ๋‚ด๋ถ€ ๊ตฌํ˜„ ๋””ํ…Œ์ผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ˆจ๊ฒจ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค.
    • package.json์— react-query ์ถ”๊ฐ€ + devtools ์„ค์ •
    • checklist / report / chat ๊ฐ๊ฐ์— ๋Œ€ํ•œ ์™„์„ฑ๋œ hook + slice ์ฝ”๋“œ ๋“œ๋ฆด๊ฒŒ์š”!
    ์ด ๊ตฌ์กฐ๋กœ ๊ฐ€๋ฉด ํ™•์žฅ์„ฑ๋„ ์ข‹๊ณ , ์บ์‹œ ํžˆํŠธ์œจ๋„ ์˜ฌ๋ผ๊ฐ€๊ณ , ์ฝ”๋“œ๋„ ํ›จ์”ฌ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค! ๐Ÿš€
  • ๋‹ค์Œ์— ์›ํ•˜์‹œ๋ฉด ์‹ค์ œ๋กœ
  •  
    ๊ณ„์ธต๋‹ด๋‹น ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ด๋‹น ๋ฐ์ดํ„ฐ๋น„๊ณ 
    ํ”„๋ก ํŠธ ์ƒํƒœ React Query ์„œ๋ฒ„์—์„œ ์˜ค๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ (๋ฆฌ์ŠคํŠธ, AI ์‘๋‹ต) ์บ์‹œ·์ž๋™ ๋ฆฌํ”„๋ ˆ์‹œ ๋‹ด๋‹น
    ํ”„๋ก ํŠธ ์ƒํƒœ Zustand UI ์ƒํƒœ, ํŽธ์ง‘ ์ค‘ ๋ฐ์ดํ„ฐ, ํƒญ, ๋ชจ๋‹ฌ ๋“ฑ ๊ฐ€๋ฒผ์šด ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ
    ๋ฐฑ์—”๋“œ ์บ์‹œ Redis AI ์‘๋‹ต ์บ์‹œ, ์„ธ์…˜, Job ํ ํœ˜๋ฐœ์„ฑ ๋น ๋ฅธ ๋ฐ์ดํ„ฐ ์ „์šฉ
    ๋ฐฑ์—”๋“œ ์˜๊ตฌ PostgreSQL ์ฒดํฌ๋ฆฌ์ŠคํŠธ, ๋ฆฌํฌํŠธ ์ตœ์ข…๋ณธ, ๋กœ๊ทธ ๋“ฑ ๊ฐ์‚ฌ·๋ฒ•์  ์ฆ์ ์šฉ
    ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ VectorDB / pgvector RAG์šฉ ๋ฌธ์„œ ์ž„๋ฒ ๋”ฉ  
  • Python
     
    def check_answer_cache_in_redis(ctx):
        key = f"ai:cache:consult:{ctx.org_id}:{ctx.prompt_hash}"
        cached = redis.get(key)
        if cached:
            ctx.answer = json.loads(cached)
            ctx.from_cache = True
        return ctx
    
    def save_cache_to_redis(ctx):
        if not ctx.from_cache:
            key = f"ai:cache:consult:{ctx.org_id}:{ctx.prompt_hash}"
            redis.setex(key, 600, json.dumps(ctx.answer))  # 10๋ถ„
        return ctx
  • Python
     
    [Controller]
       ↓
    1. load_session_from_redis
    2. check_answer_cache_in_redis     ← ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ๋ฆฌํ„ด!
    3. retrieve_docs_from_vector_db
    4. build_prompt
    5. select_model_with_lora
    6. call_llm
    7. save_session_to_redis
    8. save_cache_to_redis
    9. log_provenance_to_postgres
       ↓
    [Response]
  • 4. ๋ฐฑ์—”๋“œ: Redis ์šฉ๋„๋ฅผ ๋ช…ํ™•ํžˆ 3๊ฐ€์ง€๋กœ๋งŒ ์“ฐ๊ธฐ
  • ์ฑ„ํŒ…์€ ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•˜๋ฏ€๋กœ ์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.
  • TypeScript
     
    // hooks/useGenerateSection.ts
    export function useGenerateSection() {
      const addSection = useReportStore(s => s.addSection);
    
      return useMutation({
        mutationFn: payload => aiApi.generateSection(payload),
        onSuccess: (data, variables) => {
          addSection({
            sectionKey: variables.sectionKey,
            content: data.sectionText,
            citations: data.citations,
          });
        },
      });
    }
  • 2-3. React Query ↔ Zustand ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•  ๋•Œ
  • ๊ธฐ์กด apiFetch๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ , React Query๋Š” ์บ์‹ฑ·๋ฆฌํ”„๋ ˆ์‹œ ์ •์ฑ…๋งŒ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.
  • 2-1. QueryClientProvider ์„ธํŒ… (์ตœ์ƒ์œ„)
  • 1. ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ๊ทธ๋ฆผ (์ตœ์‹  ๋ฒ„์ „)

๐ŸŒˆ “์ง€๊ธˆ ์šฐ๋ฆฌ๊ฐ€ ๋ญ˜ ํ•˜๊ณ  ์žˆ๋Š” ๊ฑฐ๋ƒ๋ฉด์š”…”

โžก๏ธ ๋ณต์žกํ•œ ‘์•ฑ’์ด ์žˆ๋Š”๋ฐ, ์ด๊ฑธ ๋” ๋น ๋ฅด๊ณ , ๋œ ๋ง๊ฐ€์ง€๊ณ , ๋” ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ์ •๋ฆฌํ•˜๋Š” ๊ณผ์ •์ด์•ผ!

์กฐ๊ธˆ ๋” ๊ท€์—ฌ์šด ๋น„์œ ๋กœ ๊ฐ€๋ณด์ž ๐Ÿ‘‡


๐Ÿ  1. ์šฐ๋ฆฌ ์ง‘(ํ”„๋กœ์ ํŠธ)์ด ๋„ˆ๋ฌด ๋ณต์žกํ–ˆ์–ด

์›๋ž˜:

  • ์ง(๋ฐ์ดํ„ฐ)์ด ์—ฌ๊ธฐ์ €๊ธฐ ๋ง‰ ๊ตด๋Ÿฌ๋‹ค๋‹˜
  • ๋ˆ„๊ฐ€ ๋ญ ๋“ค๊ณ  ๊ฐ”๋Š”์ง€ ๋ชจ๋ฆ„
  • ์ฒญ์†Œํ•˜๊ธฐ๋„ ์–ด๋ ค์›€
  • ๋ฐฉ ํ•˜๋‚˜ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค๋ฅธ ๋ฐฉ์ด ๋‚œ๋ฆฌ๋‚จ

๊ทธ๋ž˜์„œ ์ง‘ ๊ตฌ์กฐ(์•„ํ‚คํ…์ฒ˜) ์ž์ฒด๋ฅผ ์˜ˆ์˜๊ฒŒ ๋‹ค์‹œ ๊พธ๋ฏธ๋Š” ์ค‘์ด์•ผ.


๐Ÿ  2. ๋ฐฉ์„ ์—ญํ• ๋ณ„๋กœ ๋”ฑ ๋‚˜๋ˆด์–ด

๐Ÿ”ต Zustand ๋ฐฉ

โžก๏ธ “์ง€๊ธˆ ๋ญ ๋ณด๊ณ  ์žˆ์–ด?”, “์–ด๋–ค ์ฐฝ ์—ด์—ˆ์ง€?” ๊ฐ™์€ UI ์ƒํƒœ ์ €์žฅํ•˜๋Š” ๋ฐฉ

๐ŸŸข React Query ๋ฐฉ

โžก๏ธ “์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ์ง„์งœ ๋ฐ์ดํ„ฐ”๋ฅผ ์ €์žฅํ•˜๋Š” ๋ƒ‰์žฅ๊ณ  ๊ฐ™์€ ๋ฐฉ
(์‹œ๊ฐ„ ์ง€๋‚˜๋ฉด ์ž๋™์œผ๋กœ ์ƒˆ๊ฑธ๋กœ ๊ฐˆ์•„๋ผ์›Œ์ฃผ๋Š” ๋˜‘๋˜‘ํ•œ ๋ƒ‰์žฅ๊ณ )

๐Ÿ”ด Redis ์ฐฝ๊ณ 

โžก๏ธ “์ž ๊น๋งŒ ์—ฌ๊ธฐ ๋„ฃ์–ด๋‘ฌ!” ํ•˜๋Š” ์ž„์‹œ ์ฐฝ๊ณ 

  • ์บ์‹œ
  • ์„ธ์…˜
  • ๋นจ๋ฆฌ๋นจ๋ฆฌ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ž‘์—… ํ

๋“ฑ๋“ฑ ํœ˜๋ฐœ์„ฑ ๋ฐ์ดํ„ฐ ๋ณด๊ด€


๐Ÿญ 3. ๊ทธ๋ž˜์„œ ๋ญ๊ฐ€ ์ข‹์•„์กŒ๋ƒ๋ฉด…

  • ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„์— ๊ฐ€์ง€ ์•Š์•„๋„ ๋จ → ๋นจ๋ผ์ง
  • ๋ญ๊ฐ€ ์–ด๋”” ์ €์žฅ๋ผ ์žˆ๋Š”์ง€ ๋ช…ํ™•
  • ์‹ค์ˆ˜๋กœ ๋ฐ์ดํ„ฐ ์—‰ํ‚ค๋Š” ๊ฒŒ ์ค„์–ด๋“ฆ
  • AI ์‘๋‹ต๋„ ์ค‘๋ณต ๊ณ„์‚ฐ ์•ˆ ํ•ด๋„ ๋จ → ๋ˆ ์ ˆ์•ฝ
  • ๊ตฌ์กฐ๊ฐ€ ๋„ˆ๋ฌด ๊น”๋”ํ•ด์„œ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ๋ฏธ์นœ ๋“ฏ์ด ํŽธํ•จ

๐Ÿ“ฆ 4. ํ”„๋ก ํŠธ์—”๋“œ๋„ ์ด๋ ‡๊ฒŒ ๋ง๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•จ

  • ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ์ดํ„ฐ = React Query
  • ํ™”๋ฉด์—์„œ๋งŒ ํ•„์š”ํ•˜๊ฑฐ๋‚˜, ์ž ๊น ์“ฐ๋Š” ์ƒํƒœ = Zustand
  • ๋‘˜์ด ํ•จ๊ป˜ ์จ์•ผ ํ•˜๋ฉด, React Query ์„ฑ๊ณต → Zustand์— ๋„ฃ๊ธฐ

์™„๋ฒฝํ•œ ์—ญํ•  ๋ถ„๋‹ด!

๋ง ๊ทธ๋Œ€๋กœ ์ „์ฒด ๊ตฌ์กฐ๊ฐ€
“์–ด? ์ด ๋ฐ์ดํ„ฐ๋Š” ์–ด๋А ๋ฐฉ์— ์žˆ์–ด์•ผ ํ•˜์ง€?”
์ด ๊ณ ๋ฏผ์ด 0์ด ๋œ ๊ฑฐ์ž„.


๐Ÿ”ฅ 5. ๋ฐฑ์—”๋“œ๋„ ์—ญํ• ์ด ์ •๋ฆฌ๋จ

  • PostgreSQL → ์˜๊ตฌ ์ €์žฅ(์‚ฌ๋ผ์ง€๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ๋“ค)
  • Redis → ์ž„์‹œ ์ €์žฅ(๋นจ๋ฆฌ ์“ฐ๊ณ  ๋นจ๋ฆฌ ์ง€์›Œ๋„ ๋˜๋Š” ๊ฒƒ๋“ค)
  • VectorDB → AI๊ฐ€ ๋ฌธ์„œ ์ž˜ ์ฐพ๊ฒŒ ๋„์™€์ฃผ๋Š” ๊ฒ€์ƒ‰ ์ „์šฉ

๐ŸŒŸ ๊ฒฐ๋ก  (์ดˆ๋”ฉ ๋ฒ„์ „)

“์•ฑ ์•ˆ์— ์žˆ๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋””์— ๋„ฃ์–ด์•ผ ํ• ์ง€ ๋”ฑ ์ •ํ•ด๋†“๊ณ , ๋” ๋น ๋ฅด๊ณ  ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ์ •๋ฆฌ ์ž‘์—…์„ ํ•˜๊ณ  ์žˆ๋Š” ์ค‘์ด๋‹ค!”

๊ทธ๋ฆฌ๊ณ  ๋„ˆ๊ฐ€ ์ •๋ฆฌํ•œ ๊ทธ ๊ตฌ์กฐ๋Š”
2025๋…„์— ์“ฐ๋Š” ‘๋ชจ๋ฒ”๋‹ต์•ˆ’ ์ˆ˜์ค€์œผ๋กœ ๊น”๋”ํ•จ.
์ง„์งœ ์ž˜ ์žก์•„๋†จ์–ด.

 

 


๋„๋ฉ”์ธ ์—ฐ๊ฒฐํ•˜๊ธฐ

 

 

 

 




๋ฒ„์…€ ์ ‘์†ํ•ด์„œ ํšŒ์›๊ฐ€์ž…ํ•˜๊ณ 

ํ”„๋ก ํŠธ์—”๋“œ๋งŒ ๋ณต์‚ฌํ•ด์„œ www.lca.infoํด๋”  ๋งŒ๋“ค๊ณ  ๊นƒํ—ˆ๋ธŒ ๋‹ค์‹œ ๋งŒ๋“ค๊ธฐ

 

๋ฒ„์…€์—์„œ ๊นƒํ—ˆ๋ธŒ๋กœ ์—ฐ๊ฒฐํ•˜๊ณ 

์˜ฌ๋ฆฌ๋ ค๋Š” ํ”„๋ก ํŠธ์—”๋“œ ํด๋”์— ์ž„ํฌํŠธ ๋ˆ„๋ฅด๊ธฐ

 

์—๋Ÿฌ ๋‚˜์˜ค๋ฉด ํด๋กœ๋“œ์— ๊ณ„์† ๋ฌผ์–ด๋ณด๊ธฐ