[끄적이] Websphere 8.5.5에서 Apache httpclient-4.5.13 사용 시 AllowAllHostnameVerifier.INSTANCE 관련 NoSuchFieldError 발생 이슈 해결기
Websphere 8.5.5에서 Apache httpclient-4.5.13 사용 시 AllowAllHostnameVerifier.INSTANCE 관련 NoSuchFieldError 발생 이슈 해결기
이번 이슈를 다루면서 정말 많은 공부와 성장을 했다고 느끼고 있고, 누군가에겐 이 글이 도움이 되지 않을까 싶은 마음에 이를 기록하여 남긴다. 우리나라에서 많이 사용되고 있지 않은 웹스피어 관련 문제이다 보니 찾아봤던 자료가 거의 한글로 된 것은 전무하였고, 영어로도 생각보다 많은 자료가 나오지 않았다.
배경
상반기부터 사내 SSO 재구축이 진행중이라는 이야기를 들었는데, 얼마 전 관련부서로부터 개발이 모두 완료되었으니 11월말까지 새로운 SSO를 통해 각 업무 시스템을 로그인할 수 있도록 개발을 진행해달라고 요청이 왔다. 개발 가이드, jsp로 된 예시 코드, 7개 정도의 라이브러리와 함께. 내가 우리 파트의 시스템 담당자였기 때문에 요청 메일을 내가 받았고, 어쩌다보니 내가 혼자 기존 로그인 로직을 분석하고, 새로운 로그인 방식을 공부하고 개발을 진행하게 되었다.
우리 파트는 jsp 쓰지 않고 있었고, jsp 개발을 해본 적이 없었다. jsp라니.. 일단 개발 공수를 최대한 줄이고자 jsp로 받은 예시 코드를 최대한 살리고 가이드를 충실히 따르며 테스트를 진행해보기로 하였다. 그리고 이번 참에 jsp가 어떻게 생겨먹었고, 어떤 식으로 컴파일이 되고 실행이 되는지, 또 jsp를 요즘 왜 많이 안쓰는지 등을 파악할 수 있을 것 같았다.
SSO 로그인 과정을 살펴보니 클라이언트에서 우리 사이트로 최초 로그인을 시도하면, 우리 서버에서 SSO 인증서버로 연결을 먼저 맺고 인증 서버 연결을 확인 후 인증 작업을 진행하는 것이 첫 번째였다. SSO 인증 서버에 연결을 하고 인증을 하는 작업을 수행하는 예시 코드가 있었기 때문에 몇 가지 설정만 수정 후 해당 jsp를 로컬에서 jetty를 통해 실행시켜보았고, 테스트 결과 연결과 인증에 성공하였다.
로컬에서 문제가 없어서 개발 서버로 라이브러리와 jsp 파일들을 올려 테스트를 하였다. 그런데 500 에러가 발생하였다. was 로그를 확인해보니 로컬에서는 보지 못한 메시지가 있었다.
service() 예외 근본 원인
com.ibm.websphere.servlet.error.ServletErrorReport:java.lang.NoSuchFieldError:org/apache/http/conn/ssl/AllowAllhostnameVerifier.INSTANCE
해결기
문제를 발생시킨 코드 찾기
담당부서로부터 제공받은 코드는 인증 서버와 연결을 시도할 때 HttpClientBuilder
를 통해 CloseableHttpClient
객체를 생성하고, 인증 서버에 GET 요청을 보내고 있었다. 에러가 나는 지점은 CloseableHttpClient
객체를 생성하는 지점이었다. JadClipse
가 로컬에 설치되어 있어서 httpclient
라이브러리 내 클래스들의 코드를 확인할 수 있어AllowAllHostnameVerifier
클래스를 살펴보았다. static 변수로 INSTANCE가 선언되어 있었다. 그런데 왜 NoSuchFieldError가 났을까? 구글링을 해보니 httpclient 4.4
미만의 버전에서는 INSTANCE 변수가 없어서 해당 에러가 발생한다고 하였는데 내가 사용하고 있는 httpclient는 4.5.13이었다. 이는 런타임 환경에서 AllowAllHostnameVerifier
를 호출할 때 다른 버전의 라이브러리를 참조하고 있을 가능성이 높았다. 그런데 우리 웹 프로젝트의 Application ClassLoader에 의해 로딩되는 classpath 내에 httpclient 라이브러리가 존재하지 않았다. 그러면 대체 어떤 경로에서 AllowAllHostnameVerifier
를 가져오는걸까?
클래스 호출 경로 찾기
우리 서버 내에서 AllowAllHostnameVerifier
를 찾아다녔다. 진짜 무식한 방법으로 개발 서버 내 웹 프로젝트 경로와 was 디렉토리에서 모든 jar를 뒤져 jar -tf
명령어로 클래스 목록을 확인 후AllowAllHostnameVerifier
가 있는지 확인하는 스크립트를 만들어서 돌려보기도 했다. 그런데 시간이 너무 걸리고 이건 아닌거 같다는 생각에 접고 다른 방법을 생각해보았다. 자바 문법 내에서 클래스를 import할 때 어떤 경로에서 가져오는지 확인하는 방법이 있을 것 같았다. 역시 있었다. ClassLoader의 getResource를 활용하면 되었다. 그래서 HttpClientBuilder
, HttpClients
, AllowAllHostnameVerifier
클래스의 경로를 콘솔에 찍어보았다. 그 결과 HttpClientBuilder
, HttpClients
는 웹 프로젝트 내 라이브러리 경로에서 호출하고 있었는데, AllowAllHostnameVerifier
는 bundleresource라는 이상한 경로에서 가져오는게 아닌가? 또 열심히 구글링해보니 bundleresource는 OSGI 프레임워크의 클래스로더를 뜻하고 있었고, 문제가 되는 클래스는 WAS_HOME/plugins/com.ibm.ws.prereq.jaxrs.jar
내에서 호출하고 있었다.
왜 이런 문제가 발생하였나?
우연히 웹스피어 8.5 버전에서 SSL 소켓 통신 시 AllowAllHostnameVerifier.INSTANCE 관련 에러가 발생한다는 글을 보게 되었다. SSL 소켓 통신 시 WAS의 라이브러리를 사용하도록 웹스피어 8.5 버전에서 SSL_Configuration에 디폴트로 설정이 되어있다는 내용이었다.
에러 로그에서 SSL 어쩌고 써있었던 기억이 났다. 에러 로그를 다시 살펴보았다. AllowAllHostnameVerifier
에만 집착을 하여 몰랐는데 다시 살펴보니 AllowAllHostnameVerifier.INSTANCE를 사용하는 곳은 SSLConnectionSocketFactory
였다. 해당 클래스의 코드를 살펴보았다. static 변수로 HOST_NAME_VERIFIER 같은 것이 있었고 이 변수를 AllowAllHostnameVerifier.INSTANCE로 초기화하고 있었다. 이 지점에서 에러가 나고 있었다. 그리고 SSLConnectionSocketFactory
는 HttpClientBuilder
에서 CloseableHttpClient
객체를 생성할 때 사용되고 있었다.
내가 생각한 해결 방법 세 가지
1. 부트스트랩 클래스에 httpclient 4.5.13을 추가하기
httpclient 4.5.13를 부트스트랩 클래스에 지정하여 WAS쪽 라이브러리 보다 먼저 물게 강제로 앞에다 물려주면 문제가 해결될 것 같았다.
2. SSLConnectionSocketFactory 내 문제 발생 코드를 직접 수정하여 사용하기
문제가 발생했던 SSLConnectionSocketFactory 내 static 변수는 AllowAllHostnameVerifier 객체를 할당하면 되기 때문에 AllowAllHostnameVerifier.INSTANCE가 아닌 다른 방식으로 객체를 생성해서 할당해주면 해결되지 않을까라는 생각을 하였다.
그래서 jar를 풀고 해당 클래스가 자바 몇 버전으로 컴파일이 되었는지 확인해보았다. javap -verbose org.apahce.http.conn.ssl.AllowAllHostnameVerifier.class | find "version"
명령어를 실행해보니 major version으로 50이 출력되었고 이는 자바 1.6버전을 의미했다. 그러면 문제되는 SSLConnectionSocketFactory 내 코드를 위와 같은 방식으로 수정해서 1.6으로 컴파일하고 다시 jar로 압축해서 서버에 올리면 되지않을까..? 라는 생각을 하였다.
3. CloseableHttpclient 객체를 사용하지 않고 DefaultHttpClient 사용하기
제공받은 샘플 코드를 다시 살펴보았다. CloseableHttpclient 객체를 이용해서 인증 서버로 GET요청을 보내고 있었는데, 이 객체는 단순히 인증 서버가 살아있는지, 통신이 잘 되는지 확인만 하는데 사용되었다. CloseableHttpclient는 커넥션 풀을 통해서 요청을 처리하고, 이 설정에 따라 HTTP 통신 횟수를 여러 번 가져갈 때 사용하는데, 어차피 1회성으로 한 번만 통신하면 되는 현재 상황에서 굳이 CloseableHttpclient를 사용하지 않고 DefaultHttpClient를 사용해도 되었다. 그리고 CloseableHttpclient 객체가 생성될 때처럼SSLConnectionSocketFactory를 사용해서 문제가 생기지도 않았다.
내가 생각한 해결책
1번 방법은 서버 설정을 변경해야하고, 서버에 올라가있는 다른 서비스, 그리고 로그인 처리 외에 다른 프로그램들에 영향을 줄 가능성이 있기에 리스크가 있었다.
2번 방법은 지금 터진 문제는 해결해도 다른 문제가 언제 어디서 터질지 모르기 때문에 위험성을 내포하고 있었다.
그래서 우선 3번 방법을 사용하기로 결정하고 담당 부서에 CloseableHttpclient를 사용하지 않는 다른 가이드를 받을 수 있는지 확인해보았다. 내가 직접 개발하고 테스트할 수도 있었지만 담당부서에서 샘플 코드나 라이브러리를 커스텀해서 발생하는 문제는 각 업무 부서에 책임이 있다고 으름장을 놓았기에 일주일 가까이 현재 우리 서버에서 발생하는 문제를 설명, 공유하고 가이드를 줄 것을 닥달하였다. 덕분에 새로운 가이드를 받을 수 있었고 결국 인증 서버와의 통신에 성공하였다.
문제 해결을 위해 오래 고민하고 분석했던 것 치고는 해결 방법이 다소 허망하기도 하였다. 하지만 이 문제를 해결하면서 클래스 로더의 개념, 자바 웹 프로젝트의 구조, class 파일의 컴파일 버전 확인 방법, jar 압축과 jar 내 클래스 리스트 확인, class 파일 디컴파일, javac로 컴파일하는 방법을 공부할 수 있었고, 라이브러리 코드를 분석하고 디버깅하는 실력이 향상되었다고 느끼기에 아주 유익한 시간이었다.