🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

TCP와 UDP 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 5. · 12 Views

TCP와 UDP 완벽 가이드

네트워크 통신의 핵심 프로토콜인 TCP와 UDP를 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 세그먼트 구조부터 3-way 핸드셰이크, 상태 전이까지 실무에서 꼭 알아야 할 내용을 담았습니다.


목차

  1. TCP_세그먼트_구조
  2. 제어_비트와_순서_확인_응답_번호
  3. 3_way_핸드셰이크_연결_수립
  4. 4_way_핸드셰이크_연결_종료
  5. TCP_상태_전이
  6. UDP_데이터그램_구조
  7. TCP_vs_UDP_비교

1. TCP 세그먼트 구조

어느 날 김개발 씨가 네트워크 디버깅을 하다가 이상한 현상을 발견했습니다. 분명 데이터를 보냈는데 상대방이 받지 못했다고 합니다.

Wireshark를 열어 패킷을 분석하려는데, TCP 세그먼트라는 단어가 눈에 들어왔습니다. "세그먼트가 도대체 뭐지?"

TCP 세그먼트는 TCP 프로토콜이 데이터를 전송할 때 사용하는 기본 단위입니다. 마치 택배 상자처럼, 실제 내용물인 데이터와 함께 배송에 필요한 정보가 적힌 송장이 붙어 있습니다.

이 송장에 해당하는 것이 바로 헤더이고, 내용물이 페이로드입니다. 세그먼트 구조를 이해하면 네트워크 문제를 진단하고 해결하는 능력이 크게 향상됩니다.

다음 코드를 살펴봅시다.

import struct

# TCP 세그먼트 헤더 구조 (20바이트 기본)
class TCPSegment:
    def __init__(self, src_port, dst_port, seq_num, ack_num):
        self.src_port = src_port      # 출발지 포트 (2바이트)
        self.dst_port = dst_port      # 목적지 포트 (2바이트)
        self.seq_num = seq_num        # 순서 번호 (4바이트)
        self.ack_num = ack_num        # 확인 응답 번호 (4바이트)
        self.data_offset = 5          # 헤더 길이 (4비트, 5 = 20바이트)
        self.flags = 0                # 제어 비트 (6비트)
        self.window = 65535           # 윈도우 크기 (2바이트)
        self.checksum = 0             # 체크섬 (2바이트)
        self.urgent_ptr = 0           # 긴급 포인터 (2바이트)

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 최근 서버 간 통신에서 간헐적으로 데이터가 누락되는 문제를 겪고 있었습니다.

로그를 아무리 봐도 원인을 찾을 수 없었습니다. 선배 개발자 박시니어 씨가 다가와 말했습니다.

"네트워크 레벨에서 문제를 확인해봤어요? TCP 세그먼트를 직접 분석해보면 뭔가 보일 수도 있어요." 그렇다면 TCP 세그먼트란 정확히 무엇일까요?

쉽게 비유하자면, TCP 세그먼트는 마치 국제 택배 상자와 같습니다. 해외로 물건을 보낼 때 상자 겉면에는 보내는 사람 주소, 받는 사람 주소, 내용물 설명, 무게, 취급 주의사항 등이 적힌 송장이 붙어 있습니다.

TCP 세그먼트도 마찬가지로 데이터를 안전하게 전달하기 위해 필요한 모든 정보를 헤더라는 송장에 담아서 보냅니다. TCP 세그먼트 헤더는 기본적으로 20바이트로 구성됩니다.

가장 먼저 나오는 것은 출발지 포트목적지 포트입니다. 각각 2바이트씩 차지하며, 어느 애플리케이션에서 보내고 어느 애플리케이션이 받을지를 결정합니다.

그 다음으로 중요한 것이 순서 번호확인 응답 번호입니다. 각각 4바이트로, TCP가 신뢰성 있는 전송을 보장하는 핵심 요소입니다.

순서 번호는 "이 데이터가 전체 데이터 중 몇 번째인지"를 알려주고, 확인 응답 번호는 "여기까지 잘 받았으니 다음 것을 보내달라"고 응답합니다. 위의 코드를 살펴보면, TCPSegment 클래스가 헤더의 각 필드를 속성으로 가지고 있습니다.

src_port와 dst_port는 통신의 출발점과 도착점을 나타냅니다. seq_num과 ack_num은 데이터의 순서를 관리합니다.

data_offset은 헤더의 길이를 4바이트 단위로 나타냅니다. 기본값 5는 20바이트를 의미합니다.

옵션이 추가되면 이 값이 커질 수 있습니다. window 필드는 흐름 제어에 사용됩니다.

"나는 현재 이만큼의 데이터를 더 받을 수 있어"라고 상대방에게 알려주는 역할을 합니다. 이를 통해 수신자가 처리할 수 없을 만큼 많은 데이터를 보내는 것을 방지합니다.

checksum은 데이터가 전송 중에 손상되지 않았는지 확인하는 데 사용됩니다. 마치 택배를 받았을 때 내용물이 파손되지 않았는지 확인하는 것과 같습니다.

실제 현업에서는 이런 구조를 직접 다룰 일은 많지 않습니다. 하지만 네트워크 문제가 발생했을 때 Wireshark 같은 도구로 패킷을 분석하려면 이 구조를 알아야 합니다.

세그먼트의 각 필드가 무엇을 의미하는지 이해하면, 문제의 원인을 훨씬 빠르게 찾을 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Wireshark에서 TCP 세그먼트를 분석한 결과, 특정 패킷의 체크섬 오류를 발견했습니다. 네트워크 장비 문제로 데이터가 손상되고 있었던 것입니다.

TCP 세그먼트 구조를 알았기에 문제를 해결할 수 있었습니다.

실전 팁

💡 - Wireshark에서 TCP 세그먼트를 분석할 때 헤더의 각 필드 의미를 기억하세요

  • 순서 번호와 확인 응답 번호는 상대적인 값으로 표시되는 경우가 많습니다

2. 제어 비트와 순서 확인 응답 번호

김개발 씨가 TCP 세그먼트 구조를 공부하다가 flags라는 필드를 발견했습니다. SYN, ACK, FIN 같은 약어들이 나오는데, 이게 다 무슨 뜻인지 머리가 복잡해졌습니다.

"이 작은 비트들이 도대체 무슨 역할을 하는 걸까?"

제어 비트는 TCP 세그먼트 헤더에 있는 6개의 플래그로, 연결 수립, 데이터 전송, 연결 종료 등의 상태를 제어합니다. 마치 교통 신호등처럼 통신의 흐름을 제어하는 역할을 합니다.

순서 번호는 보내는 데이터의 순번을, 확인 응답 번호는 받고 싶은 다음 데이터의 순번을 나타냅니다.

다음 코드를 살펴봅시다.

# TCP 제어 비트 (플래그) 정의
class TCPFlags:
    URG = 0b100000  # 긴급 데이터 (Urgent)
    ACK = 0b010000  # 확인 응답 (Acknowledgment)
    PSH = 0b001000  # 즉시 전달 (Push)
    RST = 0b000100  # 연결 강제 종료 (Reset)
    SYN = 0b000010  # 연결 요청 (Synchronize)
    FIN = 0b000001  # 연결 종료 (Finish)

# 순서 번호와 확인 응답 번호 예시
initial_seq = 1000          # 클라이언트 초기 순서 번호
data_length = 100           # 전송할 데이터 길이
next_seq = initial_seq + data_length  # 다음 순서 번호: 1100

# 서버의 확인 응답
server_ack = next_seq       # "1100번부터 보내줘"라는 의미

김개발 씨는 TCP의 세계에 점점 빠져들고 있었습니다. 세그먼트 구조는 이해했는데, 그 안에 있는 제어 비트라는 것이 궁금해졌습니다.

박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "제어 비트는 마치 교통 신호등 같아요.

빨간불, 노란불, 초록불이 차량의 흐름을 제어하듯이, 제어 비트가 TCP 통신의 흐름을 제어하는 거죠." TCP에는 총 6개의 제어 비트가 있습니다. 각각 1비트씩 차지하며, 0 또는 1의 값을 가집니다.

가장 중요한 세 가지는 SYN, ACK, FIN입니다. SYN은 Synchronize의 약자로, 연결을 시작하고 싶을 때 사용합니다.

"안녕하세요, 통신 시작해도 될까요?"라고 묻는 것과 같습니다. 연결을 원하는 쪽에서 먼저 SYN 플래그를 1로 설정한 세그먼트를 보냅니다.

ACK는 Acknowledgment의 약자로, "잘 받았어요"라는 의미입니다. 상대방이 보낸 데이터를 정상적으로 수신했을 때 이 플래그를 설정합니다.

연결이 수립된 후에는 거의 모든 세그먼트에 ACK가 설정됩니다. FIN은 Finish의 약자로, 연결을 끝내고 싶을 때 사용합니다.

"이제 보낼 데이터가 없으니 연결을 끊겠습니다"라는 신호입니다. 나머지 세 가지도 알아봅시다.

RST는 Reset으로, 연결을 강제로 끊을 때 사용합니다. 비정상적인 상황에서 주로 발생합니다.

PSH는 Push로, 버퍼에 쌓아두지 말고 즉시 애플리케이션에 전달하라는 의미입니다. URG는 Urgent로, 긴급 데이터가 있음을 알립니다.

이제 순서 번호확인 응답 번호를 살펴봅시다. 순서 번호는 내가 보내는 데이터의 첫 번째 바이트가 전체 데이터 스트림에서 몇 번째인지를 나타냅니다.

예를 들어, 순서 번호가 1000이고 100바이트를 보낸다면, 이 세그먼트에는 1000번부터 1099번까지의 데이터가 담겨 있습니다. 확인 응답 번호는 "다음에 이 번호의 데이터를 보내줘"라는 의미입니다.

위의 예에서 수신자가 데이터를 잘 받았다면, 확인 응답 번호를 1100으로 설정해서 보냅니다. "1099번까지 잘 받았으니, 1100번부터 보내줘"라는 뜻입니다.

이 메커니즘 덕분에 TCP는 신뢰성 있는 전송을 보장합니다. 만약 데이터가 중간에 사라지면, 확인 응답이 오지 않으므로 송신자가 다시 보낼 수 있습니다.

또한 데이터가 순서대로 도착하지 않아도, 순서 번호를 보고 올바른 순서로 재조립할 수 있습니다. 실무에서 네트워크 문제를 분석할 때, 이 번호들을 추적하면 어디서 문제가 발생했는지 알 수 있습니다.

순서 번호가 갑자기 건너뛴다면 패킷 손실이 있는 것이고, 같은 확인 응답 번호가 반복된다면 재전송이 발생하고 있는 것입니다. 김개발 씨는 고개를 끄덕였습니다.

"그래서 TCP가 신뢰성 있다고 하는 거군요. 이 번호들이 데이터가 제대로 전달되었는지 확인해주니까요."

실전 팁

💡 - SYN, ACK, FIN 세 가지 플래그만 확실히 이해해도 TCP 통신의 80%는 이해한 겁니다

  • 순서 번호와 확인 응답 번호의 관계를 그림으로 그려보면 이해가 쉬워집니다

3. 3 way 핸드셰이크 연결 수립

김개발 씨가 웹 브라우저로 사이트에 접속할 때마다 뒤에서는 어떤 일이 벌어지는지 궁금해졌습니다. "서버에 연결된다는 게 구체적으로 무슨 의미지?" 선배에게 물었더니 "3-way 핸드셰이크라고 들어봤어?"라는 대답이 돌아왔습니다.

3-way 핸드셰이크는 TCP 연결을 수립하는 과정으로, 클라이언트와 서버가 서로 세 번의 메시지를 주고받습니다. 마치 전화를 걸 때 "여보세요?" - "네, 여보세요" - "잘 들리네요, 통화합시다"라고 확인하는 것과 같습니다.

이 과정을 통해 양쪽 모두 데이터를 주고받을 준비가 되었음을 확인합니다.

다음 코드를 살펴봅시다.

# 3-way 핸드셰이크 시뮬레이션
def three_way_handshake():
    # 1단계: 클라이언트 -> 서버 (SYN)
    client_seq = 100
    print(f"[Client -> Server] SYN, Seq={client_seq}")

    # 2단계: 서버 -> 클라이언트 (SYN + ACK)
    server_seq = 300
    server_ack = client_seq + 1  # 101
    print(f"[Server -> Client] SYN+ACK, Seq={server_seq}, Ack={server_ack}")

    # 3단계: 클라이언트 -> 서버 (ACK)
    client_ack = server_seq + 1  # 301
    print(f"[Client -> Server] ACK, Seq={server_ack}, Ack={client_ack}")

    print("연결 수립 완료! 이제 데이터를 주고받을 수 있습니다.")

three_way_handshake()

김개발 씨가 카페에서 친구에게 전화를 거는 상황을 떠올려 봅시다. 김개발 씨가 전화를 걸면서 "여보세요?"라고 말합니다.

친구가 전화를 받아 "어, 여보세요? 나 잘 들려"라고 답합니다.

김개발 씨가 "응, 나도 잘 들려. 얘기하자"라고 확인합니다.

이제 둘은 대화를 시작할 수 있습니다. TCP의 3-way 핸드셰이크도 정확히 같은 원리입니다.

첫 번째 단계에서 클라이언트가 서버에게 SYN 세그먼트를 보냅니다. "안녕하세요, 연결하고 싶습니다"라는 의미입니다.

이때 클라이언트는 자신의 초기 순서 번호를 함께 전달합니다. 코드에서 client_seq = 100이 바로 이것입니다.

두 번째 단계에서 서버가 클라이언트에게 SYN+ACK 세그먼트를 보냅니다. 두 가지 의미가 담겨 있습니다.

SYN은 "나도 연결할 준비가 됐어요"이고, ACK는 "당신의 연결 요청을 잘 받았어요"입니다. 서버도 자신의 초기 순서 번호(server_seq = 300)를 보내고, 확인 응답 번호로는 클라이언트의 순서 번호에 1을 더한 값(101)을 보냅니다.

세 번째 단계에서 클라이언트가 ACK 세그먼트를 보냅니다. "서버의 응답을 잘 받았어요.

이제 통신을 시작합시다"라는 의미입니다. 확인 응답 번호로 서버의 순서 번호에 1을 더한 값(301)을 보냅니다.

왜 굳이 세 번이나 주고받아야 할까요? 한 번만 보내면 상대방이 받았는지 알 수 없습니다.

두 번이면 클라이언트는 서버가 응답했음을 알지만, 서버는 클라이언트가 자신의 응답을 받았는지 알 수 없습니다. 세 번을 주고받아야 양쪽 모두 상대방이 통신 준비가 되었음을 확인할 수 있습니다.

이 과정에서 양쪽의 초기 순서 번호도 교환됩니다. 이 번호들은 보통 무작위로 생성됩니다.

왜 0부터 시작하지 않을까요? 보안 때문입니다.

예측 가능한 순서 번호는 공격에 악용될 수 있습니다. 실무에서 네트워크 지연 문제를 분석할 때, 3-way 핸드셰이크에 걸리는 시간을 측정하면 유용합니다.

이 시간이 길다면 네트워크 경로에 문제가 있거나 서버가 과부하 상태일 수 있습니다. 또한 SYN Flood 공격이라는 것도 있습니다.

악의적인 공격자가 SYN 세그먼트만 대량으로 보내고 ACK를 보내지 않으면, 서버는 연결을 완료하지 못한 채 대기 상태로 리소스를 소모하게 됩니다. 이런 공격을 막기 위해 SYN 쿠키 같은 기법이 사용됩니다.

김개발 씨가 물었습니다. "그럼 웹 브라우저에서 사이트에 접속할 때마다 이 과정이 일어나는 건가요?" 박시니어 씨가 답했습니다.

"맞아요. 매번 새로운 TCP 연결을 맺을 때마다요.

그래서 HTTP/2나 HTTP/3에서는 이 오버헤드를 줄이려는 기술이 적용되어 있죠."

실전 팁

💡 - 3-way 핸드셰이크 시간(RTT)은 네트워크 성능의 중요한 지표입니다

  • netstat이나 ss 명령어로 현재 연결 상태를 확인할 수 있습니다

4. 4 way 핸드셰이크 연결 종료

김개발 씨가 서버와의 연결을 끊을 때도 뭔가 절차가 있을 것 같다는 생각이 들었습니다. "연결을 맺을 때 악수를 세 번 했으니, 끊을 때도 뭔가 있겠지?" 역시나 선배의 대답은 "4-way 핸드셰이크"였습니다.

4-way 핸드셰이크는 TCP 연결을 안전하게 종료하는 과정으로, 네 번의 메시지를 주고받습니다. 마치 전화를 끊을 때 "이제 끊을게요" - "네, 알겠어요" - "저도 끊을게요" - "네, 안녕히"라고 인사하는 것과 같습니다.

양쪽 모두 더 이상 보낼 데이터가 없음을 확인한 후에야 연결이 완전히 종료됩니다.

다음 코드를 살펴봅시다.

# 4-way 핸드셰이크 시뮬레이션
def four_way_handshake():
    # 1단계: 클라이언트 -> 서버 (FIN)
    print("[Client -> Server] FIN, Seq=1000")
    print("  클라이언트: '보낼 데이터가 없어요. 연결 끊을게요.'")

    # 2단계: 서버 -> 클라이언트 (ACK)
    print("[Server -> Client] ACK, Ack=1001")
    print("  서버: '알겠어요. 근데 저는 아직 보낼 게 남았어요.'")

    # 서버가 남은 데이터 전송...
    print("[Server -> Client] 남은 데이터 전송 중...")

    # 3단계: 서버 -> 클라이언트 (FIN)
    print("[Server -> Client] FIN, Seq=2000")
    print("  서버: '저도 다 보냈어요. 이제 끊어도 돼요.'")

    # 4단계: 클라이언트 -> 서버 (ACK)
    print("[Client -> Server] ACK, Ack=2001")
    print("  클라이언트: '알겠어요. 안녕히 가세요.'")

four_way_handshake()

TCP 연결을 종료하는 과정은 수립하는 과정보다 조금 더 복잡합니다. 왜 3번이 아니라 4번일까요?

연결을 수립할 때는 양쪽 모두 "시작합시다"라는 동일한 목적을 가지고 있습니다. 하지만 종료할 때는 상황이 다릅니다.

한쪽은 보낼 데이터가 없어서 끊고 싶은데, 다른 쪽은 아직 보낼 데이터가 남아 있을 수 있습니다. 이런 상황을 반이중 종료(half-close)라고 합니다.

TCP는 양방향 통신이기 때문에, 한쪽 방향만 먼저 닫고 다른 쪽은 열어둘 수 있습니다. 첫 번째 단계에서 클라이언트가 FIN 세그먼트를 보냅니다.

"저는 더 이상 보낼 데이터가 없어요"라는 의미입니다. 하지만 아직 받을 준비는 되어 있습니다.

두 번째 단계에서 서버가 ACK를 보냅니다. "알겠어요, 당신이 끊고 싶다는 거 확인했어요"라는 응답입니다.

이 시점에서 클라이언트에서 서버로 가는 방향은 닫혔지만, 서버에서 클라이언트로 가는 방향은 아직 열려 있습니다. 서버는 아직 보낼 데이터가 있다면 계속 보낼 수 있습니다.

클라이언트는 이 데이터를 받을 수 있습니다. 이것이 3-way가 아닌 4-way인 이유입니다.

세 번째 단계에서 서버도 모든 데이터를 다 보냈으면 FIN 세그먼트를 보냅니다. "저도 다 보냈어요.

이제 연결을 끊어도 돼요." 네 번째 단계에서 클라이언트가 마지막 ACK를 보냅니다. "알겠어요.

연결 종료 확인했습니다." 이제 양방향 모두 닫히고 연결이 완전히 종료됩니다. 여기서 중요한 점이 있습니다.

클라이언트는 마지막 ACK를 보낸 후 바로 연결을 닫지 않습니다. TIME_WAIT 상태에서 일정 시간(보통 2분) 동안 대기합니다.

왜 그럴까요? 마지막 ACK가 네트워크에서 사라질 수 있기 때문입니다.

만약 서버가 ACK를 받지 못하면 FIN을 다시 보낼 것이고, 클라이언트가 이미 연결을 닫아버렸다면 이 FIN에 응답할 수 없습니다. 실무에서 서버를 운영하다 보면 TIME_WAIT 상태의 연결이 많이 쌓이는 것을 볼 수 있습니다.

이것은 정상적인 현상이지만, 너무 많이 쌓이면 문제가 될 수 있습니다. 이런 경우 커널 파라미터를 조정하거나 연결 풀링을 사용하는 등의 방법으로 해결합니다.

김개발 씨가 물었습니다. "그럼 웹 브라우저로 페이지를 볼 때마다 이 과정이 일어나는 건가요?" 박시니어 씨가 답했습니다.

"HTTP/1.0에서는 그랬죠. 하지만 HTTP/1.1부터는 Keep-Alive로 연결을 재사용해서 이 오버헤드를 줄입니다."

실전 팁

💡 - TIME_WAIT 상태는 정상적인 것이며, 무작정 없애려고 하면 안 됩니다

  • 서버에서 TIME_WAIT가 많다면 클라이언트가 아닌 서버 측에서 연결을 먼저 끊고 있는지 확인하세요

5. TCP 상태 전이

김개발 씨가 netstat 명령어를 실행했더니 LISTEN, ESTABLISHED, TIME_WAIT 같은 상태들이 보였습니다. "이게 다 뭐지?

연결이 됐으면 됐고, 안 됐으면 안 된 거 아닌가?" 궁금증이 생겼습니다.

TCP 연결은 여러 상태를 거치며 생성되고 종료됩니다. 마치 주문한 음식이 "주문 접수 - 조리 중 - 배달 중 - 배달 완료"의 상태를 거치는 것처럼, TCP 연결도 LISTEN에서 시작해 ESTABLISHED를 거쳐 CLOSED로 끝납니다.

이 상태들을 이해하면 네트워크 문제를 진단하는 데 큰 도움이 됩니다.

다음 코드를 살펴봅시다.

# TCP 상태 정의 및 전이 시뮬레이션
class TCPState:
    CLOSED = "CLOSED"           # 연결 없음
    LISTEN = "LISTEN"           # 연결 대기 중 (서버)
    SYN_SENT = "SYN_SENT"       # SYN 보냄 (클라이언트)
    SYN_RECEIVED = "SYN_RECEIVED"  # SYN 받음 (서버)
    ESTABLISHED = "ESTABLISHED" # 연결 수립됨
    FIN_WAIT_1 = "FIN_WAIT_1"   # FIN 보냄
    FIN_WAIT_2 = "FIN_WAIT_2"   # ACK 받음
    CLOSE_WAIT = "CLOSE_WAIT"   # FIN 받음, 종료 대기
    LAST_ACK = "LAST_ACK"       # 마지막 ACK 대기
    TIME_WAIT = "TIME_WAIT"     # 연결 종료 대기 (2MSL)

# 클라이언트 상태 전이 예시
client_states = ["CLOSED", "SYN_SENT", "ESTABLISHED",
                 "FIN_WAIT_1", "FIN_WAIT_2", "TIME_WAIT", "CLOSED"]

김개발 씨는 서버 모니터링 중 이상한 현상을 발견했습니다. 특정 포트에 연결이 계속 쌓이고 있었는데, CLOSE_WAIT 상태가 비정상적으로 많았습니다.

박시니어 씨가 말했습니다. "TCP 상태를 이해하면 이 문제의 원인을 바로 알 수 있어요.

CLOSE_WAIT가 많다는 건 특정 상황을 의미하거든요." TCP 상태 전이를 음식 배달 앱에 비유해 봅시다. CLOSED는 음식점이 문을 닫은 상태입니다.

아무런 연결도 없습니다. LISTEN은 음식점이 문을 열고 손님을 기다리는 상태입니다.

서버가 특정 포트에서 연결 요청을 기다리고 있습니다. 웹 서버가 80번 포트에서 LISTEN 하고 있으면 "80번 포트로 오세요!"라고 안내하는 것과 같습니다.

SYN_SENT는 손님이 주문 버튼을 눌렀지만 아직 "주문 접수" 알림을 받지 못한 상태입니다. 클라이언트가 SYN을 보내고 응답을 기다리고 있습니다.

SYN_RECEIVED는 음식점에서 주문을 받았지만, 손님에게 확인 메시지를 보내고 응답을 기다리는 상태입니다. 서버가 SYN+ACK를 보내고 클라이언트의 ACK를 기다립니다.

ESTABLISHED는 주문이 확정되고 조리가 시작된 상태입니다. 양쪽 모두 연결이 수립되었음을 확인했고, 이제 데이터를 주고받을 수 있습니다.

연결 종료 과정에서는 더 많은 상태가 등장합니다. FIN_WAIT_1은 손님이 "취소할게요"라고 말하고 음식점의 응답을 기다리는 상태입니다.

FIN을 보내고 ACK를 기다립니다. FIN_WAIT_2는 음식점이 "알겠어요"라고 했지만, 아직 최종 정산을 안 한 상태입니다.

ACK를 받았지만 상대방의 FIN을 기다립니다. CLOSE_WAIT는 음식점이 손님의 취소 요청을 받았지만, 아직 정리할 것이 남아 있는 상태입니다.

FIN을 받고 ACK를 보냈지만, 아직 자신의 FIN을 보내지 않았습니다. LAST_ACK는 음식점이 모든 정리를 마치고 마지막 확인을 기다리는 상태입니다.

FIN을 보내고 ACK를 기다립니다. TIME_WAIT는 연결이 종료된 후 잠시 대기하는 상태입니다.

혹시 네트워크에 남아 있을 수 있는 패킷들이 완전히 사라질 때까지 기다립니다. 이제 김개발 씨의 문제로 돌아가 봅시다.

CLOSE_WAIT가 많다는 것은 무엇을 의미할까요? CLOSE_WAIT는 상대방이 FIN을 보냈는데 이쪽에서 아직 연결을 닫지 않은 상태입니다.

즉, 애플리케이션에서 소켓을 제대로 닫지 않고 있다는 뜻입니다. 코드에서 close() 호출이 누락되었거나, 예외 처리에서 소켓 정리를 하지 않고 있을 가능성이 높습니다.

박시니어 씨가 결론을 내렸습니다. "코드를 확인해봐요.

finally 블록에서 소켓을 제대로 닫고 있는지 확인해야 해요."

실전 팁

💡 - netstat -an 또는 ss -tan 명령어로 현재 연결 상태를 확인하세요

  • CLOSE_WAIT가 쌓이면 애플리케이션 코드를, TIME_WAIT가 쌓이면 네트워크 설정을 점검하세요

6. UDP 데이터그램 구조

김개발 씨가 실시간 게임 서버 개발 프로젝트에 투입되었습니다. 기존에 알던 TCP를 쓰려고 했더니, 선배가 "게임에서는 UDP를 많이 써요"라고 말합니다.

"UDP? TCP랑 뭐가 다르지?" 새로운 개념이 등장했습니다.

UDP(User Datagram Protocol)는 TCP와 달리 연결을 맺지 않고 데이터를 전송하는 프로토콜입니다. 마치 편지를 우체통에 넣는 것처럼, 상대방이 받았는지 확인하지 않고 그냥 보냅니다.

헤더가 매우 단순해서 8바이트밖에 되지 않으며, 빠른 전송이 필요할 때 사용됩니다.

다음 코드를 살펴봅시다.

import struct

# UDP 데이터그램 헤더 구조 (단 8바이트!)
class UDPDatagram:
    def __init__(self, src_port, dst_port, data):
        self.src_port = src_port    # 출발지 포트 (2바이트)
        self.dst_port = dst_port    # 목적지 포트 (2바이트)
        self.data = data
        self.length = 8 + len(data) # 헤더(8) + 데이터 길이 (2바이트)
        self.checksum = 0           # 체크섬 (2바이트, 선택)

    def to_bytes(self):
        header = struct.pack('!HHHH',
            self.src_port, self.dst_port,
            self.length, self.checksum)
        return header + self.data.encode()

# 사용 예시
packet = UDPDatagram(12345, 53, "DNS Query")
print(f"전체 길이: {packet.length}바이트")

김개발 씨는 TCP의 복잡한 세그먼트 구조를 공부한 후, UDP 데이터그램을 보고 깜짝 놀랐습니다. "이게 끝이에요?

헤더가 8바이트밖에 안 돼요?" 박시니어 씨가 웃으며 답했습니다. "네, UDP는 정말 단순해요.

그게 장점이자 단점이죠." UDP를 이해하려면 우편 시스템을 떠올리면 됩니다. TCP가 등기우편이라면, UDP는 일반 우편입니다.

등기우편은 받는 사람이 서명을 해야 하고, 분실되면 추적도 가능합니다. 하지만 일반 우편은 우체통에 넣으면 끝입니다.

상대방이 받았는지 확인할 방법이 없습니다. UDP 헤더를 살펴봅시다.

TCP의 최소 20바이트에 비해 단 8바이트입니다. 출발지 포트목적지 포트가 각각 2바이트씩 차지합니다.

TCP와 동일합니다. 길이 필드는 2바이트로, 헤더를 포함한 전체 UDP 데이터그램의 길이를 나타냅니다.

최소값은 헤더만 있는 8바이트이고, 최대값은 65535바이트입니다. 체크섬도 2바이트입니다.

IPv4에서는 선택 사항이지만, IPv6에서는 필수입니다. 이게 전부입니다.

TCP에 있던 순서 번호, 확인 응답 번호, 윈도우 크기, 제어 비트 같은 것들이 전혀 없습니다. 이렇게 단순한 구조가 왜 필요할까요?

첫 번째 이유는 속도입니다. 연결을 맺는 과정이 없으니 바로 데이터를 보낼 수 있습니다.

3-way 핸드셰이크 시간이 절약됩니다. 두 번째 이유는 오버헤드 감소입니다.

헤더가 작으니 실제 데이터를 더 많이 보낼 수 있습니다. 작은 데이터를 자주 보내는 경우에 특히 유리합니다.

세 번째 이유는 실시간성입니다. TCP는 패킷이 손실되면 재전송을 기다려야 합니다.

하지만 실시간 게임이나 영상 통화에서는 오래된 데이터보다 최신 데이터가 더 중요합니다. 1초 전 캐릭터 위치를 재전송 받느니, 그냥 무시하고 현재 위치를 받는 게 낫습니다.

실제로 UDP가 사용되는 곳을 살펴봅시다. DNS는 UDP 53번 포트를 사용합니다.

DNS 요청은 보통 한 번의 질문과 한 번의 응답으로 끝나므로, TCP처럼 연결을 맺을 필요가 없습니다. 실시간 게임은 캐릭터 위치, 총알 궤적 같은 정보를 초당 수십 번씩 보냅니다.

하나가 손실되어도 바로 다음 데이터가 오기 때문에 큰 문제가 없습니다. 영상 스트리밍도 UDP 기반인 경우가 많습니다.

화면 한 프레임이 손실되어도 다음 프레임이 바로 오기 때문에, 재전송을 기다리는 것보다 그냥 넘어가는 게 사용자 경험에 좋습니다. 김개발 씨가 고개를 끄덕였습니다.

"그래서 게임 서버에 UDP를 쓰는 거군요. 약간의 패킷 손실보다 지연 시간이 더 중요하니까요."

실전 팁

💡 - UDP를 사용할 때는 애플리케이션 레벨에서 필요한 만큼의 신뢰성을 직접 구현해야 합니다

  • QUIC(HTTP/3)은 UDP 위에 TCP의 장점을 구현한 현대적인 프로토콜입니다

7. TCP vs UDP 비교

김개발 씨가 새 프로젝트를 시작하면서 고민에 빠졌습니다. "이 서비스에는 TCP를 써야 할까, UDP를 써야 할까?" 둘 다 장단점이 있다 보니 선택이 쉽지 않습니다.

선배에게 조언을 구했습니다.

TCP는 신뢰성 있는 데이터 전송이 필요할 때, UDP는 빠른 전송이 필요할 때 선택합니다. TCP는 전화 통화처럼 연결을 맺고 대화하는 방식이고, UDP는 편지를 보내는 것처럼 일방적으로 전송하는 방식입니다.

서비스의 요구사항에 따라 적절한 프로토콜을 선택하는 것이 중요합니다.

다음 코드를 살펴봅시다.

# TCP vs UDP 특성 비교
comparison = {
    "항목":       ["TCP",              "UDP"],
    "연결 방식":   ["연결 지향",         "비연결"],
    "신뢰성":     ["보장",              "미보장"],
    "순서 보장":   ["보장",              "미보장"],
    "흐름 제어":   ["있음",              "없음"],
    "헤더 크기":   ["최소 20바이트",      "8바이트"],
    "속도":       ["상대적으로 느림",     "빠름"],
    "사용 사례":   ["웹, 이메일, 파일전송", "DNS, 게임, 스트리밍"]
}

# 선택 가이드
def choose_protocol(need_reliability, need_speed, data_size):
    if need_reliability and not need_speed:
        return "TCP - 데이터 손실이 허용되지 않는 경우"
    elif need_speed and not need_reliability:
        return "UDP - 실시간성이 중요한 경우"
    else:
        return "상황에 따라 판단 필요"

박시니어 씨가 화이트보드에 표를 그리며 설명을 시작했습니다. "TCP와 UDP를 비교하는 건 마치 택배 서비스를 선택하는 것과 같아요." TCP는 프리미엄 택배 서비스입니다.

배송 전에 받는 사람에게 전화해서 "지금 배송해도 될까요?"라고 확인합니다(3-way 핸드셰이크). 물건을 보내면 수신 확인을 받고, 혹시 파손되면 다시 보내줍니다.

여러 박스를 보내면 순서대로 도착하게 해줍니다. 안전하지만, 그만큼 시간이 걸리고 비용도 듭니다.

UDP는 일반 우편입니다. 우체통에 넣으면 끝입니다.

받았는지 확인하지 않고, 분실되어도 다시 보내주지 않습니다. 빠르고 저렴하지만, 책임도 없습니다.

이제 각 특성을 자세히 비교해 봅시다. 연결 방식에서 TCP는 연결 지향입니다.

데이터를 보내기 전에 반드시 연결을 맺어야 합니다. UDP는 비연결으로, 바로 데이터를 보낼 수 있습니다.

신뢰성에서 TCP는 데이터가 반드시 도착함을 보장합니다. 손실되면 재전송합니다.

UDP는 보장하지 않습니다. 보내긴 했지만 도착했는지 알 수 없습니다.

순서 보장에서 TCP는 보낸 순서대로 데이터를 전달합니다. 중간에 뒤바뀌어 도착해도 재조립합니다.

UDP는 순서를 보장하지 않습니다. 먼저 보낸 것이 나중에 도착할 수도 있습니다.

흐름 제어에서 TCP는 수신자의 처리 능력에 맞게 전송 속도를 조절합니다. UDP는 그런 거 없습니다.

수신자가 처리하든 말든 계속 보냅니다. 헤더 크기에서 TCP는 최소 20바이트, UDP는 8바이트입니다.

작은 데이터를 자주 보내는 경우 이 차이가 누적되면 꽤 큽니다. 그렇다면 언제 무엇을 써야 할까요?

TCP를 선택해야 하는 경우: - 웹 페이지 로딩(HTTP/HTTPS): 모든 데이터가 정확히 도착해야 합니다 - 이메일 전송(SMTP): 메일 내용이 손실되면 안 됩니다 - 파일 전송(FTP): 파일이 깨지면 안 됩니다 - 원격 접속(SSH): 명령어가 누락되면 심각한 문제가 생깁니다 UDP를 선택해야 하는 경우: - DNS 조회: 빠른 응답이 중요하고, 실패하면 다시 요청하면 됩니다 - 실시간 게임: 약간의 손실보다 지연이 더 치명적입니다 - 영상 통화: 끊기더라도 다음 프레임이 바로 옵니다 - IoT 센서 데이터: 하나 빠져도 바로 다음 것이 옵니다 최근에는 QUIC이라는 새로운 프로토콜이 주목받고 있습니다. UDP 위에서 TCP의 장점(신뢰성, 순서 보장)을 구현한 것입니다.

HTTP/3가 QUIC을 사용합니다. 연결 설정이 더 빠르고, 패킷 손실 복구도 효율적입니다.

김개발 씨가 결론을 내렸습니다. "우리 프로젝트는 실시간 게임이니까 UDP를 기본으로 하되, 중요한 데이터는 애플리케이션 레벨에서 확인 응답을 구현하면 되겠네요." 박시니어 씨가 미소를 지었습니다.

"정확해요. 실무에서는 하나만 고집하지 않고, 상황에 맞게 조합해서 쓰는 게 중요하죠."

실전 팁

💡 - 대부분의 웹 서비스는 TCP로 충분합니다. UDP는 특수한 경우에만 고려하세요

  • UDP를 쓰더라도 중요한 데이터는 애플리케이션 레벨에서 신뢰성을 보장하는 로직을 추가하세요
  • HTTP/3(QUIC)이 점점 보편화되고 있으니 관심을 가져보세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Network#TCP#UDP#Protocol#Handshake#CS

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.