http://kin.naver.com/knowhow/detail.nhn?docId=527939


현재 일하는 업무 상 String의 Code page를 변환해야 하는 작업이 많다.

 

하지만 이에 관한 자료들이 매우 미흡하며 잘못된 지식을 전달하는 블로그나 웹도 많이 보아왔다.

(처음에 그것이 잘못된 것인지도 몰랐지만)

 

그리고 믿고 사용했지만 여전히 깨져버리는 한글을 보며 고민하기도 했다.

 

사실 DB모니터링 툴 개발 업무를 하다보니 Character Set을 직접 변환해야 하는 작업들이 꽤 많았다.

 

Java에선 과연 어떤 형태로 변환작업을 수행할 수 있으면 읽을 수 있을까 고민도 했다.

 

이 글이 조금은 어려울 수도 있지만 천천히 읽어본다면 충분히 이해할 수 있고 명확하게 java의

 

캐릭터 셋에 대해 알 수 있을 것이다.

 

 영문은 대부분의 캐릭터셋이 1바이트기 때문에 변환작업에서 깨질일이 거의 없다고 할 수 있지만

 

한글은 utf-8의 경우 1에서 4바이트까지의 가변형으로 저장되기 때문에 1글자의 바이트 길이가 달라

 

명시적인 변환이 요구된다.

 

여기저기 자료도 많이 찾아봤지만 역시나 테스트 하는 것이 가장 빠르게 이해할 수 있었다.

 

결론 부터 언급하면 String 객체내 바이트 배열이 어떤 캐릭터셋으로 저장될 것이라는 생각이 오해를 불러왔고

 

잘못된 사고를 하게 했다. String이 실제 메모리 상에 어떠한 캐릭터셋의 바이트 배열로 저장되어 있는지

 

사용자들은 고민할 필요도 없이 자바는 잘 되어 있었다.

 

Jdk 1.4를 기준으로 내 머리의 이해를 도와봤다.

 

내 이름 한민호라는 세자를 utf-8로 byte 배열에 저장해보았다.

 

막무가내로 시작했지만 getBytes라는 함수가 어떻게 이용되는지 알 수 있었다.

 

byte [] bytes = new String("한민호").getBytes("utf-8");

 

처음 이 코드를 작성하고 "한민호"라는 객체는 어떤 캐릭터 셋의 바이트 배열로 저장되어 있을까 생각했지만

 

그것을 생각하면서 이미 정상적인 사고를 하기가 어려웠다. 자바에선 String 객체로 생성되었다면 어떠한 종류의

 

캐릭터 셋의 바이트 배열이든 리턴이 가능하기 때문이다.

 

단 한글로 생성했는데 한글을 지원하지 않는 캐릭터셋이라면 리턴한 바이트 배열의 값이 깨지게 된다.

 

 getBytes라는 메소드에 대해 전혀 알지 못했을 땐 String을 생성할 때 지정한 즉 메모리 상에 저장된

 

"한민호"라는 객체의 바이트 배열의 캐릭터 셋을 지정해줘야 되는 줄 알았다.

 

그러나 이 메소드가 내가 상상했던거 보다 훨씬 대단하다는 것을 알게 되었다.

 

 이 메소드는 현재 저장된 String값이 어떠한 캐릭터 셋으로 저장되든 상관없이

 

바이트 배열과 바이트 배열에 맞는 캐릭터 셋으로 생성만 했다면

 

java에서 한글을 지원(영문만 지원하는 캐릭터 셋은 깨지게 된다.) 하는

 

어떠한 캐릭터 셋으로든 변환하여 변환된 바이트 배열로 리턴한다.

 

 여기서 핵심은 정상적인 String 객체의 생성이라는 것이다. 아래서 정상적인 String 객체의 생성에 대해 알아보겠다.

 

 위에서 생성한 바이트 배열을 다시 String 객체로 변환해 보겠다.

 

String name = new String(bytes, "utf-8");

 

이런 식으로 변환이 가능하다. 간혹 String의 두 번째 파라메터로 넣는 캐릭터 셋을 바이트 배열의 캐릭터 셋이 아닌 다른

 

캐릭터셋을 넣어 변환하겠다는 코드를 많이 봤다. 이건 잘못된 것이다. 이 자린 바이트 배열에 저장된 바이트들의 캐릭터 셋을

 

설정하는 곳이다.

 

잘못된 변환의 사용예를 한가지 들어보겠다.

 

잘못된 변환

String convert = new String(message.getBytes("euc-kr"), "utf-8");

 

소스를 보면 message String에 저장된 문자를 getBytes를 이용하여 euc-kr라는 캐릭터셋 바이트 배열로 얻고있다.

 

분명 이것을 작성한 사람은 위 코드에서 new String(스트링배열, "euc-kr")라고 String 개체를 생성했을 것이다.

 

간단히 설명하면 euck-kr라는 캐릭터 셋으로 바이트 배열을 읽어들인 

 

다음 utf-8이라는 새로운 캐릭터셋으로 변환(?)을 시도하겠다는 것이다.

 

이것은 두 번째 파라메터에 대해 잘못된 이해를 하고 있기에 이런 코드가 가능한 것이다.

 

때문에 이렇게 변환을 하게 되면 한글이 저장된 경우 같은 계열(변환가능한)의 캐릭터 셋이 아니라면 100프로 깨지게 된다.

 

이러한 변환은 자바에서 지원을 안하는 사항이다.

 

또 문제는 다시 String 객체로 생성을 한다는 것이다. 이 때 String생성 시 잘못된 캐릭터 셋을 주었기 때문에 깨진

 

바이트 배열로 저장이 되게 된다. 때문에 이러한 경우 getBytes() 메소드를 통해 어떠한 캐릭터 셋으로 읽든

 

읽을 수가 없게 된다.

 

 그렇다면 이제까지의 사항들에 대해 테스트를 통해 명확히 알아보겠다.

 

아래는 테스트에 자주 사용될 byte 배열을 16진수로 보여주는 함수다.

 

public static String BinToHex(byte [] buf) {
  String res = "";
  String token = "";
  for (int ix=0; ix<buf.length; ix++) {
   token = Integer.toHexString(buf[ix]);
//   CommonUtil.println("[" + ix + "] token value : " + token + " len : " + token.length());
   if (token.length() >= 2)
    token = token.substring(token.length()-2);
   else {
    for(int jx=0; jx<2-token.length();jx++)
     token = "0" + token;
   }     
   res += " " + token;
  }
  
  return res.toUpperCase();
 }

 

< 테스트 소스 >

1   String name = new String("한민호");   
2   strs = name.getBytes();   
3   System.out.println("Length : " + strs.length);
4   System.out.println("Hex    : " + BinToHex(strs));
5   System.out.println("Value  : " + new String(strs));
6   System.out.println();   
7   strs = name.getBytes("utf-8");
8   System.out.println("Length : " + strs.length);
9   System.out.println("Hex    : " + BinToHex(strs));
10   System.out.println("Value  : " + new String(strs, "utf-8") );
11   System.out.println();   
12  name = new String(strs, "utf-8");
13   strs = name.getBytes();
14   System.out.println("Length : " + strs.length);
15   System.out.println("Hex    : " + BinToHex(strs));
16   System.out.println("Value  : " + name);   
17   System.out.println();   
18   String convert = new String(name.getBytes("euc-kr"), "utf-8");
19   System.out.println(convert);
20   strs = convert.getBytes();
21   System.out.println("Length : " + strs.length);
22   System.out.println("euc-kr Hex    : " + BinToHex(strs));
23   strs = convert.getBytes("utf-8");
24   System.out.println("Length : " + strs.length);
25   System.out.println("utf-8 Hex    : " + BinToHex(strs)); 
26   System.out.println();
27   System.out.println();

 

테스트 코드를 보며 이 결과들이 어떻게 나올것이라고 예측했는데 그것이 맞아떨어지지 않는다면

 

다시 글을 보면서 이해하면된다.

 

결과는 아래와 같다.

 

< 결과 >

Length : 6
Hex    :  C7 D1 B9 CE C8 A3
Value  : 한민호

Length : 9
Hex    :  ED 95 9C EB AF BC ED 98 B8
Value  : 한민호

Length : 6
Hex    :  C7 D1 B9 CE C8 A3
Value  : 한민호

????
Length : 4
euc-kr Hex    :  3F 3F 3F 3F
Length : 10
utf-8 Hex    :  EF BF BD D1 B9 EF BF BD C8 A3

 

1번 라인을 보면 한민호라는 String객체를 생성하고 있다

 

이 객체를 strs라는 바이트 배열에 getBytes()를 이용해서 받는다. getBytes에 아무 파라메터도 주지 않는다면

 

이것은 디폴트 캐릭터셋으로 바이트 배열이 리턴된다.

 

디폴트 캐릭터셋은 System.getProperty("file.encoding") 메소드를 통해 알수 있다.

 

이것을 utf-8로 저장하는 로직이 7번 라인이다. 단지 getBytes에 utf-8이란 파라메터를 주고 utf-8 캐릭터 셋의

 

바이트 배열을 받아 올 수 있다.

 

그리고 12번 라인은 이 utf-8로 저장된 바이트 배열을 다시 String객체로 파라메터 값으로 "utf-8"을 주고 생성한 것이다.

 

이때 파라메터를 주지 않거나(디폴트 파라메터가 지정됨) 다른 캐릭터셋을 준다면 깨지게 된다.

 

마지막으로 18번 라인은 잘못된 컨버팅 예이다. euc-kr을 utf-8로 변환하겠다는 건데

 

위에서 설명했듯 이러한 변환 때문에 바이트 배열이 깨져서 euc-kr이든 utf-8이든 어떠한 바이트 배열로 읽어오든

 

깨져있는 것을 확인할 수 있다. 이미 깨져서 생성된 String 객체의 바이트 배열은 어떻게든 복구가 불가능 하다.

 

<결론>

 이야기를 종합해보면 String 객체로 생성된 것을 다른 캐릭터 셋의 String 객체로 변환한다는 것은

 

어불성설인 것이다. 이러한 변환은 무지에서 나오는 것이며 String 객체에 이미 깨진 내용은 어떠한 변환이 있더라도

 

정상적인 출력이 불가능하다는 것이다. 캐릭터셋을 포함하여 관리하겠다면 철저하게 바이트 배열을 이용해야 한다.

 

그리고 String에 어떤 캐릭터 셋으로 저장되어 있는지에 대해 논하는 것은 애초부터 잘못된 것이다.