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;
}
}
2 Comments
Oh!
포스팅덕좀 보네요
도움이 됐다니 다행이네요. 지금은 ICU library 사용하는 방식으로 조금 변경된 상태입니다. 코드를 바로 사용하기에는 완벽하진 않지만 문제 해결의 단초가 되기를 바랍니다.