레이블이 Spring인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Spring인 게시물을 표시합니다. 모든 게시물 표시

Spring Quartz 혹은 Timer 를 사용한 스케쥴링

Quartz 혹은 Timer 를 사용한 스케쥴링





19.1. 소개



Spring은 스케쥴링을 지원하는 통합 클래스들을 제공한다. 현재적으로, Spring은 1.3 이후버전 JDK의 일부분인 Timer와 Quartz 스케쥴러 (http://www.quartzscheduler.org)를 지원하고 있다. 이 두개의 스케쥴러들은 각각 Timer 혹은 Triggers에 대한 선택적 참조를 가지는 FactoryBean을 사용하여 세팅된다. 게다가 당신이 타겟 object의 메써드를 편리하게 호출할 수 있도록 도와주는 Quartz 스케쥴러와 Timer에 대한 편의 클래스를 제공한다.(이것은 일반적인 MethodInvokingFactoryBeans와 비슷하다.)






19.2. OpenSymphony Quartz 스케쥴러 사용하기



Quartz는 Triggers, Jobs 그리고 모든 종류의 jobs를 인식하고 있는 JobDetail를 사용한다. Quartz에 깔려 있는 기본적인 개념을 알고 싶다면, http://www.opensymphony.com/quartz를 찾아보길 바란다. 편리한 사용을 위해서, Spring은 Spring 기반 어플리케이션 내에서 Quartz의 사용을 손쉽게 만들어주는 두 개의 클래스들을 제공한다.






19.2.1. JobDetailBean 사용하기



JobDetail 객체는 job을 실행하기 위해 필요한 모든 정보를 가지고 있다. Spring은 소위 JobDetailBean이라고 불리는 클래스를 제공하는데, 이것은 JobDetail을 합리적인 디폴트값을 가진 실질적인 JavaBean 객체로 만들어준다. 다음의 예제를 보도록 하자.

<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass">
<value>example.ExampleJob</value>
</property>
<property name="jobDataAsMap">
<map>
<entry key="timeout"><value>5</value></entry>
</map>
</property>
</bean>

위의 job detail bean은 job(ExampleJob)을 실행하기 위한 모든 정보를 가지고 있다. 타임아웃은 job data map으로 기술되었다. job data map은 (실행시 넘겨지는) JobExecutionContext를 통해 이용할 수 있지만, JobDetailBean 역시 job data map으로부터 실질적인 job의 프라퍼티들을 매핑할 수 있다. 때문에 이러한 경우, 만약 ExampleJob이 timeout이라는 프라퍼티를 가지고 있다면, JobDetailBean은 그것을 자동으로 적용할 것이다.

package example;

public class ExampleJob extends QuartzJobBean {

private int timeout;

/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailBean (5)
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}

protected void executeInternal(JobExecutionContext ctx)
throws JobExecutionException {
// do the actual work
}
}

당신은 job detail bean의 모든 부가적인 세팅들 역시 마찬가지로 이용할 수 있다.


주의: namegroup 프라퍼티를 사용함으로써, 당신은 job의 name과 group을 변경할 수 있다. default로 job의 이름은 job detail bean의 이름과 동일하다.(위의 예에서는 exampleJob이 된다.)






19.2.2. MethodInvokingJobDetailFactoryBean 사용하기



종종 당신은 특정한 객체의 메써드를 호출할 필요가 있을 것이다. 당신은 MethodInvokingJobDetailFactoryBean을 사용하여 다음과 같이 할 수 있다.

<bean id="methodInvokingJobDetail" 
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject"><ref bean="exampleBusinessObject"/></property>
<property name="targetMethod"><value>doIt</value></property>
</bean>

위의 예는 (아래에 있는) exampleBusinessObject의 doIt을 호출하는 것을 의미한다.

public class BusinessObject {

// properties and collaborators

public void doIt() {
// do the actual work
}
}

<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

MethodInvokingJobDetailFactoryBean을 사용할 때, 메써드를 호출할 한줄짜리 jobs를 생성할 필요가 없으며, 당신은 단지 실질적인 비지니스 객체를 생성해서 그것을 묶기만 하면된다.


default로는 Quartz Jobs는 비상태이며, 상호 작용하는 jobs의 가능성을 가진다. 만약 당신이 동일한 JobDetail에 대해 두 개의 triggers를 명시한다면, 첫번째 job이 끝나기 이전에 두번째가 시작할지도 모른다. 만약 JobDetail 객체가 상태 인터페이스를 구현한다면, 이런 일은 발생하지 않을 것이다. 두번째 job은 첫번째가 끝나기 전에는 시작하지 않을 것이다. MethodInvokingJobDetailFactoryBean를 사용한 jobs가 동시작용하지 않도록 만들기 위해서는, concurrent 플래그를 false로 세팅해주어야 한다.

<bean id="methodInvokingJobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject"><ref bean="exampleBusinessObject"/></property>
<property name="targetMethod"><value>doIt</value></property>
<property name="concurrent"><value>false</value></property>
</bean>

주의: 기본적으로 jobs는 concurrent 옵션에 따라 실행될 것이다.






19.2.3. triggers 와 SchedulerFactoryBean을 사용하여 jobs를 묶기



우리는 job details과 jobs를 생성했고, 당신이 특정 객체의 메써드를 호출할 수 있도록 하는 편의클래스 bean을 살펴보았다. 물론, 우리는 여전히 jobs를 그자체로 스케쥴할 필요가 있다. 이것은 triggers와 SchedulerFactoryBean을 사용하여 이루어진다. 여러가지 triggers는 Quartz 내에서 이용할 수 있다. Spring은 편의를 위해 2개의 상속받은 triggers를 기본적으로 제공한다.:CronTriggerBeanSimpleTriggerBean이 그것이다.


Triggers는 스케쥴될 필요가 있다. Spring은 triggers를 세팅하기 위한 프라퍼티들을 드러내는 SchedulerFactoryBean을 제공하고 있다. SchedulerFactoryBean은 그 triggers와 함께 실질적인 jobs를 스케쥴한다.


다음 두가지 예를 보자.

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail">
<!-- see the example of method invoking job above -->
<ref bean="methodInvokingJobDetail"/>
</property>
<property name="startDelay">
<!-- 10 seconds -->
<value>10000</value>
</property>
<property name="repeatInterval">
<!-- repeat every 50 seconds -->
<value>50000</value>
</property>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref bean="exampleJob"/>
</property>
<property name="cronExpression">
<!-- run every morning at 6 AM -->
<value>0 0 6 * * ?</value>
</property>
</bean>

OK, 이제 우리는 두 개의 triggers를 세팅했다. 하나는 10초 늦게 실행해서 매 50초마다 실행될 것이고, 다른 하나는 매일 아침 6시에 실행될 것이다. 모든 것을 완료하기 위해서, 우리는 SchedulerFactoryBean을 세팅해야 한다.

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref local="cronTrigger"/>
<ref local="simpleTrigger"/>
</list>
</property>
</bean>

당신이 세팅할 수 있는 더욱 많은 속성들이 SchedulerFactoryBean에 있다. 이를테면, job details에 의해 사용되는 calendars라던가, Quartz를 커스터마이징할 수 있게 하는 프라퍼티같은 것들이 말이다. 더 많은 정보를 위해서는 JavaDoc(http://www.springframework.org/docs/api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html)을 참조하도록 해라.






19.3. JDK Timer support 사용하기



Spring에서 스케쥴링 업무를 처리하는 또다른 방법은 JDK Timer 객체들을 사용하는 것이다. Timers 자체에 대한 더 많은 정보는 http://java.sun.com/docs/books/tutorial/essential/threads/timer.html에서 찾아볼 수 있다. 위에서 살펴 본 기본개념들은 Timer support에도 마찬가지로 적용된다. 당신은 임의의 timers를 생성하고 메써드들을 호출하기 위해 timer를 사용한다. TimerFactoryBean을 사용하여 timers를 묶는다.






19.3.1. 임의의 timers 생성하기



당신은 TimerTask를 사용하여 임의의 timer tasks를 생성할 수 있다. 이것은 Quartz jobs와 유사하다

public class CheckEmailAddresses extends TimerTask {

private List emailAddresses;

public void setEmailAddresses(List emailAddresses) {
this.emailAddresses = emailAddresses;
}

public void run() {
// iterate over all email addresses and archive them
}
}

이것을 묶는 것 역시 간단하다:

<bean id="checkEmail" class="examples.CheckEmailAddress">
<property name="emailAddresses">
<list>
<value>test@springframework.org</value>
<value>foo@bar.com</value>
<value>john@doe.net</value>
</list>
</property>
</bean>

<bean id="scheduledTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<!-- wait 10 seconds before starting repeated execution -->
<property name="delay">
<value>10000</value>
</property>
<!-- run every 50 seconds -->
<property name="period">
<value>50000</value>
</property>
<property name="timerTask">
<ref local="checkEmail"/>
</property>
</bean>


task를 단지 한번만 실행하고자 한다면, period 속성을 -1(혹은 다른 음수값으)로 바꿔주면 된다.






19.3.2. MethodInvokingTimerTaskFactoryBean 사용하기



Quartz support와 비슷하게, Timer 역시 당신이 주기적으로 메써드를 호출할 수 있도록 하는 요소들을 기술한다.

<bean id="methodInvokingTask" 
class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
<property name="targetObject"><ref bean="exampleBusinessObject"/></property>
<property name="targetMethod"><value>doIt</value></property>
</bean>

위의 예제는 (아래와 같은) exampleBusinessObject에서 호출되는 doIt에서 끝날 것이다

public class BusinessObject {

// properties and collaborators

public void doIt() {
// do the actual work
}
}

ScheduledTimerTask가 언급된 위의 예제의 참조값을 methodInvokingTask로 변경하면 이 task가 실행될 것이다.






19.3.3. 감싸기 : TimerFactoryBean을 사용하여 tasks를 세팅하기



TimerFactoryBean은 실질적인 스케쥴링을 세팅한다는 같은 목적을 제공한다는 점에서 Quartz의 SchedulerFactoryBean과 비슷하다. TimerFactoryBean는 실질적인 Timer를 세팅하고 그것이 참조하고 있는 tasks를 스케쥴한다. 당신은 대몬 쓰레드를 사용할 것인지 말것인지를 기술할 수 있다.

<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
<property name="scheduledTimerTasks">
<list>
<!-- see the example above -->
<ref local="scheduledTask"/>
</list>
</property>
</bean>

Spring을 사용한 원격(Remoting)및 웹서비스

Spring을 사용한 원격(Remoting)및 웹서비스





17.1. 소개



원격 지원을 위한 Spring통합 클래스는 다양한 기술을 사용한다. 원격 지원은 당신의 (Spring) POJO에 의해 구현되는 원격-가능 서비스의 개발을 쉽게 한다. 현재 Spring은 4가지 원격 기술을 지원한다.





  • 원격 메소드 호출 (RMI). RmiProxyFactoryBeanRmiServiceExporter의 사용을 통해 Spring은 전통적인 RMI(java.rmi.Remote 인터페이스와 java.rmi.RemoteException을 가지는)와 RMI 호출자(어떠한 자바 인터페이스를 가진)를 통한 투명한 원격 모두 지원합니다.



  • Spring의 HTTP 호출자. Spring은 어떤 자바 인터페이스(RMI 호출자와 같은)를 지원하는 HTTP를 통한 자바 직렬화를 허용하는 특별한 원격 전략을 제공한다. 관련 지원 클래스는 HttpInvokerProxyFactoryBeanHttpInvokerServiceExporter이다.



  • Hessian. HessianProxyFactoryBeanHessianServiceExporter를 사용하여 Caucho에 의해 제공되는 가벼운 바이너리 HTTP기반 프로토콜을 사용하는 당신의 서비스를 투명하게 드러낼수 있다.



  • Burlap. Burlap은 Hessian을 위한 Caucho의 XML기반의 대안이다. Spring은 BurlapProxyFactoryBeanBurlapServiceExporter 같은 지원 클래스를 제공한다.



  • JAX RPC. Spring은 JAX-RPC를 통한 웹서비스를 위한 원격 지원을 제공한다.



  • JMS (TODO).



Spring의 원격 기능에 대해 언급하는 동안 우리는 다음의 도메인 모델과 관련 서비스를 사용할것이다.

// Account domain object
public class Account implements Serializable{
private String name;

public String getName();
public void setName(String name) {
this.name = name;
}
}

// Account service
public interface AccountService {

public void insertAccount(Account acc);

public List getAccounts(String name);
}

// Remote Account service
public interface RemoteAccountService extends Remote {

public void insertAccount(Account acc) throws RemoteException;

public List getAccounts(String name) throws RemoteException;
}

// ... and corresponding implement doing nothing at the moment
public class AccountServiceImpl implements AccountService {

public void insertAccount(Account acc) {
// do something
}

public List getAccounts(String name) {
// do something
}
}


우리는 RMI를 사용하여 원격 클라이언트에 서비스를 드러내길 시작하고 RMI를 사용한 장애에 대해 조금 이야기할것이다. 우리는 Hessian을 위한 예제를 보여줄 것이다.






17.2. RMI를 사용한 서비스 드러내기



RMI를 위한 Spring의 지원을 사용하여, 당신은 RMI내부구조를 통해 당신의 서비스를 투명하게 드러낼수 있다. 이 셋업 후 당신은 보안 컨텍스트 위임이나 원격 트랜잭션 위임을 위한 표준적인 지원이 없다는 사실을 제외하고 기본적으로 remote EJB와 유사한 설정을 가진다. Spring은 RMI호출자를 사용할때 추가적인 호출 컨텍스트와 같은 것을 위한 고리(hooks)를 제공한다. 그래서 당신은 예를 들어 보안 프레임워크나 사용자정의 보안 증명에 붙일수 있다.






17.2.1. RmiServiceExporter를 사용하여 서비스 내보내기



RmiServiceExporter를 사용하여, 우리는 RMI객체처럼 AccountService객체의 인터페이스를 드러낼수 있다. 인터페이스는 RmiProxyFactoryBean을 사용하거나 전통적인 RMI서비스의 경우 일반적인 RMI를 통해 접근될수 있다. RmiServiceExporter는 RMI호출자를 통해 RMI가 아닌 서비스의 발생을 명시적으로 지원한다.


우리는 먼저 Spring BeanFactory내 우리의 서비스를 셋업한다.

<bean id="accountService" class="example.AccountServiceImpl">
<!-- any additional properties, maybe a DAO? -->
</bean>


그리고 나서 우리는 RmiServiceExporter를 사용하여 우리의 서비스를 드러낼것이다.

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
<!-- does not necessarily have to be the same name as the bean to be exported -->
<property name="serviceName"><value>AccountService</value></property>
<property name="service"><ref bean="accountService"/></property>
<property name="serviceInterface"><value>example.AccountService</value></property>
<!-- defaults to 1099 -->
<property name="registryPort"><value>1199</value></property>
</bean>

당신이 볼수 있는 것처럼, 우리는 RMI등록(registry)을 위한 포트를 오버라이딩한다. 종종, 당신의 애플리케이션 서버는 RMI등록을 유지하고 그것을 방해하지 않는것이 현명하다. 게다가 서비스 이름은 서비스를 바인드 하기 위해 사용된다. 서비스는 rmi://HOST:1199/AccountService에 비인드될것이고 우리는 클라이언트측에서 서비스로 링크하기 위해 이 URL을 사용할것이다.


노트 : 우리는 하나의 프라퍼티를 남겨두었다. 이를테면 servicePort이고 디폴트로에 의해 0이된다. 이것은 서비스와 통신하기 위해 사용될 익명 포트를 의미한다. 당신은 다른 포트를 명시할수 있다.






17.2.2. 클라이언트에서 서비스 링크하기



우리의 클라이언트는 계좌(accounts)를 관리하기 위한 AccountService을 사용하는 간단한 객체이다.

public class SimpleObject {
private AccountService accountService;
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
}


클라이언트에서 서비스에 링크하기 위해, 우리는 간단한 객체와 서비스 링크 설정을 포함하는 분리된 bean factory를 생성할 것이다.

<bean class="example.SimpleObject">
<property name="accountService"><ref bean="accountService"/></property>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl"><value>rmi://HOST:1199/AccountService</value></property>
<property name="serviceInterface"><value>example.AccountService</value></property>
</bean>

그것은 우리가 클라이언트에서 원격 계좌(account)서비스를 지원하기 위해 해야 할 필요가 있는 모든것이다. Spring은 호출자를 투명하게 생성하고 RmiServiceExporter를 통해 계좌 서비스를 원격적으로 가능하게 한다. 클라이언트에서 우리는 RmiProxyFactoryBean를 사용하여 이것을 링크한다.






17.3. HTTP를 통해 서비스를 원격으로 호출하기 위한 Hessian 이나 Burlap을 사용하기.



Hessian은 바이너리 HTTP-기반 원격 프로토콜을 제공한다. 이것은 Caucho에 의해 생성되었고 Hessian자체에 대한 좀더 상세한 정보는 http://www.caucho.com에서 찾을수 있다.






17.3.1. Hessian을 위해 DispatcherServlet을 묶기.



Hessian은 HTTP를 통해 통신하고 사용자정의 서블릿을 사용해서도 그렇게 한다. Spring의 DispatcherServlet 원리를 사용하여, 당신의 서비스를 드러내는 서블릿을 쉽게 묶을수 있다. 먼저 우리는 애플리케이션내 새로운 서블릿을 생성해야만 한다. (이것은 web.xml으로 부터 인용한다.)

<servlet>
<servlet-name>remoting</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>remoting</servlet-name>
<url-pattern>/remoting/*</url-pattern>
</servlet-mapping>


당신은 아마도 Spring의 DispatcherServlet 원리에 익숙하고 만약 그렇다면 당신은 WEB-INF 디렉토리내 (당신의 서블릿 이름 뒤) remoting-servlet.xml라는 이름의 애플리케이션 컨텍스트를 생성할것이다. 애플리케이션 컨텍스트는 다음 부분에서 사용될것이다.






17.3.2. HessianServiceExporter를 사용하여 bean을 드러내기



remoting-servlet.xml 라고 불리는 새롭게 생성된 애플리케이션 컨텍스트내에서 우리는 당신의 서비스를 내보내는 HessianServiceExporter를 생성할것이다.

<bean id="accountService" class="example.AccountServiceImpl">
<!-- any additional properties, maybe a DAO? -->
</bean>

<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service"><ref bean="accountService"/></property>
<property name="serviceInterface">
<value>example.AccountService</value>
</property>
</bean>

지금 우리는 클라이언트에서 서비스를 링크할 준비가 되어있다. 명시된 핸들러 맵핑은 없고 서비스를 향한 요청 URL을 맵핑한다. 그래서 BeanNameUrlHandlerMapping은 사용될것이다. 나아가 서비스는 bean이름(http://HOST:8080/remoting/AccountService)을 통해 표시되는 URL에 내보내어질것이다.






17.3.3. 클라이언트의 서비스로 링크하기



HessianProxyFactoryBean을 사용하여 우리는 클라이언트에서 서비스로 링크할수 있다. 같은 원리는 RMI예제처럼 적용한다. 우리는 분리된 bean factory나 애플리케이션 컨텍스트를 생성하고 SimpleObject가 계좌를 관리하기 위해 AccountService를 사용하는 다음 bean을 따른다.

<bean class="example.SimpleObject">
<property name="accountService"><ref bean="accountService"/></property>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
<property name="serviceUrl"><value>http://remotehost:8080/AccountService</value></property>
<property name="ServiceInterface"><value>example.AccountService</value></property>
</bean>

That's all there is to it.






17.3.4. Burlap 사용하기



우리는 Hessian의 XML기반의 대응물인 Burlap을 여기서 상세하게 언급하지 않을것이다. Hessian처럼 정확하게 같은 방법으로 설정되고 셋업된 후 변형물은 위에서 설명된다. Hessian 이라는 단어를 Burlap으로 교체하고 당신은 모든것을 셋팅한다.






17.3.5. Hessian 이나 Burlap을 통해 드러나는 서비스를 위한 HTTP 기본 인증 적용하기



Hessian 과 Burlap 장점중 하나는 두가지 프로토콜이 모두 HTTP-기반이기 때문에 우리가 HTTP 기본 인증을 쉽게 적용할수 있다는 것이다. 당신의 일반적인 HTTP서버의 보안 기법은 web.xml 보안 기능을 사용하여 쉽게 적용될수 있다. 대개 당신은 여기서 사용자별 보안 증명을 사용하지 않지만 공유된 증명은 Hessian/BurlapProxyFactoryBean 레벨(JDBC 데이터소스와 유사한)에서 정의된다.


<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="authorizationInterceptor"/>
</list>
</property>
</bean>

<bean id="authorizationInterceptor"
class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
<property name="authorizedRoles">
<list>
<value>administrator</value>
<value>operator</value>
</list>
</property>
</bean>


이것은 우리가 BeanNameUrlHandlerMapping을 명시적으로 언급하고 오직 관리자만을 허용하는 인터셉터를 셋팅하며 이 애플리케이션 컨텍스트내에서 언급된 bean을 호출하는 작업을 수행하는 예제이다.


노트 : 물론, 이 예제는 보안 내부구조의 유연한 종류를 보여주지 않는다. 보안이 관련된 만큼 좀더 많은 옵션을 위해서 Spring을 위한 Acegi 보안 시스템을 보라. 그것은 http://acegisecurity.sourceforge.net에서 찾을수 있다.






17.4. HTTP호출자를 사용하여 서비스를 드러내기



그들 자체의 얼마 안되는 직렬화 기법을 사용하는 가벼운 프로토콜인 Burlap 과 Hessian이 상반돤것처럼 Spring HTTP호출자는 HTTP를 통해 서비스를 드러내기 위한 표준적인 자바 직렬화 기법을 사용한다. 이것은 당신의 인자와 반환 타입이 Hessian 과 Burlap이 사용하는 직렬화 기법을 사용하여 직렬화될수 없는 복합(complex)타입이라면 커다란 장점을 가진다. (원격 기술을 선택할때 좀더 많은 숙고사항을 위해서 다음 부분을 참조하라.)


Spring은 HTTP호출을 수행하거나 Commons HttpClient를 위해 J2SE에 의해 제공되는 표준적인 기능을 사용한다. 만약 당신이 좀더 향상되었거나 사용하기 쉬운 기능이 필요하다면 후자를 사용하라. 좀더 많은 정보를 위해서는 jakarta.apache.org/commons/httpclient을 참조하라.






17.4.1. 서비스 객체를 드러내기



Hessian 이나 Burlap을 사용하는 방법과 매우 유사한 서비스 객체를 위한 HTTP 호출자 내부구조를 셋업하라. Hessian지원이 HessianServiceExporter를 제공하는 것처럼 Spring Http호출자 지원은 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter라고 불리는 것을 제공한다. (위에서 언급된) AccountService을 드러내기 위해 다음 설정이 대체될 필요가 있다.

    <bean name="/AccountService" class="org.sprfr.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service"><ref bean="accountService"/></property>
<property name="serviceInterface">
<value>example.AccountService</value>
</property>
</bean>






17.4.2. 클라이언트에서 서비스 링크하기



다시 클라이언트로부터 서비스를 링크하는것은 Hessian 이나 Burlap을 사용할때 이것을 하는 방법과 매우 유사하다. 프록시를 사용하여 Spring은 내보내어진 서비스를 위한 URL을 위해 당신의 호출을 HTTP POST요청으로 번역할수 있을것이다.

 
<bean id="httpInvokerProxy" class="org.sprfr.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl">
<value>http://remotehost:8080/AccountService</value>
</property>
<property name="serviceInterface">
<value>example.AccountService</value>
</property>
</bean>


전에 언급된것처럼, 당신은 사용하고자 하는 HTTP클라이언트를 선택할수 있다. 디폴트에 의하면 HttpInvokerProxy가 J2SE HTTP기능을 사용한다. 하지만 당신은 httpInvokerRequestExecutor 프라퍼티를 셋팅하여 Commons HttpClient를 사용할수 있다.

<property name="httpInvokerRequestExecutor">
<bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>






17.5. 웹 서비스



Spring은 다음을 위한 지원을 가진다.




  • JAX-RPC를 사용하여 서비스를 드러내기
  • 웹 서비스에 접근하기


위의 지원 목록 다음으로, 당신은 XFire xfire.codehaus.org를 사용하여 웹 서비스를 드러낼수 있다. XFire는 Codehaus에서 현재 개발중인 가벼운 SOAP라이브러리이다.






17.5.1. JAX-RPC를 사용하여 서비스를 드러내기



Spring은 JAX-RPC 서블릿 endpoint구현물(ServletEndpointSupport)을 위한 편리한 base클래스이다. 우리의 AccountService를 드러내기 위해 우리는 Spring의 ServletEndpointSupport클래스를 확장하고 여기서 언제나 비지니스 레이어로 호출을 위임하는 비지니스 로직을 구현한다.

/**
* JAX-RPC compliant RemoteAccountService implementation that simply delegates
* to the AccountService implementation in the root web application context.
*
* This wrapper class is necessary because JAX-RPC requires working with
* RMI interfaces. If an existing service needs to be exported, a wrapper that
* extends ServletEndpointSupport for simple application context access is
* the simplest JAX-RPC compliant way.
*
* This is the class registered with the server-side JAX-RPC implementation.
* In the case of Axis, this happens in "server-config.wsdd" respectively via
* deployment calls. The Web Service tool manages the life-cycle of instances
* of this class: A Spring application context can just be accessed here.
*/
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는 Spring의 기능에 접근하기 위해 허용하는 Spring컨텍스트처럼 같은 웹 애플리케이션내에서 수행할 필요가 있다. Axis의 경우, AxisServlet정의를 web.xml로 복사하고 "server-config.wsdd" 내 endpoint를 셋업한다.(또는 배치툴을 사용한다.) Axis를 사용한 웹 서비스처럼 드러나는 OrderService가 있는 샘플 애플리케이션 JPetStore를 보라.






17.5.2. 웹 서비스에 접근하기



Spring은 웹 서비스 프록시인 LocalJaxRpcServiceFactoryBeanJaxRpcPortProxyFactoryBean을 생성하기 위한 두가지 factory bean을 가진다. 전자는 JAX-RPC서비스 클래스만을 반환할수 있다. 후자는 우리의 비지니스 서비스 인터페이스를 구현하는 프록시를 반환할수 있는 완전한 버전이다. 이 예제에서 우리는 이전 단락내 나오는 AccountService Endpoint를 위한 프록시를 생성하기 위해 나중에 사용된다. 당신은 Spring이 조금의 코딩을 요구하는 웹 서비스를 위한 대단한 지원을 가진다는 것을 볼것이다. 대부분의 마법은 대개 Spring설정 파일내에서 이루어진다.

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface">
<value>example.RemoteAccountService</value>
</property>
<property name="wsdlDocumentUrl">
<value>http://localhost:8080/account/services/accountService?WSDL</value>
</property>
<property name="namespaceUri">
<value>http://localhost:8080/account/services/accountService</value>
</property>
<property name="serviceName">
<value>AccountService</value>
</property>
<property name="portName">
<value>AccountPort</value>
</property>
</bean>

serviceInterface는 클라이언트가 사용할 원격 비지니스 인터페이스이다. wsdlDocumentUrl은 WSDL파일을 위한 URL이다. Spring은 JAX-RPC서비스를 생성하기 위해 시작시 이것이 필요하다. namespaceUri는 .wsdl파일내 targetNamespace에 대응한다. serviceName은 .wsdl파일내 서비스 이름에 대응한다. portName은 .wsdl파일내 포트명에 대응한다.


웹 서비스에 접근하는 것은 우리가 RemoteAccountService인터페이스처럼 이것을 드러내는 bean factory를 가지는것처럼 매우 쉽다. 우리는 Spring내 이것을 묶을수 있다.

    <bean id="client" class="example.AccountClientImpl">
...
<property name="service">
<ref bean="accountWebService"/>
</property>
</bean>

그리고 클라이언트 코드로 부터 우리는 이것이 RemoteException을 던지는것을 제외하고 일반 클래스인것처럼 웹 서비스에 접근할수 있다.

public class AccountClientImpl {

private RemoteAccountService service;

public void setService(RemoteAccountService service) {
this.service = service;
}

public void foo() {
try {
service.insertAccount(...);
} catch (RemoteException e) {
// ouch
...
}
}

}


우리는 Spring이 관련된 체크되지 않은 RemoteAccessException으로의 자동변환을 지원하기 때문에 체크된 RemoteException을 제거할수 있다. 이것은 우리가 비-RMI인터페이스 또한 제공하는것을 요구한다. 우리의 설정은 다음과 같다.

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface">
<value>example.AccountService</value>
</property>
<property name="portInterface">
<value>example.RemoteAccountService</value>
</property>
...
</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(...);
}

}






17.5.3. Register Bean 맵핑



Account와 같은 정보를 넘어 복합 객체를 이동시키기 위해 우리는 클라이언트 측에서 bean맵핑을 등록해야만 한다.









[Note]Note

서버측에서 Axis를 사용하여 등록된 bean맵핑은 server-config.wsdd에서 언제나 수행된다.


우리는 클라이언트 측에서 bean맵핑을 등록하기 위해 Axis를 사용할것이다. 이것을 하기 위해 우리는 Spring Bean factory의 하위클래스를 만들 필요가 있고 프로그램에 따라 bean맵핑을 등록한다.

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));
}

}






17.5.4. 자체적인 핸들러 등록하기



이 장에서 우리는 SOAP메시지를 정보를 통해 보내기 전에 코드를 사용자정의 할수 있는 웹 서비스 프록시를 위한 javax.rpc.xml.handler.Handler를 등록할 것이다. javax.rpc.xml.handler.Handler는 콜백 인터페이스이다. jaxrpc.jar내 제공되는 편리한 base클래스인 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 e) {
throw new JAXRPCException(e);
}

return true;
}

}

우리의 AccountHandler를 JAX-RPC 서비스에 등록하는 것이 필요하다. 그래서 메시지가 정보를 통해 전달되기 전에 handleRequest을 호출할것이다. Spring은 이 시점에 핸들러를 등록하기 위한 선언적인 지원을 가지지 않는다. 그래서 우리는 프로그램마다 다른 접근법을 사용해야만 한다. 어쨌든 Spring은 우리가 이 bean factory를 확장하고 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 Handler [" + AccountHandler.class.getName() + "] on port " + port);
}

}

그리고 마지막으로 우리는 factory bean을 사용하기 위해 Spring설정을 변경하는 것을 기억해야만 한다.

    <bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean">
...
</bean>






17.5.5. XFire를 사용하여 웹 서비스를 드러내기



XFire는 Codehaus에서 호스팅되는 가벼운 SOAP라이브러리이다. 현 시점(2005년 3월)에, XFire는 여전히 개발중이다. 비록 Spring지원이 안정적이라고 하더라도 대부분의 기능은 나중에 추가될것이다. XFire를 드러내는 것은 당신이 WebApplicationContext에 추가할 RemoteExporter-스타일의 bean으로 조합된 XFire를 가진 XFire 컨텍스트를 사용하는 것이다.


당신이 서비스를 드러내는 것을 허용하는 모든 메소드처럼 당신은 드러낼 서비스를 포함하는 관련된 WebApplicationContext를 가진 DispatcherServlet을 생성해야 한다.

<servlet>
<servlet-name>xfire</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>


당신은 XFire설정을 링크해야만 한다. 이것은 ContextLoaderListener(또는 서블릿)가 가지는 contextConfigLocations 컨텍스트 파라미터에 컨텍스트 파일을 추가하는것이다. 설정 파일은 XFire jar파일내 위치하고 물론 애플리케이션의 클래스패스에 위치할수도 있다.

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:org/codehaus/xfire/spring/xfire.xml
</param-value>
</context-param>

<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>


당신이 서블릿 맵핑(위에서 선언된 XFire서블릿을 위한 /* 맵핑)을 추가한 후에 당신은 오직 XFire를 사용하는 서비스를 드러내기 위한 추가적인 bean을 추가해야만 한다. 예를 들어 당신은 xfire-servlet.xml을 다음에 추가하라.

<beans>
<bean name="/Echo" class="org.codehaus.xfire.spring.XFireExporter">
<property name="service">
<ref bean="echo"/>
</property>
<property name="serviceInterface">
<value>org.codehaus.xfire.spring.Echo</value>
</property>
<property name="serviceBuilder">
<ref bean="xfire.serviceBuilder"/>
</property>
<!-- the XFire bean is wired up in the xfire.xml file you've linked in earlier
<property name="xfire">
<ref bean="xfire"/>
</property>
</bean>

<bean id="echo" class="org.codehaus.xfire.spring.EchoImpl"/>
</beans>


XFire는 나머지를 다룬다. 이것은 당신의 서비스 인터페이스를 분석하고 이것으로 부터 WSDL을 생성한다. 이 문서의 일부는 XFire사이트로부터 가져왔다. XFire와 Spring통합에 대한 좀더 상세한 정보는 docs.codehaus.org/display/XFIRE/Spring를 보라.






17.6. 자동-탐지(Auto-detection)는 원격 인터페이스를 위해 구현되지 않는다.



구현된 인터페이스의 자동-탐지가 원격 인터페이스에는 발생하지 않는 가장 중요한 이유는 원격 호출자를 위해 너무 많은 문이 열리는 것을 피하는 것이다. 대상 객체는 호출자에게 드러내는 것을 원하지 않는 InitializingBean 이나 DisposableBean처럼 내부 콜백 인터페이스를 구현해야만 한다.


대상에 의해 구현된 모든 인터페이스를 가진 프록시를 제공하는 것은 로컬의 경우 언제나 문제가 되지 않는다. 하지만 원격 서비스를 내보낼때 당신은 원격 사용의 경향이 있는 특정 작업을 가진 특정 서비스 인터페이스를 보여야만 한다. 내부 콜백 인터페이스외에도 대상은 원격 노출의 경향이 있는 것중 하나를 가진 다중 비지니스 인터페이스를 구현해야만 한다. 이러한 이유로 우리는 명시되는 서비스 인터페이스를 요구한다.


이것은 설정의 편리함과 내부 메소드의 뜻하지 않는 노출의 위험사이의 거래이다. 서비스 인터페이스를 명시하는 것은 많은 노력이 필요하지 않고 당신을 특정 메소드의 제어된 노출에 관련된 안전적인 쪽에 두게된다.






17.7. 기술을 선택할때 고려사항.



여기에 표시된 각각 그리고 모든 기술은 결점을 가진다. 당신이 기술을 선택할때 당신이 드러내는 서비스와 당신이 정보를 통해 보낼 객체중 필요한 것을 주의깊게 검토해야만 한다.


RMI를 사용할때, 당신이 RMI 소통을 관통(tunneling)하지 않는 한 HTTP프로토콜을 통해 객체에 접근하는 것은 불가능하다. RMI는 정보를 통해 직렬화가 필요한 복합 데이터 모델을 사용할때 중요한 완전한 객체 직렬화를 지원하는 상당히 무거운 프로토콜이다. 어쨌든 RMI-JRMP는 자바 클라이언트에 묶인다. 이것은 자바-대-자바 원격 솔루션이다.


Spring의 HTTP호출자는 만약 당신이 HTTP-기반 원격이 필요하지만 자바 직렬화에 의존한다면 좋은 선택이다. 이것은 수송기처럼 HTTP를 사용하는 RMI호출자를 가진 기본 내부구조를 공유한다. HTTP호출자는 자바-대-자바 원격에 제한을 가지지 않을뿐 아니라 클라이언트측과 서버측 모두 제한을 가하지 않는다. (후자는 비-RMI인터페이스를 위해 Spring의 RMI호출자에 적용한다.)


Hessian 그리고/또는 Burlap은 명시적으로 비-자바 클라이언트를 허용하기 때문에 이종 환경내에서 작동할때 명백한 값을 제공한다. 어쨌든 비-자바 지원은 여전히 제한된다. 알려진 문제는 늦게 초기화하는 collection으로 조합된 Hibernate객체의 직렬화를 포함한다. 만약 당신이 그러한 데이타 모델을 가진다면 Hessian대신에 RMI나 HTTP호출자를 사용하는 것을 검토하라.


JMS는 서비스의 클러스터(clusters)를 제공하기 위해 유용할수 있고 로드 밸런싱, 발견(discovery) 그리고 자동 대체(failover)를 다루기 위해 JMS 브로커(broker)를 허용한다. 디폴트에 의해 자바 직렬화는 JMS원격을 사용하지만 JMS제공자가 서버가 다른 기술로 구현되는것을 허용하는 XStream과 같은 포맷팅을 묶기 위한 다른 기법을 사용할수 있을때 사용된다.


EJB는 표준적인 권한(role)-기반 인증과 인증, 그리고 원격 트랜잭션 위임을 지원하는 면에서 RMI를 능가하는 장점을 가진다. 이것은 비록 핵심 Spring에 의해 제공되지는 않지만 보안 컨텍스트 위임을 지원하는 RMI호출자나 HTTP호출자를 얻는것은 가능하다. 써드 파티나 사용자정의 솔루션내 플러그인하기 위한 선호하는 고리(hooks)가 있다. 


Spring EJB에 접근하고 구현하기

EJB에 접근하기





16.1.1. 개념



local또는 remote 비상태유지(stateless) 세션빈의 메소드를 호출하기 위해, 클라이언트 코드는 (local또는 remote)EJB Home객체를 얻기위해 대개 JNDI룩업을 수행해야만 한다. 그 다음 실질적인 (local또는 remote)EJB객체를 얻기 위해 객체의 'create' 메소드 호출을 사용한다. 하나 이상의 메소드는 EJB에서 호출된다.


반복적인 하위레벨 코드를 파하기 위해 많은 EJB애플리케이션은 서비스 위치자(Locator)와 비지니스 위임 패턴을 사용한다. 클라이언트 코드 도처에 JDNI룩업을 하는것보다 더 좋다. 하지만 그들의 일반적인 구현물은 명백한 단점을 가진다. 예를 들면





  • EJB를 사용한 전형적인 코드는 서비스 위치자나 비지니스 위임 패턴에 의존한다. 하지만 이것은 테스트를 좀더 어렵게 한다.



  • 비지니스 위임이 없이 사용되는 서비스 위치자 패턴의 경우, 애플리케이션 코드는 여전히 EJB Home의 create()메소드를 호출하는것으로 끝이 나고 결과 예외를 다룬다. 게다가 이것은 EJB API와 EJB프로그래밍 모델의 복잡함에 묶인다.



  • 비지니스 위임 패턴을 구현하는 것은 일반적으로 우리가 EJB의 같은 메소드를 간단히 호출하는 다양한 메소드를 써야만 하는 위치의 명백한 코드 중복의 결과를 낳는다.


Spring접근법은 대개 코드가 없는 비지니스 위임처럼 작동하는 Spring ApplicationContext나 BeanFactory내 설정되는 프록시 객체의 생성과 사용을 허용한다. 당신은 실제값을 추가하지 않는다면 다른 서비스 위치자, 다른 JNDI 룩업 또는 손으로 작성된 비지니스 위임내 중복 메소드를 사용할 필요가 없다.






16.1.2. local SLSBs에 접근하기



우리가 local EJB를 사용할 필요가 있는 웹 컨트롤러를 가지고 있다고 가정하자. 우리는 최상의 상황을 따를것이고 EJB 비지니스 메소드 인터페이스 패턴을 사용할것이다. 그래서 EJB의 local 인터페이스는 EJB 성격이 아닌 비지니스 메소드 인터페이스를 확장한다. 이 비지니스 메소드 인터페이스를 MyComponent라고 부르자.

public interface MyComponent {
...
}

(비지니스 메소드 인터페이스 패턴을 위한 중요한 이유중 하나는 local 인터페이스내 메소드 시그너처와 bean구현 클래스사이 동기화가 자동적이라는 것을 확인하는 것이다. 다른 이유는 이것이 나중에 우리는 위해 그렇게 하도록 만들때 서비스의 POJO로 교체하는것을 좀더 쉽게 만든다는 것이다. ) 물론 우리는 local home인터페이스를 구현할 필요가 필요가 있을것이다. 그리고 SessionBean과 MyComponent비지니스 메소드 인터페이스를 구현하는 bean구현 클래스를 제공한다. 지금 우리의 웹 티어 컨트롤러를 EJB구현물로 연결하는 필요가 있는 자바코드만이 컨트롤러의 MyComponent타입의 setter메소드를 드러낸다. 이것은 컨트롤러내 인스턴스 변수처럼 참조를 저장할것이다.

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
this.myComponent = myComponent;
}

우리는 컨트롤러내 어떠한 비지니스 메소드내 인스턴스 변수를 순차적으로 사용할수 있다. 지금 우리가 Spring ApplicationContext 나 BeanFactory밖에서 컨트롤러 객체를 얻는다고 가정하자. 우리는 EJB프록시 객체가 될 LocalStatelessSessionProxyFactoryBean를 설정하는 같은 컨텍스트를 사용할수 있다. 프록시의 설정은 그리고 컨트롤러의 myComponent 프라퍼티의 셋팅은 다음처럼 설정 항목으로 한다.

<bean id="myComponent"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName"><value>myComponent</value></property>
<property name="businessInterface"><value>com.mycom.MyComponent</value></property>
</bean>

<bean id="myController" class = "com.mycom.myController">
<property name="myComponent"><ref bean="myComponent"/></property>
</bean>

Spring AOP프레임워크의 도움으로 비록 당신이 이러한 결과를 즐기기 위해 AOP개념으로 작업을 강제로 하지 않더라도 이 상황뒤에는 마법같은 일이 많다. myComponent bean정의는 비지니스 메소드 인터페이스를 구현하는 EJB를 위한 프록시를 생성한다. EJBlocal home은 시작시 캐시된다. 그래서 하나의 JNDI룩업만이 있다. 각각의 EJB가 호출되는 시점에 프록시는 local EJB의 create()메소드를 호출하고 EJB의 관련된 비지니스 메소드를 호출한다.


myController bean정의는 프록시를 위한 컨트롤러의 myController 프라퍼티를 셋팅한다.


EJB접근 기법은 애플리케이션 코드의 굉장한 단순화를 초래한다. 웹 티어 코드(또는 다른 EJB클라이언트 코드)는 EJB사용의 의존성을 가지지 않는다. 만약 우리가 POJO나 모의(mock)객체 또는 다른 테스트 스텁(stub)을 가진 EJB참조를 교체하기를 원한다면 우리는 자바코드의 한줄의 변경도 없이 myComponent bean정의를 간단하게 변경할수 있다. 추가적으로 우리는 JNDI룩업을 한줄도 쓰지 않거나 우리의 애플리케이션의 일부처럼 다른 EJB 관련 코드를 쓰지 않는다.


실제 애플리케이션에서 벤치마크와 경험은 이 접근법(대상 EJB 반영적인 호출의 포함하는)의 의 성능 오버헤드가 죄소이고 일반적인 사용에서 측정불가능이라는 것을 표시힌다. 애플리케이션 서버내 EJB구조와 관련된 비용때문에 우리는 EJB를 위한 잘 정의된 호출을 만드는것을 원하지 않는다는것을 기억하라.


JNDI룩업에 관련되는 한가지 주의사항이 있다. bean컨테이너에서 이 클래스는 대개 싱글톤처럼(이것을 프로토타입으로 만들기 위한 이유는 없다) 사용되는것이 가장 좋다. 어쩄든 bean컨테이너가 싱글톤(XML ApplicationContext 변형을 하는것처럼) 을 먼저 인스턴스화 한다면 당신은 EJB컨테이너가 대상 EJB를 로드하기 전에 bean컨테이너가 로드된다면 문제를 가지게 된다. 그것은 JNDI룩업이 이 클래스의 init메소드내 수행될것이고 캐시되지만 EJB는 대상 위치에 여전히 바운드되지 않을것이기 때문이다. 해결법은 이 factory객체를 미리 인스턴스화하지 않지만 첫번째 사용시 이것이 생성되는것을 허용한다. XML컨테이너에서 이것은 lazy-init 속성을 통해 컨트롤된다.


비록 이것이 Spring사용자에게 중요 관심사가 되지는 않을것이지만 EJB를 사용한 프로그램에 따른 AOP작업을 하는것은 LocalSlsbInvokerInterceptor를 찾는것을 원할것이다.






16.1.3. remote SLSB에 접근하기



remote EJB에 접근하는것은 local EJB에 접근하는것과 비교해서 SimpleRemoteStatelessSessionProxyFactoryBean를 사용하는것을 제외하고 기본적으로 같다. 물론 Spring을 사용하든 사용하지 않든 remote호출은 의미적으로 적용한다. 다른 컴퓨터내 다른 VM안에서 객체의 메소드를 호출하는것은 때때로 사용 시나리오와 실패(failure)핸들링의 개념으로 다르게 처리된다.


Spring의 EJB 클라이언트 지원은 Spring을 사용하지 않는 접근법에 비해 하나 이상의 장점을 추가한다. 대개 EJB를 local이나 remote로 호출하는 것들간에는 쉽게 진행하고 되돌리기 위한 EJB클라이언트 코드를 위해서는 문제가 있다. 이것은 local인터페이스 메소드가 호출되지 않는동안 remote인터페이스 메소드가 RemoteException를 던지는 것을 명시해야하고 클라이언트 코드는 이것을 다루어야 하기 때문이다. remote EJB로 옮겨질 필요가 있는 local EJB를 위해 쓰여진 클라이언트 코드는 일반적으로 remote 예외를 위한 핸들링을 추가하기 위해 변경되고 local EJB로 옮겨질 필요가 있는 remote EJB를 위해 쓰여진 클라이언트 코드는 remote 예외의 많은 필요없는 핸들링이 수행되지만 같은코드로 그대로 유지될수 있거나 그 코드를 제거하기 위해 변경될 필요가 있다. Spring remote EJB프록시를 사용하여 당신은 당신의 비지니스 메소드 인터페이스내 던져지는 RemoteException을 선언하는것을 대신할수 있고 EJB코드를 구현한다. RemoteException를 던지는 것을 제외하면 동일한 remote 인터페이스를 가지고 그들이 같다면 두개의 인터페이스를 자동적으로 처리하기 위한 프록시에 의존한다. 클라이언트 코드는 체크된 RemoteException을 다루지 않는다. 어떠한 실질적인 RemoteException은 EJB호출이 RuntimeException의 하위클래스인 체크되지 않은 RemoteAccessException 클래스처럼 다시 던져질것이다. 대상 서비스는 그 다음 클라이언트 코드의 인식및 처리가 없이 local EJB나 remote EJB(또는 POJO) 구현물 사이에서 교체될것이다. 물론 이것은 옵션적이다. 당신의 비지니스 인터페이스내 선언된 RemoteExceptions으로 부터 당신을 정지시키는것은 아무것도 없다.






16.2. Spring의 편리한 EJB구현물 클래스를 사용하기.



Spring은 또한 당신이 EJB를 구현하도록 도와주는 편리한 클래스를 제공한다. EJB가 트랜잭션 설정과 원격작업을 책임지도록 놔둔체 POJO내 EJB뒤에 비지니스 로직을 두는 좋은 상황을 만들기 위해 디자인되었다.


비상태유지(stateless) 또는 상태유지(stateful) 세션빈, 또는 메시지빈을 구현하기 위해, 당신은 AbstractStatelessSessionBean, AbstractStatefulSessionBean, 그리고 AbstractMessageDrivenBean/AbstractJmsMessageDrivenBean으로 부터 반복적으로 당신의 구현 클래스를 끌어낸다.


구현물을 명확한 자바 서비스 객체로 실질적으로 위임하는 비상태유지(stateless) 세션빈을 시험하는것을 검토하라. 우리는 비지니스 인터페이스를 가진다.

public interface MyComponent {
public void myMethod(...);
...
}

We have the plain java implementation object:

public class MyComponentImpl implements MyComponent {
public String myMethod(...) {
...
}
...
}

그리고 마지막으로 비상태유지(stateless) 세션빈 자체:

public class MyComponentEJB extends AbstractStatelessSessionBean
implements MyComponent {

MyComponent _myComp;

/**
* Obtain our POJO service object from the BeanFactory/ApplicationContext
* @see org.springframework.ejb.support.AbstractStatelessSessionBean#onEjbCreate()
*/
protected void onEjbCreate() throws CreateException {
_myComp = (MyComponent) getBeanFactory().getBean(
ServicesConstants.CONTEXT_MYCOMP_ID);
}

// for business method, delegate to POJO service impl.
public String myMethod(...) {
return _myComp.myMethod(...);
}
...
}

Spring EJB지원 기초 클래스는 그들의 생명주기처럼 BeanFactory(또는 이 경우 ApplicationContext의 하위클래스)를 디폴트로 생성하고 로드함으로써 이루어진다. 그 다음 EJB(예를 들면 POJO서비스 객체를 얻기 위한 위의 코드내에서 사용된것처럼)에 사용가능하게 된다. 로딩은 BeanFactoryLocator의 하위클래스인 전략(strategy)객체를 통해 이루어진다. BeanFactoryLocator의 실질적인 구현은 디폴트인 JNDI환경 변수(EJB의 경우, java:comp/env/ejb/BeanFactoryPath)처럼 명시된 자원 위치로부터 ApplicationContext을 생성하는 ContextJndiBeanFactoryLocator에 의해 사용된다. 만약 BeanFactory/ApplicationContext 로딩 전략을 변경할 필요가 없다면 디폴트 BeanFactoryLocator구현물은 setBeanFactoryLocator()메소드를 호출하거나 setSessionContext()내, 또는 EJB의 실질적인 생성자내에서 오버라이드되어 사용된다. 좀더 다양한 정보를 위해서는 JavaDoc를 보라.


JavaDoc에서 언급된것처럼 상태유지(stateful) 세션빈은 그들의 생명주기의 일부처럼 수동적이고 재활성화되기 위해 기대되고 EJB컨테이너에 의해 저장되지 않을수 있기 때문에 수동적이고 활성화된 BeanFactory를 로드하지않고 다시 로드하기 위한 ejbPassivateejbActivate로 부터 unloadBeanFactory()loadBeanFactory를 수동으로 호출할 non-serializable한 BeanFactory/ApplicationContext 인스턴스를 사용한다.


EJB의 사용을 위해 ApplicationContext를 로드하기 위한 ContextJndiBeanFactoryLocator의 디폴트 사용법은 몇몇 상황을 위해 적절하다. 어쟀든 모든 EJB가 자신이 복사본을 가진후 ApplicationContext가 많은 수의 bean을 로드하거나 그러한 bean의 초기화가 시간을 소비하거나 메모리 집중적일때 문제가 있다. 이 경우 사용자는 디폴트 ContextJndiBeanFactoryLocator사용법을 오버라이드하길 원할것이고 다중 EJB또는 다른 클라이언트에 의해 사용되기 위한 공유 BeanFactory나 ApplicationContext를 로드하고 사용할수 있는 ContextSingletonBeanFactoryLocatore와 같은 다른 BeanFactoryLocator을 사용한다. 이것을 하는것은 EJB를 위해 유사한 코드를 추가함으로써 비교적 간단하다.

   /**
* Override default BeanFactoryLocator implementation
*
* @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
*/
public void setSessionContext(SessionContext sessionContext) {
super.setSessionContext(sessionContext);
setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
setBeanFactoryLocatorKey(ServicesConstants.PRIMARY_CONTEXT_ID);
}

그것들의 사용법에 대한 좀더 다양한 정보를 위해서 BeanFactoryLocatorContextSingletonBeanFactoryLocatore를 위한 관련 JavaDoc를 보라.



Spring, Struts 통합

다른 웹 프레임워크들과의 통합





14.1. 소개



Spring은 어떠한 자바 기반의 웹 프레임워크와도 쉽게 통합된다. 당신은 ContextLoaderListener 를 당신의 web.xml에 선언하고, 어떤 컨텍스트 파일을 로드할 것인지를 세팅하기 위해 contextConfigLocation <context-param>를 사용하기만 하면 된다.


The <context-param>:

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

The <listener>:

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

노트: Listeners는 Servlet API 2.3 버전에 추가되었다. 만약 당신이 Servlet 2.2 컨테이너를 사용한다면, 당신은 동일한 기능을 얻기 위해 ContextLoaderServlet 를 사용할 수 있다.


만약 당신이 contextConfigLocation 컨텍스트 파라미터를 명시하지 않는다면, ContextLoaderListener는 로드할 /WEB-INF/applicationContext.xml 파일을 찾을 것이다. 일단 컨텍스트 파일이 로드되면, Spring은 빈 정의에 기반하여 WebApplicationContext 객체를 생성하고 이것을 ServletContext에 담아둔다.


모든 자바 웹 프레임워크들은 Servlet API에 기반하여 만들어졌기 때문에, 당신은 Spring이 생성한 ApplicationContext를 얻기 위해 다음의 코드를 사용할 수 있다.

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 클래스는 편의를 위해 만들어진 것인데, 때문에 당신은 ServletContext 속성의 이름을 기억할 필요가 없다. 그것의 getWebApplicationContext() 메써드는 만약 (ApplicationContext) 객체가 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 키로 존재하지 않는다면 null을 반환할 것이다. 어플리케이션에서 NullPointerExceptions를 받는 위험을 감수하는 것보다는 getRequiredWebApplicationContext() 메써드를 사용하는 것이 낫다. 이 메써드는 ApplicationContext를 찾지 못할 경우, 예외를 던진다.


일단 당신이 WebApplicationContext를 참조하게 되면, 당신은 이름 혹은 타입으로 빈들을 가져올 수 있다. 대부분의 개발자들은 빈들을 이름으로 가져와서 그것의 구현된 인터페이스들 중 하나로 캐스팅한다.


운 좋게도 이번 장에서 다루는 대부분의 프레임워크들은 빈들을 룩업하는 방식이 매우 간단하다. 빈들을 BeanFactory로부터 가져오는 것이 쉬울 뿐만 아니라, 컨트롤러에 의존성 삽입(dependency injection)을 사용할 수 있도록 해준다. 각각의 프레임워크 섹션에서 그것의 특화된 통합 전략들에 기반하여 보다 세부적인 사항들을 설명할 것이다.






14.2. JavaServer Faces



JavaServer Faces (JSF) 는 컴포넌트 기반, 이벤트 드리븐 웹 프레임워크이다. Sun Microsystem의 JSF Overview 에 따르면, JSF 기술은 다음을 포함하고 있다.





  • UI 컴포넌트들을 표현하고 그것들의 상태를 관리하며, 이벤트들을 핸들링하고, 밸리데이션을 삽입하고, 페이지 네비게이션을 정의하고, 국제화와 접근성을 지원하는 API들의 세트



  • JSP 페이지 내에서 JavaServer Faces 인터페이스를 표현하기 위한 JavaServer Pages (JSP) 커스텀 태크 라이브러리






14.2.1. DelegatingVariableResolver



당신의 Spring 미들티어를 JSF 웹 레이어와 통합하는 가장 쉬운 방법은 DelegatingVariableResolver 클래스를 사용하는 것이다. 이 변수 처리자(variable resolver)를 당신의 어플리케이션에 설정하려면, faces-context.xml를 수정해야 한다. <faces-config> 요소를 연 이후에, <application> 요소를 추가하고 그 안에 <variable-resolver> 요소를 추가하면 된다. 변수 처리자의 값은 Spring의 DelegatingVariableResolver를 참조해야만 한다.

<faces-config>
<application>
<variable-resolver>org.springframework.web.jsf.DelegatingVariableResolver</variable-resolver>
<locale-config>
<default-locale>en</default-locale>
<supported-locale>en</supported-locale>
<supported-locale>es</supported-locale>
</locale-config>
<message-bundle>messages</message-bundle>
</application>

Spring의 변수 처리자를 명시함으로써, 당신은 Spring 빈들을 당신이 관리하는 빈들의 프라퍼티들로 설정할 수 있다. DelegatingVariableResolver는 처음엔 값을 룩업하는 것을 기반하는 JSF 구현의 디폴트 처리자에 위임한다. 그리고나서 Spring의 루트 WebApplicationContext에 위임한다. 이것은 당신이 JSF에 의해 관리되는 빈들에 의존성을 쉽게 주입할 수 있도록 해준다.


관리되는 빈들은 faces-config.xml 파일에 정의된다. 아래는 Spring의 BeanFactory로부터 가져온 #{userManager} 빈을 어디에 정의하느냐를 보여준다.

<managed-bean>
<managed-bean-name>userList</managed-bean-name>
<managed-bean-class>com.whatever.jsf.UserList</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>userManager</property-name>
<value>#{userManager}</value>
</managed-property>
</managed-bean>

DelegatingVariableResolver는 JSF와 Spring을 통합할 때 적극 추천되는 전략이다. 만약 당신이 보다 튼튼한 통합 예시를 찾는다면, JSF-Spring 프로젝트를 볼 수 있다.






14.2.2. FacesContextUtils



커스텀 VariableResolver는 당신의 프라퍼티들을 faces-config.xml 내의 빈들과 매핑할 때 매우 잘 동작한다. 그러나, 종종 당신은 빈을 명시적으로 가로챌 필요가 있을 것이다. FacesContextUtils 클래스는 그것을 쉽게 해준다. 이 클래스는 WebApplicationContextUtils과 비슷한데, ServletContext 파라미터가 아니라 FacesContext 파라미터를 가진다는 점만 다르다.

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());





14.3. Struts



Struts 는 자바 어플리케이션을 위한 사실상의 웹 프레임워크 그 자체이다. 이것은 주되게 Struts이 가장 먼저 릴리즈된(2001년 6월) 것 중 하나라는 사실에 기인한다. Craig McClanahan에 의해 창안된 Struts은 아파치 소프트웨어 재단에 의해 후원되는 오픈 소스 프로젝트이다. 처음부터, Struts는 JSP/Servlet 프로그래밍 패러다임을 획기적으로 간소화했고 개인 프레임워크들을 사용하던 많은 개발자들을 끌어들였다. 이것은 프로그래밍 모델을 간단하게 했으며 오픈소스였다. 그리고 이것은 이 프로젝트가 성장하고 자바 웹 개발자들 사이에 대중화될 수 있도록 하는 거대한 커뮤니티를 가졌다.


Struts 어플리케이션을 Spring과 통합하는 데는 두 가지 방법이 있다.





  • ContextLoaderPlugin를 사용하여 Spring이 Action들을 빈들로 관리하도록 설정하고 Action들의 의존성을 Spring 컨텍스트 파일에 세팅하는 방법



  • Spring의 ActionSupport 클래스를 상속해서 getWebApplicationContext() 메써드를 사용하여 Spring 관리되는 빈들을 명시적으로 가로채는 방법






14.3.1. ContextLoaderPlugin



ContextLoaderPlugin 은 Struts ActionServlet을 위해 Spring 컨텍스트 파일을 로드하는 Struts 1.1 이상 버전의 플러그인이다. 이 컨텍스트는 ContextLoaderListener에 의해 로드된 WebApplicationContext를 그것의 부모 클래스로 참조한다. 컨텍스트 파일의 디폴트 이름은 매핑된 서블릿의 이름에 -servlet.xml을 더한 것이다. 만약 web.xml에서 ActionServlet<servlet-name>action</servlet-name>라고 정의되었다면, 디폴트는 /WEB-INF/action-servlet.xml이 될 것이다.


이 플러그인을 설정하기 위해서는 다음의 XML을 struts-config.xml 파일의 아래쪽의 plug-ins 섹션에 추가해야 한다.

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"/>

컨텍스트 설정 파일의 위치는 "contextConfigLocation" 프라퍼티를 사용하여 임의대로 정할 수 있다.

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
<set-property property="contextConfigLocation"
value="/WEB-INF/action-servlet.xml.xml,
/WEB-INF/applicationContext.xml"/>
</plug-in>

모든 컨텍스트 파일들을 로드하기 위해 이 플러그인을 사용할 수 있는데, 이것은 StrutsTestCase와 같은 테스팅 툴들을 사용할 때 유용할 것이다. StrutsTestCase의 MockStrutsTestCase는 Listeners를 초기화하지 않을 것이기 때문에, 당신의 컨텍스트 파일들을 플러그인에 담는 것이 필요하다. 이 이슈에 대해서는 버그 정리 를 참조하도록 하라.


이 플러그인을 struts-config.xml에 설정한 이후에야 Action을 Spring에 의해 관리되도록 설정할 수 있다. Spring 1.1.3은 이를 위해 두 가지 방법을 제공한다.





  • Struts의 디폴트 RequestProcessor를 Spring의 DelegatingRequestProcessor로 오버라이드한다.



  • <action-mapping>의 타입 속성에 DelegatingActionProxy 클래스를 사용한다.


이 메써드들 모두 당신이 Action들과 action-context.xml 파일에 있는 그것들의 의존성들을 관리할 수 있게 해준다. struts-config.xmlaction-servlet.xml 내의 Action들은 action-mapping의 "path"와 빈의 "name"으로 연결된다. 당신이 struts-config.xml 파일에 다음과 같은 설정을 가진다고 가정하자.

<action path="/users" .../>

그러면, 당신은 Action 빈을 "/users"라는 이름으로 action-servlet.xml 내에 정의해야만 한다.

<bean name="/users" .../>





14.3.1.1. DelegatingRequestProcessor



DelegatingRequestProcessorstruts-config.xml 파일에 설정하기 위해서는, <controller> 요소 내의 "processorClass"를 오버라이드해야 한다. 이 줄들은 <action-mapping> 요소에 뒤따른다.

<controller>
<set-property property="processorClass"
value="org.springframework.web.struts.DelegatingRequestProcessor"/>
</controller>

이 세팅을 추가한 후에, 당신의 Action은 그것이 무슨 타입이건 간에 자동적으로 Spring의 컨텍스트 파일에서 룩업될 것이다. 사실, 당신은 타입을 명시할 필요조차 없다. 다음의 조각코드들은 둘 다 모두 잘 동작할 것이다.

<action path="/user" type="com.whatever.struts.UserAction"/>  
<action path="/user"/>

만약 당신이 Struts의 modules 특징을 사용한다면, 당신의 빈 이름들은 반드시 모듈 접두어를 포함해야만 한다. 예를 들어, 모듈 접두어 "admin"을 가진 <action path="/user"/>로 정의된 action은 <bean name="/admin/user"/>라는 빈 이름을 가져야 한다.


노트: 만약 당신이 Struts 어플리케이션에서 Tiles를 사용한다면, 당신은 DelegatingTilesRequestProcessor 로 <controller>를 설정해야만 한다.






14.3.1.2. DelegatingActionProxy



만약 직접 작성한 RequestProcessor를 가지고 있어서 DelegatingTilesRequestProcessor를 사용할 수 없는 상황이라면, DelegatingActionProxy 를 action-mapping에서 사용할 수 있다.

<action path="/user" type="org.springframework.web.struts.DelegatingActionProxy"
name="userForm" scope="request" validate="false" parameter="method">
<forward name="list" path="/userList.jsp"/>
<forward name="edit" path="/userForm.jsp"/>
</action>

action-servlet.xml에서의 빈 정의는 직접 작성한 RequestProcessor를 쓰건, DelegatingActionProxy를 쓰건 동일하다.


만약 당신이 컨텍스트 파일에 Action을 정의한다면, 그 Action에 대해 Spring 빈 컨테이너의 모든 특징들을 사용할 수 있을 것이다. 각각의 request에 대한 새로운 Action 인스턴스를 초기화하기 위한 옵션으로 의존성 주입을 사용하는 것 등. 후자를 사용하려면 당신의 Action 빈 정의에 singleton="false"를 추가해주어야 한다.

<bean name="/user" singleton="false" autowire="byName"
class="org.example.web.UserAction"/>





14.3.2. ActionSupport 클래스들



앞에서 언급한 것처럼, WebApplicationContextUtils 클래스를 사용해서 ServletContext로부터 WebApplicationContext를 가져올 수 있다. 더 쉬운 방법은 Struts를 위한 Spring의 Action 클래스들을 상속받는 것이다. 예를 들어, Struts의 Action 클래스를 상속하는 것 대신에 Spring의 ActionSupport 클래스를 상속할 수 있다.


ActionSupport 클래스는 getWebApplicationContext()처럼 부가적인 편의 메써드들을 제공해준다. 아래의 예제는 Action에서 어떻게 이것을 사용할 수 있는지를 보여준다.

public class UserAction extends DispatchActionSupport {

public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("entering 'delete' method...");
}

WebApplicationContext ctx = getWebApplicationContext();
UserManager mgr = (UserManager) ctx.getBean("userManager");

// talk to manager for business logic

return mapping.findForward("success");
}
}

Spring은 모든 표준 Struts Action의 하위 클래스들을 포함한다. - Spring 버전은 단지 이름에 Support만을 붙혀놓았을 뿐이다.




추천하는 전략은 당신의 프로젝트에 가장 잘 맞는 접근방법을 사용하는 것이다. 하위클래스 방법은 당신의 코드를 보다 읽기 쉽게 만들어주며 어떻게 의존성들을 해결할 것인지 명확하게 알게 해준다. 반면, ContextLoaderPlugin을 사용하는 것은 컨텍스트 XML 파일에 새로운 의존성을 추가하는 것을 쉽게 해준다. 어느 쪽이던지, Spring은 두 개의 프레임워크들을 통합하기 위해 몇몇 멋진 옵션들을 제공한다.






14.4. Tapestry



Tapestry는 아파치의 자카르타 프로젝트로부터 나온 강력한 컴포넌트 지향 웹 어플리케이션 프레임워크이다.(http://jakarta.apache.org/tapestry) Spring이 그 자체의 강력한 ui 계층을 가지고 있지만, 웹 ui를 위해 Tapestry와의 조합을 사용하여 J2EE 어플리케이션을 개발하는 것은 독특한 몇가지 장점들이 있다. 이 문서는 이 두 개의 프레임워크들을 조합하기 위한 몇 가지 최선의 실습예제들을 상세시 설명할 것이다. 당신이 Tapestry와 Spring 프레임워크 기본 둘 다에 대해 어느정도 친숙하다고 전제하에서 그것들에 대해서 여기서 설명하지는 않을 것이다. Tapestry와 Spring에 대한 일반적인 소개글은 각각의 웹 사이트들에서 접할 수 있다.






14.4.1. 아키텍쳐



Tapestry와 Spring으로 개발된 전형적인 계층적 J2EE 어플리케이션은 최상위 UI 계층은 Tapestry로, 많은 하위 계층들은 하나 혹은 그 이상의 Spring 어플리케이션 컨텍스트에 의해 관리되는 형태로 구성될 것이다.





  • 사용자 인터페이스 계층


    - 사용자 인터페이스와 관련있다.


    - 몇몇 어플리케이션 로직을 포함한다.


    - Tapestry에 의해 제공된다.


    - Tapestry를 경유하여 UI를 제공하는 것은 별개로 하고, 이 계층에서의 코드는 Service 계층의 인터페이스들을 구현한 객체들을 경유하여 작동한다. 이러한 서비스 계층 인터페이스들을 구현한 실질적인 객체들은 Spring 어플리케이션 컨텍스트로부터 가져온다.



  • 서비스 계층


    - 어플리케이션 특화된 '서비스' 코드


    - 도메인 객체들과 작동하며 그 도메인 객체들을 몇몇가지 저장소(database)에 넣고 빼기 위해 Mapper API를 사용한다.


    - 하나 혹은 그 이상의 Spring 컨텍스트에 의해 관리된다.


    - 이 계층에서의 코드는 도메인 모델 내에서 어플리케이션 특화의 형태로 객체들을 조작한다. 그것은 이 계층 내의 다른 코드와 Mapper API를 통해 작동한다. 이 계층의 객체는 그것이 작동할 때 필요한 특정한 매퍼 구현체들을 Spring 컨텍스트를 통해 받는다.


    - 이 계층의 코드는 Spring 컨텍스트에서 관리되기 때문에, 그 자신의 트랜잭션들을 관리하는 것 대신에, Spring 컨텍스트에 의해 트랜잭션화될 것이다.



  • 도메인 모델


    - 이 도메인에 특화된 데이터와 로직을 다루는 도메인 특화된 객체 계층구조이다.


    - 비록 도메인 객체 계층이 어딘가 저장되고 이것에 대한 몇가지 일반적인 특권(예를 들어, 양방향적인 관계들) 가진다는 개념에 기반하여 만들어진 것이지만, 이것은 일반적으로 다른 계층들에 대해 전혀 알 필요가 없다. 그렇기 때문에, 이것은 독자적으로 테스트될 수 있고, 생산과 테스팅을 위해 서로 다른 매핑 구현체들과 함께 사용될 수 있다.


    - 이 객체들은 독립형이거나 Spring 어플리케이션과 결합되어 사용된다. 이것은 고립성, IoC, 다른 전략적 구현체 등등의 컨텍스트의 몇가지 이점들을 가지게 한다.



  • 데이터 소스 계층


    - (데이터 접근 객체 -DAO 라고도 불리는) Mapper API : 도메인 모델을 몇 가지 (일반적으로 DB이지만 파일시스템, 메모리 등도 될 수 있는) 저장소에 저장할 때 사용되는 API


    - Mapper API 구현체들 : 하나 혹은 그 이상의 Mapper API의 특화된 구현체들, 예를 들어, Hibernate 특화 매퍼, JDO 특화 매퍼, JDBC 특화 매퍼 또는 메모리 메퍼 등


    - 매퍼 구현체들은 하나 혹은 그 이상의 Spring 어플리케이션 컨텍스트에 존재한다. 서비스 계층 객체는 컨텍스트를 통해 작동시 필요한 매퍼 객체들을 받는다.



  • 데이터베이스, 파일시스템 혹은 다른 저장소들


    - 도메인 모델 내의 객체들은 하나 혹은 그 이상의 매퍼 구현체들에 의해 하나 이상의 저장소에 저장된다.


    - 저장소는 파일시스템처럼 매우 간단할 수도 있고, DB에서의 스키마처럼 도메인 모델로부터 자신만의 데이터 표현을 가질 수도 있다. 그러나, 다른 계층들에 대해서는 알지 못한다.






14.4.2. 구현체



유일한 실질적인 질문(이 문서에 의해 설명될 필요가 있는)은 어떻게 Tapestry pages가 Spring 어플리케이션 컨텍스트의 인스턴스로 정의된 빈들인 서비스 구현체들에 접근할 수 있느냐이다.






14.4.2.1. 샘플 어플리케이션 컨텍스트



우리가 xml 형태로 다음과 같은 간단한 어플리케이션 컨텍스트 정의를 가지고 있다고 가정하자.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

<!-- ========================= GENERAL DEFINITIONS ========================= -->

<!-- ========================= PERSISTENCE DEFINITIONS ========================= -->

<!-- the DataSource -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value>java:DefaultDS</value></property>
<property name="resourceRef"><value>false</value></property>
</bean>

<!-- define a Hibernate Session factory via a Spring LocalSessionFactoryBean -->
<bean id="hibSessionFactory"
class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="dataSource"><ref bean="dataSource"/></property>
</bean>

<!--
- Defines a transaction manager for usage in business or data access objects.
- No special treatment by the context, just a bean instance available as reference
- for business objects that want to handle transactions, e.g. via TransactionTemplate.
-->
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
</bean>

<bean id="mapper"
class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
<property name="sessionFactory"><ref bean="hibSessionFactory"/></property>
</bean>

<!-- ========================= BUSINESS DEFINITIONS ========================= -->

<!-- AuthenticationService, including tx interceptor -->
<bean id="authenticationServiceTarget"
class="com.whatever.services.service.user.AuthenticationServiceImpl">
<property name="mapper"><ref bean="mapper"/></property>
</bean>
<bean id="authenticationService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref bean="transactionManager"/></property>
<property name="target"><ref bean="authenticationServiceTarget"/></property>
<property name="proxyInterfacesOnly"><value>true</value></property>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>

<!-- UserService, including tx interceptor -->
<bean id="userServiceTarget"
class="com.whatever.services.service.user.UserServiceImpl">
<property name="mapper"><ref bean="mapper"/></property>
</bean>
<bean id="userService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager"><ref bean="transactionManager"/></property>
<property name="target"><ref bean="userServiceTarget"/></property>
<property name="proxyInterfacesOnly"><value>true</value></property>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>

</beans>

Tapestry 어플리케이션 내에서, 우리는 이 어플리케이션 컨텍스트를 로드할 필요가 있고, Tapestry pages로 하여금 AuthenticationService와 UserService 인터페이스를 각각 구현하고 있는 authenticationService와 userService 빈들을 가지게끔 해야 한다.






14.4.2.2. Tapestry pages에서 빈들을 얻어오기



이 지점에서, Spring의 정적 유틸리티 함수인 WebApplicationContextUtils.getApplicationContext(servletContext)를 호출함으로써 웹 어플리케이션은 어플리케이션 컨텍스트를 사용할 수 있다. 여기에서의 servletContext는 J2EE 명세의 표준 ServletContext를 의미한다. 그렇기 때문에, 페이지가 예를 들어 UserService 인스턴스를 얻기 위한 한가지 간단한 방법은 다음과 같은 코드를 가지는 것이다.

    WebApplicationContext appContext = WebApplicationContextUtils.getApplicationContext(
getRequestCycle().getRequestContext().getServlet().getServletContext());
UserService userService = (UserService) appContext.getBean("userService");
... some code which uses UserService

이러면 된다. 이런 방식은 page 혹은 component를 위한 베이스 클래스의 메써드에 있는 대부분의 기능들을 캡슐화함으로써 난잡함을 최소화할 수 있다. 그러나, 몇 가지 관점에서 이 방식은 Spring이 장려하고, 이 어플리케이션의 다른 계층에서 사용되고 있는 IoC 접근방식에 반대된다. IoC 접근방식 내에서는 이상적으로는 page가 컨텍스트에 이름으로 특정한 빈을 요청하지 않아야 하며, 사실 이상적으로는 page는 컨텍스트에 대해 전혀 몰라야 한다.


운 좋게도, 이것을 가능하게 하는 방식이 있다. 우리는 Tapestry가 이미 page에 프라퍼티를 선언적으로 추가하는 메커니즘을 가지고 있다는 사실에 의존한다. 그리고 이러한 선언적 형태로 page에 대한 모든 프라퍼티들을 관리하는 앞서 언급한 접근방식이 사실상 사용되기 때문에, Tapestry는 그것들의 생명주기를 page와 component 생명주기의 일부로 잘 관리할 수 있다.






14.4.2.3. 어플리케이션 컨텍스트를 Tapestry에 드러내기



먼저 Tapestry page 혹은 Component가 ServletContext를 가지지 않은 채로 ApplicationContext를 사용할 수 있도록 만들 필요가 있다. 이것은 page 혹은 component의 생명주기 내에서 ApplicationContext에 접근할 필요가 생기는 단계에서, ServletContext는 page에서 쉽게 사용하기 힘들기 때문에 우리는 WebApplicationContextUtils.getApplicationContext(servletContext) 를 직접 사용할 수 없다. 하나의 방법은 이것을 드러내주는 Tapestry IEngine의 커스텀 버전을 정의하는 것이다.

package com.whatever.web.xportal;
...
import ...
...
public class MyEngine extends org.apache.tapestry.engine.BaseEngine {

public static final String APPLICATION_CONTEXT_KEY = "appContext";

/**
* @see org.apache.tapestry.engine.AbstractEngine#setupForRequest(org.apache.tapestry.request.RequestContext)
*/
protected void setupForRequest(RequestContext context) {
super.setupForRequest(context);

// insert ApplicationContext in global, if not there
Map global = (Map) getGlobal();
ApplicationContext ac = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
if (ac == null) {
ac = WebApplicationContextUtils.getWebApplicationContext(
context.getServlet().getServletContext()
);
global.put(APPLICATION_CONTEXT_KEY, ac);
}
}
}

이 엔진 클래스는 Spring 어플리케이션 컨텍스트를 Tapestry 어플리케이션의 '글로벌' 객체에 "appContext"라는 속성으로 위치시킬 것이다. 이 특별한 IEngine 인스턴스가 Tapestry 어플리케이션을 위해 사용된되었다는 사실을 Tapestry 어플리케이션 정의 파일에 시작지점으로 등록하는 것을 확실히 해야 한다. 예를 들어보자.

file: xportal.application:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE application PUBLIC
"-//Apache Software Foundation//Tapestry Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<application
name="Whatever xPortal"
engine-class="com.whatever.web.xportal.MyEngine">
</application>






14.4.2.4. Component 정의 파일들



이제 ApplicationContext로부터 우리가 필요한 빈들을 가져오기 위해 page 혹은 component 정의 파일(*.page 혹은 *.jwc)에서 우리는 단순히 property-specification 요소들을 추가하고 그것들을 위한 page 혹은 component 프라퍼티들을 생성하면 된다. 예시를 보자.

    <property-specification name="userService"
type="com.whatever.services.service.user.UserService">
global.appContext.getBean("userService")
</property-specification>
<property-specification name="authenticationService"
type="com.whatever.services.service.user.AuthenticationService">
global.appContext.getBean("authenticationService")
</property-specification>

property-specification 내의 OGNL(Original?) 표현은 프라퍼티를 위한 초기값을 컨텍스트로부터 얻어온 빈으로 나타낸다. 전체 page 정의는 아마 다음과 같을 것이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC
"-//Apache Software Foundation//Tapestry Specification 3.0//EN"
"http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">

<page-specification class="com.whatever.web.xportal.pages.Login">

<property-specification name="username" type="java.lang.String"/>
<property-specification name="password" type="java.lang.String"/>
<property-specification name="error" type="java.lang.String"/>
<property-specification name="callback" type="org.apache.tapestry.callback.ICallback" persistent="yes"/>
<property-specification name="userService"
type="com.whatever.services.service.user.UserService">
global.appContext.getBean("userService")
</property-specification>
<property-specification name="authenticationService"
type="com.whatever.services.service.user.AuthenticationService">
global.appContext.getBean("authenticationService")
</property-specification>

<bean name="delegate" class="com.whatever.web.xportal.PortalValidationDelegate"/>

<bean name="validator" class="org.apache.tapestry.valid.StringValidator" lifecycle="page">
<set-property name="required" expression="true"/>
<set-property name="clientScriptingEnabled" expression="true"/>
</bean>

<component id="inputUsername" type="ValidField">
<static-binding name="displayName" value="Username"/>
<binding name="value" expression="username"/>
<binding name="validator" expression="beans.validator"/>
</component>

<component id="inputPassword" type="ValidField">
<binding name="value" expression="password"/>
<binding name="validator" expression="beans.validator"/>
<static-binding name="displayName" value="Password"/>
<binding name="hidden" expression="true"/>
</component>

</page-specification>






14.4.2.5. abstract accessors 추가하기



이제 page 혹은 component 자체를 위한 Java 클래스 정의를 위해, 우리가 정의한 프라퍼티들에 접근하기 위한 추상 getter 메써드를 추가해야 한다. page 또는 component가 실제로 Tapestry에 의해 로드될 때, 그것은 정의된 프라퍼티들을 추가하기 위해 클래스파일에 런타임 코드 방식을 실행하고, 새롭게 생성된 필드들에 대한 추상 getter 메써드들을 연결한다. 다음의 예를 보자.

    // our UserService implementation; will come from page definition
public abstract UserService getUserService();
// our AuthenticationService implementation; will come from page definition
public abstract AuthenticationService getAuthenticationService();

For completeness, the entire Java class, for a login page in this example, might look like this:

package com.whatever.web.xportal.pages;

/**
* Allows the user to login, by providing username and password.
* After successfully logging in, a cookie is placed on the client browser
* that provides the default username for future logins (the cookie
* persists for a week).
*/
public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {

/** the key under which the authenticated user object is stored in the visit as */
public static final String USER_KEY = "user";

/**
* The name of a cookie to store on the user's machine that will identify
* them next time they log in.
**/
private static final String COOKIE_NAME = Login.class.getName() + ".username";
private final static int ONE_WEEK = 7 * 24 * 60 * 60;

// --- attributes

public abstract String getUsername();
public abstract void setUsername(String username);

public abstract String getPassword();
public abstract void setPassword(String password);

public abstract ICallback getCallback();
public abstract void setCallback(ICallback value);

public abstract UserService getUserService();

public abstract AuthenticationService getAuthenticationService();

// --- methods

protected IValidationDelegate getValidationDelegate() {
return (IValidationDelegate) getBeans().getBean("delegate");
}

protected void setErrorField(String componentId, String message) {
IFormComponent field = (IFormComponent) getComponent(componentId);
IValidationDelegate delegate = getValidationDelegate();
delegate.setFormComponent(field);
delegate.record(new ValidatorException(message));
}

/**
* Attempts to login.
*
* <p>If the user name is not known, or the password is invalid, then an error
* message is displayed.
*
**/
public void attemptLogin(IRequestCycle cycle) {

String password = getPassword();

// Do a little extra work to clear out the password.

setPassword(null);
IValidationDelegate delegate = getValidationDelegate();

delegate.setFormComponent((IFormComponent) getComponent("inputPassword"));
delegate.recordFieldInputValue(null);

// An error, from a validation field, may already have occurred.

if (delegate.getHasErrors())
return;

try {
User user = getAuthenticationService().login(getUsername(), getPassword());
loginUser(user, cycle);
}
catch (FailedLoginException ex) {
this.setError("Login failed: " + ex.getMessage());
return;
}
}

/**
* Sets up the {@link User} as the logged in user, creates
* a cookie for their username (for subsequent logins),
* and redirects to the appropriate page, or
* a specified page).
*
**/
public void loginUser(User user, IRequestCycle cycle) {

String username = user.getUsername();

// Get the visit object; this will likely force the
// creation of the visit object and an HttpSession.

Map visit = (Map) getVisit();
visit.put(USER_KEY, user);

// After logging in, go to the MyLibrary page, unless otherwise
// specified.

ICallback callback = getCallback();

if (callback == null)
cycle.activate("Home");
else
callback.performCallback(cycle);

// I've found that failing to set a maximum age and a path means that
// the browser (IE 5.0 anyway) quietly drops the cookie.

IEngine engine = getEngine();
Cookie cookie = new Cookie(COOKIE_NAME, username);
cookie.setPath(engine.getServletPath());
cookie.setMaxAge(ONE_WEEK);

// Record the user's username in a cookie

cycle.getRequestContext().addCookie(cookie);

engine.forgetPage(getPageName());
}

public void pageBeginRender(PageEvent event) {
if (getUsername() == null)
setUsername(getRequestCycle().getRequestContext().getCookieValue(COOKIE_NAME));
}
}






14.4.3. 개요



이 예제에서, 우리는 Spring ApplicationContext에 정의된 서비스 빈들을 선언적 형태로 page에 제공되도록 처리해왔다. page 클래스는 서비스 구현체들이 어디에서 왔는지 알지 못하며, 때문에 예를 들어, 테스트동안 다른 구현체로 살짝 빠져나가기 쉽다. 이러한 IoC는 Spring 프레임워크의 주요한 목적과 이점의 하나이며, 우리는 이 Tapestry 어플리케이션에서의 J2EE 스택을 향한 모든 지점에서 이것을 확장하도록 있도록 처리해왔다.






14.5. WebWork



WebWork 는 간단함을 염두에 두고 설계된 웹 프레임워크이다. 이것은 일반적인 command 프레임워크인 XWork 를 기반으로 개발되었다. XWork 역시 IoC 컨테이너를 가지고 있지만, 이것은 Spring만큼 총체적인 특징을 가지고 있지 못하며, 이 부분에서 그것을 다루지는 않을 것이다. WebWork 컨트롤러들은 Actions로 불리는데, 그것은 Action 인터페이스를 구현해야만 하기 때문일 것이다. ActionSupport 클래스는 이 인터페이스를 구현하는데, WebWork action들을 위한 일반적인 부모 클래스이다.


WebWork는 xwork-optional 프로젝트의 java.net에 위치한 자체 Spring 통합 프로젝트를 유지한다. 현재적으로, WebWork와 Spring을 통합하는데는 세 가지의 선택이 가능하다.





  • SpringObjectFactory: XWork의 디폴트 ObjectFactory 를 오버라이드 해서 XWork는 Spring 빈들을 루트 WebApplicationContext에서 찾을 것이다.



  • ActionAutowiringInterceptor: Action의 의존성들을 그것들이 생성될 때 인터셉터를 사용하여 자동으로 묶는다.



  • SpringExternalReferenceResolver: <action> 요소의 <external-ref> 요소에서 정의된 이름에 기반하여 Spring 빈들을 룩업한다.


이 모든 전략들은 WebWork's Documentation 에서 보다 상세하게 설명되어 있다.