struts로 검색한 결과 :: 시소커뮤니티[SSISO Community]
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분

회원가입 I 비밀번호 찾기


SSISO Community검색
SSISO Community메뉴
[카페목록보기]
[블로그등록하기]  
[블로그리스트]  
SSISO Community카페
블로그 카테고리
정치 경제
문화 칼럼
비디오게임 스포츠
핫이슈 TV
포토 온라인게임
PC게임 에뮬게임
라이프 사람들
유머 만화애니
방송 1
1 1
1 1
1 1
1 1
1

struts로 검색한 결과
등록일:2008-06-10 17:46:32
작성자:
제목:EJB 컴포넌트와 SPRING AOP 프레임워크의 연계 사용


개요

급성장하는 개발자 커뮤니티, 다양한 백엔드 기술(JMS, JTA, JDO, Hibernate, iBATIS 등) 지원 그리고 무엇보다도 비간섭적(non-intrusive) 경량 IoC 컨테이너와 내장 AOP 런타임으로 인해 Spring Framework는 J2EE 애플리게이션 개발에 매우 유용한 방법으로 부각되고 있습니다. Spring 관리 컴포넌트(POJO)는 EJB와 공존할 수 있는 것은 물론, AOP 접근법을 사용하여 모니터링 및 검사(auditing), 캐싱 그리고 애플리케이션 레벨 보안에서부터 애플리케이션 특정 비즈니스 요건의 해결에 이르기까지 엔터프라이즈 애플리케이션의 다양한 측면을 처리할 수 있도록 해 줍니다.

본 기술자료에서는 J2EE 애플리케이션 내에서 Spring의 AOP 프레임워크를 사용하는 여러 가지 실제 사례를 제시합니다.

소개

J2EE 기술은 서버 측 애플리케이션과 미들웨어 애플리케이션의 구현에 토대를 제공합니다. BEA WebLogic Server와 같은 J2EE 컨테이너는 애플리케이션 라이프 사이클, 보안, 트랜잭션, 리모팅(remoting) 및 동시 작업을 비롯한 시스템 레벨 요소를 관리하며 JDBC, JMS 및 JTA와 같은 공통 서비스를 지원합니다. 하지만 J2EE의 탁월함과 복잡성이 개발과 테스트를 어렵게 하는 걸림돌이 되기도 합니다. 기존의 J2EE 애플리케이션은 대체로 컨테이너의 JNDI를 통해 제공되는 서비스에 크게 의존하고 있습니다. 이는 상당량의 직접적인 JNDI 룩업(lookup)이 필요하거나 또는 다소 진보한 형태인 Service Locator 패턴을 사용한다는 것을 의미합니다. 이러한 아키텍처는 컴포넌트 간의 결합을 강화하기 때문에 개별 테스트가 거의 불가능해집니다. 이런 아키텍처의 문제점에 대한 자세한 분석을 살펴 보려면 Spring Framework의 저자가 집필한 "J2EE Development without EJB"를 참조하시기 바랍니다.

Spring Framework를 통해 기존의 J2EE 인프라스트럭처를 사용한 순수 자바 객체로 구현된 비즈니스 로직과 연결하고 J2EE 컴포넌트와 서비스를 사용하는 데 필요한 코드의 양을 획기적으로 줄일 수 있습니다. 그 외에도 기존 OO 디자인과 직교 방식의 AOP 컴포넌트화 기법을 접목시킬 수 있습니다. 본 기술자료의 후반부에서는 Spring 관리 자바 객체를 사용할 수 있도록 J2EE 컴포넌트를 리팩토링하는 방법에 대해 설명하고, 이어서 AOP 접근법을 적용하여 손쉽게 컴포넌트를 분리하고 테스트할 수 있도록 하는 새로운 기능을 구현하는 과정이 소개됩니다.

다른 AOP 툴에 비해 Spring은 제한된 AOP 기능을 제공하는데, AOP 구현과 Spring IoC 컨테이너 간의 긴밀한 통합을 통해 공통적인 애플리케이션 문제를 해결한다는 데 그 목적이 있습니다. 이러한 통합은 비간섭적인(non-intrusive) 방식으로 이루어지기 때문에 동일한 애플리케이션에서 Spring AOP와 AspectJ 같이 보다 표현력이 풍부한(expressive)한 프레임워크를 함께 사용할 수 있습니다. Spring AOP는 순수 자바 클래스를 사용하며 특별한 컴파일 프로세스나 클래스 로더 계층 제어 또는 디플로이먼트 구성 변경이 필요하지 않습니다. 대신 프록시 패턴을 사용하여 Spring IoC 컨테이너에 의해 관리되어야 하는 대상 객체에 어드바이스를 적용합니다.

상황에 따라 두 가지 타입의 프록시 중 하나를 선택할 수 있습니다.
  • 첫 번째 타입은 인터페이스에만 적용될 수 있는 자바 동적 프록시에 기반하고 있으며 표준 자바 기능인 동시에 뛰어난 성능을 제공합니다

  • 두 번째 타입은 대상 객체가 인터페이스를 구현하지 않거나 이러한 인터페이스를 활용할 수 없는 경우에만 사용됩니다(예: 레거시 코드의 경우). 이는 CGLIB library를 사용하는 런타임 바이트 코드 생성에 기반을 두고 있습니다.

프록시된 객체의 경우에 Spring을 통해 정적(정확한 이름이나 정규 표현식 또는 주석을 기반으로 한 메서드 매칭) 또는 동적(cflow 포인트 컷 타입을 포함한 매칭이 런타임에 수행됨) 포인트컷 정의를 사용하여 특정 어드바이스를 지정할 수 있으며, 각 포인트 컷은 하나 또는 여러 어드바이스와 연결될 수 있습니다. around, before, after returning, throws, introduction과 같은 어드바이스 타입이 지원됩니다. 본 기술자료의 후반부에서 around 어드바이스에 대한 예가 제시됩니다. 보다 자세한 내용을 확인하려면 Spring AOP 프레임워크 설명서를 참조하시기 바랍니다.

앞서 설명한 바와 같이 Spring IoC 컨테이너에 의해 관리되는 대상 객체에 대해서만 어드바이스할 수 있습니다. 그러나 J2EE 애플리케이션에서는 컴포넌트의 라이프 사이클이 애플리케이션 서버에 의해 관리되며, 통합 유형에 따라 J2EE 애플리케이션 컴포넌트는 다음과 같은 일반 엔드 포인트 타입 중 하나를 사용하는 원격 또는 로컬 클라이언트에 노출될 수 있습니다.

  • 무상태(Stateless), 상태유지(Stateful) 또는 엔티티 빈(bean), 로컬 또는 원격 (RMI-IOOP를 통해)
  • 로컬 또는 외부 JMS 큐(queue)와 토픽 또는 인바운드 JCA 엔드 포인트에서 수신하는 메시지 드리븐 빈(MDB)
  • 서블릿(struts 또는 다른 최종 사용자 UI 프레임 워크, XML-RPC 그리고 SOAP기반 인터페이스 포함)

Figure 1
그림 1. 일반 엔드-포인트 타입

이러한 엔드-포인트에서 Spring의 AOP 프레임워크를 사용하려면 모든 비즈니스 로직을 Spring 관리 빈(bean)으로 이동시켜야 합니다. 그러면 서버 관리 컴포넌트를 사용하여 호출을 위임하고 선택적으로 트랜잭션 경계 및 보안 컨텍스트를 정의할 수 있습니다. 본 기술자료에서도 트랜잭션 문제를 별도로 다루겠지만 리소스 부분에서 이와 관련된 다른 여러 가지 자료를 살펴볼 수 있습니다.

이제 Spring 기능을 사용할 수 있도록 J2EE 애플리케이션을 리팩토링하는 방법에 대해 자세히 살펴보겠습니다. XDoclet의 JavaDoc 기반 메타데이터를 사용하여 홈 인터페이스와 빈 인터페이스는 물론 EJB 디플로이먼트 디스크립터를 생성합니다. 본 문서에서 사용된 모든 샘플 클래스의 전체 소스 코드는 아래 다운로드 섹션에서 제공됩니다.

Spring의 EJB 클래스를 사용할 수 있도록 EJB 컴포넌트 리팩토링

현 거래가를 리턴하고 새 거래가를 설정할 수 있도록 하는 간단한 주가 정보 EJB 컴포넌트를 상상해 보십시오. 이 예는 주식 관리 애플리케이션의 작성 방법을 보여주려는 것이 아니라 Spring Framework와 J2EE 서비스의 연계 사용에 관련된 다양한 통합 측면과 최상의 사용 방법을 소개하기 위한 것입니다. 요구사항대로라면 TradeManager 비즈니스 인터페이스는 다음과 같습니다.

public interface TradeManager {
  public static String ID = "tradeManager";

  public BigDecimal getPrice(String name);
  
  public void setPrice(String name, BigDecimal price);
  
}

J2EE 애플리케이션의 일반 설계는 지속성 레이어의 facade와 엔티티 빈으로서 원격 무상태(stateless) 세션 빈을 사용합니다. 아래 TradeManager1Impl 무상태(stateless) 세션 빈에서 TradeManager 인터페이스의 가능한 구현에 대해 설명합니다. 이것은 ServiceLocator를 사용하여 로컬 엔티티 빈 TradeImpl의 홈 인터페이스를 검색합니다. XDoclet 주석은 EJB 디스크립터의 파라미터를 선언하고 EJB 컴포넌트의 노출된 메서드를 정의하는 데 사용됩니다.

/**
 * @ejb.bean
 *   name="org.javatx.spring.aop.TradeManager1"
 *   type="Stateless"
 *   view-type="both"
 *   transaction-type="Container"
 *
 * @ejb.transaction type="NotSupported"
 * 
 * @ejb.home
 *   remote-pattern="{0}Home"
 *   local-pattern="{0}LocalHome"
 *
 * @ejb.interface
 *   remote-pattern="{0}"
 *   local-pattern="{0}Local"
 */
public class TradeManager1Impl implements SessionBean, TradeManager {
  private SessionContext ctx;

  private TradeLocalHome tradeHome;

  
  /**
   * @ejb.interface-method view-type="both"
   */ 
  public BigDecimal getPrice(String symbol) {
    try {
      return tradeHome.findByPrimaryKey(symbol).getPrice();
    } catch(ObjectNotFoundException ex) {
      return null;
    } catch(FinderException ex) {
      throw new EJBException("Unable to find symbol", ex);
    }
  }

  /**
   * @ejb.interface-method view-type="both"
   */ 
  public void setPrice(String symbol, BigDecimal price) {
    try {
      try {
        tradeHome.findByPrimaryKey(symbol).setPrice(price);
      } catch(ObjectNotFoundException ex) {
        tradeHome.create(symbol, price);
      }
    } catch(CreateException ex) {
      throw new EJBException("Unable to create symbol", ex);
    } catch(FinderException ex) {
      throw new EJBException("Unable to find symbol", ex);
    }
  }

  
  public void ejbCreate() throws EJBException {
    tradeHome = ServiceLocator.getTradeLocalHome();
  }
  
  public void ejbActivate() throws EJBException, RemoteException {
  }
  
  public void ejbPassivate() throws EJBException, RemoteException {
  }
  
  public void ejbRemove() throws EJBException, RemoteException {
  }

  public void setSessionContext(SessionContext ctx) throws EJBException, RemoteException {
    this.ctx = ctx;
  }
  
}

코드가 변경됐을 때 이러한 컴포넌트를 테스트하려면 테스트 전에 빌드, 컨테이너 시작 및 애플리케이션 디플로이의 전체 사이클을 거쳐야 합니다(주로 Cactus나 MockEJB같은 특수한 인컨테이너(in-container) 테스팅 프레임워크에 기반함). 간단한 경우 hot 클래스 교체를 통해 리디플로이 시간을 절약할 수 있지만 클래스 스키마가 변경되었을 경우에는 작동하지 않습니다(예를 들면, 필드나 메서드가 추가되거나 메서드 서명이 변경되었을 경우). 이 문제점만으로도 모든 로직을 순수 자바 오브젝트로 옮겨야 하는 충분한 이유가 됩니다. TradeManager1Impl 코드에서 볼 수 있듯이 연결 코드(glue code)의 많은 라인들을 EJB에 모두 함께 배치하면 JNDI 액세스 및 예외 처리와 관련한 코드 복제를 피할 수 있습니다. 하지만 Spring은 J2EE 인터페이스를 직접 구현하는 대신 사용자 정의 EJB 빈에 의해 확장될 수 있는 추상 컨비니언스 클래스(abstract convenience class)를 제공합니다. 이런 추상 수퍼 클래스(abstract super class)를 통해 사용자 정의 빈으로부터 대부분의 연결 코드 (glue code)를 제거할 수 있으며 Spring 애플리케이션 컨텍스트의 인스턴스를 검색하는 메서드를 제공할 수도 있습니다.

먼저 TradeManager 인터페이스를 구현하는 새로운 순수 자바 클래스인 TradeDaoTradeManager1Impl의 모든 로직을 이동시켜야 합니다. TradeImpl CMP 엔티티 빈을 지속성 메커니즘으로 유지하게 되는데, 그 이유는 WebLogic 서버가 CMP 빈의 성능을 조정하는 많은 튜닝 옵션을 제공하기 때문이며 특별한 경우에 이러한 빈은 매우 뛰어난 성능을 제공할 수도 있습니다(본 문서 리소스 섹션의 CMP 성능 튜닝 부분 참조). 자세한 설명은 본 기술자료의 서술 범위를 벗어나기 때문에 생략하도록 하겠습니다. 아래 코드에서 볼 수 있듯이, Spring IoC 컨테이너를 사용하여 TradeImpl 엔티티 빈의 홈 인터페이스를 TradeDao의 생성자(constructor)에 삽입할 수도 있습니다.

public class TradeDao implements TradeManager {
  private TradeLocalHome tradeHome;
  
  public TradeDao(TradeLocalHome tradeHome) {
    this.tradeHome = tradeHome;
  }
  
  public BigDecimal getPrice(String symbol) {
    try {
      return tradeHome.findByPrimaryKey(symbol).getPrice();
    } catch(ObjectNotFoundException ex) {
      return null;
    } catch(FinderException ex) {
      throw new EJBException("Unable to find symbol", ex);
    }
  }

  public void setPrice(String symbol, BigDecimal price) {
    try {
      try {
        tradeHome.findByPrimaryKey(symbol).setPrice(price);
      } catch(ObjectNotFoundException ex) {
        tradeHome.create(symbol, price);
      }
    } catch(CreateException ex) {
      throw new EJBException("Unable to create symbol", ex);
    } catch(FinderException ex) {
      throw new EJBException("Unable to find symbol", ex);
    }
  }

}

이제 위에서 생성한 TradeDao빈의 Spring 관리 인스턴스도 가져올 수 있도록 해주는 Spring의 AbstractStatelessSessionBean 추상 클래스를 사용하여 TradeManager1Impl를 새로 작성할 수 있습니다.

/**
 * @ejb.home
 *   remote-pattern="TradeManager2Home"
 *   local-pattern="TradeManager2LocalHome"
 *   extends="javax.ejb.EJBHome"
 *   local-extends="javax.ejb.EJBLocalHome"
 *
 * @ejb.transaction type="NotSupported"
 * 
 * @ejb.interface
 *   remote-pattern="TradeManager2"
 *   local-pattern="TradeManager2Local"
 *   extends="javax.ejb.SessionBean"
 *   local-extends="javax.ejb.SessionBean, org.javatx.spring.aop.TradeManager"
 *
 * @ejb.env-entry
 *   name="BeanFactoryPath" 
 *   value="applicationContext.xml"
 */   
public class TradeManager2Impl extends AbstractStatelessSessionBean implements TradeManager {
  private TradeManager tradeManager;

  public void setSessionContext(SessionContext sessionContext) {
     super.setSessionContext(sessionContext);
     // make sure there will be the only one Spring bean config
     setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
  }
  
  public void onEjbCreate() throws CreateException {
    tradeManager = (TradeManager) getBeanFactory().getBean(TradeManager.ID);
  }
  
  /**
   * @ejb.interface-method view-type="both"
   */ 
  public BigDecimal getPrice(String symbol) {
    return tradeManager.getPrice(symbol);
  }

  /**
   * @ejb.interface-method view-type="both"
   */ 
  public void setPrice(String symbol, BigDecimal price) {
    tradeManager.setPrice(symbol, price);
  }

}

EJB는 이제 모든 호출을 onEjbCreate() 메서드의 Spring에서 획득한 TradeManager 인스턴스로 위임합니다. AbstractEnterpriseBean에서 구현된 getBeanFactory() 메서드는 Spring 애플리케이션 컨텍스트를 검색 및 생성하는 데 필요한 모든 작업을 처리합니다. 하지만 EJB가 빈 선언을 통해 설정 파일의 위치를 Spring이 알 수 있게 하기 위해 EJB 디플로이 디스크립터에서 BeanFactoryPath env-entry를 선언해야 합니다. 위의 예는 XDoclet 주석을 사용하여 이 정보를 생성했습니다.

또한 AbstractStatelessSessionBean에 EJB 빈 전반에 걸쳐 Spring의 애플리케이션 컨텍스트의 단일 인스턴스를 사용하도록 지시하기 위해 setSessionContext() 메서드를 덮어썼다는 점에 유념하시기 바랍니다.

이제 applicationContext.xml에서 tradeManager 빈을 선언할 수 있습니다. 기본적으로 위의 TradeDao 클래스의 새로운 인스턴스를 생성하여 해당 생성자로 전달해야 합니다. 이 인스턴스는 JNDI에서 가져온 TradeLocalHome의 인스턴스입니다. 다음은 가능한 정의를 나타낸 것입니다.

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

<beans>

  <bean id="tradeManager" class="org.javatx.spring.aop.TradeDao">
    <constructor-arg index="0">
      <bean class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName">
          <bean id="org.javatx.spring.aop.TradeLocalHome.JNDI_NAME"
                class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
        </property>
        <property name="proxyInterface" value="org.javatx.spring.aop.TradeLocalHome"/>
      </bean>
    </constructor-arg>
  </bean>

</beans>

Spring의 JndiObjectFactoryBean을 사용하여 JNDI 컨텍스트에서 가져와 tradeManager에 생성자 파라미터로서 삽입되는 익명으로 정의된 TradeLocalHome 인스턴스를 사용했습니다. 또한 TradeLocalHome에 실제 JNDI 이름을 하드 코딩하지 않고 그 대신 정적 필드에서 가져오기 위해(이 경우 TradeLocalHome.JNDI_NAME) FieldRetrievingFactoryBean을 사용했습니다. 일반적으로 위의 예에 나타난 바와 같이 JndiObjectFactoryBean을 사용할 때 proxyInterface 속성을 선언하는 것이 좋습니다.

JndiObjectFactoryBean을 사용하여 JNDI에서 EJB 홈 인터페이스를 가져오는 이 메서드는 JDBC 데이터 소스, JMS와 JCA 연결 팩토리 및 JavaMail 세션 등의 J2EE 컨테이너에 노출된 다른 모든 서비스에도 적용됩니다.

세션 빈에 더 간단하게 액세스하는 방법이 있습니다. Spring의 LocalStatelessSessionProxyFactoryBean을 통해 홈 인터페이스를 거치지 않고 바로 세션 빈을 가져올 수 있습니다. 예를 들어, 또 다른 Spring 관리 빈의 로컬 인터페이스를 통해 액세스되는 MyComponentImpl 세션 빈의 사용 방법에 대해 보여줍니다.

  <bean id="tradeManagerEjb" 
        class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
    <property name="jndiName">
      <bean id="org.javatx.spring.aop.TradeManager2LocalHome.JNDI_NAME"
            class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
    </property>
    <property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/>
  </bean>

이런 접근법의 장점은 SimpleRemoteStatelessSessionProxyFactoryBean을 사용하여 Spring 컨텍스트의 빈(bean) 선언만 변경하면 로컬 인터페이스에서 원격 인터페이스로 쉽게 전환할 수 있다는 것입니다. 예를 들면 다음과 같습니다.

  <bean id="tradeManagerEjb" 
        class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean">
    <property name="jndiName">
      <bean id="org.javatx.spring.aop.TradeManager2Home.JNDI_NAME"
            class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
    </property>
    <property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/>
    <property name="lookupHomeOnStartup" value="false"/>
  </bean>

lookupHomeOnStartup 속성은 lazy initialization을 수행할 수 있도록 false로 설정되어 있습니다.

이 시점에서 그간의 성과를 정리해 보겠습니다.

  • 위의 리팩토링은 소위 종속성 삽입(dependency injection) 및 AOP라는 향상된 Spring 기능의 활용 기반을 마련했습니다.
  • 클라이언트 API를 변경하지 않고도 모든 비즈니스 로직을 facade 세션 빈 외부로 이동시켰으며 이로 인해 이 EJB는 변경에 대한 저항성이 향상되고 테스트가 용이해졌습니다.
  • 이제 비즈니스 로직은 종속성(dependencies)이 JNDI의 리소스를 요구하지 않거나 이러한 종속성을 stub이나 mock으로 교체할 수 있다면 컨테이너 외부에서도 테스트할 수 있는 순수 자바 객체에 상주합니다.
  • 이제 자바 코드를 변경하지 않고도 서로 다른 tradeManager 구현을 대체하거나 초기화 파라미터 또는 종속 컴포넌트를 변경할 수 있습니다.

이제 모든 준비 단계가 완료되었으며 TradeManager 서비스의 새로운 요건 하에서 작업을 시작할 수 있게 되었습니다.

Spring에 의해 관리되는 컴포넌트 어드바이스

이전 섹션에서 Spring 관리 빈을 사용하여 서비스 엔트리 포인트를 리팩토링했습니다. 이제 이를 통해 어떻게 컴포넌트를 향상시키고 새로운 기능을 구현할 수 있는지에 대해 설명하겠습니다.

먼저 사용자들이 TradeManager 컴포넌트에 의해 관리되지 않는 symbol의 가격을 알고 싶어한다고 가정해 봅시다. 이는 현재 취급하지 않는 요청된 symbol의 현 시세를 검색하기 위해 외부 서비스에 연결해야 한다는 것을 의미합니다. 일례로 실제 애플리케이션이 Reuters, Thomson, Bloomberg, NAQ와 같은 실시간 데이터 서비스 벤더들이 공급하는 데이터에 연결되어 있지만 Yahoo 포털의 무료 HTTP기반 서비스를 사용할 수 있습니다.

동일한 TradeManager 인터페이스를 구현하는 새로운 YahooFeed 컴포넌트를 생성하여 Yahoo 금융 포털에서 가격 정보를 가져옵니다. 간단한 구현은 HttpURLConnection을 사용하여 HTTP 요청을 보낸 다음 정규식을 사용하여 응답을 파싱(parse)합니다. 예를 들면 다음과 같습니다.

public class YahooFeed implements TradeManager {
  private static final String SERVICE_URL = "http://finance.yahoo.com/d/quotes.csv?f=k1&s=";

  private Pattern pattern = Pattern.compile("\"(.*) - (.*)\"");
  
  public BigDecimal getPrice(String symbol) {
    HttpURLConnection conn;
    String responseMessage;
    int responseCode;
    try {
      URL serviceUrl = new URL(SERVICE_URL+symbol);
      conn = (HttpURLConnection) serviceUrl.openConnection();
      responseCode = conn.getResponseCode();
      responseMessage = conn.getResponseMessage();
    } catch(Exception ex) {
      throw new RuntimeException("Connection error", ex);
    }
    
    if(responseCode!=HttpURLConnection.HTTP_OK) {
      throw new RuntimeException("Connection error "+responseCode+" "+responseMessage);
    }
      
    String response = readResponse(conn);
    Matcher matcher = pattern.matcher(response);
    if(!matcher.find()) {
      throw new RuntimeException("Unable to parse response ["+response+"] for symbol "+symbol);
    }
    String time = matcher.group(1);
    if("N/A".equals(time)) {
      return null;  // unknown symbol
    }
    String price = matcher.group(2);
    return new BigDecimal(price);
  }

  public void setPrice(String symbol, BigDecimal price) {
    throw new UnsupportedOperationException("Can't set price of 3rd party trade");
  }

  private String readResponse(HttpURLConnection conn) {
    // ...
    return response;
  }

}

이 구현 작업이 끝나고 테스트(컨테이너 외부에서!)를 통과하면 다른 컴포넌트와 통합할 수 있습니다. 일반적으로 getPrice() 메서드로부터 리턴되는 값을 확인하기 위해 TradeManager2Impl에 일부 코드를 추가합니다. 이는 최소한 테스트 수의 두 배에 이를 것이며 각 테스트마다 추가적인 사전조건(precondition)을 설정해 합니다. 하지만 Spring AOP 프레임워크를 이용하면 훨씬 간편하게 처리할 수 있습니다. 원래의 TradeManager가 요청된 symbol에 대한 값을 리턴하지 않으면(이 경우의 값은 null이지만 UnknownSymbol 예외를 캐치할 수도 있습니다) YahooFeed 컴포넌트를 사용하여 가격을 검색하라는 어드바이스를 구현할 수 있습니다.

어드바이스를 구체적인 메서드에 적용하려면 Spring의 빈 구성에 Advisor를 선언해야 합니다. 편리한 NameMatchMethodPointcutAdvisor 클래스를 사용하여 이름별로 메서드를 선택할 수 있습니다. 이 경우에는 다음과 같이 getPrice가 필요합니다.

  <bean id="yahooFeed" class="org.javatx.spring.aop.YahooFeed"/>

  <bean id="foreignTradeAdvisor" 
        class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
    <property name="mappedName" value="getPrice"/>
    <property name="advice">
      <bean class="org.javatx.spring.aop.ForeignTradeAdvice">
        <constructor-arg index="0" ref="yahooFeed"/>
      </bean>
    </property>
  </bean>

주지하다시피 위의 어드바이저는 ForeignTradeAdvicegetPrice() 메서드에 지정합니다. Spring AOP 프레임워크는 어드바이스 클래스에 AOP Alliance API를 사용하며 이는 ForeignTradeAdvice 관련 어드바이스가 MethodInterceptor 인터페이스를 구현해야 한다는 의미입니다. 예를 들면 다음과 같습니다.

public class ForeignTradeAdvice implements MethodInterceptor {
  private TradeManager tradeManager;
  
  public ForeignTradeAdvice(TradeManager manager) {
    this.tradeManager = manager;
  }
  
  public Object invoke(MethodInvocation invocation) throws Throwable {
    Object res = invocation.proceed();
    if(res!=null) return res;

    Object[] args = invocation.getArguments();
    String symbol = (String) args[0];
    return tradeManager.getPrice(symbol);
  }

}

위의 코드는 invocation.proceed()를 사용하여 원래의 컴포넌트를 호출하며 null이 리턴되면 어드바이스 생성 시에 생성자 파라미터로서 삽입된 다른 tradeManager를 호출합니다. 위의 foreignTradeAdvisor 빈의 선언을 참조하십시오.

이제 Spring의 빈 구성에 정의된 tradeManager 빈의 이름을 baseTradeManager로 변경할 수 있으며 ProxyFactoryBean을 사용하여 tradeManager를 프록시로 선언할 수 있습니다. 새로운 baseTradeManager는 위에서 정의된 foreignTradeAdvisor와 함께 어드바이스하는 대상이 됩니다.

  <bean id="baseTradeManager" class="org.javatx.spring.aop.TradeDao">
    ... same as tradeManager definition in the above example
  </bean>

  <bean id="tradeManager" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="org.javatx.spring.aop.TradeManager"/>
    <property name="target" ref="baseTradeManager"/>
    <property name="interceptorNames">
      <list>
        <idref local="foreignTradeAdvisor"/>
      </list>
    </property>
  </bean>

기본적으로 다음과 같습니다. 원래의 컴포넌트를 변경하지 않고 Spring애플리케이션 컨텍스트만을 사용하여 종속성을 재구성함으로써 추가적인 기능을 구현했습니다. Spring AOP 프레임워크 없이 클래식 EJB 컴포넌트에서 유사한 변경을 수행하려면 EJB에 로직을 추가하거나(테스트의 어려움을 가중시킴) 데코레이터 패턴(decorator pattern)을 사용해야(EJB의 수를 증가시키며 테스트의 복잡성과 디플로이 시간을 증가시킴) 합니다. 위의 예에서 보았듯이 Spring을 사용하면 아무런 변경 없이도 추가 로직을 기존의 컴포넌트에 쉽게 연결할 수 있습니다. 이제는 견고하게 결합된 빈 대신에 개별적으로 테스트할 수 있고 Spring 프레임워크를 사용하여 조합할 수 있는 여러 개의 경량 컴포넌트를 확보하게 되었습니다. 이 접근법에서 ForeignTradeAdvice는 자체 기능을 구현하고 있을 뿐 아니라, 다음 섹션에 제시된 바와 같이 애플리케이션 서버 외부의 독립 실행형 단위로 테스트될 수 있는 독립 컴포넌트입니다.

어드바이스 코드 테스트

주지하다시피 코드는 TradeDaoYahooFeed에 대한 종속성을 가지고 있지 않습니다. 그러므로 이 컴포넌트는 mock 객체 테스트를 사용하여 완전히 독립적으로 테스트할 수 있습니다. mock 객체 테스트 접근 방법을 통해 컴포넌트 실행 전에 예상치를 선언할 수 있으며 컴포넌트가 호출되는 동안 이런 예상치가 충족되었는지 확인할 수 있습니다. mock 테스트에 대한 보다 자세한 내용을 확인하려면 리소스 섹션을 참조하십시오. 이제 예상치 선언을 위한 유연하고 표현력이 풍부한 API를 제공하는 jMock 프레임워크를 사용해보겠습니다.

테스트와 실제 애플리케이션 모두에 동일한 Spring 빈 구성을 사용하는 것이 바람직하지만 특정 컴포넌트를 테스팅하는 경우 컴포넌트의 독립성을 해칠 수 있기 때문에 실제 종속성을 사용할 수는 없습니다. 그러나 Spring을 통해 선택된 빈과 종속성을 대체하기 위해 Spring의 애플리케이션 컨텍스트를 생성할 때 BeanPostProcessor를 지정할 수 있습니다. 이 경우 테스트 코드에서 생성되고 Spring 구성에서 정의된 빈 대신에 사용되는 mock 객체의 Map을 사용할 수 있습니다.

public class StubPostProcessor implements BeanPostProcessor {
  private final Map stubs;

  public StubPostProcessor( Map stubs) {
    this.stubs = stubs;
  }

  public Object postProcessBeforeInitialization(Object bean, String beanName) {
    if(stubs.containsKey(beanName)) return stubs.get(beanName);
    return bean;
  }

  public Object postProcessAfterInitialization(Object bean, String beanName) {
    return bean;
  }

}

테스트 케이스 클래스의 setUp() 메서드에서 jMock API를 통해 생성된 baseTradeManageryahooFeed 컴포넌트에 대해 mock 객체를 사용하여 StubPostProcessor를 초기화합니다. 그 다음 ClassPathXmlApplicationContext(BeanPostProcessor를 사용할 수 있도록 구성됨)를 생성하여 tradeManager 컴포넌트를 인스턴스화할 수 있습니다. 그 결과로 탄생한 tradeManager 컴포넌트는 mocked 종속성을 사용합니다.

이러한 접근법을 통해 테스트용으로 컴포넌트를 분리할 수 있으며 동시에 Spring 빈 구성에서 어드바이스가 정확하게 정의되도록 할 수 있습니다. 많은 컨테이너 인프라스트럭처를 시뮬레이션하지 않고 EJB 컴포넌트에서 구현된 비즈니스 로직을 테스트하기 위해 이런 종류의 것을 사용하는 것은 사실상 불가능합니다.

public class ForeignTradeAdviceTest extends TestCase {
  TradeManager tradeManager;
  private Mock baseTradeManagerMock;
  private Mock yahooFeedMock;

  protected void setUp() throws Exception {
    super.setUp();

    baseTradeManagerMock = new Mock(TradeManager.class, "baseTradeManager");
    TradeManager baseTradeManager = (TradeManager) baseTradeManagerMock.proxy();
    
    yahooFeedMock = new Mock(TradeManager.class, "yahooFeed");
    TradeManager yahooFeed = (TradeManager) yahooFeedMock.proxy();

    Map stubs = new HashMap();
    stubs.put("yahooFeed", yahooFeed);
    stubs.put("baseTradeManager", baseTradeManager);
    
    ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(CTX_NAME);
    ctx.getBeanFactory().addBeanPostProcessor(new StubPostProcessor(stubs));

    tradeManager = (TradeManager) proxyFactory.getProxy();
  }
  ...

실제 testAdvice() 메서드에서 mock 객체에 대한 예상치를 지정하고, 예를 들어 baseTradeManagergetPrice()null을 리턴하는 경우 yahooFeedgetPrice()도 호출되는지를 확인할 수 있습니다.

  public void testAdvice() throws Throwable {
    String symbol = "testSymbol";
    BigDecimal expectedPrice = new BigDecimal("0.222");

    baseTradeManagerMock.expects(new InvokeOnceMatcher()).method("getPrice")
      .with(new IsEqual(symbol)).will(new ReturnStub(null));
    
    yahooFeedMock.expects(new InvokeOnceMatcher()).method("getPrice")
      .with(new IsEqual(symbol)).will(new ReturnStub(expectedPrice));
    
    BigDecimal price = tradeManager.getPrice(symbol);
    assertEquals("Invalid price", expectedPrice, price);
        baseTradeManagerMock.verify();
    yahooFeedMock.verify();
  }

이 코드는 jMock 제약을 사용하여 baseTradeManagerMock는 메서드 getPrice()symbol과 같은 파라미터를 통해 단 한번 호출될 것으로 기대하는지 여부와 이 호출에서 null을 리턴하게 될지 여부를 지정할 수 있습니다. 마찬가지로 yahooFeedMock도 같은 메서드의 1회 호출을 기대하지만 expectedPrice를 리턴합니다. 이를 통해 setUp() 메서드에서 생성한 tradeManager 컴포넌트를 실행할 수 있으며 리턴된 결과를 어설션(assertion)합니다. mocked 종속성을 통해 종속 컴포넌트에 대한 모든 호출이 예상치를 만족시켰는지를 확인할 수 있습니다.

이 테스트 케이스는 가능한 모든 케이스를 처리할 수 있도록 쉽게 파라미터화될 수 있습니다. 컴포넌트에 의해 예외가 throw되었을 때 쉽게 예상치를 선언할 수 있다는 점을 기억하시기 바랍니다.

Test baseTradeManager yahooFeed Expected
call return throw call return throw result exception
1 true 0.22 - false - - 0.22 -
2 true - e1 false - - - e1
3 true null - true 0.33 - 0.33 -
4 true null - true null - null -
5 true null - true - e2 - e2

이 테이블을 사용하여 다음과 같이 모든 가능한 시나리오를 만족시키는 파라미터화된 슈트를 사용할 수 있도록 테스트를 업데이트할 수 있습니다.

  ...
  
  public static TestSuite suite() {
    BigDecimal v1 = new BigDecimal("0.22");
    BigDecimal v2 = new BigDecimal("0.33");
    
    RuntimeException e1 = new RuntimeException("e1");
    RuntimeException e2 = new RuntimeException("e2");
    
    TestSuite suite = new TestSuite(ForeignTradeAdviceTest.class.getName());
    suite.addTest(new ForeignTradeAdviceTest(true, v1,   null, false, null, null, v1,   null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, e1,   false, null, null, null, e1));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  v2,   null, v2,   null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  null, null, null, null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  null, e2,   null, e2));
    return suite;
  }
  
  public ForeignTradeAdviceTest(
      boolean baseCall, BigDecimal baseValue, Throwable baseException,
      boolean yahooCall, BigDecimal yahooValue, Throwable yahooException,
      BigDecimal expectedValue, Throwable expectedException) {
    super("test");

    this.baseCall = baseCall;
    this.baseWill = baseException==null ? 
        (Stub) new ReturnStub(baseValue) : new ThrowStub(baseException);
    this.yahooCall = yahooCall;
    this.yahooWill = yahooException==null ? 
        (Stub) new ReturnStub(yahooValue) : new ThrowStub(yahooException);
    this.expectedValue = expectedValue;
    this.expectedException = expectedException;
  }
  
  public void test() throws Throwable {
    String symbol = "testSymbol";

    if(baseCall) {
      baseTradeManagerMock.expects(new InvokeOnceMatcher())
        .method("getPrice").with(new IsEqual(symbol)).will(baseWill);
    }
    
    if(yahooCall) {
      yahooFeedMock.expects(new InvokeOnceMatcher())
        .method("getPrice").with(new IsEqual(symbol)).will(yahooWill);
    }
    
    try {
      BigDecimal price = tradeManager.getPrice(symbol);
      assertEquals("Invalid price", expectedValue, price);
    } catch(Exception e) {
      if(expectedException==null) {
        throw e;
      }
    }
        baseTradeManagerMock.verify();
    yahooFeedMock.verify();
  }

  public String getName() {
    return super.getName()+" "+
      baseCalled+" "+baseValue+" "+baseException+" "+
      yahooCalled+" "+yahooValue+" "+yahooException+" "+
      expectedValue+" "+expectedException;
  }
  ...

보다 정교한 케이스의 경우에 위의 테스트 방법은 보다 큰 입력 파라미터 세트로 쉽게 확장될 수 있으며 실제로 즉시 작동할 뿐 아니라 관리도 용이합니다. 또한 QA 팀에 의해 관리되거나 요구 사항으로부터 직접 생성되는 외부 config나 심지어 Excel 스프레드시트로 모든 파라미터를 이동시키는 것이 좋습니다.

어드바이스 조합 및 변경

간단한 인터셉터 어드바이스를 사용하여 추가 로직을 구현하고 독립 컴포넌트로서 테스트도 수행했습니다. 이 설계는 다른 컴포넌트와의 추가적인 조합 및 변경 없이 일반 실행 흐름이 확장되어야 할 때 유용합니다. 예를 들어 가격 변동 시 JMS나 JavaMail을 사용하여 통보해야 할 경우 tradeManager 빈의 setPrice 메서드에 또 다른 인터셉터를 등록하고 이를 사용하여 해당 컴포넌트에 변경 내용을 알릴 수 있습니다. 많은 경우에 이러한 측면은 전통적으로 많은 AOP관련 기술 문서나 자습서에서 "hello world" 예제로 사용된 바 있는 추적, 로깅 또는 모니터링과 같은 비 업무적 요구 사항에 적용될 수 있습니다.

또 다른 일반적인 AOP의 응용 분야는 캐싱입니다. 예를 들어 CMP 엔티티 빈 기반의 TradeDao 컴포넌트는 WebLogic 서버에서 제공하는 캐싱의 이점을 얻게 됩니다. YahooFeed 컴포넌트는 인터넷을 통해 Yahoo 포털에 연결해야 하기 때문에 상당히 많은 시간이 소요됩니다. 바로 이런 경우에 캐싱을 적용해야 하며 이를 통해 외부 연결의 수를 줄여서 전반적인 시스템 부하를 감소시킵니다. expiration 기반 캐시는 정보를 새로 고칠 때 약간의 대기 시간을 필요로 하지만 대부분의 경우 용인할 수 있는 수준입니다. 캐싱을 적용하려면 yahooFeedCachingAdvisor를 정의하여 CachingAdviceyahooFeed 빈의 getPrice() 메서드에 추가합니다. 다운로드 섹션에서 CachingAdvice 구현 예를 살펴볼 수 있습니다.

  <bean id="getPriceAdvisor" abstract="true"
        class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
    <property name="mappedName" value="getPrice"/>
  </bean>

  <bean id="yahooFeedCachingAdvisor" parent="getPriceAdvisor">
    <property name="advice">
      <bean class="org.javatx.spring.aop.CachingAdvice">
        <constructor-arg index="0" ref="cache"/>
      </bean>
    </property>
  </bean>

getPrice() 메서드는 여러 어드바이스에 대해 일반적인 조인 포인트가 되기 때문에 추상 getPriceAdvisor 빈을 선언한 후 yahooFeedCachingAdvisor에서 확장하여 명확한 어드바이스 CachingAdvice를 지정합니다. 이전 섹션의 foreignTradeAdvisor는 동일한 getPriceAdvisor 상위 빈을 사용하도록 변경될 수 있습니다.

이제 ProxyFactoryBean에서 yahooFeed 빈을 래핑(wrap)하여 yahooFeedCachingAdvisor를 통해 어드바이스할 수 있도록 yahooFeed 빈의 정의를 업데이트할 수 있습니다. 예를 들면 다음과 같습니다.

  <bean id="yahooFeed" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="org.javatx.spring.aop.TradeManager"/>
    <property name="target">
      <bean class="org.javatx.spring.aop.YahooFeed">
    </property>
    <property name="interceptorNames">
      <list>
        <value>yahooFeedCachingAdvisor</value>
      </list>
    </property>
  </bean>

위의 변경은 요청이 캐시에 이미 저장돼 있는 데이터에 적중(hit)하는 경우에 탁월한 성능 향상을 가져오지만, 아직 캐시에 저장되지 않았거나 이미 만료된 symbol에 대해 여러 개의 요청이 수행되면 동일한 symbol에 대해 서비스 공급자에게 동시에 여러 건의 요청이 발생하게 됩니다. 최적화가 이루어지면 첫 번째 요청이 완료되어 첫 번째 요청에 의해 검색된 결과를 사용할 때까지 동일한 symbol에 대한 모든 순차적 요청을 차단합니다. 이러한 접근법은 여러 JVM에서 실행되는 클러스터링 환경에서는 효과가 없기 때문에 일반적으로 EJB 사양에서는 권장되지 않습니다(버전 2.1의 섹션 25.1.2, "Programming Restrictions" 참조) 그러나 단일 노드에서도 이러한 최적화는 여전히 성능을 향상시키는 방법으로 사용될 수 있습니다. 그림 2 의 차트에서 최적화 전후의 경우를 비교 설명합니다.

Figure 2
그림 2. 최적화 전후

또한 이 최적화는 다음과 같이 어드바이스로서 구현되어 yahooFeed 빈의 인터셉터 체인(interceptor chain) 끝부분에 추가될 수 있습니다.

    ...
    <property name="interceptorNames">
      <list>
        <idref local="yahooFeedCachingAdvisor"/>
        <idref local="syncPointAdvisor"/>
      </list>
    </property>

실제 인터셉터 구현은 다음과 같습니다.

public class SyncPointAdvice implements MethodInterceptor {
  private long DEFAULT_TIMEOUT = 10000L;

  private Map requests = Collections.synchronizedMap(new HashMap());

  public Object invoke(MethodInvocation invocation) throws Throwable {
    String symbol = (String) invocation.getArguments()[0];
    Object[] lock = (Object[]) requests.get(symbol);
    if(lock==null) {
      lock = new Object[1];
      requests.put(symbol, lock);
      try {
        lock[0] = invocation.proceed();
        return lock[0];
      } finally {
        requests.remove(symbol);
        synchronized(lock) {
          lock.notifyAll();
        }
      }
    }
    
    synchronized(lock) {
      lock.wait(DEFAULT_TIMEOUT);
    }
    return lock[0];
  }

}

이처럼 어드바이스 코드는 매우 간단하며 다른 컴포넌트에 종속되지 않아 단위 테스트 또한 매우 간단합니다. 리소스 섹션에서 SyncPointAdvice에 대한 JUnit 테스트의 전체 소스 코드를 찾을 수 있습니다. 복잡한 동시 작업 시나리오의 경우에는 Java 5나 이전 버전 JVM의 백포트(backport)와 함께 java.util.concurrent 패키지의 동기화 메커니즘을 사용하는 것이 좋습니다.

다운로드

sources.zip에는 본 기술자료에 사용된 모든 소스 코드 예제가 포함되어 있습니다. 코드를 작성하려면 README.txt의 지시 사항을 따르십시오.

결론

본 문서에서는 J2EE 애플리케이션의 EJB를 Spring 관리 컴포넌트로 전환하는 방법과 이러한 전환으로 인해 활용할 수 있는 강력한 기법에 대해 설명합니다. 또한 Spring의 AOP 프레임워크와 함께 영역 지향(aspect-oriented) 접근법을 적용해 J2EE 애플리케이션을 확장하고 기존 코드에 대한 변경 없이 새로운 비즈니스 요구사항을 구현하는 방법에 대한 여러 가지 실습 예제들을 제공합니다.

EJB 내에서부터 Spring Framework를 사용하면 코드 조합 작업이 줄어들고 강력한 많은 기능들을 즉시 이용할 수 있기 때문에 확장성과 기민성을 높일 수 있습니다. 뿐만 아니라 직무 기능의 구현은 물론 추적, 캐싱, 보안 및 트랜잭션과 같은 비업무적 요건을 처리하는 데에도 사용할 수 있는 새롭게 도입된 AOP 어드바이스와 인터셉터 등 애플리케이션의 개별 컴포넌트들을 손쉽게 테스트할 수 있습니다.

감사의 말씀

본 기술자료에 대해 사려 깊은 의견과 제안을 해주신 Dmitri Maximovich와 Ron Bodkin에게 감사의 말씀을 드립니다.

리소스

Eugene Kuleshov는 독립 컨설턴트로서 소프트웨어 설계 및 개발 부문에서 12년 이상의 경력을 보유하고 있으며, 애플리케이션 보안, 엔터프라이즈 통합(EAI) 및 메시지 지향 미들웨어 분야의 전문가로도 명성을 얻고 있습니다.

 

출처 : http://www.dev2dev.co.kr/pub/a/2005/12/spring-aop-with-ejb.jsp