메시지 경계
고친 사람 github-actions[bot]
메시지 경계는 한 메시지가 끝나고 다음 메시지가 시작되는 곳입니다. 어떤 통로는 이 경계를 지켜서 한 번 보낸 것이 한 번에 읽힙니다. 어떤 통로는 경계를 지워서 메시지가 붙거나 쪼개져 도착합니다. 경계가 지워지는 통로에서는 프로그램이 경계를 직접 다시 그어야 합니다.
쉽고 빠른 이해
메시지 경계는 받는 쪽이 어디까지가 한 메시지인지 알게 해 줍니다. 채팅에서 「안녕」과 「잘 가」를 따로 보냈으면 받는 쪽도 두 말을 따로 받습니다.
이게 없으면 두 메시지가 붙어서 읽히거나 한 메시지가 반만 읽힙니다. 받는 쪽은 어디서 끊어야 할지 모릅니다.
경계를 지키는 통로는 이렇게 돕니다:
- 보내는 쪽이 메시지를 한 번에 하나씩 넘깁니다
- 통로는 그 메시지를 한 덩어리로 나릅니다
- 받는 쪽은 한 번 읽을 때 메시지 하나를 받습니다
대가는 크기입니다. 메시지 하나가 한 번에 실려야 하니 크기에 상한이 생깁니다. 받는 쪽은 가장 큰 메시지가 들어갈 만큼 공간을 마련해 둬야 합니다.
한 건씩 뜻이 서는 작은 데이터에는 경계를 지키는 통로가 맞습니다. 게임 캐릭터의 위치 갱신이나 센서가 잰 값 한 벌이 그 예입니다. 파일이나 영상처럼 길게 이어지는 데이터에는 경계가 필요 없습니다.
상세
편지 세 통을 봉투에 따로 넣어 부치면 받는 사람도 봉투 세 개를 뜯습니다. 세 통을 두루마리 하나에 이어 적어 보내면 받는 사람은 어디서 한 통이 끝나는지 알 길이 없습니다.
메시지는 보내는 쪽 프로그램이 한 번에 넘긴 데이터 한 덩어리입니다. 채팅 프로그램이 「안녕」을 보내고 이어서 「잘 가」를 보냈다면 메시지는 둘입니다.
메시지 경계(message boundary)는 한 메시지가 끝나고 다음 메시지가 시작되는 곳입니다. 위의 채팅에서는 「안녕」과 「잘 가」 사이가 경계입니다. 받는 쪽은 이 경계를 알아야 두 말을 따로 화면에 띄웁니다.
통로가 이 경계를 받는 쪽까지 나르면 경계가 보존된다고 말합니다. 한 번 보낸 메시지는 한 번 읽기에 온전히 나옵니다. 두 메시지가 한 번 읽기에 붙어 나오지 않습니다. 한 메시지가 두 번 읽기로 쪼개져 나오지도 않습니다.
네트워크로 데이터를 나르는 대표 방식 둘이 이 점에서 갈립니다. UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜)는 경계를 보존합니다. TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 경계를 지웁니다.
TCP 는 보낸 데이터를 바이트가 끊김 없이 이어진 한 줄로 다룹니다. 이런 줄을 바이트 스트림이라고 합니다. 줄에는 바이트의 순서만 남고 몇 번에 나눠 보냈는지는 남지 않습니다.
flowchart TD
subgraph 보냄["보내는 쪽 · 두 번 보낸다"]
direction TB
S1["hello"]
S2["world"]
end
subgraph 지킴["UDP 로 받은 쪽 · 경계가 남는다"]
direction TB
K1["읽기 1 · hello"]
K2["읽기 2 · world"]
end
subgraph 지움["TCP 로 받은 쪽 · 경계가 사라진다"]
direction TB
N1["읽기 1 · hellowo"]
N2["읽기 2 · rld"]
end
보냄 --> 지킴
보냄 --> 지움
같은 두 번의 보내기가 UDP 에서는 두 번의 읽기로 나뉘어 나옵니다. TCP 에서는 hello 와 world 가 한 줄로 이어 붙은 뒤 받는 쪽이 읽은 만큼씩 끊겨 나옵니다. 끊기는 곳은 보낸 쪽이 나눈 곳과 상관이 없습니다.
경계를 지키는 통로와 지우는 통로
통로마다 경계를 지키는지가 정해져 있습니다. 이 성질은 데이터를 빠짐없이 순서대로 나르는지와 따로 정해집니다.
TCP 는 빠짐없이 순서대로 나릅니다. 대신 경계를 지웁니다. UDP 는 경계를 지킵니다. 대신 도중에 빠진 메시지를 다시 보내 주지 않습니다. SCTP(Stream Control Transmission Protocol, 스트림 제어 전송 프로토콜)는 빠짐없이 나릅니다. 경계도 지킵니다.
한 컴퓨터 안에서 프로그램끼리 주고받는 통로와 웹에서 쓰는 통로까지 모으면 이렇습니다.
| 통로 | 무엇인가 | 경계 | 빠짐없이 순서대로 |
|---|---|---|---|
| TCP | 연결을 맺고 바이트를 나르는 전송 프로토콜 | ✗ | ✓ |
| UDP | 연결 없이 메시지를 하나씩 보내는 전송 프로토콜 | ✓ | ✗ |
| SCTP | 연결을 맺고 메시지 단위로 나르는 전송 프로토콜 | ✓ | ✓ |
| 파이프 | 한 프로그램의 출력을 다른 프로그램의 입력으로 잇는 통로 | ✗ | ✓ |
| 웹소켓 | 브라우저와 서버가 이어 둔 연결 위로 메시지를 주고받는 프로토콜 | ✓ | ✓ |
표의 두 열은 서로 따로 움직입니다. 경계가 남는다고 빠짐없이 도착하는 것은 아닙니다. 빠짐없이 도착한다고 경계가 남는 것도 아닙니다.
메시지보다 작은 버퍼
경계를 지키는 통로에서는 한 번 읽기가 메시지 하나를 받습니다. 그래서 받는 쪽이 읽을 공간을 얼마나 잡았는지가 중요해집니다.
버퍼는 읽어 온 데이터를 담아 둘 메모리 공간입니다. 버퍼가 메시지보다 작으면 메시지가 다 안 들어갑니다.
아래는 UDP 로 보낸 열 바이트짜리 메시지 하나를 다섯 바이트 버퍼로 두 번 읽는 경우입니다.
send("helloworld") // 메시지 하나
recv(5) // "hello"
recv(5) // 다음 메시지를 기다림
첫 읽기는 버퍼에 들어가는 hello 만 받습니다. UDP 에서는 담지 못한 world 가 버려집니다. 두 번째 읽기는 world 가 아니라 그다음 메시지를 기다립니다.
TCP 라면 world 가 버려지지 않고 다음 읽기로 넘어옵니다. TCP 에는 메시지라는 단위가 없어서 남은 바이트가 줄에 남아 기다립니다.
그래서 경계를 지키는 통로를 쓸 때는 가장 큰 메시지가 들어갈 만큼 버퍼를 잡아 둡니다.
메시지 크기의 상한
경계를 지키려면 메시지 하나가 한 덩어리로 실려 가야 합니다. 그러니 메시지 크기에 상한이 생깁니다. UDP 에서 그 한 덩어리가 데이터그램입니다. 데이터그램은 혼자서 목적지까지 가는 데이터 한 덩이입니다. UDP 로 보낸 메시지 하나는 데이터그램 한 개에 실려 갑니다.
데이터그램 앞머리에는 헤더라는 안내 정보가 붙습니다. 헤더에는 데이터그램 길이를 적는 16비트 칸이 있습니다. 16비트로 적을 수 있는 가장 큰 수는 65,535입니다. 그래서 헤더까지 합쳐 데이터그램 하나는 65,535바이트를 넘지 못합니다.
상한보다 작아도 큰 메시지는 불리합니다. 네트워크가 한 번에 나를 수 있는 크기보다 크면 가는 길에서 여러 조각으로 쪼개집니다. 이를 단편화라고 합니다. UDP 에서는 조각 하나만 잃어도 메시지 전체를 잃습니다. UDP 로 주고받는 메시지를 대개 작게 잡는 까닭입니다.
경계가 없는 통로 위의 메시지
TCP 처럼 경계를 지우는 통로 위로도 메시지를 주고받는 프로그램이 많습니다. 이때 경계는 프로그램이 직접 다시 그어야 합니다.
처음 짠 코드는 이 일을 빠뜨리기 쉽습니다. 작은 메시지를 드문드문 보내면 한 번 보낸 것이 한 번에 읽히는 일이 잦습니다. 그래서 시험할 때는 경계가 지켜지는 것처럼 보입니다. 메시지가 커지거나 잇달아 보내지면 그때 붙거나 쪼개져 읽힙니다.
지워진 경계를 데이터 안에 다시 적는 일을 프레이밍이라고 합니다. 받는 쪽은 다시 적힌 경계를 보고 이어진 바이트 줄을 메시지 단위로 다시 끊습니다. 흔한 방법은 둘입니다.
길이 접두는 메시지 앞에 그 메시지의 길이를 먼저 적는 방법입니다. hello 와 world 를 보내면 줄은 5hello5world 처럼 됩니다. 받는 쪽은 길이를 먼저 읽고 그만큼만 떼어 냅니다.
구분자는 메시지 끝에 약속한 바이트를 붙이는 방법입니다. 줄바꿈을 약속했다면 줄은 hello, 줄바꿈, world, 줄바꿈 순서로 이어집니다. 받는 쪽은 그 바이트가 나올 때마다 끊습니다.
웹소켓은 길이 접두로 경계를 다시 긋는 예입니다. 웹소켓은 TCP 연결 위에서 돕니다. 그래도 경계를 지킵니다. 메시지를 실어 보내는 단위마다 앞머리에 길이를 적기 때문입니다.
경계가 값을 하는 데이터
메시지 경계가 중요한지는 데이터의 모양이 정합니다. 따로따로 뜻이 서는 작은 단위인지, 길게 이어지는 한 덩이인지를 봅니다.
작은 단위로 오가는 데이터는 경계가 곧 뜻입니다. 게임 캐릭터의 위치 갱신 한 건이나 센서가 잰 값 한 벌이 그 예입니다. DNS(Domain Name System, 도메인 네임 시스템) 질의 하나와 답 하나도 같습니다. 반쪽만 읽으면 쓸 데가 없으니 경계를 지키는 통로가 잘 맞습니다.
길게 이어지는 데이터는 경계가 필요 없습니다. 파일을 복사하거나 영상을 내려받을 때는 어디서 끊어 읽든 이어 붙이면 됩니다. 이럴 때는 바이트 스트림이 편합니다.
메시지 경계도 필요하고 빠짐없이 순서대로 도착하는 것도 필요한 때가 가장 흔합니다. 요청 하나와 응답 하나를 빠짐없이 주고받아야 하는 서버가 이런 때입니다. 이때는 웹소켓처럼 TCP 위에 경계를 다시 긋는 프로토콜을 얹습니다. SCTP 처럼 경계와 빠짐없는 전달을 둘 다 갖춘 통로를 고르기도 합니다.
관련 항목
메시지 경계를 지키는 통로
UDP · SCTP · 웹소켓 · 메시지 큐 · 데이터그램 · 데이터그램 소켓
메시지 경계를 지우는 통로
TCP · 바이트 스트림 · 스트림 · 파이프 · 스트림 소켓 · QUIC
지워진 경계를 다시 긋는 방법
프레이밍 · 길이 접두 · 구분자 · 고정 길이 레코드 · 청크 전송 인코딩 · 바이트 스터핑
경계로 나뉘는 데이터 단위
경계를 지킬 때 걸리는 제약
경계가 어긋나서 나는 오류
짧은 읽기 · HTTP 요청 스머글링 · 프레이밍 오류
메시지 경계가 갈리는 계층
다른 이름: message boundary · message boundaries · 메시지 경계 보존