TCP와 UDP, 어떤 차이가 있을까?

TCP와 UDP, 어떤 차이가 있을까?

웹 서비스를 만들거나 게임 서버를 짜다 보면 “통신은 그냥 되는 거지” 정도로 넘어가게 되는 부분이 있습니다. 정작 소켓을 직접 열어 데이터를 주고받아야 할 때면, 만들 수 있는 소켓이 한 종류가 아니라는 사실과 마주치게 됩니다. 하나는 SOCK_STREAM, 다른 하나는 SOCK_DGRAM이라고 부르는데, 이 두 가지가 각각 TCP와 UDP에 해당합니다.

왜 굳이 두 종류로 나뉘어 있을까요? 🤔 같은 인터넷 위에서 데이터를 주고받는 일인데도, TCP와 UDP는 만들어진 목적이 꽤 다릅니다. 이번 글에서는 두 프로토콜이 어떤 점에서 다른지, 그리고 언제 어느 쪽을 골라야 하는지를 차근차근 짚어보겠습니다.

IP 위에 올라가는 두 가지 선택지

먼저 그림을 그려보면 이해가 쉽습니다. 인터넷의 가장 밑단에는 IP(Internet Protocol)가 있어서, 패킷이라는 작은 데이터 조각을 출발지에서 목적지로 옮기는 일을 맡습니다. 다만 IP는 “최선을 다해 보내본다”는 정도까지만 약속할 뿐, 패킷이 도중에 사라지거나 순서가 뒤바뀌어도 신경 쓰지 않습니다.

또 한 가지 IP만으로는 부족한 점이 있는데요. 한 컴퓨터 안에는 보통 여러 응용 프로그램이 동시에 동작하고 있어서, 같은 IP 주소로 패킷이 들어와도 어떤 프로그램에 전달해야 할지를 가려내야 합니다. 이 역할을 위해 등장한 것이 포트 번호이고, 포트 번호를 가지고 통신 양 끝의 응용 프로그램을 식별하는 자리가 바로 전송 계층입니다.

응용 프로그램이 매번 이런 불확실성을 직접 처리하기는 부담스러우니, 그 위에 한 겹을 더 올려서 좀 더 다루기 편하게 만든 것이 전송 계층입니다. 이 자리에 가장 널리 쓰이는 두 프로토콜이 바로 TCP와 UDP인데요. TCP는 “보낸 건 반드시 도착하고, 보낸 순서대로 도착하게 하겠다”는 약속을 추가했고, UDP는 “그런 약속은 안 한다, 대신 빠르게 보낸다”는 쪽으로 단순함을 택했습니다.

같은 IP 위에 두 가지 선택지가 있는 셈이라, 어떤 일을 하느냐에 따라 더 어울리는 쪽이 다릅니다. 파일을 한 글자도 빠짐없이 받아야 한다면 TCP가 자연스럽고, 잠깐 끊기더라도 다음 데이터로 바로 흘러가야 하는 통화나 게임 같은 상황에는 UDP가 잘 어울립니다.

TCP는 통화 같은 프로토콜

TCP를 한마디로 표현하면 “전화 통화” 같은 프로토콜입니다. 대화를 시작하기 전에 “여보세요?” 하고 상대를 부르고, 상대가 “네” 하고 답해야 본 대화가 시작되는 흐름과 비슷한데요. 이 첫 인사 과정을 흔히 3-way handshake라고 부릅니다.

연결을 맺는 동안 클라이언트와 서버는 서로의 시작 번호를 주고받아두고, 이후 흘러가는 모든 데이터에 일련번호를 매깁니다. 받는 쪽은 “여기까지 잘 받았다”는 신호를 일정 주기로 돌려보내는데, 이 신호가 정해진 시간 안에 오지 않으면 보낸 쪽은 같은 데이터를 다시 보내는 식으로 손실에 대응합니다. 덕분에 응용 프로그램 입장에서는 데이터가 빠지거나 뒤섞일 걱정 없이 그저 읽고 쓰기만 하면 됩니다.

3-way handshake
   클라이언트                        서버
       │                             │
       │── SYN ─────────────────────▶│   "연결할까?"
       │                             │
       │◀──── SYN-ACK ───────────────│   "그래, 너도 받을 준비 됐지?"
       │                             │
       │── ACK ─────────────────────▶│   "응, 시작하자"
       │                             │
       │═════ 데이터 ═══════════════▶│
       │◀════ 데이터 ════════════════│

TCP가 신뢰성을 지키기 위해 챙기는 일이 또 있습니다. 받는 쪽의 처리 속도가 느리면 보내는 쪽이 속도를 늦추는 흐름 제어가 그 하나이고, 네트워크 전체가 붐비는 듯하면 잠시 보내는 양을 줄였다가 천천히 늘리는 혼잡 제어가 다른 하나입니다. 어쩌다 한 번 패킷이 빠진 정도라면 큰 차이를 못 느끼지만, 회선이 정말 나쁠 때는 이런 자기 조절 덕분에 통신이 완전히 멈추지 않고 그럭저럭 굴러갑니다.

대신 이런 단계가 모두 비용을 동반합니다. 첫 데이터를 보내기 전에 세 번의 인사를 주고받아야 하므로 적어도 한 번의 왕복 시간이 그냥 흘러가고, 패킷 하나하나에 일련번호와 확인 응답을 챙기느라 헤더가 두툼해집니다. 대화를 끝낼 때도 비슷한 절차가 또 한 번 필요한데, 이를 두고 4-way handshake라고 부릅니다.

UDP는 우편엽서를 던지듯

반면 UDP는 “엽서를 던진다”는 비유가 잘 어울립니다. 연결을 맺는 절차도 없고, 받는 쪽이 받았는지 확인하지도 않습니다. 보내고 싶은 순간에 데이터그램이라는 한 덩어리를 주소만 적어 던지면 끝이죠.

이렇게 단순한 만큼 헤더도 가볍습니다. 출발 포트, 도착 포트, 길이, 검사합 정도만 들어 있어서 8바이트면 충분합니다. TCP가 일반적으로 20바이트 이상(옵션을 더하면 더 늘어나는) 헤더를 다는 것과 비교하면, 같은 양의 데이터를 보낼 때 통신 자체에 들이는 비용이 훨씬 적습니다.

헤더 크기 비교
TCP 세그먼트
┌───────────────────────────────────────┐
│ 헤더 (20바이트~)  │  데이터 (가변)     │
└───────────────────────────────────────┘

UDP 데이터그램
┌─────────────────────────┐
│ 헤더 (8바이트) │ 데이터  │
└─────────────────────────┘

다만 가벼운 만큼 책임도 가벼워지는 것은 아닙니다. 패킷이 도중에 사라져도 UDP는 다시 보내주지 않습니다. 순서가 바뀌어도 모르고, 같은 패킷이 두 번 도착해도 모릅니다. 이런 부분은 응용 프로그램이 직접 신경 써야 한다는 뜻이죠.

게임을 만들면 자기 좌표를 자주 보내는 경우가 있습니다. 어차피 다음 좌표가 곧 새로 갱신되므로, 한두 개 빠진 좌표를 굳이 다시 받느라 시간을 쓰는 것보다 그냥 다음 패킷으로 넘어가는 편이 자연스러운데요. 이런 상황이 UDP의 단순함이 강점이 되는 자리입니다.

헤더 한 줄로 보는 차이

좀 더 또렷하게 보고 싶으면 두 프로토콜의 헤더를 나란히 두는 것이 가장 빠릅니다. TCP 헤더에는 출발/도착 포트 외에도 일련번호, 확인 번호, 윈도 크기, 각종 플래그 비트가 줄줄이 들어가는데요. 이 필드 하나하나가 “잃어버린 패킷을 다시 보내고, 받는 쪽 속도에 맞추고, 연결을 정확히 닫는다”는 약속을 지키기 위해 필요한 정보입니다.

UDP 헤더는 출발 포트, 도착 포트, 전체 길이, 그리고 데이터가 깨지지 않았는지 확인할 짧은 검사합으로 끝납니다. 즉 “내가 누구한테 무엇을 보냈고, 그게 깨졌는지 정도만 확인할 수 있다”가 전부인데요. 이 차이가 곧 두 프로토콜의 성격을 만듭니다.

스트림과 메시지, 데이터를 받는 단위

표면적으로는 잘 보이지 않지만 실제로 코드를 짜다 보면 부딪히는 차이가 하나 더 있습니다. TCP는 데이터를 흘러가는 “스트림”으로 다루고, UDP는 한 덩어리씩 끊어 보내는 “메시지”로 다룹니다.

TCP 소켓을 통해 한 번에 1024바이트를 보냈다고 해도, 받는 쪽이 한 번에 1024바이트를 통째로 읽어오는 보장은 없습니다. 중간에 700바이트와 324바이트로 나뉘어 도착할 수도 있고, 다른 호출에서 보낸 데이터가 뒤에 붙어 한 번에 더 큰 덩어리로 읽힐 수도 있죠. 그래서 TCP를 다룰 때는 어디까지가 한 메시지인지를 응용 프로그램이 직접 정해줘야 하는데, 길이를 앞에 붙이거나 줄바꿈 같은 구분자를 두는 식으로 해결합니다.

반면 UDP는 보내는 쪽이 한 번에 보낸 양을 받는 쪽도 한 번에 읽어옵니다. 이를 두고 메시지 경계가 보존된다고 표현하는데요. 대신 한 번에 너무 크게 보내면 중간에서 잘려 손실로 이어질 수 있어서, 안전한 크기에 맞춰 작게 끊어 보내는 책임은 여전히 응용 프로그램에 있습니다.

TCP가 더 어울리는 자리

신뢰성과 순서가 중요한 일에는 TCP가 자연스럽게 어울립니다. 웹 페이지를 받을 때 HTML 파일이 한 글자라도 빠지면 페이지가 깨지고, 이메일을 받을 때 본문 일부가 사라지면 의미가 아예 달라지는데요. 그래서 HTTP, SMTP, IMAP, FTP 같은 프로토콜이 전부 TCP 위에서 동작합니다.

데이터베이스 클라이언트나 SSH 같은 원격 접속도 마찬가지입니다. 명령어가 일부만 도착하거나 응답 일부가 빠지면 디버깅하기도 어렵고, 자칫 중요한 데이터가 망가질 수도 있습니다. 웹소켓 역시 TCP 위에서 동작하는데, 양방향 메시지가 손실 없이 순서대로 도착해야 하기 때문이죠.

요약하자면 “데이터의 정확성이 통신 속도보다 중요한가?”라고 물었을 때 그렇다고 답할 만한 자리에는 거의 모두 TCP가 들어가 있다고 봐도 무방합니다.

UDP가 더 어울리는 자리

반대로 “약간 빠지더라도 빠른 게 낫다”는 자리에는 UDP가 잘 맞습니다. 영상 통화나 인터넷 전화에서 음성 패킷 하나가 늦게 도착하면 그 사이에 침묵이 생기는데, 그 침묵을 메우려고 다시 보내봤자 이미 의미를 잃어버린 데이터입니다. 차라리 그 자리는 그냥 비워두고 다음 음성으로 넘어가는 편이 통화 품질에 도움이 되는 셈이죠.

DNS 조회도 흥미로운 예입니다. 도메인 이름 하나를 IP 주소로 바꾸는 작업은 보통 패킷 한 개로 끝날 만큼 짧아서, 굳이 연결을 맺고 끊는 절차를 거치는 편이 오히려 손해죠. 그래서 기본 동작은 UDP를 쓰고, 응답이 너무 길어 한 패킷에 담기지 않을 때만 TCP로 다시 시도하는 식으로 설계되어 있습니다.

이외에도 NTP 같은 시간 동기화, 멀티캐스트로 사내에 영상을 흘려보내는 화상회의, 짧은 측정값을 자주 보내는 일부 IoT 센서처럼 “잠깐 사라져도 큰 문제가 없는” 통신은 대체로 UDP를 골라 씁니다.

흥미로운 점은 최근에 등장한 HTTP/3가 채택한 QUIC도 UDP 위에 올라가 있다는 사실입니다. TCP의 무거운 절차를 건너뛰는 대신, 신뢰성에 해당하는 부분을 사용자 공간에서 새로 구현하는 식인데요. UDP가 단순히 “가벼운 것”이라기보다는 “TCP와 다른 토대”라는 점을 짚어두면, 앞으로 등장하는 새 프로토콜들을 이해하기가 한결 수월해집니다.

같은 포트 번호인데 둘이 같이 쓸 수 있을까

처음 네트워크를 공부하면 “포트 80은 HTTP가 쓴다”는 식으로 외우게 됩니다. 그러다 보면 “그러면 같은 포트 80을 TCP와 UDP가 동시에 쓰면 충돌하지 않나?” 하는 의문이 들기 마련이죠. 결론부터 말하자면 충돌하지 않습니다.

운영 체제는 들어오는 패킷을 보고 “어떤 프로토콜로 왔는지”를 먼저 본 다음, 같은 프로토콜끼리만 포트 번호를 따집니다. 즉 TCP 53번과 UDP 53번은 완전히 다른 두 통로처럼 취급되고, DNS 서버는 실제로 둘을 동시에 열어두고 사용하는데요. 프로토콜과 포트 번호 두 가지가 합쳐져 통로를 식별한다고 생각하면 쉽습니다.

리눅스에서 동시에 열린 포트 확인
ss -tulpn | grep ':53'

명령어 결과를 보면 같은 53번이 tcp, udp 행에 모두 등장하는 모습을 발견할 수 있는데요. 이게 바로 위 설명이 실제로 일어나고 있다는 증거입니다.

어느 쪽을 골라야 할지 헷갈릴 때

새로운 기능을 만들면서 어느 쪽을 써야 할지 망설일 때는 다음 질문 두 개만 떠올려보면 어느 정도 가닥이 잡힙니다.

먼저 “데이터가 한 조각이라도 빠지면 안 되는가?”입니다. 빠지면 안 된다면 거의 항상 TCP가 답이고, 일부 손실이 허용된다면 UDP를 후보에 둘 수 있습니다.

그다음은 “지연 시간이 정확성보다 더 중요한가?”입니다. 실시간 음성, 실시간 영상, 빠르게 갱신되는 좌표 데이터처럼 “지금 도착하는 게 핵심”이라면 UDP 쪽이 더 잘 맞고, 반대로 응답이 조금 늦더라도 정확해야 한다면 TCP 쪽이 안전합니다.

물론 이 두 질문만으로 모든 경우가 갈리지는 않습니다. 실제로는 이미 만들어져 있는 상위 프로토콜(HTTP, gRPC, MQTT 등)의 선택을 따라가는 경우가 많고, 새 프로토콜을 직접 설계할 일은 흔치 않습니다. 그래도 TCP와 UDP가 무엇을 약속하고 무엇을 약속하지 않는지 알아두면, 디버깅을 할 때나 다른 사람의 설계를 읽을 때 훨씬 편안해지죠.

마치며

같은 IP 위에서 동작하는 두 프로토콜이 서로 다른 모습으로 만들어진 데에는 이유가 있습니다. TCP는 “정확하게 다 도착시키겠다”는 약속을 위해 헤더를 무겁게 만들고 절차를 늘렸고, UDP는 “도착시키는 일은 응용 프로그램이 알아서”라고 떠넘긴 대신 가벼움을 얻었는데요. 어느 쪽이 더 좋다기보다는, 우리가 풀려는 문제가 어느 약속을 더 필요로 하는지를 먼저 물어보는 것이 출발점입니다.

다음에는 TCP의 혼잡 제어가 실제로 어떻게 동작하는지, 또 최근에 등장한 QUIC가 UDP 위에 어떻게 신뢰성을 다시 쌓아 올렸는지를 따로 다뤄보면 재미있을 것 같습니다. 프로토콜 동작을 좀 더 깊이 들여다보고 싶다면 RFC 793(TCP)과 RFC 768(UDP) 원문을 한 번 훑어보는 것을 추천합니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord