[Java] Java: String의 인코딩과 케릭터 셋

Published: by Creative Commons Licence

참고 문서

개요

자바 String의 인코딩과 케릭터 셋에 대한 내용 정리.

이 문서에서 인코딩 규칙은 '인코딩'으로, 문자와 숫자 간 변환 과정은 '부호화'와 '복호화'로 표기한다.

시스템 기본 인코딩 확인 Platform's Default Encoding

System.getProperty("file.encoding");

'한글'을 UTF-8로 인코딩 했을때의 16진수 표현값:

String string = "한글";
byte[] bytes = string.getBytes("UTF-8");
for (byte b : bytes) {
    System.out.print(String.format("0x%02X ", b));
}

인코딩 찾기

인코딩 방식이 뭐였는지 간단하게 확인하는 방법은 사실상 없다.

String의 생성자:

String(byte[] bytes): 바이트 배열을 플랫폼의 기본 charset으로 복호화하여 새로운 String을 생성
String(byte[] bytes, Charset charset): 바이트 배열을 주어진 charset으로 복호화하여 새로운 String을 생성

다만, 생성자를 이용하여, 특정 문자의 바이트가 어떤 케릭터 셋의 코드값과 일치하는지 일일이 확인하는 방법이 있긴 하다:

byte[] bs = str.getBytes();
byte[] bs2 = str.getBytes("utf-8");

// 복호화를 시도했을 때 글자가 깨지지 않고 온전한 것을 찾는다.
System.out.println(new String(bs)); // UTF-8. 시스템 기본 케릭터 셋으로 복호화됨
System.out.println(new String(bs2, "utf-8")); // UTF-8
System.out.println(new String(bs2, "euc-kr")); // �������ㅼ���

이 방법은 모든 사용 가능한 모든 문자를 대입해보지 않는한 정확한 케릭터 셋은 찾기 힘들다는 한계가 있다.

인코딩 변환하기

String은 인코딩이 없다

자바에서 String 객체는 내부적으로 문자를 UTF-16으로 표현한다.

인터넷에서 String의 인코딩 변환 방법을 검색하면 이런 결과들이 나오곤 하는데:

String originalText = "한글123";
byte[] bytes = originalText.getBytes(StandardCharsets.UTF_8);
String convertedText = new String(bytes, Charset.forName("EUC-KR"));

log.debug("convertedText: {}", convertedText); // 12 ab 媛���

결과부터 말하면 엉터리다. 얼핏 보기엔 UTF-8에서 EUC-KR 인코딩으로 변경하는 것처럼 보이지만 실상은 그렇지 않다.

originalText는 플랫폼이나 실제 입력된 값을 자바가 알아서 관리하는 String 값이다. 내부적으로는 UTF-16으로 다뤄지긴 하지만 개발자가 신경쓰지 않아도 된다.

originalText.getBytes(StandardCharsets.UTF_8)는 문자열을 UTF-8 바이트 배열로 부호화하고 그 배열을 반환한다.

new String(bytes, Charset.forName("EUC-KR"))는 바이트 배열을 EUC-KR 인코딩으로 복호화하여 String 값으로 만든다.

이 과정에서 UTF-8과 EUC-KR에서 동일한 규칙이 적용되는 ASCII 문자는 아무 문제가 없지만, 한글을 비롯한 유니코드 문자는 두 인코딩 방식 사이의 규칙이 다르므로 위의 예시처럼 , , 혹은 그 유명한 占쏙옙 같은 깨진 문자열을 얻게 된다.

그럼 어떻게 해야 하는가?

앞서 말했듯 String 객체는 UTF-16으로 내부 데이터를 관리한다. string.getBytes() 같은 메서드가 실행되면 문자를 분석하여 유니코드 코드 포인트를 식별한 뒤, 지정된(혹은 시스템 기본 설정의) 인코딩 방식을 적용한다.

이 과정은 개발자가 모르게 알아서 수행된다. 따라서 String 타입의 문자열은 인코딩이 없다고 봐도 무방하며, String 데이터 자체의 인코딩을 바꾸는 방법은 없다고 봐야 한다. 오직 이 값을 전송하거나 다른 데이터 타입으로 바꿀 때만 유효하다.

final String text = "12 ab 가나";

byte[] bytes1 = text.getBytes(StandardCharsets.UTF_8);
byte[] bytes2 = text.getBytes(Charset.forName("EUC-KR"));

// UTF-8와 EUC-KR의 바이트 값 차이
log.debug("{}", bytes1); // [49, 50, 32, 97, 98, 32, -22, -80, -128, -21, -126, -104]
log.debug("{}", bytes2); // [49, 50, 32, 97, 98, 32, -80, -95, -77, -86]

// ASCII 문자인 숫자와 알파벳, 공백은 동일하지만 한글은 다르다

이렇게 바이트 배열로 변환한 시점이 인코딩이 적용된 상태니까, 이 값을 그대로 전달하거나 사용하는 게 올바른 방법이라 할 수 있다. 여기서 다시 String으로 바꾸는 것은 헛짓이다.