이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 네트워크 › 큰 그림
계층 구조와 캡슐화
1500바이트를 보냈는데 왜 전선에는 1616바이트가 지나갈까
네트워크는 일을 계층으로 나눠 맡습니다. 보내는 쪽에서는 데이터가 위 계층에서 아래 계층으로 내려가며, 계층마다 자기 일에 필요한 정보를 적은 머리말(헤더)을 앞에 붙입니다. 이것을 캡슐화라고 합니다. 받는 쪽은 반대 순서로 머리말을 하나씩 떼어 냅니다.
일부만 실행으로 확인 · 조각 수와 크기 열 가지 조합을 Node.js 24에서 실제 머리말 바이트를 만들어 붙이고 풀어 대조. 머리말의 크기와 계층의 대응은 RFC 9293, 791, 894, 1122에 따른 설명
이 실습에 나오는 개념
- OSI 7계층
- 네트워크 통신 과정을 역할에 따라 일곱 단계로 나눈 모델이다. 아래부터 물리, 데이터 링크, 네트워크(IP), 전송(TCP/UDP), 세션, 표현, 응용(HTTP) 계층이다. 각 계층은 바로 아래 계층의 기능을 이용해 자신의 역할을 수행한다. 장비나 문제의 위치를 "몇 계층"이라고 부르는 기준이 된다.
- TCP/IP 4계층
- 인터넷이 실제로 쓰는 프로토콜들을 역할에 따라 네 단계로 나눈 모델이다. 위에서부터 응용 계층(HTTP, DNS), 전송 계층(TCP, UDP), 인터넷 계층(IP), 링크 계층(이더넷, 와이파이)이다. OSI 7계층과 견주면 응용, 표현, 세션 계층이 응용 계층 하나로, 데이터 링크와 물리 계층이 링크 계층 하나로 묶인다. 각 계층은 바로 아래 계층에 전달을 맡기고 자기 일만 하므로, 한 계층의 기술이 바뀌어도 다른 계층은 그대로 쓸 수 있다.
- 캡슐화와 역캡슐화
- 캡슐화는 보내는 쪽에서 데이터가 아래 계층으로 내려갈 때마다 그 계층의 머리말(헤더)을 앞에 붙이는 것이다. HTTP 메시지에는 TCP 머리말, IP 머리말, 이더넷 머리말이 차례로 붙으며, 각 계층은 위에서 받은 것을 내용물로만 취급하고 들여다보지 않는다. 역캡슐화는 받는 쪽에서 반대 순서로 머리말을 확인하고 떼어 내며 위 계층으로 올리는 것이다. 객체지향의 캡슐화와 이름은 같지만 다른 개념이다.
- 세그먼트, 패킷, 프레임
- 같은 데이터를 계층마다 다르게 부르는 이름이다. 응용 계층의 데이터에 TCP 머리말이 붙은 것을 세그먼트, 거기에 IP 머리말이 붙은 것을 패킷(IP 데이터그램), 다시 이더넷 머리말과 꼬리말이 붙은 것을 프레임이라고 한다. TCP 머리말에는 프로그램을 가리키는 포트 번호가, IP 머리말에는 컴퓨터를 가리키는 IP 주소가, 이더넷 머리말에는 같은 네트워크 안의 장비를 가리키는 MAC 주소가 들어 있다. 일상에서는 계층을 가리지 않고 모두 패킷이라고 부르는 일도 많다.
- MTU와 MSS
- MTU는 링크 계층이 프레임 하나에 실어 줄 수 있는 IP 패킷의 최대 크기이고, 이더넷에서는 1500바이트다. MSS는 TCP 세그먼트 하나에 실을 수 있는 데이터의 최대 크기로, MTU에서 IP 머리말과 TCP 머리말을 뺀 값이다. 머리말이 기본 크기인 20바이트씩이면 이더넷에서 MSS는 1460바이트가 된다. TCP는 보낼 데이터가 MSS보다 크면 여러 조각으로 나누어 보내고, 받는 쪽 TCP가 순서 번호를 보고 다시 이어 붙인다.
이렇게 바꿔 보세요
- 처음 설정으로 — 1500바이트를 MTU 1500으로. MTU와 같은 크기인데도 두 조각이 됩니다.
- 딱 한 조각 — 1460바이트는 TCP와 IP 머리말을 붙이면 정확히 1500바이트가 됩니다.
- 짧은 요청 — 300바이트. 나누지 않고 머리말만 붙습니다.
- 긴 데이터 — 10000바이트는 일곱 조각으로 나뉘고 조각마다 머리말이 붙습니다.
- MTU가 더 작으면 — 같은 1460바이트가 이번에는 두 조각이 됩니다.
처음 설정으로 실행하면
- 응용 계층(HTTP): 브라우저가 서버에 보낼 HTTP 메시지를 만듭니다. 크기는 1500바이트입니다. 이 계층은 "무슨 내용을 주고받을지"만 정하고, 그것이 상대에게 어떻게 닿을지는 아래 계층에 맡깁니다.
- 전송 계층(TCP): 먼저 한 번에 실어 보낼 수 있는 크기를 따집니다. 링크 계층이 한 번에 실어 주는 크기(MTU)가 1500바이트이고, 그 안에 IP 머리말 20바이트와 TCP 머리말 20바이트도 들어가야 하므로 데이터는 한 조각에 1460바이트까지 실립니다(MSS). 보낼 데이터 1500바이트는 이보다 크므로 2조각으로 나눕니다(1460, 40바이트).
- 전송 계층(TCP): 첫 조각 앞에 TCP 머리말 20바이트를 붙입니다. 이렇게 데이터 앞에 머리말을 붙여 감싸는 것이 캡슐화입니다. 머리말에는 보내는 프로그램과 받는 프로그램을 가리키는 포트 번호(51000 → 80)와, 이 조각이 전체에서 몇 번째 바이트부터인지 알려 주는 순서 번호가 들어 있습니다. 이렇게 싸인 것을 세그먼트라고 부릅니다. 1480바이트가 됐습니다.
- 인터넷 계층(IP): 세그먼트 앞에 IP 머리말 20바이트를 붙입니다. 머리말에는 보내는 컴퓨터와 받는 컴퓨터의 IP 주소(192.168.0.10 → 192.168.0.20)가 들어 있습니다. 이렇게 싸인 것을 패킷이라고 부릅니다. 1500바이트가 됐습니다. MTU 1500바이트를 꽉 채운 크기입니다.
- 링크 계층(이더넷): 패킷 앞에 이더넷 머리말 14바이트를, 뒤에 오류 검사용 꼬리말(FCS) 4바이트를 붙입니다. 머리말에는 같은 네트워크 안에서 이 데이터를 주고받는 두 장비의 MAC 주소(00:00:5e:00:53:0a → 00:00:5e:00:53:14)가 들어 있습니다. 이렇게 싸인 것을 프레임이라고 부릅니다. 1518바이트가 전선으로 나갑니다.
- 나머지 조각도 같은 순서로 쌉니다. 머리말은 조각마다 따로 붙습니다. 프레임 2개에 58바이트씩, 모두 116바이트가 덧붙었습니다. 전선으로 나간 것은 데이터 1500바이트를 합쳐 1616바이트입니다.
- 받는 쪽 링크 계층: 프레임을 받아 목적지 MAC 주소가 자기 것인지, 꼬리말의 검사값이 맞는지 확인합니다. 맞으면 이더넷 머리말과 꼬리말을 떼어 내고 안에 든 패킷을 위 계층으로 올립니다. 보낼 때 마지막에 붙인 것을 가장 먼저 뗍니다.
- 받는 쪽 인터넷 계층: IP 머리말의 목적지 IP 주소(192.168.0.20)가 자기 것인지 확인합니다. 맞으면 IP 머리말을 떼어 내고 안에 든 세그먼트를 TCP에 올립니다.
- 받는 쪽 전송 계층: TCP 머리말의 목적지 포트 80번을 보고 어느 프로그램(웹 서버)에 줄 데이터인지 정합니다. 순서 번호를 보고 조각 2개를 차례대로 이어 붙인 뒤 머리말을 떼어 냅니다.
- 받는 쪽 응용 계층: 웹 서버가 처음의 HTTP 메시지 1500바이트를 그대로 받습니다. 중간에 몇 조각으로 나뉘었는지, 어떤 머리말이 붙었다 떨어졌는지는 알지 못합니다.
실행 결과
- MTU는 데이터의 크기가 아니라 IP 머리말과 TCP 머리말까지 합친 패킷의 크기에 대한 한도입니다. 그래서 데이터는 MTU에서 40바이트를 뺀 1460바이트(MSS)까지만 한 조각에 실립니다. MTU와 같거나 조금 작은 데이터도 두 조각이 되는 이유입니다.
- 계층마다 보는 주소가 다릅니다. 전송 계층은 포트 번호로 프로그램을, 인터넷 계층은 IP 주소로 컴퓨터를, 링크 계층은 MAC 주소로 같은 네트워크 안의 바로 다음 장비를 가리킵니다. 각 계층은 자기 머리말만 읽고 그 안쪽은 내용물로만 취급하므로, 한 계층의 기술을 바꿔도(유선을 무선으로, HTTP를 다른 프로토콜로) 나머지 계층은 그대로 쓸 수 있습니다.
코드
// Node.js. 머리말을 그 크기의 바이트 덩어리로 만들어 차례로 붙여 봅니다 (내용은 비워 두고 크기만 봅니다).
const mtu = 1500;
const message = Buffer.alloc(1500); // HTTP 메시지 (응용 계층)
const tcpHeader = Buffer.alloc(20); // 포트 번호, 순서 번호 등
const ipHeader = Buffer.alloc(20); // IP 주소 등
const ethHeader = Buffer.alloc(14); // MAC 주소 등
const fcs = Buffer.alloc(4); // 오류 검사값 (프레임의 맨 뒤)
const mss = mtu - ipHeader.length - tcpHeader.length; // 한 조각에 실을 수 있는 데이터
let total = 0;
for (let at = 0; at < message.length; at += mss) {
const data = message.subarray(at, at + mss); // 조각 하나
const segment = Buffer.concat([tcpHeader, data]); // 전송 계층: 세그먼트
const packet = Buffer.concat([ipHeader, segment]); // 인터넷 계층: 패킷
const frame = Buffer.concat([ethHeader, packet, fcs]); // 링크 계층: 프레임
console.log(data.length, segment.length, packet.length, frame.length);
total += frame.length;
}
console.log(total); // 전선으로 나간 전체 크기
이 모형의 전제
머리말의 크기는 옵션이 없을 때의 기본 크기입니다. TCP 머리말 20바이트는 RFC 9293 3.1절, IPv4 머리말 20바이트는 RFC 791 3.1절, 이더넷 머리말 14바이트(목적지 MAC 6 + 출발지 MAC 6 + 종류 2)와 꼬리말 FCS 4바이트는 IEEE 802.3, 이더넷의 MTU 1500바이트는 RFC 894, MSS가 MTU에서 40바이트를 뺀 값이라는 것은 RFC 9293 3.7.1절을 따랐습니다. TCP/IP의 네 계층과 세그먼트, 패킷(IP 데이터그램), 프레임이라는 이름은 RFC 1122 1.1.3절과 1.3.3절에 있고, OSI 7계층과의 대응은 표준이 정한 것이 아니라 교재에서 널리 쓰는 짝짓기입니다. 실행으로 확인한 것은 계산입니다: 열 가지 조합마다 Node.js 24에서 실제 HTTP 요청 글자를 만들고, 표준의 자리 배치대로 TCP, IPv4, 이더넷 머리말과 검사값을 바이트로 만들어 붙인 뒤 조각 수와 크기를 재어 대조했으며, 그 프레임들을 다시 풀어 이어 붙인 것이 원래 메시지와 같은지, 같은 메시지를 127.0.0.1의 TCP 연결로 보냈을 때 받는 프로그램이 같은 바이트를 받는지도 확인했습니다. 운영체제가 실제로 전선에 내보내는 프레임을 잡아서 본 것은 아닙니다. 실제로는 연결을 맺을 때 양쪽이 MSS를 서로 알려 주고, TCP 옵션(시간 기록 12바이트 등)이 붙으면 한 조각의 데이터가 그만큼 줄며, 랜카드가 조각내기를 대신하는 설정에서는 패킷 분석 도구에 나누기 전의 큰 덩어리가 보이기도 합니다. 패킷 분석 도구는 보통 FCS를 빼고 보여 주므로 가장 큰 프레임이 1514바이트로 보입니다. 프레임 앞의 동기 신호 8바이트와 프레임 사이의 간격은 세지 않았습니다. 데이터가 6바이트보다 작은 조각에는 이더넷의 최소 크기를 채우는 바이트가 더 붙는데, 그런 조각이 생기지 않는 크기만 골랐습니다. 두 컴퓨터는 같은 네트워크에 있다고 두었습니다. 다른 네트워크라면 목적지 MAC 주소는 게이트웨이의 것이 되고, 라우터를 지날 때마다 이더넷 머리말만 새로 바뀝니다. HTTPS라면 HTTP 메시지가 TLS로 암호화된 뒤 TCP에 실립니다. 포트 51000과 문서용으로 예약된 MAC 주소 대역 00-00-5E-00-53-xx(RFC 7042, 지금은 RFC 9542)의 주소는 예시입니다.
앞 실습: 주소 입력부터 응답까지 · 다음 실습: URL의 구조