Base64

Base64는 바이너리 데이터를 텍스트 형식으로 안전하게 변환하기 위한 인코딩 시스템이다. 1987년 개발된 이후 인터넷 통신, 데이터 저장, 프로그래밍 등 다양한 분야에서 필수적인 도구로 자리 잡았다.

Base64의 기본 개념과 역사

Base64는 이진 데이터를 ASCII 문자 집합의 부분집합인 64개 문자(A-Z, a-z, 0-9, +, /)를 사용하여 표현하는 인코딩 방식이다. 이름에 ‘64’가 붙은 이유는 정확히 이 64개의 문자를 사용하기 때문이다. 인코딩된 데이터의 끝을 나타내기 위해 패딩 문자로 ‘=‘를 사용하기도 한다.

Base64는 1987년 RFC 989에서 처음 도입되었으며, 이후 1993년 MIME(Multipurpose Internet Mail Extensions) 규격의 일부로 RFC 1521에 정식으로 포함되었다. 초기에는 이메일 시스템에서 바이너리 첨부 파일을 안전하게 전송하기 위한 목적으로 개발되었지만, 오늘날에는 웹 개발, 암호화, 데이터 저장 등 광범위한 분야에서 활용되고 있다.

Base64 인코딩의 작동 원리

Base64 인코딩은 다음과 같은 단계로 이루어진다:

  1. 바이너리 데이터를 8비트 바이트로 나눕니다.
  2. 이 바이트들을 6비트 단위로 재그룹화한다. (3바이트 = 24비트는 4개의 6비트 그룹으로 변환됨)
  3. 각 6비트 그룹을 Base64 문자 테이블의 해당 문자로 매핑한다.
  4. 입력 데이터의 길이가 3의 배수가 아닌 경우, 패딩 문자(’=’)를 추가하여 4의 배수 길이로 맞춘다.

이 과정을 시각적으로 설명하자면 다음과 같다:
원본 텍스트: “Man”
ASCII 값: 77(M), 97(a), 110(n)
바이너리: 01001101 01100001 01101110
6비트로 재그룹화: 010011 | 010110 | 000101 | 101110

각 6비트 값: 19, 22, 5, 46
Base64 인코딩 결과: “TWFu” (19=‘T’, 22=‘W’, 5=‘F’, 46=‘u’)

이 과정에서 주목할 점은 3바이트의 데이터가 4바이트의 Base64 문자로 변환된다는 것이다. 따라서 Base64 인코딩은 원본 데이터 크기보다 약 33% 더 큰 출력을 생성한다.

Base64 디코딩 과정

디코딩은 인코딩의 역과정으로 진행된다:

  1. 각 Base64 문자를 해당하는 6비트 값으로 변환한다.
  2. 6비트 값들을 결합하여 원래의 8비트 바이트로 재구성한다.
  3. 패딩 문자(’=’)는 무시한다.

예를 들어, “TWFu"를 디코딩하는 과정은 다음과 같다:

Base64 문자: T, W, F, u
해당 6비트 값: 19, 22, 5, 46
바이너리: 010011 010110 000101 101110
8비트로 재그룹화: 01001101 01100001 01101110
ASCII 값: 77(M), 97(a), 110(n)
디코딩 결과: “Man”

Base64 인코딩 표

Base64 인코딩에서 사용되는 64개 문자의 매핑은 다음과 같다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
값 | 문자     값 | 문자     값 | 문자     값 | 문자
--------------------+-----------------+------------------
 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 | /

패딩 문자: ‘=’

Base64 패딩

바이너리 데이터의 길이가 3의 배수가 아닌 경우, 패딩이 필요하다:

  1. 데이터가 1바이트 부족한 경우(2바이트만 있는 경우): 4개의 6비트 그룹 중 마지막 그룹이 완전하지 않으므로, 두 개의 ‘=’ 문자로 패딩한다.
  2. 데이터가 2바이트 부족한 경우(1바이트만 있는 경우): 마지막 두 그룹이 완전하지 않으므로, 하나의 ‘=’ 문자로 패딩한다.

예시:

다양한 프로그래밍 언어에서의 Base64 구현

JavaScript에서의 Base64

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 인코딩
function encodeBase64(str) {
  return btoa(str);
}

// 디코딩
function decodeBase64(str) {
  return atob(str);
}

// 사용 예시
const original = "안녕하세요, Base64!";
// 유니코드 문자열은 바이너리로 변환 후 인코딩해야 함
const encoded = btoa(unescape(encodeURIComponent(original)));
console.log(encoded); // 7JWI64WV7ZWY7IS47JqULCBCYXNlNjQh
const decoded = decodeURIComponent(escape(atob(encoded)));
console.log(decoded); // 안녕하세요, Base64!

Python에서의 Base64

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import base64

# 인코딩
def encode_base64(s):
    # 문자열을 바이트로 변환 후 인코딩
    return base64.b64encode(s.encode('utf-8')).decode('utf-8')

# 디코딩
def decode_base64(s):
    # Base64 문자열을 디코딩 후 문자열로 변환
    return base64.b64decode(s).decode('utf-8')

# 사용 예시
original = "안녕하세요, Base64!"
encoded = encode_base64(original)
print(encoded)  # 7JWI64WV7ZWY7IS47JqULCBCYXNlNjQh
decoded = decode_base64(encoded)
print(decoded)  # 안녕하세요, Base64!

Java에서의 Base64

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class Base64Example {
    // 인코딩
    public static String encodeBase64(String input) {
        return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
    }
    
    // 디코딩
    public static String decodeBase64(String input) {
        byte[] decodedBytes = Base64.getDecoder().decode(input);
        return new String(decodedBytes, StandardCharsets.UTF_8);
    }
    
    public static void main(String[] args) {
        String original = "안녕하세요, Base64!";
        String encoded = encodeBase64(original);
        System.out.println(encoded);  // 7JWI64WV7ZWY7IS47JqULCBCYXNlNjQh
        String decoded = decodeBase64(encoded);
        System.out.println(decoded);  // 안녕하세요, Base64!
    }
}

Base64의 실제 응용 사례

  1. 이메일 첨부 파일
    이메일 프로토콜(SMTP)은 원래 7비트 ASCII 텍스트만 지원했기 때문에, 바이너리 파일(이미지, 문서 등)을 전송하기 위해서는 Base64 인코딩이 필요했다. MIME 규격에서는 이메일 첨부 파일을 Base64로 인코딩하여 전송하도록 규정하고 있다.

  2. 웹에서의 데이터 URI
    HTML이나 CSS에서 이미지를 직접 포함시키기 위해 Data URI 스키마를 사용할 수 있는데, 이 때 Base64 인코딩이 활용된다:

    1
    
    <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA…" alt="Base64 이미지">
    

    이 방식은 작은 이미지를 HTML 문서에 직접 포함시켜 HTTP 요청 수를 줄이는 데 유용하다.

  3. API 및 웹 서비스 통신
    JSON이나 XML 같은 텍스트 기반 데이터 포맷에서 바이너리 데이터를 포함시킬 때 Base64가 활용된다. 특히 RESTful API에서 이미지나 파일을 전송할 때 자주 사용된다.

    1
    2
    3
    4
    5
    6
    
    {
      "user": {
        "name": "홍길동",
        "profile_image": "iVBORw0KGgoAAAANSUhEUgAAAAUA…"
      }
    }
    
  4. 인증 시스템
    Basic 인증과 같은 HTTP 인증 방식에서 사용자 이름과 비밀번호를 Base64로 인코딩하여 전송한다:

    1
    
    Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
    

    여기서 dXNlcm5hbWU6cGFzc3dvcmQ=는 “username:password"를 Base64로 인코딩한 것이다. 단, Base64 인코딩은 암호화가 아니므로 보안을 위해서는 HTTPS와 함께 사용해야 한다.

  5. JWT(JSON Web Tokens)
    JWT는 클레임 정보를 안전하게 전송하기 위한 컴팩트한 방식으로, 헤더, 페이로드, 서명 부분을 각각 Base64URL 인코딩하여 점(.)으로 구분한다.

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  1. 데이터베이스 저장
    바이너리 데이터를 텍스트 기반 데이터베이스에 저장할 때 Base64 인코딩을 사용할 수 있다. 특히 NoSQL 데이터베이스나 JSON 문서 저장소에서 이미지나 파일을 저장할 때 유용하다.

Base64의 변형

  1. Base64URL
    URL 및 파일 이름에서 안전하게 사용할 수 있도록 ‘+‘와 ‘/’ 문자를 각각 ‘-‘와 ‘_‘로 대체한 변형이다.
    URL에서 특별한 의미를 가지는 문자를 피하기 위해 설계되었다.
    JWT와 같은 웹 기술에서 널리 사용된다.

    1
    2
    3
    4
    5
    6
    7
    
    // Base64URL 인코딩 예시 (JavaScript)
    function base64UrlEncode(str) {
      return btoa(str)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
    }
    
  2. Base32
    32개의 문자(A-Z, 2-7)를 사용하는 인코딩 방식으로, 5비트 단위로 데이터를 표현한다.
    대소문자를 구분하지 않고, 숫자 1, 8, 9, 0은 혼동을 피하기 위해 제외되었다.
    TOTP(Time-based One-Time Password) 같은 보안 애플리케이션에서 사용된다.

  3. Base16 (Hex)
    16개의 문자(0-9, A-F)를 사용하는 인코딩 방식으로, 4비트를 하나의 문자로 표현한다.
    사실상 16진수 표기법과 동일하다.
    바이너리 데이터를 사람이 읽을 수 있는 형태로 표현할 때 자주 사용된다.

Base64의 성능과 효율성

  1. 인코딩 효율성
    Base64 인코딩은 3바이트의 데이터를 4바이트의 텍스트로 변환하므로, 인코딩 후 데이터 크기가 약 33% 증가한다. 이 오버헤드는 큰 파일을 인코딩할 때 중요한 고려사항이 될 수 있다.
    효율성 계산:

    • 원본 데이터 크기: n바이트
    • Base64 인코딩 후 크기: ⌈(n × 8) ÷ 6⌉ 바이트 (패딩 고려)
  2. 계산 비용
    Base64 인코딩/디코딩은 비교적 간단한 비트 연산을 사용하므로 계산 비용이 낮다. 그러나 대용량 데이터에 대해서는 성능 최적화를 고려할 필요가 있다.

  3. 압축과의 관계
    Base64 인코딩된 데이터는 원본 바이너리 데이터보다 압축 효율이 떨어질 수 있다. 따라서 큰 파일을 전송할 때는 먼저 압축한 후 Base64 인코딩하는 것이 효율적이다.

Base64 인코딩의 자체 구현

Base64 인코딩 알고리즘을 직접 구현해보면 그 작동 원리를 더 깊이 이해할 수 있다.
다음은 JavaScript로 구현한 간단한 Base64 인코더/디코더:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Base64 인코딩 테이블
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

// 인코딩 함수
function customBase64Encode(str) {
  let result = '';
  let i = 0;
  let bytes = new Uint8Array(new TextEncoder().encode(str));
  
  while (i < bytes.length) {
    // 3바이트를 6비트씩 4개 그룹으로 변환
    let chunk = 0;
    chunk |= (i < bytes.length) ? bytes[i++] << 16 : 0;
    chunk |= (i < bytes.length) ? bytes[i++] << 8 : 0;
    chunk |= (i < bytes.length) ? bytes[i++] : 0;
    
    // 6비트씩 추출하여 Base64 문자로 변환
    result += base64Chars[(chunk >> 18) & 63];
    result += base64Chars[(chunk >> 12) & 63];
    
    // 패딩 처리
    if (i - 3 < bytes.length) {
      result += base64Chars[(chunk >> 6) & 63];
    } else {
      result += '=';
    }
    
    if (i - 2 < bytes.length) {
      result += base64Chars[chunk & 63];
    } else {
      result += '=';
    }
  }
  
  return result;
}

// 디코딩 함수
function customBase64Decode(str) {
  // 패딩 제거
  str = str.replace(/=+$/, '');
  
  let result = [];
  let i = 0;
  
  while (i < str.length) {
    // 4개의 Base64 문자를 3바이트로 변환
    let chunk = 0;
    chunk |= base64Chars.indexOf(str[i++]) << 18;
    chunk |= base64Chars.indexOf(str[i++]) << 12;
    chunk |= (i < str.length) ? base64Chars.indexOf(str[i++]) << 6 : 0;
    chunk |= (i < str.length) ? base64Chars.indexOf(str[i++]) : 0;
    
    // 3바이트 추출
    result.push((chunk >> 16) & 255);
    if (i - 3 > 0 || str.length % 4 === 3) result.push((chunk >> 8) & 255);
    if (i - 2 > 0 || str.length % 4 === 2) result.push(chunk & 255);
  }
  
  return new TextDecoder().decode(new Uint8Array(result));
}

// 사용 예시
const original = "Base64 인코딩 예시";
const encoded = customBase64Encode(original);
console.log(encoded);
const decoded = customBase64Decode(encoded);
console.log(decoded);

Base64 관련 오해와 주의사항

  1. Base64는 암호화가 아니다.
    Base64는 단순히 데이터 표현 방식일 뿐, 암호화 알고리즘이 아니다.
    따라서 보안을 위해 Base64만 사용하는 것은 적절하지 않다.
    민감한 정보는 반드시 적절한 암호화 알고리즘으로 보호해야 한다.

  2. 패딩 관련 문제
    일부 Base64 구현체는 패딩 처리 방식이 다를 수 있다. 표준 Base64는 패딩을 사용하지만, 일부 변형(예: Base64URL)에서는 패딩을 생략하기도 한다. 서로 다른 시스템 간에 Base64 데이터를 교환할 때 이 점을 고려해야 한다.

  3. 줄 바꿈 처리
    MIME 규격에서는 Base64 인코딩된 데이터의 각 줄 길이를 76자로 제한하고 있다. 일부 구현체는 이 규칙을 따르지만, 다른 구현체는 줄 바꿈 없이 연속된 문자열을 생성할 수 있다. 디코딩 시에는 이러한 차이를 처리할 수 있어야 한다.

  4. 인코딩 오버헤드
    앞서 언급했듯이, Base64 인코딩은 데이터 크기를 약 33% 증가시킨다. 대용량 데이터를 인코딩할 때는 이 오버헤드를 고려해야 한다.

Base64의 미래와 대안

  1. 바이너리 데이터 처리 발전
    최신 웹 기술(예: WebSocket, Fetch API의 blob 처리)은 바이너리 데이터를 직접 처리할 수 있는 기능을 제공하고 있어, 일부 상황에서는 Base64 인코딩의 필요성이 줄어들고 있다.

  2. Uint8Array와 같은 타입화된 배열
    JavaScript와 같은 현대 프로그래밍 언어에서는 Uint8Array와 같은 타입화된 배열을 통해 바이너리 데이터를 효율적으로 처리할 수 있다.

  3. Base85와 같은 대안
    Base85(또는 Ascii85)는 85개의 ASCII 문자를 사용하여 4바이트의 데이터를 5개의 문자로 표현하는 인코딩 방식이다. Base64보다 인코딩 효율이 높지만(약 25% 증가), 구현이 복잡하고 널리 지원되지 않는다는 단점이 있다.


용어 정리

용어설명

참고 및 출처