메인 콘텐츠로 건너뛰기

인터넷이 멈추지 않는 진짜 이유, TCP를 요즘 말로 쉽게 풀어보기

요약

클립으로 정리됨 (생성형 AI 활용)

출처 및 참고 : https://cefboud.com/posts/tcp-deep-dive-internals/

인터넷은 이제 공기나 전기처럼, "없으면 안 되는 것"이 되어버렸습니다. 하지만 그 바닥을 들여다보면 꽤나 험난한 세계가 펼쳐져 있습니다. 패킷은 중간에 사라지고, 순서가 뒤죽박죽 섞이고, 데이터가 깨지기도 합니다.

그런데도 우리는 웹사이트를 열고, 메신저로 대화하고, 동영상을 보고, SSH로 서버에 접속하면서 거의 아무 문제도 느끼지 못합니다. 이 글에서는 그 마법의 중심에 있는 TCP가 무엇을, 어떻게 해주고 있는지, 최대한 쉽고 재밌게 살펴봅니다.

소켓 코드 예시와 함께,

  • 왜 IP만으로는 부족한지

  • TCP가 어떻게 "신뢰성"을 만들어내는지

  • 흐름 제어와 혼잡 제어가 왜 중요한지

  • 간단한 TCP/HTTP 서버가 실제로 어떻게 동작하는지

  • TCP 헤더의 핵심 필드가 어떤 역할을 하는지

를 하나씩 짚어볼게요.

IP만으로는 부족하다: TCP가 꼭 필요한 이유

네트워크를 크게 나누면 이렇게 내려갑니다.

물리 계층 → 데이터 링크 계층(Ethernet/Wi‑Fi 등) → 네트워크 계층(IP) → 전송 계층(TCP/UDP)

IP는 "어느 컴퓨터까지 갈지"를 책임지는 계층입니다. 즉, 건물 주소 같은 역할만 해 줍니다. 하지만 건물 앞에 도착한 택배는 이제 "몇 호에 사는 누구에게 전달해야 할지"를 알아야 하죠.

여기서 등장하는 것이 포트입니다.

  • IP 주소: 건물

  • 포트 번호: 건물 안의 호수

  • 프로세스(앱): 그 호수에 사는 사람

TCP는 바로 이 "포트" 개념을 통해, 도착한 데이터를 올바른 프로세스(웹 서버, 메일 서버, 게임 서버 등)로 안전하게 배달해 줍니다.

더 중요한 이유도 있습니다. 인터넷 중간에는 우리가 전혀 관리하지 못하는 라우터, 스위치, 각종 장비들이 있습니다. 이 장비들은 수시로 패킷을 떨어뜨리거나, 과부하 상태에 빠지기도 합니다. TCP는 이런 "중간 장비들은 최대한 단순하게 두고, 신뢰성은 끝단(사용자 기기)에서 해결하자"는 철학으로 설계되었습니다.

덕분에 우리는:

  • 데이터가 중간에서 사라져도 자동 재전송

  • 순서가 섞여도 다시 정렬

  • 중복 데이터는 걸러내고

  • 손상된 데이터는 체크섬으로 검출

같은 기능을 "앱 개발자가 직접 구현하지 않고도" 누릴 수 있습니다. 만약 이걸 매번 직접 구현해야 했다면, 세상 모든 개발자들이 레이아웃 맞추기 전에 네트워크 스택부터 만들고 있을지도 모릅니다.

결론은 단순합니다. "인터넷이 엉망진창이어도, 우리가 쓰는 앱은 멀쩡히 돌아가게 만드는 마법의 방패"가 바로 TCP입니다.

TCP 흐름 제어: 상대 버퍼를 꽉 채우지 않는 예의

조금만 상상해볼까요. 내 컴퓨터 A가 상대 컴퓨터 B로 대용량 파일(몇백 MB, 몇 GB)을 미친 속도로 전송한다고 해봅시다.

문제는 B에도 한계가 있다는 점입니다. B의 운영체제 커널에는 "수신 버퍼"라는 공간이 있고, 여기에 네트워크에서 들어온 데이터가 잠깐 저장됩니다. 그런데 이 버퍼 크기는 유한합니다. 대략 이런 식의 설정으로 관리됩니다.

  • 최소 크기: 4KB

  • 기본 크기: 128KB

  • 최대 크기: 수 MB 수준

만약 A가 "상대가 지금 얼마까지 받을 수 있는지"도 모르고 미친 듯이 보내기만 하면, B의 버퍼는 금세 꽉 차고, 그 이후로는 패킷이 버려지게 됩니다.

이를 막기 위해 TCP는 "흐름 제어(flow control)"라는 메커니즘을 사용합니다. TCP 세그먼트에는 "윈도우(window)"라는 필드가 있는데, 여기에는 "나 지금 이만큼 더 받을 수 있어요"라는 숫자가 들어갑니다.

즉, B는 A에게 이렇게 신호를 주는 셈입니다.

  • "지금 버퍼 여유가 64KB니까, 최대 64KB까지만 더 보내줘."

  • "버퍼가 꽉 찰 것 같으니 잠깐만, 창(window) 줄인다?"

  • "이제 좀 여유 생겼어, 더 많이 보내도 돼!"

이 덕분에 송신 측이 수신 측을 압도하지 않고, 둘이 호흡을 맞추면서 통신할 수 있게 됩니다. 상대의 속도를 고려해주는, 아주 TCP다운 배려입니다.

TCP 혼잡 제어: 인터넷 전체를 보호하는 매너 모드

흐름 제어가 "상대 컴퓨터"를 배려하는 기능이라면, 혼잡 제어(congestion control)는 "인터넷이라는 도로 전체"를 배려하는 기능입니다.

문제는 네트워크 경로의 모든 구간이 같은 속도를 가지지 않는다는 점입니다. 어디는 기가비트 광케이블, 어디는 아직도 느린 회선일 수 있습니다. 가장 느린 구간이 전체 속도를 결정합니다.

만약 전 세계의 컴퓨터가 눈치 없이 "있는 힘껏" 데이터를 마구 쏟아낸다면 어떤 일이 벌어질까요? 실제로 1986년에 이런 일이 벌어졌습니다. 인터넷 전체 대역폭이 몇십 KB/s 수준에서 심지어 40bps까지 떨어지는 "혼잡 붕괴(congestion collapse)"를 겪었죠.

패킷이 잔뜩 유실되면, 시스템들은 "아, 안 갔구나?" 하고 다시 보내는데, 이 재전송이 상황을 더 악화시킵니다. 이미 막힌 길에 더 많은 차를 쑤셔 넣는 셈이니까요.

이를 막기 위해 TCP는 다음과 같은 전략을 도입했습니다.

  • 처음에는 조심스럽게 조금씩 전송량을 늘리다가

  • 패킷 손실(=혼잡의 신호)이 보이면 "살짝 물러서서" 속도를 줄이고

  • 상황을 보면서 다시 천천히 올리는

일종의 "매너 모드 전송 알고리즘"을 쓰는 것입니다.

TCP의 혼잡 제어 덕분에, 인터넷은 스스로 "너무 막히지 않게" 조절하는 능력을 가지게 되었고, 오늘날 수많은 연결이 동시에 살아 움직일 수 있는 기반이 되었습니다.

소켓으로 보는 TCP: 최소한의 코드로 감 잡기

TCP 이야기를 하려면 소켓(socket)을 빼놓을 수 없습니다. C 언어로 TCP 서버를 만들 때 자주 보는 함수들이 바로 이 소켓 API들입니다.

흐름은 대략 이렇습니다.

  • socket(): 커널에 "통신 끝점"을 하나 만든다.

  • bind(): 그 끝점에 포트 번호를 붙인다. (예: 8080번 포트)

  • listen(): 이제 여기에 접속해 오는 클라이언트를 받을 준비를 한다.

  • accept(): 실제로 들어온 연결 하나를 받아서, 그 연결 전용 소켓을 만든다.

  • recv()/send(): 이 연결 위에서 데이터를 주고받는다.

  • close(): 연결을 정리하고 종료한다.

간단한 TCP 에코 서버를 만들면, 클라이언트가 보낸 문자열을 그대로 돌려보내는 방식으로 동작합니다. 여기에 약간 변형을 주면, 재밌는 점이 보입니다.

예를 들어:

  • 클라이언트가 "첫 번째 줄"을 보내면 서버는 바로 답장을 보내고

  • 그 다음에 서버가 5초 동안 sleep()으로 잠들어 있는 동안

  • 클라이언트가 "두 번째 줄"을 보내도, 이 데이터는 커널의 "수신 버퍼"에서 조용히 기다립니다.

서버가 잠에서 깨어 다시 recv()를 호출하면, 그동안 쌓여 있던 두 번째 줄이 한꺼번에 읽힙니다. 그 사이에 서버가 클라이언트로 "마음대로" 메시지를 하나 더 보내도 전혀 문제 없습니다.

이게 바로 TCP가 "진짜 양방향(duplex) 통신"이라는 증거입니다. 초기에는 누가 서버, 누가 클라이언트인지 나뉘지만, 연결이 맺어진 뒤에는 양쪽이 각자 원하는 타이밍에 자유롭게 데이터를 주고받는 단일 스트림이 됩니다. HTTP가 요청/응답 패턴을 쓸 뿐이지, TCP 자체는 그런 패턴에 묶여 있지 않습니다.

HTTP도 결국 TCP 위에서 놀 뿐이다

우리가 매일 쓰는 HTTP, 즉 웹 브라우징도 결국 TCP 위에서 돌아갑니다. 간단한 HTTP/1.1 서버를 직접 만들어보면 이 사실이 훨씬 직관적으로 느껴집니다.

TCP 서버 코드에 다음 정도의 작업만 추가하면 됩니다.

  • 클라이언트가 보낸 HTTP 요청(텍스트)을 recv()로 읽고

  • 우리가 만들어준 문자열을 응답 본문(body)로 사용한 뒤

  • 그 길이에 맞춰 Content-Length를 채운 HTTP/1.1 헤더를 보내고

  • 마지막에 본문을 따라 보내면서 연결을 닫는 방식으로 처리합니다.

예를 들면 브라우저 대신 curl로 이런 서버에 접속해 보면:

  • 첫 번째 요청 → "[1] Yo, I am a legit web server"

  • 두 번째 요청 → "[2] Yo, I am a legit web server"

처럼 요청 순서에 따라 응답 내용이 달라진다는 것을 확인할 수 있습니다.

여기서 중요한 포인트는 이겁니다. 웹 서버가 하는 일 대부분은 "HTTP 문법에 맞게 헤더와 바디를 잘 만들어 보내는 것"이고, "패킷이 중간에서 유실되지 않게, 순서를 유지하게, 중복을 처리하게, 속도를 조절하게" 해주는 건 전부 TCP의 역할이라는 점입니다.

멋진 HTML, CSS, JS가 화면에 잘 나타나는 그 순간 뒤에는, TCP가 묵묵히 데이터를 안전하게 배달하고 있는 셈이죠.

TCP 헤더 한 장으로 보는 신뢰성의 비밀

조금 더 기술적인 눈으로 보면, TCP 세그먼트는 꽤 정교한 헤더를 갖고 있습니다. 이 헤더 덕분에 TCP는 신뢰성과 순서를 보장할 수 있습니다.

핵심 필드만 뽑아보면 이렇습니다.

  1. 포트 번호 (Source Port, Destination Port)

  • 각각 16비트입니다.

  • 그래서 한 호스트에 열 수 있는 포트가 이론상 65,535개인 이유가 여기서 나옵니다.

  • "프로토콜(TCP/UDP) + 출발 IP + 출발 포트 + 도착 IP + 도착 포트"까지 합쳐진 5‑튜플이 하나의 연결을 유일하게 구분합니다.

  1. 시퀀스 번호(Sequence Number)와 확인 응답 번호(Acknowledgment Number)

  • 시퀀스 번호는 이 세그먼트가 "스트림에서 몇 번째 바이트부터" 담고 있는지를 나타냅니다.

  • ACK 번호는 "여기까지의 바이트는 제대로 받았어요"라는 의미입니다.

  • TCP는 누적 ACK를 쓰기 때문에, "ACK = 100"은 "0~99번 바이트까지는 온전히 받았다"는 뜻입니다.

  • 중간 구간(예: 100~199)이 유실되면, 그 뒤 바이트를 먼저 받아도 ACK는 계속 100에 머무릅니다. 이걸 보고 송신 측은 "아, 100 이후 구간이 비었구나"를 알아차리고 재전송합니다.

  1. 헤더 길이(Header Length)

  • TCP 헤더에는 옵션 필드가 있어서 길이가 가변적일 수 있습니다.

  • 그래서 "헤더가 몇 바이트인지"를 알려주는 필드가 필요합니다.

  1. 플래그 비트(Flags: SYN, ACK, FIN, RST 등)

  • SYN: 연결을 시작할 때 사용

  • ACK: ACK 번호가 유효함을 표시

  • FIN: 연결을 정상적으로 종료하겠다는 뜻

  • RST: 문제 발생, 강제 종료

이 플래그들은 각각 1비트짜리 스위치지만, 조합이 중요합니다.

가장 유명한 것은 3‑way 핸드셰이크입니다.

  • A → B: SYN (나 연결하고 싶어)

  • B → A: SYN + ACK (좋아, 나도 준비됐어)

  • A → B: ACK (알겠어, 시작하자)

이 세 번의 왕복으로 "우리가 서로 어떤 시퀀스 번호부터 시작하는지"를 맞춰놓고, 연결 상태를 만들게 됩니다.

종료할 때도 비슷하게 FIN과 ACK가 오가면서 3~4번의 동작으로 "이제 그만 이야기하자"를 합의합니다. 중간에 심각한 문제가 생기면 OS가 RST 플래그를 세운 세그먼트를 보내 "지금 당장 끊어!"라고 명령하기도 합니다.

  1. 윈도우(Window Size)와 체크섬(Checksum)

  • 윈도우는 앞에서 설명한 흐름 제어에 쓰입니다. "현재 이만큼 더 보낼 수 있어요"를 숫자로 표현합니다.

  • 체크섬은 세그먼트 전체를 16비트 단위로 더해서 만든 값으로, 도착한 쪽에서 다시 계산했을 때 값이 다르면 "중간에 비트가 뒤틀렸다"고 판단하고 버립니다.

이 작은 숫자들의 조합이 모여 "불안정한 인터넷 위에 안정적인 통신"이라는 착시 마법을 만들어냅니다.

네트워크 상태를 엿보기: 버퍼와 큐를 보는 법

리눅스에서 TCP 연결이 실제로 어떻게 돌아가고 있는지 보고 싶다면 ss 같은 도구가 유용합니다.

예를 들어, TCP 서버를 열어놓고 ss -tlpmi 같은 명령을 치면:

  • Recv-Q: 도착했지만 아직 애플리케이션이 읽지 않은 바이트 수

  • Send-Q: 보냈지만 아직 상대에게 ACK를 받지 못한 바이트 수

  • rb…, tb…: 수신 버퍼/송신 버퍼 크기

같은 정보가 나옵니다.

앞서 예로 들었던 에코 서버에서 서버가 sleep으로 잠들어 있을 때, 클라이언트가 보낸 두 번째 줄은 Recv-Q에 쌓여 있게 됩니다. 서버가 다시 읽기 시작하면, 이 대기 중이던 데이터가 한 번에 애플리케이션으로 전달됩니다.

이렇게 실제 도구로 버퍼 상태와 큐 상태를 함께 보면, 우리가 코드에서 사용하는 recv()/send()가 단순 함수 호출이 아니라 "커널 버퍼와 상호 작용하는 동작"이라는 게 훨씬 명확하게 느껴집니다.

TCP가 있어서 가능한 오늘의 인터넷, 그리고 작은 감사

불안정한 선 위에서 안정적인 통신을 제공하는 것, 사실 이건 꽤나 말도 안 되는 도전이었습니다.

몇십 년 전만 해도 몇 KB를 안정적으로 보내는 것조차 어려웠습니다. 이제 우리는 4K 영상 스트리밍을 틀어놓고, 동시에 화상회의에 접속하고, 클라우드에 대용량 파일을 올리면서도 "당연히 잘 되겠지"라고 생각합니다.

그 "당연함" 뒤에는,

  • 패킷 손실, 중복, 순서 뒤바뀜, 혼잡을 모두 감당해 주는 TCP 설계

  • 혼잡과 싸우기 위해 진화해온 수많은 알고리즘

  • 운영체제, 라우터, 서버 소프트웨어를 만들어온 개발자와 연구자들

이 있다는 걸 한 번쯤 떠올려 보면 좋겠습니다.

정리하자면:

  • IP만으로는 "어디 컴퓨터까지"만 갈 수 있고, TCP가 있어야 "어느 앱까지" 정확히 도착합니다.

  • TCP는 흐름 제어와 혼잡 제어로 상대 버퍼와 네트워크 전체를 배려합니다.

  • 시퀀스/ACK 번호, 윈도우, 플래그, 체크섬 등으로 신뢰성과 순서를 보장합니다.

  • 우리가 쓰는 HTTP, 이메일, SSH 등은 모두 이 튼튼한 기반 위에서 동작합니다.

다음에 curl로 API를 호출하거나, 브라우저로 웹 페이지를 열 때, 속으로 한 번만 이렇게 말해보면 어떨까요.

"인터넷이 이렇게 멀쩡히 돌아가는 건… TCP 너 덕분이야."

출처 및 참고 : The Internet is Cool. Thank you, TCP | Moncef Abboud

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.