TCP vs UDP: 무엇을 보장하고 무엇을 보장하지 않는가
들어가며
결제 API를 호출했는데 응답 직전에 연결이 끊겼습니다. TCP는 신뢰성 있는 프로토콜이라고 배웠는데, 이 결제는 처리된 걸까요? 한편 영상 통화는 신뢰성이 없다는 UDP를 쓰는데 왜 문제가 없을까요?
두 질문 모두 TCP와 UDP가 무엇을 보장하고 무엇을 보장하지 않는가에 대한 이야기입니다. 이번 글에서는 두 프로토콜의 동작과 ACK의 정확한 의미, 고르는 기준, 그리고 접속 오류를 읽는 법까지 정리합니다.
1. TCP: 연결을 맺고 도착을 확인합니다
TCP는 데이터를 보내기 전에 연결부터 맺습니다. 3-way 핸드셰이크로 양쪽이 서로의 시작 순서 번호를 확인하고, 그 뒤로는 바이트마다 붙은 순서 번호와 ACK(받았다는 확인)로 빠진 부분을 찾아 재전송합니다.
TCP는 먼저 통화로 약속을 잡고, 받을 때마다 수령 확인을 받는 등기 택배예요.
여기에 두 가지 조절 장치가 더 있습니다. 흐름 제어는 받는 쪽 버퍼가 넘치지 않게 수신 윈도우로 속도를 맞추고, 혼잡 제어는 유실이 보이면 보내는 양을 줄여 네트워크가 막히지 않게 합니다. 하나는 상대를, 하나는 길을 보호합니다.
2. UDP: 그냥 보냅니다
UDP는 연결 없이 데이터그램을 바로 보냅니다. 순서도 도착도 보장하지 않고, 빠지면 빠진 대로 끝입니다. 대신 핸드셰이크와 재전송 대기가 없어 지연과 오버헤드가 작습니다.
UDP를 단순히 "더 빠른 TCP"로 이해하면 곤란합니다. 빠른 이유는 기다림과 재전송을 하지 않기 때문이고, 신뢰성이 필요하면 그 위의 계층이 직접 만들어야 합니다. 대표적인 예가 QUIC입니다.
QUIC은 일반 우편(UDP) 위에 등기 업무를 직접 얹은 사설 택배예요.
위 그림처럼 HTTP/1.1과 HTTP/2는 TCP 위에서, HTTP/3은 UDP 위의 QUIC에서 동작합니다. TCP에서는 패킷 하나가 빠지면 같은 연결의 모든 스트림이 기다립니다(HOL 블로킹). QUIC은 스트림별로 재전송해 이 문제를 줄이고, TLS 1.3을 내장해 연결 수립도 빠릅니다.
3. 고르는 기준: 늦는 것과 빠지는 것
"실시간이면 UDP"는 절반만 맞습니다. 기준은 늦게 오는 것과 빠지는 것 중 무엇이 더 나쁜가입니다. 패킷 하나가 유실됐을 때 두 프로토콜의 동작을 나란히 보면 차이가 분명합니다.
TCP는 빠진 쪽이 올 때까지 책을 덮고 기다리고, UDP는 그 쪽을 건너뛰고 읽어요.
뒤에 도착한 4도 3이 올 때까지 앱에 전달되지 않는다.
3은 그대로 빠지고 4부터 바로 이어진다.
영상 통화에서는 1초 전 소리를 다시 받아 봐야 쓸모가 없으니, 지나간 것은 버리고 지금 소리를 재생합니다. 그래서 WebRTC 미디어는 보통 UDP를 씁니다. 반대로 실시간 음성 인식은 조각 하나가 빠지면 단어가 빠지므로, 약간의 지연을 감수하고 TCP 위의 WebSocket을 쓰는 경우가 많습니다.
4. ACK는 도착이지 처리가 아닙니다
처음 질문으로 돌아가겠습니다. TCP의 ACK는 받은 바이트가 상대 커널 버퍼까지 왔다는 신호입니다. 상대 앱이 그 데이터를 읽었는지, 처리에 성공했는지는 알려 주지 않습니다.
ACK는 택배를 받은 경비실의 서명이에요. 집주인이 열어 봤다는 뜻은 아니에요.
클라이언트는 결제가 됐는지 알 수 없다.
같은 키는 다시 처리하지 않는다.
그래서 응답 전에 연결이 끊기면 클라이언트는 처리 여부를 알 수 없습니다. 다시 보내면 두 번 처리될 수 있으므로, 재시도는 멱등 키와 함께 해야 합니다. 서버는 같은 키를 받으면 다시 처리하지 않고 저장해 둔 결과를 돌려줍니다.
# 나쁨: 키 없이 다시 보냄 → 두 번 결제될 수 있다
retry(lambda: charge(body))
# 좋음: 같은 키로 재시도
key = str(uuid4())
for i in range(3):
try:
return charge(body, key=key)
except ConnectionError:
if i == 2:
raise
sleep(2 ** i)5. 접속 오류 읽기: refused와 timeout
두 오류는 원인을 찾는 방향이 다릅니다. connection refused는 상대 호스트까지는 닿았지만 그 주소와 포트에 듣는 프로세스가 없어 RST를 받은 경우입니다. timeout은 응답이 아예 없는 경우라 방화벽, 보안 그룹, 라우팅을 먼저 봅니다.
nc -vz db.example.com 5432 # 포트까지 닿는지 확인
ss -tlnp # 서버에서 듣는 주소와 포트 확인refused라면 서버 프로세스가 떠 있는지와 바인딩 주소를 봅니다. 127.0.0.1 에만 바인딩한 서버는 다른 호스트에서 접속하면 거절됩니다. 다른 곳에서도 받으려면 0.0.0.0 이나 해당 인터페이스 주소에 바인딩해야 합니다.
한눈에 비교
| TCP | UDP | |
|---|---|---|
| 연결 | 핸드셰이크로 맺음 | 없음 |
| 순서·도착 | 보장 (재전송) | 보장 안 함 |
| 데이터 단위 | 바이트 스트림 | 데이터그램 |
| 지연 | 유실 시 재전송을 기다림 | 작음 |
| 헤더 크기 | 20바이트 이상 | 8바이트 |
| 주요 쓰임 | HTTP/1.1·2, DB 연결, 파일 전송 | DNS 조회, QUIC, WebRTC 미디어 |
헷갈리기 쉬운 점
- TCP는 메시지 경계를 지켜 주지 않습니다. 두 번 보낸 데이터가 한 번에 읽히거나, 한 번 보낸 데이터가 나뉘어 읽힐 수 있습니다. 길이 필드나 구분자로 메시지를 나누는 일은 앱의 몫입니다.
- TCP가 데이터 손상까지 완벽히 막지는 않습니다. 체크섬은 16비트로 약해 드물게 놓칠 수 있습니다. 중요한 데이터는 TLS나 앱 수준의 해시로 한 번 더 검증합니다.
- Docker 포트 매핑의 기본은 TCP입니다. UDP 서비스는
5000:5000/udp처럼 프로토콜을 적어야 합니다. 빠뜨리면 컨테이너가 떠 있어도 패킷이 닿지 않습니다.
면접에서 자주 받는 질문
3-way 핸드셰이크는 왜 세 번인가요?
양쪽이 서로의 시작 순서 번호를 확인해야 하기 때문입니다. 두 번으로 끝내면 서버는 자기 번호가 클라이언트에 전달됐는지 알 수 없고, 네트워크에 남아 있던 옛 SYN으로 연결이 잘못 열릴 위험도 있습니다.
TCP keepalive는 왜 필요한가요?
데이터가 오가지 않는 연결은 상대가 죽어도 알 수 없습니다. keepalive는 유휴 상태에서 작은 확인 패킷을 보내 죽은 연결을 찾아냅니다. 중간의 NAT나 로드밸런서가 유휴 연결을 끊는 것을 막는 데도 씁니다.
WebSocket은 HTTP와 어떤 관계인가요?
HTTP 요청에 Upgrade 헤더를 실어 시작하고, 서버가 101 Switching Protocols로 답합니다. 그 뒤에는 같은 TCP 연결 위에서 양쪽이 아무 때나 메시지를 보냅니다. 요청마다 연결을 맺지 않아 스트리밍에 잘 맞습니다.
HTTP/3은 왜 UDP 위의 QUIC을 쓰나요?
TCP에서는 패킷 하나가 빠지면 같은 연결의 모든 스트림이 기다립니다. QUIC은 UDP 위에서 스트림별로 재전송해 이 대기를 줄입니다. TCP는 OS 커널에 구현돼 있어 바꾸기 어렵지만, QUIC은 사용자 공간에서 구현할 수 있어 개선도 빠릅니다.
정리
- TCP는 순서와 도착을 보장하고, 유실되면 재전송을 기다립니다. UDP는 보장 없이 바로 보냅니다.
- 고르는 기준은 늦는 것과 빠지는 것 중 무엇이 더 나쁜가입니다.
- ACK는 상대 TCP까지 도착했다는 뜻입니다. 앱이 처리했는지는 응답으로만 확인합니다.
놓치면 안 되면 TCP, 늦으면 버려도 되면 UDP입니다. 그리고 TCP를 쓰더라도 처리 여부는 응답을 받아야 알 수 있습니다.