지난번 TCP의 3-Way/4-Way 핸드셰이크와 세그먼트 구조 이야기, 잘 따라오셨나요? 오늘은 TCP가 어떻게 그토록 '믿음직한 전송'을 보장하는지, 그 핵심 비결이자 TCP의 슈퍼파워 3종 세트: 오류 제어, 흐름 제어, 혼잡 제어에 대해 깊숙이 파고들어 보겠습니다.
우리가 웹 서핑을 하거나 파일을 받을 때 데이터가 깨지거나 순서가 뒤죽박죽되지 않고 잘 도착하는 건, 절대 그냥 얻어지는 마법이 아닙니다! 네트워크 너머 상대방의 상태와 인터넷 도로 상황을 끊임없이 살피며 지능적으로 동작하는 TCP의 치밀한 노력 덕분이죠. 그 속사정을 함께 들여다봅시다!
인터넷 망은 생각보다 험난해서, 우리가 보낸 데이터 조각(세그먼트)들이 중간에 길을 잃거나(유실), 순서가 뒤바뀌거나, 심지어 내용이 살짝 변형되기도 합니다. TCP의 첫 번째 임무는 바로 이 문제가 생긴 데이터 조각을 찾아내고(오류 감지) 제대로 된 데이터를 다시 받아오는 것(재전송)입니다. 마치 탐정처럼 단서를 찾아 문제를 해결하죠!
오류 감지: TCP는 어떻게 문제를 알아챌까? (두 가지 핵심 단서!)
단서 1: 타임아웃 (Timeout) - "왜 이렇게 답장이 없지?"
송신 측 TCP는 세그먼트를 보낼 때마다 '이 시간 안에 응답(ACK)이 안 오면 뭔가 문제다!'라고 정한 스톱워치(재전송 타이머)를 누릅니다.
약속된 시간(RTO: Retransmission Timeout)이 지나도 해당 세그먼트에 대한 '잘 받았다!'는 ACK가 도착하지 않으면, "아... 이거 중간에 사라졌거나 너무 늦나 보네?" 하고 판단, 해당 세그먼트를 재전송합니다.
똑똑하게도 이 RTO 값은 네트워크 상태(주로 RTT: 왕복 시간)에 따라 계속 동적으로 조절됩니다. 길이 막히면 좀 더 오래 기다려주는 식이죠.
단서 2: 3개의 중복된 ACK (Duplicate ACKs) - "같은 말만 계속하네?"
수신 측은 데이터를 순서대로 착착 받기를 기대합니다. 그런데 만약 중간에 하나가 빠지고 다음 것이 먼저 도착하면? 수신 측은 "어? 내가 기대한 N
번이 아니잖아! 일단 마지막으로 제대로 받은 N-1
번까지는 확실히 받았다고 다시 알려줘야겠다!" 하면서 이전에 보냈던 ACK를 또 보냅니다. 이것이 바로 중복 ACK입니다.
송신 측에서 이 동일한 ACK를 연달아 3번 (즉, 최초 ACK 포함 총 4번) 받게 되면, "음... 같은 ACK만 4번이나 온 걸 보니, 이 ACK가 요구하는 N
번 세그먼트가 사라졌을 확률이 매우 높군!" 이라고 판단합니다.
이 경우, 타임아웃 스톱워치가 울릴 때까지 기다리지 않고 즉시 사라진 것으로 추정되는 N
번 세그먼트를 재전송합니다. 이것이 바로 빠른 재전송(Fast Retransmit)! 이름처럼 신속하게 대처하는 거죠.
재전송 전략: 어떻게 다시 보낼 것인가? (ARQ 이야기)
오류를 감지했으면 이제 다시 보내야겠죠? 어떻게 효율적으로 재전송할지에 대한 대표적인 방법론(ARQ)들이 있습니다.
Stop-and-Wait ARQ (구식): 세그먼트 하나 보내고, ACK 올 때까지 아무것도 안 하고 하염없이 기다렸다가 다음 것을 보냅니다. 구현은 쉽지만, 너무 비효율적이라 거의 안 쓰입니다.
파이프라이닝 (Pipelining - 요즘 방식): ACK를 기다리지 않고 여러 세그먼트를 파이프라인처럼 쭉~ 연속해서 보냅니다. 훨씬 효율적이죠. 현대 TCP의 기본 방식입니다.
Go-Back-N (GBN) ARQ: 오류가 난 세그먼트부터 시작해서, 그 뒤에 보냈던 것들까지 몽땅 다 재전송합니다. 구현은 상대적으로 쉽지만, 멀쩡한 세그먼트까지 불필요하게 다시 보내는 낭비가 있을 수 있습니다.
Selective Repeat (SR) ARQ: 딱! 오류가 발생한 세그먼트만 선별해서 재전송합니다. 수신 측도 순서가 안 맞아도 일단 버퍼에 가지고 있다가, 빠진 조각이 채워지면 알아서 조립합니다. GBN보다 훨씬 효율적이지만, 구현은 좀 더 복잡합니다.
그래서 현대 TCP는 뭘 쓸까? 기본적으로 가장 효율적인 Selective Repeat (SR) ARQ를 사용합니다. 여기에 빠른 재전송까지 더해져, 오류 발생 시 매우 스마트하고 신속하게 대처할 수 있게 된 거죠. 마치 택배 분실 시 전체 주문 상품을 다시 보내는 게 아니라, 딱 잃어버린 그 물건만 콕 집어 다시 보내주는 것과 비슷합니다.
송신 측이 너무 신나서 데이터를 마구 퍼부으면, 수신 측이 미처 처리하지 못해 가지고 있던 임시 저장 공간(수신 버퍼)이 넘쳐 버릴 수 있습니다(버퍼 오버플로우). 이렇게 되면 넘친 데이터는 그냥 버려지게 되죠. 마치 상대방이 "아 잠깐만!" 하는데도 계속 속사포처럼 말을 쏟아내는 것과 같아요.
흐름 제어는 이런 불상사를 막기 위해, 송신 측이 수신 측의 처리 속도(버퍼 상태)에 맞춰 데이터 전송량을 알아서 조절하는 아주 중요한 배려 기능입니다.
핵심 메커니즘: 슬라이딩 윈도우 (Sliding Window) - "나 이만큼 받을 수 있어!"
수신 윈도우 (Receive Window, rwnd
): 수신 측은 TCP 헤더의 Window Size
필드를 통해, "나 지금 이만큼(바이트 단위)의 여유 공간이 있으니, 이 크기만큼은 더 보내도 돼!" 라고 자신의 현재 상태를 송신 측에게 끊임없이 알려줍니다.
송신 윈도우: 송신 측은 이 rwnd
값을 보고, "아하, 상대방이 이 정도는 감당 가능하구나! 그럼 이 크기를 넘지 않도록 보내야겠다." 라고 판단하여, ACK 응답 없이 한 번에 보낼 수 있는 데이터의 최대 양을 조절합니다.
슬라이딩 (Sliding): 송신 측이 데이터를 보내고 수신 측으로부터 "여기까지 잘 받았어!" 라는 ACK를 받으면, 송신 윈도우는 딱 ACK가 확인해준 데이터 양만큼 오른쪽으로 스르륵 '미끄러지듯(Sliding)' 이동하며 다음 보낼 데이터 범위를 업데이트합니다. 이 과정이 반복되면서 데이터가 막힘없이, 하지만 상대방이 감당할 수 있는 속도로 흘러가게 됩니다.
흐름 제어가 송신자와 수신자 1:1 관계에서의 배려였다면, 혼잡 제어는 나와 인터넷 세상 전체의 관계를 고려하는 기능입니다. 인터넷 망은 여러 사용자가 함께 쓰는 공유 자원이죠. 특정 경로에 너도나도 데이터를 쏟아부으면 라우터 같은 중간 장비들이 과부하에 걸려 교통 체증(혼잡)이 발생하고, 결국 패킷들이 느려지거나 버려지는 대참사(혼잡 붕괴)가 일어날 수 있습니다.
혼잡 제어는 TCP가 네트워크 전체의 상황을 스스로 감지하고 데이터 전송량을 조절하여, 이런 재앙을 막고 모두가 안정적으로 인터넷을 사용할 수 있도록 하는 아주 중요한 사회적(?) 책임 기능입니다.
핵심 메커니즘: 혼잡 윈도우 (Congestion Window, cwnd
) - "현재 도로 상황 반영 속도 제한"
TCP는 패킷 손실이나 지연 등을 통해 네트워크가 얼마나 혼잡한지를 자체적으로 추정하고, 그 결과로 혼잡 윈도우(cwnd
)라는 내부적인 값을 유지합니다. 이건 마치 "현재 고속도로 교통 상황을 보니 시속 80km(데이터 전송량) 이상은 무리겠군" 하고 스스로 정한 속도 제한 같은 개념입니다.
실제로 송신 측이 한 번에 보낼 수 있는 데이터 양은 수신자의 처리 능력(rwnd
)과 네트워크 상황(cwnd
) 중 더 작은 값으로 결정됩니다. 즉, 실제 전송량 = min(cwnd, rwnd)
입니다. 상대방 페이스(흐름 제어)와 도로 사정(혼잡 제어)을 모두 고려하는 거죠!
네트워크 혼잡은 주로 패킷 손실(타임아웃 또는 3개의 중복 ACK)이라는 '교통 사고' 발생 여부를 통해 감지합니다.
혼잡 제어 알고리즘: 눈치껏 속도 조절하는 TCP의 운전 스타일! 🚗💨
TCP는 네트워크 상황에 따라 cwnd
크기를 아주 지능적으로 조절하기 위해, 다음과 같은 여러 알고리즘(운전 전략)을 조합하여 사용합니다.
AIMD (Additive Increase / Multiplicative Decrease): 기본 운전 습관 - "조심조심 가다가, 사고 나면 확 줄이기!"
AI (합 증가): 별다른 문제(혼잡 감지X)가 없으면, cwnd
를 선형적으로(조금씩, +1 MSS씩) 늘리면서 "도로 괜찮은데? 조금 더 밟아볼까?" 하고 조심스럽게 속도를 높입니다.
MD (곱 감소): '앗! 혼잡이다!(패킷 손실 감지)' 싶으면, "어이쿠! 사고 날 뻔!" 하면서 cwnd
를 급격하게(예: 절반으로 확) 줄여서 네트워크 부담을 빠르게 덜어줍니다. (이것 때문에 cwnd
변화 그래프가 톱니 모양(Sawtooth)을 그리는 경우가 많습니다.)
Slow Start (SS): 출발 시 가속 - "일단 밟아봐! 최대 속도 찾아보자!"
연결 초기나 심각한 혼잡(타임아웃)으로 재시작할 때는, cwnd
를 아주 작은 값(예: 1~10 MSS)에서 시작해서, 지수적으로(2배씩 아주 빠르게) 늘려나갑니다. "이 도로에서 최대로 낼 수 있는 속도가 얼마인지" 가능한 빠르게 파악하려는 전략이죠. (이름은 '느린' 시작이지만, 실제로는 cwnd
증가 속도가 가장 빠릅니다!)
미리 정해둔 문턱값(Slow Start Threshold, ssthresh
)에 도달하거나, 또는 혼잡(패킷 손실)이 감지되면 이 급가속 단계를 멈춥니다.
Congestion Avoidance (CA): 안정 속도 유지 - "이제부턴 도로 상황 봐가면서 살살~"
cwnd
가 ssthresh
문턱값 이상으로 커지면, 더 이상 급가속(지수적 증가)은 위험하다고 판단하고, 선형적으로(AIMD의 AI처럼 조금씩) 증가시킵니다. "이미 속도가 꽤 붙었으니, 이제부턴 주변 차량(다른 트래픽) 눈치 보면서 조심스럽게 가자"는 거죠.
만약 이 단계에서 심각한 혼잡(타임아웃)이 발생하면? "아이고, 크게 사고 났네!" 하면서 cwnd
를 거의 1로 리셋하고 ssthresh
도 확 낮춘 뒤, 다시 처음 'Slow Start'부터 시작합니다. 😭
만약 비교적 덜 심각한 혼잡('3 중복 ACK')이 발생하면? 아래 'Fast Recovery' 단계로 넘어가 좀 더 부드럽게 대처합니다.
Fast Recovery (FR): 가벼운 접촉 사고 처리 - "살짝 긁혔네? 너무 쫄진 말자!"
'3 중복 ACK'로 혼잡이 감지되었을 때 (즉, 'Fast Retransmit'이 발동되었을 때) 함께 사용되는 전략입니다. 타임아웃보다는 덜 심각한, 일시적인 혼잡일 가능성이 높다고 보는 거죠.
cwnd
를 절반으로 줄이고(MD) ssthresh
도 이 값으로 설정하는 것은 비슷하지만, cwnd
를 1까지 확 줄이지는 않습니다. 그리고 'Slow Start' 단계를 건너뛰고 바로 'Congestion Avoidance'(선형 증가) 단계로 진입합니다. 전송률 하락 폭을 줄여서 좀 더 빠르게 정상 속도로 회복하려는 시도입니다.
ECN은 라우터 같은 중간 장비가 "어? 길이 좀 막히기 시작하는데?" 하고 실제 패킷 손실이 발생하기 전에 미리 혼잡 조짐을 알려주는 아주 스마트한 기능입니다.
ECN을 지원하는 라우터는 혼잡이 예상되면 지나가는 IP 패킷 헤더에 살짝 표시(마킹)를 해둡니다.
이 표시를 본 수신 호스트는 응답 ACK의 TCP 헤더에 "라우터가 곧 막힐 것 같다고 경고하더라!" 라는 ECN 플래그를 설정해서 송신 호스트에게 알려줍니다.
송신 호스트는 이 경고 신호를 받으면, 실제 패킷 손실(사고)이 없었더라도 미리 cwnd
를 줄여서 선제적으로 혼잡을 완화시킵니다.
ECN은 불필요한 패킷 손실과 지연을 줄여 네트워크 효율성을 높일 수 있는 좋은 기술이지만, 안타깝게도 송신/수신 호스트 및 중간 라우터 모두가 이 기능을 지원해야만 동작하는 선택적 기능이라 아직 보편적으로 활성화되어 있지는 않습니다.
오늘 살펴본 것처럼, TCP가 우리에게 주는 '믿음직함'은 결코 공짜가 아닙니다. 사라진 패킷을 끈질기게 찾아내고(오류 제어), 상대방의 처리 속도를 세심하게 배려하며(흐름 제어), 나아가 네트워크 전체의 안녕까지 생각하는(혼잡 제어) 정교하고 지능적인 메커니즘들이 실시간으로 서로 영향을 주고받으며 만들어내는 환상적인 협업의 결과입니다.
TCP의 이런 눈물겨운 노력 덕분에 우리는 마치 아무런 문제 없는 것처럼 부드럽게 웹 서핑을 하고, 끊김 걱정 없이 영상을 보고, 대용량 파일을 안정적으로 주고받을 수 있는 것이죠. 개발자로서 우리가 만드는 서비스의 안정적인 통신 뒤에는 이런 네트워크 프로토콜의 깊은 고민과 지혜가 숨어 있다는 사실! TCP의 속사정을 이해하는 것은 분명 더 견고하고 성능 좋은 애플리케이션을 만드는 데 훌륭한 밑거름이 될 것입니다. 😊