이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.
Part VI. 통합
레퍼런스 문서의 이번 부분에서는 스프링 프레임워크를 다수의 Java EE(혹은 관련된) 기술과 통합하는 내용을 다룬다.
- Chapter 20, 스프링을 사용한 원격작업(remoting) 및 웹 서비스
- Chapter 21, 엔터프라이즈 자바빈(EJB) 통합
- Chapter 22, JMS (Java Message Service)
- Chapter 23, JMX
- Chapter 24, JCA CCI
- Chapter 25, Email
- Chapter 26, Task Execution and Scheduling
- Chapter 27, Dynamic language support
- Chapter 28, Cache Abstraction
20. 스프링을 사용한 원격작업(remoting) 및 웹 서비스
20.1 소개
스프링은 다양한 기술을 사용해서 원격 지원에 대한 통합 클래스를 제공한다. 원격 지원은 일반적인 (스프링) POJO를 구현해서 원격이 가능한 서비스의 배포를 쉽게 한다. 현재 스프링은 다음의 원격 기술을 지원한다.
- 원격 메서드 호출 (RMI, Remote Method Invocation). RmiProxyFactoryBean와 RmiServiceExporter를 사용하더라도 스프링은 전통전이 두 RMI(java.rmi.Remote인터페이스와 java.rmi.RemoteException)와 RMI 인보커(어떤 자바 인터페이스로도)를 통한 투명한 원격작업을 지원한다.
- 스프링의 HTTP 인보커. 스프링은 모든 Java 인터페이스를 지원하면서(RMI 인보커처럼) HTTP를 통해 자바 직렬화를 할 수 있는 특수한 원격전략을 제공한다. 이에 대응되는 지원 클래스는 HttpInvokerProxyFactoryBean와 HttpInvokerServiceExporter이다.
- 해시안(Hessian). 스프링의 HessianProxyFactoryBean와 HessianServiceExporter를 사용해서 Caucho가 제공하는 HTTP기반의 경량 바이너리 프로토콜로 서비스를 투명하게 노출할 수 있다.
- 버랩(Burlap). 버랩은 Caucho이 XML 기반으로 만든 해시안의 대안이다. 스프링은 BurlapProxyFactoryBean와 BurlapServiceExporter같은 지원 클래스를 제공한다.
- JAX-RPC. 스프링은 JAX-RPC(J2EE 1.4의 웹 서비스 API)를 사용하는 웹 서비스의 원격 지원을 제공한다.
- JAX-WS. 스프링은 JAX-WS(JAX-RPC의 후계자로 Java EE 5와 Java 6에서 도입되었다.)를 사용하는 웹 서비스의 원격 지원을 제공한다.
- JMS. 의존 프로토콜로 JMS를 사용하는 원격은 JmsInvokerServiceExporter와 JmsInvokerProxyFactoryBean 클래스로 지원한다.
스프링의 원격 기능을 설명하면서 다음의 도메인 모델과 서비스를 사용할 것이다.
public class Account implements Serializable{
private String name;
public String getName(){
return name;
}
public void setName(String name) {
this.name = name;
}
}
public interface AccountService {
public void insertAccount(Account account);
public List<Account> getAccounts(String name);
}
public interface RemoteAccountService extends Remote {
public void insertAccount(Account account) throws RemoteException;
public List<Account> getAccounts(String name) throws RemoteException;
}
// 이 구현체는 지금은 아무것도 하지 않는다
public class AccountServiceImpl implements AccountService {
public void insertAccount(Account acc) {
// 어떤 작업을 한다...
}
public List<Account> getAccounts(String name) {
// 어떤 작업을 한다...
}
}
RMI를 사용해서 원격 클라이언트에 서비스를 노출할 것이고 RIM를 사용할 때의 단점을 약간 얘기할 것이다. 그 다음 해시안을 프로토콜로 사용하는 예제를 계속해서 볼 것이다.
20.2 RMI를 사용해서 서비스 노출하기
스프링의 RMI 지원을 사용해서 RMI 인프라를 통해 서비스를 투명하게 노출할 수 있다. 이 설정을 한 후 보안 컨텍스트 전파나 원격 트랜잭션 전파에 대한 지원이 없다는 점을 제외하면 원격 EJB와 유사한 설정을 가진다. RMI 인보커를 사용할 때 추가적인 호출 컨텍스트같은 훅(hook)을 스프링이 제공하므로 여기서 보안 프레임워크나 커스텀 보안 자격(credentials)등을 연결할 수 있다.
20.2.1 RmiServiceExporter를 사용해서 서비스 내보내기(export)
RmiServiceExporter를 사용해서 RMI 객체로 AccountService 객체의 인터페이스를 노출할 수 있다. 이 인터페이스는 RmiProxyFactoryBean으로 접근할 수 있고 전통적인 RMI 서비스인 경우에는 평범한 RMI로 접근할 수 있다. RmiServiceExporter는 RMI 인보커로 RMI가 아닌 서비스의 노출을 명시적으로 지원한다.
물론 서비스를 스프링 컨테이너에 먼저 설정해야 한다.
<bean id="accountService" class="example.AccountServiceImpl">
<!-- DAO 등 추가적인 프로퍼티 -->
</bean>
그 다음 RmiServiceExporter로 서비스를 노출해야 한다.
<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
<!-- 내보낼 빈과 같은 이름이어야 할 필요는 없다 -->
<property name="serviceName" value="AccountService"/>
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
<!-- 기본값은 1099 -->
<property name="registryPort" value="1199"/>
</bean>
여기서 볼 수 있듯이 RMI 등록(registry) 포트를 오버라이딩한다. 때로는 어플리케이션 서버도 RMI 등록을 유지하면서 RMI 등록에 간섭하지 않는다는 것을 알고 있다. 게다가 서비스명은 서비스 아래에 바인딩된다. 그러므로 서비스는 'rmi://HOST:1199/AccountService'에 바인딩 될 것이다. 나중에 클라이언트 측 서비스에서 링크에 URL을 사용할 것이다.
servicePort 프로퍼티를 생략했다.(기본값은 0이다) 이는 서비스와의 통신에 익명 포트를 사용한다는 의미이다.
20.2.2 클라이언트 서비스에서의 연결(link)
클라이언트는 account를 관리하는 AccountService를 사용하는 간단한 객체이다.
public class SimpleObject {
private AccountService accountService;
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
// accountService를 사용하는 추가적인 메서드
}
클라이언트의 서비스에서 연결하기 위해 간단한 객체와 약간의 서비스 연결 설정을 가진 분리된 스프링 컨테이너를 생성할 것이다.
<bean class="example.SimpleObject">
<property name="accountService" ref="accountService"/>
</bean>
<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl" value="rmi://HOST:1199/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
이것이 클라이언트에서 원격 account 서비스를 지원하는데 필요한 전부이다. 스프링은 투명하게 인보커를 생성하고 RmiServiceExporter로 원격으로 account 서비스를 활성화할 것이다. 클라이언트에서 RmiProxyFactoryBean를 사용해서 이를 연결한다.
20.3 HTTP를 통한 서비스 원격 호출에 해시안과 버랩 사용하기
해시안은 바이너리 HTTP 기반의 원격 프로토콜을 제공한다. 이 프로토콜은 Caucho이 개발했고 해시안의 자세한 내용은 http://www.caucho.com에서 볼 수 있다.
20.3.1 해시안등에 DispatcherServlet 연결하기
해시안은 HTTP로 통신하고 커스텀 서블릿을 사용한다. 스프링 웹 MVC에서 사용하듯이 스프링의 DispatcherServlet 원리를 사용해서 서비스를 노출하는 서비스등을 쉽게 연결할 수 있다. 우선 어플리케이션에 새로운 서블릿을 생성해야 한다.(이는 'web.xml'에서 가져온다.)
<bean id="accountService" class="example.AccountServiceImpl">
</bean>
<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
이제 클라이언트에서 서비스를 연결할 준비가 됐다. 서비스에 요청 URL을 매핑하는 핸들러 매핑을 명시적으로 지정하지 않았으므로 BeanNameUrlHandlerMapping을 사용할 것이다. 그러므로 DispatcherServlet의 매핑(위에서 정의했다)의 빈 이름을 나타내는 URL인 'http://HOST:8080/remoting/AccountService'로 서비스를 노출할 것이다.
아니면 루트 어플리케이션 컨텍스트(예를 들면 'WEB-INF/applicationContext.xml')에 HessianServiceExporter를 생성해라.
<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
후자의 경우 동일한 결과가 나오도록 해당 익스포터에 대응되는 서블릿을 'web.xml'에 정의해라. 익스포터는 요청경로 /remoting/AccountService에 매핑된다. 이 서블릿 이름은 대상 익스포터의 빈 이름과 일치해야 한다.
<servlet>
<servlet-name>accountExporter</servlet-name>
<servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>accountExporter</servlet-name>
<url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>
20.3.3 클라이언트측 서비스에서의 연결
HessianProxyFactoryBean를 사용해서 클라이언트측 서비스에서 연결할 수 있다. RMI 예제와 같은 원리를 적용한다. 분리된 빈 팩토리나 어플리케이션을 생성하고 account를 관리하려고 AccountService를 사용하는 SimpleObject인 다음 빈을 정의해라.
<bean class="example.SimpleObject">
<property name="accountService" ref="accountService"/>
</bean>
<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
<property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
20.3.4 버랩 사용
앞에서 설명한 해시안계열과 정확히 동일하게 설정하고 구성하므로 여기서 버랩(해시안과 동일하지만 XML 기반이다)에 대해서 자세히 얘기하지 않을 것이다. Hessian을 Burlap으로 바꾸면 설정이 완료된다.
20.3.5 해시안이나 버랩으로 노출된 서비스에 HTTP 기본 인증(HTTP basic authentication) 적용하기
해시안과 버랩의 장점 중 하나는 두 프로토콜이 모두 HTTP 기반이므로 HTTP 기본 인증을 쉽게 적용할 수 있다는 점이다. 예를 들어 web.xml 보안 기능으로 일반적인 HTTP 서버 보안 메카니즘을 쉽게 적용할 수 있다. 보통은 여기서 사용자마다 보안 인증서를 사용하기 보다는 Hessian/BurlapProxyFactoryBean 수준(JDBC DataSource와 유사하다)에서 정의되고 공유된 인증서를 사용할 것이다.
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors" ref="authorizationInterceptor"/>
</bean>
<bean id="authorizationInterceptor"
class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
<property name="authorizedRoles" value="administrator,operator"/>
</bean>
이는 BeanNameUrlHandlerMapping를 명시적으로 언급하고(mention) 이 어플리케이션 컨텍스트에서 언급된 빈을 운영자와 관리자만 호출하도록 하는 인터셉터를 설정하는 예제이다.
물론 이 예제는 유연한 인증 인프라을 보여주지는 않는다. 인증과 관련된 자세한 옵션은 의 스프링 시큐리티 프로젝트를 봐라.
20.4 HTTP 인보커를 사용해서 서비스 노출하기
자신만의 가벼운 직렬화 메카니즘을 사용하는 경량 프로토콜인 버랩이나 해시안과는 반대로 스프링 HTTP 인보커는 HTTP로 서비스를 노출할 때 표준 자바 직렬화 메카니즘을 사용한다. 인자나 반환 타입이 해시안이나 버랩이 사용하는 직렬화 메카니즘을 사용해서 직렬화할 수 없는 복잡한 타입이라면 이는 엄청난 장점이다.(원격 기술을 선택할 때의 고려사항은 다음 섹션에서 설명한다.)
내부적으로 스프링은 HTTP 호출에 J2SE가 제공하는 표준 기술이나 Commons HttpClient를 사용한다. 더 고급이고 사용하기 쉬운 기능이 필요하다면 후자를 사용해라. 자세한 내용은 jakarta.apache.org/commons/httpclient를 참고해라.
20.4.1 서비스 객체 노출하기
서비스 객체에 대한 HTTP 인보커 인프라를 설정하는 방법은 해시안이나 버랩으로 할 때와 아주 유사하다. 해시안 지원이 HessianServiceExporter를 제공하듯이 스프링의 HttpInvoker 지원은 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter를 제공한다.
스프링 웹 MVC DispatcherServlet에서 AccountService(위에서 나왔던)를 노출하려면 디스패처의 어플리케이션 컨텍스트에 다음 설정을 해야 한다.
<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
해시안부분에서 설명했듯이 이러한 익스포터(exporter) 정의를 DispatcherServlet의 표준 매핑 기능으로 노출할 것이다.
아니면 루트 어플리케이션 컨텍스트(예를 들면 'WEB-INF/applicationContext.xml'파일에)에 HttpInvokerServiceExporter를 생성해라.
<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
추가적으로 'web.xml'의 이 익스포터에 대한 서블릿(대상 익스포터의 빈 이름과 일치하는 서블릿 이름으로)을 정의해라.
<servlet>
<servlet-name>accountExporter</servlet-name>
<servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>accountExporter</servlet-name>
<url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>
서블릿 컨테이너 밖에서 돌아가고 Sun의 Java 6를 사용한다면 내장 HTTP 서버 구현체를 사용할 수 있다. 이 예제에서 보듯이 SimpleHttpServerFactoryBean를 SimpleHttpInvokerServiceExporter와 함게 설정할 수 있다.
<bean name="accountExporter"
class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
<property name="service" ref="accountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
<bean id="httpServer"
class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
<property name="contexts">
<util:map>
<entry key="/remoting/AccountService" value-ref="accountExporter"/>
</util:map>
</property>
<property name="port" value="8080" />
</bean>
20.4.2 클라이언트에서의 서비스 연결
클라이언트에서 서비스를 연결하는 방법은 헤시안이나 버랩에서 사용한 방법과 아주 유사하다. 스프링이 프록시를 사용해서 호출을 익스포트된 서비스를 가리키는 URL에 대한 HTTP POST 요청으로 변환할 수 있다.
<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/>
<property name="serviceInterface" value="example.AccountService"/>
</bean>
앞에서 얘기했듯이 사용하고자 하는 HTTP 클라이언트를 선택할 수 있다. 기본적으로 HttpInvokerProxy는 J2SE HTTP 기능을 사용하지만 httpInvokerRequestExecutor 프로퍼티를 설정해서 Commons HttpClient를 사용할 수도 있다.
<property name="httpInvokerRequestExecutor">
<bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>
20.5 웹 서비스
스프링은 표준 자바 웹서비스 API를 완전히 지원한다.
- JAX-RPC를 사용한 웹서비스 노출
- JAX-RPC를 사용한 웹서비스 접근
- JAX-WS를 사용한 웹서비스 노출
- JAX-WS를 사용한 웹서비스 접근
왜 두 개의 표준 자바 웹서비스 API가 있는가?
JAX-RPC 1.1는 J2EE 1.4의 표준 웹서비스 API이다. 이름에서 알 수 있듯이 수년전에 이미 인기가 없어진 RPC 바인딩에 집중하고 있다. 그 결과 바인딩은 더 유연하지만 상당히 어노테이션 기반인 Java EE 5의 JAX-WS 2.0가 그 자리를 대체했다. JAX-WS 2.1도 Java 6에 포함되었고(구체적으로 말하면 Sun의 JDK 1.6.0_04에 포함되었고 Sun JDK 1.6.0에는 JAX-WS 2.0가 포함되었다.) JDK의 내장 HTTP 서버와 통합되었다.
스프링은 두 표준 자바 웹서비스 API에서 모두 동작한다. Java EE 5 / Java 6에서는 JAX-WS가 좋다. Java 5에서 동작하는 J2EE 1.4 환경에서는 JAX-WS 프로바이터를 연결할 수 있다. Java EE 서버 문서를 확인해 봐라.
스프링 코어의 JAX-RPC와 JAX-WS에 대한 지원에 추가적으로 스프링 포트폴리오에 계약 우선(contract-first)이면서 문서주도의 웹서비스 솔루션인 스프링 웹 서비스 기능도 있다. 현재적이고 경쟁력있는 웹서비스를 만든다면 스프링 웹 비스를 강력히 추천한다.
20.5.1 JAX-RPC를 사용해서 서블릿 기반의 웹 서비스 노출
스프링은 JAX-RPC 서블릿 엔드포인트(endpoint) 구현체(ServletEndpointSupport)를 위한 편리한 기반 클래스를 제공한다. 예제의 AccountService를 노출하려면 스프링의 ServletEndpointSupport 클래스를 확장하고 여기에 비즈니스 로직을 구현하고 비즈니스 계층에 호출을 위임하는 것이 일반적이다.
/**
* JAX-RPC를 따르는 RemoteAccountService 구현체로
* 루트 웹 어플리케이션 컨텍스트에서 AccountService 구현체에 위임한다.
*
* JAX-RPC가 전용 엔드포인크 클래스와 동작해야 하므로 이 랩퍼 클래스가 필요하다.
* 기존에 존재하는 서비스를 익스포트해야 하는 경우 어플리케이션 컨텍스트 접근을 위해
* ServletEndpointSupport를 확장한 래퍼가 JAX-RPC와 호환되는 가장 간단한 방법이다.
*
* 이는 서버측 JAX-RPC 구현체로 등록된 클래스다. Axis에서 이는 배포호출(deployment calls)로
* 각각 "server-config.wsdd"에서 이뤄진다. 웹서비스 엔진이 이 클래스의 인스턴스 생명주기를
* 관리한다. 스프링 어플리케이션가 접근할 수 있다.
*/
import org.springframework.remoting.jaxrpc.ServletEndpointSupport;
public class AccountServiceEndpoint extends ServletEndpointSupport implements RemoteAccountService {
private AccountService biz;
protected void onInit() {
this.biz = (AccountService) getWebApplicationContext().getBean("accountService");
}
public void insertAccount(Account acc) throws RemoteException {
biz.insertAccount(acc);
}
public Account[] getAccounts(String name) throws RemoteException {
return biz.getAccounts(name);
}
}
스프링 기능에 접근하게 하려면 AccountServletEndpoint가 스프링 컨텍스트와 같은 웹 어플리케이션에서 동작해야 한다. Axis의 경우에는 'web.xml'에 AxisServlet 정의를 복사하고 'server-config.wsdd'에 엔드포인트를 설정해라.(또는 배포도구를 사용해라.) Axis를 사용해서 OrderService를 웹 서비스로 노출한 JPetStore 예제 어플리케이션을 참고해라.
20.5.2 JAX-RPC를 사용해서 웹 서비스에 접근하기
스프링은 JAX-RPC 웹서비스 프록시를 생성하는 두 팩토리 빈 LocalJaxRpcServiceFactoryBean와 JaxRpcPortProxyFactoryBean를 제공한다. LocalJaxRpcServiceFactoryBean는 사용할 JAX-RPC 서비스만 반환할 수 있다. JaxRpcPortProxyFactoryBean는 비즈니스 서비스 인터페이스를 구현한 프록시를 반환할 수 있는 완전히 독립적인 버전이다. 이 예제에서는 이전 섹션에서 노출한 AccountService 엔드포인트에 대한 프록시를 생성하려고 JaxRpcPortProxyFactoryBean를 사용한다. 약간의 코딩을 필요하지만 스프링이 웹서비스를 아주 잘 지원하는 것을 볼 수 있을 것이다. 대부분의 설정은 다른 때와 동일하게 스프링 설정파일에서 이뤄진다.
<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface" value="example.RemoteAccountService"/>
<property name="wsdlDocumentUrl" value="http://localhost:8080/account/services/accountService?WSDL"/>
<property name="namespaceUri" value="http://localhost:8080/account/services/accountService"/>
<property name="serviceName" value="AccountService"/>
<property name="portName" value="AccountPort"/>
</bean>
serviceInterface가 클라이언트가 사용할 원격 비즈니스 인터페이스이다. wsdlDocumentUrl는 WSDL 파일의 URL이다. 스프링이 JAX-RPC 서비스를 생성하려면 구동 시 wsdlDocumentUrl가 필요하다. namespaceUri는 .wsdl 파일의 targetNamespace와 대응된다. serviceName은 .wsdl 파일의 서비스명과 대응된다. portName는 .wsdl 파일의 포트명과 대응된다.
웹서비스에 대한 빈 팩토리가 있는 것처럼 이제 웹 서비스 접근은 아주 쉽고 이는 RemoteAccountService 인터페이스로 노출될 것이다. 스프링에서 이를 연결할 수 있다.
<bean id="client" class="example.AccountClientImpl">
...
<property name="service" ref="accountWebService"/>
</bean>
클라이언트 코드에서 일반적인 클래스인 것처럼 웹서비스에 접근할 수 있다. 다른 점은 RemoteException를 던진다는 것 뿐이다.
public class AccountClientImpl {
private RemoteAccountService service;
public void setService(RemoteAccountService service) {
this.service = service;
}
public void foo() {
try {
service.insertAccount(...);
}
catch (RemoteException ex) {
// ouch
}
}
}
스프링이 체크드 RemoteException을 대응되는 언체크드 RemoteException로 자동변환해주므로 체크드 RemoteException을 제거할 수 있다. 이를 위해서는 RMI가 아닌 인터페이스도 제공해야 한다. 이제 설정은 다음과 같다.
<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface" value="example.AccountService"/>
<property name="portInterface" value="example.RemoteAccountService"/>
...
</bean>
serviceInterface는 RMI가 아닌 인터페이스로 바뀐다. RMI 인터페이스는 이제 portInterface 프로퍼티를 사용해서 정의한다. 클라이언트 코드는 이제 java.rmi.RemoteException 처리를 안해도 된다.
public class AccountClientImpl {
private AccountService service;
public void setService(AccountService service) {
this.service = service;
}
public void foo() {
service.insertAccount(...);
}
}
"portInterface" 부분을 버리고 평범한 비즈니스 인터페이스를 "serviceInterface"로 지정할 수도 있다. 이 경우 자동적으로 JaxRpcPortProxyFactoryBean는 고정된 포트 스텁(stub)없이도 동적 호출을 수행하는 JAX-RPC "동적 호출 인터페이스(Dynamic Invocation Interface)"로 바뀔 것이다. RMI호환의 자바 포트 인터페이스가(예를 들면 대상 웹서비스가 자바가 아닌 경우) 필요없다는 점이 장점이다. 비즈니스 인터페이스만 맞추면 된다. 런타임의 동작은 JaxRpcPortProxyFactoryBean의 javadoc을 참고해라.
20.5.3 JAX-RPC 빈(bean) 매핑 등록하기
Account처럼 복잡한 객체를 네트워크간에 변환하려면 클라이언트측에서 빈 매핑을 등록해야 한다.
Axis를 사용하는 서버측의 빈 매핑 등록은 'server-config.wsdd' 파일에서 보통 이뤄진다.
클라이언트측에서 빈 매핑을 등록할 때 Axis를 사용할 것이므로 프로그래밍적으로 빈 매핑을 등록해야 한다.
public class AxisPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {
protected void postProcessJaxRpcService(Service service) {
TypeMappingRegistry registry = service.getTypeMappingRegistry();
TypeMapping mapping = registry.createTypeMapping();
registerBeanMapping(mapping, Account.class, "Account");
registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping);
}
protected void registerBeanMapping(TypeMapping mapping, Class type, String name) {
QName qName = new QName("http://localhost:8080/account/services/accountService", name);
mapping.register(type, qName,
new BeanSerializerFactory(type, qName),
new BeanDeserializerFactory(type, qName));
}
}
20.5.4 자신만의 JAX-RPC 핸들러 등록하기
이번 섹션에서는 SOAP 메시지를 보내기 전에 커스텀 코드를 실행할 수 있는 웹 서비스 프록시에 자신만의 javax.rpc.xml.handler.Handler를 등록할 것이다. Handler는 콜백 인터페이스다. jaxrpc.jar에서 편리한 기반 클래스를 제공하므로 javax.rpc.xml.handler.GenericHandler를 확장할 것이다.
public class AccountHandler extends GenericHandler {
public QName[] getHeaders() {
return null;
}
public boolean handleRequest(MessageContext context) {
SOAPMessageContext smc = (SOAPMessageContext) context;
SOAPMessage msg = smc.getMessage();
try {
SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope();
SOAPHeader header = envelope.getHeader();
...
}
catch (SOAPException ex) {
throw new JAXRPCException(ex);
}
return true;
}
}
이제 AccountHandler를 JAX-RPC 서비스에 등록하면 메시지를 보내기 전에 handleRequest(..)를 호출할 것이다. 스프링이 이러한 핸들러 등록에 선언적인 지원을 하지 않으므로 프로그래밍적인 접근을 취해야 한다. 하지만 스프링에서는 이 목적으로 만들어진 postProcessJaxRpcService(..) 메서드를 오버라이스할 수 있으므로 이 작업을 아주 쉽게 할 수 있다.
public class AccountHandlerJaxRpcPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {
protected void postProcessJaxRpcService(Service service) {
QName port = new QName(this.getNamespaceUri(), this.getPortName());
List list = service.getHandlerRegistry().getHandlerChain(port);
list.add(new HandlerInfo(AccountHandler.class, null, null));
logger.info("Registered JAX-RPC AccountHandler on port " + port);
}
}
마지막으로 팩토리빈을 사용하도록 스프링 설명을 변경하는 것을 잊지 말아야 한다.
<bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean">
...
</bean>
20.5.5 JAX-WS를 사용해서 서블릿기반의 웹서비스 노출하기
스프링은 JAX-WS 서블릿 앤드포인트 구현체에 대한 편리한 기반 클래스 SpringBeanAutowiringSupport를 제공한다. AccountService를 노출하려고 스프링의 SpringBeanAutowiringSupport를 확장해서 비스니스 로직을 작성한다.(보통은 비즈니스 계층에 호출을 위임한다.) 스프링이 관리하는 빈의 의존성 등을 나타내기 위해서 간단히 스프링 2.5의 @Autowired 어노테이션을 사용할 것이다.
/**
* JAX-WS 호환 AccountService 구현체로 루트 웹 어플리케이션 컨텍스트에서
* AccountService 구현체에 위임한다.
*
* JAX-WS가 전용 엔드포인트 클래스와 동작해야 하므로 이 랩퍼 클래스가 필요하다.
* 기존에 존배하는 서비스를 익스포트해야 하면 스프링 빈 자동연결(autowiring)을 위해서
* SpringBeanAutowiringSupport를 확장한 래퍼가 JAX-WS와 호환이 되는 가장 간단한 방법이다.
*
* 서버측 JAX-WS 구현체와 등록된 클래스다. Java EE 5 서버의 경우 이 클래스는 web.xml에
* 서블릿으로 정의될 것이고 서버는 이 클래스가 JAX-WS 엔드포인트라는 것을 탐지하고 적절하게 반응할
* 것이다. 보통 서블릿 이름이 지정한 WS 서비스명과 일치해야 한다.
*
* 웹 서비스 엔진은 이 클래스 인스턴스의 생명주기를 관리한다.
* 스프링 빈 참조가 여기서 연결될 것이다.
*/
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@WebService(serviceName="AccountService")
public class AccountServiceEndpoint extends SpringBeanAutowiringSupport {
@Autowired
private AccountService biz;
@WebMethod
public void insertAccount(Account acc) {
biz.insertAccount(acc);
}
@WebMethod
public Account[] getAccounts(String name) {
return biz.getAccounts(name);
}
}
스프링의 기능에 접근할 수 있도록 스프링 컨텍스트와 같은 웹 어플리케이션에서 AccountServletEndpoint를 실행해야 한다. JAX-WS 서블릿 엔드포인트 배포의 표준 계약을 사용하는 Java EE 5 환경에서는 이것이 기본값이다. 자세한 내용은 Java EE 5 웹서비스 튜토리얼을 봐라.
20.5.6 JAX-WS를 사용해서 독립(standalone) 웹 서비스 익스포트하기
Sun JDK 1.6의 내장 JAX-WS 프로바이더는 JDK 1.6에 포함된 내장 HTTP 서버를 사용해서 웹서비스를 노출한다. 스프링의 SimpleJaxWsServiceExporter는 스프링 어플리케이션 컨텍스트에서 @WebService 어노테이션이 붙은 모든 빈을 찾아내서 기본 JAX-WS 서버(JDK 1.6 HTTP 서버)로 익스포트한다.
이 시나리오에서 엔드포인트 인스턴스는 스프링 빈으로 정의하고 관리된다. 엔드포인트 인스턴스는 JAX-WS 엔진과 함께 등록될 것이지만 라이프사이클은 스프링 어플리케이션 컨텍스트에 달려있다. 즉, 명시적인 의존성 주입같은 스프링의 기능이 엔드포인트 인스턴스에 적용될 것이다. 물론 @Autowired를 사용한 어노테이션기반 주입도 잘 동작할 것이다.
<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter">
<property name="baseAddress" value="http://localhost:8080/"/>
</bean>
<bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint">
...
</bean>
...
AccountServiceEndpoint는 스프링의 SpringBeanAutowiringSupport에서 파생되지만 엔드포인트가 스프링이 완전히 관리하는 빈이므로 꼭 그럴 필요는 없다. 이는 엔드포인트 구현체가 슈퍼클래스를 정의하지 않고 다음과 같이 작성될 것이고 스프링의 @Autowired 설정 어노테이션을 사용할 수 있다.
@WebService(serviceName="AccountService")
public class AccountServiceEndpoint {
@Autowired
private AccountService biz;
@WebMethod
public void insertAccount(Account acc) {
biz.insertAccount(acc);
}
@WebMethod
public List<Account> getAccounts(String name) {
return biz.getAccounts(name);
}
}
20.5.7 JAX-WS RI의 스프링 지원을 사용한 웹서비스 익스포트
GlassFish 프로젝트의 일부인 Sun의 JAX-WS RI는 JAX-WS Commons 프로젝트의 일부로 스프링을 지원한다. 이는 앞 섹션에서 설명한 독립(standalone) 모드와 유사하게(지금은 서블릿 환경이다.) 스프링이 관리하는 빈으로 JAX-WS 엔드포인트를 정의할 수 있다. 이는 Java EE 5환경에서는 사용할 주 없다. 웹어플리케이션의 일부로 JAX-WS RI를 임베딩한 톰캣같은 EE가 아닌 환경을 위한 것이다.
서블릿에 기반한 엔트포인트 익스포트의 표준 방식과 다른 점은 엔드포인트 인스턴스의 생명주기는 스프링이 관리할 것이고 web.xml에 딱 하나의 JAX-WS 서블릿이 정의되어 있을 것이다. 표준 Java EE 5 방식으로는(위에서 설명한) 서비스 엔드포인트마다 하나의 서블릿 정의를 가질 것이고 각 엔드포인트는 보통 스프링 빈(위에서 처럼 @Autowired를 사용해서)에 위임할 것이다.
자세한 설정과 사용방법은 https://jax-ws-commons.dev.java.net/spring/를 참고해라.
20.5.8 JAX-WS로 웹서비스 접근하기
JAX-RPC 지원과 비슷하게 스프링은 JAX-WS 웹서비스 프록시를 생성하는 두 팩토리 빈 LocalJaxWsServiceFactoryBean와 JaxWsPortProxyFactoryBean를 제공한다. LocalJaxWsServiceFactoryBean는 사용할 JAX-WS 서비스 클래스만을 반환할 수 있고 JaxWsPortProxyFactoryBean는 완전히 독립적인 버전으로(full-fledged version) 비즈니스 서비스 인터페이스를 구현한 프록시를 반환할 수 있다. 이 예제에서 AccountService 엔드포인트에 대한 프록시를 생성하려고 JaxWsPortProxyFactoryBean를 사용한다.
<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
<property name="serviceInterface" value="example.AccountService"/>
<property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/>
<property name="namespaceUri" value="http://example/"/>
<property name="serviceName" value="AccountService"/>
<property name="portName" value="AccountServiceEndpointPort"/>
</bean>
serviceInterface가 클라이언트가 사용할 비즈니스 인터페이스다. wsdlDocumentUrl는 WSDL 파일의 URL이다. 스프링이 JAX-WS 서비스를 생성도록 구동시에 이 파일이 필요하다. namespaceUri는 .wsdl 파일의 targetNamespace과 대응되고 serviceName는 .wsdl 파일의 서비스명과 대응되고 portName는 .wsdl파일의 포트명과 대응된다.
AccountService 인터페이스로 노출할 빈 팩토리를 가지고 있으므로 이제 웹서비스 접근이 아주 쉽다. 스프링에서 이를 연결할 수 있다.
<bean id="client" class="example.AccountClientImpl">
...
<property name="service" ref="accountWebService"/>
</bean>
클라이언트 코드에서는 일반적인 클래스인 것처럼 웹 서비스에 접근할 수 있다.
public class AccountClientImpl {
private AccountService service;
public void setService(AccountService service) {
this.service = service;
}
public void foo() {
service.insertAccount(...);
}
}
NOTE: JAX-WS에서는 엔드포인트 인터페이스와 구현 클래스에 @WebService, @SOAPBinding 등의 어노테이션이 붙어야 하므로 앞에서는 약간 간소화되었다. 즉, JAX-WS 엔드포인트 아티팩트(artifacts)로 평번한 자바 인터페이스와 구현클래스를 (쉽게)사용할 수 없다. 그러므로 우선 이러한 클래스에 적절하게 어노테이션을 붙혀야 한다. 요구사항에 대한 자세한 내용은 JAX-WS 문서를 확인해 봐라.
최종적으로 상대의 컴퓨터를 조종할수 있는건가요?