[V2] SSE(Server‐Sent Events) 도입 보고서 - 100-hours-a-week/6-nemo-ai GitHub Wiki
1. 개요
현재 챗봇 시스템은 사용자의 질문에 대해 응답이 모두 생성된 후에 결과를 보여주는 구조입니다. 그러나 LLM 기반 응답 생성에는 시간이 소요되며, 이로 인해 사용자는 지연을 체감하게 됩니다.
이 문제를 해결하기 위해 SSE(Server-Sent Events) 기반의 스트리밍 응답 방식을 도입함으로써, 챗봇이 응답을 생성하면서 실시간으로 사용자에게 보여주는 구조로 전환할 것을 제안합니다.
2. SSE란 무엇인가?
- **SSE(Server-Sent Events)**는 서버에서 클라이언트로 실시간으로 데이터를 전송할 수 있는 HTTP 기반의 단방향 통신 기술입니다. 브라우저가 기본적으로 지원하며, 복잡한 설정 없이도 실시간 UI 구현이 가능합니다.
- 서버 → 클라이언트 단방향 스트리밍
Content-Type: text/event-stream
으로 응답 전송- 클라이언트는
EventSource
객체로 메시지 수신
➡️ 특히 LLM 기반 응답을 사용하는 시스템이라면, SSE는 거의 모든 서비스에서 쉽게 통합될 수 있습니다.
- OpenAI, Hugging Face, 로컬 모델 등 LLM이 응답을 토큰 단위로 생성하는 구조와 매우 잘 맞음
- 챗봇뿐 아니라 모임 정보 생 등 LLM 기반 응용 서비스 전반에 적용 가능
예를 들어, 사용자가 긴 문장을 생성하거나 대화형 UI를 경험해야 하는 경우, SSE를 통해 LLM 응답이 생성되는 즉시 실시간으로 전달할 수 있어 대기 시간을 줄이고 사용자 만족도를 높일 수 있습니다.
3. 전체 아키텍처 및 흐름
✅ 전반적 흐름
- 사용자가 메시지를 입력하면 프론트엔드에서 SSE 연결을 생성
- 백엔드(Spring Boot)가 연결을 수립하고 AI 서버에 질문 전달
- AI 서버(Python)가 응답을 실시간 생성 및 전송
- 백엔드가 해당 내용을 프론트엔드에 스트리밍
- 프론트엔드는 응답을 실시간으로 출력
4. 역할별 기술 요구사항 (프론트, 백엔드, AI, 클라우드 포함)
🧩 프론트엔드 (Next.js)
항목 | 설명 |
---|---|
SSE 수신 | EventSource API를 사용해 서버에서 오는 응답을 수신 |
응답 출력 | 메시지를 onmessage 로 받아 실시간 렌더링 처리 |
사용자 경험 | 로딩 효과, 타이핑 효과, 오류 시 재연결 처리 필요 |
const eventSource = new EventSource('/api/chat/stream');
eventSource.onmessage = (e) => setChat(prev => prev + e.data);
🧩 백엔드 (Spring Boot)
항목 | 설명 |
---|---|
SSE 스트림 제공 | SseEmitter 로 클라이언트에 데이터 스트리밍 |
AI 서버 통신 | Python 서버에 HTTP 요청 후 응답 chunk를 받아 클라이언트로 전송 |
에러 처리 | 연결 중단, 타임아웃, 예외 처리 로직 필요 |
@GetMapping("/api/chat/stream")
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter();
// AI 응답 수신 후 emitter.send(chunk)
return emitter;
}
🧩 AI 서버 (Python, FastAPI)
항목 | 설명 |
---|---|
토큰 스트리밍 | LLM 응답을 생성되는 대로 yield 로 스트리밍 |
Streaming API | StreamingResponse 로 서버 → 백엔드에 실시간 응답 |
모델 통합 | OpenAI, Hugging Face, 로컬 모델 등 연동 가능 |
@app.post("/generate")
async def generate(request: Request):
async def stream():
for chunk in model.stream_response(prompt):
yield f"data: {chunk}\\n\\n"
return StreamingResponse(stream(), media_type="text/event-stream")
☁️ 클라우드/DevOps (GCP 기준)
항목 | 설명 |
---|---|
로드밸런서 | GCP HTTP(S) Load Balancer 사용 시, keep-alive , chunked 전송 허용 필요 |
타임아웃 | Backend 서비스의 timeout을 충분히 크게 설정 (기본 30초 → 300초 이상 권장) |
프록시/캐시 | 중간 계층(Nginx, CDN)에서 buffering 비활성화 설정 (proxy_buffering off ) |
CORS 설정 | AI 서버와 백엔드 도메인이 다를 경우, Access-Control-Allow-Origin 설정 필요 |
건강 체크 | /health 엔드포인트 구현 → GCP에서 상태 모니터링 가능 |
GKE 사용 시 | Ingress controller에서 proxy_read_timeout 설정 및 스트리밍 허용 필요 |
✅ 체크리스트:
- Load Balancer의 Backend timeout 증가
- AI 서버의 CORS 허용 설정 완료
- GKE 또는 Nginx의 buffering 비활성화
-
/health
구현 완료 및 Load Balancer에 등록
5. 장점 (Why SSE?)
항목 | 설명 |
---|---|
✅ 실시간성 | 사용자 응답 대기 시간을 줄이고, 스트리밍 UI 경험 제공 |
✅ 낮은 구현 난이도 | WebSocket 대비 설정과 유지가 간단 |
✅ 브라우저 호환성 | 별도 라이브러리 없이 대부분 브라우저에서 사용 가능 |
✅ HTTP 기반 | 기존 REST API 아키텍처와의 호환성 우수 |
6. 단점 및 고려사항
항목 | 설명 |
---|---|
❌ 단방향 제한 | 서버 → 클라이언트만 가능 (클라이언트 → 서버는 별도 처리) |
❌ 연결 유지 비용 | 서버와의 지속 연결이 많을 경우 커넥션 관리 필요 |
❌ 중간 계층 제한 | Cloudflare, CDN 등에서 SSE가 잘리거나 차단될 수 있음 |
❌ 모바일 네트워크 | 이동 중 연결 끊김 등 처리 필요 |
🔐 SSE 연결 유지와 보안 전략
SSE(Server-Sent Events)는 장시간 연결이 유지되며 실시간 데이터를 전달하는 데 적합하지만, 보안 및 리소스 관리 측면에서 주의가 필요합니다. 특히 LLM 기반 챗봇처럼 입력 대기 시간이 긴 경우에는 아래의 전략이 필수입니다.
✅ 주요 보안 이슈 및 대응
항목 | 문제 설명 | 대응 전략 |
---|---|---|
인증 없는 연결 | 토큰 없이 누구나 연결 가능 | JWT 기반 인증 후 SSE 허용 |
HTTPS 미적용 | 패킷 도청 및 중간자 공격 위험 | HTTPS 적용 필수 |
장시간 연결 | 서버 자원 과점유, DDOS 위험 | 연결 수 제한, 타임아웃 설정 |
CORS 문제 | 도메인 간 차단 가능성 | 명시적 CORS 허용 도메인 지정 |
📌 Spring Boot 인증 예시
@GetMapping("/api/chat/stream")
public SseEmitter stream(@RequestHeader("Authorization") String token) {
if (!isValidJWT(token)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
return new SseEmitter(300000L); // 5분 타임아웃
}
🕐 Idle 상태 대응 전략 (사용자 입력 없음)
SSE 연결은 사용자 입력이 없어도 계속 유지되므로 서버 자원 낭비가 발생할 수 있습니다. 이를 방지하기 위한 전략은 다음과 같습니다.
전략 | 설명 |
---|---|
서버 keep-alive ping | 주기적으로 빈 메시지 전송 → 네트워크 활성 상태 유지 |
클라이언트 idle 감지 | 마우스 움직임/입력 없을 시 연결 종료 |
자동 타임아웃 설정 | 서버/클라이언트 양쪽에서 시간 기반 종료 처리 |
📌 서버 ping 메시지 전송
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
emitter.send(SseEmitter.event().name("ping").data("keep-alive"));
} catch (IOException e) {
emitter.completeWithError(e);
}
}, 0, 30, TimeUnit.SECONDS); // 30초 간격
📌 클라이언트 측 idle 타이머
const eventSource = new EventSource("/api/chat/stream");
let idleTimer = setTimeout(() => {
eventSource.close(); // 5분 후 종료
}, 5 * 60 * 1000);
window.addEventListener("mousemove", () => clearTimeout(idleTimer));
✅ Best Practice 요약
- HTTPS 및 토큰 인증은 기본 전제
- SSE 연결은 서버/프론트 양쪽에서 idle 감지 및 종료 처리
- ping 메시지를 통해 네트워크 상태 확인
- SSE 커넥션 수 제한 또는 서버 스케일링 고려
7. 결론 및 제안
SSE는 현재 사용 중인 기술 스택(Next.js, Spring Boot, Python 기반 AI 서버, GCP 인프라)과 자연스럽게 통합될 수 있으며, 구현 비용 대비 사용자 경험 향상 효과가 큽니다.
특히 LLM 응답이 길고, 스트리밍이 가능한 구조에서는 반드시 고려해야 할 전략적 기술 선택지입니다. 이에 따라 다음 스프린트에서 해당 기능을 우선순위로 도입할 것을 권장합니다.
8. 자료
https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/
https://blog.riido.io/llm-structured-streaming-with-langchain-sse/