Ruby에서의 Base64 인코딩: 완전한 가이드
당신의 데이터는 지금의 모습을 받아 주지 않는 목적지가 있습니다. JSON 문서 안에 살아야 하는 이미지. URL을 건너야 하는 토큰. 7비트 텍스트를 위해 설계된 프로토콜을 견뎌야 하는 첨부 파일. 인용을 깨뜨리지 않으면서 환경 변수 안에 있어야 하는 비밀. 이 각각의 장소에서, 여기와 저기 사이 어딘가 당신의 바이너리를 곧 파괴할 무언가가 대기 중입니다 - 그리고 그 수단에 이름이 있습니다. Base64.
Ruby에서는 이 일 전체가 언어와 함께 배포되는 하나의 모듈 안에 있습니다. 인코더 셋, 설치할 것 제로, 그리고 코드를 한 번 실행하기도 전에 정확한 문자 단위까지 예측할 수 있는 출력. 그 예측 가능성이 바로 대부분의 가이드가 건너뛰는 이야기의 절반인데, 인코딩이 놀람이 요금을 받는 곳이기 때문입니다. 꼬리 줄바꿈이 당신의 JSON에 슬그머니 들어오고, 달아 달라고 하지 않은 줄바꿈이 토큰을 찢고, 알파벳 선택 하나를 잘못해서 URL이 망가지죠. 이 가이드는 세 인코더 전부, 출력의 수학, 그리고 Ruby 개발자가 실제로 인코딩하는 모든 페이로드를 지나갑니다. 놀람이 더 이상 놀람이 아니게 하려고요.
시작 전에 빠른 복습을 하나. Base64는 데이터를 3바이트씩 다시 쓰며, 64문자 알파벳에서 4문자를 뱉어내고, 입력이 3으로 나누어 떨어지지 않으면 패딩으로 = 문자 하나 또는 둘을 붙입니다 - 그래서 출력이 입력보다 대략 3분의 1 더 커지는 이유가 그것이기도 하죠. 이 사이트의 홈 페이지가 포맷을 철저히 다루므로, 이 기사는 포맷 이야기를 한숨 분량으로 줄이고 바로 일로 갑니다.
어떤 인코더가 필요할까?
Ruby는 인코더 셋을 줍니다. 그리고 그 셋 사이의 선택은 세 질문짜리 퀴즈입니다. 출력에 줄바꿈이 들어갈 수 있는가? +나 /가 들어갈 수 있는가? 패딩이 들어갈 수 있는가? 여기 전체 라인업입니다:
| 인코더 | 출력 모양 | 줄바꿈 | 패딩 | 언제 손이 가는가 |
|---|---|---|---|---|
Base64.strict_encode64(bin) |
한 줄, 표준 알파벳 | 절대 없음 | 항상 있음 | JSON, 토큰, API, 파일 - 안전한 기본값 |
Base64.encode64(bin) |
여러 줄, 표준 알파벳 | 모든 60문자마다, 그리고 꼬리 하나 | 항상 있음 | 이메일 본문과 그 밖의 줄 지향 텍스트 프로토콜 |
Base64.urlsafe_encode64(bin, padding: true) |
한 줄, 하이픈-밑줄 알파벳 | 절대 없음 | 당신의 선택, 기본적으로 켜짐 | URL, 쿠키, 식별자 안에 도착하는 모든 것 |
시간 압박 아래에서 결정해야 한다면, 짧은 답은 이것입니다. 기본적으로 strict_encode64, 결과가 URL 안에 여행한다면 urlsafe_encode64, 그리고 수신 측이 짧은 줄을 원하는 텍스트 프로토콜일 때에만 encode64. 아래 내용은 전부 왜 그런지, 그리고 각 선택이 어디서 조용히 대가를 치르게 하는지를 설명합니다.
strict_encode64: 일꾼
Base64.strict_encode64는 당신의 코드 대부분에서 실제로 쓰게 될 인코더입니다. 정확히 한 줄의 출력을, 항상 올바른 패딩과 함께, 표준 알파벳에서 만들어 냅니다:
require "base64"
Base64.strict_encode64("hello world")
# => "aGVsbG8gd29ybGQ="
Base64.strict_encode64("s")
# => "cw=="
그리고 알고리즘이 결정적이므로, 입력부터 출력의 정확한 길이를 예측할 수 있습니다 - 추측도, 데이터베이스 컬럼의 오프바이원 버그도 없죠. 아래 표가 그 수학 전체입니다:
| 입력 길이 | 출력 길이 | 꼬리 패딩 |
|---|---|---|
| 3n 바이트 (나눠 떨어짐) | 4n 문자 | 없음 |
| 3n + 1 바이트 | 4n + 4 문자 | = 둘 |
| 3n + 2 바이트 | 4n + 4 문자 | = 하나 |
그러니 11바이트는 16문자가 되고, 100바이트는 136문자가 되며, 1메가바이트 파일은 대략 1.33메가바이트짜리 텍스트가 됩니다. 그 3분의 1 성장은 당신이 보내는 모든 Base64 페이로드의 입장료이며, 컬럼이나 캐시나 API 호출 한도가 조여 오기 시작할 때마다 주머니에 넣어 둘 숫자입니다.
Base64.strict_encode64("123")
# => "MTIz" 3바이트 들어가서 4문자 나옴
Base64.strict_encode64("1234")
# => "MTIzNA==" 4바이트 들어가서 8문자 나옴, 패딩 문자 둘
Base64.strict_encode64("12345")
# => "MTIzNDU=" 5바이트 들어가서 8문자 나옴, 패딩 문자 하나
encode64: 줄바꿈을 붙여 주는 그 인코더
Base64.encode64는 고전이고, 오후 시간대를 몇 번이고 끝내 버린 특성이 하나 있습니다. 출력을 래핑한다는 것. 매 60문자마다 새 줄을 시작하고, 항상 꼬리 줄바꿈으로 마무리합니다:
Base64.encode64("hello world")
# => "aGVsbG8gd29ybGQ=\n"
Base64.encode64("*" * 46)
# => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
래핑은 버그가 아닙니다 - 이 메서드의 원산지가 긴 줄을 프로토콜 위반으로 여긴 MIME 세계에서 물려받은 기능입니다. Ruby의 mail gem은 이것을 의지합니다 - 그 Base64 인코더에는 Ruby의 줄 래핑이 출력을 SMTP 줄 길이 한계 안에 유지해 준다는 취지의 코멘트가 붙어 있죠. 이메일 본문을 인코딩한다면, encode64는 당신에게 호의를 베푸는 것입니다.
하지만 그 밖의 모든 문맥에서는, 래핑은 세금입니다. 가장 흔한 사고는 Base64 값이 갑자기 두 줄에 걸친 JSON 문서입니다:
payload = { "logo" => Base64.encode64(File.binread("logo.png")) }
puts payload.to_json
# logo 값에 아무도 달아 달라고 하지 않은 줄바꿈이 실려 있다
그리고 같은 버그의 작은 쌍둥이는 짧은 문자열의 꼬리 줄바꿈입니다. Base64.encode64("s")는 "cw==\n"를 돌려 주므로, URL에 붙여 넣거나 기대 값과 비교하는 토큰은 실패하고, 그 이유를 20분간 쫓게 됩니다. 치료제는 strip이지만, 더 나은 치료제는 strict_encode64입니다. 당신이 벌어 들이지 않은 문자를 하나도 붙이지 않으니까요. 그리고 아는 것이 좋은 매혹적인 비대칭이 하나 더 있습니다. 빈 입력은 꼬리 줄바꿈 없는 빈 문자열을 만들어 내므로, Base64.encode64("")는 그냥 ""입니다.
urlsafe_encode64: 링크에 안전한 알파벳
표준 알파벳에서 두 글자가 URL 파서를 대하는 곳이라면 어디든 문제를 일으킵니다. + (쿼리 스트링에서는 공백)과 / (경로 구분자). RFC 4648은 교체로 이것을 해결했습니다 - -가 +의 자리를, _가 /의 자리를 맡고 - Ruby는 이것을 Base64.urlsafe_encode64로 구현합니다:
Base64.urlsafe_encode64("\xfb\xef\xbe".b)
# => "----"
Base64.urlsafe_encode64("\xff\xff\xff".b)
# => "____"
이 두 예가 바로 알파벳의 전시판입니다. 표준 인코더가 ++++나 ////로 그리던 같은 바이트가 ----와 ____로 나옵니다. percent-인코딩 없이 URL, 경로, 파일명, 폼 필드를 전부 무사히 건너는 글자들이죠. 출력은 strict_encode64와 마찬가지로 한 줄입니다.
이 메서드의 유일한 옵션은 Ruby 2.3에서 추가된 padding: 키워드인데, 꼭 알아야 할 것이 바로 이것입니다. JSON Web Token 규격은 패딩 없는 base64url을 요구하며, 많은 다른 토큰 체계도 그렇습니다:
Base64.urlsafe_encode64("*")
# => "Kg=="
Base64.urlsafe_encode64("*", padding: false)
# => "Kg"
패딩을 꺼면 길이 수학이 바뀝니다. 3n + 1 바이트는 이제 4n + 2 문자를, 3n + 2 바이트는 4n + 3 문자를 만들어 냅니다. 디코더 쪽은 알아서 맞춰 줍니다 - Ruby의 urlsafe_decode64는 빠진 패딩을 스스로 붙이므로 - 패딩 없는 출력을 내보내도 안전하지만, 상대가 엄격한 RFC 2045 리더라면 패딩 있는 출력이 더 친절한 기본값입니다. 주의 하나: 규격이 요구할 때에만 패딩을 꺼세요. 한두 개 문자는 아끼지만, 디코더 항의의 한 종류를 사 오는 거래가 되니까요.
Ruby가 실제로 인코딩하는 것: 문자열은 바이트
사용 사례에 앞서, 모든 것을 규정하는 Ruby 특유의 사실 하나: Ruby 문자열은 인코딩 태그를 쓴 바이트의 연속이며, 인코더는 오직 바이트만 봅니다. 태그는 Ruby에게 문자열을 어떻게 표시하고 비교할지를 알려 줄 뿐, 인코딩되는 것을 바꾸지는 않습니다:
require "base64"
s = "h\u{e9}llo"
puts s.encoding
# => UTF-8
puts s.bytes.length
# => 6 발음 기호가 붙은 e는 두 바이트
Base64.strict_encode64(s)
# => "aMOpbGxv"
"왜 내 출력이 기대보다 길지?"라는 물음 뒤의 함정이 바로 이것입니다. 당신이 입력한 문자열은 문자로 세면 바이트로 세는 것보다 보통 짧고, Base64는 바이트당 요금을 받습니다. 반대 방향도 똑같이 조용합니다 - 잘못된 UTF-8 문자열은 아무 불평 없이 인코딩됩니다. 인코더가 검증할 것이 아무것도 없으니까요:
broken = "h\u{e9}llo".b.force_encoding("UTF-8")
broken.setbyte(1, 0xFF)
puts broken.valid_encoding?
# => false
Base64.strict_encode64(broken)
# => 어떤 base64, 오류 없음, 바이트는 바이트다
진짜 바이너리라면, 텍스트 기계 통째로 건너뛰고 pack으로 바이트를 만들어 내거나 File.binread로 읽으세요. 만족스러운 예는 PNG 시그니처입니다 - 이 지구의 모든 PNG 파일을 여는 여덟 바이트죠:
png_magic = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A].pack("C*")
Base64.strict_encode64(png_magic)
# => "iVBORw0KGgo="
JWT: 읽히면서도 서명되는 데이터
JSON Web Token이 바로 Ruby의 URL-safe 인코더 소비처 중 가장 유명한 곳입니다. 토큰은 점으로 이어진 base64url 세그먼트 셋 - 헤더, 페이로드, 서명 - 이고, 규격은 분명합니다. 알파벳은 URL-safe 쪽이어야 하고, 패딩은 꺼져 있어야 하죠. jwt gem이 이 모든 것을 다뤄 줍니다:
# Gemfile에는: gem "jwt"
require "jwt"
token = JWT.encode(
{ sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
"my-secret-key",
"HS256"
)
puts token
# => eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIi...
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts header
# => {"alg"=>"HS256"}
세그먼트가 그저 JSON의 base64url일 뿐이므로, 토큰 안에서 Base64 계층이 자기 일을 하는 것을 지켜볼 수도 있습니다:
require "base64"
require "json"
payload_json = JSON.generate({ "sub" => "1234567890", "name" => "Alice" })
segment = Base64.urlsafe_encode64(payload_json, padding: false)
puts segment
# => eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0
이 사용 사례에는 규칙 둘이 함께합니다. 프로덕션에서는 손으로 JWT를 굴리지 마세요 - 서명이야말로 토큰을 자백이 아닌 무엇으로 만들어 주는 것이니까요 - 그리고 gem으로 디코딩할 때는 위와 같이 옵션 해시에서 알고리즘을 고정하세요. 그러면 토큰의 헤더가 당신을 대신해 검증 방식을 고를 수 없게 되죠.
HTTP Basic Auth: 헤더 만들기
HTTP에서 "나 누구냐"고 말하는 가장 오래된 방식은 아직도 가장 단순합니다. 자격 증명을 Base64로 만들어, Basic이라는 단어 뒤에 붙이고, 헤더를 보내는 것. Ruby에서 하나를 만드는 것은 한 줄입니다:
require "base64"
credentials = Base64.strict_encode64("alice:s3cr3t!")
puts "Basic #{credentials}"
# => Basic YWxpY2U6czNjcjN0IQ==
Ruby 표준 라이브러리는 Net::HTTP에서 정확히 이것을 대신해 줍니다. 코어의 pack 템플릿을 직접 부르는데 - basic_auth는 내면에서 ["user:pass"].pack("m0")로 줄어듭니다:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
그리고 보안 주의점, 기록으로 남겨야 하므로 한 번 말합니다. Base64는 번역사일 뿐, 자물쇠가 아닙니다. Basic auth 헤더의 자격 증명은 패킷을 읽을 수 있는 누구나 읽을 수 있습니다. 이 헤더는 전송 자체가 실제로 보호를 해 주는 HTTPS 위에서만 허용됩니다.
Data URI: 인라인 이미지와 폰트
data URI는 웹의 "별도 파일 없이 이 이미지를 갖고 싶다"에 대한 답입니다. 미디어 타입, base64라는 단어, 쉼표, 그리고 바이트. 단일 파일 HTML 데모가 로고를 싣는 방식이고, 파비콘이 CSS 안에 숨는 방식이며, 만들어진 이미지가 템플릿 문자열 안에 통째로 살아갈 수 있는 방식이기도 하죠:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
css = "background-image: url(#{data_uri});"
puts css.length
# => HTTP 요청 하나 뺀, 당신의 스타일시트
여기서는 strict_encode64를 쓰세요. 페이로드는 깨끗한 한 줄이므로, 래핑도 줄바꿈도 없어야 하니까요. 그리고 크기를 지켜 보세요. 인라인으로 넣는 이미지는 대략 3분의 1 커지므로, data URI는 작은 에셋(파비콘, 로고, 아이콘 폰트)에서는 빛나고 큰 에셋에서는 부풀어 오릅니다. 2메가바이트 히어로 사진은 당신의 HTML 문서 2.7메가바이트가 되며, 사용자는 첫 4G 스크롤에서 그것을 느끼게 되죠.
이메일: Base64가 온 곳
이 기사 안의 그 밖의 모든 사용 사례는 이것의 후손입니다. SMTP는 1980년대에 7비트 텍스트의 짧은 줄을 위해 설계되었는데, 다시 말해 JPEG를 싣지 못했습니다. 수선은 - Privacy-Enhanced Mail, 그리고 1993년의 MIME - 바이너리를 64문자 알파벳의 텍스트로 다시 쓰는 것이었고, 바로 당신이 오늘 쓰고 있는 포맷입니다. 그 흉터는 여전히 Ruby의 출력에서 보입니다. encode64는 60자마다 래핑하는데 - 나중에 보겠지만 특별한 프로토콜 하나에도 맞지 않는 폭이지만, 이메일을 예의 있게 유지할 만큼은 짧죠.
실무에서는 mail gem이 MIME 일을 하도록 둡니다. 바이너리 파일을 첨부하면 gem은 Base64 인코더를 고르고, 줄을 래핑하고, 헤더를 씁니다:
# Gemfile에는: gem "mail"
require "mail"
message = Mail.new do |m|
m.from = "dev@example.org"
m.to = "ops@example.org"
m.subject = "Binary report"
m.add_file("report.bin")
end
puts message.encoded
# 첨부 파트에 Content-Transfer-Encoding: base64가 실려 있다
헤더 안의 ASCII가 아닌 텍스트는 좀 다른 복장으로 같은 대우를 받습니다. RFC 2047 인코딩 워드인데, 물음표 사이의 문자셋 태그로 Base64를 감싸는 것 - =?UTF-8?B?w7wgc2VjcmV0cw==?= 같은 것이죠. 그런 것을 언제 손으로 만들거나 파싱하게 된다면, 안에 있는 Base64는 범상한 종류입니다. decode64로 디코딩한 뒤, 워드가 선언한 문자셋으로 다시 라벨을 붙이는 것이죠.
키와 인증서를 위한 PEM 아머
키와 인증서는 PEM 아머를 입는데, 그 아머는 틀을 단 Base64입니다. BEGIN 줄, 64자 줄로 나뉜 인코딩된 바이트, 그리고 END 줄. 원시 DER 바이트에서 PEM 파일을 만들어야 할 때가 있다면, 구성은 두 단계 래핑입니다:
require "base64"
der_bytes = File.binread("server.der")
body_lines = Base64.strict_encode64(der_bytes).scan(/.{1,64}/)
pem = (["-----BEGIN PRIVATE KEY-----"] + body_lines +
["-----END PRIVATE KEY-----"]).join("\n") + "\n"
File.write("server.key", pem)
주의점 둘입니다. 첫째, 거의 필요하지 않을 겁니다. openssl gem이 PEM을 대신 써 주니까요 (key.to_pem). 그리고 BEGIN 줄과 END 줄 사이의 라벨은 안에 있는 것과 일치해야 합니다 - 틀리면 인터넷의 모든 도구가 거부하는 파일이 만들어집니다. 둘째, 여기의 줄 길이는 64, 고전적인 PEM 폭입니다. Ruby의 encode64는 60에서 래핑하지만, 제대로 된 PEM 파서는 줄 길이를 통째로 무시하므로, 어느 폭으로 디코딩해도 괜찮습니다.
파일: .b64 관례
Base64 세계에서 가장 흔한 파일 포맷은 인코딩된 페이로드 하나를 담는, .b64 (또는 .base64) 확장자의 평범한 텍스트 파일입니다. "파일이지만, 어디에 붙여 넣어도 안전하다"고 생각하세요. Ruby에서 하나를 만들어 내는 것은 원라이너입니다:
require "base64"
File.write("payload.b64", Base64.strict_encode64(File.binread("payload.bin")))
puts File.size("payload.b64")
# => 원래 크기의 대략 1.33배
strict_encode64를 써서 파일이 깨끗한 한 줄을 담게 하세요. 대부분의 디코딩 도구 (그리고 Ruby의 엄격한 디코더)가 기대하는 관례니까요. 다시 읽는 것은 거울 그림입니다. 읽고, 디코딩하고, 나가는 길에 바이트가 변하지 않게 바이너리 모드로 씁니다:
encoded = File.read("payload.b64")
bytes = Base64.strict_decode64(encoded)
File.binwrite("restored.bin", bytes)
당신의 .b64 파일이 줄을 래핑하는 도구에서 온 것이라면 - 어떤 base64 CLI 변형은 그렇게 하죠 - 엄격한 디코딩 전에 줄바꿈을 제거하세요. 아니면 그것들을 무료로 건너뛰는 관대한 디코더를 쓰세요.
설정 파일, 환경 변수, 데이터베이스
바이너리 데이터가 텍스트 문서 안에 살아야 할 때마다, Base64가 다리가 됩니다. 이 패턴은 세 장소에서 작은 변주로 반복됩니다.
환경 변수와 .env 파일은 원시 바이트를 담을 수 없으므로, 바이트는 그것을 가진 머신을 떠나기 전에 인코딩됩니다:
require "base64"
# 어딘가에서 앱을 프로비저닝할 때
ENV["APP_LOGO"] = Base64.strict_encode64(File.binread("logo.png"))
# 어딘가에서 앱이 시작될 때
b64 = ENV.fetch("APP_LOGO")
File.binwrite("logo.png", Base64.decode64(b64))
YAML에는 네이티브 바이너리 타입이 있고, Psych가 Base64를 대신 처리합니다 - BINARY 문자열이 YAML로 덤프되면 !binary 스칼라로 나가고, 바이트 단위 동일하게 다시 로드되죠:
require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT
데이터베이스에서는 질문이 인코딩이 아니라 저장 타입입니다. 데이터베이스에 진짜 바이너리 컬럼이 있다면 - BLOB, BYTEA, VARBINARY - 그것을 쓰고, 바이트는 드라이버가 싣게 하세요. TEXT 컬럼에 Base64를 넣는 패턴은 저장 계층이 문자열만 말할 때를 위한 것입니다. 어떤 문서 저장소, JSON 모양의 API, 당신이 바꿀 수 없는 레거시 스키마. 대가는 컬럼의 3분의 1 크기 세금, 그리고 모든 경계에서 예외 없이 들어갈 때 인코딩하고 나올 때 디코딩하는 규율입니다.
텍스트로 여행하는 체크섬
해시는 바이너리지만, 체크섬은 대부분 텍스트로 여행합니다. 파일 무결성 목록, 캐시 키, 지문, 로그 줄. Ruby의 digest 클래스마다 인코딩을 한 번의 호출로 해 주는 base64digest 메서드가 있습니다:
require "digest"
Digest::SHA256.base64digest("hello")
# => "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
출력은 패딩 있는 표준 Base64입니다 - Base64.strict_encode64(Digest::SHA256.digest("hello"))로 얻는 것과 같은 것 - 그래서 저장하고, 비교하고, 붙여 넣는 것이 안전합니다. 한 가지 결정은 일관성입니다. Base64로 생성한 체크섬 목록은 Base64 출력과 대조해서 확인해야 하고, 같은 해시의 16진수와 Base64 표기는 다른 문자열이므로, 하나를 고르고 그걸 고수하세요.
큰 것을 작은 조각으로 인코딩하기
디코더와 마찬가지로, 인코더도 버퍼 기반입니다. 입력 통째를 읽고 출력 통째를 뱉어 내죠. 표준 라이브러리에 스트리밍 인코더는 없으므로, 큰 페이로드는 메모리 계획이 답이고, 수학에는 기쁜 대칭이 있습니다. 인코딩은 데이터를 3분의 1 불려 주므로, 가장 큰 할당은 입력이 아니라 출력입니다. 1기가바이트 파일이라면 눈앞에 대략 1.33기가바이트의 텍스트를 예상해야 합니다.
그것을 한 번에 들고 가기 부담스럽다면, 조각으로 인코딩할 수 있습니다. Base64 알파벳은 3바이트 경계에서 자기 동기화되니까요. 각 3바이트 슬라이스를 독립적으로 인코딩하면, 이어 붙인 결과가 통째로 인코딩한 것과 동일합니다:
require "base64"
require "securerandom"
bin = SecureRandom.random_bytes(10_001)
whole = Base64.strict_encode64(bin)
chunked = bin.scan(/.{1,3}/m).map { |slice| Base64.strict_encode64(slice) }.join
puts chunked == whole
# => true
같은 트릭으로 encode64와 정확히 일치하는 직접 만든 줄 래퍼도 만들 수 있습니다. 45바이트는 언제나 정확히 60문자로 인코딩되므로, 입력을 45바이트마다 자르고 조각들을 줄바꿈으로 이어 붙이면, 고전적인 MIME 출력을 한 줄씩 재현합니다. 한 번에 메모리에 있는 슬라이스는 하나뿐이죠:
def wrap_like_encode64(bin)
lines = bin.scan(/.{1,45}/m).map { |slice| Base64.strict_encode64(slice) }
lines.join("\n") + "\n"
end
bin = SecureRandom.random_bytes(10_001)
puts wrap_like_encode64(bin) == Base64.encode64(bin)
# => true
명령줄에서
인코딩도 스크립트 파일이 필요 없습니다. 원라이너 형태는 파일을 읽고 그 Base64를 표준 출력으로 씁니다:
ruby -rbase64 -e 'print Base64.strict_encode64(File.binread(ARGV[0]))' payload.bin > payload.b64
그리고 파이프 형태는 표준 입력을 읽는데, 다른 어떤 명령에서 오는 바이트 흐름을 이렇게 감쌀 수 있습니다:
some_command | ruby -rbase64 -e 'print Base64.strict_encode64(STDIN.read)'
둘 다 print를 유지하세요. 실수로 puts를 쓰면 Base64 뒤에 줄바꿈이 붙고, strict_encode64 출력에서는 깨끗한 토큰이 깨진 토큰이 됩니다. 디코딩 쪽과 같은 경험칙입니다. 출력의 다음 소비자가 엄격하다면, Base64 자신 외에는 아무것도 동승할 수 없습니다.
Ruby 개발자에게 여분의 바이트를 청구하는 함정
- JSON의 꼬리 줄바꿈.
Base64.encode64는 모든 비어 있지 않은 결과를 줄바꿈으로 끝내므로, 깨끗한 토큰이어야 할 값이 꼬리에 뜻밖의\n를 달고 당신의 JSON에 도착합니다. 한 줄로 저장되거나, 비교되거나, 보내질 모든 것에는strict_encode64를 쓰세요. - 토큰과 URL의 60자 래핑. 같은 메서드가 긴 출력을 여러 줄로 래핑합니다. URL 안의 래핑된 문자열은 URL 둘이 되고, 래핑된 토큰은 깨진 토큰입니다. 다시 말하지만:
strict_encode64, 또는encode64출력에 묶여 있다면 줄바꿈을strip/delete하세요. - URL의 플러스와 슬래시. 쿼리 스트링의 표준 Base64는 나가는 길에
%2B,%2F,%3Dpercent-인코딩을 하고, 상대가 그것들을 디코딩하기를 바라는 것을 뜻합니다.urlsafe_encode64는 원천에서 문제를 없앱니다. - 틀린 곳의 패딩. JWT와 다른 토큰 체계는 패딩 꺼짐을 원합니다. 반면 MIME 리더는 패딩 누락을 처리하지 못할 수 있습니다. 규격이 요구하는 곳에만
padding: false를 내보내고, 당신의 소비자 각자가 그 울타리의 어느 편에 앉아 있는지 알아 두세요. - 문자는 바이트가 아니다. 발음 기호 글자 하나 들어간 5문자 문자열은 UTF-8에서 6바이트이고, 출력 길이 수학은 바이트 위에서 움직입니다. 인코딩된 결과가 "너무 길다"고 하면, 문자가 아니라 바이트를 세세요.
- 스키마 설계의 3분의 1 세금. 16KB BLOB은 TEXT 컬럼에서 대략 22KB Base64 문자열이 됩니다. 컬럼, 캐시, API 페이로드는 바이너리 형태가 아니라 인코딩된 형태에 맞춰 크기를 잡으세요.
- 알파벳 둘, 문자열 둘. 같은 바이트도 표준 알파벳과 URL-safe 알파벳에서 다르게 인코딩되므로, 인코딩된 값은 같은 알파벳의 다른 값과만 비교할 수 있습니다. 비교하지도, 섞지도 마세요.
- Base64는 자물쇠가 아니다. 비밀을 인코딩한다고 비밀이 되는 것이 아닙니다. 그 문자열을 가진 사람은 누구나 당신의 데이터를 가진 것이고, Base64가 조종하는 것은 바이트의 모습이지, 누가 읽을 수 있는지가 아닙니다.
바이트와 버그를 아껴 주는 습관
strict_encode64를 기본값으로 삼으세요. 출력이 URL, 쿠키, 식별자 안에 살아야 하는 순간에는urlsafe_encode64로, 그리고 목적지가 이메일 같은 줄 지향 텍스트 프로토콜일 때에만encode64로 바꾸세요.- 선의 양쪽 끝에서, 인코더와 디코더 사이의 알파벳을 일치시켜 두세요. 가장 흔한 "Base64가 깨졌다"는 버그는 표준 알파벳 생산자가 URL-safe 소비자를 만나는 것, 또는 그 반대입니다.
- 인코더에 인코딩하려는 바이트를 대세요. 파일은
File.binread, 만든 바이너리는pack, 문자열이 데이터라면 UTF-8 문자열. 인코더는 당신의 선택을 문제 삼지 않습니다 - 그저 바이트를 셀 뿐이죠. - 성장을 예산에 넣으세요. Base64 문자열이 크기가 정해진 컨테이너 안으로 경계를 건너갈 때마다, 4/3을 곱하고 패딩을 위한 작은 여유를 더하세요.
- Base64는 기밀성이 아니라 이동성을 위해 쓰세요. 데이터의 비공개가 목표라면, 그 도구는 암호화이고, Base64는 그저 그 뒤에 암호문을 다룰 때 하는 일에 불과합니다.
Base64가 gem이 된 이야기
Base64 모듈 생의 대부분 동안, 그것은 표준 라이브러리의 한 파일에 불과했습니다. Ruby의 가장 오래된 헬퍼들이 그러하듯요. strict와 URL-safe 메서드들은 1.9 개발 라인에서 원래의 쌍에 합류했고 - 네 메서드 전체를 가진 base64 라이브러리 통째가 2008년 9월에 트렁크에 더해져 1.9.1 (2009)에서 처음 실렸고, padding: 키워드는 2015년 Ruby 2.3과 함께 도착했습니다. 당신이 오늘 보는 API의 모든 것은 그때까지 이미 자리를 잡았죠 - 나머지 이야기는 이 모듈이 어떻게 배포되는가에 관한 것입니다.
2020년, Ruby 3.0과 함께 코어 팀은 표준 라이브러리를 자기만의 gem으로 추출하기 시작했고, base64가 그 하나가 되었습니다. 버전 0.1.0, 코어 기여자들이 ruby/base64 저장소에서 관리하죠. 그것은 default gem으로 실렸습니다 - Ruby와 함께 배포되어 언제나 사용할 수 있으니, require "base64"는 의례 제로로 계속 동작했습니다. 2023년 Ruby 3.3에서 버전 0.2.0이 따라와, Base64::VERSION 상수와 훨씬 풍성한 문서 집합을 추가했습니다.
그리고 2024년 12월의 Ruby 3.4가 선을 다시 그렸습니다. base64는 default gem 목록에서 bundled gem 목록으로 옮겨갔고, csv와 drb와 같은 선반이죠. bundled gem도 여전히 언어와 함께 실리지만, Bundler 기반 프로젝트는 그것들을 선언할 것으로 기대됩니다. 따라서 Ruby 3.4 이상을 쓰면서 앱이 Bundler를 타고 있다면, Gemfile에 gem "base64"를 추가하세요 (또는 gem install base64를 실행하세요). 그러면 준비 끝입니다. 2025년의 Ruby 4.0은 버전 0.3.0을 가져왔고, 정적 검사기가 모듈을 제대로 볼 수 있게 RBS 타입 시그니처를 포함했습니다.
이 여정 내내 구현은 늘 그 모습 그대로였습니다. 코어의 pack와 unpack 템플릿을 감싼 몇십 줄짜리 순수 Ruby. C 확장도, 의존성도 없고 - rubygems.org에서 수억 건의 다운로드 수를 기록하며 - 이 플랫폼에서 가장 많이 설치된 gem 중 하나입니다.
재미 있는 Ruby 사실들
- 인코더를 포함해 모듈 전체는 커피 휴식 한 번에 읽을 수 있을 만큼 짧습니다.
encode64는[bin].pack("m")그 자체이고,strict_encode64는[bin].pack("m0"),urlsafe_encode64는 엄격한 인코더 위에 두 글자 교차를 얹은 것 - 그리고 요청하면 패딩을 빼죠. encode64의 60자 래핑은 MIME의 76자 최대치도, PEM의 고전적인 64자도 맞지 않습니다. 그냥mpack 템플릿이 늘 그랬던 것뿐입니다. 그리고mailgem의 Base64 인코더는 그것에 긍정적으로 코멘트를 붙여 줍니다. Ruby의 자동 줄 래핑이 출력을 SMTP 한계 안에 유지해 준다는 것이죠.- Ruby의
Net::HTTP는 Basic auth에Base64모듈을 꺼리지 않습니다.pack템플릿을 직접 부르는데, 이것은 모듈이 코어 위의 편의 계층이지 그 반대는 아니라는 좋은 리마인더입니다. - 모든 digest 클래스는
base64digest메서드를 싣고 다니므로,Digest::SHA256.base64digest는hexdigest곁의 일급 시민입니다. 두 번째 호출 없이 텍스트로 가는 체크섬이죠. - YAML의
!binary태그는 위장한 Base64입니다. BINARY 문자열을 덤프하는 순간 Psych가 인코딩을 하므로, 바이너리로 가득한 설정 파일이 그렇게 보이는 이유가 바로 그것입니다. - 당신이 쓰는 모듈은 늘 당신이 기억하는 모듈이었지 않습니다. 오래된 Ruby에는
b64encode(고른 폭에서 래핑)와decode_b(RFC 2047 헤더 디코딩)가 있었고, 둘 다 1.9 라인에서 사라졌으니, 2010년 이전 코드를 이어받아 그것들을 부르면NoMethodError로 죽습니다. - YouTube의 비디오 ID는 패딩 없는 base64url입니다 - 11문자, 플러스도, 슬래시도, 등호도 없이 - URL-safe 알파벳이 위해 설계된 바로 그 종류인, 짧고 링크에 안전한 식별자입니다.
그 반대편
이제 당신은 완전한 인코딩 그림을 갖게 되었습니다. 당신을 결코 놀라게 하지 않는 기본값 일꾼, 그것을 요구하는 프로토콜을 위해 줄을 래핑하는 고전, 패딩 스위치를 가진 링크에 안전한 알파벳, 그리고 당신의 출력이 정확히 어떤 모습일지를 결정하는 바이트 단위 규칙들. 반대 방향 - Base64 문자열을 뜯어 내고, Ruby의 세 디코더 사이에서 고르고, 결과된 바이트를 당신이 쓸 수 있는 무엇으로 바꾸는 일 - 은 자기만의 조용한 함정들을 싣고 있습니다. 절대 아니라고 말하지 않는 디코더로부터 시작하죠. 그 거리 반대편은 아래에서 링크된 Base64 디코딩 기사에서 깊게 다룹니다.
마지막 업데이트: 2026-09-08
관련 문서: Ruby에서의 Base64 디코딩: 완전한 가이드