[SpringBoot] AI LLM 실시간 스트리밍 응답, 왜 SSE + Spring WebFlux 조합일까?

devdo·2026년 7월 31일

SpringBoot

목록 보기
47/47
post-thumbnail

LLM 기반 서비스를 개발할 때 ChatGPT처럼 "한 글자씩 실시간으로 생성되는 타이핑 효과(Streaming UI)"는 선택이 아닌 필수 사용자 경험(UX)이 있습니다.

백엔드 개발자 입장에서 이 기능을 구현하려고 할 때 가장 많이 언급되는 기술 조합이 바로 SSE (Server-Sent Events)Spring WebFlux입니다.

1. 왜 LLM 응답은 스트리밍(Streaming)이어야 할까?

LLM(Large Language Model)은 문맥을 분석해 다음 토큰(Token)을 순차적으로 생성하는 특성을 가집니다.

전체 응답을 완성하는 데 5초~10초 이상 걸릴 수 있는데, 이를 동기식(Blocking)으로 기다렸다가 한 번에 응답하면 사용자는 화면이 멈춘 것으로 오인하게 됩니다.

따라서 생성되는 토큰을 생성되는 즉시 클라이언트로 흘려보내는(Streaming) 아키텍처가 필수적입니다.


2. 웹 브라우저로 흘려보내는 통신 규약: SSE (Server-Sent Events)

실시간 단방향 데이터 전송을 구현할 때 고려하는 대표적인 기술은 WebSocketSSE입니다.

  • 왜 WebSocket 대신 SSE인가?
  • 단방향 스트리밍 최적화: LLM 응답은 "클라이언트가 요청 1번 -> 서버가 연속 데이터 전송" 구조입니다. 양방향 통신이 필요한 WebSocket은 오버스펙입니다.
  • HTTP 기반의 간편함: SSE는 표준 HTTP 프로토콜(Content-Type: text/event-stream)을 사용하므로 방화벽, L7 스위치, JWT 인증 등을 기존 Web 환경 그대로 활용할 수 있습니다.
  • 자동 재연결: 네트워크가 끊겼을 때 브라우저 차원의 재연결 메커니즘을 기본 제공합니다.

💡 한 줄 요약: SSE는 서버에서 클라이언트로 토큰 데이터를 조각조각 실시간으로 쏘아주는 "통신 도로" 역할을 합니다.


3. 서버 내부를 책임지는 비동기 엔진: Spring WebFlux

SSE 통신 규약을 적용하더라도, 백엔드 서버 내부의 동작 방식이 비효율적이라면 대규모 사용자 요청을 견딜 수 없습니다.

Spring MVC (Servlet 기반)의 한계

  • Spring MVC에서 SSE를 구현할 때 사용하는 SseEmitter는 연결이 유지되는 동안 서버 스레드(Thread)를 점유하게 됩니다.
  • 동시 접속자가 1,000명만 되어도 스레드 풀이 고갈되어 서버 전체가 다운되는 현상(Thread Starvation)이 발생할 수 있습니다.

Spring WebFlux의 활약!

  • WebFlux는 Netty 기반의 Event Loop 및 Non-blocking I/O 아키텍처를 가집니다.
  • 소수의 스레드(CPU 코어 수 수준)만으로 수천, 수만 개의 SSE 커넥션을 대기 시간(I/O Block) 없이 처리할 수 있습니다.
  • Project Reactor의 Flux 타입을 사용하면 LLM API로부터 들어오는 스트림 데이터를 SSE 포맷으로 자연스럽게 파이프라이닝(Pipelining) 할 수 있습니다.

⚠️ Spring webflux vs @Aysnc 차이는?

Spring의 @Async와 Spring WebFlux는 모두 비동기(Asynchronous) 처리를 가능하게 하지만, 동작 메커니즘, 스레드 모델, 그리고 설계 목적에서 근본적인 차이가 있습니다.

쉽게 비유하자면,

@Async는 "동기식 작업장에서 특정 작업만 별도 일꾼에게 미뤄두는 방식"이고,

WebFlux는 "시스템 전체가 이벤트 기반으로 쉴 새 없이 돌아가는 비동기 전용 작업장"입니다.

💡 한 줄 요약: WebFlux는 수많은 사용자가 동시에 LLM 스트리밍을 요청해도 서버 자원(Thread/Memory)을 거의 쓰지 않고 버텨주는 "내부 엔진" 역할을 합니다.


4. SSE + WebFlux 전체 데이터 흐름도

[Client (Browser)] 
       │ 1. POST /api/chat (SSE Connection Request)
       ▼
[Spring WebFlux Server] 
       │ 2. WebClient로 Non-blocking LLM API 호출
       ▼
[LLM API (OpenAI 등)]
       │ 3. Token 스트리밍 반환 (Chunk 1, Chunk 2, ...)
       ▼
[Spring WebFlux (Flux Pipeline)]
       │ 4. MediaType.TEXT_EVENT_STREAM_VALUE 변환
       ▼
[Client (Browser)] ➔ 실시간 타이핑 UI 렌더링!

5. 한 눈에 보는 핵심 코드 (Spring Boot 3.x)

Controller

@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {

    private final LlmStreamService llmStreamService;

    // MediaType을 TEXT_EVENT_STREAM_VALUE로 지정하는 것이 핵심!
    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamChat(@RequestBody ChatRequest request) {
        return llmStreamService.getLlmResponseStream(request.getPrompt());
    }
}

Service (WebClient 활용)

@Service
@RequiredArgsConstructor
public class LlmStreamService {

    private final WebClient webClient;

    public Flux<String> getLlmResponseStream(String prompt) {
        return webClient.post()
                .uri("/v1/chat/completions")
                .header("Authorization", "Bearer " + API_KEY)
                .bodyValue(Map.of(
                        "model", "gpt-4o",
                        "stream", true, // LLM Stream 옵션 켜기
                        "messages", List.of(Map.of("role", "user", "content", prompt))
                ))
                .retrieve()
                .bodyToFlux(String.class); // Non-blocking 토큰 스트림 수신
    }
}

6. 실제 실무 적용 시 주의할 점 (Tips)

  1. Nginx 등 Reverse Proxy 설정
  • Nginx를 사용하는 경우 응답을 버퍼링하지 않도록 설정해야 스트리밍이 끊기지 않고 내려갑니다.
    ✅ Header: X-Accel-Buffering: no 설정 필수!
  1. 프론트엔드 수신 처리
  • SSE 수신 시 GET 요청만 지원하는 기본 EventSource 대신, POST Body 및 Custom Header(Bearer Token) 전송을 위해 @microsoft/fetch-event-source 라이브러리 사용을 추천합니다.

💡 정리하며

  • SSE는 LLM의 스트리밍 데이터를 브라우저에 실시간으로 던져주기 위한 "표준 통신 규약"
  • Spring WebFlux는 그 스트리밍 데이터를 서버 자원 낭비 없이 효율적으로 흘려보내기 위한 "Non-blocking 백엔드 아키텍처"

LLM 서비스를 더욱 빠르고, 효율적이며, 확장성 있게 설계하고 싶다면 SSE + Spring WebFlux 조합은 검증된 최고의 선택지라고 할 수 있습니다.

profile
자바 스프링 백엔드 개발자입니다. 배운 것을 기록합니다.

0개의 댓글