base64 는 왜 정확히 33% 를 증가시킬까
base64 는 왜 정확히 33% 인가
3줄 요약
- base64 는 바이트를 6비트씩 잘라 글자 하나로 바꾼다. 그런데, 글자는 8비트를 먹는다.
- 6비트인 이유는 알파벳 + 숫자가 64글자라서고, 64인 이유는 텍스트로 안전한 문자가 256개 중 93개뿐이라 거기 들어가는 가장 큰 2의 거듭제곱이 64라서다.
- "그냥 0 과 1 로 보내면 되지 않나"는 정반대다. 심볼이 2개면 글자당 1비트라 8배가 된다. 배율은
8 / log₂(심볼 수)하나로 정리된다.
33% 라는 숫자가 어디서 오나
이미지와 같은 바이너리 파일들을 JSON 에 실으려면 base64 로 바꿔야 한다.
그때마다 "33% 커진다"는 경고를 심심치 않게 들을 수 있다. 왜 하필 33% 인지는 크게 신경 안쓰고 넘어갔다.
직접 재보면?
$ head -c 100000 /dev/urandom > raw.bin
$ base64 -i raw.bin | wc -c
133336 (133337 이라면, CLI 가 개행 1 바이트를 붙였을 수 있다.)정확히 4/3 이다.
8비트 그릇에 6비트만 담는다
base64 는 raw 값을 3바이트(24비트)씩 끊어 6비트 조각 4개로 자르는 것을 의미한다.
입력 3바이트: 0x4D 0x61 0x6E ("Man")
비트열: 01001101 01100001 01101110 ← 24비트
6비트씩 재분할: 010011 010110 000101 101110
값: 19 22 5 46
알파벳 매핑: T W F u
출력 4바이트: "TWFu" ← 32비트24비트가 32비트가 됐다. 정보량은 그대로인데 크기가 늘었다.
이유는 단순하다. 문자 하나는 1바이트, 즉 8비트를 먹는다.
그런데 그 안에 담긴 정보는 6비트뿐이다. (2비트씩 낭비가 발생)
8 / 6 = 1.333...어떤 값인지에 상관없이, 무조건 1.33... 배수가 된다.
왜 6비트인가, 왜 64글자인가
6비트인 이유는 알파벳이 64글자라서다. 64 = 2⁶ 이라 글자 하나가 정확히 6비트를 지목한다.
그럼 왜 64개만 쓰는걸까? 1바이트로 표현할 수 있는 값은 256가지인데 말이다.
그중 텍스트 채널에서 안전한 건 아래와 같다.

1바이트 = 256가지 값
├─ 0x00–0x1F 제어문자 32개 → JSON 문자열에 그대로 못 들어간다(`\u...`)
├─ 0x20–0x7E printable 95개 → 이 중 " 와 \ 를 빼면 93개
├─ 0x7F DEL 1개
└─ 0x80–0xFF 128개 → 단독으로는 유효한 UTF-8 이 아님쓸 수 있는 게 93개뿐이니 7, 8비트(128, 256가지)를 다 쓰는 건 애초에 불가능하다. (안전문자가 모자름)
그 다음 2의 거듭제곱을 고르면 64다.
=> base64 의 64 는 "텍스트로 안전한 문자 집합 안에 들어가는 가장 큰 2의 거듭제곱" 를 의미
JSON 은 왜 base64 를 받아야 하는가
그냥 base64 가 아니라 이진 raw 데이터를 그대로 넣으면 안되는건가? 라고 생각할 수 있다.
JSON 의 문법은 raw 데이터를 받을수 없게 되어있다.
- JSON 문자열은
"로 시작해"로 끝나야 한다. - JSON 은 처리하는 변수 값의 길이를 미리 알려주지 않는다.
raw 데이터에서 "(0x22, 00100010) 가 없을 확률은 사실상 0에 가깝다.
-> 그렇기에 " 이 나오지 않게 보장하는 base64 를 쓰는 것이다.
길이만 알려주면 해결이 되는건가?
-> 문법이 그렇게 안되어있다.
- JSON 의 타입은
object | array | number | string | true | false | null만 가능하다.- RFC 8259 에 명시
raw bytes타입 X
- JSON 은 키 순서가 보장되지 않는다.
{ "file": <raw 100KB>, "len": 100000 }file 이 len 보다 먼저 올 수 있다.
- JSON 텍스트는 UTF-8 로 인코딩 가능해야 한다.
- RFC 8259 에 명시
0x80–0xFF는 단독으로는 유효한 UTF-8 이 아니다. (EX:가는EA B0 80이 뭉쳐야만 된다.)
마무리
33% 는 텍스트 포맷 안에 바이너리를 넣기에 발생한 값이다.
길이를 적는 포맷(MessagePack, multipart) 에서는 이 값이 발생하지 않는다.
그렇기에, POST 요청에 파일을 보내야하면, Multipart 요청을 일반적으로 사용한다.
JSON 이 편하지만, 33% 가 부담이 된다면 다른것도 고려해볼 필요는 있다.
(EX: 33% 증가면, 20MB -> 27MB 가량이 된다!!)