SOAP 사양은 데이터베이스, 프로그래밍 언어 (예 : 자바 프로그래밍 언어) 및 데이터 리파지토리에서 발견되는 일반적인
유형들을 표시하기 위한 인코딩을 명시한다. 아파치 SOAP의 툴킷은 자바 유형들을 직렬화된 XML 표현으로 매핑시키는 작업을
수행하는 클래스인 기초적인 (비)직렬자 세트를 제공함으로써 인코딩을 지원한다. Gavin Bong은 툴킷에서 제공되는
것이 요구사항과 맞지 않을 때 직접 (비)직렬자를 작성하는 방법을 보여준다. 예제 애플리케이션도
제공한다.
Part
1에서 SOAP이 데이터 유형을 XML에 어떻게 매핑시키는지를 보았고, 아파치 SOAP 툴킷에 포함되어 있는 직렬자와 비직렬자
(지금부터는 집합적으로 (비)직렬자라고 칭하겠다)를 어떻게 사용하는지를 배웠다. 이번에는 (비)직렬자 작성 방법을 보여줄 설명서를
살펴보겠다. Part
1의 "유형 매핑 패턴" 섹션을 다시 읽어보기 바란다.
아파치 SOAP 툴킷과 함께 제공되는 (비)직렬자 중 여러분의 자바 클래스에 맞는 것이 없을 경우 스스로 사용자 정의 방식의 (비)직렬자를 작성해야 한다. 먼저 소위 루트 (비)직렬자와 일반 (비)직렬자를 구분해야 한다. RPC 매개
변수와 응답들에 대한 직렬화와 비직렬화의 초기 부트스트래핑은 루트 (비)직렬자에의해 처리된다. 아파치 SOAP에 있는 세 가지 루트 (비)직렬자가 표
1에 나와 있다.
적절한 루트 (비)직렬자는 두 가지 사항에 기반하여 처리된다. encodingStyle과
Parameter 클래스 (직렬화용), 혹은 QName
SOAP-ENV:Parameter (비직렬화용)이 그것이다. 실제 처리가 어떻게 일어나는지 알려면 클라이언트
측에서 자바 유형의 직렬화로 나아가는 연속적인 이벤트들을 이해해야 한다.
3행에서 RPCConstants.Q_ELEM_PARAMETER는 SOAP-ENV:Parmeter로 바뀐다. 바로 이 3행에서 unmarshall() 메쏘드가 불리워질 때 루트 비직렬자로의 배치가 이루어진다.
루트 (비) 직렬자는 다음 일반 (비)직렬자가 호출되도록 유형 매핑 레지스트리를 차례로
조회할 것이다. 이 호출 스택은 (직렬화 과정 동안) 레지스트리가 원시 자바 유형에 대한 (비)직렬자를 반환할 때나 (비직렬화 과정
동안) XML element가 간단한 유형에 대한 순수한 컨테이너일 때만 해결되기 시작할 것이다.
제공되는 일반 (비)직렬자의 대부분은 -org.apache.soap.encoding.soapenc
패키지에서 발견될 수 있는- 아파치 SOAP 내의 대부분의 헬퍼 클래스가 그러하듯이 Section 5 인코딩에서 작동한다. 이것은 왜
내가 이 설명서의 포커스를 Section 5 인코딩 (비)직렬자에만 맞추는지의 이유이다.
inScopeEncStyle: 둘러싸고 있는 Call이나
Response 객체에 지정된대로 encodingStyleURI를 표현한다.
javaType: 직렬화되어야 하는 객체의 런타임 유형이다.
src: 직렬화될 자바 객체에 대한 참조이다.
context: 접근자 이름을 표시하는 String이다. 이
직렬자가 ParameterSerializer에 의해 호출되면 이
문맥값은 Parameter 클래스 (SOAP 클라이언트에 선언된) 내의 명명된 특성값과 일치하거나 만일
이것이 SOAP 서버라면 반환을 위해 하드 코딩된다. 이것은 null 값이어서는 안 된다.
sink: SOAP XML 인스턴스가 작성될 목적지 싱크
nsStack: 현재 범위 내에 있는 다수의 이름공간 선언을 구현하는 데이터 구조
xjmr: 자바 유형에 기반하여 다음에 여러분이 사용할 직렬자를 조회하기 위해 사용하는
smr이다. [여러분은 또한 javaType과
encodingStyle에 기반하여 다른 직렬자에 위임할 xjmr 배열 메쏘드를 호출할 것이다. 예를 들어
Hashtable이나 Vector와 같은 복합 구조를 위해 이를 수행한다.
ctx: 서블릿 문맥으로부터 나오는
javax.servlet.http.HttpServletRequest과
javax.servlet.http.HttpSession 같은 것을 전달하는데 사용된다.
이 코드는 매핑된 QName for javaType을 검색하기 위해
queryElementType을 호출할 것이다.
<context xsi:type="QName">
객체 인자인 src가 간단한 유형이라면, 두번째 단계인 객체 값 직렬화하기는
src.toString() 메쏘드를 호출하고 이를 작성하는 간단한 작업이다. 그렇지 않다면, 객체의 구성
요소들을 확인하고 이들을 보다 원시적인 직렬자로 개별적으로 전달해야 한다. 내장된 직렬자들의 소스를 검토해보면 구성 요소들이 여러
방식으로 확인될 수 있음을 알게 될 것이다.
Java reflection (예 : BeanSerializer)
List 데이터 구조 반복 적용하기 (예 : ArraySerializer)
사전에 알고 있는 클래스를 통한 직접적인 접근 (즉 여러분은 여러분의 직렬자가 하나의 특정 클래스에 대해서만 작동한다는
것을 사전에 알고 있다.)
여기에서, componentType과 componentValue는 각각 원래의
src 매개변수의 구성 요소들에 대한 런타임 유형과 객체 참조를 표시한다.
marshall() 메쏘드는 연관된 직렬자를 검색하기 위해 실제로
querySerializer을 호출하고 뒤이어 연관된 직렬자의 marshall()
메쏘드를 호출한다. 분명히, 이것은 여러분이 유형 매핑 레지스트리의 모든 컴포넌트에 대해 직렬자들을 등록했을 때만 작동한다.
마지막 단계인 element 종료하기는 간단히 접근자에 대한 종료 태그를 작성함으로써 완료된다.
sink.write("</" + context + '>');
마지막으로, 현재의 이름공간 범위를 남겨두고 마무리해야 한다.
nsStack.popScope();
비직렬자 설명서
비직렬자는 org.apache.soap.util.xml.Deserializer을 구현하고 하나의 메쏘드를
실현한다.
unmarshall() 메쏘드의 목적은 매개변수를 자바 객체로 재구축하는 것이다. 이를 위해
src DOM 노드에 포함된 XML 부분을 처리해야 한다. 이를 수행하기 위해 선호되는 프로그래밍 모델은
org.apache.soap.utils.xml.DOMUtils 내의 DOM wrapper 메쏘드를 유형 매핑
레지스트리와 연계하여 사용하는 것이다. 일반적으로, 비직렬화의 DOMUtils는 직렬화의
SoapEncUtils에 필적하는 것이다.
src에 들어 있는 XML은 다중 참조 값의 제약을 받지 않도록 보증된다는 점에 주의하는 것이 중요하다.
모든 다중 참조 hrefs는 루트 비직렬자인 ParameterSerializer에 의해
실제 값으로 다시 바뀐다.
자바 객체 재구축 절차는 데이터 유형이 속하는 카테고리에 따라 다양하다. ( Part
1 참조)
간단한 유형
여러분이 간단한 유형을 비직렬화하고 있다면, 반환될 객체를 초기화하기 위해 이를 사용하기 전에 먼저
DOMUtils.getChildCharacterData(Element)을 사용하여
src의 문자열 값을 검색하고 선택적으로 이를 사전 처리 (예 : "NaN" 문자열을
FloatDeserializer 내의 Float.NaN에 매핑시킴)해야 한다.
복합 유형
복합 유형은 두 개의 주요 카테고리로 나뉜다. 첫번째는 같은 구조로 된 반복되는 element를 가진 유형으로 구성된다.
예컨대, java.util.List와 java.util.Map을 구현하는 자바 배열이나
클래스가 여기에 포함된다. 다른 카테고리는 임의적인 구조를 보이는 다른 모든 자바 클래스를 나타낸다. 그러면 비직렬화 절차는 결국
관련된 자손 element를 확인하기 위해 XML 구조를 항해하고 뒤이어 보다 원시적인 비직렬자로 직렬화 책임을 위임하는 것이
되는데, 이것은 다음과 같다.
DOM 항해하기 여러분이 첫번째 카테고리에 속하는 복합 유형을 다루고 있다면 반복되는 모든 멤버들 간을
항해하기 위해 DOMUtils.getFirstChildElement()과
DOMUtils.getNextSiblingElement()를 사용할 수 있다. 그렇지 않다면 멤버 특성을
표현하는elements를 확인하기 위해 DOM API를 사용한다.
xjmr.unmarshall은 내부적으로 queryDeserializer를
호출한 후 반환된 비직렬자에 unmarshall을 호출한다. 위의 두 단계들은 비직렬화를
ParameterSerializer로 위임함으로써 하나로 합쳐지는 것이 낫다. 이는 xsi:type 속성이
빠진 상황에서 QName {""}/X (X는 루트 element의 tagName)에 설정된
soapType과 함께 xjmr.unmarshall()을 호출하고 싶기 때문에 수행된다. 이를 수행하기 위한 코드는 이미
ParameterSerializer.unmarshall() 내에 편리하게 들어 있기 때문에 이 절차는
다음과 같이 단축된 형태로 된다.
Bean 클래스는 런타임 유형과 실제 반환되는 인스턴스를 캡슐화한다. 비직렬자는 대부분의 경우 특정
클래스에 맞게 만들어졌기 때문에 어떤 클래스를 반환해야 하는지를 알고 있다. BeanSerializer와
ArraySerializer 같은 일반 비직렬자에 대해서는 유형 매핑 내의
javaType 속성이 반환되어야 하는 유형을 전달한다.
return(new Bean(Foo.class, foo));
루트 (비)직렬자 등록하기
여러분이 정의한 encodingStyles을 도입하려고 한다면 루트 (비)직렬자를 작성해야 한다고 이미
언급했다. 루트 (비)직렬자는 한 가지 작은 차이를 제외하고는 일반 (비)직렬자와 같은 방식으로 구현된다. 모든 (비)직렬자는 특별히
지정된 QName과 자바 유형으로 유형 매핑 레지스트리에 등록되는데, 이들은 아파치 SOAP에게
encodingStyles 특성에 기반하여 (비)직렬화 절차를 시동하라고 지시한다. 아래의 샘플 코드에서
강조된 값을 기록해 두었다가 루트 (비)직렬자를 등록할 떄 사용해야 한다.
이 섹션에서 나는 복잡한 유형을 (비)직렬화하기 위한 BeanSerializer에 대한 대체 솔루션을
소개할 것이다. 내가 스키마의 제한을 받는 SOAP이라고 부르는 이 기법은 PRC 매개변수의 literal XML 구조를
기술하기 위해 XML 스키마를 사용한다. 여기에서 우리는 클라이언트와 서버측의 데이터 모델에 대해서는 개의치 않고 메시지 포맷에 대해
엄격하게 상호운용하기로 동의한다. 혼란을 피하기 위해, PRC 호출은 여전히 Section 5를 사용해 인코딩되지만 매개변수는 그렇지
않다는 점에 주의해야 한다.
예제 애플리케이션을 사용해 이 기법을 보여 주겠다. 참고자료에서
전체 코드를 다운로드받을 수 있다. 클라이언트는 웹 서비스에게 구매 주문을 보내고, 서비스는 승인 문자열로 응답한다. 웹 서비스가
엑스포트한 메쏘드 서명은 다음과 같다.
public String eatPo (PurchaseOrder p);
이 기법이 작동하도록 하려면 XML/객체 데이터 바인딩 프레임워크가 필요하다. 이 예제에 Exolab의 Castor 툴킷을
사용했다. (참고자료)
이 기법의 단계들은 다음과 같다.
PurchaseOrder에 대한 XML 포맷에 합의한다.
Castor를 사용하여 자바 클래스를 생성한다.
사용자 정의 (비)직렬자를 작성한다.
클라이언트와 서버 부분에 대한 유형 매핑을 작성한다.
1단계 :
PurchaseOrder에 대한 XML 포맷에 합의하기
설명을 간단하게 하기 위해 PurchaseOrder 스키마에서 주문 상세 섹션을 없앴다. 또한
PONumber 속성은 이 스키마가 Section 5 인코딩을 따르지 않도록 만든다는 점에 주의한다.
SourceGenerator 툴은 최신 스키마 이름공간만 인식한다. --
http://www.w3.org/2001/XMLSchema
다음에 자바 클래스 세트를 컴파일한다. 생성된 파일이 SAX 1.0 API를 사용함에 따라 여러분은
-deprecation 옵션을 사용해야 할 것임에 주의한다. 이러한 수작업 컴파일을 피하기 위해 Exolab은
이를 자동화하기 위한 Ant taskdef에 대해 작업하고 있다.
3단계 : 사용자 정의 (비)직렬자 작성하기
여러분은 이제 PurchaseOrderSerializer 내의 (비)직렬화 메쏘드를 구현하는데, 이는
PurchaseOrder 클래스가 나타내는 대응 메쏘드를 활용함으로써 수행된다. 직렬화를 위해
PurchaseOrder는 java.io.Writer나
org.xml.sax.DocumentHandler로 배열될 수 있다. Listing
1에 나와 있듯이 여러분은 직렬화를 PurchaseOrder's
marshal() 메쏘드에 위임한다. (주의사항: marshal()에 의해 생성된
XML 스트림은 XML 프롤로그를 포함하고 있다.) PurchaseOrderSerializer는
sink를 java.io.FilterWriter인
FilterXmlProlog로 포장함으로써 이 프롤로그를 삭제한다. Listing
2는 비직렬화 과정 동안 발행할 수 있는 몇 가지 예외 상황을 포함하고 있다.
(b1) Null PO.
---------------------------
<po
xmlns:ns2="urn:raverun"
xsi:type="ns2:po"
xsi:null="true"/>
---------------------------
(b2) Non null but nothing submitted in the body.
---------------------------
<po
xmlns:ns2="urn:raverun"
xsi:type="ns2:po" />
---------------------------
(b3) PO that violates the schema.
---------------------------
<po
xmlns:ns2="urn:raverun"
xsi:type="ns2:po">
<foo bar="123"/>
</po>
---------------------------
4단계 : 클라이언트와 서버 부분에 대한 유형 매핑
작성하기
마지막으로 여러분이 정의한 (비)직렬자를 참조하기 위한 유형 매핑을 선언해야 한다. 여러분은
PurchaseOrder에 대한 인코딩으로 Section 5가 지정되어 있는 것을 보면 놀랄 것이다. 이렇게
하면 여러분이 비직렬화 절차를 시동하기 위해 ParameterSerializer를 사용할 수 있고 직렬화 코드
내의 SoapEncUtils도 사용할 수 있기 때문에 편의상 이렇게 지정한 것이다.
스키마의 제한을 받는 SOAP 예제를 검토할 때 여러분은 다음과 같은 사항들을 염두에 두어야 한다.
표준을 따르기 위해 여러분은 <po> element 내에 있는 Section 5 인코딩에
관해 어떤 클레임도 활성화시키지 말아야 한다. (표준에 보다 상응하는 SOAP XML 인스턴스에 대해서는 Listing
3 참조). SOAP 1.1 사양 (참고자료)은
이 요건을 다음과 같이 기술하고 있다.
길이가 0인 URI ("")의 값은 포함된 element의 인코딩 유형에 대해 어떤
클레임도 이루어지지 않았음을 명시적으로 나타낸다.
null 값을 가진 encodingStyle에 대한 대안은 여러분의 통신 요구에 맞게 만들어진 사용자
정의 encodingStyleURI를 도입하는 것이다.
Castor에는 조심해야 할 몇 가지 버그가 있지만, 모두 해결책을 가지고 있다. 여러분이 버전 0.9.3 이전의
Castor를 사용하고 있다면 스키마 유효성 확인이 기대처럼 작동하지 않는다. 여기에 대한 솔루션은 최신 출시판으로 업그레이드하는
것이다. 반면 Castor 0.9.3 (내가 사용하는 버전)은 표준 출력 스트림에 가짜 메시지를 생성한다. 내가 만난 메시지는
다음과 같다.
Warning : preserved is a bad entry for the whiteSpace value.
최신 버전인 Castor 0.9.3.9에서는 이 경고가 나타나지 않는다.
PurchaseOrderSerializer는 복수 참조 값에 대해 직렬화하지 않는다. 그러나 이들을 올바로
비직렬화할 것이다. 이것은 본질적으로 PurchaseOrderSerializer의 기능이 아니라
ParameterSerializer의 기능이다.
아파치 SOAP의 최신 공식 출시판 (버전 2.2)은 2001년 5월에 나왔다. 개발 포커스가 Axis (현재 베타 1 버전)로
옮겨갔지만, 버그 수정이 계속 추가되고 있다. 우리는 2.3 출시를 기다리고 있지만 (만약 있다면), 공식 출시판 사용자들은
코드기반, 특히 SOAPMappingRegistry 및 이와 관련된 클래스들에 주요 업그레이드가 있었음을
알아야 한다. 기존의 코드는 이 수정 사항들과 상호작용하기 위해 몇 가지 변경이 필요할 수 있다.
다음은 주목할만한 변경 사항들이다.
스키마 이름공간은 이제 2001 권고 이름 공간을 기본으로 참조한다. 버전 2.2는 1999 이름공간을 참조하였다.
결과적으로, 여러분이 인자가 없는 생성자로
SOAPMappingRegistry를 인스턴스화할 경우 2001 이름 공간을 인식하는 인스턴스가 반환된다.
SOAPMappingRegistry에 대한 인스턴스 생성이 정적인 팩토리 유형에 따라 재설계되었다.
따라서 여러분은 이제 과부하된 생성자인 SOAPMappingRegistry(schemaURI) 대신
getBaseRegistry(schemaURI) 팩토리 메쏘드를 사용해야 한다.
public static SOAPMappingRegistry getBaseRegistry (String schemaURI);
버전 2.2는 레지스트리들을 연결시키는 기능을 제공한다. 이 메쏘드들은 최근에 추가되었다.
public SOAPMappingRegistry(SOAPMappingRegistry parent);
public SOAPMappingRegistry(SOAPMappingRegistry parent, String schemaURI);
public SOAPMappingRegistry getParent()
public String getSchemaURI()
일치되는 것이 발견될 때까지 체인 전반에 걸쳐 유형
매핑이 전환이 이루어질 것이다.
DeploymentDescriptor 클래스는 유형 매핑 선언에서 qname
속성을 옵션으로 처리할 것이다.
결론
이 글에 나온 예제가 Part 1의 이론적 개념들을 명확하게 했기를 기대한다. 네트워크 상의 많은 머신에서 작동하는 웹
서비스들이 광범위하게 실현되면 개발자들은 프로그램화된 객체들이 한 머신에서 다른 머신으로 어떻게 전송되는지를 이해해야 한다.
SOAP의 유형 매핑 기능을 잘 이해하면 보다 나은 분산 애플리케이션과 서비스 개발에 도움이 될 것이다.