네트워크 5편 - CDN, WebSocket, 멱등성 키: 실전 트래픽을 견디는 구조
작성일: 2026년 5월 6일
2편에서 TCP 핸드셰이크는 모든 연결에 부과되는 협상 수수료였다.
3편에서 HTTP는 그 수수료의 굴레에서 벗어나기 위해 세 번 진화했다.
4편에서 로드 밸런서는 얼마나 ‘알고’ 있느냐에 따라 트래픽을 정교하게 나누었다.하지만 실제 서비스는 이 문제들을 하나씩 차례로 겪지 않는다. 상품 페이지 하나를 로딩할 때 RTT, 멀티플렉싱, CDN 배치는 한꺼번에 맞물려 돌아간다. 결제 한 건을 처리하는 과정에서도 TCP의 신뢰성, 애플리케이션의 재시도 로직, 멱등성이 한 호흡으로 움직인다. 재료는 모두 준비되었다. 이제 이들을 조합해 진짜 트래픽을 마주할 시간이다.
이 글은 1~4편에서 다룬 개념들이 실전에서 어떻게 시너지를 내는지 다룬다. 각 개념이 처음 등장한 편을 미리 읽어둔다면, 특정 시나리오에서 왜 그런 선택을 했는지 더 깊이 이해할 수 있을 것이다.
지금까지는 각 구성 요소를 하나씩 해체해서 살펴봤다. 한 계층, 하나의 프로토콜, 하나의 라우팅 결정씩 말이다. 병목의 원인을 모르면 문제를 진단할 수 없기에 꼭 필요한 과정이었다.
하지만 현실의 시스템은 독립된 요소들의 단순한 합이 아니다. 사용자의 단 한 번의 클릭 (예. 페이지 로딩, 메시지 전송, 결제 완료)은 여러 계층을 동시에 관통하며, 병목은 그 경로 어디에서든 발생할 수 있다. 이제 질문은 “이 기술이 무엇인가?”가 되어서는 안 된다. **지금 시스템의 제약은 무엇이며, 어떤 조합으로 그 제약을 해소할 것인가?**가 되어야 한다.
서로 다른 병목을 가진 네 가지 시나리오를 통해, 우리가 배운 재료들이 어떤 조합으로 트래픽을 견디는 구조를 만들어내는지 살펴보자.
이미지 대량 로딩 — 두 개의 제약이 동시에 올 때
섹션 제목: “이미지 대량 로딩 — 두 개의 제약이 동시에 올 때”이커머스 상품 페이지를 떠올려 보자. 스크롤 한 번에 고해상도 이미지 80장이 쏟아진다. 각 이미지는 별도의 HTTP 요청이며, 모든 요청은 TCP 핸드셰이크라는 수수료를 지불해야 한다. 여기서 병목은 두 계층에 걸쳐 형성된다. 바로 Network Layer(L3 - 물리적 거리)과 Application Layer(L7 - 요청 전달 방식)이다.
첫 번째 병목은 ‘거리’다 — L3의 제약.
서울에서 미국 서버까지 왕복 시간은 약 150ms다. 이것이 RTT다. L3의 라우팅 경로에 의해 결정되는, 물리 법칙에 묶인 고정 비용인 셈이다. 이미지 80장을 하나씩 가져온다면 순수 네트워크 지연 시간만 12초(80장 × 150ms)가 걸린다. 서버가 연산을 시작하기도 전에 이미 게임은 끝난 것이다. 병목의 정체는 연산 속도가 아니라, 지리적 한계에 있다.
CDN(Content Delivery Network)은 정적 콘텐츠를 사용자와 물리적으로 가까운 ‘엣지 서버’에 미리 옮겨두어 이 문제를 해결한다. 서울 사용자는 서울에 있는 서버를 만나고, 상파울루 사용자는 상파울루 서버를 만난다. RTT가 줄어드는 원리는 통신 기술을 혁신한 것이 아니라, L3 경로 자체를 물리적으로 단축한 것에 있다.
제약 이론(TOC)의 관점에서 보면, RTT가 제약일 때의 전략은 패킷이 도착한 뒤의 처리를 최적화하는 데 있지 않다. 서버를 사용자 옆으로 옮겨 패킷의 이동 거리 자체를 줄이는 것이 유일한 해답이다.
두 번째 병목은 ‘순차 전달’이다 — L7의 제약.
아무리 가까운 곳에 CDN이 있어도, HTTP/1.1로 이미지 80장을 요청하면 Head-of-Line Blocking이라는 늪에 빠진다. 앞선 이미지 하나가 늦어지면 뒤따르는 모든 이미지가 줄줄이 대기해야 하기 때문이다.
이때 HTTP/2 멀티플렉싱이 구원투수로 등장한다. 80개의 요청을 작은 조각(Frame)으로 쪼개 하나의 연결 통로에 섞어 보낸다. 작은 썸네일들이 용량이 큰 이미지 사이사이를 빠져나가며 전송된다. 연결은 Keep-Alive로 유지되고, 멀티플렉싱으로 대기열은 사라진다. 이 모든 개선은 오직 L7 계층에서만 일어난다. 그 아래 계층은 아무것도 바뀔 필요가 없다.
[Without CDN + HTTP/1.1]
Client ├─ Request image1 → (150ms) → Response ├─ Request image2 → (150ms) → Response ├─ Request image3 → (150ms) → Response │ ├─ ... │ └─ Request image80 → (150ms) → Response
80 images × 150ms RTT × sequential= 12,000ms (12s)= 고통스럽게 느림
------------------------------------------
[With CDN + HTTP/2]
Client ├─────── Single Connection ──────┐ │ │ │ ┌ image1 ┐ ┌ image2 ┐ │ │ ├──────────┤ ├──────────┤ │ │ ├ image3 ┤ ├ image4 ┤ │ │ ├──────────┤ ├──────────┤ │ │ ├ ... ┤ ├ ... ┤ │ │ └ image80 ┘ └──────────┘ │ │ │ │ (sent & received concurrently) │ └────────────────────────────────┘
80 images × ~5ms RTT × multiplexed≈ 수십= 빠름정적 콘텐츠가 많고 사용자가 전 세계에 퍼져 있다면, CDN과 HTTP/2의 조합은 선택이 아닌 필수다. 반면 콘텐츠가 실시간으로 변하고 사용자가 특정 지역에 밀집해 있다면, CDN의 효용은 급격히 떨어진다.
제약 이론(TOC) 핵심: 서로 다른 계층에서 두 개의 제약이 동시에 발생할 때, 해법 또한 입체적이어야 한다. 하나만 해결하고 나머지를 방치하면, 병목은 사라지는 것이 아니라 다른 계층으로 옮겨갈 뿐이다.
Head-of-Line Blocking, 멀티플렉싱 → 네트워크 3편
제약 이론(TOC) → 네트워크 1편
실시간 메시징 — 비용이 옮겨가는 곳
섹션 제목: “실시간 메시징 — 비용이 옮겨가는 곳”채팅이나 알림 시스템의 핵심은 즉시성이다. 클라이언트가 묻기 전에 서버가 먼저 정보를 밀어 넣어줘야 한다. 하지만 우리가 사용하는 HTTP는 철저히 클라이언트가 묻고 서버가 답하는 구조로 설계되었다. 서버에 새로운 알림이 있어도, 클라이언트의 요청이 올 때까지 기다려야 하는 L7 계층의 구조적 한계가 명확히 존재한다.
이 제약을 넘어서기 위해 엔지니어들은 여러 우회로를 고안해냈다. 재미있는 점은, L7에서 이 문제를 해결하려고 할수록 그 비용은 고스란히 Transport Layer(L4 계층 - 연결 수와 포트 자원)으로 전가된다. 세 가지 대표적인 전략이 비용을 어디서 지불하는지 살펴보자.
| 프로토콜 | 연결 비용 | 서버 자원 | 방향 |
|---|---|---|---|
| Long Polling | 메시지 전송 시마다 | 낮음 (단기 유지) | 클라이언트 → 서버 |
| SSE | 세션 수립 시 1회 | 중간 (단방향) | 서버 → 클라이언트 |
| WebSocket | 세션 수립 시 1회 | 높음 (영구 유지) | 양방향 |
Long Polling — 한 번의 응답, 한 번의 재연결, 매번 새 계약.
클라이언트가 요청을 보내면 서버는 새 요청이 생길 때까지 응답을 붙잡고 있다. 그러다 메시지가 생기면 그제야 대답을 한다. 답을 받은 클라이언트는 즉시 다시 요청을 보낸다. 겉보기엔 연결이 유지되는 듯하지만, 실상은 매번 새로운 계약(Connection)을 맺는 과정의 반복이다.
[Long Polling — 재연결 루프]
Client Server│ ││ ───── Request ────────────────> ││ ││ (대기중...) ││ (대기중...) ││ <──── Response ─────────────── │ "새 메시지"│ ││ ───── Request ────────────────> │ ← 즉시 재연결│ ││ (대기중...) ││ <──── Response ─────────────── │ "또 다른 메시지"│ ││ ───── Request ────────────────> ││ (무한 반복) │거래비용이론의 관점에서 보면, 롱 폴링은 TCP 핸드셰이크 문제가 변형된 형태다. 매 응답-요청 사이클이 곧 새로운 협상이며, 계약은 다음으로 이월되지 않는다. 거래비용을 줄이는 가장 확실한 방법은 거래 횟수 자체를 줄이는 것인데, 롱 폴링은 정반대로 거래 횟수를 늘리는 구조를 취한다.
오버헤드는 가상적 추론이 아닌 현실의 문제다. 재연결마다 HTTP 헤더가 중복 전달되고, Keep-Alive가 만료되면 매번 새 TCP 핸드셰이크가 발생한다. 이는 서버 연결 풀의 슬롯 점유로 이어진다. 수만 명의 사용자가 메시지를 대기하는 환경에서는, 실제 데이터를 주고받기도 전에 이 ‘협상 수수료’만으로도 서버는 포화 상태에 이른다.
SSE (Server-Sent Events) — 하나의 채널, 한 방향, 무기한 유지.
SSE는 하나의 HTTP 연결을 무기한 열어둔다. 서버는 서버는 새로운 소식이 있을 때마다 이 통로를 통해 데이터를 밀어 넣는다. 클라이언트는 재연결하지 않고, 그냥 받기만 한다.
[SSE — 단방향 스트림]
Client Server│ ││ ───── Request (구독) ─────────> ││ ││ ││ <──── Event Stream ─────────── │ "메시지 1"│ <──── Event Stream ─────────── │ "메시지 2"│ <──── Event Stream ─────────── │ "업데이트"│ <──── Event Stream ─────────── │ "알림"│ ││ (연결 유지 중) │이건 Keep-Alive 논리가 실시간 통신에 적용된 것이다. 핸드셰이크 한 번, 메시지 여러 개. 협상 수수료를 한 번 내고 이후의 모든 이벤트에 걸쳐 분할 상환한다. 주가 알림이나 라이브 스코어처럼 서버가 일방적으로 정보를 쏟아내는 상황에 최적이다.
다만, 클라이언트가 답장을 보내려면 별도의 HTTP 요청을 새로 만들어야 하므로, 채팅처럼 주고받는 대화에서는 비용 절감 효과가 반감된다.
WebSocket — 하나의 연결, 양방향, 영구적.
WebSocket은 HTTP 요청으로 시작해서, 곧장 영구적인 전이중(Full-duplex) 채널로 연결을 업그레이드 한다. 이제 양쪽 모두 언제든 소통할 수 있다. 재협상도, 새 연결도 필요 없이, 딱 한 번의 연결로 성립된다.
[WebSocket — 전이중 통신]
Client Server│ ││ ─── HTTP Upgrade ───────────> ││ <── 101 Switching ─────────── ││ ││════════════════════════════════││ 영구 양방향 채널 ││════════════════════════════════││ ││ ───────── "헤이" ────────────> ││ <──────── "안녕" ────────────── ││ ─────── "확인" ─────────────> ││ <──────── "뉴스" ────────────── ││ ... │메시지당 거래비용은 거의 0에 수렴한다. 전체 협상 오버헤드가 연결 시작 시 발생하는 단 한 번의 업그레이드 핸드셰이크에 집중되기 때문이다.
하지만 비용이 소멸한 것은 아니다. 단지 형태를 바꿔 옮겨갔을 뿐이다. 각 웹소켓 연결은 서버의 영구적인 TCP 소켓 자원을 점유한다. 서버당 할당 가능한 포트는 약 28,000개 수준으로 제한되어 있다. 연결이 종료된 후 발생하는 TIME_WAIT 상태는 계속 유지 중인 웹소켓 연결에는 해당하지 않으나, 역설적으로 연결이 끊기지 않는 한 포트는 영구적으로 묶인다. 동시 접속자가 50,000명인 채팅 서비스라면 50,000개의 소켓이 상시 열려 있어야 하며, 이는 물리적 자원의 연속적인 점유를 의미한다.
결국 질문은 하나로 귀결된다: 자주 재협상하는 비용이 비싼가, 아니면 통로를 계속 열어두는 유지비가 비싼가?
메시지가 쏟아진다면(높은 빈도): 웹소켓이 정답이다. 롱 폴링의 반복적인 협상 수수료를 감당할 수 없기 때문이다. 사용자는 많지만 알림은 가끔 온다면(낮은 빈도): SSE나 롱 폴링이 유리할 수 있다. 상호작용이 없을 때는 자원을 해제하여 서버의 부하를 줄여줄 수 있다.
제약 이론(TOC) 은 여기서도 유효하다. 어느 자원이 먼저 포화되는지 — 메시지 처리량인지 연결 수인지 — 를 찾고, 그에 맞춰 선택하라.
L4 포트 한계(28,000개) → 네트워크 1편
제약 이론(TOC) → 네트워크 1편
글로벌 라우팅 — 정보 격차를 메우는 곳
섹션 제목: “글로벌 라우팅 — 정보 격차를 메우는 곳”서울, 런던, 상파울루에 사용자가 있는 서비스가 모든 서버를 미국 버지니아us-east-1에서만 운영한다고 가정해보자.
서울 사용자가 접속할 때마다 발생하는 왕복 시간(RTT)은 약 150ms이다. 서버가 단 5ms 만에 계산을 끝내더라도, 사용자는 화면이 뜨기 시작할 때까지 최소 150ms 이상을 멍하니 기다려야 한다.
여기서 병목은 서버의 성능이 아니라, 서버와 사용자 사이를 잇는 ‘L3 물리적 거리’ 그 자체에 있다.
L7 계층에서 동작하는 DNS는 이 경로를 결정하는 ‘나침반’ 역할을 한다. 하지만 DNS 라운드 로빈은 지리적 거리 문제를 전혀 해결하지 못한다. 클라이언트가 어디에 있든 상관없이 그저 IP를 순서대로 던져줄 뿐이다. 서울 사용자가 지구 반대편 버지니아 서버로 연결되어 고통받는 동안, 바로 옆 도쿄 서버는 한가하게 놀고 있을 수 있다는 뜻이다.
이것이 바로 정보 비대칭(Information Asymmetry)의 폐해이다. 결정을 내리는 주체(DNS)가 결정에 꼭 필요한 정보(클라이언트의 위치)를 모르고 있기 때문이다.
GeoDNS는 그 정보 격차를 메운다.
DNS 쿼리가 도달하면 GeoDNS는 클라이언트의 IP 주소를 통해 지리적 위치를 추론하고, 가장 가까운 서버의 IP를 반환한다. L7에서 획득한 이 정보는 결과적으로 L3의 물리적 라우팅 경로를 결정한다. 상위 계층의 정보가 하위 계층의 비용을 바꾸는 구조다. 이제 서울 사용자는 도쿄 서버로, 런던 사용자는 프랑크푸르트 서버로 연결된다.
[전통적 DNS 라운드 로빈]
┌───────────┐ │ DNS │ │ (로직 없음) │ └─────┬─────┘ │ ┌────────────┼────────────┐ │ │ │서울 사용자 런던 사용자 상파울루 사용자 │ │ │ └──────┬─────┴─────┬──────┘ ▼ ▼ ┌────────────┬────────────┐ │ │ │192.168.1.1 192.168.1.2 192.168.1.3 (버지니아) (버지니아) (버지니아) │ │ │ 150ms 80ms 180ms
--------------------------------------
[GeoDNS (위치 인식 라우팅)]
┌────────────┐ │ DNS │ │ (위치 로직) │ └─────┬──────┘ │ ┌──────────────┼─────────────┐ │ │ │ 서울 사용자 런던 사용자 상파울루 사용자 │ │ │ ▼ ▼ ▼ (아시아) (유럽) (남미) │ │ │┌──────────┐ ┌──────────┐ ┌───────────┐│ 도쿄 │ │ 프랑크푸르트 │ │ 상파울루 ││ 10.0.1.1 │ │ 10.0.2.1 │ │ 10.0.3.1 │└──────────┘ └──────────┘ └───────────┘ │ │ │ 30ms 15ms 10msDNS 라운드 로빈이 기계적인 배분이었다면, GeoDNS는 철저히 정보에 기반한 라우팅이다. 그동안 누락되었던 정보인 ‘클라이언트의 위치’가 의사결정의 핵심 변수가 된 셈이다. 결국 정보의 격차를 어떻게 다루느냐가 아키텍처의 성패를 결정한다. GeoDNS는 가장 본질적인 정보인 사용자가 어디에 있는가를 확보함으로써 지리적 병목을 해결한다.
엣지 서버는 이 논리를 정적 콘텐츠 너머로 확장한다.
CDN은 이미지와 파일을 캐싱하는 데 그치지만, 엣지 서버(Edge Server)는 인증 확인, 개인화 로직, API 응답과 같은 L7 연산을 사용자 가까이에서 직접 수행한다. CDN이 L3의 물리적 거리를 줄이는 것에 집중한다면, 엣지 서버는 L7의 처리 지점 자체를 사용자 쪽으로 전진 배치하는 전략이다.
사용자가 두 개 대륙 이상에 걸쳐 분산되어 있다면, GeoDNS와 엣지 서버의 조합은 RTT를 구조적으로 단축할 수 있는 유일한 대안이다. 그러나 단일 리전만으로 충분한 서비스라면, 이러한 구성은 도리어 운영 복잡성만 가중시킨다.
정보 비대칭의 관점에서 보면, 라우팅의 효율은 클라이언트의 위치를 얼마나 정확히 알고 있느냐에 결정된다. DNS가 사용자의 위치를 모른 채(정보 부재) IP를 무작위로 배분하면 지리적 비용이 치솟지만, 이 정보의 격차를 메우면 최적의 경로를 찾아낼 수 있다. 정보를 무시하고 내린 결정의 대가는 결국 지리적 한계가 만드는 속도 저하로 나타난다.
DNS 라운드 로빈, 정보 비대칭 → 네트워크 4편
결제 재시도 — TCP의 신뢰가 끝나는 곳
섹션 제목: “결제 재시도 — TCP의 신뢰가 끝나는 곳”하나의 시나리오를 가정해 보자. 사용자가 결제 버튼을 누르고 서버가 승인까지 마쳤으나, 응답이 돌아오는 길에 네트워크 타임아웃이 발생한다. 클라이언트는 이를 실패로 오해하고 재시도하며, 보호 장치가 없다면 카드는 중복 결제된다. 기술적으로는 성공한 전송이 비즈니스에는 참사로 이어지는 순간이다.
TCP가 약속하는 범위를 명확히 할 필요가 있다. TCP는 신뢰성을 위해 속도를 희생하며 패킷의 무결성을 보장하지만, 그 책임은 전송 계층(L4)의 경계에서 끝난다. 데이터가 상대방의 입구까지 전달되는 것만 약속할 뿐, 애플리케이션(L7)이 그 데이터를 처리한 이후의 상황은 책임지지 않는다.
위 사례의 타임아웃은 TCP의 실패가 아니다. TCP는 요청을 서버에 무사히 전달했고 서버도 처리를 마쳤지만, 돌아오는 응답만 유실되었을 뿐이다. L4의 TCP는 계약을 완수했으나, 그 너머의 신뢰를 직접 구축해야 할 L7이 무방비로 남겨진 셈이다.
Client Server| || ——— "$50 청구" ————————> | ✓ TCP 전달 완료| | ✓ 서버가 카드 청구| <—— 응답 ———————————— ✕ | ✗ 응답이 전송 중 유실| || (타임아웃 — 사용자 재시도) || || ——— "$50 청구" ————————> | ✓ TCP 다시 전달 완료| | ✗ 서버가 카드를 한 번 더 청구멱등성 키는 이 문제를 L7에서 해결한다. 클라이언트는 의도된 행동마다 고유 키를 생성해 요청에 첨부한다. 동일한 키가 다시 도착하면 서버는 이를 재시도로 판단하고, 중복 결제 없이 보관해 둔 원래의 결과만을 반환한다.
Client Server| || ——— "$50 청구" || key: abc-123 ————————> | ✓ abc-123 처음 수신| | ✓ 카드 청구, 결과 저장| <—— 응답 ———————————— ✕ | ✗ 응답 유실| || ——— "$50 청구" || key: abc-123 ————————> | → abc-123 이미 처리됨| | → 저장된 결과 반환, 재청구 없음| <—— 응답 ———————————————— | ✓ 클라이언트 확인 수신핵심 구분은 명확하다. TCP는 at-least-once(최소 한 번) 전달을 보장한다. 애플리케이션은 exactly-once(정확히 한 번) 실행이 필요하다. 멱등성 키가 이 두 세계의 간극을 메운다.
여기가 네트워크 계층의 개념과 애플리케이션 계층의 설계가 맞물리는 지점이다. TCP 핸드셰이크가 확보한 신뢰는 L4 경계까지만 유효하다. 그 너머의 신뢰는 L7이 직접 구축해야 한다.
네트워크 재시도가 발생할 수 있는 모든 쓰기 작업(예. 결제, 주문, 예약 등)에는 멱등성 키가 필수적이다. 반면 읽기 전용 API에는 이 비용을 지불할 필요가 없다.
거래비용이론을 대입해 보면 명확해진다. TCP의 계약은 L4까지만 유효하다. L7에서의 실행을 완벽히 보장하려면 별도의 추가 계약이 필요하며, 멱등성 키는 바로 그 계약을 유지하기 위해 지불하는 비용이다.
The Bottom Line
섹션 제목: “The Bottom Line”네 가지 시나리오와 각기 다른 병목, 그리고 우리가 앞서 다룬 기술적 구성 요소들이 어떻게 다르게 조합되는지 살펴보았다.
이미지 로딩은 전송 거리와 대기열이라는 두 가지 비용을 각기 다른 계층에서 동시에 해결했다. 실시간 메시징은 메시지마다 지불하던 협상 수수료를 세션 단위로 통합했다. 통신 빈도를 줄이는 대신 자원 점유를 선택한 결과다. 글로벌 라우팅은 기존 DNS가 보지 못했던 정보의 격차를 메우며 최적의 경로를 도출했다. 결제 재시도 시나리오는 TCP의 신뢰가 만료되는 경계를 명확히 드러냈으며, 그 너머의 신뢰는 애플리케이션이 직접 구축해야 함을 증명했다.
모든 시나리오는 결국 동일한 질문을 던진다. 병목은 어디에 있으며, 이를 해소하기 위해 무엇을 기꺼이 내줄 것인가?
답은 단 한 번도 같지 않았다.
보편적인 아키텍처는 없다. 오직 당면한 제약 조건에 가장 적합한 아키텍처만 있을 뿐이다
다음 편에서는 네트워크가 요청을 무사히 전달한 이후의 과정을 다룬다. 서버가 요청을 처리하기 위해 마주하는 첫 번째 관문은 데이터베이스다. 결제 시나리오에서 언급한 멱등성 키, 즉 중복 요청을 단 한 번만 실행한다는 약속은 데이터베이스의 트랜잭션 없이는 완성될 수 없다. 모든 쿼리와 쓰기 작업이 직면하는 최후의 물리적 제약은 디스크 I/O다. 데이터베이스 시리즈는 바로 그 지점에서 시작한다.