
LLM 기반 서비스를 개발할 때 ChatGPT처럼 "한 글자씩 실시간으로 생성되는 타이핑 효과(Streaming UI)"는 선택이 아닌 필수 사용자 경험(UX)이 있습니다.
백엔드 개발자 입장에서 이 기능을 구현하려고 할 때 가장 많이 언급되는 기술 조합이 바로 SSE (Server-Sent Events)와 Spring WebFlux입니다.
LLM(Large Language Model)은 문맥을 분석해 다음 토큰(Token)을 순차적으로 생성하는 특성을 가집니다.
전체 응답을 완성하는 데 5초~10초 이상 걸릴 수 있는데, 이를 동기식(Blocking)으로 기다렸다가 한 번에 응답하면 사용자는 화면이 멈춘 것으로 오인하게 됩니다.
따라서 생성되는 토큰을 생성되는 즉시 클라이언트로 흘려보내는(Streaming) 아키텍처가 필수적입니다.
실시간 단방향 데이터 전송을 구현할 때 고려하는 대표적인 기술은 WebSocket과 SSE입니다.
Content-Type: text/event-stream)을 사용하므로 방화벽, L7 스위치, JWT 인증 등을 기존 Web 환경 그대로 활용할 수 있습니다.💡 한 줄 요약: SSE는 서버에서 클라이언트로 토큰 데이터를 조각조각 실시간으로 쏘아주는 "통신 도로" 역할을 합니다.
SSE 통신 규약을 적용하더라도, 백엔드 서버 내부의 동작 방식이 비효율적이라면 대규모 사용자 요청을 견딜 수 없습니다.
SseEmitter는 연결이 유지되는 동안 서버 스레드(Thread)를 점유하게 됩니다.Flux 타입을 사용하면 LLM API로부터 들어오는 스트림 데이터를 SSE 포맷으로 자연스럽게 파이프라이닝(Pipelining) 할 수 있습니다.Spring의 @Async와 Spring WebFlux는 모두 비동기(Asynchronous) 처리를 가능하게 하지만, 동작 메커니즘, 스레드 모델, 그리고 설계 목적에서 근본적인 차이가 있습니다.
쉽게 비유하자면,
@Async는 "동기식 작업장에서 특정 작업만 별도 일꾼에게 미뤄두는 방식"이고,
WebFlux는 "시스템 전체가 이벤트 기반으로 쉴 새 없이 돌아가는 비동기 전용 작업장"입니다.
💡 한 줄 요약: WebFlux는 수많은 사용자가 동시에 LLM 스트리밍을 요청해도 서버 자원(Thread/Memory)을 거의 쓰지 않고 버텨주는 "내부 엔진" 역할을 합니다.
[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 렌더링!
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 토큰 스트림 수신
}
}
X-Accel-Buffering: no 설정 필수!GET 요청만 지원하는 기본 EventSource 대신, POST Body 및 Custom Header(Bearer Token) 전송을 위해 @microsoft/fetch-event-source 라이브러리 사용을 추천합니다.LLM 서비스를 더욱 빠르고, 효율적이며, 확장성 있게 설계하고 싶다면 SSE + Spring WebFlux 조합은 검증된 최고의 선택지라고 할 수 있습니다.