톰캣 한글 인코딩 문제와 관련된 코드들

Java로 6년째 개발을 하면서 한글 문제는 더 이상 새로운 것이 없겠지 생각을 하지만 매번 알 수 없는 상황에 부딪히게 된다.

EUC-KR 기반의 아파치-톰캣 시스템에서 브라우져 주소창의 한글을 처리하려고 할 때 다음과 같은 문제가 발생했다.

주소창에 ‘http://v.daum.net/search?q=한글’을 직접 입력하는 경우 크게 3가지 경우가 발생했다.

1) ‘한글’이라는 파라미터 값이 ‘EUC-KR’로 URLEncoding 되서 넘어오는 경우
2) ‘한글’이라는 파라미터 값이 ‘UTF-8′로 URLEncoding 되서 넘어오는 경우
3) ‘한글’이 bytes로 변환되서 넘어오는 경우

이와 유사한 문제가 아파치 rewrite module을 사용할 때도 발생했다. 예를 들어 ‘http://v.daum.net/한글’을 ‘http://v.daum.net/search?q=한글’로 재작성하는 경우 ‘한글’이 bytes로 변환된다.

이에 대한 해결방법은 여러가지가 있을 수 있는데, 다음과 같은 아이디어를 기반으로 문제를 해결했다.

1. URLEncoding된 문자열의 헥사값을 bytes로 변환할 수 있다.
2. bytes의 인코딩을 예측할 수 있다.

(구현된 코드는 톰캣 내부의 파라미터 처리 부분과 Daum 검색개발팀의 황재석님의 코드에 도움을 받았습니다.)

‘getParameterSafely(request.getQueryString(), “q”);’와 같이 사용하면 됩니다. 단, 파라미터에 ‘%’ 문자가 포함된 경우 오작동하게 됩니다.

/* public query parsing method */
public static String getParamterSafely(String queryString, String key) {
  if (StringUtils.isEmpty(queryString)) {
    return "";
  }

  try {
    String safeQueryString = urlDecodeSafely(queryString);
    return getParameter(safeQueryString, key);
  } catch (UnsupportedEncodingException e) {
    //ignore
    e.printStackTrace();
  }
  return "";
}

/* URLDecoding 수행 */
private static String urlDecodeSafely(String queryString)
  throws UnsupportedEncodingException {
  if (StringUtils.isEmpty(queryString))
    return "";
  byte[] src = queryString.getBytes("ISO-8859-1");
  byte[] bytes = urlDecode(src);
  String charset = guessCharset(bytes);
  String charset = CharsetDetector.detect(bytes);
  return new String(bytes, charset);
}

/* 헥사코드를 bytes로 변환 */
private static byte[] urlDecode(byte[] data)
  throws UnsupportedEncodingException {
  int ix = 0;
  int ox = 0;
  while (ix < data.length) {
    byte c = data[ix++];
    switch ((char) c) {
      case '+':
        data[ox++] = (byte) ' ';
        break;
      case '%':
        data[ox++] = (byte) ((convertHexDigit(data[ix++]) << 4)
          + convertHexDigit(data[ix++]));
        break;
      default:
        data[ox++] = c;
    }
  }

  return ArrayUtils.subarray(data, 0, ox);
}

/* 헥사코드 변환 */
private static byte convertHexDigit(byte b) {
  if ((b >= '0') && (b <= '9'))
    return (byte) (b - '0');
  if ((b >= 'a') && (b <= 'f'))
    return (byte) (b - 'a' + 10);
  if ((b >= 'A') && (b <= 'F'))
    return (byte) (b - 'A' + 10);
  return 0;
}

/* 재석님의 캐릭터셋 추측하는 메소드 */
private static String guessCharset(byte[] bytes) {
  try {
    CharsetDecoder decoder = Charset.forName("MS949").newDecoder();
    ByteBuffer bb = ByteBuffer.wrap(bytes);
    decoder.decode(bb);
    bb.clear();
    return "MS949";
  } catch (Exception e) {
    return "UTF-8";
  }
}

/* 쿼리 문자열에서 특정 키의 값을 가져온다. */
private static String getParameter(String safeQueryString, String key) {
  String parameters[] = safeQueryString.split("&");
  if (parameters != null) {
    for (String parameter : parameters) {
      if (parameter.startsWith(key + "=")) {
        return parameter.replaceFirst(key + "=", "");
      }
    }
  }
  return "";
}

임동문님의 블로그를 참고해서 캐릭터셋 판별 부분을 보완했습니다.

public class CharsetDetector {
  private static final String DEFAULT_CHARSET = "MS949";
  private static final int MS949_NORMAL = 0;
  private static final int MS949_2BYTE = 1;
  private static final int MS949_KSC_2BYTE = 2;
  private static final int UTF8_NORMAL = 0;
  private static final int UTF8_2BYTE = 1;
  private static final int UTF8_3BYTE = 2;
  private static final int UTF8_4BYTE = 3;

  public static String detect(byte[] bytes) {
    if (isUTF8(bytes))
      return "UTF-8";
    if (isMS949(bytes))
      return "MS949";
    return DEFAULT_CHARSET;
  }

  public static boolean isMS949(byte[] bytes) {
    int status = MS949_NORMAL;
    for (int ch : bytes) {
      if (status < 0)
        return false;

      switch (status) {
        case MS949_NORMAL:
          if (ch < 0x80)
            status = MS949_NORMAL;
          else if (0x81 <= ch && ch <= 0xc5)
            status = MS949_2BYTE;
          else if (0xc5 < ch && ch <= 0xfe)
            status = MS949_KSC_2BYTE;
          else
            status = -1;
          break;
        case MS949_2BYTE:
          if ((0x41 <= ch && ch <= 0x5a) || (0x61 <= ch && ch <= 0x7a)
            || (0x81 <= ch && ch <= 0xfe))
            status = MS949_NORMAL;
          else
            status = -1;
          break;
        case MS949_KSC_2BYTE:
          if (0xa1 <= ch && ch <= 0xfe)
            status = MS949_NORMAL;
          else
            status = -1;
          break;
        default:
          break;
      }
    }
    return true;
  }

  public static boolean isUTF8(byte[] bytes) {
    int status = UTF8_NORMAL;

    for (int ch : bytes) {
      if (status < 0)
        return false;

      switch (status) {
        case UTF8_NORMAL:
          if ((ch & 0x80) == 0)
            status = UTF8_NORMAL;
          else if (((ch & 0xe0) ^ 0xc0) == 0)
            status = UTF8_2BYTE;
          else if (((ch & 0xf0) ^ 0xe0) == 0)
            status = UTF8_3BYTE;
          else if (((ch & 0xf8) ^ 0xf0) == 0)
            status = UTF8_4BYTE;
          else
            status = -1;
          break;
        case UTF8_2BYTE:
          if (((ch & 0xc0) ^ 0x80) == 0)
            status = UTF8_NORMAL;
          else
            status = -1;
          break;
        case UTF8_3BYTE:
          if (((ch & 0xc0) ^ 0x80) == 0)
            status = UTF8_2BYTE;
          else
            status = -1;
          break;
        case UTF8_4BYTE:
          if (((ch & 0xc0) ^ 0x80) == 0)
            status = UTF8_3BYTE;
          else
            status = -1;
          break;
        default:
          break;
      }
    }
    return true;
  }
}
This entry was posted in Java and tagged , , . Bookmark the permalink. Post a comment or leave a trackback: Trackback URL.

2 Comments

  1. 란카
    Posted August 30, 2009 at 2:08 am | Permalink

    Oh!
    포스팅덕좀 보네요

  2. Posted August 30, 2009 at 7:15 pm | Permalink

    도움이 됐다니 다행이네요. 지금은 ICU library 사용하는 방식으로 조금 변경된 상태입니다. 코드를 바로 사용하기에는 완벽하진 않지만 문제 해결의 단초가 되기를 바랍니다.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>