Skip to content
ioob.dev
Go back

네트워크 기초 3편 — TCP와 UDP

· 12분 읽기
Network 시리즈 (3/7)
  1. 네트워크 기초 1편 — OSI와 TCP/IP 모델
  2. 네트워크 기초 2편 — IP 주소와 서브넷
  3. 네트워크 기초 3편 — TCP와 UDP
  4. 네트워크 기초 4편 — DNS
  5. 네트워크 기초 5편 — HTTP와 HTTPS
  6. 네트워크 기초 6편 — TLS/SSL
  7. 네트워크 기초 7편 — 로드 밸런서와 프록시
Table of contents

Table of contents

IP만으로는 부족하다

앞선 2편에서 IP 주소는 “어느 호스트로 보낼지”를 정한다고 했다. 그런데 한 호스트 안에는 수많은 프로그램이 떠 있다. 웹 서버도 있고, 메일 서버도 있고, SSH 데몬도 있다. IP 주소만으로는 “이 데이터가 어느 프로그램에 가야 하는지”를 알 수 없다. 그래서 필요한 것이 포트(port)다. 그리고 IP + 포트의 조합으로 양 끝의 프로세스를 연결해주는 장치가 소켓(socket)이다.

그 위에 깔리는 두 개의 프로토콜이 TCPUDP다. 둘 다 OSI 4계층(Transport)에 속하지만 성격은 정반대다. TCP는 “믿을 수 있게”에 모든 것을 걸었고, UDP는 “빠르고 가볍게”에 모든 것을 걸었다. 이번 편은 이 두 프로토콜이 왜 그렇게 설계됐고, 어떤 상황에서 무엇을 고르는지를 따라간다.

포트와 소켓 — 프로세스를 가리키는 번호

포트는 16비트 숫자(0~65535)다. 같은 IP를 쓰는 여러 프로세스를 구분하는 번호표다. 웹 서버는 80번이나 443번을 듣고, SSH는 22번을 듣고, PostgreSQL은 5432번을 듣는다. 숫자에 구속력은 없지만 관례로 정해진 번호들이 있다.

소켓은 IP 주소와 포트를 하나로 묶은 통신 종단점이다. 두 소켓이 연결되면 양 끝의 프로세스는 파일에 쓰듯 데이터를 주고받을 수 있다. 리눅스 관점에서 소켓은 파일 디스크립터 하나에 불과하다. read, write로 다룬다.

한 연결은 네 개의 값으로 유일하게 식별된다 — 출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트. 이 네 값을 4-tuple이라고 부른다. 같은 서버가 같은 포트(예: 443)로 수만 개의 동시 연결을 받을 수 있는 건, 클라이언트 쪽 IP나 포트가 달라서 tuple이 모두 다르기 때문이다.

flowchart LR
    subgraph C["클라이언트 192.168.1.10"]
        CA["브라우저<br/>sport: 54321"]
    end
    subgraph S["서버 93.184.216.34"]
        SA["웹 서버<br/>dport: 443"]
    end
    CA -- "4-tuple<br/>(192.168.1.10, 54321, 93.184.216.34, 443)" --> SA

현재 리눅스에서 열린 소켓은 ss 명령으로 볼 수 있다.

ss -tunap
# TCP/UDP, 숫자로, 모든 상태, 프로세스 이름까지
# Netid  State   Recv-Q Send-Q  Local Address:Port   Peer Address:Port   Process
# tcp    LISTEN  0      128     0.0.0.0:22           0.0.0.0:*           users:(("sshd",...))
# tcp    ESTAB   0      0       192.168.1.10:54321   93.184.216.34:443   users:(("chrome",...))

LISTEN 상태는 “이 포트로 누군가 들어오기를 기다린다”는 뜻이고, ESTAB은 “연결이 열려 있다”는 뜻이다. 실무에서 “이 포트가 뭐가 쓰고 있지?”를 확인할 때 가장 먼저 꺼내는 명령이다.

TCP는 왜 “믿을 수 있는” 프로토콜인가

TCP(Transmission Control Protocol)가 신뢰성을 보장한다고 할 때, 이는 다음을 의미한다.

이 모든 걸 IP 위에서 해낸다. IP는 그냥 “던져보고 도착하면 좋고 아니면 말고”인 프로토콜이다. TCP는 그 불안정한 기반 위에 신뢰성 레이어를 씌운다. 덕분에 애플리케이션은 “데이터를 쓰면 순서대로 상대에게 간다”는 착각을 유지할 수 있다. 이 착각이 TCP의 본질이다.

3-way handshake — 연결을 여는 세 번의 악수

TCP는 연결 지향(connection-oriented) 프로토콜이다. 데이터를 주고받기 전에 먼저 “연결을 엽시다”라는 합의를 맺는다. 이 합의 과정이 세 번의 패킷으로 이뤄지기 때문에 3-way handshake라고 부른다.

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버

    Note over C,S: 연결 수립
    C->>S: SYN (seq=x)
    Note right of S: 들어오는 연결 수락 준비
    S->>C: SYN-ACK (seq=y, ack=x+1)
    C->>S: ACK (ack=y+1)
    Note over C,S: 이제 데이터 주고받을 수 있음

    C->>S: HTTP GET /
    S->>C: HTTP 200 OK + body

세 단계의 의미를 풀어본다.

  1. SYN (Synchronize): 클라이언트가 “연결 좀 엽시다. 내 초기 순서번호는 x요” 하고 첫 패킷을 보낸다
  2. SYN-ACK: 서버가 “그래요. 내 초기 순서번호는 y고, 당신의 x 다음인 x+1을 기다립니다” 하고 응답한다
  3. ACK: 클라이언트가 “당신의 y 다음인 y+1을 기다립니다” 하고 마무리한다

왜 두 번이 아니라 세 번인가? 양쪽 모두 자기 초기 순서번호를 상대에게 알리고, 상대가 그걸 받았는지 확인해야 하기 때문이다. 순서번호가 엇갈리면 재전송 로직이 꼬인다. 또 오래된 SYN이 뒤늦게 도착해서 생기는 “유령 연결”을 막기 위해서도 세 번이 필요하다.

이 핸드셰이크는 지연 시간을 만드는 주범이기도 하다. RTT(Round-Trip Time)가 50ms인 네트워크에서는 첫 데이터가 오가기 전에 최소 50ms의 지연이 생긴다. HTTPS는 여기에 TLS 핸드셰이크까지 겹쳐서 수백 ms가 쉽게 쌓인다. 이런 비용 때문에 HTTP/2는 연결 하나를 오래 유지하며 여러 요청을 나르고, HTTP/3은 아예 TCP를 버리고 QUIC(Quick UDP Internet Connections) 위에서 동작한다.

연결을 닫는 4-way handshake

연결을 여는 게 셋이었으니 닫는 건 넷이다. 양쪽이 독립적으로 “나는 더 보낼 게 없다”를 선언하고 확인받는 구조다.

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버

    C->>S: FIN
    S->>C: ACK
    Note over S: 서버는 아직 보낼 게 있을 수 있다
    S->>C: FIN
    C->>S: ACK
    Note over C: TIME_WAIT 상태로 잠시 대기

FIN을 보낸 쪽은 반대 방향으로 들어오는 데이터는 계속 받을 수 있다. 이걸 half-close라고 한다. 파이프라인 처리가 필요한 프로토콜(SSH, 대용량 파일 전송 등)에서 중요하다.

마지막 ACK를 보낸 클라이언트는 잠시 TIME_WAIT 상태로 남는다. 이건 “혹시 마지막 ACK가 유실돼서 서버가 FIN을 재전송하면 받아주기 위한” 안전장치다. 기본값은 2 * MSL(약 60초)이다. 이 특성 때문에 짧은 연결을 수만 건 처리하는 서버는 TIME_WAIT 소켓이 쌓여 포트 고갈이 생기기도 한다. 이럴 때 net.ipv4.tcp_tw_reuse 같은 커널 파라미터를 건드리는 일이 있다.

흐름 제어 vs 혼잡 제어

TCP가 “믿을 수 있다”고 할 때, 그 믿음의 핵심은 속도 조절이다. 두 종류가 있다.

흐름 제어(flow control)는 받는 쪽을 보호한다. 수신자가 버퍼를 다 쓰면 더 보내지 말라고 신호를 보낸다. TCP 헤더의 Window Size 필드로 “지금 내가 받을 수 있는 바이트 수”를 매번 알려준다. 이 값이 0이면 송신자는 잠시 멈춘다.

혼잡 제어(congestion control)는 네트워크 자체를 보호한다. 경로 중간의 라우터가 막혀서 패킷이 버려지기 시작하면, 송신자는 속도를 확 줄였다가 천천히 올린다. 대표적인 알고리즘이 TCP Reno의 Slow Start, Congestion Avoidance, Fast Retransmit, Fast Recovery 네 단계다.

flowchart LR
    SS["Slow Start<br/>지수적으로 증가"] --> CA["Congestion Avoidance<br/>선형적으로 증가"]
    CA -- 패킷 손실 --> FR["Fast Retransmit<br/>즉시 재전송"]
    FR --> FCR["Fast Recovery<br/>창 크기 절반으로"]
    FCR --> CA

더 최근에 나온 TCP BBR(Bottleneck Bandwidth and RTT)은 손실이 아니라 실제 대역폭과 RTT를 측정해서 속도를 조절한다. 구글이 밀고 있고, 장거리 연결이나 모바일 환경에서 Reno보다 빠른 경우가 많다. 자세한 배경은 Wikipedia: TCP congestion control에 잘 정리돼 있다.

흐름 제어와 혼잡 제어를 헷갈리지 않는 감각 — “수신자 보호냐, 네트워크 보호냐” — 이 있으면 TCP 튜닝이 필요한 순간에 어느 쪽을 건드려야 할지 판단이 선다.

TCP 상태 기계

TCP 연결은 생명주기 동안 여러 상태를 거친다. 자주 마주치는 상태들을 정리한다.

CLOSE_WAIT이 쌓이는 건 애플리케이션 버그의 신호다. 상대는 “끝” 신호를 보냈는데 우리 쪽 프로세스가 close()를 안 부른 상태. ss로 확인해서 CLOSE_WAIT이 많으면 코드의 연결 해제 로직을 점검해야 한다.

UDP — 신뢰성을 버리는 선택

UDP(User Datagram Protocol)는 TCP와 정반대의 철학이다. “보내고 잊어라(fire and forget).” 순서 보장도 없고, 재전송도 없고, 흐름 제어도 없다. 헤더는 고작 8바이트다.

UDP 헤더(8바이트):
  출발지 포트 (16bit)
  목적지 포트 (16bit)
  길이        (16bit)
  체크섬       (16bit)

왜 이런 프로토콜이 필요한가? 어떤 종류의 데이터는 늦게 오는 것보다 차라리 빠지는 게 낫기 때문이다.

UDP 위에서 신뢰성이 필요하면? 애플리케이션이 직접 구현하면 된다. QUIC이 대표적인 사례다. UDP 위에 TCP의 신뢰성 + TLS의 암호화를 합쳐서 올린 프로토콜이고, HTTP/3가 이 위에서 돈다. TCP보다 빠른 핸드셰이크(0-RTT 가능), 연결 마이그레이션, 헤드 오브 라인 블로킹 해결 같은 이점을 준다.

TCP와 UDP 직접 비교

항목TCPUDP
연결연결 지향 (handshake)비연결
신뢰성보장 (재전송, 순서)없음
흐름/혼잡 제어있음없음
헤더 크기20바이트 이상8바이트
속도상대적으로 느림빠름
용도HTTP, SSH, DB, 파일 전송DNS, VoIP, 게임, QUIC

어느 쪽을 쓸지 고르는 기준은 하나다. “데이터가 늦게라도 꼭 도착해야 하는가, 아니면 늦으면 쓸모없는가?” 답이 전자면 TCP, 후자면 UDP다.

눈으로 보는 TCP — tcpdump로 핸드셰이크 찍기

말로만 하면 추상적이니 tcpdump로 실제 패킷을 떠본다. 이 명령은 루트 권한이 필요하다.

# 대상 호스트와 주고받는 TCP 패킷을 캡처
sudo tcpdump -i any -n 'host example.com and tcp port 443' -c 6

# 출력 예시 (일부 단순화)
# IP 192.168.1.10.54321 > 93.184.216.34.443: Flags [S], seq 1000
# IP 93.184.216.34.443 > 192.168.1.10.54321: Flags [S.], seq 2000, ack 1001
# IP 192.168.1.10.54321 > 93.184.216.34.443: Flags [.], ack 2001
# IP 192.168.1.10.54321 > 93.184.216.34.443: Flags [P.], len 517  ← ClientHello
# ...

[S]가 SYN, [S.]가 SYN-ACK, [.]만 있으면 ACK, [P.]는 Push(데이터). 이 Flags 문자열이 TCP 헤더의 플래그 비트를 한 글자씩 축약한 표기다. 한 번 눈으로 핸드셰이크를 보고 나면 이 프로토콜이 훨씬 구체적으로 와닿는다.

UDP도 똑같이 볼 수 있다. sudo tcpdump -i any -n 'udp port 53'으로 DNS 질의를 떠보면 핸드셰이크 없이 요청 하나, 응답 하나로 끝나는 간결함이 확인된다.

실무에서 자주 만나는 이슈

TCP/UDP와 관련된 현장의 대표적인 곤란들을 짚어둔다.

이 주제들은 한 편 한 편 별도의 글이 될 만한 양이다. 여기서는 “이런 게 있다”는 레이블만 달아둔다.


다음 편에서는 이 TCP/UDP 위에서 도는 가장 많이 쓰이는 프로토콜 중 하나 — DNS를 해부한다. example.com이라는 사람의 언어가 어떻게 93.184.216.34라는 숫자로 바뀌는지, 그 번역 과정에 참여하는 여러 서버의 역할을 따라간다.

4편: DNS


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
네트워크 기초 2편 — IP 주소와 서브넷
Next Post
네트워크 기초 4편 — DNS