컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
arch · 20

엔디언(빅/리틀)

바이트순서데이터표현엔디언

컴퓨터의 메모리는 바이트 단위로 주소가 붙어 있습니다. 그런데 32비트 정수 0x12345678처럼 바이트 여러 개로 이뤄진 값은 어떤 바이트를 낮은 주소에 놓을지 정해야 합니다. 이 바이트 배치 순서를 엔디언(endianness)이라고 부릅니다. 가장 큰 자릿수 바이트(MSB)를 낮은 주소에 먼저 놓으면 빅 엔디언, 가장 작은 자릿수 바이트(LSB)를 먼저 놓으면 리틀 엔디언입니다.

같은 값이라도 순서가 다르면 메모리에 놓인 바이트 모양이 다릅니다. 평소 변수를 쓸 때는 CPU가 알아서 처리하므로 드러나지 않지만, 메모리를 바이트 단위로 직접 들여다보거나, 파일과 네트워크로 여러 바이트 정수를 주고받거나, 서로 다른 CPU 사이에서 데이터를 옮길 때 문제가 됩니다. 이 토픽에서는 두 방식의 바이트 배치, 네트워크 바이트 순서가 필요한 이유, 같은 바이트가 읽는 쪽의 엔디언에 따라 다른 값이 되는 과정을 봅니다.

핵심 개념

빅 엔디언

가장 큰 자릿수 바이트를 가장 낮은 주소에 저장합니다. 0x12345678은 낮은 주소부터 12 34 56 78로 놓여 값을 쓴 모양 그대로 읽힙니다.

핵심 개념

리틀 엔디언

가장 작은 자릿수 바이트를 가장 낮은 주소에 저장합니다. 0x12345678은 낮은 주소부터 78 56 34 12로 놓입니다. x86과 x86-64가 이 방식입니다.

핵심 개념

바이트 단위 순서

엔디언은 바이트를 놓는 순서이고 바이트 안의 비트 순서가 아닙니다. 바이트 하나(8비트)짜리 값에는 순서가 없어 엔디언의 영향을 받지 않습니다.

핵심 개념

네트워크 바이트 순서

네트워크 프로토콜이 여러 바이트 필드를 보낼 때 쓰는 표준 순서로, 빅 엔디언입니다. 호스트가 리틀 엔디언이면 보내기 전에 변환하고 받은 뒤 되돌립니다.

핵심 개념

htons·htonl 계열

host to network short/long의 약자로, 16비트와 32비트 값을 호스트 순서에서 네트워크 순서로 바꾸는 함수입니다. 반대 방향은 ntohs, ntohl입니다. 호스트 순서가 이미 빅 엔디언이면 값이 그대로입니다.

핵심 개념

바이트로 접근할 때 드러남

정수를 정수로만 읽고 쓰면 엔디언이 보이지 않습니다. 정수가 놓인 메모리를 1바이트씩 읽거나, 바이트 배열을 정수로 해석하거나, 바이트를 그대로 파일·소켓에 쓸 때 드러납니다.

값은 같고 배치만 다릅니다 — 0x12345678이라는 수 자체는 바뀌지 않습니다. 메모리에서 어느 바이트가 어느 주소에 놓이는지만 다릅니다.
주고받는 양쪽이 약속해야 합니다 — 송신자와 수신자가 서로 다른 순서를 가정하면 같은 바이트가 다른 값으로 읽힙니다. 그래서 네트워크 프로토콜은 바이트 순서를 하나로 정해 둡니다.
한 바이트는 해당 없습니다 — char 한 개나 ASCII 문자열은 순서 문제가 생기지 않습니다. 2바이트 이상의 정수와 부동소수점 값이 대상입니다.
어느 쪽이 더 낫지는 않습니다 — 두 방식 모두 값을 올바르게 표현합니다. 어떤 방식이 쓰이는지는 ISA 설계와 역사적 선택의 문제입니다.
이 시각화에서 확인할 것 — "바이트 저장 순서" 탭에서 같은 값이 놓이는 순서를, "네트워크 바이트 순서" 탭에서 변환이 필요한 이유를, "같은 바이트 다른 값" 탭에서 읽는 크기에 따른 값의 차이를 STEP으로 확인하세요.

엔디언의 배경

엔디언이라는 이름은 걸리버 여행기에서 달걀을 어느 쪽 끝으로 깰지를 두고 싸우는 이야기에서 따왔으며, 컴퓨터 분야에서는 1980년의 글 "On Holy Wars and a Plea for Peace"가 이 용어를 알렸습니다. 두 방식 모두 기술적으로 타당해서 CPU마다 서로 다른 선택을 했고, 그 결과 서로 다른 시스템 사이에서 데이터를 주고받을 때 바이트 순서를 맞추는 일이 생겼습니다.

숫자로 확인하기

같은 값 0x12345678 — 빅 엔디언은 낮은 주소부터 12 34 56 78, 리틀 엔디언은 78 56 34 12입니다. 4바이트의 순서가 정확히 뒤집힌 모양입니다.
순서가 어긋나면 — 16비트 값 0x1234(4660)를 리틀 엔디언 메모리 바이트 34 12 그대로 보내 빅 엔디언 수신자가 읽으면 0x3412(13330)가 됩니다. 값이 달라집니다.
읽는 크기에 따라 — 메모리 바이트 12 34 56 78을 32비트로 읽으면 빅 엔디언은 0x12345678, 리틀 엔디언은 0x78563412입니다. 1바이트로 읽으면 둘 다 0x12입니다.

자주 하는 오해

엔디언은 비트 순서를 뒤집는다 — 바이트 단위의 순서입니다. 바이트 안의 비트 모양은 그대로입니다.
정수 연산 결과가 엔디언에 따라 달라진다 — 덧셈이나 비교 같은 연산 결과는 같습니다. 달라지는 것은 메모리 속 바이트 배치이고, 이는 바이트 단위로 다룰 때만 드러납니다.
문자열도 순서가 뒤집힌다 — ASCII 같은 1바이트 문자열은 엔디언과 무관하게 앞 글자가 낮은 주소에 놓입니다. 2바이트 코드 단위를 쓰는 UTF-16(UTF-32는 4바이트)처럼 한 글자가 여러 바이트 단위로 이뤄진 인코딩에서만 순서가 문제입니다.
네트워크 순서는 항상 변환이 필요하다 — 빅 엔디언 호스트에서는 htons가 값을 바꾸지 않습니다. 이식성을 위해 변환 함수를 항상 호출하는 것이 안전합니다.

면접에서 이렇게 물어요

면접 질문

빅 엔디언과 리틀 엔디언의 차이는 무엇인가요?

여러 바이트로 이뤄진 값을 메모리에 놓는 순서입니다. 빅 엔디언은 MSB를 낮은 주소에, 리틀 엔디언은 LSB를 낮은 주소에 먼저 놓습니다.

면접 질문

네트워크 바이트 순서가 왜 필요한가요?

통신하는 호스트들의 엔디언이 다를 수 있어서 전송되는 바이트 순서를 하나로 정해야 같은 값으로 해석됩니다. 인터넷 프로토콜은 빅 엔디언을 씁니다.

면접 질문

내 컴퓨터의 엔디언을 코드로 확인하려면?

정수 1을 저장한 뒤 첫 바이트를 char 포인터로 읽어 보면 됩니다. int가 2바이트 이상일 때 첫 바이트가 1이면 리틀 엔디언, 0이면 빅 엔디언입니다.

면접 질문

엔디언 문제는 언제 생기나요?

바이트 단위로 정수를 다룰 때입니다. 소켓으로 정수를 보내거나, 바이너리 파일 형식을 읽고 쓰거나, 서로 다른 아키텍처 사이에서 메모리 덤프를 옮길 때 순서를 맞춰야 합니다.

실제 시스템에서는

인터넷 프로토콜 — IP, TCP, UDP 헤더의 포트 번호와 길이 같은 여러 바이트 필드는 빅 엔디언(네트워크 바이트 순서)으로 전송됩니다. IP 헤더는 RFC 791 부록 B(데이터 전송 순서)가 정하고, TCP와 UDP도 같은 인터넷 관례를 따라 빅 엔디언으로 보냅니다.
소켓 API — POSIX는 htons, htonl, ntohs, ntohl 함수를 정의합니다. 포트와 IP 주소를 소켓 주소 구조체에 넣을 때 사용합니다.
언어와 도구 — Java의 ByteBuffer는 기본이 빅 엔디언이며 order()로 바꿀 수 있고, Python의 struct 모듈은 형식 문자열 앞의 문자로 바이트 순서를 지정합니다.

함께 보면 좋은 토픽

IEEE 754 부동소수점 — 부동소수점 값의 비트 구조를 봅니다. 이 값을 바이트로 보낼 때도 엔디언이 적용됩니다.
메모리 계층 — 바이트 주소가 붙는 메모리가 어떤 계층 구조 위에 있는지 봅니다.

참고 자료

논문 — Danny Cohen, "On Holy Wars and a Plea for Peace", 1980 (IEN 137)

C — 내 컴퓨터의 엔디언과 바이트 배치 확인

#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t v = 0x12345678;
    unsigned char *p = (unsigned char *)&v;

    for (int i = 0; i < 4; i++)
        printf("p[%d] = %02x\n", i, p[i]);
    puts(p[0] == 0x78 ? "little endian" : "big endian");
}

정수 v의 첫 바이트부터 차례로 읽어 메모리에 놓인 순서를 출력합니다. 리틀 엔디언이면 p[0]이 가장 작은 자릿수 바이트인 78이고, 빅 엔디언이면 12입니다. char 포인터로 읽는 접근은 C에서 객체의 바이트를 들여다보는 허용된 방법이라서, 이 코드가 가리키는 값이 바이트 배치를 정확히 보여 줍니다. x86이나 x86-64 컴퓨터에서 돌리면 78 56 34 12 순서와 little endian이 나옵니다.

C — 소켓에 포트를 넣을 때 네트워크 순서로 변환

#include <arpa/inet.h>
#include <netinet/in.h>

struct sockaddr_in addr = {0};
addr.sin_family      = AF_INET;
addr.sin_port        = htons(8080);          /* 호스트 → 네트워크 */
addr.sin_addr.s_addr = htonl(INADDR_ANY);

/* 받은 값을 호스트 순서로 되돌릴 때 */
uint16_t port = ntohs(addr.sin_port);

sockaddr_in 구조체의 포트와 주소 필드는 네트워크 바이트 순서로 채워야 합니다. htons는 16비트 포트를, htonl은 32비트 주소를 호스트 순서에서 네트워크 순서로 바꿉니다. 호스트가 이미 빅 엔디언이라면 이 함수들은 값을 바꾸지 않으므로 항상 호출해 두면 어떤 플랫폼에서도 같은 코드로 동작합니다. INADDR_ANY는 값이 0이라 순서와 무관하지만 관례로 htonl을 씁니다. 받은 값을 읽을 때는 반대 방향의 ntohs, ntohl을 씁니다.

Python · Java — 바이트 순서를 직접 지정

# Python: struct 형식 앞의 문자로 순서 지정  > 빅, < 리틀
import struct
struct.pack(">I", 0x12345678).hex()   # '12345678'
struct.pack("<I", 0x12345678).hex()   # '78563412'
(0x12345678).to_bytes(4, "big").hex()  # '12345678'

// Java: ByteBuffer는 기본이 빅 엔디언
ByteBuffer buf = ByteBuffer.allocate(4);
buf.order(ByteOrder.LITTLE_ENDIAN).putInt(0x12345678);

Python의 struct.pack은 형식 문자열의 첫 글자로 바이트 순서를 정합니다. 큰 부등호는 빅 엔디언, 작은 부등호는 리틀 엔디언이라 같은 값이 서로 다른 바이트열로 직렬화됩니다. 정수의 to_bytes도 "big"과 "little"로 순서를 직접 고릅니다. 바이트 순서를 코드에 명시해 두면 어느 플랫폼에서 돌려도 같은 결과가 나옵니다. Java의 ByteBuffer는 기본이 빅 엔디언이고, order 메서드로 리틀 엔디언으로 바꾼 뒤 putInt를 호출하면 바이트가 뒤집혀 기록됩니다.

Q1

32비트 값 0x12345678을 메모리의 낮은 주소부터 12 34 56 78 순서로 저장하는 방식은?

가장 큰 자릿수(MSB) 바이트가 먼저 옵니다.
가장 상위 바이트를 낮은 주소에 먼저 저장하는 방식이 빅 엔디언입니다. 반대로 하위 바이트(78)를 먼저 저장하면 리틀 엔디언입니다.

Q2

x86 계열 CPU가 사용하는 바이트 저장 순서는?

낮은 자릿수 바이트가 낮은 주소에 옵니다.
x86과 x86-64는 리틀 엔디언을 사용합니다. 그래서 0x12345678을 메모리에 저장하면 낮은 주소부터 78 56 34 12 순서로 놓입니다.

Q3

네트워크 바이트 순서(Network Byte Order)로 정해진 방식은?

호스트 순서와 네트워크 순서를 바꿔주는 htons, htonl 함수가 있습니다.
네트워크 프로토콜은 통신 상대의 CPU 종류와 무관하게 빅 엔디언을 표준 바이트 순서로 사용합니다. 리틀 엔디언 호스트는 전송 전에 htons/htonl 같은 함수로 변환합니다.