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

Kotlin에서의 Base64 인코딩: 완전한 가이드

Base64 대화의 절반은 패킹된 데이터를 다시 바이트로 읽는 것에 관한 것이다. 나머지 절반, 그리고 이 사이트가 바로 그 부분이다: 처음부터 그 패킹된 데이터를 만들어 내는 것에 관한 것이다. 당신의 어딘가에는 텍스트만 이해하는 통로를 통해 이동해야 하는 바이트가 있다: JSON 문자열, HTTP 헤더, 이메일, URL, 설정 파일. Base64는 고전적인 답이고, Kotlin은 표준 라이브러리에 이에 대한 일급 답을 갖고 있다: kotlin.io.encoding의 Base64 클래스, Kotlin 2.2부터 안정적이며.

이 가이드는 당신이 Base64를 만드는 쪽일 때 실제로 필요한 것을 다룬다: 네 가지 프리셋 스키마, 패딩 다이얼, 이메일과 인증서가 사는 줄 감기 규칙, URL 안전 알파벳, 바이트는 어디에서 오는지, 그리고 각 상황에 Kotlin을 얹은 실전 시나리오 모음. 형식 자체 - 3바이트가 어떻게 4자가 되는지, =는 어디에서 오는지 - 은 홈 페이지에서 다루므로, 여기서는 곧바로 Kotlin으로 들어간다.

한 클래스, 네 가지 프리셋

API 전체는 kotlin.io.encoding 패키지의 한 클래스, Base64다. 만들어야 하는 encoder 오브젝트도 없고, 빌더도 없다. 대신, 이 클래스는 RFC 스키마마다 하나씩인 프리셋 인스턴스 네 개를 함께 갖고 있고, 가장 흔한 것을 조용히 대신 서 있는 companion object가 있다:

인스턴스알파벳인코딩 시 줄 감기인코딩 시 패딩무엇에 쓸 때
Base64.Default+와 /없음= 출력일반 용도, API, 데이터 URL
Base64.UrlSafe-와 _없음= 출력 (끄기)URL, 토큰, JWT
Base64.Mime+와 /76자마다 CRLF= 출력이메일 본문과 첨부 파일
Base64.Pem+와 /64자마다 CRLF= 출력인증서와 개인 키

사람들을 걸려 넘어지게 하는 이름 짓기 디테일: 이것은 인스턴스이지 팩토리가 아니다. 모든 인스턴스는 불변 값이며, 그 동작을 바꾸면 - 패딩 같은 것 - 기존 인스턴스를 수정하는 대신 새로운 인스턴스가 돌아온다. 덕분에 프리셋은 스레드 사이에서 공유하고 오브젝트에 저장하는 것이 안전하며, 그것이 바로 이 클래스 전체가 내부 상태 없는 단순한 값 타입일 수 있는 이유다.

첫 인코딩: 바이트를 넣고 문자열을 뽑다

사이트에서 쓸 수 있는 가장 작은 프로그램: 바이트 5개를 넣고, 8자 문자열 하나를 뽑아 낸다. 입력은 언제나 ByteArray(또는 그것의 조각)이고, 결과는 텍스트가 허용되는 어디에든 넣을 수 있는 평범한 String이다:

import kotlin.io.encoding.Base64
fun main() {
  val bytes = "Hello".encodeToByteArray()
  val packed = Base64.encode(bytes)
  println(packed)  // SGVsbG8=
}

그 한 줄은 겉보기보다 많은 일을 한다. Kotlin은 같은 동작의 여러 모양을 주고, 전부 함수 이름이 말하는 대로 읽힌다:

  • encode(bytes)는 String을 돌려준다. 위 모양.
  • encodeToByteArray(bytes)는 ASCII 문자의 ByteArray를 돌려준다. 패킹된 형태 자체가 또 다른 버퍼에 들어갈 때 유용하다.
  • encodeIntoByteArray(bytes, destination)는 이미 할당해 둔 ByteArray에 쓰므로, 핫 패스에서 할당을 하나 줄인다.
  • encodeToAppendable(bytes, builder)는 Appendable을 구현한 것, 예컨대 StringBuilder 같은 것에 덧붙인다. 더 큰 문서를 조립할 때 자연스러운 선택지다.

네 가지 모두 같은 선택 startIndex와 endIndex 범위를 받으므로, 먼저 복사하지 않고 큰 버퍼의 조각을 패킹할 수 있다. Base64.Default가 companion object이기 때문에, 인스턴스를 빼고 Base64.encode(bytes)라고 간단히 쓸 수도 있다. 두 형태는 같은 호출이다.

4/3 규칙: 얼마나 길어질까?

인코더를 내놓기 전에, 출력이 정확히 얼마나 더 클지 아는 것이 값지다. Base64는 이미 갖고 있는 정보에 문자를 써 나가니까. 수학은 엄격하다: 입력 바이트 3개가 매번 정확히 출력 문자 4개가 되고, 남은 1바이트든 2바이트든 여전히 4개의 완전한 그룹을 쓰며, =로 그룹을 채운다. 처음 몇 가지 크기에 대한 결과는 이렇다:

입력 바이트12345678
출력 문자 수4448881212

표 뒤의 공식은 4 * ceil(bytes / 3)이다. 최악의 경우, 바이트 1개가 문자 4개가 되어 300퍼센트 할증이다. 3바이트부터는 유선 위의 데이터가 대략 3분의 1 더 많은 것으로 수렴한다. 이것이 비용 모델 전체다. 인스턴스별 차이가 없으며, 그래서 큰 페이로드를 Base64로 돌리기 전에 의도적으로 판단하고, 반사적으로 손대지 말아야 하는 이유다.

패딩은 운명이 아니라 설정

인코딩 쪽에서 = 문자는 정책 결정이며, Kotlin은 그것을 일급 시민으로 만든다. 모든 인스턴스는 PaddingOption을 갖고, withPadding은 다이얼을 옮긴 새로운 인스턴스를 건넨다. 네 가지 프리셋 모두 PRESENT에서 시작하므로, "Hello"는 SGVsbG8가 아니라 SGVsbG8=로 나와:

import kotlin.io.encoding.Base64
fun main() {
  val bytes = "Hello".encodeToByteArray()
  val noPad = Base64.Default.withPadding(Base64.PaddingOption.ABSENT)
  println(Base64.encode(bytes))      // SGVsbG8=
  println(noPad.encode(bytes))       // SGVsbG8
}

다이얼에는 네 가지 위치가 있다. 이름의 첫 번째 말이 인코더가 무엇을 출력할지 정하고, 후반부는 나중에 당신이 (또는 상대방이) 그것을 거꾸로 돌릴 때 같은 인스턴스의 디코더가 얼마나 엄격할지 정한다:

PaddingOption인코더가 = 출력디코더가 = 수용
PRESENT예필수, 그 외는 실패
ABSENT아니오금지, 여분의 패딩은 실패
PRESENT_OPTIONAL예둘 다
ABSENT_OPTIONAL아니오둘 다

인코딩 시 가장 흔한 선택은 UrlSafe 알파벳에 ABSENT이며, 이는 JSON Web Token과 많은 URL 스키마가 기대하는 정확한 모양이다. 잠시 후에 다시 만나게 될 것이다.

base64url: URL, 토큰과 JWT

고전 알파벳에는 +와 /가 있고, 둘 다 URL에서는 재앙이다: 쿼리 문자열의 +는 습관적으로 공백으로 읽히고, /는 경로 구분자다. RFC 4648 5절이 URL 안전 변형을 정의하며 -와 _를 넣어 바꾸고, Base64.UrlSafe가 바로 그 스키마다. 고전 알파벳에서 /를 만드는 것과 같은 바이트를 인코딩하면 그 스왑이 실제로 작동하는 것을 볼 수 있다:

import kotlin.io.encoding.Base64
fun main() {
  val bytes = "Hello?".encodeToByteArray()
  println(Base64.encode(bytes))          // SGVsbG8/
  println(Base64.UrlSafe.encode(bytes))  // SGVsbG8_
}

정석적인 실사용자는 JWT로, 그 헤더와 페이로드는 마침표로 이어진 패딩 없는 base64url이다. 여기서 하나를 만드는 인코딩 쪽 절반을 보여 준다. 최종 토큰의 서명을 라이브러리가 하든, 당신이 이해해야 하는 모양이다:

import kotlin.io.encoding.Base64
fun main() {
  val header = """{"alg":"HS256","typ":"JWT"}"""
  val payload = """{"sub":"1234567890","name":"John Doe"}"""
  val noPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
  val h = noPad.encode(header.encodeToByteArray())
  val p = noPad.encode(payload.encodeToByteArray())
  val token = "$h.$p.Ym9nVXNlZlNpZ25hdHVyZUZvckRlbW8"
  println(token)
}

출력:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.Ym9nVXNlZlNpZ25hdHVyZUZvckRlbW8

여기에 경고 두 개가 속한다. 첫째, 세 번째 세그먼트는 서명이며, 진짜 서명을 만들려면 진짜 암호학(JCA/JCE 서명기나 JWT 라이브러리)이 필요하다. 손수 만든 바이트는 절대 안 된다. 위의 스니펫은 인코딩 모양을 보여주는 것뿐이다. 둘째, JVM에서 습관으로 java.util.Base64.getUrlEncoder()를 손대려 한다면, 그것은 기본적으로 패딩하므로, JWT 스타일 출력을 위해서는 거기서 .withoutPadding()이 필요하다는 것을 알아 두자. Kotlin 프리셋도 기본 상태로 똑같이 패딩을 붙이고, 대신 withPadding 호출로 제외한다.

줄 감기: Mime과 Pem 프리셋

네 가지 프리셋 중 두 개가 출력을 짧은 줄로 감싸는데, 그 이유는 역사적이다. 오래된 이메일 전송망은 긴 줄을 망가뜨렸으니, RFC 2045 6.8절은 MIME base64를 줄당 76자로 제한하고, PKI 도구는 더 오래된 PEM 전통을 따라 64자를 쓴다. Kotlin은 두 규칙 모두를 프리셋 자체에 녹여 놓았다: 줄 구분기는 CRLF, 줄 바꿈은 정확히 제한에 떨어지고, 아주 마지막에 끝 구분기는 없다. 바이트 200 페이로드를 각각의 래퍼에 통과시키면 이 모양이다:

import kotlin.io.encoding.Base64
fun main() {
  val data = ByteArray(200) { (it % 251).toByte() }
  println(Base64.Mime.encode(data).lines().maxOf { it.length })  // 76
  println(Base64.Pem.encode(data).lines().maxOf { it.length })   // 64
}

그 입력에 대해 Mime은 4줄을, Pem은 5줄을 만든다. Mime의 첫 번째 줄은:

AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4

기억해야 할 함정은, 당신이 코드를 쓰고 있는 방향의 반대 방향이다: 감겨 나온 출력은 한 줄이 아니다. Mime 출력을 엄격한 한 줄 소비자에게 먹이면, CRLF가 디코딩 오류가 되니, 편의가 아니라 채널로 래퍼를 고르자. API, 데이터 URL, 그리고 모든 현대적인 것에는 Default가 맞는 기본값이며, 감기는 이메일과 인증서의 이야기다.

바이트는 어디에서 오는가?

인코더는 당신이 건네는 바이트만큼 정직할 뿐이며, 흥미로운 결정은 encode가 불리는 바로 한 단계 전에 일어난다. 가장 흔한 출처는 텍스트이고, 가장 흔한 실수는 문자 인코딩이 조용히 결정하게 두는 것이다:

  • text.encodeToByteArray()는 모든 플랫폼에서 언제나 UTF-8이다. JSON, 이메일, 웹 데이터에는 맞는 선택이고, 텍스트가 Latin-1 또는 UTF-16이면서 다른 쪽이 그에 맞춰 디코딩한다면 그 반대다.
  • JVM에서는 인라인 확장 text.toByteArray(charset)로 명시적으로 고를 수 있다. 이것은 Kotlin 1.0부터 표준 라이브러리에 있으며, Java의 getBytes(charset)에 대한 Kotlin 쪽의 답이다. kotlin.String에는 getBytes가 없으므로, Kotlin 문자열에 text.getBytes()를 쓰면 컴파일러가 알려 준다. 확장이 바로 그 길이다.
import kotlin.io.encoding.Base64
fun main() {
  val text = "héllo"
  println(Base64.encode(text.encodeToByteArray()))               // aMOpbGxv
  println(Base64.encode(text.toByteArray(Charsets.ISO_8859_1)))  // aOlsbG8=
}

같은 다섯 글자, 두 가지 다른 패킹된 형태. Base64가 개입하기도 전에 바이트가 달랐기 때문이다. 나중에 디코더가 UTF-8을 가정한다면, Latin-1 버전은 모지바케로 디코딩되며, 어느 쪽 끝에서 Base64 트릭을 써도 문자 인코딩 불일치를 고칠 수는 없다.

다른 바이트 출처들도 같은 모양을 따른다. 파일은 file.readBytes() 또는 path.readBytes()에 이은 encode다. 미리 할당된 버퍼는 encodeIntoByteArray(bytes, destination)를 쓴다. 조립 중인 문서는 encodeToAppendable(bytes, builder)를 쓰며, 이것이 목적지를 돌려주므로 호출이 빌더 메서드처럼 연쇄된다:

import kotlin.io.encoding.Base64
fun main() {
  val sb = StringBuilder("prefix-")
  Base64.encodeToAppendable("Hello".encodeToByteArray(), sb)
  println(sb)  // prefix-SGVsbG8=
}

그리고 JVM에는 메모리에 들어가지 않는 입력을 위한 스트리밍 형태가 있다. 아직 실험적이라는 표식이 붙어 있고, 자기 이름으로 임포트한다. 이름의 비틀린 곳: encodingWith는 출력 스트림을 감싸므로, 그것을 통해 쓰이는 것은 base64로 나와, base64 바이트는 그 아래에 있는 스트림에 떨어진다:

import java.io.ByteArrayOutputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.encodingWith
@OptIn(ExperimentalEncodingApi::class)
fun main() {
  val raw = ByteArray(10_000) { (it % 251).toByte() }
  val packed = ByteArrayOutputStream()
  packed.encodingWith(Base64.Default).use { encoded ->
    encoded.write(raw)
  }
  println(packed.size())  // 13336
}

실용 규칙: 들어가는 것은 전부 메모리 내 encode, 들어가지 않는 스트림은 encodingWith, 그리고 바이트가 실제로 텍스트일 때는 언제나 명시적인 문자 인코딩.

현장 기록: HTTP Basic 인증

HTTP Basic 인증은 인터넷에서 가장 오래된 Base64 용도이며, 여전히 서비스 간 트래픽 어디에나 있다. RFC 7617이 스키마를 정의한다: 사용자와 패스워드를 가져와, 단일 콜론으로 이어 붙이고, 결과를 base64로 하고, Authorization 헤더에 Basic와 공백, 그리고 패킹된 문자열로 실어 보낸다. Kotlin으로:

import kotlin.io.encoding.Base64
fun main() {
  val credentials = "alice:s3cr3t"
  val header = "Basic " + Base64.encode(credentials.encodeToByteArray())
  println(header)  // Basic YWxpY2U6czNjcjN0
}

여기서 Base64가 아니라 더 강한 것을 쓰지 않는 이유는? 헤더 값은 하나의 인쇄 가능 토큰이어야 하고, Base64는 그것을 보장하기 때문이다. 정직한 경고: Base64는 인코딩이지 암호화가 아니다. 어떤 클라이언트든 YWxpY2U6czNjcjN0를 한 단계로 거꾸로 돌려 alice:s3cr3t를 되찾을 수 있으니, Basic 인증은 TLS 연결 위에만 머물러야 하며, 가능하면 인간이 만드는 패스워드보다 토큰 자격 증명이 낫다. 이런 헤더를 파싱할 때는, 패스워드에 법적으로 콜론이 들어갈 수 있으므로, 콜론으로 정확히 한 번만 나눠라.

현장 기록: 이미지와 데이터 URL

데이터 URL은 바이너리 자산을 HTML, CSS, JSON에 직접 넣어서 브라우저가 두 번째 요청을 만들지 않게 한다. 모양은 미디어 타입, 쉼표, 단어 base64, 또 하나의 쉼표, 그리고 패킹된 바이트다:

import kotlin.io.encoding.Base64
fun main() {
  val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D.toByte(), 0x0A.toByte(), 0x1A.toByte(), 0x0A.toByte())
  val dataUrl = "data:image/png;base64," + Base64.encode(png)
  println(dataUrl)  // data:image/png;base64,iVBORw0KGgo=
}

위의 바이트는 PNG 파일의 처음 여덟 개, 모든 디코더가 확인하는 매직 넘버다. 왜 Base64가 맞는가: 페이로드는 마크업 안에서 URL 안전 텍스트 토큰이여야 하고, Base64는 안정적인 문법을 가진, 널리 지원되는 유일한 바이너리-텍스트다. 함정은 크기다. 킬로바이트 300의 로고는 마크업 약 400킬로바이트가 되고, 그 추가 1킬로바이트마다 그것을 포함하는 모든 페이지 로딩에서 비용을 치른다. 데이터 URL은 아이콘, 아바타, 작은 스프라이트에는 훌륭한 도구지만, 비디오에는 끔찍한 도구이고, 큰 사진에도 형편없는 도구다. 인라인으로 넣기 전에 먼저 재라.

현장 기록: 이메일 첨부 파일

SMTP는 바이너리라는 개념보다 오래된 텍스트 프로토콜이므로, 당신이 지금까지 받아온 모든 이메일의 모든 첨부 파일은 Base64이며, 76자에서 감겨 있고, Content-Transfer-Encoding: base64 헤더로 선언된다. 작은 바이너리 첨부 파일을 가진 최소한의 MIME 부분은 이 모양이며, 바이트 5개의 %PDF- 헤더에 대해 Kotlin이 만든 본문 슬롯을 채워 넣었다:

From: sender@example.com
To: receiver@example.com
Subject: report
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="cut-here"

--cut-here
Content-Type: text/plain; charset="utf-8"

The quarterly report follows as an attachment.

--cut-here
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="report.pdf"

JVBERi0=
--cut-here--

(본문의 JVBERi0=는 바이트 5개 %PDF-의 base64다. 진짜 보고서라면 76자 줄 여러 개에 걸쳐 감길 것이다.) 파일 바이트를 갖고 있다면, Kotlin 쪽은 한 줄이다:

import kotlin.io.encoding.Base64
fun main() {
  val pdf = byteArrayOf(0x25, 0x50, 0x44, 0x46, 0x2D)  // "%PDF-"
  println(Base64.Mime.encode(pdf))  // JVBERi0=
}

함정은 채널의 규율이다. 본문에는 Default가 아니라 Mime를 써야 한다. 엄격한 MIME 파서가 감기를 기대하기 때문이고, 10,000자의 평범한 Default 한 줄은 어떤 전송망에서 거부되거나 망가져 버린다. Content-Transfer-Encoding 줄의 헤더 대소문자를 정확히 base64로 유지하고, 감기는 형식의 일부라는 것을 기억하라: 감기지 않은 출력과 감긴 출력은 같은 바이트의 다른 표현이며, 다른 쪽의 파서는 자신이 무엇을 먹고 있는지 알아야 한다.

현장 기록: JSON API와 업로드

API가 JSON 문서 안에 바이너리를 원할 때, 관례는 base64를 담은 문자열 필드이며, 텍스트에 이미 자리가 있는 JSON에서 가장 편리한 패턴 중 하나다. kotlinx.serialization를 쓰면 왕복은 명쾌하다:

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
@Serializable
data class UploadRequest(val name: String, val payload: String)
fun main() {
  val icon = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
  val request = UploadRequest("icon.png", Base64.encode(icon))
  val json = Json.encodeToString(UploadRequest.serializer(), request)
  println(json)  // {"name":"icon.png","payload":"iVBORw=="}
}

여기서 Base64인 이유: JSON에는 바이너리 타입이 없으니 페이로드는 텍스트여야 하고, Base64는 문서 없이도 API 사용자가 알아볼 수 있는 가장 놀라지 않는 바이너리 문법이다. 함정은 규모다. 4/3 할증은 모든 요청과 모든 응답에서 치러지고, 10메가바이트 업로드는 파서가 메모리에 담아, 이스케이프하고, 검증해야 하는 13.3메가바이트 JSON 문자열이 된다. 큰 파일에는 multipart/form-data 또는 바이너리 본체가 거의 항상 더 나은 전송 형식이다. JSON 안의 base64는 편의가 세금을 이기는 썸네일, 아이콘, 서명, 작은 덩어리에 아껴 두자.

현장 기록: 설정과 명령줄

마지막 두 패턴은 모든 코드베이스에 나타나는 작은 것들이다. 설정 값, 토큰, 라이선스 키, 때로는 작은 시크릿도, 전송이 텍스트 전용이고 값에 인용부호나 줄 바꿈이 들어갈 수 있어서, base64로 환경 변수와 프로퍼티 파일을 통해 이동한다. 그것들을 다시 읽는 것은 같은 두 단계 춤을 거꾸로 하는 것이다: System.getenv 또는 프로퍼티 조회, 그리고 디코딩. 명령줄에서는, 전송이나 검사를 위해 파일을 인코딩하는 것은 10줄짜리 프로그램이다:

import java.io.File
import kotlin.io.encoding.Base64
fun main(args: Array<String>) {
  require(args.isNotEmpty()) { "usage: b64encode <file>" }
  val bytes = File(args[0]).readBytes()
  val encoded = Base64.encode(bytes)
  File(args[0] + ".b64").writeText(encoded)
  println("Wrote ${encoded.length} characters to ${args[0]}.b64")
}

바이트 10개 hello file을 담은 파일에서 실행하면, 16자, aGVsbG8gZmlsZQ==를 쓴다. 두 경우의 함정은 같은 두 가지다: 설정 안의 Base64는 금고가 아니다 - 값은 평문에서 한 단계 거리에 있으며, 어쨌든 전송 위에서 시크릿처럼 대우해야 하고 - 손수 만든 CLI 도구는 알파벳을 의도적으로 결정해야 한다. 당신의 출력을 URL로 파이프하는 사용자는 Default가 아니라 UrlSafe를 필요로 하니까.

인코딩 때 무엇이 잘못될 수 있는가

인코딩은 내용에 관대하다: 어떤 바이트 시퀀스든 유효한 입력이므로, 디코더가 갖는 그런 "무효한 심볼" 실패는 없다. 정말로 던지는 것은 기하학이며, 메시지는 쓸모 있을 만큼 정확하다:

상황예외메시지
endIndex가 배열 끝을 넘을 때IndexOutOfBoundsExceptionstartIndex: 0, endIndex: 100, size: 5
startIndex가 endIndex를 넘을 때IllegalArgumentExceptionstartIndex: 3 > endIndex: 2
목적지 배열이 encodeIntoByteArray에 너무 작을 때IndexOutOfBoundsExceptionThe destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8

이것들과 함께 있는 Kotlin 특유의 함정이 두 개 있다. 첫째는 고전적인 int + String 실수: bytes.size + " bytes"는 컴파일되지 않는다. Int의 더하기는 문자열을 이어 붙이지 않으니까. 보간 형태 "${bytes.size} bytes"가 Kotlin 방식이다. 둘째는 문자 인코딩 조회로, JVM이 모르는 이름에는 UnsupportedCharsetException을 던지니, 예컨대 Charset.forName("utf-9")처럼, 문자 인코딩 이름의 오타는 컴파일 오류가 아니라 런타임 예외이며, 이름을 쓴 곳이 아니라 인코더가 실행되는 곳에서 나타난다.

함정, Kotlin 스타일

아래 함정들은 표준 라이브러리에 처음 손을 뻗는 Kotlin 개발자에게 특히 물리는 것들이다:

  • encodeToByteArray()가 문자 인코딩을 골라지게 두는 것. 그것은 언제나, 조용히 UTF-8이며, Latin-1 또는 UTF-16 소스는 디코더가 다시 읽을 수 없는 바이트로 패킹된다. 문자 인코딩을 의도적으로 결정하고, UTF-8이 아니라면 JVM에서 toByteArray(charset)를 써라.
  • 근육 기억으로 java.util.Base64를 손대려는 것. 그 getUrlEncoder()는 기본적으로 패딩하므로, .withoutPadding()을 기억하지 않으면 JWT에는 잘못된 모양이다. Kotlin 프리셋은 양쪽 모두에서 선택을 명시적으로 만든다.
  • Mime 또는 Pem의 감긴 출력을 한 줄 소비자에게 먹이는 것. CRLF는 표현의 일부이며, 한 줄을 기대하는 엄격한 디코더에서 실패한다. 채널이 감기를 기대할 때만 감아라.
  • Kotlin 문자열에 text.getBytes()를 쓰는 것. Java의 메서드는 kotlin.String에서 보이지 않는다. Kotlin 1.0부터 있는 인라인 toByteArray(charset) 확장이 대체재다.
  • 오래된 도구 체인을 돌리는 것. 일부 배포판의 시스템 Kotlin은 여전히 1.3으로, 표준 라이브러리의 Base64보다 훨씬 이전이다. 이 클래스는 존재하려면 1.8.20, 패딩 제어를 위해서는 2.0.20, 안정화는 2.2가 필요하다.
  • Base64를 보안 계층으로 취급하는 것. 이것은 공개된, 한 단계 역함수를 가진 전송 인코딩이다. 시크릿인 것은 패킹되기 전에 암호화해야지, 단순히 패킹하는 것으로는 안 된다.

잘 고르는 법: 빠른 결정 가이드

불확실하면, 결정은 거의 언제나 내용이 아니라 채널이 한다. 짧은 버전:

  • Base64.Default - API, JSON, 데이터 URL, 그리고 실질적으로 한 줄의 텍스트인 모든 것. 패딩된 출력이 전송 위에서 가장 호환되는 모양이다.
  • Base64.UrlSafe에 ABSENT 패딩 - 토큰, JWT, 그리고 URL 세그먼트나 쿼리 매개변수에 떨어지는 모든 것.
  • Base64.Mime - 이메일 본문과 첨부 파일. 76자 줄이 형식의 단단한 요구인 곳.
  • Base64.Pem - 인증서와 개인 키. 모든 PKI 도구가 기대하는 64자 줄이 그곳에 있는 곳.

그다음, 두 가지 가로지르는 습관: 입력이 텍스트일 때는 언제나 문자 인코딩을 명시적으로 만들고, 큰 페이로드는 base64 통로가 아니라 바이너리 통로를 타도록 4/3 할증을 눈여겨보라.

표준 라이브러리까지의 길

표준 라이브러리에서 Base64까지의 길은, Base64가 없는 오래된 Kotlin을 만나게 될 만큼 최근이다. 이 클래스는 2023년 4월의 Kotlin 1.8.20에 처음 등장했는데, 실험적 표식이 붙었고, 세 인스턴스와 더 단순한 표면으로: 인코딩은 언제나 패딩했고, 덜을 달라고 할 길은 없었다. 문자열 끝의 =를 removeSuffix로 자르는 1.8 시대 코드를 본 적이 있다면, 그것이 패딩 없는 출력을 위한 그 시대의 유일한 도구였으며, 이제 떨어뜨릴 가치가 있는 습관이다. 2025년 6월 출시된 Kotlin 2.2가 API를 안정화하고 마지막 조각, Pem 인스턴스를 추가했다. PaddingOption 다이얼과 withPadding은 이미 2.0 라인에, 정확히 2.0.20에 도착해 있었으니, 양쪽 방향으로 패딩을 제어하는 첫 번째 일급 방식이다. 스트리밍 헬퍼 encodingWith와 decodingWith는 여전히 실험적이고 JVM 전용이며, 이것이 표준 라이브러리가 동결하기 전에 더 많은 현장 경험을 원하는 API를 표시하는 방식이다. 2.4.0 라인부터, 언어는 표준 라이브러리의 18개월 지원 기간으로 옮겨갔으니, 이 글이 쓰이는 시점에서 현재인 2.4.10 안정 릴리스 같은 2.4.x 컴파일러에 고정된 프로젝트는, 그 지원 주기의 수명 동안 Base64 API 전체를 얻는다.

작은 경이들

알게 되면 이 클래스를 더 흥미롭게 만드는 몇 가지 디테일:

  • 인스턴스 없이 Base64.encode(bytes)가 동작하는 이유는, Base64.Default가 companion object에 정의되어 있기 때문이다. companion object가 기본 스키마 그 자체이므로, 간단한 형태와 이름 붙인 형태는 문자 그대로 같은 오브젝트다.
  • encodeToAppendable 함수는 빌더 스타일이다: 목적지 appendable을 돌려주므로, 문서화된 패턴은 반환 값을 무시하고 자신의 빌더를 계속 쓰는 것이다.
  • 패딩은 절대 그룹 전체를 채우지 않는다: base64 문자열은 0개, 1개, 또는 2개의 =로 끝나며, 패딩을 세면 원래의 마지막 3바이트 묶음에서 몇 바이트가 남았는지 정확히 안다.
  • Pem은 네 가지 프리셋 중 가장 최신이며 2.2에 합류했다. 64자 감기는, 바로 옆에 있는 RFC 2045 규칙보다 오래된 PKI 관례다.
  • JVM에서 표준 라이브러리는 의도적으로 java.util.Base64에 위임하지 않는다. 두 구현은 별개이며, 이것은 플랫폼 전반에서 동작이 동일하도록 유지하되, Kotlin 팀이 Java API가 허용하는 미래를 위해 트리에 유지해 온 주석 처리된 최적화의 비용을 치르는 방식이다.
  • Base64를 안정화한 그 2.2 릴리스는 Kotlin 1.9부터 실험적이던 kotlin.text의 16진수 형식 클래스 HexFormat도 함께 안정화했으니, 바이트 수준의 텍스트 인코딩은 이제 표준 라이브러리에 정착한 보금자리를 갖게 되었다.

정리와 다음 단계

Kotlin에서 Base64를 만드는 것은 짧은 목록의 의도된 선택이다: 채널로 프리셋을 고르고, 패딩을 의도적으로 결정하고, 입력이 텍스트일 때는 문자 인코딩을 명시적으로 유지하고, 페이로드가 클 때는 4/3 할증을 존중한다. 나머지 모든 것 - 파일, 버퍼, appendable, 스트림 - 은 같은 네 인스턴스 위의 얇은 래퍼다. 반대 방향, 그 패킹된 텍스트를 다시 바이트로 가져오는 것, 은 자기만의 엄격성 규칙, 자기만의 실패 모드, 자기만의 함정을 갖고 있으며, 자매 사이트의 관련 글은 Kotlin에서의 Base64 디코딩을 깊이 있게 다룬다.

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

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