Byte: 8비트로 묶인 컴퓨터의 최소 주소 지정 단위
Byte는 8개의 비트(bit)를 하나로 묶은 데이터 단위로, CPU가 메모리에 접근할 때 사용하는 최소 주소 지정 단위다. 문자 하나를 표현하는 기본 단위로 널리 쓰이며, 네트워크 프로토콜과 파일 포맷의 기본 구성 요소이기도 하다. 이 글에서는 Byte가 왜 8비트로 표준화되었는지, 엔디안(Endian)과 어떻게 연결되는지, 실무에서 어떤 방식으로 다뤄야
1. 개념
Byte는 8개의 비트(bit)를 하나로 묶은 데이터 단위다. 1비트가 0 또는 1 두 가지 상태만 표현할 수 있는 반면, 8비트를 묶으면 2^8 = 256가지 상태를 표현할 수 있다. 이 범위는 아스키(ASCII) 문자 집합(0~127)을 표현하기에 충분했고, 이후 확장 아스키와 유니코드(UTF-8)의 기본 단위로도 이어졌다.
역사적으로 Byte의 크기는 항상 8비트였던 것은 아니다. 1960년대 이전에는 6비트, 7비트, 9비트 Byte를 쓰는 컴퓨터도 있었다. IBM System/360이 8비트 Byte를 채택하면서 사실상 업계 표준이 되었고, 현재는 8비트로 고정된 Byte를 명확히 지칭할 때 옥텟(Octet)이라는 용어를 쓰기도 한다. IETF RFC 문서에서는 지금도 "octet"이라는 표현을 자주 사용한다.
2. 왜 사용하는가
CPU와 메모리는 비트 단위가 아니라 Byte 단위로 주소를 매긴다. 즉 메모리의 각 주소는 1바이트를 가리키며, 비트 단위로는 개별 주소가 존재하지 않는다. 이렇게 설계한 이유는 다음과 같다.
- 비트 단위 주소 지정은 하드웨어 복잡도와 비용을 크게 높인다. 8비트를 하나의 단위로 묶으면 주소 버스와 회로 설계가 단순해진다.
- 문자 하나(아스키 기준)를 표현하는 데 8비트면 충분해서, 실용적인 최소 단위로 자리 잡았다.
- 킬로바이트(KB), 메가바이트(MB) 같은 용량 단위 체계가 Byte를 기준으로 만들어지면서 소프트웨어 생태계 전체가 Byte 단위 계산에 맞춰졌다.
3. 동작 원리
Byte 내부에서 비트의 순서는 보통 왼쪽이 최상위 비트(MSB, Most Significant Bit), 오른쪽이 최하위 비트(LSB, Least Significant Bit)로 표기한다. 예를 들어 십진수 13은 8비트로 00001101로 표현된다.
여러 Byte를 묶어 더 큰 수(예: 4바이트 정수)를 표현할 때는 바이트의 배치 순서, 즉 엔디안(Endian)이 문제가 된다.
- 빅 엔디안(Big Endian): 최상위 바이트를 가장 낮은 메모리 주소에 저장한다. 네트워크 프로토콜(TCP/IP)은 기본적으로 빅 엔디안을 사용하며 이를 네트워크 바이트 순서(Network Byte Order)라 부른다.
- 리틀 엔디안(Little Endian): 최하위 바이트를 가장 낮은 메모리 주소에 저장한다. x86, x86-64 아키텍처가 리틀 엔디안을 사용한다.
정수 0x12345678(4바이트)을 메모리에 저장하면 다음과 같이 배치된다.
주소: 0x00 0x01 0x02 0x03
빅 엔디안: 0x12 0x34 0x56 0x78
리틀 엔디안: 0x78 0x56 0x34 0x124. 코드 예제
Python에서 정수와 Byte 배열 간의 변환, 엔디안 차이를 직접 확인해보면 이해가 빠르다.
# 정수를 4바이트(32비트)로 변환
value = 0x12345678
big_endian = value.to_bytes(4, byteorder='big')
little_endian = value.to_bytes(4, byteorder='little')
print(big_endian.hex()) # 12345678
print(little_endian.hex()) # 78563412
# 반대로 바이트를 정수로 복원
restored = int.from_bytes(big_endian, byteorder='big')
print(restored == value) # True
# 개별 비트 접근: 특정 바이트의 최상위 비트 확인
byte_value = 0b10010110
msb = (byte_value >> 7) & 1
print(msb) # 1TypeScript에서는 ArrayBuffer와 DataView를 사용해 저수준 바이트 조작을 할 수 있다. 네트워크 통신이나 바이너리 파일 파싱에서 자주 쓰이는 패턴이다.
// 4바이트 버퍼에 32비트 정수를 빅 엔디안/리틀 엔디안으로 저장
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 0x12345678, false); // false = 빅 엔디안
console.log(new Uint8Array(buffer)); // Uint8Array(4) [ 18, 52, 86, 120 ]
view.setUint32(0, 0x12345678, true); // true = 리틀 엔디안
console.log(new Uint8Array(buffer)); // Uint8Array(4) [ 120, 86, 52, 18 ]
// 개별 바이트 읽기
console.log(view.getUint8(0)); // 1205. 성능 특성
Byte 자체는 시간복잡도로 논할 대상이 아니지만, Byte 단위 메모리 접근과 정렬(Alignment)은 실제 성능에 영향을 준다.
- CPU 캐시는 보통 64바이트 단위의 캐시 라인(Cache Line)으로 메모리를 읽어온다. 필요한 데이터가 1바이트뿐이어도 캐시 라인 전체가 로드된다.
- 정렬되지 않은 메모리 접근(Unaligned Access)은 일부 아키텍처(과거 ARM 등)에서 예외를 발생시키거나, x86처럼 예외 없이 처리되더라도 두 번의 메모리 접근이 필요해 성능이 떨어질 수 있다.
- 구조체 패딩(Padding)은 컴파일러가 필드를 특정 바이트 경계(4바이트, 8바이트 등)에 맞추기 위해 의도적으로 삽입하는 빈 공간이다. 이로 인해 구조체의 실제 크기가 필드 크기의 단순 합보다 커질 수 있다.
6. 실무 사용 사례
- 네트워크 프로토콜: TCP/IP 헤더의 각 필드는 옥텟(Byte) 단위로 정의되며, 정수 필드는 네트워크 바이트 순서(빅 엔디안)로 전송된다. 소켓 프로그래밍에서
htons(),ntohs()같은 함수가 이 변환을 처리한다. - 파일 포맷 파싱: PNG, JPEG, ELF 같은 바이너리 파일 포맷은 헤더의 매직 넘버(Magic Number)와 필드 크기를 바이트 단위로 정의한다. 파일 시그니처 검사도 첫 몇 바이트를 비교하는 방식이다.
- 문자 인코딩: UTF-8은 문자 하나를 1~4바이트 가변 길이로 인코딩한다. 아스키 범위 문자는 1바이트, 한글은 보통 3바이트를 차지한다.
- Base64 인코딩: 3바이트(24비트)를 4개의 6비트 그룹으로 나누어 텍스트로 안전하게 전송 가능한 형태로 변환한다.
7. 주의할 점
- 엔디안을 고려하지 않고 바이너리 데이터를 다른 아키텍처나 네트워크로 그대로 전송하면 값이 깨진다. 네트워크 통신 코드에서는 항상 명시적으로 바이트 순서를 변환해야 한다.
- "1바이트 = 1문자"라는 가정은 UTF-8, UTF-16 같은 가변 길이 인코딩에서는 성립하지 않는다. 문자열 길이를 바이트 수로 계산하면 다국어 텍스트에서 오류가 생긴다.
- 저장 용량 표기에서 1KB를 1000바이트로 볼지 1024바이트로 볼지는 맥락에 따라 다르다. 국제단위계(SI) 기준 KB는 1000바이트, 이진 접두어 KiB는 1024바이트로 구분해서 쓰는 것이 정확하다.
- signed byte(부호 있는 8비트, 범위 -128~127)와 unsigned byte(부호 없는 8비트, 범위 0~255)를 혼동하면 오버플로우나 예기치 않은 음수 값이 발생할 수 있다.
8. 핵심 정리
Byte는 8비트를 묶은 단위로, CPU와 메모리가 데이터를 주소 지정하는 최소 단위다. 여러 바이트를 묶어 큰 수를 표현할 때는 엔디안이라는 바이트 배치 순서가 중요하며, 네트워크 프로그래밍이나 바이너리 파일 처리에서 이를 명시적으로 다루지 않으면 데이터가 깨진다. 문자 인코딩에서는 1바이트가 1문자를 보장하지 않는다는 점, 캐시 라인과 메모리 정렬이 실제 성능에 영향을 준다는 점을 함께 기억해두는 것이 실무에서 유용하다.