문자열 인코딩과 디코딩
Base64.encode는 ByteArray를 받아 String을 반환합니다. encodeToByteArray()와 decodeToString()이 양방향 UTF-8 변환을 맡습니다.
import kotlin.io.encoding.Base64
val enc = Base64.encode("héllo wörld".encodeToByteArray())
println(enc) // aMOpbGxvIHfDtnJsZA==
println(Base64.decode(enc).decodeToString()) // héllo wörld
Base64 자체가 표준 인스턴스(Base64.Default)입니다. 그 밖에 Base64.UrlSafe와 Base64.Mime도 있습니다.
Kotlin 2.2 미만에서는 이 API에 @OptIn(ExperimentalEncodingApi::class)이 필요합니다. 오래된 버전을 벗어날 수 없다면 java.util.Base64가 Java에서 Base64 사용하기와 똑같이 동작합니다.
java.util.Base64.getEncoder().encodeToString("hi".toByteArray()) // aGk=
대표적인 용도는 HTTP 헤더와 Data URI입니다.
val token = Base64.encode("user:pass".encodeToByteArray())
println("Authorization: Basic $token") // Authorization: Basic dXNlcjpwYXNz
val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47)
println("data:image/png;base64," + Base64.encode(png)) // data:image/png;base64,iVBORw==
URL-safe Base64와 패딩 옵션
Base64.UrlSafe는 +/를 -_로 바꿉니다. 패딩은 새로 설정된 인스턴스를 반환하는 withPadding으로 제어합니다.
val d = "<<???>>".encodeToByteArray()
println(Base64.UrlSafe.encode(d)) // PDw_Pz8-Pg==
val urlNoPad = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
println(urlNoPad.encode(d)) // PDw_Pz8-Pg
println(urlNoPad.decode("PDw_Pz8-Pg").decodeToString()) // <<???>>
네 가지 옵션:
| 옵션 | 인코딩 | 디코딩 |
|---|---|---|
PRESENT (기본값) | =를 씀 | =를 요구 |
ABSENT | = 없음 | =를 거부 |
PRESENT_OPTIONAL | =를 씀 | 둘 다 허용 |
ABSENT_OPTIONAL | = 없음 | 둘 다 허용 |
기본 인스턴스는 이 점에서 엄격합니다.
Base64.UrlSafe.decode("PDw_Pz8-Pg")
// IllegalArgumentException: The padding option is set to PRESENT,
// but the input is not properly padded
JWT 부분이나 다른 토큰에는 대개 ABSENT_OPTIONAL이나 PRESENT_OPTIONAL이 알맞습니다. 생성하는 쪽마다 패딩 처리가 다르기 때문입니다. 배경 지식: Base64와 Base64URL 비교, 패딩 설명.
예를 들어 JWT 페이로드는 이렇게 읽습니다.
val jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
val url = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
println(url.decode(jwt.split(".")[1]).decodeToString()) // {"sub":"1234"}
MIME 줄 바꿈
Base64.Mime은 \r\n으로 구분된 76자 줄을 쓰고, 디코딩할 때는 줄 구분자를 무시합니다.
Base64.Mime.encode("x".repeat(60).encodeToByteArray())
// "eHh4eHh4...eHh4" (76 chars) + "\r\n" + "eHh4"
Base64.Mime.decode("aGVs\r\nbG8K").decodeToString() // "hello\n"
비교하려면 같은 입력을 MIME Base64 인코더에 붙여넣어 보세요.
파일 인코딩과 디코딩
작은 파일은 메모리에 올려도 됩니다.
import java.io.File
println(Base64.encode(File("hello.txt").readBytes())) // aGVsbG8K
File("copy.txt").writeBytes(Base64.decode("aGVsbG8K"))
큰 파일 스트리밍
JVM에서는 encodingWith와 decodingWith가 스트림을 감쌉니다. Kotlin 2.4에서도 아직 실험적 기능이므로 옵트인과 별도의 import가 필요합니다.
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
import kotlin.io.encoding.encodingWith
import java.io.File
@OptIn(ExperimentalEncodingApi::class)
fun encodeFile(src: File, dst: File) {
dst.outputStream().encodingWith(Base64).use { out ->
src.inputStream().use { it.copyTo(out) }
}
}
@OptIn(ExperimentalEncodingApi::class)
fun decodeFile(src: File, dst: File) {
src.inputStream().decodingWith(Base64).use { inp ->
dst.outputStream().use { inp.copyTo(it) }
}
}
인코딩 스트림을 닫아야 마지막 그룹과 패딩이 기록되며, use 블록이 이 일을 대신 해 줍니다. 300 KB 테스트 파일의 스트리밍 출력은 Base64.encode(readBytes())와 같았고, 왕복하면 같은 바이트가 돌아왔습니다. 실험적이지 않은 방법을 원한다면 java.util.Base64.getEncoder().wrap(outputStream)이 같은 일을 합니다.
검증과 예외 메시지
Kotlin 디코더는 Java보다 엄격합니다. Java를 비롯한 대부분의 라이브러리가 받아들이는 0이 아닌 패딩 비트를 거부합니다. 모든 실패는 IllegalArgumentException입니다.
Base64.decode에 넣은 입력 | 메시지 |
|---|---|
PDw_Pz8-Pg== | Invalid symbol '_'(137) at index 3 |
"aGVs\nbG8K" | Invalid symbol '\n'(12) at index 4 |
aGk=!! | Symbol '!'(41) at index 3 is prohibited after the pad character |
aGk== | Symbol '='(75) at index 3 is prohibited after the pad character |
aGl= | The pad bits must be zeros |
a | Input should have at least 2 symbols for Base64 decoding, startIndex: 0, endIndex: 1 |
괄호 안의 숫자는 8진수 문자 코드입니다(137은 _, 12는 줄 바꿈). 검증 도우미 함수는 한 줄이면 됩니다.
fun isBase64(s: String) = runCatching { Base64.decode(s) }.isSuccess
특정 문자열이 어디에서 깨지는지 보려면 Base64 검사기를 사용하세요. 흔한 원인은 Base64 오류 가이드에 정리되어 있습니다.
함정
- 기본 인스턴스에 패딩 없는 입력 넣기. JWT 구간과 많은 토큰에는
=가 없습니다. 문자열에 직접 패딩을 붙이지 말고PRESENT_OPTIONAL이나ABSENT_OPTIONAL로 설정하세요. - Android
DEFAULT플래그.android.util.Base64.encodeToString(bytes, Base64.DEFAULT)는 76자마다, 그리고 끝에 줄 바꿈을 넣습니다. HTTP 헤더에서는 이 때문에 요청이 깨집니다.NO_WRAP을 쓰고, 토큰이라면NO_WRAP or URL_SAFE or NO_PADDING을 쓰세요. String.toByteArray()의 문자 집합. 오래된 JDK에서 Java의getBytes()와 달리 Kotlin에서는 기본값이 UTF-8이므로"héllo".toByteArray()는 안전합니다.- 바이너리를 문자열로 디코딩하기. 이미지 바이트에
decodeToString()을 쓰면 잘못된 시퀀스가U+FFFD로 바뀌어 데이터가 손상됩니다.ByteArray로 유지하세요.
버전별 참고 사항
- Kotlin 1.8.20:
kotlin.io.encoding.Base64가 실험적 기능으로 추가됨. - Kotlin 2.0.20:
withPadding과PaddingOption. - Kotlin 2.2: 핵심 API가 안정화됨. JVM 스트림 도우미는 여전히 실험적 기능.
자주 묻는 질문
아직도 @OptIn(ExperimentalEncodingApi::class)이 필요한가요?
Kotlin 2.2 이상의 Base64.encode와 Base64.decode에는 필요 없습니다. 스트림 도우미인 encodingWith와 decodingWith는 Kotlin 2.4에서도 여전히 옵트인이 필요합니다.
Kotlin에서 패딩 없는 Base64를 디코딩하려면 어떻게 하나요?
설정한 인스턴스를 사용하세요. 패딩 없는 입력에는 Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)를, 둘 다 받으려면 PRESENT_OPTIONAL을 씁니다. 기본 인스턴스는 패딩을 요구합니다.
android.util.Base64와 kotlin.io.encoding.Base64 중 무엇을 써야 하나요?
새 코드라면 Kotlin API가 표준 라이브러리의 일부이므로 모든 Android 버전에서 동작합니다. android.util.Base64는 Base64.NO_WRAP을 넘기지 않으면 줄 바꿈을 추가하는데, 이것이 헤더가 깨지는 흔한 원인입니다.