어제 오늘 시간을 많이 잡아먹고 해결한 문제가 있어서 기록으로 남겨볼까 한다.

요즘 내가 진행 중인 웹 프로젝트가 있는데 운영 환경이 자바 버전 5에 제우스를 사용하는 환경이다. 개발 환경도 그에 맞추는 게 맞으므로 자바 버전은 5를 사용하는데 서블릿 컨테이너는 별 문제가 없겠지 생각하고 톰캣(Tomcat) 버전 6을 사용하기로 했다. 원칙적으로 서블릿 버전, JSP 버전도 운영 환경에 맞춰서 선택해야겠지만 요즘엔 아파치 톰캣 사이트에 가보면 6 버전 미만은 창고에 쳐박힌지 오래다. (여기서 창고란 메뉴 항목 중 Archives, 즉 보관소를 말한다)

예전에는 자바 5에서 톰캣 5.5를 많이 사용했지만 이제는 자바 6 이상을 많이 사용하고 톰캣도 6 이상을 많이 사용하므로 자바 5 + 톰캣 6 조합은 사실 일반적이지 않은 조합이었다.

아무튼 이렇게 웹 개발 환경을 구성하고 웹사이트를 띄워보면 매개변수 값의 길이가 한 글자인 경우 request.getParameter가 빈 문자열을 반환하는 문제가 발견되었다. (발견하는 데도 시간이 좀 걸렸다.) 예를 들어 다음과 같이 서버측으로 폼을 넘기면

<form>
<input name="key1" value="1">
<input name="key2" value="12">
</form>

request.getParameter("key1")의 값은 빈 문자열 ""로 나오고 request.getParameter("key2")의 값은 "12"로 정상적으로 나온다.

영 이상한 현상이므로 이건 아무래도 뭔가 환경적인 문제일 것이라는 감이 왔다. 그래서 자바 소스 및 톰캣 소스를 활용해 디버깅을 시작했고 매개변수를 파싱하는 루틴, 특히 HTTP 데이터를 문자열로 디코딩하는 자바 5 런타임의 java.nio.charset.CharsetDecoder.decode(ByteBuffer in) 메서드에 문제가 있음을 알게 됐다. 다음은 그 소스다.

    public final CharBuffer decode(ByteBuffer in)
throws CharacterCodingException {
int n = (int)(in.remaining() * averageCharsPerByte());
CharBuffer out = CharBuffer.allocate(n);

if (n == 0)
return out;

위에서 3행은 문자 당 평균 바이트 수 정보를 활용해 버퍼에 확보할 바이트 수를 구한다. 그런데 우리 프로젝트에서 사용하는 문자셋은 euc-kr인데 이 경우 averageCharsPerByte()는 0.5를 반환한다. 즉 euc-kr은 2바이트가 모여 한 글자를 만들기 때문이다. 그리고 in.remaining()은 파싱해야할 매개변수의 바이트 수를 나타내는데 한 글자인 경우 n = 1 x 0.5, 결국 0이 된다. 따라서 위에서 강조 표시한 6행으로 인해 매개변수 파싱이 중단되는 어이 없는 일이 일어난다.

자바 6에서는 이 문제가 수정되었는데 위 소스에서 나머지는 똑같고 6행이 다음과 같이 바뀌었다.

        if ((n == 0) && (in.remaining() == 0))
return out;

자바에서 바이트 자료형을 문자열로 바꾸는 루틴은 여러가지가 있다. 그 중 위에서 본 CharsetDecoder.decode(ByteBuffer in) 메서드가 자바 5에서는 버그가 있었는데 톰캣 6이 이걸 사용하면서 결국 문제를 일으킨 것이다.

일단 해결은 톰캣을 자바 6에서 실행하는 것으로 처리했다. 그러나 컴파일은 5에서 하므로 별로 깔끔한 방법은 아니다. 원칙대로 운영 환경과 같게 톰캣 5.5를 사용하는 것이 좋을 것 같다.

오늘의 교훈은 검증되지 않은 것을 함부로 사용하지 말아야겠다는 것이다.