본문으로 건너뛰기

안동민 개발노트

본문 시작

캡슐화와 역캡슐화

응용 데이터에 TCP·IP·이더넷 제어 정보가 붙고 벗겨지는 과정과 PDU·오버헤드를 실제 패킷으로 확인합니다.

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

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

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

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

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


보내는 쪽: 캡슐화

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

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

캡슐화는 내려갈수록 바깥 이름과 헤더가 바뀐다

응용 데이터 자체는 안쪽에 남고, 각 계층이 자기 판단에 필요한 정보를 바깥쪽에 덧붙입니다.

  1. 응용 · Message

    HTTP 메시지 안에 Hello라는 응용 데이터를 담습니다.

  2. 전송 · Segment

    port와 seq를 담은 TCP 헤더가 HTTP 메시지 바깥에 붙습니다.

  3. 인터넷 · Packet

    src와 dst를 담은 IP 헤더가 TCP 헤더와 HTTP 메시지 바깥에 붙습니다.

  4. 링크 · Frame

    MAC을 담은 Ethernet 헤더와 오류 검사용 FCS가 IP 패킷을 감쌉니다.

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

브라우저가 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 제외)

프레임의 앞 14B가 한 홉의 전달 규칙을 정한다

목적지와 출발지 MAC은 링크 안의 전달 대상을, EtherType은 뒤따르는 패킷을 어느 프로토콜로 해석할지를 알려 줍니다.

  1. 목적지 MAC

    byte 0–5 · 먼저 확인 목적지 MAC 6B 이 링크에서 프레임을 받을 인터페이스를 가리킨다.

  2. 출발지 MAC

    byte 6–11 · 보낸 쪽 기록 출발지 MAC 6B 현재 링크에서 프레임을 보낸 인터페이스를 가리킨다.

  3. EtherType

    byte 12–13 EtherType 2B IPv4·IPv6·ARP 등 상위 프로토콜을 선택한다.

  4. Payload

    변 길이 EtherType이 가리킨 IP 패킷이나 ARP 메시지가 이어진다.

  5. FCS

    4B · 맨 뒤 전송 중 비트 오류를 검출한다.

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

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

IPv4 헤더 (20~60바이트)

필드크기설명
Version4비트IP 버전 (4)
IHL4비트헤더 길이 (보통 5 = 20바이트)
Total Length2바이트IP 패킷 전체 길이
TTL1바이트최대 홉 수 (보통 64 또는 128)
Protocol1바이트상위 프로토콜 (6=TCP, 17=UDP)
Source IP4바이트출발지 IP
Destination IP4바이트목적지 IP

TCP 헤더 (20~60바이트)

필드크기설명
Source Port2바이트출발지 포트 (0~65535)
Destination Port2바이트목적지 포트
Sequence Number4바이트바이트 순서 번호
Acknowledgment4바이트확인 응답 번호
Data Offset4비트TCP 헤더 길이. 옵션이 있으면 헤더가 길어짐
Flags고정 비트 필드SYN, ACK, FIN, RST 등 제어 비트
Window Size2바이트수신 버퍼 여유 공간
Checksum2바이트무결성 검증

받는 쪽: 역캡슐화

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

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

역캡슐화는 바깥 포장을 확인하고 안쪽 데이터만 올린다

수신자는 링크 계층에서 시작해 각 헤더를 검사하고, 문제가 없으면 그 계층의 포장을 벗겨 위로 넘깁니다.

  1. 링크

    목적지 MAC과 FCS를 확인합니다. 벗김: Ethernet header, FCS 올림: IP packet

  2. 인터넷

    목적지 IP와 TTL, 프로토콜 번호를 확인합니다. 벗김: IP header 올림: TCP segment

  3. 전송

    목적지 포트와 순서 번호를 확인합니다. 벗김: TCP header 올림: byte stream

  4. 응용

    TLS 복호화 뒤 HTTP 메시지를 파싱합니다. 처리: GET /, Host header 결과: 요청 라우팅

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

이더넷 헤더를 읽어 목적지 MAC 주소가 자신의 것인지 확인하고, 트레일러의 FCS로 오류를 검사합니다.

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

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

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

전송 계층이 TCP 헤더를 읽어 목적지 포트 번호를 확인하고, 해당 포트를 사용하는 프로세스에게 바이트 스트림을 전달합니다.

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

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


PDU 이름 정리

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

계층PDU 이름구성
응용메시지 (Message)순수 애플리케이션 데이터
전송세그먼트 (Segment) / 데이터그램TCP 헤더 + 데이터 / UDP 헤더 + 데이터
인터넷패킷 (Packet)IP 헤더 + 세그먼트
네트워크 인터페이스프레임 (Frame)L2 헤더 + 패킷 + 트레일러
물리비트 (Bit)0과 1의 전기/광 신호

일상적으로 패킷이라는 단어를 계층 구분 없이 사용하는 경우가 많지만, 정확히 말하면 패킷은 인터넷 계층의 PDU입니다.


크기 오버헤드 계산

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 알고리즘은 이런 작은 세그먼트가 과도하게 많이 생기는 것을 줄이려는 최적화 중 하나입니다.


중간 라우터에서의 처리

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

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

라우터는 L2를 갈아끼우고 L3 목적지는 유지한다

프레임은 홉마다 새로 만들어지지만, 일반 라우팅에서는 IP 목적지와 TCP 데이터가 그대로 지나갑니다.

  1. 들어온 프레임

    L2 Header A dst MAC = 라우터 IP Header dst IP = 서버 TCP + Data 그대로 통과

  2. 2

    IP로 다음 홉 선택

  3. 3

    새 L2 생성

  4. 나가는 프레임

    L2 Header B dst MAC = 다음 홉 IP Header dst IP = 서버 TCP + Data 그대로 통과

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

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

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

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

일반 라우팅에서는 IP 주소가 종단 간 유지되고 MAC 주소는 홉마다 바뀝니다.

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


Wireshark로 실제 확인하기

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

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는 보통 캡처 결과에 나타나지 않습니다.

캡처 바이트는 계층별 범위로 읽는다

Wireshark에서 계층을 클릭하면 그 계층이 차지하는 바이트 범위가 강조됩니다.

  1. L2

    목적지 MAC, 출발지 MAC 같은 링크의 다음 장비가 맞는가

  2. L3

    목적지 IP, TTL 라우팅과 홉 수가 정상인가

  3. L4

    포트, SYN/ACK/RST 연결이 열리고 유지되는가

  4. L7

    Host, Path, 상태 코드 응용 요청과 응답이 올바른가

오프셋내용계층
0x0000~0x000dMAC 주소 + 이더타입네트워크 인터페이스
0x000e~0x0021IP 버전, TTL, 주소 등인터넷
0x0022~0x0035포트, 순서번호, 플래그전송
0x0036~HTTP 요청 데이터응용

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

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


Python으로 직접 패킷 파싱

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,0x34,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 주소, 이더타입tcpdump -e브로드캐스트 폭풍, ARP 문제
L3IP 주소, TTLping, tracerouteTTL=0 (라우팅 루프), ICMP 도달불가
L4포트, TCP 플래그ss, netstatSYN만 보이고 SYN-ACK 없음 (방화벽)
L7HTTP 상태, DNS 응답curl, dig503 에러, NXDOMAIN
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

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

트러블슈팅은 아래 계층부터 증거를 좁힌다

응답이 느리거나 끊길 때는 응용 로그만 보지 말고, 링크에서 응용까지 한 단계씩 확인합니다.

  1. L2

    MAC, EtherType, ARP, 인터페이스 drop을 확인합니다. tcpdump -e arp -n

  2. L3

    IP 주소, TTL, 라우팅 경로와 ICMP 오류를 확인합니다. ping traceroute

  3. L4

    포트, 연결 상태, SYN/ACK/RST, 재전송을 확인합니다. ss -tn netstat

  4. L7

    DNS, TLS, HTTP 상태 코드와 응답 헤더를 확인합니다. dig curl -I

열 구성 · 증상 · 먼저 볼 계층 · 대표 신호
증상먼저 볼 계층대표 신호
게이트웨이 불통L2 / L3ARP 실패, default route 없음
포트 연결 안 됨L4SYN만 반복, RST, timeout
응답 코드 오류L7503, NXDOMAIN, TLS alert

캡슐화와 역캡슐화에서는 계층 위치, 주소/식별자, 캡처에서 보이는 신호를 확인합니다.

캡슐화 점검은 위치, 주소, 캡처, 홉 변화를 함께 본다

계층별 포장이 보이면 어디서 문제가 생겼는지 더 빨리 좁힐 수 있습니다.

  1. 위치

    현재 보고 있는 값이 L2, L3, L4, L7 중 어느 헤더에 있는지 먼저 구분합니다.

  2. 주소

    MAC은 홉마다 바뀌고, IP는 일반 라우팅에서 종단 간 유지됩니다.

  3. 캡처

    Wireshark의 하이라이트 범위로 어느 계층 바이트인지 확인합니다.

  4. 라우터

    프레임은 새로 만들지만 TCP 헤더와 응용 데이터는 보통 그대로 통과합니다.