주요글: 도커 시작하기
반응형
NIO의 Charset 클래스를 이용한 캐릭터셋 처리에 대해서 살펴본다.

Charset 클래스와 캐릭터셋변환

지난 1부에서는 NIO API의 버퍼와 채널에 대해서 살펴보았다. NIO의 채널은 자바 1.3 까지의 입출력스트림(또는 Reader와 Writer)라는 것을 알게 되었을 것이며, 이 채널은 버퍼를 데이터 저장소로 사용한다는 것도 알게 되었을 것이다. 이번 2부에서는 채널을 통해서 읽어온 데이터의 캐릭터셋을 변환할 때 사용되는 Charset과 논블럭킹 데이터 입출력을 위해서 사용되는 Selector에 대해서 살펴보도록 하겠다.

자바는 애초에 나올때부터 유니코드를 지향해왔다. 하지만, 자바가 제 아무리 유니코드를 지향한다해도 모든 곳에서 유니코드가 통용되는 것은 아니다. 예를 들어, 우리가 매일같이 접속하는 인터넷만하더라도 유니코드 보다는 각 페이지에 알맞은 캐릭터셋을 사용한다. 예를 들어, 대부분의 한글 웹사이트에서 사용되는 캐릭터셋은 EUC-KR(또는 KSC5601)이다. 물론, 윈도우즈 2000이나 윈도우즈 XP와 같이 비교적 최근에 나온 운영체제들은 유니코드를 지원하고 있긴 하다. 하지만, 실제로 사용되는 캐릭터셋은 대부분 그 나라에 알맞은 것들이다.

자바 1.3의 경우 한글로 된 텍스트 파일을 읽어올 때는 InputStreamReader를 간접적으로 사용했었다. InputStreamReader는 바이트 배열을 특정 캐릭터셋에 따라서 알맞게 유니코드로 변환해주기 때문이다. 예를 들어, 한글 윈도우의 MS949 캐릭터셋(EUC_KR과 거의 유사하며 윈도우즈 한글 코드를 처리할 때 사용된다)을 사용하여 작성된 문서를 InputStreamReader를 사용하여 읽어올 경우, InputStreamReader는 MS949 캐릭터셋으로 구성된 바이트 배열을 유니코드로 알맞게 변환해준다. 반대의 경우, 즉 OutputStreamWriter의 경우는 반대의 과정인 유니코드를 해당 캐릭터셋에 알맞은 바이트 배열로 변환해주는 처리를 해준다.

NIO API 역시 IO API와 마찬가지로 캐릭터셋의 변환 처리를 할 수 있는 방법을 제공하고 있는데, 그것이 바로 Charset 클래스이다. Charset 클래스를 포함해서 캐릭터셋 변환과 관련된 클래스들은 java.nio.charset 패키지에 정의되어 있으며, 주요 클래스는 다음과 같다.

  • Charset - 캐릭터셋을 나타내는 클래스
  • CharsetEncoder - 캐릭터를 바이트로 변환해주는 인코더
  • CharsetDecoder - 바이트 데이터로부터 캐릭터를 생성해주는 디코더
Charset 클래스

Charset 클래스는 캐릭터셋 자체를 나타내며, 이 클래스의 인스턴스가 나타내는 캐릭터셋과 유니코드 사이의 변환을 처리해주는 클래스이다. Charset 클래스의 인스턴스는 생성자를 통해서 생성하지 않고 static으로 제공되는 forName() 메소드를 사용해서 생성한다. 예를 들어, 유니코드와 아스키코드 사이의 인코딩/디코딩을 하는 Charset 인스턴스는 다음과 같이 생성할 수 있다.

    Charset cset = Charset.forName("US-ASCII");

위에서 cset은 유니코드와 아스키코드(US-ASCII) 사이의 인코딩/디코딩을 처리해주는 Charset 인스턴스가 된다. JDK1.4는 다음에 표시한 8개의 캐릭터셋을 기본적으로 지원하고 있다.

    ISO-8859-1, ISO-8859-15, US-ASCII
    UTF-16, UTF-16BE, UTF-16LE, UTF-8
    windows-1252

하지만, 위에 없는 캐릭터셋 이외에 EUC-KR이나 EUC-JP와 같은 캐릭터셋에 해당하는 Charset 인스턴서의 지원여부는 사용하는 자바 가상 머신에 따라 다르다. 현재 썬에서 제공하는 가상 머신은 위의 8가지 캐릭터셋에 대한 Charset 인스턴스만을 제공하고 있다. 이 얘기는 썬에서 제공한 JDK로는 EUC-KR 캐릭터셋을 사용하여 표현한 한글문자열을 Charset 클래스를 사용하여 인코딩하거나 디코딩 할 수 없다는 것을 의미한다. 따라서, 앞의 8가지 기본 캐릭터셋으로 표현할 수 없는 글자들에 대해서는 다른 방법으로 인코딩/디코딩 처리를 해야만 한다. 이에 대해서는 뒤에서 설명하기로 하겠다. (앞으로 출시될 1.4.1 버전에서는 EUC-KR을 거의 모든 캐릭터셋에 대해서 Charset을 지원할 예정이다. 따라서 1.4.1 버전을 사용할 경우에는 EUC-KR 캐릭터셋을 처리하기 위해서 별도의 방법을 사용할 필요가 없다.)

Charset 클래스는 유니코드와 지정된 캐릭터셋 사이에 변환을 할 수 있도록 encode() 메소드와 decode() 메소드를 제공하고 있다. 다음은 encode() 메소드를 사용하여 자바의 문자열을 지정한 캐릭터셋으로 인코딩하여 파일로 저장하는 예제 코드이다.

    import java.nio.charset.*;
    import java.nio.channels.*;
    import java.nio.*;
    import java.io.*;
    
    public class CharsetTest {
       public static void main(String[] args) {
          if (args.length != 2) {
             System.out.println("[사용] java CharsetTest 캐릭터셋 문장");
             System.exit(0);
          }
          
          FileChannel channel = null;
          
          try {
             Charset charset = Charset.forName(args[0]);
             ByteBuffer buff = charset.encode(args[1]);             
             FileOutputStream out = new FileOutputStream("temp.tmp");
             channel = out.getChannel();
             channel.write(buff);
          } catch(IllegalCharsetNameException ex) {
             System.out.println("잘못된 캐릭터셋 이름: " + args[0]);
          } catch(UnsupportedCharsetException ex) {
             System.out.println("지원하지 않는 캐릭터셋: " + args[0]);
          } catch(IOException ex) {
             System.out.println("입출력 예외: " + ex.getMessage());
          } finally {
             if (channel != null) try { channel.close(); } catch(IOException ex) {}
          }
       }
    }

위 프로그램은 매우 간단하지만, 위 프로그램을 통해서 유니코드를 앞에서 언급한 8가지 캐릭터셋에 알맞은 바이트배열로 변경하는 방법을 알 수 있을 것이다. 예를 들어, CharsetTest를 실행해보자.

    d:\test>java CharsetTest UTF-16 한글과Alphabet의조합

이때 생성되는 temp.tmp 파일은 30바이트를 차지하게 된다. 30바이트가 생성되는 이유는 "한글과Alphabet의조합"은 14글자이고 UTF-16(유니코드)에서 한글자는 2바이트를 차지하며, 그리고 추가적으로 2바이트가 어떤 순서로 구성되는지를 나타내기 위해 2바이트가 추가되기 때문이다. 위와 같이 실행한 결과 화면을 도스창의 type 명령어를 사용하여 보면 다음과 같이 출력된다.

    D:\test>type temp.tmp
    ?? ? A l p h a b e t???

이와 같이 출력되는 이유는 도스창의 type 명령어가 유니코드를 지원하지 않기 때문이다. (UTF-16은 한글자가 2바이트를 차지하기 때문에 알파벳도 2바이트를 사용하여 저장되는 것을 알 수 있다.) 유니코드를 지원하는 윈2000이나 윈XP의 메모장에서 temp.tmp 파일을 열어보면 글자가 깨지지 않고 올바르게 보일 것이다.

특정 캐릭터셋의 바이트 배열을 다시 유니코드로 디코딩하는 과정도 인코딩만큼이나 간단하다. 예를 들어, CharsetTest 클래스를 사용하여 인코딩한 바이트 배열을 다시 유니코드로 변환하여 화면에 출력해주는 프로그램은 다음과 같다.

    import java.nio.charset.*;
    import java.nio.channels.*;
    import java.nio.*;
    import java.io.*;
    
    public class CharsetTest2 {
       public static void main(String[] args) {
          if (args.length != 2) {
             System.out.println("[사용] java CharsetTest 캐릭터셋 파일명");
             System.exit(0);
          }
          
          FileChannel channel = null;
          
          try {
             Charset charset = Charset.forName(args[0]);
             ByteBuffer buff = ByteBuffer.allocate(32);
             
             FileInputStream in = new FileInputStream(args[1]);
             channel = in.getChannel();
             channel.read(buff);
             buff.flip();
             CharBuffer charBuffer = charset.decode(buff);
             System.out.println(charBuffer.toString());
          } catch(IllegalCharsetNameException ex) {
             System.out.println("잘못된 캐릭터셋 이름: " + args[0]);
          } catch(UnsupportedCharsetException ex) {
             System.out.println("지원하지 않는 캐릭터셋: " + args[0]);
          } catch(IOException ex) {
             System.out.println("입출력 예외: " + ex.getMessage());
          } finally {
             if (channel != null) try { channel.close(); } catch(IOException ex) {}
          }
       }
    }

CharsetTest2 클래스는 앞에서 작성한 CharsetTest 클래스와는 정반대로 파일로부터 byte 데이터를 읽어와 ByteBuffer에 저장한 후, 그 ByteBuffer에 있는 데이터를 지정한 캐릭터셋으로 디코딩하여 CharBuffer에 저장한다. 디코딩 작업은 위 코드에서 보다시피 Charset 클래스의 decode() 메소드를 사용하여 수행한다. 디코딩할 바이트 데이터가 저장된 ByteBuffer를 파라미터로 전달하면, 알맞게 유니코드로 디코딩된다.

CharsetEncoder와 CharsetDecoder

앞에서 살펴본 Charset 클래스의 encode() 메소드와 decode() 메소드는 내부적으로는 CharsetEncoder와 CharsetDecoder 클래스를 사용한다. CharsetEncoder와 CharsetDecoder는 이름에서 알 수 있듯이 각각 인코딩처리와 디코딩처리를 해 주는데 이 둘은 Charset 클래스의 newEncoder() 메소드와 newDecoder() 메소드를 사용하여 구할 수 있다. 예를 들어, CharsetEncoder를 사용하여 인코딩 처리를 하려면 다음과 같이 하면 된다.

    Charset charset = Charset.forName(charsetName);
    CharsetEncoder encoder = charset.newEncoder();
    ByteBuffer byteBuff = encoder.encode(charBuff);

CharsetEncoder 클래스는 위 코드에서 보다시피 encode() 메소드를 제공한다. 앞서 살펴봤던 Charset.encode() 메소드도 내부적으로 CharsetEncoder의 encode() 메소드를 사용한다. 실제로 Charset.encode() 메소드는 다음과 같다.

    cs.newEncoder()
      .onMalformedInput(CodingErrorAction.REPLACE)
      .onUnmappableCharacter(CodingErrorAction.REPLACE)
      .encode(bb); 

여기서 onMalformedInput() 메소드와 onUnmappableCharacter() 메소드는 모두 CharsetEncoder 자신을 리턴하는데, 이 두 메소드에 대해서는 뒤에서 설명하도록 하겠다.

CharsetDecoder도 CharsetEncoder를 구하는 방법과 비슷하게 Charset 클래스의 newDecoder() 메소드를 사용하여 구할 수 있다. 즉, 다음과 같이 CharsetDecoder를 구하면 된다.

    Charset charset = Charset.forName(charsetName);
    CharsetDecoder decoder = charset.newDecoder();
    CharBuffer charBuff = decoder.decode(byteBuff);

CharsetDecoder 클래스는 decode() 메소드는 사용하는데, Charset.decode() 메소드는 Charset.encode() 메소드와 마찬가지로 CharsetDecoder.decode() 메소드를 내부적으로 사용한다. 실제 Charset.decode() 메소드는 다음과 동일하다.

    cs.newDecoder()
      .onMalformedInput(CodingErrorAction.REPLACE)
      .onUnmappableCharacter(CodingErrorAction.REPLACE)
      .decode(bb); 

캐릭터 변환 처리 방법 지정하기

바이트 데이터에 해당하는 유니코드가 없는 경우에는 예외를 발생시키거나 또는 처리하지 않고 넘어간다던가 하는 등의 별도 처리가 필요할 것이다. 이처럼 예외나 에러 상황이 발생할 때 어떻게 처리할지의 여부를 지정해주는 메소드가 있는데, 그 메소드가 바로 CharsetEncoder 클래스와 CharsetDecoder 클래스는 모두 onMalformedInput() 메소드와 onUnmappableCharacter() 메소드이다.

onMalformedInput() 메소드는 잘못된 데이터를 만났을 때 어떻게 처리할지를 지정한다. 예를 들어, CharsetEncoder.encode() 메소드에 전달한 문자열중의 일부 글자가 유니코드가 아니거나 또는 반대로 CharsetDecoder.decode() 메소드에 전달한 바이트 데이터가 잘못된 경우 onMalformedInput() 메소드에서 지정한 방식에 따라서 에러 처리를 하게 된다. 예를 들어, 잘못된 데이터를 만났을 때 CharacterCodingException(또는 유니코드가 아닌 경우 MalformedInputException)을 발생시키고자 한다면 다음과 같이 onMalformedInput() 메소드를 호출하면 된다.

    csEncoder = cs.newEncoder();
    csEncoder.onMalformedInput(CodingErrorAction.REPORT);

onMalformedInput() 메소드에는 CodingErrorAction 클래스에 정의되어 있는 상수값이 파라미터로 전달되는 데, CodingErrorAction 클래스에 정의되어 있는 상수는 다음과 같이 세 가지가 존재한다.

  • CodingErrorAction.IGNORE : 에러를 발생시킨 글자(또는 바이트)를 무시하고 다음 글자(또는 바이트)를 인코딩(디코딩)한다.
  • CodingErrorAction.REPLACE : 에러를 발생시킨 글자(또는 바이트) 대신에 지정한 데이터를 삽입하고 인코딩(디코딩)작업을 계속 진행한다.
  • CodingErrorAction.REPORT : 인코딩/디코딩 작업을 중단하고 CharacterCodingException(또는 CharacterCodingException을 상속받은 하위 클래스) 예외를 발생시킨다.
onUnmappableCharacter() 메소드도 onMalformedInput() 메소드와 마찬가지로 CodingErrorAction 클래스에 정의된 상수인 IGNORE, REPLACE, REPORT 중 하나의 값을 파라미터로 전달받는다. onUnmappableCharacter() 메소드는 인코딩/디코딩 작업시에 변환할 수 없는 글자를 만났을 때 어떻게 처리할지를 나타낸다. 예를 들어, A 캐릭터셋에는 존재하지만 B 캐릭터셋에는 존재하지 않는 글자를 인코딩하거나 디코딩하려 할 때에는 변환을 할 수 없을 것이며, 이런 경우 onUnmappableCharacter() 메소드를 사용하여 onMalformedInput()과 마찬가지로 그냥 무시할지 아니면 다른 글자로 변환할지 아니면 예외를 발생할지의 여부를 지정할 수 있다.

onMalformedInput() 메소드와 onUnmappableCharacter() 메소드에 CodingErrorAction.REPLACE를 지정하면 인코딩/디코딩 작업을 할 수 없는 글자나 문자에 대해서는 지정한 데이터로 변환을 한다고 했었는데, 이때 변환될 데이터는 replaceWith() 메소드를 사용하여 지정할 수 있다. CharsetEncoder와 CharsetDecoder는 각각 다음과 같이 repalceWith() 메소드를 정의하고 있다.

  • CharsetEncoder: replaceWith(byte[] newReplacement)
  • CharsetDecoder: replaceWith(String newReplacement)
replaceWith() 메소드에 전달할 수 있는 값에는 몇가지 제약사항이 존재한다. CharsetEncoder.replaceWith() 메소드에 전달되는 byte 배열의 경우 길이가 0보다 커야 하고 maxBytesPerChar()가 리턴하는 값보다 길어서는 안 되며, 인코딩할 캐릭터셋에 존재하는 바이트 배열이어야 하고 유니코드로 디코드 할 수 있어야만 한다. CharsetDecoder.replaceWith() 메소드에 전달되는 String은 null이 아니어야 하고 길이가 0보다 길어야 한다.

CoderResult를 사용하여 인코딩/디코딩 결과 처리하기

CharsetEncoder 클래스와 CharsetDecoder 클래스는 앞에서 살펴본 encode()/decode() 메소드 뿐만 아니라, 다음과 같이 인코딩/디코딩을 할 수 있는 메소드를 추가로 제공하고 있다.

  • CharsetEncoder: CoderResult encode(CharBuffer in, ByteBuffer out, boolean endOfInput)
  • CharsetDecoder: CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)
두 메소드는 모두 CoderResult를 리턴하는데, CoderResult는 인코딩/디코딩 결과를 저장하고 있다. 위의 encode()/decode() 메소드는 앞에서 살펴봤던 encode(CharBuffer in)/decode(ByteBuffer in) 메소드와 달리 캐릭터변환 과정에서 캐릭터 매핑 에러가 있거나 입력이 잘못된 경우 예외를 발생시키지 않는다. 대신 에러가 발생했다는 사실을 리턴하는 CoderResult 객체에 표시한다. CoderResult는 다음과 같은 메소드를 제공하고 있으며, 이들 메소드를 사용하여 처리가 올바르게 되었는지 여부를 알려준다.

메소드 설명
isError() 처리 과정에서 에러가 발생한 경우 true를 리턴한다.
isMalformed() 잘못된 입력(Malformed Input) 데이터가 있을 경우 true를 리턴한다.
isUnmappable() 매핑할 수 없는 데이터를 입력한 경우 true를 리턴한다.
isOverflow() 오버플로우가 발생한 경우 true를 리턴한다.
isUnderflow() 언더플로우가 발생한 경우 true를 리턴한다.
throwException() 인코딩/디코딩 처리 결과에 알맞은 예외를 발생시킨다. 발생하는 예외 종류는 다음과 같다.
  • MalformedInputException
  • UnmappableCharacterException
  • CharacterCodingException
  • BufferOverflowException
  • BufferUnderflowException

위 코드에서 오버플로우와 언더플로우는 에러라기 보다는 계속해서 인코딩/디코딩 작업이 필요하다는 것을 의미한다. CharsetEncoder의 encode(CharBufff, ByteBuffer, boolean) 메소드에서 오버플로우와 언더플로우 그리고 잘못된 입력, 매핑불가능은 다음과 같은 의미를 나타낸다.

  • CoderResult.UNDERFLOW - 입력 버퍼(CharBuff)에서 최대한 많은 양의 데이터를 인코딩했음을 나타낸다. 만약 입력 버퍼에 글자가 남아 있지 않고 입력 데이터가 더 이상 존재하지 않는다면 인코딩 처리가 완료된다. 그렇지 않고 입력 데이터가 불충하다면 입력 데이터를 추가적으로 받아서 인코딩 처리를 해야 한다는 것을 나타내기도 한다.
  • CoderResult.OVERFLOW - 출력 버퍼가 다 찼음을 의미한다. 따라서 다차있지 않은 출력 버퍼를 사용하여 다시 한번 인코딩 처리를 해 주어야 한다.
  • 잘못된 입력(malfored-input) - 잘못된 입력 데이터가 있음을 나타낸다. 버퍼의 현재 위치(position)는 잘못된 글자에 위치한다. 단, onMalformedInput() 메소드에 CodingErrorAction.REPORT를 지정한 경우에만 동작한다.
  • 매핑할 수 없는 글자 - 지정된 캐릭터셋으로 인코딩할 수 없는 글자가 있음을 나타낸다. 버퍼의 현재 위치는 매핑할 수 없는 글자에 위치한다. 단, onUnmappableCharacter() 메소드에 CodingErrorAction.REPORT를 지정한 경우에만 동작한다.
이 중, 오버플로우와 언더플로우는 버퍼를 사용할 때 반드시 필요한 정보중의 하나이다. 이 두 정보를 어떻게 사용할 수 있는 지에 대해서는 뒤에서 살펴볼 것이다.

캐릭터변환시 주의 사항

지금까지 살펴본 Charset, CharsetEncoder 그리고 CharsetDecoder는 그 동안 개발자들이 코드 변환을 위해서 사용했던 방식을 대신 처리해주는 참(!) 좋은 기능을 제공해주는 클래스들이다. 하지만, 이들 클래스를 사용할 때에는 주의해야 할 점이 두가지가 있다. 그것은 바로 다음과 같다.

  • 지원하지 않는 캐릭터셋의 처리
  • 긴 길이를 가진 데이터의 인코딩/디코딩 처리
이글에서는 마지막으로 위의 두가지 문제를 어떤 식으로 처리할 수 있는 지 함께 살펴보도록 하겠다.

위의 두 가지 문제중 첫번째 문제는 NIO API의 치명적인 약점이라 할 수 있다. 당장 한글만 하더라도, 윈도우즈에서 사용되는 캐릭터셋은 MS949(EUC-KR과 호환되는 캐릭터셋이다)인데, NIO의 Charset은 MS949 또는 EUC-KR을 지원하지 않는다. 따라서 개발자가 Charset 클래스를 사용하여 인코딩/디코딩 작업을 처리할 수 없다는 문제가 발생하게 된다. 물론, 한국 사람이 JVM을 새로 만든다면 앞에서 언급했던 기본 8개의 캐릭터셋 이외에 EUC-KR도 지원하도록 구현하겠지만 Sun에서 배포하는 JVM은 지원하지 않고 있다.

따라서, NIO가 지원하지 않는 캐릭터셋에 대해서 버퍼 기능을 활용하기 위해서는 별도의 방법을 생각해내야 한다. 데이터를 저장할 때 사용할 수 있는 방법은 String.getBytes() 메소드를 사용하여 원하는 캐릭터셋의 바이트 배열로 변환한 후 ByteBuffer를 사용하는 것이다. 무식한 방법이라고 생각할 수도 있지만, 기본 캐릭터셋 이외에 다른 캐릭터셋을 사용할 경우 NIO API를 사용하는 가장 좋은 방법이라 할 수 있다. 예를 들면 다음과 같이 사용하면 된다.

    byte[] temp = someString.getBytes("EUC-KR");
    ByteBuffer buff = new ByteBuffer(32);
    buff.put(temp, 0, temp.length);
    
    // 버퍼 사용
    ...
    buff.flip();
    channel.write(buff);
    

조금 무식해 보이긴 하지만 InputStreamReader나 OutputStreamWriter가 사용하는 방식도 궁극적으로 위와 동일하다. 위와 같이 해당 캐릭터셋의 바이트 배열을 구한 후 채널에 출력할 때에는 배열의 길이에 신경을 써 주어야 한다. 예를 들어, ByteBuffer의 크기를 5로 잡아주었는데 getBytes()로 구한 byte[] 배열의 크기가 10일 경우에는 다음과 같이 루프를 사용하여 알맞게 데이터를 출력해주어야 한다.

    byte[] srcData = args[1].getBytes();
    ByteBuffer buff = ByteBuffer.allocate(5);
    
    FileOutputStream out = new FileOutputStream(args[0]);
    channel = out.getChannel();
    
    int count = 0; // srcData를 처리한 바이트 수
    int len = 0;
    while(count < srcData.length) {
        len = buff.remaining();
        if (srcData.length - count < len) {
            len = srcData.length - count;
        }
        buff.put(srcData, count, len);
        buff.flip();
        channel.write(buff);
        buff.clear();
        count += len;
    }

위 코드는 ByteBuffer의 여유 공간에 알맞게 바이트 배열의 데이터를 ByteBuffer에 나누어서 삽입해주고 있다. 이와 같이 나누어서 해 주어야 하는 이유는 버퍼의 여유공간에 알맞게 데이터를 삽입하지 않을 경우 BufferOverflowException이 발생하기 때문이다.

CharsetEncoder를 사용하여 데이터를 처리할 때에도 버퍼의 길이에 신경써야 하기는 마찬가지다. 예를 들어, CharBuffer에 20개의 char가 저장되어 있는데, 이 20개의 캐릭터를 길이가 5인 ByteBuffer를 사용하여 파일에 저장한다고 생각해보자. 이 경우 어떤 캐릭터셋을 사용한다 할지라도 20개의 글자를 모두 인코딩처리하기 위해서는 ByteBuffer에 여러 차례에 걸쳐서 저장해야 할 것이다. 이때의 처리 방식은 byte 배열에 저장된 데이터를 분할해서 출력할 때와 비슷하다. 다음은 실제로 CharBuffer에 저장된 데이터를 인코딩하여 ByteBuffer에 저장한 후 파일에 출력해주는 코드를 보여주고 있다.

    CharBuffer charBuff = CharBuffer.allocate(64);
    charBuff.put(args[1]);
    charBuff.flip();
    
    ByteBuffer buff = ByteBuffer.allocate(5);
    
    FileOutputStream out = new FileOutputStream(args[0]);
    channel = out.getChannel();
    
    CoderResult result = null;
    while(true) {
        result = encoder.encode(charBuff, buff, true);
        buff.flip();
        channel.write(buff);
        buff.clear();
        
        if (result == CoderResult.UNDERFLOW) {
            if (charBuff.position() == charBuff.limit())
                break;
        }
    }

채널을 통해서 데이터를 읽어올 때는 약간 상황이 다르다. Charset이 지원하는 8개의 캐릭터셋에 대해서는 CharsetDecoder를 사용하여 손쉽게 디코딩 작업을 처리할 수 있지만, Charset이 지원하지 않는 캐릭터셋에 대해서는 인코딩할때와 마찬가지로 Charset을 사용하지 않은 별도의 방법을 처리해야 한다. Charset이 지원하지 않는 캐릭터셋을 읽어오는 가장 쉬운 방법은 그냥 속 편하게 java.io.Reader를 사용하는 것이다. 사실, Selector를 사용하여 논블럭킹 I/O 기능을 사용하지 않는 이상 EUC-KR과 같은 캐릭터셋의 데이터를 읽어올 때는 Reader를 사용하는 것이 가장 단순하게 일을 처리할 수 있는 방법이다.

하지만, 논블럭킹 I/O 기능을 사용해야 하는 경우와 같이 반드시 NIO API를 사용해야 하는 경우에는 별도의 방법을 사용해야 한다. 필자가 추천할 만한 방법은 일단 데이터를 ByteBuffer에 저장한 후 필요에 따라 일부 데이터를 알맞게 형변환하라는 것이다. 예를 들어, 웹 서버를 개발한다고 해 보자. 웹브라우저는 웹서버에 다음과 비슷한 데이터를 전송한다.

    POST / HTTP/1.1
    Accept: image/gif, image/x-xbitmap, image/jpeg
    Accept-Language: ko
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip, deflate
    User-Agent: Mozilla/4.0
    Host: localhost
    Content-Length: 50
    Connection: Keep-Alive
    Cache-Control: no-cache
    
    param1=%C7%D1%B1%DB%C0%BA¶m2=%B0%FA%BF%AC+abcd

위 데이터에서 굵게 표시한 부분은 사용자가 폼을 통해서 입력한 파라미터 데이터이다. 웹 서버는 모든 데이터에 대해서 캐릭터셋 변환을 할 필요는 없으며, 파라미터에 해당하는 부분만 알맞게 변환해주면 되는 것이다. 따라서 NIO API를 사용하여 바이트 버퍼에 데이터를 읽어오데, 헤더에 해당하는 CharsetDecoder를 사용하여 읽어오고, 파라미터에 해당하는 부분은 별도로 데이터를 처리하는 방식을 채택하면 된다.

CharsetDecoder를 사용하여 데이터를 읽어올 경우 CharsetEncoder에서 버퍼의 길이에 신경을 써 주었던 것과 마찬가지로 버퍼의 길이에 신경을 써 주어야 한다. 앞에서 작성해보았던 CharsetTest2.java는 긴 길이의 데이터를 처리하지 못한다. 왜냐면 버퍼의 크기가 작기 때문이다. 긴 길이의 데이터를 읽어오기 위해서 버퍼의 길이를 무작정 늘리는 건 불필요하게 낭비되는 메모리의 양만 늘릴 수 있으므로, 적당한 크기의 버퍼를 사용하면서 동시에 긴 길이의 데이터를 CharsetDecoder를 사용하여 처리할 수 있어야 한다. 그러기 위해서는 CharsetEncoder와 마찬가지로 CoderResult를 사용해야 한다.

다음 코드는 CoderResult를 사용하여 긴 길이의 데이터를 디코딩하는 방법을 보여주고 있다.

    int len = -1;
    CoderResult result = null;
    boolean endOfInput = false;
    
    while ( true ) {
        len = channel.read(byteBuff);
        byteBuff.flip();
        if (len == -1) {
            endOfInput = true;
        }
        
        result = decoder.decode(byteBuff, charBuff, endOfInput);
        if (result == CoderResult.UNDERFLOW) {
            if (!endOfInput) byteBuff.compact();
        }
        
        if (result == CoderResult.OVERFLOW) {
            System.out.print(charBuff.flip().toString());
            charBuff.clear();
            byteBuff.compact();
        }
        
        if (endOfInput) {
            System.out.print(charBuff.flip().toString());
            break;
        }
    }

위 코드에서 보다시피, CoderResult.UNDERFLOW와 CoderResult.OVERFLOW를 적절히 사용하여 ByteBuffer와 CharBuffer를 알맞게 처리해주어야 한다. (인코딩하는 경우와 비슷한 것을 알 수 있다.) CharsetDecoder의 decode(ByteBuffer, CharBuffer, Boolean) 메소드를 사용할 때 또 하나 주의할 점은 이 메소드의 실행한 후 CharBuffer를 사용하려면 flip() 메소드를 사용하는 것이 좋다는 점이다. 이 점만 주의한다면 큰 문제없이 사용할 수 있을 것이다.

결론

이번 2부에서는 Charset 클래스를 이용하여 인코딩/디코딩을 처리하는 방법과 인코딩/디코딩 작업시 주의해야 할 점에 대해서 살펴보았다. 현재 자바 1.4의 Charset 클래스가 지원하는 캐릭터셋이 기본적으로 8개로 제한되어 있긴 하지만 Charset 클래스는 분명히 유용하게 사용할 수 있는 클래스 중의 하나이다. 그리고 앞으로 출시될 1.4.1 버전에서는 EUC-KR을 비록한 다양한 캐릭터셋에 대한 지원기능이 NIO API에 추가될 것이므로 반드시 알아두어야 하는 클래스 중의 하나이다.

다음 3부에서는 NIO API가 제공하는 기능중에서 '꽃'이라고 불릴만한 Selector에 대해서 살펴볼 것이다. 이 Selector는 이 글의 처음에 언급했던 논블럭킹 I/O를 구현하는 데 필수적인 클래스이다. 실제로 어플리케이션의, 특히 웹서버나 채팅 서버와 같은 서버 애플리케이션의 전체적인 처리량을 높이는 데 있어서 필수적으로 필요한 것이 바로 Selector이므로, 자바 고급 개발자로 거듭나기 위해서는 NIO 패키지의 다른 요소들과 더불어 이 Selector를 마음대로 사용할 수 있어야만 할 것이다.

관련링크:

+ Recent posts