인코딩1을 배우다 보면 자연스럽게 Base64라는 용어를 접하게 됩니다. Base64 인코딩은 바이너리 데이터를 텍스트 형식으로 변환하는 인코딩 기법 입니다. 이번 포스팅에는 Base64에 대해 자세히 알아보겠습니다.
개념
Base64는 바이너리 데이터를 텍스트로 표현하기 위한 인코딩 방식입니다. Base64는 64진법을 의미하는데, 이는 6비트()를 한 묶음으로 하여 표현할 수 있는 경우의 수가 64가지이기 때문에 붙은 이름입니다. Base64 인코딩 과정에서는 원본 데이터 비트를 6비트씩 잘라 각각 0부터 63 사이의 값으로 보고 해당 값에 대응하는 미리 정의된 문자로 치환합니다. 이렇게 하면 바이너리 데이터가 사람이 읽을 수 있는 ASCII 문자들로만 이루어진 문자열로 변환되어 텍스트 전용 환경에서 안정적으로 데이터를 전송하거나 저장할 수 있게 됩니다.
Base64 인코딩은 64개의 특정 ASCII 문자만으로 구성됩니다.
A-Z
(0-25),a-z
(26-51)0-9
(52-61)+
(62),/
(63)
값 | 문자 | 값 | 문자 | 값 | 문자 | 값 | 문자 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
이 64개 문자가 각각 6비트 값(0~63)에 대응되며 이들로만 이루어진 문자열이 Base64 인코딩 결과가 됩니다. 추가로 =
기호가 등장하는데, =
는 실제 데이터가 아니며 인코딩 결과 끝을 채우는 패딩(padding) 용도로 사용됩니다. (패딩에 대해서는 뒤에서 자세히 설명)
도입 배경
그렇다면 왜 Base64가 등장한 것일까요? Base64가 등장한 배경에는 네트워크 통신 환경의 제약이 있습니다. 초기 인터넷 주요 프로토콜(STMP)은 영어 문자(7비트 ASCII)만을 안전하게 전송할 수 있도록 만들어졌습니다. 이 때문에 한글과 같은 비ASCII 문자는 물론이고, 이미지나 동영상, 실행파일과 같은 바이너리 데이터를 그대로 전송할 수 없는 문제가 있었습니다. 이러한 제약을 해결하기 위해 MIME(Multipurpose Internet Mail Extensions) 표준이 도입되었고, 여기서 바이너리 데이터를 텍스트로 변환하는 인코딩 방식의 하나로 Base64가 정의되었습니다. Base64와 기존의 표현 방식인 16진수 인코딩과 다른 점은 효율성과 사용 문자 수입니다. 16진수 인코딩은 한 바이트()를 두 개의 16진수 문자()로 나타내므로 데이터의 크기를 100% 증가시킵니다. 반면 Base64는 3바이트()를 4개의 64진수 문자()로 표현하므로 데이터의 크기를 약 33%배 증가시킵니다. 이처럼 초기 통신 환경의 한계를 극복하기 위한 해결책으로 탄생한 Base64는, 단순히 데이터 크기를 줄이는 것뿐만 아니라 안정적인 데이터 전송을 가능하게 했습니다. 이제 Base64 인코딩이 실제로 어떻게 작동하는지, 그 원리를 자세히 알아보겠습니다.
동작 원리
Base64 인코딩 과정은 다음과 같이 이루어집니다.
- 3바이트(24비트) 단위로 그룹화
- 6비트씩 4개로 분할
- Base64 문자로 매핑
- 남은 데이터 처리 및 패딩
3바이트(24비트) 단위로 그룹화
인코딩할 원본 바이너리 데이터를 세 개의 바이트씩 묶어서 한 그룹(총 24비트)으로 처리합니다. 예를 들어, 원본 데이터가 “ABC”라는 3글자 문자열이라면 ASCII 바이트 시퀀스 [0x41, 0x42, 0x43]
(65, 66, 67)가 하나의 24비트 그룹이 됩니다.
6비트씩 4개로 분할
24비트로 된 한 그룹을 6비트씩 4개의 청크2로 나눕니다. 각 청크는 0부터 63 사이의 값을 나타낼 수 있습니다. 위 “ABC” 예시의 24비트를 이진수로 쓰면 01000001 01000010 01000011
이고, 이를 6비트씩 나누면 010000
, 010100
, 001001
, 000011
의 네 부분으로 분리됩니다.
Base64 문자로 매핑
각 6비트 청크 값을 미리 정해진 Base64 문자로 변환합니다. 앞서 분할한 010000
010100
001001
000011
을 10진수로 보면 16, 20, 9, 3
이며, 이에 대응하는 Base64 문자는 각각 Q
, U
, J
, D
입니다. 따라서 “ABC”의 Base64 인코딩 결과는 QUJD
로 얻어집니다.
남은 데이터 처리 및 패딩
원본 데이터의 길이가 3의 배수라면 위 과정으로 끝나지만, 마지막에 남는 바이트가 1개 또는 2개인 경우에는 특별한 처리가 필요합니다. 우선 남는 바이트들을 3바이트 길이로 맞추기 위해 부족한 바이트 수만큼 0을 바이트 오른쪽에 덧붙여 24비트로 채웁니다. 그런 다음 6비트 청크로 나누어 Base64 문자로 변환하는데, 추가된 0들은 실제 데이터가 아니므로 인코딩 결과에 =
패딩 문자로 표시합니다.
1바이트 남을 경우
1바이트 남을 경우에는 해당 1바이트로 2개의 Base64 문자를 생성하고, 나머지 2개의 Base64 문자는 값이 없으므로 ==
두 개의 =
를 붙입니다. (예: M
→ TQ==
)
- 아스키 코드 및 이진수 변환:
M
의 아스키 코드는77
이고, 이진수로 변환하면01001101
- 24비트 블록 만들기(패딩 적용):
01001101 00000000 00000000
- 6비트씩 분할
- 그룹1: 첫 6비트 →
010011
- 그룹2: 남은 2비트(
01
)와 뒤에 4비트 0 채움 →010000
- 그룹3: 패딩으로 채워진 6비트 →
000000
- 그룹4: 패딩으로 채워진 6비트 →
000000
- 그룹1: 첫 6비트 →
- 각 그룹을 Base64 문자로 매핑
- 그룹1:
010011
→ 19 → Base64에서 19번째 값T
- 그룹2:
010000
→ 16 → Base64에서 16번째 값Q
- 그룹3과 그룹4는 실제 데이터가 없으므로 문자열에 직접 문자를 매핑하지 않고 각각
=
로 표기
- 그룹1:
- 최종 결과:
M
→TQ==
2바이트 남을 경우
2바이트 남을 경우에는 해당 2바이트로 3개의 Base64 문자를 생성하고, 남은 1개의 Base64 문자는 값이 없으므로 =
한 개를 붙입니다. (예: Ma
→ TWE=
)
- 아스키 코드 및 이진수 변환
M
의 아스키 코드는77
이고, 이진수로 변환하면01001101
a
의 아스키 코드는97
이고, 이진수로 변환하면01100001
- 24비트 블록 만들기(패딩 적용):
01001101 01100001 00000000
- 6비트씩 분할
- 그룹1: 첫 6비트 →
010011
- 그룹2:
M
의 나머지 2비트(01
)와a
의 처음 4비트(0110
) →010110
- 그룹3:
a
의 남은 4비트(0001
)와 뒤에 2비트 0 채움 →000100
- 그룹4: 패딩으로 채워진 6비트 →
000000
- 그룹1: 첫 6비트 →
- 각 그룹을 Base64 문자로 매핑
- 그룹1:
010011
→ 19 → Base64에서 19번째 값T
- 그룹2:
010110
→ 22 → Base64에서 22번째 값W
- 그룹3:
000100
→ 4 → Base64에서 4번째 값E
- 그룹4는 실제 데이터가 없으므로 문자열에 직접 문자를 매핑하지 않고 각각
=
로 표기
- 그룹1:
- 최종 결과:
Ma
→TWE=
3바이트의 배수로 딱 떨어질 경우
패딩 =
를 붙이지 않습니다. (예: Man
→ TWFu
)
이러한 패딩 문자의 사용으로 인해 Base64 인코딩 출력의 길이는 항상 4의 배수가 되며, 디코더는 문자열 끝의 =
개수를 보고 원본 데이터에 몇 바이트의 패딩이 있었는지 알 수 있습니다. 패딩된 Base64 문자열을 디코딩할 때는 =
를 제거하고 해당 부분의 데이터를 버림으로써 원본 바이너리를 복원합니다.
Base64 인코딩/디코딩 예제 코드
실제 프로그래밍 언어에서 Base64 인코딩과 디코딩을 다루는 방법을 살펴보겠습니다. 대부분의 언어는 표준 라이브러리나 API로 Base64 인코딩 기능을 제공하므로 쉽게 사용할 수 있습니다.
import base64
data = "Hello, Base64!"
encoded = base64.b64encode(data.encode('utf-8'))
print(encoded) # 출력: b'SGVsbG8sIEJhc2U2NCE='
print(encoded.decode()) # 출력: SGVsbG8sIEJhc2U2NCE=
decoded_bytes = base64.b64decode(encoded)
decoded_str = decoded_bytes.decode('utf-8')
print(decoded_str) # 출력: Hello, Base64!