Base64 형식을 다루어야 하나요? 그러면 여러분에게 이 웹사이트가 딱 맞네요! 저희 웹사이트의 아주 편리한 온라인 도구를 사용하여 데이터를 인코딩하거나 디코딩해보세요.

Ruby에서의 Base64 디코딩: 완전한 가이드

당신과 원본 데이터 사이 어딘가에 문자의 벽이 놓여 있습니다. 대문자와 소문자, 숫자, 어쩌면 플러스나 슬래시나 하이픈, 그리고 꼬리에 서서히 자리를 잡고 선 등호 하나. 에디터는 그게 무슨 파일 종류인지 감도 잡지 못합니다. 데이터베이스는 이를 텍스트 컬럼에 쑤셔 넣었습니다. HTTP 헤더, URL, YAML 키, 또는 .b64 첨부 파일이 달린 서포트 티켓을 타고 도착한 걸 수도 있겠죠. 당신은 일순간에 알아봅니다 - Base64 - 그리고 이제 그 바이트를 되찾아야 합니다. Ruby에서는 그 욕구가 require 문 하나, 메서드 호출 한 번의 거리입니다.

포맷이 낯설다면, 30초짜리 버전을 드릴게요. Base64는 원시 데이터를 3바이트씩 다시 씁니다. 3바이트 그룹 하나마다 64문자 알파벳에서 고른 4문자가 되고, 입력이 3으로 나누어 떨어지지 않으면 패딩으로 = 문자 하나 또는 둘이 붙어, 출력은 언제나 4의 배수에 정확히 닿습니다. 디코딩은 그 반대 방향의 여행 - 4문자가 들어가면 3바이트가 나옵니다 - 그래서 결과는 언제나 입력보다 작고, 크기는 대략 사분의 삼 정도입니다. 이 사이트의 홈 페이지가 포맷의 모든 조각을 상세히 짚어 주므로, 이 가이드는 에너지가 마땅히 갈 자리에 에너지를 씁니다. 바로 이 일의 Ruby 쪽입니다.

좋은 소식입니다. 모든 Ruby 설치에는 완비된 디코딩 도구함이 이미 실려 있습니다. Base64 모듈은 설치할 것이 하나도 없는데, 세 개의 디코더는 한 자리에 앉아 전체 소스를 한 번에 끝까지 읽을 수 있을 만큼 작습니다. 다만 조심할 점도 있어요. 손이 가장 먼저 가는 디코더가 곧 불평도 절대 하지 않는 디코더인데, 이메일에서는 아름다운 성질이고 보안에서는 끔찍한 성질입니다. 이 가이드를 읽는 동안 각 디코더가 정확히 무엇을 받아들이는지, 돌려 주는 바이트를 Ruby가 써도 좋다고 인정하는 텍스트로 어떻게 바꾸는지, 그리고 Ruby 개발자가 실제로 디코딩하는 모든 페이로드 - JWT, 인증 헤더, data URI, 이메일 본문, PEM 아머, 파일, 설정 덩어리, 그 중에서도 거대한 녀석들 - 에 대해 각각 어떻게 처신해야 하는지 알게 될 겁니다.

도구함 소개

모든 것은 require에서 시작됩니다. 설치 단계도, 플랫폼 특이점도, 빌드할 네이티브 확장도 없어요:

require "base64"
puts Base64::VERSION
# => 0.2.0 (예를 들어, 무첨가 Ruby 3.3에서)

도구함의 디코딩 쪽 전체를 한 장의 표로 모았습니다. 각 메서드에 손이 갈 빈도 순서대로요:

디코더 알파벳 밖의 문자 처리 패딩 규칙 무언가 잘못된 때
Base64.decode64(str) 줄바꿈과 공백을 포함해 표준 알파벳에 없는 것은 전부 무시 어떻게 돼 있어도 상관없음, 틀린 패딩도 포함 아무것도 없음 - 절대 예외를 던지지 않고, 디코딩할 수 있었던 것만 돌려 준다
Base64.strict_decode64(str) 표준 알파벳 밖의 문자는 전부 거부 반드시 존재해야 하고 정확해야 함 ArgumentError를 던짐
Base64.urlsafe_decode64(str) URL-safe 알파벳과 표준 알파벳을 받아들이고, 나머지는 전부 거부 선택이지만, 있으면 정확해야 함 ArgumentError를 던짐

도구가 내부에서 무슨 일을 하는지 궁금하시다면, 이 모듈의 디코딩 쪽 전체는 코어 pack/unpack 기계의 두 템플릿 위에 얇은 래퍼 하나입니다. 그리고 그 기계는 Ruby 코어 안에서 C로 구현되어 있어요:

# 모듈의 디코딩 쪽 전체, 압축해서 보여 줌
def decode64(str)
  str.unpack1("m")
end
def strict_decode64(str)
  str.unpack1("m0")
end

m 템플릿이 관대하게 읽는 쪽이고, m0가 엄격한 쪽인데, 이 한 문자의 차이가 앞의 두 디코더 사이의 성격 격차를 전부 설명합니다. 무거운 작업은 코어 속도로 일어나기 때문에, 모듈은 순 Ruby로 남아 있으면서도 메가바이트를 한 자릿수 밀리세컨드에 씹어 삼킵니다.

decode64: 카멜레온

Base64.decode64는 모든 것에 그렇다고 답하는 디코더입니다. 깨끗한 페이로드를 주면 디코딩합니다. 줄바꿈이 가득한 MIME 스타일 덩어리를 주면 어깨만 으쓱합니다. 아예 Base64가 아닌 문자열을 주면, 짜낼 수 있는 것을 전부 짜서 돌려 주는데 경고는 하나도 하지 않아요:

require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"

두 번째 줄 하나가 이 디코더의 성격을 통째로 보여 줍니다. 디코더는 표준 알파벳의 일부가 아닌 것을 전부 건너뜁니다 - 줄바꿈, 공백, 희한한 제어 문자 - 그리고 나머지만 디코딩합니다. 이것이 바로 MIME Base64가 가져야 할 동작이므로, 이메일을 거쳐 온 어떤 것이든 decode64가 알맞은 도구입니다.

뒤집힌 면이 바로 이것을 위험하게 만듭니다. 디코더가 절대 불평하지 않으므로, 입력이 틀렸을 때도 절대 알려 주지 않습니다:

Base64.decode64("not base64 at all!")
# => 그럴듯해 보이는 쓰레기 열 바이트
Base64.decode64("====")
# => ""

첫 번째 예는 우연히 유효한 알파벳 글자인 문자들을 찾아 디코딩하고, 그대로 파일에 씌우고 싶어 질 법한 바이트를 돌려 줍니다. 두 번째 예는 패딩 문자 네 개짜리 문자열에 대해 빈 문자열을 돌려 줍니다. 예외도 던져지지 않고, 로그에도 남지 않습니다. 입력을 신뢰할 수 없다면, 그 침묵은 꺼놓고 싶은 기능입니다 - 바로 다음 두 디코더가 그 일을 위해 존재하니까요.

아는 것이 좋은 특이한 점이 하나 더 있습니다. 이런 종류의 문제는 프로덕션에서 수개월간 숨어 있기 마련이거든요. 디코딩은 첫 = 문자에서 멈춥니다. 패딩 뒤에 있는 것은 오류가 아닙니다. 그저 읽히지 않을 뿐이에요:

Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello"   "Zm9vYmFy" 부분은 디코더에게 보이지 않는다

strict_decode64: 문지기

Base64.strict_decode64는 클립보드를 들고 있는 디코더입니다. 표준 알파벳(A부터 Z, a부터 z, 0부터 9, 플러스, 슬래시)만 받아들이고, 패딩은 정확해야 한다고 요구하며, 규칙 하나라도 어기면 바이트 하나조차 만들어 내기를 거부합니다:

Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => ArgumentError를 던짐
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => ArgumentError를 던짐

마지막 줄이 모든 것을 말해 줍니다. decode64가 기꺼이 디코딩해 주던 바로 그 페이로드가, 줄바꿈 하나 때문에 이제 예외를 던집니다. 패딩 누락, 패딩 과다, 하이픈, 밑줄, 공백 - 전부 죄이고, 페이로드 전체가 그것과 함께 가라앉습니다:

begin
  Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
  puts e.message
end
# => invalid base64

문지기는 당신이 검사할 생각조차 하지 못할 포맷의 구석까지 단속합니다. Base64 문자열이 패딩으로 끝날 때, 마지막 문자의 비트 중 일부는 사용되지 않는데, RFC는 규격에 맞는 인코더는 그 비트를 0으로 설정해야 한다고 말합니다. Ruby는 이것을 검증합니다:

Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => ArgumentError를 던짐 (패딩 비트가 0이 아님)

디코더가 대충이었다면, 두 번째 문자열은 첫 번째와 같은 바이트로 디코딩되었을 겁니다. Ruby는 대충하지 않습니다. 실무적으로, 이 덕분에 strict_decode64는 본인이 직접 인코딩하지 않은 모든 입력에 알맞은 기본값이 됩니다. 오타, 잘림, 틀린 알파벳을 조용한 손상 대신 크고 잡을 수 있는 예외로 바꿔 주니까요.

urlsafe_decode64: 외교관

Base64.urlsafe_decode64는 +와 /가 예약어인 곳을 여행하는 페이로드를 위해 존재합니다. URL, 토큰, 데이터베이스 식별자가 그곳이죠. 내부적으로 URL-safe 알파벳(하이픈과 밑줄)을 표준 알파벳으로 되돌려 번역하고, 패딩을 정규화한 뒤, 결과를 엄격한 디코더에게 넘깁니다:

Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => ArgumentError를 던짐 (열다섯 자 문자열은 패딩 두 개가 아니라 한 개가 필요)

첫 번째 예가 이 디코더의 가장 쓸모 있는 특성을 보여 줍니다. 패딩 없는 입력도 괜찮다는 것입니다. 문자열에 패딩이 없고 길이가 4의 배수가 아니라면, 디코더가 빠진 = 문자를 대신 붙여 줍니다 - URL-safe Base64의 가장 큰 소비자인 JSON Web Token이 정확히 이렇게 생산하죠. 패딩이 이미 있으면, 엄격한 디코더와 마찬가지로 정확해야 합니다.

문서가 큰 소리로 말하지 않는 특이점이 하나 있습니다. 외교관은 두 가지 언어를 다 해요. 이 메서드가 엄격한 디코딩 전에 하이픈과 밑줄을 다시 쓰기 때문에, 표준 알파벳 문자열도 받아들입니다:

Base64.urlsafe_decode64("aGVsbG8=")
# => "hello"   표준 알파벳도 받아들여진다

그 관대함은 편하지만, 이 메서드로 페이로드가 어떤 알파벳에서 왔는지 구분할 수 없다는 뜻이기도 합니다. 그것이 중요하다면, 디코딩하기 전에 문자를 스스로 확인하세요.

그리고 decode64와 달리, 외교관은 공백에 자비를 베풀지 않습니다. URL-safe 페이로드 어디에 줄바꿈이 있어도 ArgumentError가 던져지므로, 입력이 래핑된 파일에서 온 것이라면 먼저 줄바꿈을 제거하세요.

바이트는 텍스트가 아니다: 인코딩 단계

여기가 바로 베테랑 개발자조차 넘어뜨리는 단계인데, Ruby가 그것을 눈에 띄게 만들어 주기 때문이에요. 디코딩한 Base64 문자열은 원본 데이터가 PNG든, JWT 페이로드든, UTF-8으로 쓰여진 러브 레터든, 항상 ASCII-8BIT 인코딩 (즉 BINARY) 태그를 달고 있습니다:

bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]

페이로드가 바이너리라면 - 사진, zip 파일, 해시 - 그 모습 그대로 유지하고 File.binwrite로 씁니다. 변환도, 질문도 필요 없죠. 페이로드가 텍스트라면, 그 바이트는 거의 절대로 UTF-8일 테고, Ruby에게 그렇게 알려야 합니다:

text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
  puts text
else
  puts "not valid UTF-8 after all"
end

두 호출은 서로 다른 일을 합니다. force_encoding은 바이트에 다시 라벨을 붙여 줄 뿐이고, valid_encoding?은 그 바이트가 진짜 UTF-8을 이루는지 검증합니다. 그 순서로 실행하세요. BINARY 문자열을 먼저 검증하면 검증할 것이 아무것도 없으니까요. 그리고 평생 기억해 둘 작은 비교 함정이 하나 있습니다. Ruby는 BINARY 문자열을 UTF-8 문자열과 같다고 여기는 경우가 두 문자열 모두 순수 ASCII인 때뿐이므로, 디코딩한 텍스트를 원본과 비교하기 전에 먼저 라벨을 다시 붙이세요:

decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false   같은 바이트, 다른 라벨
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true

JWT: 키 없이 토큰 읽기

JSON Web Token은 점으로 이어진 Base64 문자열 셋입니다: 헤더, 페이로드, 서명. 앞의 둘은 JSON 문서의 URL-safe, 패딩 없는 Base64라서, 토큰은 그것을 본 누구나 읽을 수 있습니다 - 라이브러리 하나 없이 당신도요:

require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}

진짜 일에서는 jwt gem을 쓰게 될 텐데, 이것이 실제로 당신을 보호하는 부분 - 서명 - 과 클레임 검증을 다뤄 줍니다:

# Gemfile에는: gem "jwt"
require "jwt"
token = JWT.encode(
  { sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
  "my-secret-key",
  "HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice

여기에는 보안 주의점 두 개가 속합니다. 둘 다 실로 사고를 만들어 낸 적이 있기 때문이죠. 첫째, 페이로드는 암호화되어 있지 않습니다. 디코딩하는 것은 읽는 것이지 해독하는 것이 아니며, 유일한 보호 장치는 서명입니다. 따라서 디코딩한 페이로드를 신뢰할 수 있는 입력으로 대우하지 마세요. 둘째, 위와 정확히 같이 JWT.decode에서 알고리즘을 고정하세요. 생략하면 토큰의 헤더 스스로 검증 방식을 결정하게 되는데, 그 유연성 한 조각이 바로 유명한 JWT 알고리즘 혼동 공격이 이용하는 것입니다.

Basic Auth: 한눈에 보이는 곳에 숨겨진 비밀번호

웹에서 가장 오래된 인증 헤더는 Base64 그 자체입니다. HTTP Basic 인증은 자격 증명을 user:password 형태로 인코딩해 Basic이라는 단어 뒤에 보내는데 - 이 헤더는 모든 요청에 동승하므로, 당신이 디버깅하게 될 모든 로그에 등장합니다. 하나를 디코딩하는 것은 걷어내고 나누는 작업입니다:

require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!

split의 한계값 2가 중요합니다. 비밀번호는 합법적으로 콜론을 포함할 수 있고, 당신은 언제나 첫 콜론에서만 자르는 것을 원하니까요. Ruby 자신의 표준 라이브러리는 Net::HTTP 안에서 코어의 pack 템플릿을 직접 사용해, 이 헤더를 역방향으로 만들어 냅니다:

require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==

그리고 분명하지만 말해 두어야 할 보안 주의점 하나: Base64는 번역사일 뿐, 자물쇠가 아닙니다. Basic 인증은 HTTPS 위에서만 허용됩니다. 이 인코딩은 자격 증명이 인쇄 가능한 텍스트로 선을 타고 다닐 수 있게 하기 위해 존재하는 것이지, 비밀로 만들기 위해 존재하는 것이 아닙니다.

Data URI: 파일이 아닌 이미지

data URI는 URL 안에 파일 하나를 통째로 숨깁니다. 미디어 타입, base64라는 단어, 쉼표, 그리고 인코딩된 바이트. 브라우저는 img 태그와 CSS에서 그것들을 렌더링하고, 단일 파일 HTML 앱은 두 번째 요청을 할 필요가 없어서 그것들을 좋아합니다. Ruby에서 하나를 만드는 것은 한 줄이면 됩니다:

require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"

하나를 디코딩하는 것은 그 역의 과정인데, 사람들을 넘어지게 하는 디테일이 두 개 있습니다. 쉼표가 구분자이므로 정확히 한 번만 나누고, 미디어 타입 부분은 아무것도 아닌 것을 포함해 무엇이든 될 수 있습니다:

data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)

여기서는 decode64가 아니라 strict_decode64를 쓰세요. data URI 페이로드는 깨끗한 한 줄이므로, 손상되면 큰 소리로 울리는 예외를 원하니까요. 그리고 크기 세금을 기억해 두세요 - 인라인으로 넣는 모든 이미지가 대략 3분의 1씩 커지므로 - data URI는 파비콘, 작은 로고, 폰트에는 완벽하지만 히어로 사진에는 나쁜 아이디어입니다.

이메일: 60자 줄과 mail gem

Base64는 이메일을 위해 발명되었고, 그 흉터가 보입니다. SMTP는 7비트 텍스트의 짧은 줄을 위해 설계되었으므로, MIME Base64는 출력을 짧은 줄로 래핑하고, 규격에 맞는 디코더는 줄바꿈을 무시해야 합니다. Ruby의 decode64는 정확히 그렇게 동작하므로, 래핑된 MIME 본문은 쉬운 먹잇감입니다:

body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"

그걸 손으로 쓰는 일은 드물 겁니다. mail gem이 MIME 일을 통째로 대신해 줍니다. 첨부 파일은 자동으로 Base64 인코딩되고, 줄은 60자마다 래핑되어 MIME의 76자 한계 안에 안락하게 들며, 올바른 헤더가 붙습니다:

# 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 인코딩 워드로 도착합니다. 문자셋, 글자 B, 그리고 물음표 사이의 Base64. 하나를 손으로 디코딩하는 것은 작은 규모의 문자열 수술 연습입니다:

header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ü secrets

PEM: 갑옷에 둘러싼 키와 인증서

키와 인증서는 생의 대부분을 PEM 아머 안에서 보냅니다. BEGIN 줄, Base64 블록, 그리고 END 줄. 이 갑옷은 1980년대로 거슬러 올라가 - Privacy-Enhanced Mail이 바로 Base64 계보 전체가 시작된 곳이지만 - 오늘 당신의 .crt 파일과 .key 파일이 입은 포맷은 여전히 그것입니다.

PEM 파일을 손으로 디코딩하는 것은 갑옷을 벗겨 내는 일, 그리고 관대한 디코더가 줄바꿈을 씹어 넘기도록 두는 일뿐입니다:

require "base64"
pem = File.read("server.key")
body = pem.lines
  .reject { |line| line.start_with?("-----") || line.strip.empty? }
  .join
key_bytes = Base64.decode64(body)

실사용에서는 보통 수동 단계를 건너뛰고 PEM 문자열을 통째로 OpenSSL에게 넘깁니다. OpenSSL은 아머를 스스로 읽으니까요:

require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, 또는 키가 실제로 나타나는 그대로

알아두기 만한 상호 운용 디테일은 하나뿐입니다. PEM 줄은 고전적으로 64자 길이지만, 디코더는 어쨌든 줄바꿈을 무시하므로, 60자 래퍼든 하나의 거대한 줄이든 똑같이 잘 디코딩됩니다.

파일과 .b64 관례

Base64 세계에서 가장 흔한 파일 포맷은 인코딩된 페이로드 하나를 담는, .b64 (가끔 .base64) 확장자의 평범한 텍스트 파일입니다. 하나를 읽는 것은 3단계 왕복입니다:

require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)

나가는 길에는 File.binwrite를 쓰세요. 디코딩한 PNG나 zip은 바이너리이고, 줄 끝에 변환하는 플랫폼에서는 텍스트 모드로 쓰면 손상되니까요. 당신의 .b64 파일이 줄을 래핑하는 도구에서 온 것이라면, decode64가 줄바꿈을 무료로 처리해 줍니다. 관대하게 용인하는 대신 검증하고 싶다면, 파일을 바이너리 모드로 읽고 엄격한 디코딩 전에 줄바꿈을 제거하세요:

encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)

바이너리 리드는 Windows에서 중요합니다. 거기서는 텍스트 모드가 CRLF 줄 끝을 LF로 다시 쓰기 때문에 - 당신이 곧 검증하려 하는 문자열 안에서 일어나기를 원하지 않는 바로 그 종류의 변형이니까요.

URL-safe Base64: 링크를 여행하는 페이로드

이것이 URL-safe 변형에 대한 디코더 쪽의 관점입니다. 여기서 내리는 선택이 세 디코더 중 어느 것에 손이 가느냐를 바꾸기 때문이죠. URL-safe Base64 (RFC 4648, 5절)는 URL이 싫어하는 두 문자를 교체합니다 - +가 -가 되고, /가 _가 되며 - 보통 패딩도 함께 뺍니다. Ruby에서는 쿼리 파라미터, 쿠키 값, API 식별자, YouTube 스타일 비디오 ID, 그리고 물론 JWT에서 그것과 만나게 됩니다.

세 디코더가 같은 입력에서 어떻게 동작하는지 여기에 있습니다. 차이가 바로 버그가 태어나는 곳이니까요:

입력 decode64 strict_decode64 urlsafe_decode64
aGVsbG8= (표준, 패딩 있음) "hello" "hello" "hello"
aGVsbG8 (패딩 없음) "hello" ArgumentError "hello"
SGVsbG8gd29ybGQ- (마지막 그룹에 하이픈) "Hello world" (한 바이트 부족!) ArgumentError 12바이트, 정답
aGVsbG8=\n (꼬리에 줄바꿈) "hello" ArgumentError ArgumentError
aGVs!bG8= (떠돌이 느낌표) "hello" ArgumentError ArgumentError

세 번째 줄이 바로 사람들을 물게 하는 줄입니다. URL-safe 페이로드를 표준 디코더로 디코딩하면, 아무것도 던지지 않고 마지막 바이트를 조용히 잃어버립니다. decode64가 단순히 하이픈을 무시하니까요. 페이로드가 URL에서 올 수 있다면, urlsafe_decode64로 디코딩하세요.

실무 노트 하나: 표준 알파벳만 이해하는 문맥(라이브러리, 타 시스템)으로 URL-safe 페이로드를 옮겨야 할 때가 있을 수 있는데, 고전적인 상호 운용 트릭 - 알파벳을 번역하고 패딩을 스스로 붙이는 것 - 은 3줄입니다:

def standardize_urlsafe(b64)
  b64 = b64.tr("-_", "+/")
  b64 += "=" * ((4 - b64.length % 4) % 4)
  b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"

거의 필요하지 않을 겁니다 - urlsafe_decode64가 이미 대신 패딩하니까요 - 하지만 남의 코드에서 알아봐야 할 패턴이고, 상대 쪽이 표준 알파벳을 기대할 때 손이 가야 할 패턴이기도 합니다.

설정, 환경 변수, 데이터베이스

바이너리 데이터가 텍스트 문서 안에 있어야 할 때마다, Base64는 설정에서 모습을 드러냅니다. .env 파일이든, YAML 설정이든, JSON 설정 덩어리든 원시 바이트를 안전하게 실어 나를 수 없으므로, 바이트는 인코딩되고, 당신의 애플리케이션 안의 무언가가 시작할 때 그것들을 디코딩해야 합니다:

require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)

YAML은 특별히 언급할 자격이 있습니다. 포맷에 네이티브 바이너리 태그가 있거든요. BINARY 문자열을 덤프하면 Psych는 Base64를 담은 !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

데이터베이스에서는 경험칙이 있습니다. 데이터베이스에 진짜 바이너리 타입이 있다면 그것을 쓰세요. TEXT 컬럼에 Base64를 넣는 패턴은 저장 계층이 문자열만 말할 때 손이 가는 선택입니다 - 어떤 문서 저장소, JSON 모양의 API, 혹은 당신이 바꿀 수 없는 레거시 스키마 - 그리고 대가는 컬럼에 붙는 3분의 1 크기 세금, 그리고 모든 경계에서 들어갈 때 디코딩하고 나갈 때 다시 인코딩하는 규율입니다.

큰 입력, 단단한 메모리

이 모듈은 버퍼 기반입니다. 디코딩 호출이 문자열 통째를 한 번에 읽고 결과 통째를 돌려 주니까요. 표준 라이브러리에 스트리밍 디코더는 없으므로, 큰 페이로드에 대한 정직한 조언은 메모리를 계획하라는 것입니다. 좋은 소식은 디코딩이 늘 더 작게만 만든다는 점 - 출력은 입력의 사분의 삼을 넘지 못합니다 - 그래서 큰 할당은 입력 문자열 하나뿐입니다.

걱정될 만큼 큰 페이로드는 4문자 그룹으로 디코딩할 수 있습니다. Base64의 4문자 그룹은 스스로 완결되어 있고, 마지막의 부분 그룹은 자기 패딩을 갖고 있으니까요:

require "base64"
def decode_in_chunks(b64)
  b64.scan(/.{1,4}/).reduce("") do |result, group|
    result + Base64.strict_decode64(group)
  end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000

이는 깨끗하고 래핑되지 않은 입력에서 동작합니다 - strict_decode64가 강제하는 것과 같은 규칙 - 꼬리의 홀로 남은 그룹은 패딩이 함께 있을 때만 유효하니까요. 정말로 거대한 파일, 기가바이트 단위 아카이브 같은 것에서는, 파일을 슬라이스로 읽고 각 슬라이스를 디코딩해, 바이트를 디스크에 스트리밍하는 패턴입니다. 그러면 메모리에는 언제나 하나의 슬라이스만 있게 되죠.

터미널을 위한 원라이너

셸에서 디코딩하려면 스크립트 파일이 필요 없습니다. Ruby는 모듈을 그 자리에서 require할 수 있으니까요:

ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world

파일의 경우에는, 페이로드 자체 대신 파일 경로를 넘기세요:

ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin

여기에는 함정이 둘 살고 있습니다. 첫째, echo나 어떤 텍스트 명령을 통해서 파이프를 넘기면, 꼬리의 줄바꿈이 동승합니다. 그러면 strict_decode64는 그것 때문에 예외를 던지죠 - decode64를 쓰거나, 입력을 chomp하세요:

echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'

둘째, 바이너리 출력에는 puts 대신 print를 유지하세요. puts는 자기 줄바꿈을 덧붙여서, 복원한 파일의 끝을 손상시키니까요.

Ruby 개발자가 실제로 부딪히는 함정

  • decode64는 절대 예외를 던지지 않는다. 쓰레기가 들어가면 쓰레기가 나온다. 입력을 신뢰할 수 없는데 손상된 바이트를 조용히 받아들이면, 그 버그는 디코딩 줄이 아니라 몇 주 뒤 손상된 파일 안에서 모습을 드러냅니다. 본인이 직접 인코딩하지 않은 모든 것에는 기본적으로 엄격한 디코더를 쓰세요.
  • strict_decode64와 꼬리 줄바꿈. 텍스트 파일, echo 파이프, 붙여넣기는 전부 줄바꿈으로 끝내기를 좋아합니다. 그리고 엄격한 디코더는 그것에 대해 ArgumentError를 던지죠. 먼저 입력을 chomp하세요 - 또는 바이너리 모드로 읽고 줄바꿈을 제거하세요.
  • 인코딩 단계를 잊는 것. 디코딩한 문자열은 당신이 다르게 말하기 전까지 BINARY입니다. 결과를 텍스트로 취급하기 전에 UTF-8을 강제하고 (유효성도 확인하고) 하지 않으면, UTF-8 문자열과 섞는 순간 글자 깨짐과 Encoding::CompatibilityError를 얻게 됩니다.
  • BINARY와 UTF-8을 비교하는 것. 같은 바이트, 다른 라벨. ==는 false라고 말합니다 - 문자열이 우연히 순수 ASCII인 경우를 제외하고는요. 비교하기 전에 라벨을 다시 붙이세요.
  • 잘못된 디코더를 지나는 URL-safe 입력. decode64는 하이픈과 밑줄을 조용히 떨어뜨리므로, URL-safe 페이로드는 한 바이트 부족한 손상된 상태로 돌아옵니다. 오류도 전혀 없이요. urlsafe_decode64를 쓰세요.
  • 패딩 이후의 데이터는 보이지 않는다. decode64는 첫 =에서 멈춥니다. MIME에는 훌륭하지만, 잘려 나간 뒤 다른 어떤 도구에 의해 다시 패딩된 페이로드를 잡으려는 것에는 끔찍합니다.
  • 비정준 패딩이 조용히 받아들여진다. QR== 같은 문자열은 마땅한 인코더라면 0으로 설정했을 패딩 비트를 싣고 다닙니다. decode64는 기꺼이 디코딩하는 동안 strict_decode64는 거부합니다. 당신의 인코더가 거짓말하고 있었다는 것을 알려 줄 것은 아무것도 없습니다.
  • Windows에서의 텍스트 모드 파일 읽기는 당신이 보기 전에 줄 끝을 다시 씁니다. 검증하려고 한다면 .b64 파일을 바이너리 모드로 읽으세요.

디코딩 쪽을 위한 좋은 습관

  • 데이터의 출처에서 디코더를 고르세요. 신뢰할 수 없는 모든 것은 strict_decode64 (그리고 ArgumentError를 잘못된 입력 분기로 rescue하고), URL에서 태어난 페이로드는 urlsafe_decode64, 그리고 MIME 본문처럼 진정으로 관대한 포맷에만 decode64.
  • 바이트가 디코딩되는 순간, 그 정체성을 결정하세요. 바이너리 (ASCII-8BIT를 유지하고 File.binwrite로 쓰기) 또는 텍스트 (UTF-8으로 force_encoding하고, 사용 전에 valid_encoding?).
  • 디코딩했다고 신뢰하지 마세요. JWT 페이로드는 Base64이기 때문에 바로 읽을 수 있는 것입니다. 그것이 진짜인지 결정하는 것은 서명입니다. 설정 파일 안의 Base64 문자열은 데이터이지, 증거가 아닙니다.
  • 검증기를 쓴다면, 지루한 케이스로 테스트하세요. 빈 문자열, 패딩 없는 입력, 래핑된 입력, URL-safe 입력, 틀린 패딩. 바로 그 케이스들이 세 디코더를 갈라 놓습니다.

Ruby에서 Base64의 짧은 역사

Base64 모듈은 15년 넘게 Ruby 표준 라이브러리의 일부였는데, 배포 방식은 당신이 상상하는 것보다 더 많이 바뀌었습니다:

  • 2008, Ruby 1.8.7: 모듈은 encode64, decode64와 함께, 더 이상 존재하지 않는 두 메서드 - b64encode (고른 줄 길이마다 래핑)와 decode_b (RFC 2047 이메일 헤더 디코딩) - 를 싣고 나옵니다. 오래된 책, 심지어 일부 오래된 gem도 여전히 그것들을 참조하는데, 오늘 그 어느 것을 불러도 NoMethodError입니다.
  • 2009, 1.9 라인: strict_encode64, strict_decode64, urlsafe_encode64, urlsafe_decode64가 도착하고, 두 레거시 메서드는 퇴역합니다 (1.9.1은 2009년 1월에 이미 두 변화를 모두 싣고 나왔죠).
  • 2015, Ruby 2.3: urlsafe_encode64에 padding: 키워드가 생기고, 토큰과 URL을 위해 패딩 없는 출력을 만들 수 있게 됩니다.
  • 2020, Ruby 3.0: base64는 표준 라이브러리에서 ruby/base64 저장소 아래, 버전 0.1.0의 자기만의 gem으로 추출됩니다. default gem으로 배포되므로, require "base64"는 여전히 그냥 동작합니다.
  • 2023, Ruby 3.3: 버전 0.2.0이 Base64::VERSION과 훨씬 풍성한 문서 집합을 추가합니다.
  • 2024, Ruby 3.4: 이 gem은 default gem에서 bundled gem으로 재분류됩니다. 실무적 결과: Ruby 3.4 이상에서 Bundler 기반 프로젝트라면, Gemfile에 gem "base64"를 적어야 합니다 (또는 gem install base64로 설치하세요).
  • 2025, Ruby 4.0: 버전 0.3.0이 도착하며, RBS 타입 시그니처를 그 밖의 유지보수와 함께 추가합니다.

이 모든 것 동안, 한 가지 사실은 결코 바뀌지 않았습니다. 이 모듈은 코어의 pack와 unpack 템플릿 위에 놓인 몇십 줄짜리 순수 Ruby일 뿐이라는 것. C 확장도, 의존성도, 빌드할 것도 없습니다 - 그리고 rubygems.org에서 수억 건의 다운로드 수가 있죠.

궁금증 많은 분들을 위한 Ruby 상식

  • 모듈의 디코딩 쪽은 한 줄짜리 메서드 본문 둘, str.unpack1("m")과 str.unpack1("m0"), 그리고 urlsafe 변형 - 엄격한 것 위에 글자 교체와 패딩 수리를 얹은 것입니다 - 까지입니다. require를 지우고 스스로 써볼 수도 있어요.
  • Ruby 자신의 Net::HTTP는 Basic auth에 Base64 모듈조차 쓰지 않습니다 - pack 템플릿을 직접 부르거든요: ["user:pass"].pack("m0").
  • Rails의 서명된 쿠키와 암호화된 쿠키는 내면에서 Base64 문자열입니다. ActiveSupport의 메시지 코덱은 일반 쿠키에는 strict_encode64를, URL-safe 서명 ID에는 padding: false가 붙은 urlsafe_encode64를 골라요. 당신은 아마 모르게 하나를 디코딩해 본 적이 있죠.
  • 모든 digest 클래스에는 base64digest 메서드가 있습니다 - Digest::SHA256.base64digest("hello") - 텍스트 안에 살아야 하는 체크섬을 위한 원라이너죠.
  • YAML의 !binary 태그는 Base64입니다. Psych로 BINARY 문자열을 덤프하면, 포맷이 조용히 대신 인코딩해 줍니다.
  • decode64는 당신의 줄이 60자, 64자, 76자이든, 하나의 거대한 줄이든 아랑곳하지 않습니다. m 템플릿이 줄바꿈을 건너뛰므로, 래핑된 입력과 래핑되지 않은 입력은 동일하게 디코딩됩니다.

계속 나아가기

이제 당신은 완비된 디코딩 도구함을 갖게 되었습니다. MIME 모양의 덩어리를 위한 관대한 리더, 신뢰할 수 없는 모든 것을 위한 엄격한 문지기, 토큰과 링크를 위한 URL-safe 외교관, 그리고 결과된 바이트를 Ruby가 써도 좋다고 인정하는 텍스트로 바꾸는 인코딩 단계까지. 반대 방향 - Ruby의 세 인코더 중 어디에 바이트를 먹일지 결정하고, 알파벳과 패딩과 줄바꿈을 조종하는 일 - 은 아무도 달아 달라고 하지 않은 꼬리 줄바꿈을 시작으로, 자기만의 놀람 묶음을 싣고 있습니다. 그 거리 반대편은 아래에서 링크된 Base64 인코딩 기사에서 깊게 다룹니다.

마지막 업데이트: 2026-09-08

관련 문서: Ruby에서의 Base64 인코딩: 완전한 가이드