WebSocket vs SSE, 실시간 통신 방식 선택 기준
채팅, 실시간 알림, AI 응답 스트리밍처럼 서버가 클라이언트에게 계속 데이터를 밀어줘야 하는 기능을 만들 때 가장 먼저 부딪히는 선택이 있다. WebSocket을 쓸 것인가, SSE(Server-Sent Events)를 쓸 것인가. 실제로 GPT나 Claude 같은 LLM API가 응답을 토큰 단위로 흘려보내는 방식도 SSE 기반이다. 이 글에서는 두 방식의 근본적인 차이와, Spring Boot에서 어떻게 구현하는지 코드로 정리한다.
근본적인 차이 — 양방향이냐 단방향이냐
가장 큰 차이는 통신 방향이다.
- WebSocket: 클라이언트 ↔ 서버 양방향 통신. 한 번 연결을 맺으면 양쪽 모두 언제든 메시지를 보낼 수 있다.
- SSE: 서버 → 클라이언트 단방향 통신. 클라이언트는 최초 요청만 보내고, 이후엔 서버가 이벤트를 계속 흘려보낸다.
| 구분 | WebSocket | SSE |
|---|---|---|
| 통신 방향 | 양방향 | 단방향(서버→클라이언트) |
| 프로토콜 | 별도 프로토콜(ws://, wss://)로 업그레이드 | 일반 HTTP(text/event-stream) |
| 재연결 | 직접 구현 필요 | 브라우저가 자동 재연결 |
| 프록시/방화벽 | 일부 환경에서 막힐 수 있음 | 일반 HTTP라 대부분 통과 |
| 구현 난이도 | 상대적으로 높음 | 낮음(순수 HTTP 위에서 동작) |
| 대표 용도 | 채팅, 협업 편집, 게임 | 알림, 실시간 피드, LLM 스트리밍 응답 |
언제 SSE로 충분한가
클라이언트가 서버에 계속 뭔가를 보낼 필요가 없다면, SSE만으로 충분한 경우가 많다. LLM 스트리밍 응답이 대표적이다 — 클라이언트는 질문 하나를 보내고, 그 이후로는 서버가 토큰을 계속 흘려보내기만 하면 된다. 알림 배지, 실시간 대시보드 수치 업데이트, 진행률 표시(파일 업로드·배치 작업 진행상황)도 마찬가지다.
SSE는 일반 HTTP 연결이기 때문에 별도 프로토콜 업그레이드 없이 로드밸런서·리버스 프록시·방화벽을 그대로 통과한다는 실무적 장점이 크다. 브라우저의 EventSource API도 연결이 끊기면 자동으로 재연결을 시도해준다.
Spring Boot로 SSE 구현
@RestController
public class NotificationController {
@GetMapping(value = "/notifications/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamNotifications() {
SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 10; i++) {
emitter.send(SseEmitter.event()
.name("notification")
.data("알림 #" + i));
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
Spring WebFlux를 쓴다면 Flux로 훨씬 간결하게 표현할 수 있다.
@RestController
public class ReactiveNotificationController {
@GetMapping(value = "/notifications/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamNotifications() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> "알림 #" + seq)
.take(10);
}
}
클라이언트(브라우저)는 EventSource로 별다른 라이브러리 없이 바로 받을 수 있다.
const eventSource = new EventSource('/notifications/stream');
eventSource.addEventListener('notification', (event) => {
console.log('받은 알림:', event.data);
});
언제 WebSocket이 필요한가
클라이언트도 서버에 수시로 메시지를 보내야 한다면 WebSocket이 정답이다. 채팅(메시지 전송·수신이 둘 다 필요), 여러 사용자가 같은 문서를 동시에 편집하는 협업 툴, 실시간 게임처럼 지연 시간이 중요하고 양방향 트래픽이 빈번한 경우다.
Spring Boot로 WebSocket 구현
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-chat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/public")
public ChatMessage sendMessage(ChatMessage message) {
return message; // 받은 메시지를 구독자 전체에게 브로드캐스트
}
}
WebSocket은 SockJS 같은 폴백 라이브러리와 STOMP 같은 상위 프로토콜을 함께 쓰는 경우가 많은데, 이는 순수 WebSocket만으로는 메시지 라우팅(누구에게 보낼지)이나 구독 관리를 직접 구현해야 하기 때문이다.
실무 선택 기준 정리
- 클라이언트→서버 전송이 필요 없다 → SSE. 구현이 훨씬 간단하고 인프라 호환성도 좋다.
- 양방향 실시간 상호작용이 핵심이다 → WebSocket.
- 프록시·방화벽이 까다로운 환경(사내망, 특정 클라우드 게이트웨이) → SSE가 안전한 선택인 경우가 많다.
- 연결 수가 매우 많고(수만 이상) 서버 자원이 걱정된다 → 둘 다 커넥션을 유지해야 하므로, 이 경우엔 폴링 간격을 넓힌 롱폴링이나 메시지 큐 기반 아키텍처까지 검토 범위를 넓히는 게 낫다.
- 재연결 로직을 직접 짜기 싫다 → SSE(
EventSource자동 재연결)가 개발 비용을 줄여준다.
마무리
두 기술은 경쟁 관계라기보다 용도가 다른 도구에 가깝다. 서버가 일방적으로 밀어주기만 하면 되는 대부분의 "실시간처럼 보이는" 기능은 SSE로 충분하고, 오히려 과하게 WebSocket을 끌어다 쓰면 연결 관리·재연결·메시지 라우팅 같은 부가 복잡도만 늘어난다. 반대로 진짜 양방향 상호작용이 필요한 기능에 SSE를 억지로 끼워 맞추면 클라이언트→서버 경로를 별도 REST 호출로 우회해야 해서 설계가 지저분해진다. 기능 요구사항에서 "클라이언트도 실시간으로 뭔가를 보내야 하는가"라는 질문 하나로 대부분 정리된다.
참고 자료
- MDN Web Docs — Server-Sent Events
- Spring 공식 문서 — WebSocket Support
- Spring 공식 문서 — Web Reactive: Flux 기반 스트리밍 응답
'Computer Science > NetWork' 카테고리의 다른 글
| HTTP 캐시 완벽 정리: Cache-Control·ETag·304 동작 원리와 Spring Boot 구현 (0) | 2026.07.28 |
|---|---|
| 502 Bad Gateway가 떴다: Nginx가 범인인 척했지만 진범은 Spring Boot였다 (0) | 2026.07.19 |
| 오픈소스 공급망 공격, 2026년 개발자 대응 (0) | 2026.07.17 |
| HTTP/3 vs HTTP/2, QUIC 도입 기준 정리 (0) | 2026.07.15 |
| CORS 동작 원리와 Spring Boot 설정 방법 (0) | 2026.07.03 |