안동민 개발노트

본문 시작

캡슐화와 역캡슐화

응용 데이터에 TCP·IP·이더넷 제어 정보가 붙고 벗겨지는 과정과 PDU·오버헤드를 가상 바이트 예제와 캡처 절차로 확인합니다.

계층 모델을 배웠으니, 이제 핵심 질문에 답할 수 있어야 합니다.

계층들 사이에서 데이터는 정확히 어떤 방식으로 전달될까요?

웹 브라우저에서 Hello라는 텍스트를 서버에 보낸다고 합시다.

이 텍스트가 상대방에게 도달하려면 응용 계층에서 시작하여 아래 계층으로 내려가며 각 계층의 정보가 추가되고, 수신 측에서는 아래 계층부터 올라가며 그 정보를 하나씩 벗깁니다.

이 과정이 바로 캡슐화(Encapsulation)와 역캡슐화(Decapsulation)입니다.


보내는 쪽: 캡슐화

캡슐화는 데이터가 상위 계층에서 하위 계층으로 내려가면서, 각 계층이 자기 계층에서 필요한 제어 정보를 덧붙이는 과정입니다.

대부분은 앞쪽의 헤더(Header)이지만, 이더넷처럼 뒤쪽에 오류 검사용 트레일러(Trailer)를 붙이는 경우도 있습니다.

하위 계층 페이로드에 포함되는 상위 PDU

하위 계층 페이로드에 포함되는 상위 PDU

계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.송신: 위에서 아래로 감싸기응용 데이터TCP 헤더응용 데이터IPv4 헤더TCP 헤더응용 데이터Ethernet헤더IPv4 헤더TCP 헤더응용 데이터FCS수신: 아래에서 위로 바깥 전달 정보 처리
계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.Ethernet 프레임의 포함 관계Ethernet 헤더IPv4 헤더TCP 헤더응용 데이터FCS

IPv4/TCP를 Ethernet에 싣는 개념도이며 크기 비율은 실제 바이트 수와 다릅니다. 한 응용 메시지가 한 TCP 세그먼트에 대응한다는 뜻은 아닙니다.

응용 계층에서 시작합니다.

브라우저가 HTTP 요청 메시지를 생성합니다.

여기에는 요청 메서드, URL, 헤더, 바디 등이 포함됩니다.

이 시점의 데이터를 메시지(Message)라고 부릅니다.

이 메시지가 전송 계층으로 내려갑니다.

TCP는 응용 데이터를 바이트 스트림으로 받아 전송할 부분에 TCP 헤더를 붙입니다. 한 메시지가 여러 세그먼트로 나뉘거나 여러 메시지의 바이트가 한 세그먼트에 실릴 수 있습니다.

TCP 헤더에는 출발지/목적지 포트 번호, 순서 번호(Sequence Number), 확인 번호(Acknowledgment Number), 윈도우 크기 등이 담깁니다.

이 결과물을 세그먼트(Segment)라고 부릅니다.

세그먼트가 인터넷 계층으로 내려갑니다.

IP는 세그먼트 앞에 IP 헤더를 붙입니다.

IP 헤더에는 출발지/목적지 IP 주소, TTL(Time To Live), 프로토콜 번호 등이 담깁니다.

이 결과물을 패킷(Packet)이라고 부릅니다.

패킷이 네트워크 인터페이스 계층으로 내려갑니다.

이더넷은 패킷 앞에 이더넷 헤더(목적지 MAC, 출발지 MAC, EtherType)를, 뒤에는 트레일러(FCS: 오류 검출 코드)를 붙입니다.

이 최종 결과물을 프레임(Frame)이라고 부릅니다.

프레임은 최종적으로 비트(Bit)로 변환되어 물리적 매체(케이블, 전파)를 통해 전송됩니다.


각 계층 헤더의 구조

이더넷 헤더 (14바이트, FCS 제외)

이더넷 프레임의 기본 헤더는 목적지 MAC 6바이트, 출발지 MAC 6바이트, EtherType 2바이트로 구성됩니다.

프레임 끝에는 FCS 4바이트가 붙지만, 일반적인 패킷 캡처 도구에서는 NIC나 드라이버가 FCS를 제거한 뒤 전달하는 경우가 많습니다.

IPv4 헤더 (20~60바이트)

필드크기·의미
버전Version, 4비트. IPv4는 4.
헤더 길이IHL, 4비트. 32비트 단위이며 보통 5 = 20바이트.
전체 길이Total Length, 2바이트. IP 헤더와 페이로드를 합한 길이.
수명 제한TTL, 1바이트. 라우터를 지날 때 감소하며 0이면 전달하지 않음. 초기값은 시스템 설정에 따라 다름.
상위 규약Protocol, 1바이트. 6 = TCP, 17 = UDP.
출발지Source IP, 4바이트. 출발지 IPv4 주소.
목적지Destination IP, 4바이트. 목적지 IPv4 주소.

TCP 헤더 (20~60바이트)

필드크기·의미
출발지 포트Source Port, 2바이트. 0~65535 범위.
목적지 포트Destination Port, 2바이트. 목적지의 소켓 식별에 사용.
순서 번호Sequence Number, 4바이트. 바이트 스트림의 위치.
확인 번호Acknowledgment, 4바이트. 확인 응답 번호.
헤더 길이Data Offset, 4비트. 옵션이 있으면 TCP 헤더가 길어짐.
제어 플래그Flags, 비트 필드. SYN·ACK·FIN·RST 등.
수신 윈도Window Size, 2바이트. 수신자가 광고하는 윈도 값이며 협상한 스케일이 적용될 수 있음.
오류 검사Checksum, 2바이트. 전송 오류 검출용이며 암호학적 무결성 보장은 아님.

받는 쪽: 역캡슐화

수신 측에서는 큰 흐름상 반대 방향의 처리가 일어납니다.

다만 실제 수신 경로에는 오류가 있는 프레임을 버리거나, IP 조각을 재조립하거나, TCP 바이트 스트림을 다시 맞추거나, TLS를 복호화하는 단계가 추가될 수 있습니다.

물리적 신호가 도착하면 네트워크 인터페이스 계층이 비트를 프레임으로 복원합니다.

일반적인 수신 필터는 목적지 MAC, 허용된 브로드캐스트·멀티캐스트 등을 확인하고 FCS로 전송 오류를 검사합니다. 캡처용 promiscuous 모드 등에서는 수신 조건이 달라질 수 있습니다.

문제가 없으면 이더넷 헤더와 트레일러를 벗겨내고, 안에 있는 패킷을 인터넷 계층으로 올려보냅니다.

인터넷 계층이 IP 헤더를 읽어 목적지 IP 주소를 확인하고, IP 헤더를 벗겨냅니다.

안에 있는 세그먼트를 전송 계층으로 올려보냅니다.

전송 계층은 출발지·목적지 주소와 포트 등으로 TCP 연결을 구분하고 해당 소켓에 바이트 스트림을 전달합니다.

TCP 세그먼트 경계와 HTTP 메시지 경계가 항상 일치하는 것은 아니며, HTTPS라면 TLS 복호화 뒤에 HTTP 메시지를 볼 수 있습니다.

응용 계층에서 애플리케이션(예: 웹 서버)이 HTTP 메시지를 파싱하여 요청을 처리합니다.


PDU 이름 정리

각 계층에서의 데이터 단위를 PDU(Protocol Data Unit)라고 합니다.

계층PDU와 구성
응용메시지: 애플리케이션이 해석하는 데이터.
전송TCP 세그먼트: TCP 헤더 + 데이터. UDP 데이터그램: UDP 헤더 + 데이터.
인터넷IP 패킷: IP 헤더 + 상위 계층 데이터.
링크프레임: L2 헤더 + 페이로드. Ethernet에서는 FCS 같은 트레일러도 포함.
물리비트: 전기·빛·전파 신호로 전달.

패킷은 넓은 의미로도 쓰입니다. 이 절에서는 계층을 구분하기 위해 IP 패킷, TCP 세그먼트, Ethernet 프레임이라고 부릅니다.


크기 오버헤드 계산

아래 계산은 VLAN 태그와 IP/TCP 옵션이 없는 프레임을 기준으로 합니다. preamble·IFG·TLS·응용 헤더를 제외한 프레임 내 TCP 페이로드 비율이며 실측 처리량이 아닙니다.

overhead_calculator.py
"""캡슐화로 인한 오버헤드를 계산"""

ethernet_header = 14    # 목적지MAC(6) + 출발지MAC(6) + 이더타입(2)
ip_header = 20          # 기본 IPv4 헤더
tcp_header = 20         # 기본 TCP 헤더 (옵션 제외)
fcs = 4                 # Frame Check Sequence
ethernet_min_frame = 64 # 목적지 MAC부터 FCS까지의 최소 프레임 크기

total_overhead = ethernet_header + ip_header + tcp_header + fcs

# 일반적인 이더넷 MTU = 1500 바이트 (IP 패킷 최대 크기)
mtu = 1500
max_tcp_payload = mtu - ip_header - tcp_header  # MSS

print(f"=== 캡슐화 오버헤드 ===")
print(f"이더넷 헤더: {ethernet_header} bytes")
print(f"IP 헤더:     {ip_header} bytes")
print(f"TCP 헤더:    {tcp_header} bytes")
print(f"FCS:         {fcs} bytes")
print(f"총 오버헤드:  {total_overhead} bytes")
print()
print(f"이더넷 MTU:  {mtu} bytes")
print(f"TCP MSS:     {max_tcp_payload} bytes")
print(f"최대 프레임:  {mtu + ethernet_header + fcs} bytes")
print(f"데이터 효율:  {max_tcp_payload/(mtu+ethernet_header+fcs)*100:.1f}%")
print()

# 작은 패킷의 비효율
small_data = 1   # 1바이트 데이터 (예: Telnet 키 입력)
small_frame_without_padding = small_data + ip_header + tcp_header + ethernet_header + fcs
small_frame = max(ethernet_min_frame, small_frame_without_padding)
padding = small_frame - small_frame_without_padding
print(f"=== 작은 패킷의 비효율 ===")
print(f"1바이트 전송 시 프레임 크기: {small_frame} bytes")
print(f"이더넷 padding: {padding} bytes")
print(f"데이터 효율: {small_data/small_frame*100:.1f}%")
print(f"→ 작은 TCP 세그먼트를 줄이려는 최적화가 필요한 이유")

1바이트 데이터를 보내더라도 이더넷 최소 프레임 크기 때문에 목적지 MAC부터 FCS까지 최소 64바이트가 전송됩니다.

데이터 효율은 약 1.6%에 불과합니다.

TCP의 Nagle 알고리즘은 작은 세그먼트를 묶으려는 최적화 중 하나이며, 응용의 지연 요구와 함께 판단해야 합니다.


중간 라우터에서의 처리

캡슐화와 역캡슐화는 송수신 양쪽에서만 일어나는 것이 아닙니다.

중간에 있는 라우터에서도 부분적인 역캡슐화와 재캡슐화가 일어납니다.

라우터 경계에서 바뀌는 링크 주소

라우터 경계에서 바뀌는 링크 주소

라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC라우터서버첫 링크MAC: PC → 라우터 AIP: PC → 서버두 번째 링크MAC: 라우터 B → 서버IP: PC → 서버라우터: TTL 감소 · IPv4 헤더 체크섬 갱신
라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC첫 링크MAC: PC → 라우터 AIP: PC → 서버라우터두 번째 링크MAC: 라우터 B → 서버IP: PC → 서버서버TTL 감소 · IPv4 헤더 체크섬 갱신

NAT·터널이 없는 두 Ethernet 링크의 예시입니다. A와 B는 라우터의 서로 다른 인터페이스입니다.

라우터는 프레임을 수신하면 링크 계층 헤더를 벗기고 IP 헤더에서 목적지 IP를 확인합니다.

라우팅 테이블을 조회해 다음 홉을 결정한 뒤, 다음 링크에 맞는 새로운 L2 헤더를 붙여 프레임을 재생성합니다.

다음 링크가 이더넷이라면 이때 다음 홉의 MAC 주소가 들어갑니다.

핵심은 일반적인 라우터가 L3(IP)까지 확인해 다음 홉을 결정하고, TCP 헤더나 HTTP 데이터는 그대로 통과시킨다는 것입니다.

NAT·터널이 없는 일반 IP 전달에서는 종단 IP 주소가 유지됩니다. Ethernet 링크가 라우터 경계에서 바뀌면 새 링크의 MAC 주소를 사용하며, 같은 L2 안의 스위치를 지날 때마다 교체하는 것은 아닙니다. IPv4 라우터는 TTL을 감소시키고 헤더 체크섬도 갱신합니다.

다만 NAT, 방화벽, 프록시, 터널링 장비는 이보다 더 깊은 계층의 정보를 보거나 주소를 바꿀 수 있습니다.


Wireshark로 실제 확인하기

Wireshark는 네트워크 인터페이스를 통해 오가는 패킷을 캡처하여 각 계층의 헤더를 시각적으로 보여주는 프로그램입니다.

아래는 권한과 대상 인터페이스를 준비한 Linux 환경의 캡처 명령 예시입니다. 이 교재가 실제 실행한 캡처 결과는 아니며 eth0와 주소는 대상에 맞게 확인해야 합니다. 포트 80 필터가 HTTP 프로토콜을 검증하는 것은 아닙니다.

tcpdump_capture.sh
# Wireshark 대신 CLI로 패킷 캡처 (서버 환경)
# HTTP 트래픽 캡처 (80번 포트)
sudo tcpdump -i eth0 port 80 -c 5 -nn

# 헥스 덤프로 헤더 구조 확인
sudo tcpdump -i eth0 port 80 -c 1 -XX

# 특정 호스트와의 통신만 캡처
sudo tcpdump -i eth0 host 93.184.216.34 -c 10

# pcap 파일로 저장 (나중에 Wireshark에서 열기)
sudo tcpdump -i eth0 -w capture.pcap -c 100

tcpdump의 -XX 옵션으로 캡처한 패킷의 헥스 덤프를 보면, 캡슐화 구조를 바이트 단위로 확인할 수 있습니다.

단, 앞에서 언급한 것처럼 이더넷 FCS는 보통 캡처 결과에 나타나지 않습니다.

다음 범위는 VLAN 태그가 없는 Ethernet + 옵션 없는 IPv4/TCP를 가정합니다. 실제 헤더 길이는 필드를 읽어 결정해야 하며 HTTPS의 암호화된 바이트를 평문 HTTP로 해석할 수는 없습니다.

  • 링크 헤더: 0x0000~0x000d, MAC 주소와 EtherType.
  • IPv4 헤더: 0x000e~0x0021, 버전·TTL·주소 등.
  • TCP 헤더: 0x0022~0x0035, 포트·순서 번호·플래그 등.
  • TCP 페이로드: 0x0036부터. 평문 HTTP 데이터가 있다면 이 구간에서 시작할 수 있습니다.

Wireshark에서는 각 계층을 클릭하면 해당 바이트가 하이라이트됩니다.

캡처된 프레임 안에서는 계층별 헤더와 페이로드가 어떤 바이트 범위를 차지하는지 확인할 수 있습니다.


Python으로 직접 패킷 파싱

아래는 직접 만든 바이트 배열에서 필드를 읽는 교육용 예제입니다. IP 전체 길이는 20바이트 IPv4 + 20바이트 TCP = 40바이트이며 TCP 페이로드는 없습니다. 체크섬은 계산하지 않았고 padding·FCS도 없으므로 실제 회선에 보낼 유효 프레임이 아닙니다.

파서는 EtherType·IP 버전·입력 길이·체크섬·단편화 등을 검증하지 않습니다. 특히 parse_ipv4는 Total Length로 페이로드 끝을 제한하지 않으므로 임의 캡처 파일의 안전한 일반 파서로 사용할 수 없습니다. 실제 네트워크 연결 없이 이 고정 배열의 필드 관계만 확인합니다.

parse_ethernet_frame.py
"""이더넷 프레임을 바이트 단위로 파싱하는 예시"""
import struct

def parse_ethernet(raw_bytes):
    """이더넷 헤더 파싱 (14바이트)"""
    dst_mac = raw_bytes[0:6]
    src_mac = raw_bytes[6:12]
    ether_type = struct.unpack("!H", raw_bytes[12:14])[0]

    dst = ":".join(f"{b:02x}" for b in dst_mac)
    src = ":".join(f"{b:02x}" for b in src_mac)

    protocols = {0x0800: "IPv4", 0x86DD: "IPv6", 0x0806: "ARP"}
    proto = protocols.get(ether_type, f"0x{ether_type:04x}")

    print(f"[이더넷] {src} → {dst} ({proto})")
    return raw_bytes[14:]  # 페이로드 반환

def parse_ipv4(raw_bytes):
    """IPv4 헤더 파싱 (20바이트)"""
    version_ihl = raw_bytes[0]
    ihl = (version_ihl & 0x0F) * 4  # 헤더 길이
    total_length = struct.unpack("!H", raw_bytes[2:4])[0]
    ttl = raw_bytes[8]
    protocol = raw_bytes[9]

    src_ip = ".".join(str(b) for b in raw_bytes[12:16])
    dst_ip = ".".join(str(b) for b in raw_bytes[16:20])

    proto_names = {6: "TCP", 17: "UDP", 1: "ICMP"}
    proto = proto_names.get(protocol, str(protocol))

    print(f"[IPv4] {src_ip} → {dst_ip} (TTL={ttl}, {proto})")
    print(f"       헤더: {ihl}B, 전체: {total_length}B")
    return raw_bytes[ihl:]  # 페이로드 반환

def parse_tcp(raw_bytes):
    """TCP 헤더 파싱 (20바이트~)"""
    src_port = struct.unpack("!H", raw_bytes[0:2])[0]
    dst_port = struct.unpack("!H", raw_bytes[2:4])[0]
    seq_num = struct.unpack("!I", raw_bytes[4:8])[0]
    ack_num = struct.unpack("!I", raw_bytes[8:12])[0]
    data_offset = (raw_bytes[12] >> 4) * 4
    flags = raw_bytes[13]

    flag_str = []
    if flags & 0x02: flag_str.append("SYN")
    if flags & 0x10: flag_str.append("ACK")
    if flags & 0x01: flag_str.append("FIN")
    if flags & 0x04: flag_str.append("RST")
    if flags & 0x08: flag_str.append("PSH")

    print(f"[TCP] :{src_port} → :{dst_port}")
    print(f"      SEQ={seq_num}, ACK={ack_num}")
    print(f"      플래그: {', '.join(flag_str)}")
    return raw_bytes[data_offset:]  # 페이로드 반환

# 예시: 가상 패킷 파싱
sample_frame = bytes([
    # 이더넷 헤더 (14B)
    0xff,0xff,0xff,0xff,0xff,0xff,  # 목적지 MAC
    0x00,0x1a,0x2b,0x3c,0x4d,0x5e,  # 출발지 MAC
    0x08,0x00,                       # IPv4
    # IPv4 헤더 (20B)
    0x45,0x00,0x00,0x28,0x00,0x00,0x40,0x00,
    0x40,0x06,0x00,0x00,             # TTL=64, TCP
    0xc0,0xa8,0x01,0x64,             # 192.168.1.100
    0x5d,0xb8,0xd8,0x22,             # 93.184.216.34
    # TCP 헤더 (20B)
    0xcc,0x95,0x00,0x50,             # 포트 52373 → 80
    0x00,0x00,0x00,0x01,             # SEQ=1
    0x00,0x00,0x00,0x00,             # ACK=0
    0x50,0x02,0xff,0xff,0x00,0x00,0x00,0x00,  # SYN
])

ip_data = parse_ethernet(sample_frame)
tcp_data = parse_ipv4(ip_data)
payload = parse_tcp(tcp_data)
print(f"\n페이로드: {len(payload)} bytes")

실무 트러블슈팅에서의 계층별 접근

패킷 캡처에서 문제를 진단할 때, 각 계층을 순서대로 확인합니다.

계층확인할 정보·도구와 해석
L2MAC·EtherType, tcpdump -e. 브로드캐스트 증가와 ARP 문제를 확인합니다.
L3IP·TTL, ping·traceroute. TTL 소진은 루프 외에 긴 경로 등도 가능하며 ICMP 도달 불가의 코드·출처를 함께 봅니다.
L4포트·TCP 플래그, ss·netstat. SYN만 보일 때는 서버·경로·반환 경로·필터링 등 여러 원인이 가능합니다.
L7HTTP 상태·DNS 응답, curl·dig. 503·NXDOMAIN의 의미와 해당 서비스 상태를 확인합니다.

다음 Linux 명령들은 계층별 조회 예시입니다. 고정 IP와 example.com이 같은 목적지라는 전제는 없으므로 서로의 성공·실패를 같은 연결의 증거로 결합하지 않습니다.

layer_debug.sh
# L2: ARP 테이블에서 게이트웨이 MAC 확인
arp -n | grep "$(ip route | grep default | awk '{print $3}')"

# L3: 목적지까지 경로 확인
traceroute -n 93.184.216.34

# L4: TCP 연결 상태 확인
ss -tn state established | head -20

# L7: HTTP 응답 확인
curl -I https://example.com 2>/dev/null | head -5

다음 장에서는 이 계층 구조의 가장 아래, 물리적 세계에서 데이터가 어떻게 전달되는지를 살펴봅니다.