SOAP connection reset
글을 읽기 전에 앞 서 모든 connection reset 오류 메시지 해결책에 대해 해당하지 않습니다.
여러 가지 오류가 있는 것으로 알고 있습니다.
stack trace를 잘 살펴보면 더 세밀한 정보를 얻을 수 있습니다.
문제발생
SOAP 관련한 통신을 JAXWS 라이브러리를 통해 사용하고 있었습니다.
SOAP 관련 통신 플랫폼 자체를 변경하게 되어 스프링부트 3.x로 버전을 올리고 관련 라이브러리 들도 전부 버전을 올리게 되었는데
간헐적으로 Could not send message. 라는 오류 메시지와 함께 SOAP 요청이 목적지 API에 전달되지 않는 오류가 발생하였습니다.
메시지 내용을 자세히 파악해보니? connection reset 이라는 오류 메시지도 있습니다.
좀 더 파보니 ConnectionExpiredException 이 발생하고 있었습니다.
기존 플랫폼과 비교해 보니 다른 점이 기존에는 SOAP 통신 시 사용하는 Client class 가 라이브러리 자체에서 ClientProxy를 통해 (예상)
설정된 java에 포함된 HTTPUrlConnection을 사용하여 통신하게 되어 있었습니다.
기존 버전은 cxf 를 통해 생성된 클래스 파일 중 SOAP 통신을 할 수 있는 클래스파일이 있었습니다.
해당 클래스 파일에서 HTTPClient에 대한 여러 옵션을 설정하려니? ClientProxy.getClient() 메서드를 통해 해당 서비스에서 사용되는 클라이언트 객체를 가져올 순 있지만 헤더 설정 등 변경할 수 있는 옵션에 한계가 있었습니다.
가설
스프링 부트 3.x 버전으로 올리면서 spring-boot-starter-web-services 의 버전도 함께 올라가면서 변경된 건지 jaxws 라이브러리 버전이 변경되면서 사용하는 클라이언트가 변경된 건지는 정확히 알 수 없으나 일단은 통신하는 HTTPClient 가 문제라는 사실은 알아냈습니다.
디버깅해보니 기존 버전에서는 HTTPUrlConnection 클래스를 사용하여 타 API와 통신하였고
새로운 버전의 플랫폼에서는 HTTPClientImp과 SocketTube 클래스를 사용하여 HTTP 요청을 한다는 사실을 알아냈고 무엇인가 요청을 시도하는 클라이언트 옵션 때문이라는 가설이 생겼습니다.
제가 생각한 가설은 HTTP 1.1 의 Keep-alive로 인해 이미 연결이 만료된 커넥션을 클라이언트 측에서 재사용 시도하다가 오류가 발생한 것으로 생각했습니다.
해결
https://docs.spring.io/spring-ws/docs/current/reference/html/#client-web-service-template
스프링 부트 Docs 에 예시로 있는 소스코드를 발견하게 되었습니다.
통신 부분의 소스코드를 수정하기 시작했습니다.
기존 : .wsdl 파일 기반으로 apache-cxf 플러그인으로 생성된 클래스파일을 통해 통신
변경: WebServiceTemplate 클래스를 통한 통신
해당 소스코드에서는 MessageSender를 HttpComponentsMessageSender 로 변경하고 apache HttpClient를 사용하여 클라이언트 측 커넥션 풀과 커넥션에 대해 여러 가지 옵션을 조정할 수 있었습니다.
SOAP-Client 예제 코드를 적용한 GIT Repository 입니다.
https://github.com/infitry/soap-client/tree/master
아래 코드에서 유휴 상태의 커넥션 제거주기 (evictIdleConnections(40, TimeUnit.SECONDS)와
만료된 커넥션 (evictExpiredConnections)을 제거하는 옵션을 추가하고 정상 동작하는 것을 확인할 수 있었습니다.
실제 해당 옵션을 주고 로그 레벨을 DEBUG로 설정하게 되면 다음과 같은 메시지와 함께 열심히 정리해 줍니다.😃
PoolingHttpClientConnectionManager : Closing expired connections
PoolingHttpClientConnectionManager : Closing connections idle longer than 40000 MILLISECONDS
정확한 건 아니지만 ConnectionExpiredException 이 발생한 것만 보면 이미 만료된 커넥션을 재사용하여 문제가 되었던 것 같습니다.
아래는 apache HttpClient 설정 예시입니다.
package infitry.soap.client.configuration;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.http.HttpComponentsMessageSender;
import java.util.concurrent.TimeUnit;
@Configuration
public class CountryConfiguration {
@Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
// this package must match the package in the <generatePackage> specified in
// pom.xml
marshaller.setContextPath("infitry.soap.client.wsdl");
return marshaller;
}
@Bean
public WebServiceTemplate countryWebServiceTemplate(Jaxb2Marshaller marshaller) {
var webServiceTemplate = new WebServiceTemplate();
webServiceTemplate.setMessageSender(createMessageSender());
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
return webServiceTemplate;
}
private HttpComponentsMessageSender createMessageSender() {
var httpComponentsMessageSender = new HttpComponentsMessageSender();
httpComponentsMessageSender.setConnectionTimeout(10000);
httpComponentsMessageSender.setReadTimeout(60000);
httpComponentsMessageSender.setHttpClient(createHttpClient(createHttpClientConnectionManager()));
return httpComponentsMessageSender;
}
private PoolingHttpClientConnectionManager createHttpClientConnectionManager() {
var poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(20);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);
return poolingHttpClientConnectionManager;
}
private CloseableHttpClient createHttpClient(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
return HttpClients.custom()
.addInterceptorFirst(new HttpComponentsMessageSender.RemoveSoapHeadersInterceptor())
.setConnectionManager(poolingHttpClientConnectionManager)
.evictExpiredConnections()
.evictIdleConnections(40, TimeUnit.SECONDS)
.build();
}
}