여러분이 당근마켓의 개발자인데, 사용자들이 중고 물품을 실시간으로 보여주며 대화할 수 있는 '1:1 영상 통화' 기능을 만들어 달라는 미션을 받았습니다. 일반적인 생각으로는 철수의 스마트폰 카메라(영상) 데이터를 스프링 부트 서버로 쏘고, 서버가 그걸 다시 영희에게 쏴주는(Relay) 방식을 떠올립니다. 하지만 영상 데이터는 1초에 수십 메가바이트(MB)를 차지합니다. 1,000명이 동시에 영상 통화를 한다면 서버의 네트워크 대역폭(Bandwidth) 요금만 한 달에 수억 원이 청구될 것이며 서버는 펑 하고 터질 것입니다. "서버를 거치지 않고, 철수 폰에서 영희 폰으로 직접(Peer-to-Peer, P2P) 영상 데이터를 쏴버릴 수는 없을까?" 이 미친 효율의 혁신적인 기술이 바로 웹 브라우저 간의 다이렉트 통신 표준, WebRTC(Web Real-Time Communication)입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "줌(Zoom)과 구글 미트의 심장!" WebRTC의 핵심 원리와, 두 사람을 만나게 해주는 스프링 부트 시그널링(Signaling) 서버 구축에 대하여 다뤄보겠습니다.

🎥 1. WebRTC의 함정: P2P 연결을 위해선 '중매쟁이'가 필요하다
WebRTC는 100% P2P 통신입니다. 하지만 치명적인 모순이 하나 있습니다. 철수가 영희에게 영상 데이터를 쏘려면 '영희의 현재 IP 주소(집 주소)'를 알아야 합니다. 철수는 영희의 IP를 어떻게 알 수 있을까요? 텔레파시로 알 수는 없으니, 결국 처음 서로의 주소(명함)를 교환할 때는 반드시 한 번은 중앙 서버를 거쳐야 합니다. 이 "명함을 교환해 주는 중매쟁이 서버"를 시그널링 서버(Signaling Server)라고 부릅니다.

교환하는 명함(데이터)은 딱 2가지 텍스트입니다.
- SDP (Session Description Protocol): "내 카메라는 해상도가 1080p고 음성 코덱은 Opus를 써." (미디어 스펙 교환)
- ICE Candidate: "내 공유기 외부 IP 주소는 220.123.45.67 이야. 일로 데이터 쏴줘!" (네트워크 경로 교환)
⚙️ 2. Spring Boot 시그널링 서버 구축 (WebSocket)
시그널링 서버는 텍스트(SDP, ICE)를 실시간으로 빠르게 전달해주기만 하면 됩니다. 그래서 31단계에서 배웠던 WebSocket이 시그널링 서버로 가장 완벽한 기술입니다. 프론트엔드가 생성한 텍스트 덩어리를 받아 반대편 브라우저로 토스만 해주는 아주 단순한 핸들러를 짭니다.
// WebRTC 시그널링(명함 교환)을 위한 스프링 부트 순수 웹소켓 핸들러
@Component
@Slf4j
public class SignalingWebSocketHandler extends TextWebSocketHandler {
// 현재 화상 회의방에 들어온 세션들 보관 (실무에서는 방 번호별 Map으로 관리해야 함)
private final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session); // 접속
}
// 핵심: 철수가 SDP나 ICE Candidate 텍스트(JSON)를 보내면, 내가(서버가) 읽어보고 영희한테 그대로 던져준다!
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("시그널링 메시지 도착: {}", message.getPayload());
// 나(보낸 사람)를 제외한 나머지 방 안의 사람들에게 명함(메시지) 전달!
for (WebSocketSession connectedSession : sessions) {
if (!connectedSession.getId().equals(session.getId())) {
connectedSession.sendMessage(message);
}
}
}
}
백엔드의 역할은 정말 이게 끝입니다! 이 텍스트 명함을 주고받은 이후부터 프론트엔드(JavaScript)의 RTCPeerConnection 객체가 각자의 카메라를 켜고 마법처럼 P2P로 영상 스트리밍을 시작합니다. 서버는 어떠한 미디어 트래픽도 감당하지 않으므로 부하가 0에 가깝습니다.
💀 3. 실무의 장벽: TURN 서버 (방화벽의 늪)
"완벽하네요! 당장 출시합시다!" 하지만 실무(LTE, 회사 망) 환경에 앱을 배포하면 10번 중 3번은 영상이 까맣게 나오며 연결이 안 됩니다. 왜 그럴까요? 바로 방화벽과 NAT(공유기) 때문입니다. 회사나 카페의 엄격한 공유기는 허락되지 않은 외부 IP(영희 폰)에서 다이렉트로 들어오는 P2P 트래픽을 바이러스로 간주하고 다 차단해 버립니다.
이 30%의 연결 실패를 구제하기 위해, 결국 P2P를 포기하고 "서버가 영상을 릴레이(중계) 해주는" 우회 서버를 두게 됩니다. 이것을 TURN 서버(Coturn 등)라고 부릅니다. 100% P2P 무비용을 꿈꿨지만, 결국 서비스의 안정성을 위해 별도의 미디어 중계 서버 인프라 비용을 어느 정도 감수해야 하는 것이 WebRTC 실무의 냉혹한 현실입니다.
🎯 4. 마무리 및 다음 단계
지금까지 어마어마한 영상 트래픽을 서버가 부담하지 않고 클라이언트끼리 직접 주고받게(P2P) 만드는 천재적인 아키텍처 WebRTC의 원리와, 이 둘의 IP를 최초 교환해 주는 백엔드의 중매쟁이 역할(Spring WebSocket Signaling)에 대해 상세히 다루어 보았습니다. 이제 여러분의 이력서에 '실시간 화상 커뮤니케이션 서버 구축'이라는 화려한 스펙이 추가되었습니다.
지금까지 다양한 기술(JPA, Redis, Kafka, WebRTC 등)을 단일 스프링 부트 프로젝트(모놀리식)에 다 때려 넣으며 프로젝트가 비대해졌습니다. 소스코드 폴더 안에는 웹 로직, 배치 로직, 도메인 엔티티가 난장판으로 뒤섞여 "엔티티 하나 고쳤더니 배치 서버가 빌드 에러를 내뿜는" 끔찍한 스파게티 의존성 지옥이 펼쳐집니다. "API 서버, 관리자 서버, 배치 서버가 똑같은 JPA 엔티티(User, Order)를 써야 하는데, 코드를 복사 붙여넣기 하지 않고 깔끔하게 '공통 모듈'로 빼서 레고 블록처럼 조립할 순 없을까?" 이어지는 46단계 포스팅에서는 "거대해진 프로젝트의 우아한 독립!" Gradle 멀티 모듈(Multi-Module) 아키텍처를 통한 도메인 주도 설계(DDD)와 의존성 분리 실무 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] Flyway (플라이웨이): 데이터베이스 형상 관리의 정석 (0) | 2026.08.12 |
|---|---|
| [Spring Boot] 멀티 모듈(Multi-Module) 아키텍처: 거대해진 모놀리스의 분리 (0) | 2026.08.12 |
| gRPC 통신: JSON을 버리고 초고속 바이너리(Binary)를 달다 (0) | 2026.08.05 |
| GraphQL (그래프QL): REST API의 한계를 부수고 오버패칭을 끝내다 (0) | 2026.08.05 |
| [Spring Boot] Spring HATEOAS: 진정한 REST API (Level 3) 달성기 (0) | 2026.08.05 |