Skip to content

Latest commit

 

History

History
216 lines (164 loc) · 8.08 KB

File metadata and controls

216 lines (164 loc) · 8.08 KB

HTTP vs SSE vs WebSocket 서버 부하 비교 벤치마크

테스트 환경

항목
Framework Spring Boot 4.0.2 (Tomcat 11)
JDK Azul Zulu 21.0.9
OS macOS Darwin 24.6.0
DB MySQL 8.0 (Docker)
측정 방식 JVM 내부 (Runtime.getRuntime(), Thread.activeCount())
동시 연결 수 100

측정 방법: 각 프로토콜 테스트 전에 System.gc()를 2회 호출하고 500ms 대기하여 baseline을 확보한 뒤, 100개의 동시 연결을 생성하고 2초 후 측정했다. 테스트 간에도 GC + 2초 대기를 두어 이전 테스트의 영향을 최소화했다.


1. JVM 메모리 사용량 비교

전체 결과

프로토콜 Baseline 부하 중 정리 후 Delta (증가량) 연결 당 메모리
HTTP 34.2 MB 39.8 MB 31.5 MB +5.6 MB 57.4 KB
SSE (SseEmitter) 31.9 MB 56.0 MB 40.7 MB +24.1 MB 247.2 KB
SSE (Flux) 40.5 MB 71.8 MB 41.3 MB +31.4 MB 321.1 KB
WebSocket (STOMP) 40.6 MB 61.1 MB 46.6 MB +20.5 MB 209.7 KB

분석

  • HTTP: 연결 당 57 KB로 가장 가벼움. 요청-응답 후 즉시 자원 반환되므로 정리 후 baseline 이하로 회복.
  • SSE (SseEmitter): 연결 당 247 KB. 각 연결마다 SseEmitter 객체 + 별도 가상 스레드가 생성되어 버퍼를 점유.
  • SSE (Flux): 연결 당 321 KB로 가장 높음. Reactor의 Flux.interval() 스케줄러, ServerSentEvent 빌더 객체, 내부 연산자 체인이 모두 힙에 올라감.
  • WebSocket: 연결 당 210 KB. 세션 객체 + STOMP 프레임 버퍼가 연결 동안 유지되지만, Flux 방식보다는 가벼움.

정리 후 메모리 (자원 회수 여부)

프로토콜 Baseline 정리 후 회수율
HTTP 34.2 MB 31.5 MB 완전 회수 (오히려 감소)
SSE (SseEmitter) 31.9 MB 40.7 MB 부분 회수 (약 +9 MB 잔여)
SSE (Flux) 40.5 MB 41.3 MB 거의 완전 회수
WebSocket 40.6 MB 46.6 MB 부분 회수 (약 +6 MB 잔여)

2. 스레드 사용량 비교

프로토콜 Baseline 부하 중 Delta 특징
HTTP 33 46 +13 Tomcat 스레드풀에서 할당 후 반환
SSE (SseEmitter) 46 62 +16 연결 당 가상 스레드 1개 생성
SSE (Flux) 74 86 +12 Reactor 스케줄러 스레드 공유 (소수)
WebSocket 86 103 +17 연결 당 I/O 스레드 할당

분석

  • HTTP: Tomcat 기본 스레드풀(200개)에서 빌려쓰고 돌려줌. 순간적 동시 요청에도 13개 정도만 사용.
  • SSE (SseEmitter): Thread.startVirtualThread()로 가상 스레드를 사용해 OS 스레드 점유는 적음. +16은 캐리어 스레드 수준.
  • SSE (Flux): Reactor의 parallel() 스케줄러가 CPU 코어 수 만큼만 스레드를 만들어 공유. 100개 연결에도 +12.
  • WebSocket: 각 연결이 I/O 스레드를 점유. +17로 가장 많지만, 양방향 통신이 가능한 대가.

3. 패킷 구조 비교

HTTP (매 요청마다 전체 헤더 교환)

# 요청 (약 80 bytes)
GET /api/hello HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.7.1
Accept: */*

# 응답 (약 130 bytes + 14 bytes body)
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Fri, 30 Jan 2026 13:09:10 GMT

Hello, Spring!

특징: 매 요청마다 ~80 bytes 요청 헤더 + ~130 bytes 응답 헤더 반복. 데이터 14 bytes 전송에 헤더 오버헤드가 210 bytes.


SSE (최초 1회 핸드셰이크, 이후 데이터만 스트리밍)

# 최초 요청 (약 80 bytes) — HTTP와 동일
GET /api/sse/stream HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.7.1
Accept: */*

# 최초 응답 헤더 (약 100 bytes) — 1회만 전송
HTTP/1.1 200
Content-Type: text/event-stream
Transfer-Encoding: chunked
Date: Fri, 30 Jan 2026 13:14:11 GMT

# 이후 데이터 프레임 (약 62 bytes/이벤트) — 헤더 없이 반복
id:1
event:time
data:Server time: 2026-01-30T22:14:11.779986

id:2
event:time
data:Server time: 2026-01-30T22:14:12.785223
...

특징:

  • 최초 연결 시에만 HTTP 헤더 교환 (~180 bytes)
  • 이후 이벤트는 id: + event: + data:\n\n 텍스트만 전송 (~62 bytes/이벤트)
  • HTTP로 동일 데이터를 10번 요청하면: 10 x (80+130+62) = 2,720 bytes
  • SSE로 10개 이벤트 전송하면: 180 + 10 x 62 = 800 bytes (약 70% 절약)

WebSocket (HTTP 업그레이드 후 바이너리 프레임)

# 핸드셰이크 요청 (약 220 bytes)
GET /ws/websocket HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# 핸드셰이크 응답 (약 130 bytes) — 101 Switching Protocols
HTTP/1.1 101
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Date: Fri, 30 Jan 2026 13:18:17 GMT

# 이후 데이터 프레임 (2~6 bytes 프레임 헤더 + payload)
# 예: "Hello" (5 bytes payload) → 총 7 bytes (2 byte header + 5 byte data)

특징:

  • 핸드셰이크 1회: ~350 bytes (HTTP보다 큼 — 추가 헤더 필요)
  • 이후 프레임: 2~6 bytes 헤더 + payload (HTTP 헤더 ~210 bytes 대비 극소)
  • 양방향 가능: 클라이언트→서버도 동일한 경량 프레임
  • 10회 양방향 메시지: 350 + 20 x 7 = 490 bytes
  • HTTP로 동일 작업: 20 x 272 = 5,440 bytes (약 91% 절약)

4. 종합 비교 요약

HTTP SSE WebSocket
연결 방식 요청마다 새로 (또는 Keep-Alive) 단방향 지속 연결 양방향 지속 연결
연결 당 메모리 57 KB 247~321 KB 210 KB
스레드 증가 +13 (풀 공유) +12~16 +17
데이터 프레임 오버헤드 ~210 bytes/요청 ~0 bytes (텍스트 스트림) 2~6 bytes
방향 클라이언트 → 서버 서버 → 클라이언트 양방향
자원 회수 즉시 연결 종료 시 연결 종료 시
재연결 불필요 EventSource 자동 재연결 직접 구현 필요

5. 프로토콜 선택 가이드

HTTP를 선택해야 할 때

  • 단발성 요청/응답 (REST API, CRUD)
  • 클라이언트가 필요할 때만 데이터 요청
  • 연결 유지 비용을 감당할 수 없는 대규모 서비스

SSE를 선택해야 할 때

  • 서버 → 클라이언트 단방향 실시간 데이터 (알림, 주가, 로그 스트리밍)
  • HTTP/2 환경에서 멀티플렉싱 활용 가능
  • 자동 재연결이 필요한 경우 (EventSource API 내장)
  • SseEmitter vs Flux:
    • SseEmitter: 전통적 서블릿 환경, 직관적 코드
    • Flux: 리액티브 환경, 스레드 효율적이지만 메모리 오버헤드 더 큼

WebSocket을 선택해야 할 때

  • 양방향 실시간 통신 (채팅, 게임, 협업 편집)
  • 메시지 빈도가 높아 HTTP 헤더 오버헤드가 부담인 경우
  • 가장 낮은 데이터 프레임 오버헤드 (2~6 bytes)

부록: 테스트 코드

벤치마크 엔드포인트:

GET /api/benchmark/all?connections=100   # 전체 프로토콜 순차 테스트
GET /api/benchmark/http?connections=100  # HTTP만
GET /api/benchmark/sse-emitter?connections=100
GET /api/benchmark/sse-flux?connections=100
GET /api/benchmark/websocket?connections=100

측정 원리:

// 1. GC 2회 → baseline 측정
System.gc(); Thread.sleep(500); System.gc();
long baseMem = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
int baseThreads = Thread.activeCount();

// 2. 동시 연결 N개 생성 → 2초 대기

// 3. 부하 중 측정
long duringMem = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

// 4. 연결 정리 → GC → 정리 후 측정

이 벤치마크는 JVM 내부에서 직접 측정하여 외부 프로세스 영향을 최소화했다. 단, System.gc()는 JVM에 대한 힌트일 뿐 보장이 아니므로, 수치는 상대적 비교 목적으로 참고해야 한다.