폴링(long-폴링) 사용 한계
매번 핸드셰이킹(handshaking)"이 필요한 구조에서는 CPU 사용량이 증가할 수 있습니다.
| 원인 | 설명 |
| 핸드셰이크는 무거운 작업 | 연결 설정, 포트 확보, 암호화 키 교환, 인증서 검증 등 CPU가 계산할 일이 많음 |
| 매 요청마다 연결 생성/해제 | Polling처럼 요청이 잦으면 매번 핸드셰이크 → 반복적인 부하 |
| 컨텍스트 스위칭 증가 | 요청마다 네트워크 스레드가 개입되면 CPU가 자주 컨텍스트 전환 → 오버헤드 발생 |
핸드셰이킹은 CPU 계산량과 리소스를 소모하는 작업입니다.
따라서 매번 새로운 연결을 열고 닫아야 하는 구조(폴링, REST 반복 요청 등)는 CPU 부하가 커지고, 실시간성도 떨어지며, 서버 확장성에도 한계가 생깁니다.
👉 그렇다면 이 문제를 Long Polling으로 해결 가능한가?
Long-polling은
- 클라이언트가 서버에 요청을 보냄
- 서버는 이벤트가 발생할 때까지 응답을 지연
- 이벤트가 발생하면 응답하고, 클라이언트는 즉시 다시 요청을 재전송
→ 이벤트가 자주 발생하지 않으면 일단 폴링보다 성능적으로도 좋고, 실시간성도 좋다.
but Long Polling도 부하가 커질 수 있는 경우
- 서버가 응답을 주자마자 클라이언트가 즉시 재요청
- 결국 짧은 간격으로 지속적인 요청 반복
- CPU는 매번 연결 처리 + 핸드셰이크 (HTTP, TLS 등) 수행
- → Poll 방식과 유사한 부하가 발생
SSE(Server-Sent Events)로 폴링의 CPU 사용량 부하 해결
SSE는 한 번 연결되면 클라이언트에 데이터가 push되므로, 매번 연결을 새로 만들 필요가 없어 CPU 오버헤드가 줄어듭니다.
SSE(Server-Sent Events HTTP 프로토콜을 기반으로 클라이언트와 서버 간의 단방향 통신 프로토콜입니다. 이를 통해 서버는 클라이언트로부터 명시적인 요청 없이도 실시간으로 데이터를 푸시할 수 있습니다. 표준 HTTP 프로토콜을 기반으로 하는 SSE는 단방향 지속 연결을 활용하여 서버가 클라이언트에게 이벤트와 데이터를 능동적으로 전송할 수 있게 합니다.
기본적으로 스트리밍 방식을 사용하여 구현되며, 클라이언트가 서버에 연결 요청을 시작하고 연결을 유지합니다. 그러면 서버가 클라이언트에게 메시지를 능동적으로 푸시합니다. Server-Sent Events(SSE)를 사용하여 서버에서 클라이언트로 전송되는 데이터는 UTF-8로 인코딩되어야 하며, 반환되는 콘텐츠 유형은 text/event-stream입니다.
SSE의 장점
- 실시간 통신: SSE는 실시간 통신 메커니즘을 제공하여 서버가 클라이언트에게 능동적으로 데이터를 푸시할 수 있습니다. 이러한 실시간 기능은 실시간 채팅, 온라인 협업 도구, 실시간 데이터 표시 및 알림 전달과 같이 즉각적인 업데이트가 필요한 애플리케이션에 특히 적합합니다.
- 네트워크 부하 감소: 전통적인 폴링 방식과 비교하여 SSE는 장기 연결을 활용합니다. 단일 HTTP 연결을 통해 서버는 클라이언트에게 여러 이벤트를 푸시할 수 있으며, 빈번한 HTTP 요청을 피함으로써 네트워크 부하를 줄입니다.
- 경량: SSE는 HTTP 프로토콜을 기반으로 하며, 기존 서버 소프트웨어에서 지원되며 WebSocket에 비해 사용이 간단합니다.
- 자동 재연결: SSE는 연결이 끊어진 후 자동으로 재연결을 시도할 수 있으며, 추가 코드가 필요하지 않습니다. 이 자동 재연결 메커니즘은 시스템 안정성을 향상시켜 불안정한 네트워크 조건에서도 지속적인 통신을 보장합니다.
SSE & Nginx 사용시 주의할 점
Nginx는 기본적으로 Upstream으로 요청을 보낼때 HTTP/1.0 버전을 사용한다는 것을 확인했습니다.
HTTP/1.1은 지속 연결이 기본이기 때문에 헤더를 따로 설정해줄 필요가 없지만, Nginx에서 백엔드 WAS로 요청을 보낼 때는 HTTP/1.0을 사용하고 Connection: close 헤더를 사용하게 됩니다.
SSE는 지속 연결이 되어 있어야 동작하는데 Nginx에서 지속 연결을 닫아버려 제대로 동작하지 않았습니다.
따라서 아래와 같이 nginx 설정을 추가해야 제대로 동작합니다.
proxy_set_header Connection '';
proxy_http_version 1.1;
또 Nginx의 proxy buffering 기능도 조심해야 하는데요, SSE 통신에서 서버는 기본적으로 응답에 Transfer-Encoding: chunked를 사용합니다. SSE는 서버에서 동적으로 생성된 컨텐츠를 스트리밍하기 때문에 본문의 크기를 미리 알 수 없기 때문입니다.
Nginx는 서버의 응답을 버퍼에 저장해두었다가 버퍼가 차거나 서버가 응답 데이터를 모두 보내면 클라이언트로 전송하게 됩니다.
문제는 버퍼링 기능을 활성화하면 SSE 통신 시 원하는대로 동작하지 않거나 실시간성이 떨어지게 된다는 것입니다. 따라서 SSE 응답에 대해서는 proxy buffering 설정을 비활성화 해주는 것이 좋습니다.
proxy_buffering off;
하지만 Nginx의 설정 파일에서 버퍼링을 비활성화하면 다른 모든 API 응답에 대해서도 버퍼링을 하지 않기 때문에 비효율적일 수 있습니다. 이때 nginx의 X-accel 기능을 활용하면 좋습니다.
백엔드의 응답 헤더에 X-accel로 시작하는 헤더가 있으면 Nginx가 이 정보를 이용해 내부적인 처리를 따로 하도록 만들 수 있습니다. 따라서 SSE 응답을 반환하는 API의 헤더에 X-Accel-Buffering: no를 붙여주면 SSE 응답만 버퍼링을 하지 않도록 설정할 수 있습니다.
또는 /api/sse/와 같이 SSE 전용 endpoint를 별도의 location 블록으로 분리하여 proxy_buffering off;를 설정하면, 해당 경로에 대해서만 버퍼링을 비활성화할 수 있으므로 전체 API에 미치는 영향을 최소화할 수 있습니다.
location /api/sse/ {
proxy_pass <http://localhost:8080/api/sse/>;
proxy_buffering off;
...
}
Nginx의 타임아웃 설정은 오랫동안 데이터가 전송되지 않는 연결을 비정상 상태로 간주하여 강제로 종료하는 메커니즘입니다. 예를 들어 클라이언트가 SSE(Server-Sent Events) 연결을 열고 대기하는 동안, 서버가 이벤트가 생기지 않아 아무 데이터도 보내지 않으면, 중간에 위치한 Nginx 프록시가 일정 시간 이상 응답이 없다고 판단해 연결을 강제로 종료시킬 수 있습니다. 이로 인해 클라이언트는 아무 이벤트도 받지 못한 채 연결이 끊긴 상태가 되며, 이를 인지하지 못한 채 대기하거나 재연결을 반복하게 됩니다.
이러한 문제를 방지하기 위해 서버는 일정 주기로 heartbeat(예: data: ping)와 같은 최소한의 데이터를 보내 연결이 살아있음을 알리는 것이 필요합니다. 특히 Nginx의 proxy_read_timeout 기본값이 60초이기 때문에, 서버는 최소 60초마다 한 번씩 heartbeat를 전송해야 중간 연결이 끊기지 않고 안정적인 SSE 통신을 유지할 수 있습니다.
SSE의 heartbeat 주기 주의 사항
heartbeat는 연결 유지를 위한 “나는 살아 있다” 신호이자, 중간에 끊겼는지 감지하기 위한 메커니즘입니다.
하지만 지속적인 유지와 실시간성을 가져가려다가 heartbeat의 주기를 너무 짧게 잡게되면 다음과 같은 문제가 발생할 수 있다.
- CPU 사용량 증가: 메시지 생성, 전송, I/O 작업을 매번 반복
- GC 및 메모리 부하: 메시지를 보낼 때마다 새로운 문자열/이벤트 객체 또는 버퍼가 생성
- 짧은 생명 주기를 가진 객체들이 쌓이고 → Eden 영역에 부하 → GC 빈도 증가
SpringBoot의 tomcat 서버에서 SSE의 단점
1. CPU 사용량
SSE는 연결 유지 방식이라서, 클라이언트 수가 많아질수록 서버가 각 연결을 관리해야 하고, 그 연결마다 리소스를 소모합니다.
| 이유 | 설명 |
| 컨텍스트 스위칭 비용 | 수천 개 스레드가 동시에 살아 있으면 OS가 스레드 간 전환을 계속함 → CPU 부하 커짐 |
| CPU Wait 관리 | 블로킹 I/O일 경우 스레드가 대기만 하고 있어도 CPU는 해당 스레드 상태를 계속 추적함 |
Netty / Undertow / WebFlux 와 같은 서버에서는 이 문제점을 쉽게 해결할 수 있다. Non-blocking I/O (비동기 I/O) 구조를 사용함으로서 SSE 연결 수가 많아도 CPU 사용량이 크게 늘지 않는다.
하지만 Spring Boot 기본 설정은 Tomcat + 블로킹 I/O 기반이기 때문에, SSE 연결마다 스레드가 1개씩 점유됩니다.
요약: CPU 부하 증가
→ 컨텍스트 스위칭, 스레드 관리
2. 메모리량
- VM은 스레드를 생성할 때 기본적으로 약 512KB~1MB의 스택 메모리를 잡습니다.
- 각 요청 연결 상태를 저장하는 (HTTP 요청 객체, 응답 객체, 버퍼, 커넥션 컨텍스트 등과 같은)객체들이 힙영역에서 늘어납니다. → 이 객체들도 계속 살아 있고 GC 대상이 아니므로 메모리를 차지
요약: 메모리 사용량 증가
→ 스레드 스택 + 요청 상태 + 버퍼 + 오래 살아있는 객체
끝.
'카카오테크 부트캠프' 카테고리의 다른 글
| Redis 캐시, Cache Stampede 방지 전략 보고서 (TTL Jitter, Mutex Locking) (0) | 2025.07.13 |
|---|---|
| Redis 캐시 키 설계 관련 보고서: 사용자 domain 정보 처리 전략 (1) | 2025.07.11 |
| MVP 이후의 코드 품질 개선 전략 — 리팩토링 (0) | 2025.06.21 |
| SSE 연결로 인한 HikariCP 커넥션 미반납 문제 (0) | 2025.06.14 |
| Java의 record 타입 — DTO에 적합한 이유와 실제 적용기 (1) | 2025.06.10 |