파일 업로드
  1. base64hub
  2. 코드로 다루는 Base64
  3. Java
코드

Java에서 Base64 사용하기

Java 8부터 java.util.Base64가 스트리밍을 포함해 표준, URL-safe, MIME Base64를 모두 처리합니다. 아래 출력은 모두 Java 21 기준입니다.

문자열 인코딩과 디코딩

java.util.Base64는 바이트를 다룹니다. 문자열은 문자 집합을 명시해서 변환하세요. 인수 없는 getBytes()는 플랫폼 기본값을 쓰는데, Java 18 이전에는 이것이 항상 UTF-8은 아니었습니다.

import java.nio.charset.StandardCharsets;
import java.util.Base64;

String enc = Base64.getEncoder()
        .encodeToString("héllo wörld".getBytes(StandardCharsets.UTF_8));
System.out.println(enc);   // aMOpbGxvIHfDtnJsZA==

String dec = new String(Base64.getDecoder().decode(enc), StandardCharsets.UTF_8);
System.out.println(dec);   // héllo wörld

Base64.Encoder와 Base64.Decoder 인스턴스는 스레드 안전하므로 static final 필드에 두고 써도 됩니다.

패딩 있는 URL-safe Base64와 패딩 없는 URL-safe Base64

getUrlEncoder()는 -와 _를 씁니다. 패딩은 기본으로 켜져 있으며, withoutPadding()은 패딩을 생략하는 새 인코더를 반환합니다.

byte[] d = "<<???>>".getBytes(StandardCharsets.UTF_8);

Base64.getUrlEncoder().encodeToString(d);                   // PDw_Pz8-Pg==
Base64.getUrlEncoder().withoutPadding().encodeToString(d);  // PDw_Pz8-Pg

new String(Base64.getUrlDecoder().decode("PDw_Pz8-Pg"), StandardCharsets.UTF_8);
// <<???>>

디코더는 패딩이 있든 없든 입력을 받으므로 getDecoder().decode("aGk")도 동작해 hi를 반환합니다. JWT 구간은 패딩 없는 Base64URL입니다. 어느 쪽을 언제 쓰는지는 Base64와 Base64URL 비교를 참고하세요.

MIME 인코더와 디코더

getMimeEncoder()는 RFC 2045의 요구대로 \r\n으로 구분된 76자 줄을 씁니다. 마지막 줄 뒤에는 줄 바꿈을 붙이지 않습니다.

String mime = Base64.getMimeEncoder().encodeToString("x".repeat(60).getBytes());
// eHh4eHh4...eHh4  (76 chars) + "\r\n" + "eHh4"

Base64.getMimeEncoder(64, "\n".getBytes());   // PEM-style 64-char lines

줄 길이는 4의 배수로 내림됩니다. getMimeDecoder()는 관대한 디코더로, 줄 구분자와 알파벳 밖의 다른 모든 문자를 무시합니다.

Base64.getMimeDecoder().decode("aGVs\r\nbG8K!");   // bytes of "hello\n"

MIME Base64 인코더는 브라우저에서 같은 형식을 만들어 줍니다.

파일 인코딩과 디코딩

작은 파일:

import java.nio.file.*;

byte[] file = Files.readAllBytes(Path.of("hello.txt"));
System.out.println(Base64.getEncoder().encodeToString(file));   // aGVsbG8K

Files.write(Path.of("copy.txt"), Base64.getDecoder().decode("aGVsbG8K"));

wrap()으로 큰 파일 스트리밍

Encoder.wrap(OutputStream)과 Decoder.wrap(InputStream)은 데이터를 흘려보내면서 인코딩하고 디코딩하므로, 파일이 아무리 커도 메모리 사용량이 일정합니다.

try (InputStream in = new FileInputStream("big.bin");
     OutputStream out = Base64.getEncoder().wrap(new FileOutputStream("big.b64"))) {
    in.transferTo(out);
}

try (InputStream in = Base64.getDecoder().wrap(new FileInputStream("big.b64"));
     OutputStream out = new FileOutputStream("big.out")) {
    in.transferTo(out);
}

감싼 스트림을 닫는 것이 중요합니다. 인코더는 close() 시점에 마지막 그룹과 패딩을 씁니다. 이를 잊으면 패딩을 포함해 최대 네 문자로 된 마지막 그룹이 통째로 빠집니다. 300 KB 테스트 파일에서 출력은 encodeToString과 정확히 일치했고 왕복 결과도 바이트 단위로 같았습니다. InputStream.transferTo는 Java 9가 필요합니다. Java 8에서는 버퍼 반복문으로 복사하세요.

검증과 예외

기본 디코더와 URL 디코더는 엄격하며 java.lang.IllegalArgumentException을 던집니다. 메시지는 무엇이 잘못되었는지 알려 주며, 문자는 16진수로 표시됩니다.

getDecoder()에 넣은 입력메시지
PDw_Pz8-Pg==Illegal base64 character 5f
"aGVs\nbG8K"Illegal base64 character a
aGk=!!Input byte array has incorrect ending byte at 4
aGk==Input byte array has incorrect ending byte at 4
aInput byte[] should at least have 2 bytes for base64 bytes

그래서 검증은 try/catch만으로 충분합니다.

static boolean isBase64(String s) {
    try {
        Base64.getDecoder().decode(s);
        return true;
    } catch (IllegalArgumentException e) {
        return false;
    }
}

빈틈이 하나 있습니다. 디코더는 0이 아닌 패딩 비트를 받아들이므로 aGl=도 아무 불평 없이 hi로 디코딩됩니다. 정규 형식의 입력만 받아야 한다면 다시 인코딩해서 비교하세요. Base64 검사기는 어떤 문자가 문제인지 보여 주고, 흔한 Base64 오류는 실패 유형별로 설명합니다.

흔한 함정

  • 플랫폼 문자 집합. "text".getBytes()와 new String(bytes)는 기본 문자 집합을 씁니다. Java 17 이하의 Windows에서는 대개 Windows-1252(한국어 Windows에서는 MS949)였고, 이 때문에 비 ASCII 텍스트가 깨졌습니다. Java 18에서 UTF-8이 기본값이 되었지만(JEP 400), StandardCharsets.UTF_8을 넘기는 데는 아무 비용도 들지 않습니다.
  • 입력에 맞지 않는 디코더. URL-safe 입력을 기본 디코더에 넣으면 Illegal base64 character 5f나 2d가 나옵니다. MIME 입력은 Illegal base64 character a나 d가 나옵니다.
  • encode()와 encodeToString(). encode()는 ASCII 문자로 된 byte[]를 반환합니다. 그 배열을 출력하면 Base64가 아니라 [B@1b6d3586 같은 것이 보입니다.
  • 감싼 스트림을 닫지 않음. close()가 없으면 인코더 출력이 잘립니다.
  • Base64가 데이터를 숨긴다고 생각함. 숨기지 않습니다. 인코딩과 암호화의 차이를 참고하세요.

버전별 참고 사항

  • Java 8: 기본, URL, MIME 변형과 withoutPadding(), wrap()을 갖춘 java.util.Base64 도입.
  • Java 9: 위에서 스트림 복사에 쓴 InputStream.transferTo.
  • Java 11: JDK에서 JAXB가 제거되면서 javax.xml.bind.DatatypeConverter.printBase64Binary도 사라짐.
  • Java 18: UTF-8이 기본 문자 집합이 됨.

Kotlin에서도 위 내용을 모두 쓸 수 있으며, Kotlin 자체의 멀티플랫폼 API도 있습니다. Kotlin에서 Base64 사용하기에서 다룹니다.

자주 묻는 질문

'Illegal base64 character 5f'는 무슨 뜻인가요?

숫자는 문제가 된 문자를 16진수로 나타낸 것입니다. 5f는 _, 2d는 -이므로 URL-safe Base64를 getDecoder()로 디코딩하고 있다는 뜻입니다. 대신 getUrlDecoder()를 사용하세요. a는 줄 바꿈, 20은 공백이므로 getMimeDecoder()를 쓰거나 공백을 제거하세요.

아직도 sun.misc.BASE64Encoder나 javax.xml.bind.DatatypeConverter를 써야 하나요?

아닙니다. sun.misc.BASE64Encoder는 최신 JDK에서 사라진 내부 API이고, javax.xml.bind는 Java 11에서 JDK에서 제거되었습니다. java.util.Base64가 둘 다 대체합니다.

Java 디코더는 패딩 없는 Base64를 받나요?

네. Base64.getDecoder().decode("aGk")는 hi의 바이트를 반환합니다. 디코더는 패딩 누락은 받아들이지만 잘못된 패딩은 거부합니다.

Android에서는 Base64 인코딩을 어떻게 하나요?

API 레벨 26 이상에서는 java.util.Base64를 쓸 수 있습니다. 예전 코드는 android.util.Base64.encodeToString(bytes, Base64.NO_WRAP)을 쓰며, NO_WRAP이 없으면 줄 바꿈이 추가됩니다.

계속 읽기