엔디언
엔디언은 여러 바이트로 이루어진 값을 저장할 때, 바이트를 어떤 순서로 늘어놓을지 정해 주는 규칙입니다. 정수처럼 한 바이트보다 큰 값을 쓰려면 값을 몇 조각으로 나눠 저장해야 합니다. 그 조각의 순서를 컴퓨터마다 미리 정해 둡니다. 그렇게 정한 순서를 엔디언이라고 부릅니다.
쉽고 빠른 이해
엔디언은 여러 바이트로 된 값을 저장할 때 바이트를 늘어놓는 순서를 정해 두는 규칙입니다. 정수 하나도 4바이트, 8바이트로 나눠 써야 할 때가 많아서 순서가 필요합니다.
이게 없으면 같은 값을 적어도 컴퓨터마다 바이트가 다른 순서로 쌓입니다. 한 컴퓨터가 쓴 바이트를 다른 컴퓨터가 그대로 읽으면 전혀 다른 수로 읽힙니다.
크게 두 가지로 나뉩니다:
- 빅엔디언 — 최상위 바이트부터 먼저 씁니다
- 리틀엔디언 — 최하위 바이트부터 먼저 씁니다
인터넷으로 값을 주고받을 때는 빅엔디언으로 통일해 쓰기로 정해 두었습니다. x86 계열 CPU(Central Processing Unit, 중앙 처리 장치)는 리틀엔디언을 씁니다. 둘이 만나는 지점에서 순서를 안 맞추면 값이 뒤집혀 엉뚱한 수로 읽힙니다.
상세
급히 받아 적은 전화번호를 뒤에서부터 옮겨 적으면 4913 이 3194 가 됩니다. 숫자는 한 자도 빠지지 않아서 적어 놓은 것만 봐서는 어디가 틀렸는지 알 수 없습니다. 신호음도 여느 때처럼 울려서 엉뚱한 사람 목소리를 듣고서야 알아챕니다.
엔디언은 컴퓨터가 값을 저장하는 순서를 다룹니다. 같은 순서로 다시 읽어야 원래 값이 나옵니다.
엔디언(endian)은 여러 바이트로 이루어진 값을 메모리나 파일에 쓸 때, 어느 바이트를 먼저 쓸지 정하는 규칙입니다. 정수 하나가 1바이트보다 크면 값을 여러 바이트로 나눠 저장해야 합니다. 예를 들어 32비트 정수는 4바이트를 씁니다.
두 가지 순서
값을 나누는 바이트 개수는 자료형이 정합니다. 32비트 정수인 0x12345678 은 12 · 34 · 56 · 78 이라는 네 바이트로 나뉩니다. 이 네 바이트를 늘어놓는 순서가 둘입니다.
빅엔디언(big-endian)은 값의 최상위 바이트부터 씁니다. 사람이 숫자를 적는 순서와 같습니다. 234라는 수를 적을 때 백의 자리인 2를 맨 앞에 적는 것과 같은 이치입니다.
리틀엔디언(little-endian)은 반대로 값의 최하위 바이트부터 씁니다. 위 예의 0x12345678 은 78 · 56 · 34 · 12 순서로 저장됩니다.
주소가 작은 곳부터 큰 곳까지, 어느 바이트가 놓이는지를 그리면 이렇습니다. 먼저 빅엔디언입니다.
block-beta columns 4 a0["주소 0"]:1 a1["주소 1"]:1 a2["주소 2"]:1 a3["주소 3"]:1 b0["12"]:1 b1["34"]:1 b2["56"]:1 b3["78"]:1
다음은 리틀엔디언입니다.
block-beta columns 4 c0["주소 0"]:1 c1["주소 1"]:1 c2["주소 2"]:1 c3["주소 3"]:1 d0["78"]:1 d1["56"]:1 d2["34"]:1 d3["12"]:1
두 그림에서 주소 0 이 담는 값이 다릅니다. 빅엔디언은 최상위 바이트 12 를, 리틀엔디언은 최하위 바이트 78 을 거기 둡니다.
CPU 마다 순서를 다르게 고른 것은 회로 설계의 편의 때문입니다. 계산 회로가 최하위 바이트부터 다루면 리틀엔디언 순서가 그 흐름과 맞습니다. 사람이 값을 눈으로 읽습니다. 그렇게 읽은 값을 비교할 때는 빅엔디언이 자연스럽습니다.
컴퓨터를 벗어나면 문제가 된다
예를 들어 x86 계열 CPU 로 네트워크에서 받은 4바이트 정수를 읽는 경우를 봅니다. 한 컴퓨터 안에서는 이 순서를 신경 쓸 일이 거의 없습니다. CPU 가 값을 읽을 때도 쓸 때도 언제나 같은 순서를 씁니다. 문제는 바이트가 그 컴퓨터를 벗어날 때 생깁니다.
네트워크로 값을 주고받는 프로토콜은 순서를 하나로 통일해 두었습니다. 그 약속을 네트워크 바이트 순서라고 부르며, 정해 둔 순서는 빅엔디언입니다. x86 계열 CPU 는 리틀엔디언을 쓰므로, 받은 4바이트를 정수로 그대로 읽으면 순서가 뒤집힌 값이 나옵니다. 받는 쪽 프로그램이 순서를 맞춰 다시 늘어놓아야 원래 값이 나옵니다.
문자를 2바이트 이상으로 저장하는 인코딩도 같은 순서 문제를 겪습니다. UTF-16 (Unicode Transformation Format 16-bit, 유니코드 변환 형식 16비트)은 문자 하나를 2바이트로 적는 인코딩입니다.
그 순서를 알려 주는 표시가 파일 맨 앞에 붙습니다. 이 표시를 바이트 순서 표시라고 부릅니다. 영어로는 BOM(Byte Order Mark)이라고 줄여 부릅니다.
그래서 네트워크 코드는 받은 바이트를 언어가 정한 방법으로 순서를 맞춰 읽습니다. 파이썬의 struct 모듈은 형식 문자로 순서를 고릅니다.
import struct
v = 0x1234
struct.pack('>H', v).hex() # '1234' 빅
struct.pack('<H', v).hex() # '3412' 리틀
> 는 빅엔디언, < 는 리틀엔디언을 뜻합니다. H 는 2바이트 정수를
나타내는 형식 문자입니다. 받은 바이트가 어느 순서인지 미리 알아야 이 중
하나를 고를 수 있습니다.
자바에서는 방식이 다릅니다. DataInputStream 과 DataOutputStream 은 값을 읽을 때도 쓸 때도 언제나 빅엔디언 순서를 씁니다. 리틀엔디언 값을 다루려면 ByteBuffer 를 만들어 순서를 따로 지정해야 합니다.
순서를 놓치면 생기는 문제
순서를 잘못 맞추면 프로그램이 오류를 내지 않습니다. 대신 조용히 틀린 값을 돌려줍니다. 4바이트 정수 1은 빅엔디언으로 00 · 00 · 00 · 01 로 저장됩니다. 이 바이트를 리틀엔디언으로 잘못 읽으면 16777216 이라는 큰 수가 나옵니다.
예외나 충돌 없이 프로그램이 계속 돌아가기 때문에 이 값이 잘못됐다는 것을 로그만 보고는 알아채기 어렵습니다. 파일 형식이나 프로토콜 문서에 적힌 순서를 코드가 따르는지 확인하는 것이 유일한 예방책입니다.
언제 신경 쓰나
값을 텍스트로 주고받으면 이 문제가 없습니다. JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)처럼 숫자를 사람이 읽는 문자로 적는 형식은 순서를 가릴 바이트가 애초에 없습니다.
XML(eXtensible Markup Language, 확장 가능 마크업 언어) 같은 텍스트 기반 형식도 마찬가지입니다.
이진 프로토콜을 직접 구현하거나, 다른 컴퓨터가 만든 이진 파일을 읽거나, 네트워크 패킷의 헤더를 파싱할 때는 순서를 확인해야 합니다. 쓰는 언어나 라이브러리가 어느 순서를 기본값으로 쓰는지, 상대가 보낸 바이트가 어느 순서인지 둘 다 알아야 값이 맞게 읽힙니다.
관련 항목
엔디언을 채택한 CPU 계열
CPU · x86 · ARM · RISC-V · 레지스터
순서가 어긋나면 값이 깨지는 통신·저장 수단
문자를 여러 바이트로 적을 때 순서를 같이 정하는 인코딩
UTF-8 · UTF-16 · 문자 인코딩 · 바이트 순서 표시
메모리에서 순서와 함께 정해지는 자리 배치
코드에서 순서를 직접 지정하는 도구
struct · ByteBuffer · htonl
다른 이름: endian · endianness · big-endian · little-endian · 바이트 순서 · 엔디안