목차
1. 이상징후, 장애, 보안 — 용어부터 정리
네트워크를 공부하다 보면 장애와 보안 이슈가 매우 비슷한 증상으로 나타나는 경우가 많습니다. 패킷이 갑자기 줄거나, 연결이 끊기거나, 응답이 느려지는 현상은 단순한 시스템 장애일 수도 있고 보안 장비의 정상적인 차단 작동일 수도 있습니다.
그래서 이런 비정상적인 현상들을 처음에는 모두 묶어서 이상징후라고 부릅니다. 이상징후가 발견되면 그 다음 단계로 "이게 시스템 장애인지" 아니면 "보안 이슈인지"를 판단하게 됩니다. 즉 순서는 이상징후 발견이 먼저고, 장애인지 보안인지의 판단은 그 다음입니다.
이 글에서는 그중에서도 전송 계층, 즉 TCP가 담당하는 영역에서 발생하는 장애 유형을 다룹니다. 특히 소켓 프로그래밍을 직접 하는 개발자라면 오늘 다룰 내용 중 하나는 반드시 코드 레벨에서 직접 영향을 주는 문제이니 끝까지 잘 따라오시면 좋습니다.
2. 유형 1 — 패킷 유실과 순서 뒤바뀜 (인프라 레벨)
TCP 전송 과정에서 자주 관찰되는 대표적인 두 가지 현상이 있습니다. 하나는 리트랜스미션(Retransmission, 재전송)이고, 다른 하나는 듀플리케이트 어크놀리지먼트(Duplicate Acknowledgement, 중복 확인응답)입니다. 이 두 가지는 결과적으로 나타나는 현상이고, 그 원인은 다음 두 가지 중 하나입니다.
- 패킷(세그먼트)이 중간에 유실된 경우 (Loss)
- 패킷이 도착했지만 순서가 뒤바뀐 경우 (Out-of-Order)
이런 현상이 관찰되면 일반적으로 네트워크 혼잡 상황이라고 규정합니다. 이건 인프라 레벨의 문제입니다. 인터넷이라는 것은 라우터의 거대한 집합체입니다. 어디로 통신을 하든 중간에 라우터가 보통 20~30개 정도 거쳐갑니다. 이 수십 개의 장비 중에서 단 하나라도 오작동을 하면 패킷이 유실되거나 순서가 뒤바뀌는 현상이 발생합니다.
보안 장비도 패킷 유실의 원인이 될 수 있다
여기서 한 가지 중요한 점은, 패킷 유실이 항상 인프라 결함 때문만은 아니라는 것입니다. 패킷 필터링 방화벽 같은 보안 장비가 의도적으로 특정 패킷을 걸러내는 경우에도 송수신 엔드포인트 입장에서는 동일하게 "패킷이 유실된" 것으로 보입니다.
이건 정상적인 보안 활동이라서 엄밀히는 장애가 아니지만, 엔드포인트 입장에서는 결과적으로 장애와 똑같은 현상으로 관찰됩니다. 그래서 네트워크 장애를 분석할 때는 항상 보안 영역까지 함께 고려해야 합니다. 의도치 않게 보안 정책이 장애의 원인으로 작용하는 사례는 생각보다 많습니다.
이런 인프라 레벨 문제는 비교적 최근에는 발생 빈도가 낮은 편에 속하는 장애 유형입니다.

3. TCP 송수신 기본 원리 — 왜 한 번에 다 안 보낼까
본격적으로 두 번째 장애 유형인 제로 윈도우를 이해하려면, 먼저 TCP가 데이터를 어떻게 주고받는지 기본 원리를 알아야 합니다.
예를 들어 PC에서 서버로 약 2MB짜리 파일을 업로드한다고 가정해 봅시다. TCP의 최대 세그먼트 크기(MSS)는 보통 1,460바이트이기 때문에, 2MB 파일을 보내려면 세그먼트가 1,000개 이상 필요합니다.
이 많은 세그먼트를 한 번에 다 보내면 좋겠지만 TCP는 그렇게 하지 않습니다. 처음에는 아주 조심스럽게 시작합니다.
2. ACK 수신 확인 → 이번엔 2개 전송
3. 또 ACK 수신 확인 → 이번엔 4개 전송
4. 계속 2배씩 증가 → 8개, 16개, 32개...
이렇게 한 번에 보내는 세그먼트 개수를 점점 2배씩 늘려가는 과정을 거칩니다. 처음에 한 개씩 보내고 ACK를 기다리면 너무 느립니다. 이때 발생하는 대기 시간을 RTT(Round Trip Time, 왕복 시간)라고 부릅니다. 만약 RTT가 20ms라면, 세그먼트 하나를 보낼 때마다 최소 20ms 이상의 지연이 발생하게 됩니다. 그래서 한 번에 보내는 양을 점차 늘려가야 전체 전송 효율이 올라갑니다.
4. 슬라이딩 윈도우와 전송 속도 증가 그래프
이렇게 전송량을 늘려가다 보면, 어느 시점에서 자신의 최대 대역폭에 도달하게 됩니다. 그래프로 그리면 다음과 같은 형태입니다.
↑
| ___________________ (대역폭 한계 도달, 안정화)
| /
| /
| / (지수적으로 증가)
|/
+------------------------→ 시간(t)
처음에는 천천히, 그러다가 어느 지점에서 급격히 증가하고, 대역폭 한계에 가까워지면 다시 천천히 늘리다가 결국 평평하게 수렴합니다. 대략 80~90% 지점에 도달하면 더 이상 무리하게 보내는 양을 늘리지 않고 그 상태를 유지합니다. 회선 품질이 좋다면 이런 형태의 그래프가 안정적으로 유지됩니다.
여기서 한 번에 관리하는 세그먼트의 묶음, 즉 "한 번에 바라보는 데이터의 범위"를 윈도우(Window)라고 부릅니다. 요즘 표현으로 치면 일종의 '컨텍스트'와 비슷한 개념입니다. 이 윈도우가 시간이 지남에 따라 오른쪽으로 이동하면서 점점 커지기 때문에 슬라이딩 윈도우(Sliding Window)라고 부릅니다.
5. 패킷 유실 시 복구 — 누적 ACK vs 선택적 ACK
이제 만약 전송 도중 특정 세그먼트가 유실되거나 순서가 뒤바뀌면 어떻게 될까요?
케이스 1: 세그먼트가 아예 유실된 경우
4번, 5번, 6번, 7번 세그먼트를 보냈는데 6번이 도중에 유실됐다고 가정합시다. 수신 측에서는 4번, 5번을 받고 6번을 못 받은 채 갑자기 7번을 받게 됩니다. 이때 수신 측은 ACK를 보낼 때 "나는 5번까지 받았고, 6번을 기다리고 있다"는 의미로 ACK 번호를 6으로 보냅니다.
송신 측은 이 ACK를 보고 "6번이 유실됐구나"라고 판단하고, 6번과 7번을 다시 전송합니다. 이렇게 다시 보내는 행위가 바로 리트랜스미션입니다. 패킷 로스가 발생하면 자연스럽게 일어나는 현상입니다.
케이스 2: 세그먼트 순서만 뒤바뀐 경우 (Out-of-Order)
이번엔 6번과 7번이 모두 도착했지만, 순서가 바뀌어서 7번이 먼저 도착하고 6번이 나중에 도착한 경우입니다. 수신 측 입장에서는 4번, 5번을 받고 6번이 안 온 상태에서 7번이 먼저 온 것처럼 보입니다.
여기서 중요한 판단 포인트가 등장합니다. 수신 측이 7번을 받자마자 즉시 "6번 못 받았다"고 응답할 것인지, 아니면 잠깐 기다려볼 것인지 결정해야 합니다.
누적 ACK (Cumulative Acknowledgement)
가장 단순한 방식은, 6번을 못 받았으니 6번부터 그 이후 모든 세그먼트(6번, 7번)를 다시 보내달라고 요청하는 것입니다. 이를 누적 ACK라고 부릅니다. 그런데 이 방식은 비효율적입니다. 7번은 이미 잘 받았는데도 다시 보내야 하기 때문입니다.
선택적 ACK (Selective Acknowledgement, SACK)
이를 개선한 방식이 선택적 ACK입니다. "7번까지는 잘 받았으니, 빠진 6번만 다시 보내달라"고 정확히 콕 집어서 요청하는 방식입니다. 누락된 부분만 정확히 재전송받기 때문에 누적 ACK보다 훨씬 효율적입니다.
ACK는 언제 오는가 — 정해진 답이 없다
흥미로운 점은, ACK를 정확히 언제 보내야 하는지에 대한 명확한 표준이 없다는 것입니다. 이는 전적으로 TCP를 구현하는 쪽(운영체제)의 재량에 달려 있습니다. 그래서 TCP에는 컴파운드 TCP, 리노(Reno) 등 수십 가지의 구현 방식이 존재합니다.
과거에는 윈도우 운영체제의 TCP 구현과 리눅스의 TCP 구현 방식이 서로 잘 맞지 않아 호환성 문제가 있었던 적도 있습니다. 지금은 이런 부분들이 많이 보완되어 큰 문제 없이 상호 운용됩니다.

6. 유형 2 — 제로 윈도우, 진짜 범인은 누구인가
이제 오늘의 핵심인 제로 윈도우(Zero Window)를 다룰 차례입니다. 소켓 프로그래밍을 하는 개발자라면 반드시 기억해야 할 내용입니다.
제로 윈도우는 수신 측 버퍼의 여유 공간이 완전히 소진됐을 때 발생합니다. 여기서 말하는 버퍼는 정확히는 운영체제(OS) 버퍼입니다. 이 운영체제 버퍼에 더 이상 빈 공간이 없을 때 제로 윈도우가 발생합니다.
윈도우 사이즈 vs 슬라이딩 윈도우 — 다른 개념
여기서 용어를 정확히 구분해야 합니다.
| 용어 | 의미 |
|---|---|
| 슬라이딩 윈도우 | 한 번에 관리하는 세그먼트의 범위(컨텍스트). 시간이 지나며 오른쪽으로 이동하고 커짐 |
| 윈도우 사이즈 | 수신 측 TCP 버퍼의 '여유 공간' 크기. 데이터가 쌓일수록 줄어듦 |
네트워크에서 데이터가 들어오면, 이 데이터는 네트워크 인터페이스 카드(NIC)를 거쳐 TCP 고유의 버퍼 메모리 공간에 저장됩니다. 데이터가 1번, 2번, 3번... 순서대로 쌓이면서 이 버퍼의 여유 공간은 점점 줄어듭니다. 바로 이 여유 공간이 윈도우 사이즈입니다.
여유 공간이 0이 되면 벌어지는 일
만약 이 윈도우 사이즈가 0이 된 상태에서 새로운 세그먼트(예: 100번)가 도착하면 어떻게 될까요? 네트워크 단까지는 정상적으로 도착했지만, OS 레벨에서 이 데이터를 담을 공간이 없어서 처리가 불가능한 상황이 됩니다.
이때 수신 측은 ACK를 보내면서 윈도우 사이즈가 0이라는 정보를 함께 전달합니다. "나는 더 이상 네 데이터를 받을 저장 공간이 없다"는 신호입니다. 이를 받은 송신 측은 더 이상 데이터를 보내지 않고 대기합니다.
참고로 평소에 주고받는 모든 ACK에는 항상 현재 윈도우 사이즈 정보가 포함되어 있습니다. 그래서 송신 측은 ACK를 받을 때마다 "수신 측의 여유 공간이 점점 줄어들고 있구나"를 실시간으로 파악할 수 있습니다.
원인은 어디에 있을까 — OS인가, 애플리케이션인가
제로 윈도우 현상이 관찰되면 원인을 어디서 찾아야 할까요? 가능성은 크게 두 곳입니다.
| 가능한 원인 | 실제 가능성 |
|---|---|
| 운영체제(OS) 자체의 문제 | 매우 매우 매우 낮음 |
| 애플리케이션 프로세스의 문제 | 대부분 이쪽이 원인 |
이게 왜 까다로운 문제냐면, 네트워크 담당자는 "네트워크 인프라를 어떻게 손봐야 하나"라고 생각하기 쉽지만, 실제로는 프로그램 코드를 고쳐야 하는 문제이기 때문입니다. 네트워크 담당자가 "나는 코딩을 모른다"고 하더라도, 이 레벨의 수정이 이루어지지 않으면 근본적인 해결이 불가능하다는 점을 알고 있어야 합니다.
7. 제로 윈도우 해결책 — Receive와 처리를 분리하라
그렇다면 왜 애플리케이션에서 이런 문제가 생기는지, 그리고 어떻게 해결해야 하는지 살펴보겠습니다.
recv() 함수의 본질
소켓 프로그래밍에서 사용하는 recv() 같은 수신 함수는 본질적으로 '읽기'가 아니라 '이동(move)'에 가깝습니다. TCP 버퍼에 쌓여있는 데이터를 애플리케이션 영역으로 옮겨오는 작업입니다. 데이터를 옮겨오면 그만큼 OS 버퍼에는 여유 공간이 다시 생깁니다.
recv()는 대표적인 블로킹(Blocking) I/O 함수입니다. 읽어올 데이터가 없으면 이 함수는 리턴하지 않고 계속 대기 상태(Wait)에 머뭅니다. 네트워크에서 데이터가 도착해야 비로소 깨어나서 데이터를 가져옵니다.
문제는 '수신 속도'와 '처리 속도'의 차이
여기서 핵심 비교 대상이 등장합니다. 데이터를 수신하는 속도와 그 데이터를 처리하는 속도입니다.
만약 데이터 하나를 처리하는 데 3초가 걸리는데, 그 사이에도 네트워크로는 계속 데이터가 들어온다면 어떻게 될까요? 처리 속도가 수신 속도를 따라가지 못하면, 데이터는 버퍼에 계속 쌓이기만 하고 비워지지 않습니다. 결국 여유 공간은 완전히 소진되고 제로 윈도우 상태에 빠집니다.
해결책: 수신과 처리를 반드시 분리하라
해결 방법은 명확합니다. 데이터 수신(receive)과 데이터 처리(processing)를 별도의 스레드로 반드시 분리해야 합니다.
↓ (큐에 데이터 적재)
[처리 스레드] : N개 (CPU 코어 수에 따라 조정)
구체적인 방법은 큐(Queue) 자료구조를 활용하는 것입니다. 수신 스레드는 데이터가 도착하는 대로 큐에 차곡차곡 쌓기만 합니다. 그리고 별도의 처리 스레드들이 이 큐에서 데이터를 하나씩 꺼내서 처리합니다.
이를 식당에 비유하면 이해하기 쉽습니다. 식당의 출입구는 1개면 충분합니다. 손님이 입장하는 통로 역할만 하면 되니까요. 하지만 식당 내부에서 서빙을 담당하는 직원(알바생)은 식당 규모에 따라 여러 명이 필요합니다.
마찬가지로 수신 스레드는 1개로 충분하지만(입구 역할), 처리 스레드는 여러 개가 필요하며(서빙 역할) 이 개수는 상황에 따라 달라집니다. 보통 이 처리 스레드의 개수는 해당 서버를 구동하는 컴퓨터의 CPU 코어 개수를 기준으로 결정합니다.
8. 정리 — 장애 원인을 어디서 찾아야 하는가
오늘 다룬 두 가지 TCP 장애 유형을 정리하면 다음과 같습니다.
| 장애 유형 | 주요 증상 | 주요 원인 | 대응 방향 |
|---|---|---|---|
| 패킷 유실 / 순서 뒤바뀜 | 리트랜스미션, 중복 ACK | 네트워크 인프라 (라우터·스위치 오작동), 또는 보안 장비의 패킷 필터링 | 네트워크 경로 점검, 보안 정책 확인 |
| 제로 윈도우 | 수신 측 윈도우 사이즈 0, 송신 중단 | 애플리케이션의 논리적 결함 (수신·처리 미분리) | 수신/처리 스레드 분리, 큐 구조 도입 |
전송 그래프 관점에서 보면, 정상적인 경우 전송 속도는 천천히 증가하다가 안정 구간에 도달해서 직선처럼 유지됩니다. 다만 자세히 확대해서 보면 이 직선도 사실은 미세하게 톱니 모양으로 오르내리는 형태입니다. 그러다 중간에 장애(Burst)가 발생하면 전송량이 뚝 떨어졌다가, 다시 처음부터 또는 일부 수준부터 재시작하면서 새로운 안정 구간을 찾아갑니다. 이 패턴을 이해하고 있으면 네트워크 모니터링 그래프를 볼 때 어느 지점에서 무슨 일이 있었는지 훨씬 잘 해석할 수 있습니다.
※ 이 글은 네트워크 강의 스크립트를 기반으로 정리한 학습 자료입니다. 실제 환경에서의 장애 진단 시에는 패킷 캡처 도구(Wireshark 등)를 활용한 정밀 분석을 권장합니다.