2026.01.19 - [프로젝트/Side-Project] - [Media] Spring Boot로 구현하는 미디어 스트리밍
[Media] Spring Boot로 구현하는 미디어 스트리밍
0. Repohttps://github.com/ckaanf/media-lab GitHub - ckaanf/media-lab: From HTTP Range Request to WebRTC: A progressive study on media streaming architecture with variableFrom HTTP Range Request to WebRTC: A progressive study on media streaming architecture
romanc3.tistory.com
위 글에서 Spring Boot로 간단히 미디어 서버를 구현하고, Cpp 서버로의 전환을 예고했습니다.
간단한 Cpp 서버를 토대로 비교한 결과를 기록하려 합니다.
개요
간략한 기능 개발은 완료 성능을 확인해 볼 차례
지난 포스팅에서 Spring Boot와 ResourceRegion을 활용해 RFC 9110 표준을 준수하는 미디어 서버를 구축했습니다.
몇 가지 의문점을 토대로 실험을 진행했습니다.
"과연 이 Blocking I/O 모델이 넷플릭스나 유튜브 같은 대규모 트래픽 환경에서도 버틸 수 있을까?"
일반적인 웹 애플리케이션과 달리, 미디어 스트리밍은 대용량 파일 전송과 긴 연결 유지라는 특수한 환경을 갖습니다.
이번 포스팅에서는 Java(Tomcat) 기반 서버와 리눅스 시스템 콜(sendfile, epoll)을 직접 제어하는 C++ 서버를 동일한 환경에서 벤치마킹하고, 왜 고성능 미디어 서버에는 Zero-Copy 기술이 필수적인지 실험 데이터로 증명해 봅니다.
1. 실험 설계: 병목 지점(Bottleneck)을 찾아서
공정한 비교를 위해 두 서버는 동일한 하드웨어 환경에서 테스트되었으며, 부하 테스트 도구인 k6를 사용했습니다.
- Java Server: Spring Boot (Blocking I/O, stream() 방식)
- C++ Server: Epoll + Sendfile (Non-blocking I/O, Zero-Copy)
- 핵심 가설:
- 메모리 복사 비용: Java는 데이터를 유저 공간(User-space)으로 복사하는 과정에서 CPU를 소모하여 Latency가 튈 것이다.
- 스레드 모델의 한계: 동시 접속자가 늘어나면 Java의 스레드 풀 모델은 콘텍스트 스위칭 비용으로 인해 한계에 봉착할 것이다.
2. [ 실험 1] Chunk Size 변화에 따른 Latency 분석
스트리밍의 효율을 위해 한 번에 보내는 데이터 조각(Chunk Size)을 64KB에서 1MB로 늘렸을 때, 서버가 얼마나 안정적으로 반응하는지 측정했습니다.
벤치마크 결과 비교
| 지표 (100 VUs) | Java (64KB) | Java (1MB) | 변화율 | C++ (1MB) | 비고 |
| Avg Latency | 2.6ms | 4.13ms | +58.8% 🔺 | 1.61ms | C++이 2.5배 빠름 |
| P95 Latency | 3.66ms | 9.38ms | +156.3% 🔺 | 2.7ms | Java 병목 발생 |
| Throughput | 68 MB/s | 1.1 GB/s | - | 1.1 GB/s | 대역폭 포화 |
결과 분석: User-space Copy의 비용
- Java: 청크 사이즈를 키우자 P95 Latency(하위 5%의 느린 요청)가 156%나 폭증했습니다.
이는 1MB짜리 데이터를 매번 커널 영역 → 유저 영역(Heap) → 커널 영역으로 복사하면서 CPU가 과부하에 걸렸기 때문입니다. - C++: 반면 C++은 청크가 커져도 Latency 변화가 거의 없습니다(-5.3%). sendfile을 통해 데이터를 유저 영역으로 가져오지 않고, 커널 안에서(PageCache → NIC) 바로 쏘아 보냈기 때문입니다.
3. [실험 2] 동시 접속자(VUs) 확장에 따른 Scalability
동시 접속자를 유의미하게 100명에서 500명까지 늘려보았습니다. 여기서 두 아키텍처의 결정적 차이가 드러납니다.
Scalability 테스트 결과 (Chunk 256KB)
| 동시 접속자 (VUs) | Java P95 Latency | C++ P95 Latency | 성능 격차 |
| 100 VUs | 3.6ms | 2.8ms | 1.2배 차이 |
| 300 VUs | 52.4ms | 3.1ms | 16배 차이 |
| 500 VUs | 154.9ms | 3.8ms | 40배 차이 |
결과 분석: Thread vs Event Loop
- Java (Thread Pool Hell): 500명이 동시에 접속하자 톰캣의 스레드 풀(Max 200)이 고갈되고,
남은 요청들은 큐에서 대기하거나 스레드 경합(Contention)으로 인해 Latency가 150ms까지 치솟았습니다. - C++ (Linear Scalability): 단일 스레드 기반의 Event Loop(Epoll) 방식을 사용한 C++ 서버는 500명이 접속해도 Latency가 3.8ms로 유지되었습니다. 이는 연결 개수와 상관없이 활성화된 I/O 이벤트만 처리하는 Epoll의 특성 덕분입니다.
4. [실험 3] Java도 Zero-Copy를 쓰면 되지 않을까? (transferTo)
Java가 느린 게 아니라 read()/write() 방식이 느린 게 아닐까?라는 의문을 해결하기 위해,
Java NIO의 FileChannel.transferTo()를 적용해 재실 험했습니다.
- 기존 방식 (Stream): P95 Latency 154.9ms
- 개선 방식 (transferTo): P95 Latency 110.9ms (약 28% 개선)
- C++ Native: P95 Latency 3.8ms
결론: Java에서도 Zero-Copy를 흉내 낼 수는 있었지만, JVM 런타임 오버헤드와 서블릿 컨테이너(Tomcat) 자체의 구조적 비용 때문에 Native C++의 성능(3.8ms)에는 근접할 수 없음을 확인했습니다.
5. 왜 이런 차이가 발생하는가?
이 실험 결과는 단순히 언어의 차이가 아닙니다. I/O 처리 방식(Architecture)의 차이입니다.
5.1 4 Copies (Java Standard I/O)
일반적인 파일 전송은 하드디스크에서 랜선까지 가는데 4번의 데이터 복사와 4번의 컨텍스트 스위칭이 발생합니다.
- Disk → Kernel Buffer (DMA)
- Kernel → User Buffer (CPU Copy) ⚠️ 여기서 CPU 낭비 발생
- User → Socket Buffer (CPU Copy) ⚠️ 여기서 CPU 낭비 발생
- Socket → NIC (DMA)
5.2 Zero-Copy (C++ Sendfile)
C++ 서버가 사용한 sendfile 시스템 콜은 유저 영역을 완전히 건너뜁니다.
- Disk → Kernel Buffer (DMA)
- Kernel Buffer → NIC (DMA) ✅ CPU Copy 0회!
CPU가 데이터 이동이라는 단순 노동에서 해방되니, 남는 자원으로 더 많은 동시 접속자를 처리할 수 있게 된 것입니다.
6. 결론
이번 실험을 통해 "대용량 미디어 처리에는 유저 레벨의 메모리 복사가 성능의 주범(Bottleneck)" 임을 명확히 확인했습니다.
Spring Boot는 훌륭한 프레임워크지만, 바이트 하나하나를 극한으로 제어해야 하는 미디어 엔진의 영역에서는 Native 레벨의 접근이 필요합니다.
그러나 성능 지표를 확실히 계산하고, C++ 서버를 도입해야 할 것 같습니다.
프레임워크가 주는 간편함과 고수준의 추상화는 확실히 이점이 있습니다.
transferTo()를 활용하고 서블릿 pool을 늘리는 것도 충분히 도움이 될 것입니다.
적절한 수준의 엔지니어링을 파악하고 선택할 수 있는 역량이 더욱 필요하단 생각이 들었습니다.
References
- [Github] Project Repository: media-lab/experiments
- Linux man-pages: sendfile(2), epoll(7)
- Java NIO: FileChannel.transferTo()