본문 바로가기

Programing/Framework

[Spring] 스프링 부트에서 SOAP 클라이언트 사용하기

최근에 내가 개발했던 웹서비스(클라이언트던 서버던 간에) RESTful 웹서비스였다.

메세지의 포맷도 JSON으로 사실상의 표준(De facto)으로 자리잡은 듯하다고 느꼈다.

 

얼마전부터 멤버십 서비스 연동을 할 일이 있어서 타 사와의 업무를 진행하게 되었다.

하지만 연동해야 하는 방식이 이제는 더 이상 안쓸 것 같다고 생각했던, 소켓(socket) 통신을 실 환경에서도 사용되고 있었다.

좀 더 추상화된 레벨을 요구했더니 SOAN 기반의 웹서비스를 제공해주었다.

 

SOAP과 RESTful 웹서비스가 익숙하지 않은 사람은 ETRI(한국전자통신연구원)에서 2010년에 출간한 비교 자료를 참고하면 좋다.

25-2_112_120.pdf
0.33MB

SOAP with Spring Framework

일반적인 스프링 서적중에 SOAP 웹서비스를 다루고 있는 책은 스프링 5 레시피(개정4판)정도였다. 주변에 있는 스프링 책중에서는 말이다.

2권의 13장(스프링 자바 엔터프라이즈 서비스와 원격 기술)

였는데, 이 책도 오래된 내용을 다루고 있어(예를 들자면 XML 마샬러중에 Castor와 XMLBeans는 스프링 4.3.13, 4.2부터 deprecated되었는데 스프링5라는 책이 아직도 소개한다.)

 

따라서 책보다는 최신을 따라가려면 웹 리소스가 더 나아보였다.

스프링의 프로젝트중에 Spring Web Services라는 프로젝트가 있다.(줄여서 ws) 아직 프로젝트가 없어지지 않은 걸(not retire)보니 웹서비스의 통합은 중요하게 인식되는 것 같다.

https://spring.io/projects

스프링에서 SOAP 클라이언트

Refernece Document가 유용하다.

 

Learn 탭에 보면 레퍼런스 문서가 있는데, 스프링에서 SOAP 클라이언트를 구현하기 위해 가장 적합하다.

스프링에서는 클라이언트 쪽 API를 위해 스프링 템플릿 패턴과 유사한 방식을 제공한다.

 

WebServiceTemplate

https://docs.spring.io/spring-ws/docs/3.0.7.RELEASE/reference/#client

위의 링크에도 제일먼저 나오지만 SOAP 클라이언트로 가장 중요한 클래스는 WebServiceTemplate 이다.

이 클래스를 직접 주입받아 사용할 수도 있지만 나는 WebServiceGatewaySupport 라는 추상 클래스를 상속받아 구현하는 쪽으로 하였다.

상속을 받고 나면 getWebServiceTemplate() 메서드를 통해 WebServiceTemplate 를 얻을 수 있다.

다음 섹션에서 설명할 샘플을 참고하면 이해하기 편하다.

Spring Guide Sample

스프링에서는 예를 통해서 어떻게 사용하는지 보여준다.

Producing a SOAP web service 와 Consuming a SOAP web service 가 바로 그것이다.

앞의 예제가 서비스 하는 쪽 코드이고, 뒤의 예제가 소비, 즉 클라이언트의 코드이다. 후자를 돌리기 위해서는 서버도 같이 띄워야 한다.

환경설정코드: https://github.com/spring-guides/gs-consuming-web-service/blob/master/complete/src/main/java/hello/CountryConfiguration.java

마샬링/언마샬링

SOAP은 메세지를 XML을 통해서 구성한다.

따라서 serialize 및 deserialize를 해야 하는데 이를 담당하는 클래스를 Marshaller와 Unmarshaller라고 부른다.

이에 대한 내용은 ws 프로젝트가 아닌 core쪽에서 OXM이라는 부분에서 소개를 한다.

이에 대해서는 레퍼런스 문서를 참고한다.

개발하면서 발생했던 에러 로그

@XmlRootElement 주석이 누락되어 "hello.wsdl.SelectCustomerInfo$RequestBody" 유형을 요소로 마셜링할 수 없습니다.

Jaxb2Marshaller를 이용해서 마샬링과 언마셜링을 했었는데, setContextPath 라는 메서드를 이용하여 마샬링 혹은 언마샬링을 할 경로를 지정해준다. 이때 ContextPath 상 클래스에 @XmlRootElement 어노테이션이 없는 경우에 발생한다.

JAXB 제너레이터(생성기)로 만든 클래스인데 해당 어노테이션이 없이 만들어져서 의아했다.

 

Caused by: org.springframework.ws.soap.client.SoapFaultClientException: Message part {http://customerinfo.example.com/}CustomerInfoRequest was not recognized.  (Does it exist in service WSDL?)

서비스 URL과 WSDL제공 URL은 다르다.

위에서 언급한 gs-producing-web-service 예의 경우, wsdl 주소는 http://localhost:8080/ws/countries.wsdl 이지만, HTTP POST 요청을 보내는 서비스 주소는 http://localhost:8080/ws 이다.

wsdl 주소
SOAP 요청 주소

위의 캡쳐는 Wizdler 이라는 크롬 확장 프로그램인데 SOAP 서비스에 대해 브라우저에서 테스트를 해볼 수 있어서 이번 개발에 유용했다.

(실제 요청도 할 수 있다.)

 

Caused by: org.springframework.ws.soap.SoapMessageCreationException: Could not create message from InputStream: Unable to create envelope from given source: ; nested exception is com.sun.xml.internal.messaging.saaj.SOAPExceptionImpl: Unable to create envelope from given source: 
at org.springframework.ws.soap.saaj.SaajSoapMessageFactory.createWebServiceMessage(SaajSoapMessageFactory.java:218) ~[spring-ws-core-3.0.6.RELEASE.jar:na]

솔직히 왜 이 메세지가 나왔는지는 아직 잘 모르겠다. 위의 메세지에 언급되는 WebServiceTemplate 클래스를 초기화 할 때 기본적으로 사용하는 팩토리 클래스이다.

SAAJ는 SOAP with Attachments API for Java™의 약자로 스프링 부트 애플리케이션이 뜰 때 아래와 같은 로그를 볼 수 있다.

2019-05-03 11:01:42.268  INFO 23282 --- [  restartedMain] o.s.ws.soap.saaj.SaajSoapMessageFactory  [] : Creating SAAJ 1.3 MessageFactory with SOAP 1.1 Protocol
@Override
public void afterPropertiesSet() {
  if (messageFactory == null) {
    try {
      if (SaajUtils.getSaajVersion() >= SaajUtils.SAAJ_13) {
        if (!StringUtils.hasLength(messageFactoryProtocol)) {
          messageFactoryProtocol = SOAPConstants.SOAP_1_1_PROTOCOL;
        }
        if (logger.isInfoEnabled()) {
          logger.info("Creating SAAJ 1.3 MessageFactory with " + messageFactoryProtocol);
        }
        messageFactory = MessageFactory.newInstance(messageFactoryProtocol);
      }
      else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_12) {
        logger.info("Creating SAAJ 1.2 MessageFactory");
        messageFactory = MessageFactory.newInstance();
      }
      else if (SaajUtils.getSaajVersion() == SaajUtils.SAAJ_11) {
        logger.info("Creating SAAJ 1.1 MessageFactory");
        messageFactory = MessageFactory.newInstance();
      }
      else {
        throw new IllegalStateException(
            "SaajSoapMessageFactory requires SAAJ 1.1, which was not found on the classpath");
      }
    }
    catch (NoSuchMethodError ex) {
      throw new SoapMessageCreationException(
          "Could not create SAAJ MessageFactory. Is the version of the SAAJ specification interfaces [" +
              SaajUtils.getSaajVersionString() +
              "] the same as the version supported by the application server?", ex);
    }
    catch (SOAPException ex) {
      throw new SoapMessageCreationException("Could not create SAAJ MessageFactory: " + ex.getMessage(), ex);
    }
  }
  if (logger.isDebugEnabled()) {
    logger.debug("Using MessageFactory class [" + messageFactory.getClass().getName() + "]");
  }
}

 

SAAJ 1.3 버전을 쓰는데 정작 사용한 SOAP은 1.1 프로토콜이었다.