Skip to Content
Software ArchitectureAudit Event에서 AI 챗봇까지
🏗️ Software Architecture2026년 3월 1일

Audit Event에서 AI 챗봇까지

#identity-platform#audit-event#chatbot#sse#auto-charting#fastapi#react
Identity Platform · Series 3 · Audit Event Architecture12 / 15Software Architecture
Dual Engine 챗봇의 최종 구현 — SSE 스트리밍, 자동 차트 생성, 그리고 NPE 배포.

엔진은 있지만 제품은 없었다

이전 글에서 Text-to-SQL과 RAG 엔진을 만들었다. LLM이 SQL을 생성하고, 문서를 검색해서 답변을 만들 수 있었다. 하지만 이 상태로는 API 호출을 직접 해야만 결과를 볼 수 있었다.

실제 사용자가 이 시스템을 활용하려면:

  • 대화형 인터페이스가 필요했다
  • 답변이 생성되는 동안 기다리는 게 아니라 실시간으로 스트리밍되어야 했다
  • 데이터 쿼리 결과는 숫자 나열이 아니라 차트로 시각화되어야 했다
  • 사내 플랫폼에 안정적으로 배포되어야 했다

전체 아키텍처

레이어기술 스택
FrontendReact, Vite, react-markdown, 차트 라이브러리
BackendFastAPI, Uvicorn, Pydantic
AI/MLDatabricks Vector Search, LLM Serving Endpoints
데이터Databricks SQL Warehouse, Delta Tables
배포Docker (multi-stage), NPE (사내 managed EKS)

SSE 스트리밍: 한 글자씩 흘러가는 답변

LLM 응답은 생성에 수 초가 걸린다. 사용자에게 빈 화면을 보여주고 기다리게 하는 것은 UX적으로 치명적이다. **Server-Sent Events(SSE)**를 사용해 토큰이 생성되는 즉시 프론트엔드로 스트리밍했다.

이벤트 프로토콜

백엔드에서 프론트엔드로 전달되는 SSE 이벤트 타입을 설계했다:

이벤트페이로드용도
intent{intent: "sql"} 또는 {intent: "wiki"}라우팅 결과 알림
sources[{url, title}]RAG 소스 문서 (wiki)
sql_query{query: "SELECT ..."}생성된 SQL (sql)
token{content: "답변"}스트리밍 텍스트 청크
chart_loading{}차트 생성 시작
data_table{data, chart_config}테이블 + 차트 데이터
done{model, tokens_used}완료
error{message}에러

백엔드 구현

python
# SSE 스트리밍 개념 예시 def sse_format(event: str, data: dict) -> str: return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" async def chat_stream(request): def generate(): intent = router.route(request.question) yield sse_format("intent", {"intent": intent}) if intent == "sql": for evt in sql_engine.query_stream(request.question): yield sse_format(evt["event"], evt["data"]) else: for evt in rag_engine.chat_stream(request.question): yield sse_format(evt["event"], evt["data"]) return StreamingResponse( generate(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} )

프론트엔드 수신

javascript
// SSE 수신 개념 예시 const response = await fetch('/chat/stream', { method: 'POST', body: ... }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.startsWith('event: ')) currentEvent = line.slice(7); if (line.startsWith('data: ')) handleEvent(currentEvent, JSON.parse(line.slice(6))); } }

Auto Charting: LLM이 차트까지 결정한다

Text-to-SQL의 결과는 보통 테이블 형태다. 하지만 “월별 로그인 추이”를 묻는 사용자에게 숫자 테이블만 보여주는 건 불친절하다. LLM이 데이터를 해석할 때, 적절한 차트 타입과 축 매핑까지 함께 결정하도록 했다.

작동 방식

LLM의 해석 프롬프트에 차트 설정을 함께 출력하도록 지시한다. 텍스트 답변 뒤에 ---CHART_CONFIG--- 마커를 넣고, 그 뒤에 JSON 형태의 차트 설정을 출력한다.

json
{ "chart_type": "stacked_bar", "x_column": "month", "y_columns": ["LOGIN", "REGISTRATION", "PASSWORD_RESET"], "title": "Monthly Events by Type" }

지원 차트 타입

14가지 이상의 차트 타입을 지원하도록 구현했다:

카테고리차트 타입
추이 분석line, area, composed
비교 분석bar, stacked_bar, horizontal_bar
비율 분석pie, donut, treemap
분포 분석scatter, radar
특수radial_bar, funnel, none

같은 데이터에 대해 bar ↔ stacked_bar ↔ line ↔ area 간 전환도 가능하도록 구현했다.

스트리밍 중 차트 감지

SSE 스트리밍 중에 CHART_CONFIG 마커를 감지하는 것은 까다로운 문제였다. 토큰이 한 글자씩 들어오기 때문에 마커가 여러 토큰에 걸쳐 나뉠 수 있다.

30자 look-ahead 버퍼를 두어, 마커가 완전히 들어올 때까지 텍스트 출력을 지연시키는 방식으로 해결했다. 마커가 감지되면 chart_loading 이벤트를 먼저 보내고, JSON 파싱이 완료되면 data_table 이벤트로 차트 데이터를 전달한다.

배포: NPE로 올리기

챗봇은 사내 managed EKS 플랫폼인 NPE에 배포했다. Docker multi-stage 빌드로 프론트엔드와 백엔드를 하나의 이미지에 담았다.

Docker 빌드 전략

dockerfile
# Stage 1: Frontend 빌드 FROM node:20-alpine AS frontend-builder WORKDIR /app/frontend COPY frontend/package*.json ./ RUN npm ci --production=false COPY frontend/ ./ RUN npm run build # Stage 2: Backend + Frontend 정적 파일 FROM python:3.11-slim WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY app.py backend/ ./ COPY --from=frontend-builder /app/frontend/dist ./frontend/dist EXPOSE 8000 CMD ["python", "app.py"]

FastAPI가 프론트엔드의 정적 파일도 함께 서빙하는 SPA(Single Page Application) 구조를 사용해서, Nginx 같은 별도 웹 서버 없이 하나의 프로세스로 동작한다.

완성된 이미지는 사내 managed EKS 플랫폼(NPE)에 컨테이너로 배포했다. 비용 효율을 위해 ARM64 아키텍처로 빌드했고, Databricks 자격 증명은 사내 시크릿 관리 시스템에서 주입받도록 구성했다.

운영에서 배운 것

프롬프트 버전 관리

LLM 프롬프트는 사실상 코드다. SQL 생성 프롬프트, 해석 프롬프트, 라우팅 프롬프트 모두 버전 관리가 필요했다. 프롬프트를 바꾸면 결과가 바뀌기 때문에, 변경 전후를 비교할 수 있는 테스트 세트를 만들어 두는 것이 중요했다.

예시 품질이 정확도를 결정한다

Text-to-SQL의 정확도를 가장 크게 올린 것은 모델을 바꾸는 게 아니라 예시 쿼리의 품질을 높이는 것이었다. 실제 사용자가 자주 묻는 질문과 그에 대한 정확한 SQL을 예시 인덱스에 추가할수록 정확도가 올라갔다.

안전 장치

  • 생성된 SQL은 SELECT만 허용, DDL/DML은 전부 차단
  • 기본 LIMIT 1000, 반환은 최대 200행
  • 쿼리 실행 타임아웃 설정
  • 에러 시 사용자에게 기술 세부 사항이 아닌 안내 메시지 표시

감사 이벤트에서 시작한 여정

이 챗봇의 출발점은 이 시리즈의 첫 글에서 다룬 감사 이벤트 데이터 설계였다. ActionType, TargetType, ActorType을 신중하게 정의한 것이 Gold 테이블의 컬럼이 되었고, 그 컬럼이 Vector Search 인덱스에 임베딩되어 자연어 질문의 답이 되었다.

“KPI를 뽑을 수 없었던 관리자 플랫폼”에서 시작한 문제가, 데이터 설계 → 파이프라인 → 메달리온 → AI 챗봇이라는 흐름을 거쳐, 관리자가 자연어로 KPI를 물어볼 수 있는 시스템으로 도달했다. 기술의 진화보다 더 중요했던 건 “어떤 질문에 답할 수 있어야 하는가”를 처음부터 명확히 한 것이었다.

다음 시리즈에서는 시야를 바꿔, 운영 중인 시스템의 성능 병목을 어떻게 찾고 개선했는지로 초점을 옮긴다.

Last updated on