데이터는 자주 사라지고, 망가지고, 순서가 뒤바뀐 채 도착한다. 그런데도 우리가 받은 파일은 늘 완전하다. 그 마법을 만드는 규약, 전송 제어 규약(TCP, Transmission Control Protocol)의 속을 들여다본다.
인터넷으로 사진 한 장을 내려받을 때, 우리는 그것이 한 픽셀도 어긋나지 않고 보낸 순서 그대로 도착하리라 의심 없이 믿는다. 그러나 그 믿음을 떠받치는 실제 통로는 조금도 믿을 만한 곳이 아니다. 중간 어딘가에서 데이터 조각이 통째로 사라지기도 하고, 전기적 잡음에 한두 비트가 뒤집히기도 하며, 먼저 보낸 조각이 나중에 보낸 조각보다 늦게 도착하기도 한다.
그렇다면 어떻게 우리는 늘 멀쩡한 파일을 받을 수 있을까. 그 간극을 메우는 것이 바로 전송 제어 규약, 줄여서 TCP다. 이 글은 TCP가 '믿을 수 없는 통로' 위에서 어떻게 '믿을 수 있는 통신'이라는 착시를 만들어 내는지를, 전공 지식 없이도 따라올 수 있도록 풀어 쓴 것이다.
네트워크를 설명할 때 흔히 쓰는 그림이 있다. 한쪽에 내 노트북이 있고, 반대쪽에 멀리 떨어진 서버가 있으며, 그 사이의 인터넷은 속을 알 수 없는 '구름'으로 그린다. 이 구름 속에서 무슨 일이 벌어지는지 우리는 정확히 알 수 없다. 옛 지도 가장자리에 "여기에 괴물이 산다"고 적어 두던 것처럼, 데이터에게 인터넷이라는 구름은 결코 안전한 곳이 아니다.
통신은 여러 겹의 층(layer)으로 나뉘어 있다. 가장 위에 우리가 쓰는 응용 프로그램(웹 브라우저, 메신저 등)이 있고, 그 아래에 데이터를 실제로 실어 나르는 일을 맡는 전송 계층이 있으며, 다시 그 아래에 패킷이 어느 길로 갈지 정하는 망 계층(network layer)과 물리적 전송을 담당하는 계층이 차례로 놓인다. 응용 프로그램이 보낸 데이터는 이 계단을 한 칸씩 내려가 인터넷으로 흘러 들어가고, 받는 쪽에서는 반대로 한 칸씩 올라간다.
전송 계층에는 성격이 다른 두 일꾼이 있다. 하나는 사용자 데이터그램 규약(UDP, User Datagram Protocol)으로, "일단 던지고 책임지지 않는" 단순한 방식이다. 빠르지만, 데이터가 사라져도 알려 주지 않는다. 다른 하나가 오늘의 주인공인 TCP다. TCP는 느려도 좋으니 한 조각도 잃지 않고, 망가뜨리지 않고, 순서대로 전달하겠다고 약속한다. 전송 계층의 규약 가운데 가장 복잡하고, 또 그만큼 가장 널리 쓰이는 것이 TCP다.
TCP를 한 문장으로 요약하면 이렇다. 밑에 깔린 통로가 아무리 엉망이어도, 위에서 보기에는 완벽한 통로인 것처럼 흉내 낸다. 응용 프로그램이 바라는 것은 단순하다. 한쪽에서 데이터(바이트)를 부어 넣으면, 반대쪽에서 똑같은 데이터가 똑같은 순서로 흘러나오는 튼튼한 파이프다.
그러나 그 파이프 아래에 실제로 깔린 것은 곳곳에서 물이 새는 낡은 관이다. 데이터가 새어 사라지고, 녹물이 섞여 들어가 내용이 망가지고, 갈림길에서 뒤처진 조각이 늦게 도착해 순서가 뒤바뀐다. 양쪽 끝에 앉은 TCP의 임무는, 이 새는 파이프가 마치 존재하지 않는 것처럼, 위쪽 응용 프로그램에게는 매끄러운 파이프 하나만 보이도록 속이는 것이다. 중요한 점은 TCP가 통신의 양 끝 장치에만 존재한다는 사실이다. 중간의 라우터들은 TCP를 알 필요가 없다. 신뢰성은 오직 두 끝에서 만들어진다.
비유 · 찢어지는 봉투로 편지 보내기
긴 편지를 여러 장으로 나눠, 봉투마다 한 장씩 넣어 부친다고 하자. 그런데 이 우편망은 봉투를 가끔 잃어버리고, 빗물에 글씨가 번지게 하고, 나중에 부친 봉투를 먼저 배달하기도 한다. 받는 사람이 완전한 편지를 읽으려면 어떻게 해야 할까. 모든 봉투에 쪽 번호를 매기고, 받을 때마다 "몇 번까지 잘 받았다"는 답장을 보내며, 빠진 쪽이 있으면 다시 보내 달라고 요청하면 된다. TCP가 하는 일이 정확히 이것이다.
이 일을 해내기 위해 TCP는 두 가지 장치를 갖춘다. 하나는 지금까지 무엇을 주고받았는지 양쪽 끝에서 추적하는 상태 기계(state machine)이고, 다른 하나는 모든 데이터 조각에 붙이는 머리표(header)다. 표준 문서를 끝까지 파고들면 그 양이 방대하다. TCP의 원래 규격인 RFC 793은 1981년에 나왔고, 2022년 RFC 9293이 그동안 흩어져 있던 수정 사항을 한데 모아 이를 대체했다. 여기서는 그 가운데 핵심만, 두 가지 큰 줄기로 좁혀 본다. 하나는 연결을 만드는 법이고, 다른 하나는 신뢰성을 얻는 법이다.
응용 프로그램이 보내려는 것은 끊김 없는 바이트의 흐름이다. TCP는 이 흐름을 작은 조각으로 잘라 내고, 각 조각 앞에 약속된 정보 묶음을 붙인다. 이 묶음을 머리표, 곧 헤더라고 부른다. 헤더는 최소 20바이트로, 데이터를 제대로 배달하는 데 필요한 정보를 빼곡히 담는다.
헤더의 항목을 모두 알 필요는 없다. 다만 다음 셋은 이 글 내내 등장하므로 기억해 둘 만하다.
헤더에는 이 밖에도 데이터가 오는 도중 망가졌는지 검사하는 검사합(checksum)이 들어 있다. 받는 쪽은 검사합을 다시 계산해 보고 값이 어긋나면 그 조각이 손상되었음을 알아챈다.
TCP는 "지금까지 어디까지 진행됐는가"를 끊임없이 추적해야 한다. 그래서 통신이 이어지는 동안 양쪽 컴퓨터의 메모리에 자리 잡고 있을 구조물이 필요하다. 이 구조물을 소켓(socket)이라 부른다. 보내는 쪽에 하나, 받는 쪽에 하나, 두 소켓을 짝지어 연결하면 응용 프로그램은 마치 직통 전화선을 가진 것처럼 데이터를 주고받을 수 있다. 한쪽 소켓에 바이트를 넣으면, 반대쪽 소켓에서 똑같은 바이트가 같은 순서로, 빠짐없이 흘러나온다.
그렇다면 두 컴퓨터는 어떻게 이 소켓 한 쌍을 맺기로 합의할까. 여기서 TCP의 첫 번째 의식, 세 번의 악수가 등장한다.
TCP가 연결을 맺는 절차를 3방향 악수(three-way handshake)라고 한다. 이름 그대로 메시지가 세 번 오간다. 두 번으로는 부족하다. 그 까닭은 곧 분명해진다.
비유 · 전화 통화의 시작
전화를 걸어 "여보세요"라고 한다(①). 상대가 "네, 들립니다"라고 답한다(②). 여기서 그치면 거는 쪽은 상대가 들리는 걸 알지만, 받는 쪽은 자기 목소리가 상대에게 닿는지 아직 모른다. 그래서 거는 쪽이 한 번 더 "네, 저도 들려요"라고 화답해야(③) 비로소 둘 다 "양방향으로 잘 통한다"는 사실을 확신한다. 두 번이 아니라 세 번이 필요한 이유다.
소켓이 맺어졌으니, 이제 진짜 문제로 들어간다. 조각이 통째로 사라질 수 있다면 어떻게 알아챌까. 답은 간단하다. 모든 조각에 일련 번호를 매기고, 받은 쪽이 "어디까지 받았다"고 답장(확인 응답)을 보내면 된다.
여기에 두 가지 미묘한 점이 있다. 첫째, 시퀀스 번호는 사실 조각 단위가 아니라 바이트 단위로 매겨진다. 즉 "이 조각은 전체 흐름에서 몇 번째 바이트부터 시작한다"를 가리킨다. 둘째, 그 번호는 1이나 0에서 시작하지 않는다. 연결을 맺을 때마다 예측 불가능한 무작위 값에서 출발한다. 이는 보안과 직결되는데, 그 이유는 뒤에서 따로 다룬다. 설명의 편의를 위해 이 글에서는 조각에 1, 2, 3처럼 간단한 순번을 붙여 부르겠다.
받는 쪽이 보내는 확인 응답 번호는 "이미 받은 번호"가 아니라 "다음에 받고 싶은 번호"를 가리킨다. 예컨대 4, 5, 6번을 잘 받았다면 "7번을 달라"는 뜻으로 응답한다. 이 작은 차이가 손실 처리의 열쇠가 된다.
1, 2, 3번 조각을 차례로 보냈는데, 가는 길에 2번이 사라졌다고 하자. 라우터가 너무 붐벼 버려졌을 수도, 손상되어 폐기되었을 수도 있다. 받는 쪽은 1번을 받고 "2번을 달라"고 응답한다. 그런데 곧이어 3번이 도착한다. 받는 쪽은 3번을 버리지는 않되, 응용 프로그램에 넘기지도 못한다. 넘기면 순서가 어긋나기 때문이다. 그래서 3번을 잠시 보관하고, 다시 "2번을 달라"고 같은 응답을 보낸다. 즉 "뭔가 받긴 했지만, 내가 정작 기다리는 건 그게 아니다"라는 신호다.
보내는 쪽에도 안전장치가 있다. "2번에 대한 응답을 받고 싶었는데 오지 않는다"면, 일정 시간을 기다린 뒤 그래도 소식이 없으면 2번을 사라진 것으로 간주하고 다시 보낸다. 이를 재전송 시간 초과(RTO, Retransmission Timeout)라 한다.
그런데 시간 초과만 기다리면 너무 느리다. 다행히 더 빠른 단서가 있다. 같은 확인 응답이 거듭 도착하는 것이다. "2번을 달라"는 응답이 한 번, 두 번, 세 번 반복되면, 셜록 홈스가 아니어도 무언가 잘못되었음을 알 수 있다. 표준에서는 처음 응답에 더해 같은 응답이 세 번 더 쌓이는 시점, 곧 중복 확인 응답 3회를 손실의 강력한 증거로 본다. 이때 보내는 쪽은 시간 초과를 기다리지 않고 곧장 잃어버린 조각을 다시 보낸다. 이것이 빠른 재전송(fast retransmit)이다.
왜 하필 한 번이 아니라 세 번일까. 조각이 단지 순서만 뒤바뀌어 도착하는 경우에도 중복 응답이 한두 번은 나올 수 있기 때문이다. 한두 번으로 섣불리 재전송하면, 멀쩡히 가고 있는 조각을 괜히 또 보내는 낭비가 생긴다. 세 번이라는 문턱은 "단순한 순서 뒤바뀜"과 "진짜 손실"을 가르는 경험적 경계선이다.
다시 보낸 2번이 마침내 도착하면, 받는 쪽은 보관해 두었던 3번과 합쳐 순서를 맞춘다. 이제는 "2번을 달라"가 아니라 "4번을 달라"고 응답할 수 있다. 2번과 3번을 모두 확보했다는 뜻이다. 이렇게 손실·중복·순서 뒤바뀜이 모두 같은 도구, 곧 시퀀스 번호와 확인 응답으로 해결된다.
비유 · 쪽 번호가 매겨진 원고
편집자가 작가에게서 원고를 받는다. 모든 장에 쪽 번호가 있어, 2쪽이 빠지면 3쪽·4쪽이 와도 편집자는 "아직 2쪽이 없다"고 거듭 알린다. 빠진 2쪽이 다시 도착하면 비로소 "여기까지 다 받았다"고 확인한다. TCP의 받는 쪽이 하는 일이 바로 이 편집자의 일이다.
지금까지의 방식에는 한 가지 큰 약점이 있다. 너무 느리다는 것이다. 한 조각을 보내고, 그에 대한 응답을 받고, 다시 다음 조각을 보내고… 이렇게 한 번에 하나씩 주고받으면 통신이 답답할 만큼 느려진다.
비유 · 한 단어마다 "응?"
대화 중에 내가 한 단어를 말할 때마다 상대에게 "잘 들었다"는 확답을 받아야 다음 단어를 꺼낼 수 있다면 어떨까. "오늘"… "응" … "우리"… "응" … "회의"… "응". 대화는 견디기 힘들 만큼 느려진다. 그래서 우리는 여러 단어를 한 번에 말하고, 상대도 한 번에 끄덕인다.
TCP도 마찬가지다. 응답을 받기 전에 여러 조각을 한꺼번에 내보낸다. 이렇게 한 번에 띄워 보내는 조각 묶음을 흔히 플라이트(flight, 비행 중인 조각들)라 부른다. 그렇다면 응답을 기다리지 않고 미리 몇 개까지 보내도 될까. 너무 적게 보내면 느리고, 너무 많이 보내면 중간 라우터가 감당하지 못해 오히려 손실이 늘어난다.
이 "한 번에 보낼 수 있는 양"을 정하는 것이 윈도우(window)다. 헤더에는 윈도우 크기를 알리는 항목이 있다. TCP는 처음에는 조심스럽게 적은 양으로 시작해, 손실 없이 잘 도착하면 양을 점점 늘려 가며 네트워크가 얼마나 받아 줄 수 있는지 더듬어 본다. 그러다 손실이 감지되면 양을 줄여 혼잡을 누그러뜨린다. 이 일련의 규칙을 혼잡 제어(congestion control)라 한다. 앞서 본 빠른 재전송은 이 혼잡 제어와 맞물려, 손실 직후 통신을 처음부터 다시 시작하는 대신 절반쯤의 속도에서 부드럽게 이어 가도록 돕는다.
아주 단순해 보이는 절차에도 공격의 틈은 생긴다. 앞서 본 세 번의 악수가 그렇다. 첫 인사인 SYN 조각은 데이터를 거의 담지 않은, 머리표만 있는 아주 작은 조각이다. 그런데 서버는 이 작은 인사를 받으면 "연결을 맺으려는 모양이군" 하고 소켓을 위한 메모리를 미리 떼어 두고, SYN-ACK로 답한다. 그러고는 상대의 마지막 ACK가 오기를 기다린다.
공격자는 바로 이 점을 노린다. 마지막 ACK를 보내지 않은 채, 작은 SYN 조각만 끝없이 쏟아붓는 것이다. 서버는 매 SYN마다 자원을 조금씩 떼어 두고 오지 않을 ACK를 기다린다. 이렇게 완성되지 못한 연결을 반쪽 열린 연결(half-open connection)이라 한다. 한 대가 아니라 수백, 수천 대의 좀비 컴퓨터를 동원해 이 작은 조각을 퍼부으면, 서버의 자원은 야금야금 바닥나고 결국 정상 이용자를 받아 줄 여력이 사라진다. 이것이 SYN 폭주 공격(SYN flood)이며, 서비스 거부(DoS, Denial of Service) 공격의 고전적 형태다. 1996년에 공개적으로 알려진 뒤로 지금까지도 쓰이는 수법이다.
막는 법 · 자원을 미리 떼어 주지 않기
대표적 방어가 SYN 쿠키(SYN cookie)다. 서버가 SYN을 받아도 메모리를 떼어 두지 않고, 연결 정보를 SYN-ACK의 시퀀스 번호 안에 암호화해 실어 보낸다. 진짜 이용자가 마지막 ACK로 그 값을 돌려보내면, 그때 비로소 연결 정보를 복원해 자원을 배정한다. 이 밖에도 대기열을 키우거나, 요청 속도를 제한하거나, 가장 오래된 반쪽 연결을 밀어내는 방법이 함께 쓰인다. 단순한 인사 절차 하나에도 이런 공격과 방어의 줄다리기가 숨어 있다는 점이, 인터넷이라는 환경의 성격을 잘 보여 준다.
앞서 시퀀스 번호가 1이나 0이 아니라 예측 불가능한 무작위 값에서 출발한다고 했다. 이 초기 시퀀스 번호(ISN, Initial Sequence Number)의 무작위성은 단순한 기술적 취향이 아니라 보안의 문제다.
만약 연결의 시작 번호가 예측 가능하다면, 통신 경로 바깥에 있는 공격자가 그 번호를 알아맞혀, 신뢰받는 상대인 척 위조한 조각을 끼워 넣을 수 있다. 받는 쪽은 시퀀스 번호가 들어맞으면 정상 데이터로 받아들이기 때문이다. 실제로 1980년대 중반에 이런 약점이 지적되었고, 1990년대에는 시작 번호를 예측해 연결을 가로채는 공격이 시연되었다. 그래서 1996년 RFC 1948이 방어 표준을 제시했고, 2012년 RFC 6528이 이를 갱신해 정식 표준으로 끌어올렸다.
오늘날 운영체제는 시작 번호를 만들 때, 시간에 따라 변하는 값과 연결을 식별하는 정보(양쪽의 주소와 포트), 그리고 외부에서 알 수 없는 비밀 키를 한데 섞어 암호학적 함수에 넣는다. 그 결과 공격자는 다른 연결의 시작 번호를 사실상 추측할 수 없게 된다. 보이지 않는 작은 무작위성 하나가, 연결 전체의 안전을 떠받치는 셈이다.
대화를 시작할 때 인사가 필요했듯, 끝낼 때도 작별이 필요하다. 한쪽이 "이제 보낼 것이 없다"는 뜻의 FIN(finish, '끝') 깃발을 세운 조각을 보내고 상대가 이를 확인하면, 그 방향의 통로가 닫힌다. TCP의 연결은 양방향이므로, 반대 방향도 같은 방식으로 따로 닫아야 완전히 종료된다. 한쪽 방향씩 인사를 주고받으며 끝내는 이 절차 덕분에, 아직 길 위에 남은 데이터가 있더라도 황급히 끊기지 않고 마저 전달될 수 있다.
지금까지 본 장치들을 모으면 TCP의 골격이 드러난다. 머리표에 번호를 매겨 순서를 지키고, 확인 응답으로 손실을 알아채며, 시간 초과와 빠른 재전송으로 사라진 조각을 되살리고, 윈도우로 속도와 안정 사이의 균형을 잡는다. 여기에 시작 번호의 무작위화와 SYN 쿠키 같은 장치가 더해져 악의적 환경에서도 버틴다.
흥미로운 점은, 이 모든 일이 통신의 두 끝에서만 일어난다는 사실이다. 중간의 인터넷은 여전히 데이터를 잃고 망가뜨리고 뒤섞는 '믿을 수 없는 구름'으로 남아 있다. TCP는 그 구름을 고치려 들지 않는다. 다만 양 끝에서 부지런히 번호를 세고, 빠진 것을 다시 청하고, 순서를 맞추며, 위쪽 응용 프로그램에게는 완벽한 파이프 하나만 보이도록 끈질기게 흉내 낼 뿐이다. 우리가 매일 의심 없이 누리는 "완전하게 도착한 파일"이라는 경험은, 사실 이 끈질긴 흉내가 빚어낸 정교한 착시다.
이 글은 전송 제어 규약의 작동 원리를 비전공 독자를 위해 개념 중심으로 정리한 것이다. 시퀀스 번호의 구체적 산술, 혼잡 제어 알고리즘의 세부 변형, 옵션 필드의 다양한 확장 등은 단순화해 다루었다. 정확한 규격은 RFC 9293을 비롯한 표준 문서에 정의되어 있다.