세션 클러스터링이 멈춘 원인은 보이지 않는 문자 하나
Hazelcast 세션 클러스터링을 붙인 WAS에서 클라이언트 설정 XML이 cvc-complex-type.2.3 검증 오류로 거부되고, 그 여파로 세션 저장이 NullPointerException으로 실패했던 기록입니다. 설정 값 자체는 맞았습니다.
원인은 눈에 보이지 않는 U+FEFF 문자였습니다. 파일 첫머리의 BOM이 아니라 태그 앞 들여쓰기에 섞여 있었기 때문에, 흔히 쓰는 BOM 제거 방법으로는 해결되지 않았습니다.
WAS 로그에 남은 두 줄의 경고
로그에는 경고 두 줄이 연달아 남았습니다. 첫 줄은 Hazelcast 클라이언트가 설정을 읽다가 실패한 것이고, 둘째 줄은 연결되지 않은 클라이언트로 세션을 쓰려다 난 NullPointerException입니다.
09:52:06,504 WARNING [com.hazelcast.web.ClusteredSessionService] (default task-1) Cannot connect to Hazelcast server: cvc-complex-type.2.3: Element 'near-cache' cannot have character [children], because the type's content type is element-only. 09:52:06,962 WARNING [com.hazelcast.web.HazelcastHttpSession] (default task-1) Unexpected error occurred.: java.lang.NullPointerException at com.hazelcast.web.ClusteredSessionService.updateAttributes(ClusteredSessionService.java:285) at com.hazelcast.web.HazelcastHttpSession.sessionDeferredWrite(HazelcastHttpSession.java:300) at com.hazelcast.web.WebFilter.doFilter(WebFilter.java:303) at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61) at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) ...
첫 경고만 보면 Hazelcast 서버에 연결하지 못한 네트워크 문제처럼 읽힙니다. XML 스키마 검증 오류 코드가 메시지 뒤쪽에 붙어 있다는 점이 단서였습니다. 클라이언트가 설정을 읽는 단계에서 멈췄기 때문에 연결은 시도조차 못 한 것입니다. Undertow 기반 WAS였다는 것은 스택의 io.undertow에서 알 수 있습니다.
오류 문구가 가리키는 것
“Element ‘near-cache’ cannot have character [children]”은 near-cache 요소 안에 자식 요소가 아닌 글자가 들어 있다는 뜻입니다. XML 스키마에서 element-only로 정의된 요소는 자식 요소와 공백만 가질 수 있습니다. 여기서 공백은 XML 규격이 정한 스페이스, 탭, CR, LF 네 가지뿐입니다.
U+FEFF(Zero Width No-Break Space)는 이 네 가지에 속하지 않습니다. 화면에는 아무것도 보이지 않지만 검증기에게는 엄연한 문자 내용입니다. 그래서 들여쓰기에 하나만 섞여도 같은 오류가 납니다. Java 검증기는 이 규칙 위반을 cvc-complex-type.2.3으로 표시하고, libxml2는 “Character content other than whitespace is not allowed”로 표시합니다.

BOM 없는 UTF-8로 저장해도 남아 있던 이유
처음에는 Windows 에디터가 붙이는 BOM을 의심했습니다. 그런데 에디터에서 BOM 없는 UTF-8로 저장하는 방법이 통하지 않았고, 태그 앞 여백을 모두 지운 뒤에야 오류가 사라졌습니다.
BOM은 파일 맨 앞 3바이트(EF BB BF)를 가리키고, 에디터의 “BOM 없이 저장”도 그 3바이트만 뺍니다. U+FEFF가 줄 중간에 있으면 BOM이 아니라 평범한 문자로 취급되어 그대로 남습니다. HTML 문서나 웹 페이지에서 설정 예제를 복사해 붙일 때 이런 문자가 함께 딸려 오기도 합니다.
숨은 문자를 찾고 지우는 방법
아래는 같은 상황을 재현한 예제 파일로 확인한 결과입니다. 4번째 줄의 들여쓰기에 U+FEFF를 하나 넣었습니다.
$ file hazelcast-client.xml
hazelcast-client.xml: XML 1.0 document, Unicode text, UTF-8 text
$ grep -c $'\xEF\xBB\xBF' hazelcast-client.xml
1
$ grep -nP '\x{FEFF}' hazelcast-client.xml | cat -A
4: M-oM-;M-?<in-memory-format>OBJECT</in-memory-format>$
file은 BOM이 파일 첫머리에 있을 때만 “with BOM”이라고 알려 줍니다. 그래서 이 경우는 그냥 UTF-8 텍스트로 보고합니다. EF BB BF 바이트 열을 grep으로 직접 찾는 편이 확실하고, cat -A로 보면 M-oM-;M-? 표시로 위치까지 드러납니다.
지울 때는 첫 줄만 고치는 sed '1s/^\xEF\xBB\xBF//'로는 부족합니다. 파일 전체에서 바꿔야 줄 중간의 문자까지 사라집니다.
$ sed -i 's/\xEF\xBB\xBF//g' hazelcast-client.xml $ grep -c $'\xEF\xBB\xBF' hazelcast-client.xml 0
제거 전의 예제 파일을 최소 스키마로 검증하면 libxml2가 near-cache 요소에서 element-only 위반을 보고했습니다. 제거한 뒤에는 같은 스키마로 검증을 통과했습니다.
다시 겪지 않으려면
- 설정 파일을 배포하기 전에
grep -rl $'\xEF\xBB\xBF' <설정 디렉터리>로 한 번 훑습니다. CI 단계에 넣어 두면 사람이 놓쳐도 걸러집니다. - VS Code는 보이지 않는 유니코드 문자를 강조하는
editor.unicodeHighlight.invisibleCharacters설정을 제공합니다. Windows에서 설정 파일을 편집한다면 이 설정이 켜져 있는지 확인합니다. .editorconfig의charset = utf-8은 파일 첫머리 BOM만 막아 줍니다. 줄 중간의 문자는 따로 검사해야 합니다.
참고
관련 포스트:
- Hazelcast WEB Session Clustering — 이 오류가 난 WebFilter와 hazelcast-client.xml 구성
참고 문서: XML 1.0 공백 정의 · XML Schema cvc-complex-type 규칙 · Unicode BOM FAQ · 바이트 순서 표식(위키백과)
