[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. 전체 아키텍처 및 흐름

✅ 전반적 흐름

  1. 사용자가 메시지를 입력하면 프론트엔드에서 SSE 연결을 생성
  2. 백엔드(Spring Boot)가 연결을 수립하고 AI 서버에 질문 전달
  3. AI 서버(Python)가 응답을 실시간 생성 및 전송
  4. 백엔드가 해당 내용을 프론트엔드에 스트리밍
  5. 프론트엔드는 응답을 실시간으로 출력

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://guiwoo.tistory.com/95

https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/

https://jforj.tistory.com/419

https://blog.riido.io/llm-structured-streaming-with-langchain-sse/