Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 21장 엔터프라이즈 자바빈(EJB) 통합

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



21. 엔터프라이즈 자바빈(EJB) 통합

21.1 소개

경량 컨테이너인 스프링를 때로는 EJB의 대안으로 생각한다. 대부분의 어플리션과 유즈케이스는 아니지만 트랜잭션, ORM, JDBC 접근의 영역에서 풍부한 기능을 지원하도록 통합하는 많은 경우에 스프링을 컨테이너로 사용하는 EJB 컨테이너와 EJB로 동일한 기능을 구현하는 것보다 더 낫다고 생각한다.

하지만 스프링을 사용하는 것이 EJB의 사용을 막는 것은 아니라는 것이 중요하다. 사실 스프링은 EJB에 접근하고 EJB와 EJB의 기능을 구현하기 쉽게 한다. 게다가 EJB가 제공하는 서비스에 접근하는데 스프링을 사용하는 것은 이러한 서비스의 구현을 나중에 클라이언트 코드를 변경하지 않고도 로컬 EJB, 원격 EJB, POJO (plain old Java object)의 변형으로 투명하게 바꿀수 있게 한다.

이번 장에서 스프링이 EJB 접근과 구현을 어떻게 도와주는지를 살펴본다. 스프링은 상태가 없는 세션 빈(SLSBs, stateless session beans)에 접근할 때 특정 값을 제공하므로 이 부분부터 얘기해 보겠다.

21.2 EJB에 접근하기

21.2.1 개념

로컬 혹은 원격의 상태가 없는 세션 빈의 메서드를 호출하려면 클라이언트 코드는 보통 (로컬이나 원격) EJB Home 객체를 획득하는데 JNDI 검색을 수행해야 하고 실제 (로컬 혹은 원격) EJB 객체를 획득하려고 해당 객체의 'create' 메서드를 호출한다.

반복되는 저수준 코드를 피하려고 많은 EJB 어플리케이션은 Service Locator 패턴과 Business Delegate 패턴을 사용한다. 클라이언트 코드 곳곳에서 JNDI 검색을 하는 것보다 이 방법이 낫지만 일반적인 구현체는 중대한 결점을 가진다. 예를 들면 다음과 같다.

  • Service Locator나 Business Delegate 싱글톤에 의존하는 EJB를 사용하는 일반적인 코드는 테스트하기가 어렵다.
  • Business Delegate 없이 Service Locator 패턴을 사용한 경우 어플리케이션 코드는 여전히 EJB home의 create() 메서드를 호출하게 되고 예외를 다룬다. 그래서 어플리케이션 코드가 EJB API와 복잡한 EJB 프로그래밍 코델에 묶이게 된다.
  • Business Delegate 패턴의 구현은 보통 EJB의 같은 메서드를 호출하는 다수의 메서드를 작성해야 하는 곳에 상당한 코드중복을 유발한다.

스프링의 접근은 코드없이 business delegates처럼 동작하는 프록시 객체(보통 스프링 컨테이너에 설정한)를 생성하고 사용할 수 있게 한다.이러한 코드에 실제 값을 추가하지 않고 별도의 Service Locator나 JNDI 검색, 하드코딩된 Business Delegate의 중복된 메서드를 작성할 필요가 없다.

21.2.2 로컬 SLSBs에 접근하기

로컬 EJB를 사용해야 하는 웹 컨트롤러가 있다고 해보자. 베스트 프렉티스를 따르고 EJB Business Methods Interface 패턴을 사용할 것이므로 EJB의 로컬 인터페이스는 EJB에 특화되지 않은 비즈니스 메서드 인터페이스를 확장한다. 이 비즈니스 인터페이스를 MyComponent라고 하자.

public interface MyComponent {
  ...
}

Business Methods Interface 패턴을 사용하는 주요 이유 중 하나는 로컬 인터페이스의 메서드 시그니처와 빈 구현 클래스간의 동기화를 자동으로 보장하기 때문이다. 다른 이유로는 나중에 필요하다면 서비스의 구현을 POJO(plain old Java object)로 바꾸기 아주 쉽기 때문이다. 물론 로컬 홈(home) 인터페이스를 구현해야 하고 SessionBean과 MyComponent 비즈니스 메서드 인터페이스를 구현하는 구현 클래스를 제공해야 할 것이다. 이제 웹계층 컨트롤러를 EJB 구현체와 연결하기 우해서 필요한 Java 코딩은 컨트롤러에 MyComponent 타입의 setter 메서드를 노출하는 것 뿐이다. 컨트롤러에 인스턴스 변수에 대한 참조를 아낄 수 있다.

private MyComponent myComponent;

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

이 다음부터는 컨트롤러의 어떤 비즈니스 메서드에서도 이 인스턴스 변수를 사용할 수 있다. 이제 스프링 컨테이너 외부의 컨트롤러 객체를 획득한다고 가정하면 EJB 프록시 객체가 될 LocalStatelessSessionProxyFactoryBean 인스턴스를 (같은 컨텍스트내에서)설정할 수 있다. 프록시의 설정(컨트롤러의 myComponent 프로퍼티의 설정)은 다음과 같은 설정으로 한다.

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

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

AOP 개념을 사용하도록 하지 않았더라도 내부에서 스프링 AOP 프레임워크덕에 많은 작업이 이뤄진다. myComponent 빈 정의는 비즈니스 메서드 인터페이스를 구현하는 EJB의 프록시를 생성한다. EJB 로컬 홈(home)은 구동시에 캐싱되므로 딱 하나의 JNDI 검색만 존재한다. EJB가 호출될 때마다 프록시는 로컬 EJB의 classname 메서드와 EJB에서 대응되는 비즈니스 메서드를 호출한다.

myController 빈 정의는 컨트롤러 클래스의 myComponent 프로퍼티를 EJB 프록시로 설정한다.

아니면 (많은 프록시 정의가 있는 경우에는) 스프링의 "jee" 네임스페이스의 설정요소를 사용하는 것을 고려해봐라.

<jee:local-slsb id="myComponent" jndi-name="ejb/myBean"
    business-interface="com.mycom.MyComponent"/>

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

이 EJB 접근 메카니즘은 어플리케이션 코드를 엄청나게 간소화 한다. 웹계층 코드(또는 다른 EJB 클라이언트 코드)는 EJB 사용에 대한 의존성을 같지 않는다. 이 EJB 참조를 POJO나 목(mock) 객체, 테스트 스텁등으로 교체하고자 한다면 자바 코드를 변경하지 않고 myComponent 빈 정의를 변경하면 된다. 게다가 JNDI 검색이나 어플리케이션의 다른 EJB관련 코드를 한줄도 작성할 필요가 없다.

실제 어플리케이션의 벤치마크와 경험에 따르면 이 접근(대상 EJB의 리플렉션 호출(reflective invocation)을 포함해서)의 성능 부하(performance overhead)가 최소이고 일반적인 사용해서는 발견할 수 없는 정도임을 나타낸다. 어플리케이션 서버의 EJB 인프라와 관련된 비용때문에 어떤 식으로든 EJB에 세밀한 호출을 원치 않는다는 점을 기억해라.

JNDI 검색과 관련해서 한가지 주의할 점이 있다. 빈 컨테이너에서 이 클래스는 싱글톤으로 사용하기 가장 좋다.(프로토타입으로 만들 이유가 없다.) 하지만 해당 빈 컨테이너가 싱글턴을 미리 인스턴스화했다면(다양한 XML ApplicationContext에 따라서) EJB 컨테이너가 대상 EJB를 로그하기 전에 빈 컨테이너가 로드되는 문제가 있을 것이다. JNDI 검색이 이 클래스의 init() 메서드에서 수행된 후 캐시되지만 EJB는 아직 대상 로케이션에 바인딩되지 않았기 때문에 이 문제가 발생한다. 이 팩토리 객체를 미리 인스턴스화 하지 않고 처음 사용할 때 생성하도록 해서 해결할 수 있다. XML 컨테이너에서 lazy-init 속성으로 이를 제어한다.

대부분의 스프링 사용자들은 관심이 없겠지만 EJB와 동작하는 프로그래밍적인 AOP를 사용하려면 LocalSlsbInvokerInterceptor를 참고해라.

21.2.3 원격 SLSB 접근

원격 EJB에 접근하는 것은 SimpleRemoteStatelessSessionProxyFactoryBean나 설정 요소를 사용하는 것외에는 본질적으로 로컬 EJB에 접근하는 것과 같다. 물론 스프링을 사용하든지 사용하지 않는지 간에 원격 호출의 개념이 적용된다. 즉, 다른 컴퓨터의 VM에서 객체의 메서드를 호출하려면 사용 시나리오와 실패처리를 다르게 처리해야 한다.

스프링의 EJB 클라이언트는 스프링을 사용하지 않은 접근보다 더 많은 장점을 제공한다. 보통 EJB 클라이언트 코드를 로컬 EJB 호출과 원격 EJB 호출간에 변경하는 것은 문제의 소지가 있다. 이는 원격 인터페이스 메서드가 RemoteException를 던지도록 선언해야 하고 클라이언트 코드는 이를 다뤄야(로컬 인터페이스 메서드는 다루지 않는다) 하기 때문이다. 로컬 EJB용으로 작성한 클라이언트 코드를 원격 EJB로 바꾸어야 한다면 보통 원격 예외에 대한 처리를 추가해야 하고 원격 EJB용으로 작성한 클라이언트 코드를 로컬 EJB로 변경해야 한다면 바꾸지 않고 놔두어도 되지만 불필요한 다수의 원격 예외처리를 하거나 이러한 부분을 제거해야 한다. 스프링 원격 EJB 프록시를 사용하면 Business Method Interface에 RemoteException를 던지도록 선언해서 EJB 코드를 구현하지(RemoteException을 던지는 부분을 제외하고는 동일한 원격 인터페이스를 가지면서) 않고 두 인터페이스를 동일한 것처럼 동적으로 다루도록 프록시에 의존할 수 있다. 즉, 클라이언트 코드든 체크드 RemoteException 클래스를 다루 않아야 한다. EJB 호출중에 던져진 실제 RemoteException은 체크드가 아닌 RemoteAccessException 클래스(RuntimeException의 하위클래스)로 다시 던져질 것이다. 대상 서비스를 클라이언트 코드가 인지하거나 신경쓸 필요없이 로컬 EJB와 원격 EJB(또는 평범한 자바 객체더라도) 구현간에 변경할 수 있다. 물론 이는 선택사항이므로 비즈니스 인터페이스에 RemoteExceptions를 선언하지 않을 이유는 없다.

21.2.4 EJB 2.x SLSBs 접근과 EJB 3 SLSBs 접근 비교

스프링의 EJB 2.x Session Bean 접근과 EJB 3 Session Bean 접근은 아주 투명하다. 를 포함해서 스프링의 EJB 접근자는 투명하게 런타임에서 실제 컴포넌트에 맞게 맞춰진다. home 인터페이스를 찾으면(EJB 2.x 방식) home 인터페이스를 처리하고 home 인터페이스가 없으면(EJB 3 방식) 바로 컴포넌트를 호출한다.

Note: 평범한 JNDI 검색에 사용할 수 있는 컴포넌트 참조가 완전히 노출되므로 EJB 3 Session Bean에서 JndiObjectFactoryBean / 를 효과적으로 사용할 수 있다. 명시적으로 / 검색을 정의하면 일관성있고 더 명확한 EJB 접근 설정을 할 수 있다.

21.3 EJB 구현을 지원하는 스프링 클래스 사용하기

21.3.1 EJB 2.x에 기반한 클래스

스프링은 EJB를 쉽게 구현하도로고 편리한 클래스를 제공한다. 이 클래스들은 EJB가 트랜잭션 경계와 (추가적으로) 원격을 담당하고 POJO로 EJB뒤에 비즈니스 로직을 좋은 방법을 권장하도록 설계되었다.

상태를 보관하든 보관하지 않든 세션 빈이나 Message Driven 빈을 구현하려면 AbstractStatelessSessionBean, AbstractStatefulSessionBean, AbstractMessageDrivenBean/AbstractJmsMessageDrivenBean를 각각 상속받아서만 구현해야 한다.

실제로 구현을 평범한 자바 서비스 객체레 위임하는 상태없는 세션 빈의 예제를 생각해 보다. 다음과 같은 비즈니스 인터페이스를 가지고 있다.

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

다음과 같은 펑범한 자바 구현 개체도 있다고 하자.

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

다음은 상태없는 세션 빈이다.

public class MyFacadeEJB extends AbstractStatelessSessionBean
    implements MyFacadeLocal {

  private 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 myFacadeMethod(...) {
    return myComp.myMethod(...);
  }
  ...
}

스프링 EJB 지원 기반(base) 클래스들은 기본적으로 생명주기의 일부로 스프링 IoC 컨테이너를 생성하고 로드해서 EJB에서사용할 수 있게 한다.(예를 들면 앞의 코드에서 POJO 서비스 객체를 얻으려고 사용했듯이) BeanFactoryLocator의 하위클래스인 전략(strategy) 객체로 IoC 컨테이너를 로드한다. 기본적으로 사용하는 BeanFactoryLocator의 실제 구현체는 JNDI 환경변수(EJB 클래스에서는 java:comp/env/ejb/BeanFactoryPath)로 지정한 리소스 위치에서 ApplicationContext를 생성하는 ContextJndiBeanFactoryLocator이다. BeanFactory/ApplicationContext 로딩전략을 변경해야 한다면 setBeanFactoryLocator() 메서드나 EJB의 진짜 생성자에서 setBeanFactoryLocator() 메서드를 호출해서 기본 BeanFactoryLocator 구현체를 오버라이드한다. 자세한 내용은 자바독을 참고해라.

자바독에 나와있듯이 생명주기내에서 보호하거나(passivated) 재가동(reactivated)해야 하면서 직렬화할 수 없는 컨테이너 인스턴스를 사용하는 상태를 가진 세션빈은 EJB 컨테이너가 저장할 수 없으므로 보호와 활성화에서 BeanFactory를 내리거나 다시 로드하려면 ejbPassivate()와 ejbActivate()에서 unloadBeanFactory()와 loadBeanFactory()를 수동으로 각각 호출해야 할 것이다.

ContextJndiBeanFactoryLocator 클래스의 기본동작은 EJB가 사용하는 ApplicationContext를 로드하는 것이고 이는 일부의 상황에서는 적합하다. 하지만 ApplicationContext가 다수의 빈을 로딩하거나 초기화하는데 시간이 많이 걸리거나 메모리를 많이 쓰는(하이버네이트 SessionFactory 초가화 등) 경우 각 EJB가 자신만의 복사본을 갖기 때문에 문제가 될 수 있다. 이러한 경우 기본 ContextJndiBeanFactoryLocator 사용을 오버라이드하고 여러 EJB나 다른 클라이언트가 사용할 공유 컨테이너를 로딩해서 사용할 수 있는 ContextSingletonBeanFactoryLocator같은 다른 BeanFactoryLocator를 사용하길 원할 것이다. 이 작업은 상대적으로 간단한데 EJB에 이를 위한 코드를 추가해서 할 수 있다.

/**
  * 기본 BeanFactoryLocator 구현을 오버라이드한다
  * @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
  */
 public void setSessionContext(SessionContext sessionContext) {
   super.setSessionContext(sessionContext);
   setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
   setBeanFactoryLocatorKey(ServicesConstants.PRIMARY_CONTEXT_ID);
 }

그 다음 beanRefContext.xml 파일에 빈 정의를 생성해야 한다. 이 파일은 EJB에서 사용할 모든 빈 팩토리를 정의한다.(보통은 어플리케이션 컨텍스트의 형식이다) 대다수의 경우 이 파일은 다음과 같은 (businessApplicationContext.xml는 모든 비즈니스 서비스 POJO의 빈 정의를 담고 있다.) 하나의 빈 정의만 담고 있을 것이다.

<beans>
  <bean id="businessBeanFactory" class="org.springframework.context.support.ClassPathXmlApplicationContext">
    <constructor-arg value="businessApplicationContext.xml" />
  </bean>
</beans>

위의 예제에서 ServicesConstants.PRIMARY_CONTEXT_ID 상수는 다음과 같이 정의될 것이다.

public static final String ServicesConstants.PRIMARY_CONTEXT_ID = "businessBeanFactory";

사용방법에 대한 자세한 내용은 BeanFactoryLocator와 ContextSingletonBeanFactoryLocator의 자바독을 참고해라.

21.3.2 EJB 3 주입 인터셉터

EJB 3 Session Bean과 Message-Driven Bean을 위해서 스프링은 EJB 컴포넌트 클래스에서 스프링 2.5의 @Autowired 어노테이션을 처리하는 편리한 인터셉터 org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor를 제공한다. EJB 컴포넌트 클래스에서 @Interceptors 어노테이션을 사용하거나 EJB 배포 디스크립터 (deployment descriptor)에서 interceptor-binding XML 요소를 사용해서 이 인터셉터를 적용할 수 있다.

@Stateless
@Interceptors(SpringBeanAutowiringInterceptor.class)
public class MyFacadeEJB implements MyFacadeLocal {

  // 일치하는 스프링 빈을 자동으로 주입한다
  @Autowired
  private MyComponent myComp;

  // 비즈니스 메서드를 위해서 POJO 서비스 구현에 위임한다.
  public String myFacadeMethod(...) {
    return myComp.myMethod(...);
  }
  ...
}

기본적으로 SpringBeanAutowiringInterceptor는 ContextSingletonBeanFactoryLocator에서 beanRefContext.xml 빈 정의 파일에 정의된 컨텍스트와 대상 빈을 가져온다. 기본값은 이름이 아니라 타입으로 가져오는 단일 컨텍스트 정의이지만 여러 컨텍스트 정의에서 선택해야 한다면 특정 로케이터(locator) 키가 필요하다. 로케이터 키(예를 들면 beanRefContext.xml의 컨텍스트 정의 이름)는 커스텀 SpringBeanAutowiringInterceptor 하위클래스의 getBeanFactoryLocatorKey 메서드를 오버라이딩해서 명시적으로도 지정할 수 있다.

아니면 SpringBeanAutowiringInterceptor의 getBeanFactory 메서드를 오버라이딩 하는 것을 고려해라. 예를 들면 커스텀 소유(holder) 클래스에서 공유된 ApplicationContext를 가져오는 식이다.

2013/07/30 23:51 2013/07/30 23:51