SQL에서의 Base64 인코딩: 완전한 가이드
가끔은 데이터베이스가 바깥 세상과 이야기를 해야 하는데, 바깥 세상은 늘 바이트를 말하지 않습니다. 어떤 API는 JSON 문자열 안의 로고를 원합니다. 설정 익스포트는 따옴표 없이, 백슬래시 없이 YAML 한 줄에 들어맞는 시크릿을 원합니다. 유지보수 스크립트는 텍스트만 실어 나르는 시스템을 통해 파일을 보내고 싶어 합니다. 바로 그때, 여러분의 데이터가 글자라는 분장 의상을 입게 되고, 그 의상의 이름이 base64입니다.
포맷 자체는 홈 페이지에서 이미 다루었습니다(인쇄 가능한 문자 64개, 4글자마다 입력 바이트 3개를 대신하며, 마지막 그룹을 채우는 = 기호가 최대 두 개), 그래서 이 글은 그 강의는 건너뛰고 바로 기계적인 일로 갑니다. 붙들어 둘 것이 두 가지입니다: 인코딩은 데이터가 커지는 방향이라 열 너비와 패킷 한도가 그 모든 바이트를 느끼고, 이 SQL 가족의 인코더들은 나중에 되돌리기 가장 어려운 두 가지에서 의견이 다릅니다. 열에 텍스트가 들어 있을 때 어떤 바이트를 읽을지, 그리고 쓰는 결과물에 줄바꿈을 어디다 놓을지.
인코더 치트 시트
지금 당직 중인 멤버는 누구인지, 무엇을 입력으로 삼는지, 출력을 어디서 부러뜨리는지입니다. 마지막 두 열이 물고 들어오는 열입니다: 초대받지 못한 줄바꿈으로 가득한 문자열도, 다른 알파벳의 문자열도, 둘 다 완벽하게 유효한 base64 문자열이지만 소비자 쪽에서는 여전히 거부당하니까요:
| 방언 | 호출 | 입력 타입 | 76에서 래핑? | URL-safe 옵션 | 언제부터 |
|---|---|---|---|---|---|
| MySQL 8.x / MariaDB 10.x | TO_BASE64(str) |
문자열 (문자 인코딩 적용) | 예 | 없음 | MySQL 5.6 (2013) |
| PostgreSQL | encode(bytea, 'base64') |
bytea |
예, LF만 | 없음 | 7.2 (2002) |
| SQLite (CLI 3.41+) | base64(blob) |
BLOB |
예, 72에서 | 없음 | 3.41.0 (2023) |
| DuckDB | to_base64(blob) |
BLOB |
아니오 | 없음 | 최신 릴리스 |
| ClickHouse 18.16+ | base64Encode(x) |
무엇이든, String으로 캐스트 | 아니오 | base64URLEncode() |
18.16 (2018) |
| SQL Server 2025+ | BASE64_ENCODE(bin [, url_safe]) |
varbinary |
아니오 | 두 번째 인수 | 2025 |
| Oracle | UTL_ENCODE.BASE64_ENCODE(raw) |
RAW |
아니오 | 없음 | 9i 시절 |
| Snowflake | BASE64_ENCODE(binary) |
BINARY |
아니오 | 없음 | 최신 릴리스 |
표에서 왼쪽을 오른쪽으로 읽으면, 일은 두 결정으로 나뉩니다. 첫째, 여러분의 바이트가 어떻게 그곳에 도착하는가: 입력 타입 열은 문자 인코딩 서프라이즈가 태어나는 곳입니다. "같은 텍스트"도 콜레이션이 다르면 다른 바이트이니까요. 둘째, 다른 쪽에서 무엇이 나오는가: 래핑 열이 여러분의 결과가 한 줄짜리 평탄한 것이 될지, 76자마다 줄바꿈이 있는 시가 될지를 결정하고, URL-safe 열이 그 결과를 링크에 아예 넣을 수 있는지를 결정합니다.
먼저, 어떤 바이트를 뜻하는지 정하세요
인코더는 바이트를 압축하지만, 여러분의 열은 보통 글자를 담고 있고, 글자가 바이트가 되는 것은 여러분이 어떤 바이트 알파벳을 말하는지 말해 줄 때입니다. MySQL의 TO_BASE64()는 인수를 연결 문자 인코딩으로 읽는데, 이게 편의가 아닌 순간까지 편리합니다: 같은 'héllo'도 latin1 클라이언트와 utf8mb4 클라이언트 아래에서는 다른 base64로 발송됩니다. 저장된 바이트 그대로를 뜻한다면, 먼저 바이너리 캐스트로 그것을 고정하세요:
SELECT TO_BASE64('hello') AS from_text;
SELECT TO_BASE64(CAST('héllo' AS BINARY)) AS utf8_bytes;
두 번째 행은 aMOpbGxv로 돌아오며, 그 가운데의 두 16진 바이트 C3 A9가 é를 UTF-8식으로 쓰는 방법입니다. PostgreSQL은 처음부터 더 엄격합니다: encode()는 bytea가 아닌 것은 아예 보기를 거부하므로, 텍스트 값은 먼저 자기 인코딩을 명시해야 하고, 생 바이트는 16진 리터럴로 올 수 있습니다:
SELECT encode(convert_to('héllo', 'UTF8'), 'base64') AS text_as_utf8;
SELECT encode('\x68656c6c6f'::bytea, 'base64') AS raw_bytes;
나머지 모든 방언은 같은 아이디어로 들어가는 자기만의 정문을 갖고 있으며, 전부 "바이트를 가져오고, 압축한다"로 수렴합니다:
| 방언 | 텍스트에서 바이트로 | 인코더 호출 |
|---|---|---|
| T-SQL | CAST('héllo' AS VARBINARY(8000)), 열의 콜레이션을 거치는 |
BASE64_ENCODE(bin) |
| Snowflake | TO_BINARY('héllo', 'UTF-8') |
BASE64_ENCODE(binary) |
| Oracle | UTL_RAW.CAST_TO_RAW('héllo') |
UTL_ENCODE.BASE64_ENCODE(raw) |
| DuckDB | encode('héllo')는 BLOB |
to_base64(blob) |
| SQLite CLI | 리터럴인 BLOB 값, X'68656C6C6F' |
base64(blob) |
실용적 규칙은 디코딩 쪽과 같습니다: 인코딩 전에 문자 인코딩을 정하고, 쿼리에 리터럴로 쓰며, 열을 믿기 전에 강세부호가 있는 페이로드 하나를 온전한 파이프라인을 통과시켜 보세요. héllo 하나면 틀린 콜레이션을 전부 잡을 수 있고, 공짜입니다.
그리고, 무엇이 나오는지 지켜 보세요
바이트가 압축되면, 인코더들은 줄바꿈을 놓고 갈라섭니다. 셋이 출력을 래핑합니다: MySQL과 PostgreSQL은 76자, 이메일의 습관대로, 그리고 SQLite CLI는 72에서. 나머지는 길이가 아무리 늘어도 한 줄짜리 평탄한 것을 돌려줍니다. 차이는 놓치기 쉽고 발견할 땐 비쌉니다. 숨은 줄바꿈을 가진 base64 필드는 JSON 파서를 반쯤에서 부러뜨리는 필드이니까요:
SELECT LENGTH(TO_BASE64(REPEAT('x', 300))) AS wrapped,
LENGTH(REPLACE(TO_BASE64(REPEAT('x', 300)), '\n', '')) AS flat;
300바이트의 입력은 400개의 base64 문자로 돌아오고, 같은 호출은 405로 측정됩니다. 다섯 줄바꿈이 함께 탑승했으니까요. 그 뒤의 산술은 머리에 넣어 둘 만큼 작습니다: 평탄한 길이는 입력 길이를 3으로 나눈 뒤 올림하고, 4를 곱한 것입니다. 인코더가 래핑한다면, 76자 행 사이에 줄바꿈 하나씩을 더하세요. 평탄한 길이를 76으로 나눈 값의 올림에 1을 뺀 것이죠. 300바이트: 400 평탄, 405 래핑. 111바이트: 148 평탄, 149 래핑. 예산보다 줄바꿈이 하나 더 있으면, VARCHAR(500) 열이 VARCHAR(480) 페이로드를 조용히 잘리기 시작하는 바로 그 방법입니다.
쓰어 둘 가치가 있는 결과가 두 가지입니다. 작성자가 래핑할 수 있다면, 텍스트 열은 평탄한 길이에 조금의 여유를 더해 사이즈를 정하세요. 아니면 작성자에서 래핑을 금지하고 평탄한 길이에 맞춘 것으로 하세요. 그리고 기억하세요: 여러분의 결과가 싸우는 한도는 문자열의 한도이지, 바이트의 한도가 아닙니다. MySQL에서는 래핑된 텍스트가 max_allowed_packet(MySQL 8 기본값 64 MB)에 대고 계산되므로, 50메가바이트 사진이 약 67메가바이트의 글자로 인코딩되면, 생 파일은 들어맞아도 기본 패킷에는 들어가지 않습니다.
URL-safe Base64: 여행하는 알파벳
RFC 4648의 5절은 base64를 위한 두 번째 알파벳을 정의했습니다. 원래 알파벳에는 URL 문법에서 역할을 하는 문자가 두 개가 있기 때문이죠. 더하기 기호는 쿼리 매개변수를 더하는 것이고, 슬래시는 경로 세그먼트를 나누는 것이며, 패딩 등호는 쿼리 스트링을 만나는 순간 퍼센트 인코딩됩니다. URL-safe 변형은 +를 -로, /를 _로 바꾸고, 그 위에 JWT 명세는 패딩을 통째로 버립니다. 그래서 토큰은 퍼센트 기호가 하나 없이 링크, 경로 세그먼트, 파일명에 앉아 있을 수 있습니다.
이 가족에서 스위치를 네이티브로 동봉하는 방언은 하나뿐입니다. SQL Server 2025의 BASE64_ENCODE()는 선택 가능한 두 번째 인수를 가지며, 그것이 켜지면 결과는 -와 _를 쓰고 패딩을 건너뜁니다:
SELECT BASE64_ENCODE(0xCAFECAFE) AS standard;
SELECT BASE64_ENCODE(0xCAFECAFE, TRUE) AS url_safe;
같은 네 바이트가 yv7K/g==와 yv7K_g로 돌아옵니다. ClickHouse는 변형들을 별개의 함수로 유지하고, 그 URL-safe 형태도 패딩을 버립니다:
SELECT base64URLEncode('https://clickhouse.com') AS url_safe;
그것은 aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ로 도착하는데, 표준 형태의 두 패딩 기호가 잘려 나간 것입니다. 다른 모든 곳에서는 레시피가 두 문자 번역과 한 트리밍이며, 모든 토큰 파이프라인이 필요로 하므로 데이터베이스 함수로 한 번 써 둘 가치가 있습니다. PostgreSQL에서는 이렇게 읽힙니다:
SELECT rtrim(replace(replace(
encode(convert_to('https://clickhouse.com', 'UTF8'), 'base64'),
'+', '-'),
'/', '_'),
'=') AS url_safe;
변환: +를 -로, /를 _로 번역하고, 끝 패딩을 잘라 내면 끝입니다. SQL Server 쪽에 경고 하나: url_safe 출력은 서버 자신의 XML과 JSON base64 디코더가 기대하는 것이 아니므로, 바깥 세상을 위해 URL-safe 형태로 압축된 열은 내장 함수로 데이터베이스 안에서 되돌릴 수 없습니다. 알파벳을 고르기 전에 청중을 생각하세요.
JWT: 데이터베이스로 찍어 내는 토큰
인코더로 만들 수 있는 것 중 가장 흥미로운 것은 JSON Web Token입니다. JWT란 그저 나란히 놓인 base64 조각 세 개일 뿐이므로: 헤더와 페이로드(둘 다 패딩 없는 URL-safe로 압축된 JSON 객체)와, 첫 두 조각 위에서 계산된 서명. 배치 작업이 토큰을 발행해야 할 때(테스트 환경을 시드하거나, 만료된 API 자격 증명을 다시 만들거나, 감사 피드를 만들거나), HMAC을 위해 pgcrypto를 받아들인다면 온전한 의식은 하나의 PostgreSQL 쿼리에 들어갑니다(CREATE EXTENSION IF NOT EXISTS pgcrypto;로 한 번 활성화하세요):
WITH head AS (
SELECT encode(convert_to('{"alg":"HS256","typ":"JWT"}', 'UTF8'), 'base64') AS h
),
body AS (
SELECT encode(convert_to('{"sub":"1234567890","name":"Dev User"}', 'UTF8'), 'base64') AS p
),
joined AS (
SELECT rtrim(replace(replace(h, '+', '-'), '/', '_'), '=') AS h64u,
rtrim(replace(replace(p, '+', '-'), '/', '_'), '=') AS p64u
FROM head, body
)
SELECT h64u || '.' || p64u || '.' ||
rtrim(replace(replace(
encode(hmac((h64u || '.' || p64u)::bytea, 'sql-secret-key'::bytea, 'sha256'), 'base64'),
'+', '-'),
'/', '_'),
'=') AS token
FROM joined;
각 단계는 이 글이 이미 보여 준 기법들 중 하나입니다: JSON을 base64로 압축하고, 패딩 없는 URL-safe 알파벳으로 모양을 바꾼 뒤, 첫 두 조각에 서명을 하고 서명도 같은 방식으로 모양을 바꾸는 것. 위의 JSON과 시크릿 sql-secret-key에 대해, 결과는 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIn0.7_3VdmM8vH0L2bRBNqXXQXzZIR1l8T4_S4QxPPNBEFM이며, 어떤 HS256 검사자도 받아들일 토큰입니다. 경고들도 트릭 못지않게 말할 자격이 있습니다: HMAC 알고리즘(HS256, HS384, HS512)만 다루고, 공유 시크릿을 데이터베이스 문 안에 넣으며, 프로덕션 토큰 서비스가 아니라 배치와 감사 작업을 위해 만들어졌으니까요. 그 토큰을 시크릿에 대해 증명하는 검증 쪽은, 애플리케이션 계층의 일이거나 디코딩 글의 서명 검사입니다.
텍스트 열의 이미지와 파일
SQL에서 인코딩을 하는 가장 흔한 이유는, 텍스트로 여행해야 하는 파일입니다: 이미지를 참조 대신 인라인으로 넣는 API, 바이너리를 실지 못하는 시스템을 위한 익스포트, 새 서버에서 데이터베이스를 다시 만드는 시드 스크립트. DuckDB는 왕복을 거의 자명하게 만듭니다: glob 패턴을 받는 테이블 함수로 파일을 BLOB으로 읽고, 인코더는 도착한 것을 무엇이든 평탄하게 만들어 주니까요:
SELECT filename, to_base64(content) AS b64
FROM read_blob('/data/pics/*.png');
파일당 한 행, 행당 평탄한 base64 문자열 하나, 벗겨 낼 래핑은 없지만, 평소의 패딩 기호 하나둘이 끝에 따라 탑니다. 결과를 텍스트 열에 쓰면, 이미지는 텍스트를 옮기는 어떤 채널로도 운반 가능합니다. 그리고 비용을 정직하게 이야기합시다: 1메가바이트 사진은 약 1.33메가바이트의 글자로 도착하고, 그 뒤로는 모든 스캔, 정렬, 색인 항목이 그 대가를 치릅니다. 스키마를 좌우할 수 있다면, 더 나은 설계는 BLOB 열에 API 경계의 인코딩입니다. 실제로 건물 밖으로 나가는 바이트만 분장하니까요.
HTTP, JSON, 그리고 API 트래픽
헤더와 페이로드는 base64가 조용한 일상 일을 하는 곳입니다. Basic 인증 헤더는 리터럴 접두사 Basic 뒤에 username:password의 base64가 따라온 것이며, SQL에서 하나를 만드는 것은 결합 하나와 인코딩 하나입니다:
SELECT 'Basic ' || encode(convert_to('alice' || ':' || 's3cret', 'UTF8'), 'base64') AS header;
이것은 Basic YWxpY2U6czNjcmV0를 만들어 냅니다. 클라이언트가 보내올 정확한 헤더가죠. 통합 테스트가 비교할 픽스처를 생성하거나, 감사하기 전에 저장된 헤더 열을 정규화하는 데 쓰세요. JSON 쪽에서는, MySQL이 필드를 압축해 문서 안에 임베드하는 것을 한 식으로 할 수 있습니다. 애플리케이션 코드는 처리 흐름에 끼어들지 않습니다:
SELECT JSON_OBJECT('img', TO_BASE64(CAST('hello file' AS BINARY))) AS doc;
결과는 {"img": "aGVsbG8gZmlsZQ=="}, 즉시 보낼 준비가 된 페이로드입니다. 같은 모양은 인증서, 공개 키, 그리고 여러분의 API가 인라인하기로 결정한 모든 다른 파일에도 동작하며, 당신이 트래픽을 디코딩하는 쪽이 아니라 트래픽을 생산하는 쪽일 때 중요한 방향입니다.
설정 파일, 시크릿, 그리고 환경 변수
익스포트 습관 중 하나가 자기만의 단락을 받을 자격이 있습니다. 어디에나 있기 때문이죠: 설정 테이블에 base64로 저장된 시크릿. Kubernetes가 이 습관을 살려 왔는데, 거기서는 시크릿 값이 저장 시 base64라 따옴표도, 줄바꿈도, 백슬래시 없이도 YAML 한 줄에 들어맞습니다. Kubernetes 파이프라인과 한 번이라도 마주친 모든 사내 설정 시스템은 그것을 습관으로 삼았고요. 압축 방향은 값당 인코딩 하나이며, 바이너리 캐스트가 문자 인코딩 일을 해 주므로 익스포트된 텍스트가 정확히 저장된 바이트가 됩니다:
SELECT name, TO_BASE64(CAST(value AS BINARY)) AS for_config
FROM app_config
WHERE name LIKE '%_secret%';
57바이트 이하의 각 값은 평탄하고 붙여넣기 준비가 된 문자열로 나옵니다. 더 긴 것은 YAML에 들어가기 전에 래핑 줄을 벗겨 낼 REPLACE() 하나가 필요하고, 새 환경은 반대쪽에서 그것을 다시 디코딩합니다. 그 결과를 그에 걸맞은 신중함으로 다뤄야 합니다: 방금 저장된 시크릿의 열을, 어떤 인간도 약 열 초면 읽을 수 있는 저장된 시크릿의 열로 바꿔 버렸으니까요. 그리고 평문에 이 정도로 가까이 서 있는 순간, 설정 안에 base64를 넣는 습관은 두 번째 시선을 받습니다. Base64는 운송이지 금고가 아닙니다. 환경에 진짜 시크릿 저장소가 있다면, base64 열은 그것으로부터의 마이그레이션입니다.
이메일과 76자의 습관
76자 래핑은 이 페이지의 모든 데이터베이스보다 오래됐습니다. MIME, 즉 이메일이 바이너리 첨부 파일을 실을 수 있게 하는 표준 모음(RFC 2045, 6.8절, 1996)은 base64 출력을 76자에서 래핑하고, 각 줄을 캐리지 리턴과 줄바꿈으로 끝냅니다. 옛 이메일 네트워크가 더 긴 줄을 신뢰할 수 없었기 때문이죠. 여기 세 인코더가 래핑을 기본값으로 물려 받았습니다(MySQL, PostgreSQL, SQLite CLI). 이메일에 들어간 것에게는 선물이고, 들어가지 않은 것에게는 함정입니다. 그리고 절반으로만 물려 받았습니다: PostgreSQL은 줄을 홀로 남은 줄바꿈으로 끝내지, MIME 표준이 정한 캐리지 리턴과 줄바꿈으로 끝내지 않으므로, 진짜 이메일 첨부 파일에 붙여넣어야 할 출력은 한 번 더 지나가야 합니다:
WITH t AS (
SELECT replace(encode(attachment::bytea, 'base64'), chr(10), '') AS s
FROM email_outbox
)
SELECT regexp_replace(s, '(.{1,76})', '\1' || chr(13) || chr(10), 'g') AS mime_ready
FROM t;
PostgreSQL이 이미 추가한 줄바꿈을 걷어 내고, 다시 76에서 래핑하되 매 줄 뒤로 온전한 CRLF를 붙이면, 문자열은 한 식으로 MIME에 맞게 됩니다. 300바이트를 그 안을 통과시키면, 압축한 400글자가 412가 됩니다: 래핑된 줄 여섯, CRLF 쌍 여섯, 운송 의식의 글자 열둘. 같은 모양의 문제는 64에서 래핑하는 PEM 블록과, 필드 한가운데에서 줄바꿈을 만나지 않으려는 JSON 파서를 가진 API(아예 래핑을 원하지 않는)에서도 나타납니다. 모두에게 적용되는 규칙: 인코딩하기 전에 소비자가 어떤 계약을 맺었는지 알아 내세요. 저장된 base64 열을 다시 래핑하는 것은 쿼리가 아니라 마이그레이션이니까요.
함정들: 인코더가 당신에게 거짓말하는 곳
이 목록의 모든 함정은 base64가 놓은 것이 아니라, 특정 방언이 놓은 것이며, 하나같이 프로덕션에서 그것을 발견한 코드베이스가 적어도 하나씩 있습니다:
- 청하지 않은 래핑. MySQL과 PostgreSQL은 기본값으로 출력을 76에서 래핑하고, SQLite CLI는 72에서인데, 여러분의 쿼리에서 아무도 그것을 요청하지 않았습니다. 여러분의 JSON의 base64 필드에는 이제 줄바꿈이 들어 있고, 명세에서 포맷을 받는 소비자가 현장에서 그것을 거부합니다. 평탄한 형태는 줄바꿈 문자에 대한
REPLACE()하나이며, 열이 읽는 쪽이 원하는 것을 저장하도록 작성자 쪽에서 적용합니다. - 문자 인코딩 실수.
TO_BASE64('héllo')에 바이너리 캐스트가 없으면, 연결 문자 인코딩이 글자를 무엇으로 믿는지를 그대로 인코딩하며,latin1클라이언트와utf8mb4클라이언트는 다른 것을 믿습니다. 같은 쿼리 텍스트, 두 가지 다른 base64 결과, 그리고 틀린 쪽은 인코더로 거슬러 올라가지 않는 깨진 문자로 디코딩됩니다. 바이너리 캐스트, 또는 명시적인convert_to()가 이 쿼리의 유일한 정직한 버전입니다. - BLOB 문. DuckDB의
to_base64()는BLOB를 원합니다: 텍스트를 주면 암묵적으로 캐스트하므로, UTF-8이 아닌 인코딩의varchar열이 조용히 틀린 바이트를 인코딩할 수 있습니다. 정직한 경로가to_base64(encode(...))이며, 이 페이지의 예가 텍스트 입력에 항상 그 쌍을 보여주는 이유입니다. - 26.7 공백 변경. ClickHouse의
base64Decode()와base64URLDecode()는 수년간 입력에 대해 엄격했지만, 26.7부터는 거부 대신 공백(공백, 탭, 줄바꿈, 캐리지 리턴, 폼 피드)을 무시하므로, 래핑된 열에서 예전엔 에러를 내던 스크립트가 이제 조용히 성공합니다. 그것 자체로 하나의 회귀죠. 디코딩을 믿기 전에 서버 버전을 확인하세요. - 6000바이트 경계. SQL Server의
BASE64_ENCODE()는varchar(8000)를 돌려주는데, 입력이 n이 6000 이하인varbinary(n)인 경우이며, 그 이상이면varchar(max)를 돌려줍니다. 매핑은 값이 아니라 선언된 크기에 근거하므로,varbinary(8000)열은 3바이트여도varchar(max)를 돌려줍니다. 그 사이, 같은 함수의url_safe출력은 서버 자신의 XML과 JSON base64 디코더가 읽을 수 없는데, 그것들은 패딩 있는 표준 알파벳을 기대하니까요. 그것을 읽을 청중을 위해 변형을 고르세요. - 2000바이트 RAW. Oracle에서는 순수 SQL 문장의
RAW값이 2000바이트가 상한입니다. 인코더가RAW를 받아RAW를 돌려주므로, 단일 문장 인코딩은 약 1500바이트의 입력만 받을 수 있고(그 2000문자의 출력은 들어맞지만, 그 이상은 못 들어갑니다), 단일 문장 디코딩은 2000문자의 base64만 받을 수 있습니다. 더 큰 페이로드는 PL/SQL로 옮기는데, 거기서RAW변수는 32767바이트를 갖고 한 호출이 대부분을 실어 나르며, base64 출력이 그 상한을 초과하는 페이로드만 청크 루프가 필요합니다. 1990년대 한도에서 한 번도 움직인 적 없는 루프죠. - 패킷 세금. MySQL은 인코딩된 문자열을
max_allowed_packet에 대고 세고, 생 바이트가 아닙니다. 표에 남을 만큼 들어맞던 사진도 33퍼센트 더 커진 채 래핑되면 패킷을 넘칠 수 있고, 실패 양상은 잘린 값 또는 데이터 손상처럼 보이는NULL입니다. 열 너비와 같은 호흡으로 그 한도를 확인하세요. - 맨 끝의 줄바꿈. SQLite CLI의
base64()는 마지막 줄을 줄바꿈으로 끝냅니다. 줄 끝맺음 습관이 마지막 줄에도 적용된 것이죠. 셸 출력을 JSON 필드에 붙여넣으면, 줄바꿈이 든 base64 문자열을 발송한 셈입니다. 청하지 않은 래핑이 다른 모자를 쓴 것이죠. - 알파벳 가정. 표준 알파벳용으로 지은 소비자가 여러분의 URL-safe 출력을 만나면(또는 그 반대로) 알지 못하는 문자를 봅니다. 대부분의 디코더는 밑줄에서 우렁차게 실패하고, 일부는 그것을 건너뛰며 조용히 실패합니다. 다음 개발자는 그 행을 어떤 토큰 파이프라인이 썼는지 기억하지 못할 테니, 모든 base64 열의 알파벳을 스키마 주석에 문서화하세요.
크기가 실제로 중요한 순간
크기 산술은 평탄한 길이에 인코더가 추가하는 래핑을 더한 것이고, 비율의 온전한 유도 과정은 홈 페이지에서 합니다. 여기서 할 가치가 있는 것은, 그 숫자가 호기심을 벗어 나는 곳을 걸어 보는 것입니다. 입력의 바이트 수에 맞춘 VARCHAR 열은, 페이로드가 추가 여유 공간을 필요로 할 만큼 긴 첫 순간에 출력을 조용히 잘깁니다. 입력 3바이트가 4문자의 가격이기 때문이죠. base64 텍스트 열 위의 색인은 세금을 두 번 냅니다: 저장에서 한 번, 모든 비교에서 다시 한 번. 색인 항목이 바이트가 아니라 래핑된 글자들이니까요. MySQL의 max_allowed_packet과 PostgreSQL의 1 GB bytea 상한이 대부분의 사람들이 가장 먼저 부딪히는 두 벽이고, 둘 다 교환의 더 큰 쪽인 텍스트 기준으로 확인됩니다. 설계의 답은 거의 언제나 다른 인코더를 고르는 것에 있지 않습니다(base64는 하나이니까요). 인코딩이 어디서 일어나는지를 고르는 것입니다. BLOB 열, 조회용 해시 열, 경계에서의 인코딩: base64는 자리가 마땅한 트래픽 안에만 존재합니다.
보안: Base64가 아닌 것
Base64는 암호화가 아닙니다. 그리고 소리 내어 말할 필요가 있는 습관은, 앞선 설정 테이블의 그것 하나입니다: base64로 저장된 시크릿은 다른 글꼴로 저장된 시크릿일 뿐입니다. 그 변환은 키 없는 일대일 대응(쌍대사상)이며, 세상 모든 프로그래밍 언어가 함수 호출 한 번으로 되돌릴 수 있고, 그 유일한 실제 효과는 값을 YAML 한 줄에 머무르게 하는 것입니다. 위협 모델에 이 데이터베이스의 다른 사용자, 익스포트를 읽는 다른 서비스, 그 행을 찍어둔 로그가 들어 있다면, base64가 방어에 기여하는 것은 정확히 제로입니다. 값이 인간 눈에서 몇 초간 가려지는 것뿐이라, 코드 리뷰에서는 보호처럼 느껴지고, 인시던트에서는 실패하는 것입니다. 시크릿이어야 하는 것은 암호화하고, 누군가 실제로 시크릿으로 지킬 수 있는 키로 암호화하며, base64에는 그것이 잘하는 일을 맡기세요: 텍스트만 실어 나르는 채널로 바이트를 옮기는 일.
각 방언이 래핑을 배운 시점
릴리스 노트는 디코딩 쪽이 한 이야기와 같은 이야기를 들려 줍니다. 글자의 방향만 반대일 뿐이고, 그 일정은 각 엔진에 대해 뭔가 말해 줍니다:
2002. PostgreSQL 7.2는 base64를 encode()와 decode()의 일급 포맷으로 나열합니다 - Oracle의 9i 시대 UTL_ENCODE와 동시대의 것이며, 근소한 차이로 이 가족에서 가장 오래된 base64 기계입니다. 진짜 바이너리 타입과 포맷 인수를 가진 데이터베이스는 일찍 그곳에 도착했습니다. 답이 열거형 값 하나만 떨어져 있었으니까요.
2000년대 초. Oracle의 UTL_ENCODE 패키지는 9i 시대에, MIME 헤더, quoted-printable, uuecode 형제들 옆에 BASE64_ENCODE()를 실고 나옵니다. 들어가는 것은 RAW, 나오는 것도 RAW, 그리고 25년이 지난 뒤에도 그 패키지는 생각을 바꾸지 않았습니다.
2013. MySQL 5.6이 TO_BASE64()와 FROM_BASE64()를 짝짝이로 추가하고, MariaDB 10.0이 둘 다 물려 받습니다. 이 쌍의 계약은 그때부터 한 번도 움직이지 않았습니다: 나갈 때 76자 행, 들어올 때 공백 관용.
2018. ClickHouse 18.16(2018년 12월)이 MySQL 스타일 별명과 함께 base64Encode()와 base64Decode()를 출시합니다. 컬럼형 세계가 로그 스키마에 이미 base64를 실어 나르던 워크로드를 들여오는 중이었기 때문이죠.
2023. SQLite 3.41.0이 base64()를 애플리케이션 정의 함수로 커맨드라인 셸에 추가합니다. 코어 라이브러리는 늘 그렇듯 아무것도 받지 않고, 사람들이 실제로 SQLite 파일을 만지작거리는 셸이 도구를 받습니다.
2025. 2025년 11월에 일반 가용이 된 SQL Server 2025가, 마침내 BASE64_ENCODE()와 BASE64_DECODE()를 출시합니다. 제품 출시 36년 후, 그리고 그 사용자들이 XML 우회책을 외워 버린 한 세대 후.
패턴은 디코딩 글이 끝맺는 것과 같은 것의 거울입니다: 진짜 바이너리 타입과 포맷 인수를 가진 엔진은 필요가 명확해진 날 base64를 얻었고, 모든 것이 문자열인 엔진은 그것을 나중에로 일정을 잡았습니다.
아는 가치가 있는 기이한 것들
- SQLite CLI의
base64()는 이 가족의 변형자이며, 인코딩 방향에서는 그 트릭을 가장 잘 보여 줍니다:BLOB을 주면 끝 줄바꿈이 붙은 래핑된 텍스트를 돌려주고, 텍스트를 주면BLOB을 돌려줍니다. 이름은 하나, 일은 두 개, 인수의 타입이 고르고, 이 가족에서 다른 인코더는 아무도 하지 못합니다. - ClickHouse의
base64Decode()가족은 26.7에서 관대가 되었습니다: 입력의 공백이 이제 거부 대신 무시되므로, 래핑된 열에서의 같은 쿼리는 오래된 서버에서는 실패하고 새 서버에서는 조용히 값을 돌려줍니다. 디코더가 부서진 것이 아니라, 느슨해진 것이죠. 어째서인지 디버깅이 더 힘든 쪽입니다. - PostgreSQL은 1996년 MIME 표준과 똑같이 76자에서 래핑하지만, 줄을 캐리지 리턴과 줄바꿈이 아니라 홀로 남은 줄바꿈 하나로 끝냅니다. 명세 20여 년 뒤, 줄마다 글자 하나를 덜 쓰고, 그 반란은 바이트를 디프하지 않는 한 보이지 않습니다.
- 클라이언트
mysql에서, 인코딩한 바이트는 base64 텍스트로 잘 출력되지만,CAST(... AS BINARY)로 맨 열을 보는 순간 클라이언트는 16진수 표시(binary-as-hex)로 전환되고, 멀끔한hello는 화면에0x68656C6C6F로 도착합니다. 이 설정은 수많은 개발자의 인코더가 고장 났다고 믿게 만들어 왔습니다. - Snowflake는 모든 결과 집합에서
BINARY값을 16진수로 표시하므로, 인코딩 쿼리의TO_BINARY()입력 열은 전부 잘 됐는데도 체크섬처럼 읽힙니다. 방언 두 개, 16진수 표시 두 개, 불쾌감은 똑같이 하나. - Oracle의 SQL 레벨
RAW는 2000바이트로 상한이라, 3킬로바이트 인증서는RAW리터럴로 SQL 문에 아예 붙여넣을 수 없습니다. 인코딩은 PL/SQL에서 이루어져야 하는데, 거기서RAW변수는 32767바이트를 갖습니다. 그래서 3킬로바이트 인증서는 호출 하나이고, base64 출력이 32킬로바이트를 초과하는 페이로드만 1990년대로 거슬러 가는 청크 루프가 필요합니다. - MIME base64 한 줄은 76문자이고, 4문자가 3을 실어 나르기 때문에 57개의 생 바이트입니다. 세 인코더의 기본값에 나오는 숫자 76은 한도라기보다 압축 밀도입니다: 옛 이메일 첨부에서 보는 래핑된 줄 하나하나가 정확히 57바이트의 여러분의 데이터를 실어 나랐으니까요.
반대 방향
이 글은 의상을 입히는 것에 관해서였습니다: 어떤 바이트를 의도하는지 정하고, 무엇이 나오는지를 지켜 보고, 청중을 위해 알파벳을 고르고, 열이 잘리기 전에 크기 산술을 하는 것. 의상을 벗기는 것은 완전히 다른 기질입니다. 어떤 방언은 어깨를 으쓱하며 조용한 NULL을 돌려주면, 다른 방언은 소리 높여 하드 에러를 내고, 가족의 절반은 URL-safe 알파벳을 아예 모릅니다. 그것 전부, FROM_BASE64()에서 decode()에서 BASE64_DECODE()까지, 바로 아래에 링크된 관련 SQL용 Base64 디코딩 글에서 심층적으로 다룹니다. 여기서 인코딩하고, 거기서 디코딩하면, 온전한 왕복이 한 오후 안에 들어맞습니다.
마지막 업데이트: 2026-09-08
관련 문서: SQL에서의 Base64 디코딩: 완전한 가이드