1. 라이브러리
  2. TCP와 UDP
  3. TCP 심층 분석

업데이트됨 1개월 전

보내는 패킷은 사라질 수도 있습니다. 손상된 채로, 순서 없이, 두 번이나, 혹은 아예 도착하지 않을 수도 있습니다. 인터넷은 아무것도 보장하지 않습니다.

TCP는 그럼에도 불구하고 보장합니다.

웹 페이지를 불러오거나, 이메일을 보내거나, 파일을 내려받을 때, TCP는 아무것도 보장하지 않는 네트워크를 가로질러서도 데이터가 손상 없이, 순서대로, 완전하게 도착하도록 보증합니다. 어떻게 신뢰할 수 없는 것에서 신뢰를 만들 수 있을까요? 검증을 통해서입니다.

3-웨이 핸드셰이크: 서로 들을 수 있음을 증명하기

TCP가 데이터를 단 한 바이트도 보내기 전에, 근본적인 문제를 해결합니다: 두 기계는 실제로 통신할 수 있다는 것을 어떻게 알 수 있을까요?

인터넷은 전화 통화가 아닙니다. 연결 신호도 없습니다. 패킷을 보낼 때, 그것이 도착했는지 알 방법이 없습니다. 상대 기계는 패킷이 도착하기 전까지—도착한다면—당신이 존재한다는 것조차 모릅니다. 그래서 TCP는 상호 증명의 의식으로 시작합니다.

클라이언트가 SYN(동기화) 패킷을 보냅니다: "나 여기 있어. 대화하고 싶어. 우리 대화를 추적하는 데 쓸 임의의 숫자야."

서버가 SYN-ACK로 응답합니다: "들렸어. 나도 여기 있어. 내 임의의 숫자야. 네 패킷을 실제로 읽었다는 것을 증명하기 위해 네 숫자에 1을 더했어."

클라이언트가 ACK를 보냅니다: "네가 날 들었다는 걸 들었어. 네 숫자에 1을 더한 거야. 이제 동기화됐어."

세 개의 패킷. 신뢰할 수 없는 네트워크를 가로지른 두 기계가 서로의 말을 들을 수 있음을 증명했습니다. 이제 통신할 수 있습니다.

임의의 시작 번호(초기 순서 번호, ISN)는 미묘한 재앙을 막습니다: 오래되어 끊어진 연결의 패킷이 새 데이터로 오인되는 것입니다. 항상 0에서 시작한다면, 이전 연결에서 지연된 패킷이 유효한 것처럼 보일 수 있습니다. 임의의 번호는 그런 충돌이 생길 가능성을 천문학적으로 낮춥니다.

데이터 전송: 모든 바이트에 번호 붙이기

핸드셰이크가 완료되면, TCP는 핵심 과제에 직면합니다: 신뢰할 수 없는 네트워크에서 안정적으로 데이터를 전송하는 것입니다.

해결책은 집요한 장부 기록입니다. TCP는 모든 바이트에—모든 패킷이 아니라, 모든 바이트에—순서 번호를 할당합니다. 순서 번호 5000에서 시작하는 1000바이트 세그먼트를 보낸다면, 바이트는 5000부터 5999까지 번호가 붙습니다. 다음 세그먼트는 6000에서 시작합니다.

왜 패킷 대신 바이트일까요? 패킷은 크기가 제각각이고, 분할되거나, 합쳐지거나, 다른 조각으로 재전송될 수 있습니다. 바이트에 번호를 붙임으로써, TCP는 데이터가 어떻게 도착하든 올바르게 재조립할 수 있습니다. 5000-5999와 7000-7999바이트는 도착했지만 6000-6999가 없다면, 수신자는 정확히 무엇이 빠져 있는지 압니다.

수신자는 ACK 패킷으로 수신을 확인합니다: "N바이트까지 모두 받았습니다. 다음은 N바이트를 보내주세요." 이것이 누적 확인입니다—하나의 ACK가 그 이전의 모든 데이터 수신을 확인합니다.

  • 송신자: "5000-5999바이트입니다"
  • 송신자: "6000-6999바이트입니다"
  • 송신자: "7000-7999바이트입니다"
  • 수신자: "8000까지 모두 받았습니다"

송신자는 더 보내기 전에 각 ACK를 기다리지 않습니다. 미확인 데이터의 윈도우를 전송 중인 상태로 유지하며, 처리량을 최대화하는 동시에 모든 바이트를 추적하고, 확인되지 않은 것은 무엇이든 재전송할 준비를 합니다.

문제가 생겼을 때: 재전송

패킷은 사라집니다. 언제 일어나느냐의 문제이지, 일어나느냐의 문제가 아닙니다. TCP의 재전송 메커니즘은 이 필연적인 현상을 해결 가능한 문제로 바꿉니다.

모든 세그먼트는 내용의 수학적 지문인 체크섬을 가집니다. 수신자가 이 지문을 재계산합니다. 일치하지 않으면, 패킷이 전송 중에 손상된 것입니다. TCP는 조용히 그것을 버립니다. 확인 없음. 송신자에게 침묵은 "다시 시도하라"는 의미입니다.

송신자는 미확인 세그먼트마다 타이머를 유지합니다. ACK가 도착하기 전에 타이머가 만료되면, TCP는 재전송합니다. 타임아웃은 고정이 아닙니다—TCP는 왕복 시간(RTT)을 지속적으로 측정하고 조정합니다. 같은 방 안의 연결은 대양을 가로지른 연결보다 짧은 타임아웃이 필요합니다.

하지만 타임아웃을 기다리는 것은 느립니다. TCP에는 더 빠른 메커니즘이 있습니다: 수신자가 순서 없이 데이터를 받으면, 무언가 잘못된 것입니다. 5000-5999바이트가 도착하고, 그다음 7000-7999, 그다음 8000-8999가 도착한다면—6000-6999는 어디에 있을까요?

수신자는 빠진 구간을 건너뛰어 확인할 수 없으므로, 계속해서 "6000이 필요합니다"를 반복합니다. 송신자가 같은 바이트를 요청하는 중복 ACK를 세 번 받으면, 타임아웃을 기다리지 않습니다. 즉시 누락된 세그먼트를 재전송합니다. 이 "빠른 재전송"은 손실된 패킷을 초 단위가 아닌 밀리초 단위로 복구합니다.

선택적 확인 응답(SACK)은 이를 더 확장합니다. "6000이 필요합니다"라고만 하는 대신, 수신자는 "6000이 필요하지만, 7000-8999는 이미 받았습니다"라고 말할 수 있습니다. 송신자는 정확히 무엇이 빠져 있는지 알고 그것만 재전송합니다.

흐름 제어: 수신자에게 과부하 주지 않기

신뢰성은 네트워크에 관한 것만이 아닙니다. 송신자가 수신자가 처리할 수 있는 것보다 빠르게 전송한다면 어떻게 될까요?

수신자는 메모리가 제한된 스마트폰, 수천 개의 연결을 처리하는 서버, 또는 응용 프로그램이 다른 작업으로 바쁜 기계일 수 있습니다. 데이터가 소비할 수 있는 것보다 빠르게 도착하면, 수신 버퍼가 가득 찹니다. 새 패킷이 버려집니다. 재전송도 다시 버려집니다. 연결이 멈춥니다.

TCP는 수신 윈도우로 이것을 해결합니다—수신자가 모든 ACK에 포함하는 숫자: "N바이트 더 받을 공간이 있습니다. 그것보다 많이 보내지 마세요."

윈도우는 실제 버퍼 공간을 반영합니다. 응용 프로그램이 데이터를 읽으면 윈도우가 커집니다. 응용 프로그램이 뒤처지면 윈도우가 줄어듭니다. 버퍼가 완전히 가득 차면, 윈도우가 0으로 떨어집니다: "멈추세요. 더 이상 받을 수 없습니다."

송신자는 이 제한을 절대적으로 존중합니다. 윈도우가 0이면, 데이터 전송을 멈추지만 주기적으로 작은 "윈도우 탐색" 패킷을 보냅니다—공간이 생겼는지 확인하는 것입니다. 이 탐색이 없으면, 손실된 윈도우 업데이트가 연결을 영원히 교착 상태로 만들 수 있습니다.

혼잡 제어: 네트워크에 과부하 주지 않기

흐름 제어는 수신자를 보호합니다. 혼잡 제어는 모두를 보호합니다.

송신자와 수신자 사이의 네트워크는 용량이 제한되어 있습니다. 라우터는 초당 일정 수의 패킷만 전달할 수 있습니다. 송신자들이 집합적으로 네트워크가 처리할 수 있는 것보다 많이 전송하면, 패킷이 라우터 버퍼에 쌓입니다. 결국 버퍼가 넘칩니다. 패킷이 버려집니다. 재전송이 혼잡을 더 악화시킵니다. 네트워크가 붕괴합니다.

TCP는 네트워크의 용량을 직접 볼 수 없습니다. 동작을 보고 추론합니다. 핵심 통찰: 패킷 손실은 혼잡을 의미합니다. 패킷이 버려지고 있다면, 너무 빠르게 보내고 있는 것입니다.

송신자는 혼잡 윈도우를 유지합니다—네트워크가 처리할 수 있는 데이터 양에 대한 추정치. 실제 전송 한도는 혼잡 윈도우와 수신 윈도우 중 더 작은 값입니다.

새 연결은 보수적으로 시작합니다. 혼잡 윈도우는 작게 시작합니다—하나 또는 두 세그먼트. ACK가 성공적으로 도착할수록, 윈도우가 지수적으로 커집니다("슬로우 스타트"). 하나의 세그먼트가 둘이 되고, 둘이 넷이 되고, 넷이 여덟이 됩니다. 공격적으로 들릴 수 있지만, 실제로는 신중한 접근입니다: 0에서부터 네트워크의 용량을 탐색하는 것이니까요.

윈도우가 임계값에 도달하거나 패킷 손실이 발생하면, TCP는 "혼잡 회피" 모드로 전환합니다—선형 성장. 왕복 시간(RTT)당 세그먼트 하나를 추가합니다. 신중하고 측정된 증가입니다.

손실이 발생하면, TCP는 크게 후퇴합니다. 타임아웃은 혼잡 윈도우를 최솟값으로 재설정하고 슬로우 스타트를 처음부터 다시 시작합니다. 세 개의 중복 ACK는 윈도우를 절반으로 줄이지만 슬로우 스타트는 건너뜁니다—이것이 "빠른 복구"입니다.

이 과정—위로 탐색하고, 손실이 생기면 후퇴하고, 다시 탐색하는—은 네트워크의 용량을 찾아내고 조건이 변해도 계속 추적합니다. CUBIC과 BBR 같은 현대적인 알고리즘은 더 정교한 모델을 사용하지만, 핵심 원칙은 같습니다: 패킷 손실은 "속도를 줄이라"는 신호입니다.

연결 종료: 우아하게 마무리하기

TCP 연결은 시작만큼 신중하게 끝납니다. 어느 쪽이든 FIN(종료) 패킷을 보내 종료를 시작할 수 있습니다: "더 이상 보낼 데이터가 없습니다."

상대방이 FIN을 확인하지만, 전송이 완료되지 않았을 수 있습니다. TCP는 한쪽 방향만 닫힌 연결을 지원합니다—한 방향은 종료됐지만, 반대 방향은 여전히 활성 상태인 것입니다. HTTP 요청은 이미 보냈지만, 아직 응답을 기다리고 있는 상황이 바로 이것입니다.

양쪽이 모두 FIN을 보내고 확인했으면, 연결이 완전히 닫힙니다. 하지만 마지막 보호막이 있습니다: 마지막 ACK를 보낸 쪽은 몇 분 동안 TIME_WAIT 상태에 들어갑니다. 그 마지막 ACK가 손실되었을 수 있기 때문입니다. 상대방이 FIN을 재전송하면, 누군가 그것을 확인해야 합니다.

긴급 상황을 위해, TCP는 RST(재설정) 패킷을 통한 강제 종료를 지원합니다. 협상도, 대기도 없이—연결이 즉시 끊깁니다.

신뢰의 프로토콜

TCP는 네트워크를 신뢰하지 않습니다. 프로토콜을 신뢰합니다.

모든 메커니즘은 검증을 위한 것입니다. 핸드셰이크는 상호 도달 가능성을 증명합니다. 순서 번호는 순서를 증명합니다. 확인 응답은 수신을 증명합니다. 체크섬은 무결성을 증명합니다. 재전송은 지속성을 증명합니다. 흐름 제어는 수신자가 준비되어 있음을 증명합니다. 혼잡 제어는 네트워크가 부하를 감당할 수 있음을 증명합니다.

이것이 TCP가 인터넷의 대부분을 지탱하는 이유입니다. 빠르기 때문이 아닙니다—UDP가 더 빠릅니다. 단순하기 때문이 아닙니다—UDP가 더 단순합니다. 데이터가 손상 없이, 순서대로, 누구에게도 과부하를 주지 않고 도착했다는 것을 알아야 할 때, TCP는 네트워크가 아무것도 보장하지 않는 곳에서 수학적 확실성을 제공하기 때문입니다.

복잡성이 핵심입니다. 신뢰성은 바라는 것에서 오지 않습니다. 검증과 확인 응답, 그리고 일이 잘못되었을 때 다시 시도하려는 의지에서 옵니다.

TCP에 관한 자주 묻는 질문

TCP는 왜 2-웨이가 아닌 3-웨이 핸드셰이크를 사용하나요?

두 메시지만으로는 단방향 통신만 증명됩니다: 클라이언트가 서버에 도달할 수 있다는 것. 하지만 TCP는 양방향 통신이 가능하다는 증명이 필요합니다. 세 번째 메시지는 서버의 응답이 클라이언트에게 돌아왔다는 것을 증명합니다. 없으면, 서버는 양방향으로 작동하지 않을 수 있는 연결에 자원을 할당하게 됩니다. 세 개의 패킷은 상호 도달 가능성을 증명하는 최소한의 교환입니다.

ACK가 손실되면 어떻게 되나요?

송신자의 재전송 타이머가 만료되고 데이터를 다시 보냅니다. 재전송된 데이터가 도착하면, 수신자가 다시 ACK를 보냅니다. 중복 데이터는 무해합니다—순서 번호 덕분에 수신자가 이를 인식하고 버릴 수 있습니다. 시스템은 스스로 복구합니다: 손실된 ACK가 재전송을 유발하고, 재전송이 새 ACK를 유발합니다.

TCP는 왜 패킷 손실을 혼잡의 신호로 해석하나요?

대부분의 네트워크에서, 패킷 손실은 라우터 버퍼가 넘치기 때문에 발생합니다—너무 많은 데이터가 전송되고 있는 것입니다. 다른 이유로 손실이 발생할 수도 있지만(예: 무선 간섭), 유선 네트워크에서는 혼잡이 지배적인 원인입니다. TCP의 보수적인 반응이 때로는 불필요하게 속도를 줄일 수 있지만, 훨씬 더 심각한 혼잡 붕괴를 막습니다.

TCP는 순서 없이 도착하는 패킷을 어떻게 처리하나요?

수신자는 순서 번호를 사용하여 도착 순서에 관계없이 데이터를 올바르게 재조립합니다. 순서가 뒤바뀐 패킷은 빠진 세그먼트가 도착할 때까지 버퍼에 보관됩니다. 수신자는 마지막으로 연속 수신된 바이트에 대한 중복 ACK를 보내 빠진 구간을 알리고, 빠른 재전송을 유발합니다. SACK가 활성화되면, 수신자는 어떤 세그먼트가 도착했는지 정확히 알려줘서 복구를 더욱 효율적으로 만듭니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃
TCP의 작동 원리: 완전한 개요 • 라이브러리 • Connected