웹 서비스 호출

본 장에서는 웹 서비스 호출 방식에 대해서 알아보고 예제를 통해 실제 구현 방식을 설명한다.

1. 개요

하나의 JAX-WS 웹 서비스 클라이언트 애플리케이션은 원격의 웹 서비스 Endpoint에 다음의 2가지 방식으로 접근할 수 있다.

2. 동적 프록시 방식의 웹 서비스 호출

동적 프록시(Dynamic Proxy) 방식의 웹 서비스 호출은 크게 Java SE 방식과 Jakarta EE 방식으로 나누어진다. 이 2가지 방식은 모두 공통적으로 Client Artifact들을 생성하는 것을 전제로 하고 있다.

본 절에서는 이러한 Client Artifact들의 생성 방법에 대해 설명하고, Java SE 방식 그리고 Jakarta EE 방식의 웹 서비스 호출에 대해 자세히 설명한다.

2.1. Client Artifact 생성

웹 서비스의 클라이언트 호출은 그 서비스가 Java 클래스로부터 생성되든 WSDL 파일로부터 생성되든 결국 그 서비스가 Publish하는 WSDL 파일을 이용하여 Client Artifact들을 생성한 후 호출하는 Java 클래스의 작성을 통해 이루어진다. 따라서 본 절에서는 간단히 앞 장에서 Java 클래스를 통해 생성한 웹 서비스를 호출하는 클라이언트를 위한 Client Artifact들을 생성해 본다.

웹 서비스의 Client Artifact(프록시)들은 그 서비스가 제공하는 WSDL로부터 JEUS 9 웹 서비스에서 제공하는 툴인 wsimport를 통해 얻어진 서비스 인터페이스 클래스들과 서비스 Endpoint 인터페이스 클래스로 이루어진다.

콘솔에서 wsimport를 이용해서 Client Artifact들을 얻는 방법은 다음과 같다.

//db-font-size 88% $ >wsimport -help
Usage: wsimport [options] <WSDL_URI>

where [options] include:
  -b <path>                 specify jaxws/jaxb binding files or additional schemas
                            (Each <path> must have its own -b)
  -B<jaxbOption>            Pass this option to JAXB schema compiler
  -catalog <file>           specify catalog file to resolve external
                            entity references supports TR9401,
                            XCatalog, and OASIS XML Catalog format.
  -d <directory>            specify where to place generated output files
  -extension                allow vendor extensions - functionality
                            not specified by the specification. Use
                            of extensions may result in applications
                            that are not portable or may not
                            interoperate with other implementations
  -help                     display help
  -httpproxy:<host>:<port>  specify a HTTP proxy server (port defaults to 8080)
  -keep                     keep generated files
  -p <pkg>                  specifies the target package
  -quiet                    suppress wsimport output
  -s <directory>            specify where to place generated source files
  -target <version>         generate code as per the given JAXWS spec version
                            e.g. 2.0 will generate compliant code for JAXWS 2.0 spec
  -verbose                  output messages about what the compiler is doing
  -version                  print version information
  -wsdllocation <location>  @WebServiceClient.wsdlLocation value

Examples:
  wsimport stock.wsdl -b stock.xml -b stock.xjb
  wsimport -d generated http://example.org/stock?wsdl

JEUS 9 웹 서비스는 wsimport의 Ant Task도 지원하는데 보다 자세한 콘솔 명령어에 대한 설명과 Ant Task 항목에 대한 설명은 JEUS Reference 안내서의 wsimport 콘솔 툴과 wsimport 플러그인의 내용을 참고한다.

다음과 같은 원격 웹 서비스의 WSDL 파일로 그 아래의 wsimport의 Ant Task를 사용하여 Portable Artifact들을 생성한다. (http://host:port/AddNumbers/AddNumbersImplService?wsdl)

<?xml version="1.0" encoding="UTF-8"?>
<definitions targetNamespace="http://server.fromjava/"
    name="AddNumbersImplService" xmlns:tns="http://server.fromjava/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns="http://schemas.xmlsoap.org/wsdl/">
    <types>
        <xsd:schema>
            <xsd:import namespace="http://server.fromjava/"
                schemaLocation="AddNumbersImplService_schema1.xsd" />
        </xsd:schema>
    </types>
    <message name="addNumbers">
        <part name="parameters" element="tns:addNumbers" />
    </message>
    <message name="addNumbersResponse">
        <part name="parameters" element="tns:addNumbersResponse" />
    </message>
    <portType name="AddNumbersImpl">
        <operation name="addNumbers">
            <input message="tns:addNumbers" />
            <output message="tns:addNumbersResponse" />
        </operation>
    </portType>
    <binding name="AddNumbersImplPortBinding" type="tns:AddNumbersImpl">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
            style="document" />
        <operation name="addNumbers">
            <soap:operation soapAction="" />
            <input>
                <soap:body use="literal" />
            </input>
            <output>
                <soap:body use="literal" />
            </output>
        </operation>
    </binding>
    <service name="AddNumbersImplService">
        <port name="AddNumbersImplPort"
            binding="tns:AddNumbersImplPortBinding">
            <soap:address location="REPLACE_WITH_ACTUAL_URL" />
        </port>
    </service>
</definitions>
Client Artifact 생성 : <build.xml>
...
<target name="build_client" depends="do-deploy-success, init">
    <antcall target="wsimport">
        <param name="package.name" value="fromjava.client" />
        <param name="binding.file" value="" />
        <param name="wsdl.file"
            value="http://localhost:8088/AddNumbers/AddNumbersImplService?wsdl" />
    </antcall>
    <antcall target="do-compile">
        <param name="javac.excludes" value="fromjava/server/" />
    </antcall>
</target>
...

위의 wsimport Ant Task를 가지고 호출할 웹 서비스의 WSDL 파일을 사용하여 Portable Artifact들을 생성하면 그 결과로 생성되는 파일들은 다음과 같다.

fromwsdl/client/AddNumbers.class
fromwsdl/client/AddNumbersImpl.class
fromwsdl/client/AddNumbersImplService.class
fromwsdl/client/AddNumbersResponse.class
fromwsdl/client/ObjectFactory.class
fromwsdl/client/package-info.class

AddNumbers와 AddNumbersResponse 파일들은 각각 JAXB가 addNumbers 메소드에 대한 요청과 응답을 Java 오브젝트로 혹은 XML 문서로 변환하기 위해 사용하는 Java Bean이다.

AddNumbersImpl 파일은 서비스 Endpoint 인터페이스를 나타내며 AddNumbersImplService 파일은 클라이언트에서 프록시의 용도로 사용하는 서비스 인터페이스의 Java 클래스이다. 그리고 나머지 ObjectFactory 클래스와 package-info 클래스는 JAXB가 생성한 파일들이다.

다음은 wsimport 툴을 사용하여 위의 원격 웹 서비스의 WSDL 파일로부터 얻은 서비스 인터페이스 클래스인 AddNumbersImplService 클래스이다.

서비스 인터페이스 클래스 : <AddNumbersImplService.java>
//db-font-size 87% @WebServiceClient(name = "AddNumbersImplService",
    targetNamespace = "http://server.fromjava/",
    wsdlLocation = "http://localhost:8088/AddNumbers/AddNumbersImplService?wsdl")
public class AddNumbersImplService extends Service {

    private final static URL ADDNUMBERSIMPLSERVICE_WSDL_LOCATION;
    private final static WebServiceException ADDNUMBERSIMPLSERVICE_EXCEPTION;
    private final static QName ADDNUMBERSIMPLSERVICE_QNAME = new QName("http://server.fromjava/", "AddNumbersImplService");

    static {
        URL url = null;
        WebServiceException e = null;
        try {
            url = new URL(
                "http://localhost:8088/AddNumbers/AddNumbersImplService?wsdl");
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        ADDNUMBERSIMPLSERVICE_WSDL_LOCATION = url;
        ADDNUMBERSIMPLSERVICE_EXCEPTION = e;
    }

    public AddNumbersImplService() {
        super(__getWsdlLocation(), ADDNUMBERSIMPLSERVICE_QNAME);
    }

    public AddNumbersImplService(WebServiceFeature... features) {
        super(__getWsdlLocation(), ADDNUMBERSIMPLSERVICE_QNAME, features);
    }

    public AddNumbersImplService(URL wsdlLocation) {
        super(wsdlLocation, ADDNUMBERSIMPLSERVICE_QNAME);
    }

    public AddNumbersImplService(URL wsdlLocation, WebServiceFeature... features) {
        super(wsdlLocation, ADDNUMBERSIMPLSERVICE_QNAME, features);
    }

    public AddNumbersImplService(URL wsdlLocation, QName serviceName) {
        super(wsdlLocation, serviceName);
    }

    public AddNumbersImplService(URL wsdlLocation, QName serviceName,
                                 WebServiceFeature... features) {
        super(wsdlLocation, serviceName, features);
    }

    @WebEndpoint(name = "AddNumbersImplPort")
    public AddNumbersImpl getAddNumbersImplPort() {
     return super.getPort(new QName("http://server.fromjava/", "AddNumbersImplPort"),
                             AddNumbersImpl.class);
    }

    @WebEndpoint(name = "AddNumbersImplPort")
    public AddNumbersImpl getAddNumbersImplPort(WebServiceFeature... features) {
     return super.getPort(new QName("http://server.fromjava/", "AddNumbersImplPort"),
                            AddNumbersImpl.class, features);
    }

    private static URL __getWsdlLocation() {
        if (ADDNUMBERSIMPLSERVICE_EXCEPTION!= null) {
            throw ADDNUMBERSIMPLSERVICE_EXCEPTION;
        }
        return ADDNUMBERSIMPLSERVICE_WSDL_LOCATION;
    }
}

위의 서비스 인터페이스는 클라이언트에서 실제 프록시 객체를 얻을 때 사용한다.

클라이언트에서는 위의 클래스에서 AddNumbersImplService 생성자를 통해 얻은 AddNumbersImplService 객체로부터 getAddNumbersImplPort() 메소드를 통해 서비스 Endpoint 인터페이스인 AddNumbersImpl을 얻을 수 있다.

다음은 wsimport 툴을 사용하여 위의 원격 웹 서비스의 WSDL 파일로부터 얻은 서비스 Endpoint 인터페이스인 AddNumbersImpl 클래스이다.

서비스 Endpoint 인터페이스 : <AddNumbersImpl.java>
@WebService(name = "AddNumbersImpl", targetNamespace = "http://server.fromjava/")
@XmlSeeAlso( { ObjectFactory.class })
public interface AddNumbersImpl {

    @WebMethod
    @WebResult(targetNamespace = "")
    @RequestWrapper(localName = "addNumbers",
        targetNamespace = "http://server.fromjava/",
        className = "fromjava.client.AddNumbers")
    @ResponseWrapper(localName = "addNumbersResponse",
        targetNamespace = "http://server.fromjava/",
        className = "fromjava.client.AddNumbersResponse")
    public int addNumbers(@WebParam(name = "arg0", targetNamespace = "")
    int arg0, @WebParam(name = "arg1", targetNamespace = "")
    int arg1);
}

위와 같이 생성된 서비스 Endpoint 인터페이스는 Dynamic Binding 혹은 런타임에 동작하는 Java 오브젝트로 혹은 XML 문서로의 변환을 위한 Annotation들을 가지고 있다(이 서비스 Endpoint 인터페이스는 다시 WSDL과 스키마 파일들을 생성하는 데 사용될 수 있으나 이 서비스 Endpoint 인터페이스를 얻기 위해 사용된 WSDL이나 스키마 파일과 일치하지 않을 수 있다).

JAXB가 addNumbers 메소드에 대한 요청과 응답을 Java 오브젝트로 혹은 XML 문서로 변환하기 위해 사용하는 Java Bean 클래스인 AddNumbers 클래스와 AddNumbersResponse 클래스는 다음과 같다.

Java Bean 클래스 : <AddNumbers.java>
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "addNumbers", propOrder = { "arg0", "arg1" })
public class AddNumbers {

    protected int arg0;
    protected int arg1;

    public int getArg0() {
        return arg0;
    }

    public void setArg0(int value) {
        this.arg0 = value;
    }

    public int getArg1() {
        return arg1;
    }

    public void setArg1(int value) {
        this.arg1 = value;
    }
}
Java Bean 클래스 : <AddNumbersResponse.java>
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "addNumbersResponse", propOrder = { "_return" })
public class AddNumbersResponse {

    @XmlElement(name = "return")
    protected int _return;

    public int getReturn() {
        return _return;
    }

    public void setReturn(int value) {
        this._return = value;
    }
}

위의 2개의 Java Bean 클래스는 원격 웹 서비스의 WSDL 파일의 message element들인 AddNumbers, AddNumbersResponse에 해당하는 것을 알 수 있다.

2.2. Java SE 클라이언트 방식

본 절에서는 Client Artifact 생성에서 얻은 클라이언트 Portable Artifact들을 가지고 실제로 원격의 웹 서비스를 호출하는 로직을 담고 있는 클라이언트 클래스를 Java SE 방식으로 작성하고 호출해 본다.

2.2.1. Java SE 클라이언트 프로그램 작성

Java SE 클라이언트 클래스를 작성하는 것은 매우 간단하다. 위에서 얻은 서비스 인터페이스인 AddNumbersImplService 객체를 하나 생성하고 그로부터 서비스 Endpoint 인터페이스인 AddNumbersImpl을 구현한 객체를 얻는다. 실제로 이 객체는 직접 동적 프록시를 통해 원격의 웹 서비스를 호출하는 로직을 담고 있다. 이렇게 서비스 Endpoint 인터페이스를 구현하는 객체를 얻었으면 이로부터 실제의 웹 서비스를 호출하는 메소드를 호출한다.

다음은 위의 wsimport Ant Task를 통해 얻은 Portable Artifact들을 가지고 원격의 웹 서비스를 호출하는 코드의 일부분이다.

Java SE 클라이언트 프로그램 : <AddNumbersClient.java>
public class AddNumbersClient {

    public static void main(String[] args) {
       AddNumbersImpl port = new AddNumbersImplService().getAddNumbersImplPort();

       int number1 = 10;
       int number2 = 20;

       System.out.println("##############################################");
       System.out.println("### JAX-WS Webservices examples - fromjava ###");
       System.out.println("##############################################");
       System.out.println("Invoking addNumbers(" + number1 + ", " + number2 + ")");
       int result = port.addNumbers(number1, number2);
       System.out.println("Result: " + result);
    }
}

2.2.2. Java SE 클라이언트 프로그램 호출

본 절에서는 지금까지 구현한 클래스들 및 기타 설정 파일들을 사용하여 웹 서비스를 구현하여 JEUS 9에 deploy한 후 클라이언트를 실행하는 방법을 설명한다.

원격의 웹 서비스에 접근하는 클라이언트 프로그램의 호출 방식은 그 서비스가 Java로부터 구현하거나 WSDL로부터 구현하거나 동일하다.

다음과 같이 Java로부터 구현한다면 fromjava 디렉터리에서 서비스를 생성하고, WSDL 파일로부터 구현한다면 fromwsdl 디렉터리에서 서비스를 생성하여 JEUS 9에 deploy한다.

$ ant build deploy

위의 과정이 모두 실행되어 서비스가 정상적으로 deploy되면, 클라이언트를 빌드한다. Java SE 클라이언트에서 wsimport의 과정을 거치므로 서비스의 deploy가 모두 완료되었을 때 클라이언트의 구성이 가능하다.

다음과 같이 클라이언트를 생성하고 서비스를 호출한다.

$ ant run
...

run:
     [java] ##################################################
     [java] ##### JAX-WS Webservices examples - fromjava #####
     [java] ##################################################
     [java] Invoking addNumbers(10, 20)
     [java] Result: 30

...

BUILD SUCCESSFUL

호출 결과로 클라이언트가 정상적으로 이 서비스를 호출하고 원하는 값을 얻었음을 알 수 있다.

2.3. Jakarta EE 클라이언트 방식

본 절에서는 간단히 앞 장에서 WSDL 문서를 통해 생성한 웹 서비스를 호출하는 클라이언트를 Jakarta EE 방식으로 작성하고 호출한다. 시작하기 전에 우선 Client Artifact 생성에서 설명한 방식으로 클라이언트 Portable Artifact들을 얻은 것을 가정한다.

2.3.1. Jakarta EE 클라이언트 프로그램 작성

Jakarta EE 클라이언트 방식에서 중요한 점은 기존 JAX-RPC 환경의 Jakarta EE 웹 서비스 클라이언트 구성에서는 서블릿일 경우 web.xml에 EJB일 경우에는 ejb-jar.xml에 <service-ref>를 추가하여 웹 서비스 클라이언트 프록시 생성에 필요한 정보를 JNDI에 등록할 수 있었으나, JAX-WS 웹 서비스 환경에서의 Jakarta EE 클라이언트 구성은 @WebServiceRef Annotation을 설정함으로써 그와 동일한 작업이 가능하다는 것이다.

다음은 @WebServiceRef Annotation을 설정하는 하나의 예이다.

...
@WebServiceRef(wsdlLocation="http://host:port/TmaxService/TmaxService?wsdl)
static TmaxServiceImplService tsvc;
...

위에서 얻은 서비스 인터페이스인 AddNumbersService 타입을 이용하여 클라이언트 Java 클래스의 멤버 변수로 @WebServiceRef Annotation과 함께 선언한다. 그러면 이 멤버 변수는 런타임 중 클라이언트 클래스가 초기화될 때 실제 set 메소드가 없어도 서블릿일 경우 서블릿 컨테이너, EJB일 경우 EJB 컨테이너에서 자동으로 이 값을 Injection한다.

이로부터 서비스 Endpoint 인터페이스인 AddNumbersPortType을 구현한 객체를 얻는다. 실제로 이 객체는 직접 동적 프록시를 통해 원격의 웹 서비스를 호출하는 로직을 담고 있다. 이렇게 서비스 Endpoint 인터페이스를 구현하는 객체를 얻었으면, 실제의 웹 서비스를 호출하는 메소드를 호출한다.

다음은 위의 wsimport Ant Task를 통해 얻은 Portable Artifact들을 가지고 원격의 웹 서비스를 호출하는 클라이언트 프로그램이다.

Jakarta EE 클라이언트 : <AddNumbersClient.java>
public class AddNumbersClient extends HttpServlet {

    @WebServiceRef
    static AddNumbersImplService svc;

    protected void doGet(HttpServletRequest arg0, HttpServletResponse arg1)
            throws ServletException, IOException {
       AddNumbersImpl port = svc.getAddNumbersImplPort();

       int number1 = 10;
       int number2 = 20;

       System.out.println("##############################################");
       System.out.println("### JAX-WS Webservices examples - fromjava ###");
       System.out.println("##############################################");
       System.out.println("Invoking addNumbers(" + number1 + ", " + number2 + ")");
       int result = port.addNumbers(number1, number2);
       System.out.println("##############################################");
       System.out.println("### JAX-WS Webservices examples - fromjava ###");
       System.out.println("##############################################");
       System.out.println("Result: " + result);
    }
}

2.3.2. Jakarta EE 클라이언트 프로그램 호출

지금까지 구현한 클래스들 및 기타 설정 파일들을 사용하여 웹 서비스를 구현하여 JEUS 9에 deploy한 후 Jakarta EE 클라이언트를 실행할 수 있다. 원격 웹 서비스에 접근하는 클라이언트 프로그램의 호출 방식은 그 서비스가 Java로부터 구현하거나, WSDL로부터 구현하거나 동일하다.

다음과 같이 Java로부터 구현한다면 fromjava 디렉터리에서 서비스를 생성하고, WSDL 파일로부터 구현한다면 fromwsdl 디렉터리에서 서비스를 생성하여 JEUS 9에 deploy한다.

$ ant build deploy

위의 과정이 모두 실행되어 서비스가 정상적으로 deploy되면, Jakarta EE 클라이언트를 빌드한다. Jakarta EE 클라이언트에서 wsimport의 과정을 거치므로 서비스의 deploy가 모두 완료되었을 때 클라이언트의 구성이 가능하다.

다음과 같이 클라이언트를 생성하고 호출한다.

$ ant run
...

##############################################
### JAX-WS Webservices examples - fromjava ###
##############################################
Invoking addNumbers(10, 20)

...

##############################################
### JAX-WS Webservices examples - fromjava ###
##############################################
Result: 30

...

위와 같이 콘솔에서 입력하면 웹 브라우저를 통해 클라이언트 서블릿이 정상적으로 이 서비스를 호출하고 원하는 값을 서버의 로그를 통해 알 수 있다.

3. 디스패치 방식의 웹 서비스 호출

디스패치(Dispatch) 방식의 웹 서비스 호출은 XML 구성을 java.lang.transform.Source 또는 jakarta.xml.soap.SOAPMessage, 즉 XML 수준에서 다루는 것을 선호하는 개발자들을 위한 것이다. 디스패치 방식의 웹 서비스 호출은 메시지 모드와 페이로드(Payload) 모드로 사용될 수 있고 XML/HTTP 바인딩(jakarta.activation.DataSource)으로 레스트(REST) 웹 서비스를 생성할 때 사용될 수 있다. 이에 대한 자세한 내용은 프로바이더와 디스패치 인터페이스를 참고한다.