티스토리 뷰

728x90

Web Service는 xml이라는 것 때문에 쉽지가 않다. 그리고 자바의 JAXB라이브러리가 XML를 마샬링 할 때 발생하는 오류와 그로 인한 영향 때문에 처음에 세팅하는 것이 굉장히 까다롭다. SOAP Header가 있는 경우는 스프링을 아주 잘하지 않는 이상 며칠을 고생할 수도 있다. 클라이언트 개발은 서버로 보내지는 구문이 정상적인지 확인하는 것이 가장 중요하기 때문에 개발은 아래처럼 하면 된다.

1. 어떤 것이 실행되는 검증된 Request구문을 찾는다. 이것은 서비스를 제공하는 곳에서 같이 제공하는 것이 일반적이다.
2. 현재 자바에서 어떻게 xml로 변환되어 보내지고 있는지를 확인한다.
3. 되는 구문과 안되는 구문의 차이점을 찾아내어 자바에서 보내는 xml 구문을 수정하면서 디버깅 한다.

1. 검증된 Request 구문 찾기
검증된 구문을 찾기 위해서는 client가 필요한데, SOAP UI가 가장 간편한 것 같다. WSDL만 있으면 관련 테스트 정보를 가져올 수 있기 때문이다. 물론 자동으로 만들어진 XML Request구문은 모든 파라메터가 다 보여지고 ?로 된 부분을 확인하여 채워야 하기 때문에 조금 시행착오가 필요하다.

사실 가장 쉽게 Web Service 클라이언트를 개발하는 방법은 nodejs의 node-soap을 사용하는 것이다. JavaScript는 타입이 없기 때문에 wsimport 같은 것을 할 필요가 없다. 헤더 넣는 것도 메소드 하나로 처리가능하기 때문에 너무 간편하게 프로그램을 작성할 수 있다. 토이 프로그램으로 nodejs로 작성하여 실행되는 request를 찍어 보는 것도 좋은 방법이다. 물론 SOAP 라이브러리의 소스를 수정해서 찍어야 하지만 자바처럼 컴파일이 필요 없기에 쉬운 작업이다.

2. 자바에서 어떻게 JAXB가 변환하여 보내지고 있는지를 확인하는 것은 아래의 구문을 applicaton.properties에 넣어 주는 것으로 가능하다.

logging.level.org.springframework.web=DEBUG
logging.level.org.springframework.ws.client.MessageTracing.sent=DEBUG
logging.level.org.springframework.ws.server.MessageTracing.sent=DEBUG
logging.level.org.springframework.ws.client.MessageTracing.received=TRACE
logging.level.org.springframework.ws.server.MessageTracing.received=TRACE


2-1 이렇게 찍어본 구문은 보기가 쉽지 않은 덩어리로 출력이 된다. 해당 정보를 온라인 formatter를 가지고 변환하여 분석하면 편리하다.

3. 되는 구문과 안되는 구문의 가장 큰 차이점은 일반적으로 namespace이다. wsimport 같은 클래서 생성 툴로 만들어진 클래스는 대부분 내부에 타입정보와 네임스페이스 정보를 가지고 있다. 그리고 그 내용이 정확하다. 아래의 내용은 커스텀 헤더가 있는 경우인데 일반적인 생성 클래스 처럼 @XmlRootElement가 없다. 아래의 문제는 헤더와 바디 동일하게 적용된다.

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "OGHeader", namespace = "http://webservices.micros.com/og/4.3/Core/", propOrder = {
    "origin",
    "destination",
    "intermediaries",
    "authentication"
})
public class OGHeader {


3-1 첫번째 문제는 믾은 개발자들이 단순히 ObjectFactory를 가지고 createOGHeader 식으로 클래스를 생성하여 사용하는데 이럴 경우 @XmlType의 name의 값을 사용하지 않고 이름이 ogHeader가 되어 버려 정상적인 xml이 만들어지지 않는다. 그냥 new 로 클래스를 생성하는 것을 추천한다. 아니 그렇게 해야 동작한다.
3-2 두번째 문제이다. SpringBoot에서 헤더를 넣으려고 하는 경우는 이미 marshall이 된 메시지 객체를 받아와서 그 객체에 다가 Header을 넣는 방식이다. 마샬링을 별도로 하는 순간 @XmlRootElement 문제가 발생한다. 노드 라이브러리와 다르게 사용자가 직접 만들어야 할 부분이 있기 때문에 까다롭게 보인다.

 

  3-2-1 일반적으로는 아래 경우처럼 문제가 되는 클래스의 @XmlRootElement를 넣어주고 대소문자가 정확하게 이름해서 해결할 수 있는데  name이 실제 xml로 변환될 때 그대로 나오게 된다. 이 xml 파싱오류는 이렇게 헤더를 추가하는 경우를 포함하여 여러 곳에서 날 수 있는데 발생하는 경우 추가하면 된다. 하지만 이 방법은 그 다지 좋은 방법이 아니다. 자동 생성되는 클래스의 내용을 임의로 변경하기 때문에 나중에 다시 자동 생성할 경우 설정한 내용들이 모두 사라지게 된다.  

 

@XmlRootElement(name = "OGHeader")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "OGHeader", namespace = "http://webservices.micros.com/og/4.3/Core/", propOrder = {
    "origin",
    "destination",
    "intermediaries",
    "authentication"
})
public class OGHeader {


3-3 세번째 문제는 위의 내용처럼 생성된 클래스에 직접 @XmlRootElement를 추가한 경우 더 이상 @XmlType에 있는 namespace가 element에 지정되지 않는다. 따라서 아래처럼 namespace를 명시해 주어야 한다. 그렇지 않으면 Package-Info.java에 기본값으로 설정된 그것이 xmlns에 기본값으로 설정되어 request가 정상적으로 되지 않는다. Package-Info의 namespace기본값을 바르게 설정해 주거나 빼버려도 상관없다. 두 개 이상의 namespace를 사용하는 대형 서비스의 경우는 그냥 빼주는 것이 좋다. 위의 3-2-1의 내용과 동일한 이유로 이것을 권장하지 않는다. 대용량 시스템의 경우는 하나하나 다 맞추어야 할 경우가 생기게 된다.

 

@XmlRootElement(name = "OGHeader", namespace = "http://webservices.micros.com/og/4.3/Core/")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "OGHeader", namespace = "http://webservices.micros.com/og/4.3/Core/", propOrder = {
    "origin",
    "destination",
    "intermediaries",
    "authentication"
})
public class OGHeader {


4. 결과적으로 내가 하기 원하는 것은 자동 생성 되는 클래스에 영향을 미치지 않고 정확하게 조회가 되는 시스템이다. 

  4-1 정리하면 첫째로 자동생성되는 클래스는 수정하지 않는다.

  4-2 위의 3-1에 말한 내용처럼 FactoryObject를 사용하여 생성하지 않는다. 헤더 대소문자 변경된다.

 

5. 제약 조건에 맞추어 정리하면 사용하는 데이터와 헤더는 FactoryObject를 사용하지 않고 직접 생성하고 이 클래스를 마샬링을 위해 전달할 때는 ObjectFactory의 create문을 사용하여 JAXBElement<데이터타입> 형식으로 @XmlRootElement을 생성하여 돌려 준다.

 

spring ws에서 커스텀 헤더를 붙이는 것이 쉽지 않은데 아래의 코드를 참고하기 바란다. OGHeader를 SOAP Header 붙이는 부분이다. 중요한 부분은 OGHeader를 먀샬링할 수 있는 마샬러를 만들어서 soapHeader에서 가져온 Result에 기록하는 부분이다. 여기에 전달되는 값이 JAXBElement로 한번 감싼 헤더가 된다. 바디도 마찬가지로 생성은 직접하고 send할 때 한번 감싸서 보내면 된다. 

 

public class UnauthHeader implements WebServiceMessageCallback {
  private String soapAction;

  private final String TRANSACTION_ID = "-";

  private final String ORIGIN_ENTITY_ID = "-";

  private final String ORIGIN_SYSTEM_TYPE = "-";

  private final String DESTINATION_ENTITY_ID = "-";

  private final String DESTINATION_SYSTEM_ID = "-";
  
  private final String PRIMARY_LANGUAGE_ID = "-";

  public UnauthHeader(String soapAction) {
    this.soapAction = soapAction;
  }
 
  public void setSoapAction(String soapAction) {
    this.soapAction = soapAction;
  }

  @Override
  public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException {
    var soapMessage = (SoapMessage) message;
    if (this.soapAction == null) {
      this.soapAction = "";
    }

    soapMessage.setSoapAction(this.soapAction);
    var soapHeader = soapMessage.getSoapHeader();

    var unauthHeader = new OGHeader();
    unauthHeader.setTransactionID(TRANSACTION_ID);

    var timeStamp = new GregorianCalendar();
    timeStamp.setTime(new Date());

    try {
      unauthHeader
          .setTimeStamp(DatatypeFactory.newInstance().newXMLGregorianCalendar(timeStamp));
      unauthHeader.setPrimaryLangID(PRIMARY_LANGUAGE_ID);
    } catch (Exception e) {
    }

    var origin = new EndPoint();
    origin.setEntityID(ORIGIN_ENTITY_ID);
    origin.setSystemType(ORIGIN_SYSTEM_TYPE);
    unauthHeader.setOrigin(origin);

    var destination = new EndPoint();
    destination.setEntityID(DESTINATION_ENTITY_ID);
    destination.setSystemType(DESTINATION_SYSTEM_ID);
    unauthHeader.setDestination(destination);

    try {
      JAXBContext context = JAXBContext.newInstance(OGHeader.class);
      var marshaller = context.createMarshaller();
      marshaller.marshal(new ObjectFactory().createOGHeader(unauthHeader), soapHeader.getResult());
    } catch (Exception e) {
      System.err.println("exception occurred " + e.getMessage());
    }
  }
  
}
728x90
댓글