by Juergen Hoeller
2005/07/04
널리 사용되고 있는 Java/J2EE 애플리케이션 프레임워크로서 lightweight Inversion-of-Control 컨테이너에서 구축된 Spring Framework는 특히 데이터 액세스와 트랜잭션 관리 기능으로 잘 알려져 있습니다. EJB CMT(Container-Managed Transactions)에서와 같은 정교한 선언적 트랜잭션과 함께 어떠한 POJO 대상 객체에도 Spring의 선언적 트랜잭션 경계 설정(demarcation)을 적용할 수 있습니다. 백엔드 트랜잭션 매니저에 대한 선택 범위는 단순한 JDBC 기반 트랜잭션으로부터 JTA를 통한 완전한 J2EE트랜잭션에 이르기까지 다양합니다.
이 문서에서는 Spring의 트랜잭션 관리 기능에 대해 자세히 다루도록 하겠습니다. JTA를 백엔드 트랜잭션 전략으로 하여 POJO에 대한 Spring의 선언적 트랜잭션을 이용하는 내용을 중점적으로 설명할 것입니다. 또한 Spring의 트랜잭션 서비스가 J2EE 서버의 트랜잭션 코디네이터(예: EJB CMT의 전형적인 트랜잭션 경계 설정 방식에 대한 대안으로 사용되는 BEA WebLogic Server용 트랜잭션 코디네이터)와 완벽하게 연동됨을 보여줄 것입니다.
Spring의 선언적 트랜잭션 경계 설정 방식의 예로서 Spring PetClinic 샘플 애플리케이션의 중앙 서비스 facade에 대한 구성을 살펴보겠습니다.
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/petclinic</value> </property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> <bean id="clinicTarget" class="org.springframework.samples.petclinic.jdbc.JdbcClinic"> <property name="dataSource"><ref bean="dataSource"/></property> </bean> <bean id="clinic" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref bean="clinicTarget"/></property> <property name="transactionAttributes"> <props> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>
이는 Spring의 표준 XMLBean 정의 형식을 따르며 다음의 내용을 정의합니다.
참고: 명시적 프록시 정의의 대안으로서 Spring은 자동 프록시(auto-proxying) 메커니즘과 Commons Attributes 또는 J2SE 5.0 주석을 통한 소스 수준(source-level ) 메타데이터의 사용을 지원합니다. 이런 대안에 관한 논의는 이 문서의 취지를 벗어나는 것이니, 자세한 사항은 Spring 문서를 참조하십시오.
사용된 서비스 인터페이스와 서비스 구현은 애플리케이션에 따라 다르며 Spring이나 Spring 트랜잭션 관리에 대한 특별한 지식 없이도 구현할 수 있습니다. 일반 Java객체도 대상 객체가 될 수 있으며 어떤 일반 Java 인터페이스도 서비스 인터페이스가 될 수 있습니다. 다음은 Clinic 인터페이스의 예입니다.
public interface Clinic { Pet loadPet(int id); void storePet(Pet pet); ... }
이 인터페이스의 간단한 구현 방법은 아래와 같으며, 여기서는 필요한 데이터에 액세스하기 위해 JDBC를 사용한다고 가정하였습니다. 빈 속성 설정자(bean property setter)를 통해 JDBC DataSource가 수신되는데, 이는 위에서 설정한 dataSource 속성 정의에 직접 대응됩니다.
public class JdbcClinic implements Clinic { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Pet loadPet(int id) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } public void storePet(Pet pet) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } ... }
이처럼 코드는 매우 단순하며 간단한 Java 객체가 사용됩니다. 트랜잭션은 다음에 설명할 트랜잭션 프록시에 의해 관리됩니다.
PetClinic 샘플 애플리케이션에서 실제로 JDBC 기반의 Clinic을 구현할 경우 Spring의 JDBC 지원 클래스를 이용함으로써 일반 JDBC API 수준에서의 작업을 피할 수 있습니다. 그러나 Spring의 트랜잭션 관리는 위에서 설명했듯이 일반 JDBC 기반의 구현과도 작동합니다.
code>JdbcClinic 인스턴스 외에도 구성을 통해 트랜잭션 프록시를 정의할 수 있습니다. 필요할 경우 이 프록시에 의해 노출되는 실제 인터페이스를 명시적으로 지정할 수 있습니다. 기본적으로 대상 객체에 의해 구현되는 모든 인터페이스(이 경우 애플리케이션의 Clinic
서비스 인터페이스)는 노출됩니다.
클라이언트의 관점에서 볼 때 "clinic" 빈은 애플리케이션의 Clinic
인터페이스 구현과 같습니다. 클라이언트는 이러한 작업이 트랜잭션 프록시를 통해 처리되고 있음을 알 필요가 없습니다. 이것이 인터페이스의 강점입니다. 대상 객체에 대한 직접적인 참조는 동일한 인터페이스를 구현하는 프록시(이 경우 암시적으로 트랜잭션을 생성하는 프록시)로 쉽게 대체할 수 있습니다.
프록시의 구체적인 트랜잭션 동작은 특정 메서드나 메서드 이름 패턴에 대한 트랜잭션 속성에 의해 수행됩니다. 아래의 예를 보십시오.
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop>
key
속성은 프록시에 의해 추가된 트랜잭션 동작을 어떤 메서드에 할당할지 결정합니다. 그러한 속성의 가장 중요한 부분이 전달 동작(propagation behavior)입니다. 다음과 같은 옵션을 사용할 수 있습니다.
처음 여섯 개 전략은 같은 상수 이름을 가지고 있는 EJB CMT와 비슷하기 때문에 EJB 개발자에게 무척 익숙할 것입니다. 일곱 번째 PROPAGATION_NESTED
는 Spring에서 제공하는 특수한 variant입니다. 여기에는 중첩된 트랜잭션 동작(예: Spring의 DataSourceTransactionManager
)을 제공하기 위해 JDBC 3.0 Savepoint API와 함께 작동하거나 JTA를 통해 중첩된 트랜잭션을 지원하는 트랜잭션 매니저가 필요합니다.
트랜잭션 속성의 readOnly
플래그는 상응하는 트랜잭션이 읽기 전용 트랜잭션으로 최적화되어야 한다는 것을 나타내는, 일종의 최적화 힌트입니다. 어떤 트랜잭션 전략은 중요한 최적화를 수행할 수 있는데, 예를 들어 Hibernate나 TopLink 같은 객체/관계 매핑 도구를 사용할 때 dirty checking("flush" 시도)을 회피하는 경우를 들 수 있습니다.
또한 트랜잭션 속성에서 "timeout" 값을 초 단위로 지정할 수 있는 옵션이 있습니다. JTA의 경우 이 작업이 J2EE 서버 트랜잭션 코디네이터로 넘겨지며 거기에서 적절히 해석됩니다.
런타임 시, 클라이언트는 "clinic" 빈에 대한 참조를 페치하여 이를 Clinic
인터페이스로 전달하고 loadPet
이나 storePet
과 같은 작업을 호출합니다. 이는 대상 객체 앞에 등록된 "트랜잭션 인터셉터"와 함께 암시적으로 Spring의 트랜잭션 프록시 전 과정을 거치게 됩니다. 그런 다음 새로운 트랜잭션이 생성되면 JdbcClinic
대상 메서드에게 호출이 위임됩니다.
그림 1은 "advisor chain" 및 마지막 대상이 포함된 AOP 프록시의 기본 개념을 보여줍니다. 이 경우, 유일한 advisor는 대상 메서드 주위로 트랜잭션 동작을 래핑(wrap)하는 트랜잭션 인터셉터 뿐입니다. 이것이 Spring의 선언적 트랜잭션 기능 아래에서 작동하는 프록시 기반의AOP(Aspect-Oriented Programming)입니다.
그림 1. advisor chain 및 마지막에 대상이 포함된 AOP 프록시
예를 들어 PetClinic 웹 애플리케이션의 웹 계층 컴포넌트는 ServletContext
조회를 수행하여, Spring의 WebApplicationContext
에 대한 참조를 얻은 다음 거기서 관리되는 "clinic" 빈에 대한 참조를 얻습니다.
WebApplicationContext ctx = WebApplicationContexUtils.getWebApplicationContext(servletContext); Clinic clinic = (Clinic) ctx.getBean("clinic); Pet pet = new Pet(); pet.setName("my new cat"); clinic.storePet(pet);
storePet()
호출의 시작 부분에서 Spring의 트랜잭션 프록시는 암시적으로 트랜잭션을 생성합니다. storePet()
호출이 리턴될 때 트랜잭션은 커밋되거나 롤백됩니다. 기본적으로 RuntimeException이나 Error thrown은 롤백을 유도합니다. 커밋과 롤백에 대한 실제 규칙을 지정할 수 있습니다. Spring의 트랜잭션 속성은 "롤백 규칙"이라고 하는 개념을 지원합니다.
예를 들어, 확인된 PetClinicException
지정하고, 예외가 throw될 경우 트랜잭션 프록시에서 롤백하도록 지시할 수 있습니다.
<prop key="load*">PROPAGATION_REQUIRED,readOnly,-PetClinicException</prop> <prop key="store*">PROPAGATION_REQUIRED,-PetClinicException</prop>
또한 "commit rules,"과 유사한 구문도 있는데, 이는 커밋을 트리거하도록 하는 특별한 예외의 경우를 나타냅니다.
위에서 설명한 명시적 조회는 Spring 관리 빈에 액세스하기 위한 일반적인 variant로서, 서블릿이나 필터와 같은 모든 웹 리소스와 함께 작동합니다. Spring 자체의 웹 MVC 프레임워크로 웹 계층을 구축할 경우 이러한 빈을 웹 컨트롤러에 직접 삽입할 수 있습니다. Struts, WebWork, JSF, Tapestry에서도 Spring 빈으로 편리하게 액세스할 수 있습니다. 자세한 내용은 Spring 문서를 참조하십시오.
PlatformTransactionManager
전략Spring의 트랜잭션 지원을 위한 핵심 인터페이스는 org.springframework.transaction.PlatformTransactionManager
입니다. 실제로 트랜잭션을 수행하고 적절한 TransactionDefinition
인스턴스로 전달할 수 있도록 Spring의 모든 트랜잭션 경계 설정 기능은 PlatformTransactionManager
로 위임됩니다. PlatformTransactionManager
인터페이스가 직접 사용될 수도 있지만, 보통 애플리케이션에서는 트랜잭션 경계 설정을 위해 단지 구체적인 트랜잭션 매니저를 구성하고 선언적 트랜잭션을 사용합니다.
Spring에서는 PlatformTransactionManager
를 다양하게 구현할 수 있는데, 이는 다음의 두 범주로 분류됩니다.
org.springframework.jdbc.datasource.DataSourceTransactionManager
및 org.springframework.orm.hibernate.HibernateTransactionManager
.
org.springframework.transaction.jta.JtaTransactionManager
, 이며 JTA 사양을 따르는 트랜잭션 코디네이터에 위임됩니다(대개 J2EE 서버이지만 반드시 그렇지는 않음). PlatformTransactionManager
추상화의 중요한 가치는 애플리케이션이 특정한 트랜잭션 관리 환경에 얽매이지 않는다는 점입니다. 대신 다른 PlatformTransactionManager
구현 클래스를 선택함으로써 트랜잭션 전략을 쉽게 변경할 수 있습니다. 따라서 어떤 환경에서 애플리케이션 컴포넌트가 사용되건 애플리케이션 코드와 선언적 트랜잭션 경계 설정을 동일하게 유지할 수 있습니다.
예를 들어 단일 Oracle 데이터베이스를 사용하면서 애플리케이션의 기본 버전을 Tomcat에 배포할 수 있습니다. 거기서 편리한 Spring 트랜잭션 경계 설정을 활용하여 트랜잭션 전략으로 JDBC DataSourceTransactionManager
를 선택할 수 있습니다. Spring은 트랜잭션의 경계 설정을 수행하고 JDBC 드라이버는 이에 상응하는 일반 JDBC 트랜잭션을 실행합니다.
두 개의 Oracle 데이터베이스를 사용하면서 동일한 애플리케이션의 다른 버전을 WebLogic Server에 배포할 수도 있습니다. 애플리케이션 코드와 트랜잭션 경계 설정은 변경할 필요가 없습니다. Spring이 트랜잭션의 경계 설정을 수행할 수 있도록 JtaTransactionManager
를 트랜잭션 전략으로 선택하기만 하면 WebLogic Server의 트랜잭션 코디네이터가 알아서 트랜잭션을 실행합니다.
UserTransaction
대 JTA TransactionManager
Spring의 JTA의 지원 내용을 좀 더 자세히 검토하겠습니다. 이 메커니즘을 이해하면 도움이 되지만 몰라도 걱정할 필요는 없습니다. 앞에서 살펴본 것 같이 간단하게 사용할 경우, J2EE 서버에서 제공하는 XA-aware DataSources와 함께 표준 JtaTransactionManager
정의만 있으면 됩니다.
기본 설정된 Spring의 JtaTransactionManager
는 J2EE: java:comp/UserTransaction
에 의해 지정된 표준 JNDI 위치로부터 JTA의 javax.transaction.UserTransaction
객체를 페치합니다. 이는 표준 J2EE 환경에서 사용되는 대부분의 경우 아무 문제도 없습니다.
그러나 기본 JtaTransactionManager
는 트랜잭션 일시 중지를 수행하지 못합니다. 즉 PROPAGATION_REQUIRES_NEW와 PROPAGATION_NOT_SUPPORTED를 지원하지 않습니다. 그 이유는 표준 JTA UserTransaction
인터페이스가 새로운 트랜잭션의 시작과 완료만 지원하고, 트랜잭션의 일시 중지와 재시작을 지원하지 않기 때문입니다.
트랜잭션 일시 중지를 수행하려면 JTA에서 지정된 대로 표준 일시 중지와 재시작 메서드를 제공하는 javax.transaction.TransactionManager
인스턴스가 필요합니다. 불행히도, J2EE는 JTA TransactionManager에 대한 표준 JNDI 위치를 정의하지 않습니다! 따라서 벤더별 조회 메커니즘을 사용해야 합니다.
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManagerName"> <value>vendorSpecificJndiLocation</value> </property> </bean>
J2EE는 본질적으로 JTA TransactionManager
인터페이스를 공용 API로 간주하지 않습니다. JTA 사양 자체는 컨테이너 통합 목적으로 TransactionManager
인터페이스를 선언합니다. 이는 이해가 가지만, JTA TransactionManager
에 대한 표준 JNDI 위치는 특히 Spring과 같은 lightweight 컨테이너에 여전히 중요한 가치를 더합니다. J2EE 서버의 JTA TransactionManager
에 대한 위치도 동일한 방식으로 처리됩니다.
JTA TransactionManager
에 대한 액세스로 인해 Spring의 JtaTransactionManager
는 물론 Hibernate, Apache OJB, Kodo JDO 같은 O/R 매핑 도구도 혜택을 받게 됩니다. 이들이 JTA 환경에서 캐시 동기화를 수행하려면(즉 JTA 트랜잭션 완료 시 캐시 잠금을 해제하려면) JTA TransactionManager
에 액세스할 수 있어야 하기 때문입니다. 트랜잭션 동기화를 등록하는 기능은 JTA TransactionManager
인터페이스에서만 제공하고 UserTransaction
에서는 제공하지 않습니다. 그 결과 이러한 도구는 각자의 벤더 전용 TransactionManager
조회 어댑터를 구현해야 합니다.
JTA TransactionManager
에 대한 표준 JNDI 위치 정의는 많은 인프라스트럭처 소프트웨어 공급자들의 J2EE 요구 목록(wish list) 중 상위에 올라 있는 항목입니다. J2EE 5.0 사양 팀이 이 기능의 중요성을 인식하는 것이 중요합니다. 다행히도 WebLogic Server와 같은 고급 J2EE 서버는 벤더별 익스텐션을 비롯하여 JTA TransactionManager
를 이미 공용 API로 간주합니다.
WebLogic Server의 경우 JTA TransactionManager의 공식적인 JNDI 위치는 javax.transaction.TransactionManager
입니다. 이 값은 Spring의 JtaTransactionManager
에서 "transactionManagerName"으로 지정할 수 있습니다. 원칙적으로 이를 통해 WebLogic의 JTA 하위 시스템과 함께 Spring에서 트랜잭션 일시 중지를 수행할 수 있으며 PROPAGATION_REQUIRES_NEW
와 PROPAGATION_NOT_SUPPORTED
에 대한 지원이 활성화됩니다.
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManagerName"> <value>javax.transaction.TransactionManager</value> </property> </bean>
표준 JtaTransactionManager
와 여기에서 지원되는 일반 구성 옵션 외에도 Spring은 WebLogic의 JTA 익스텐션을 직접 이용하는 특별한 WebLogicJtaTransactionManager
어댑터를 제공합니다.
<bean id="transactionManager" class="org.springframework.transaction.jta.WebLogicJtaTransactionManager"/>
WebLogic의 JTA TransactionManager는 편리한 자동 감지 기능 외에도 표준 JTA를 뛰어넘는 세 가지 중요한 기능을 제공합니다.
TransactionManager
인터페이스를 사용하여 forceResume()
메서드를 호출해야 합니다. 다음 그림은 Spring에서 수행되는 몇몇 트랜잭션을 이름순으로 보여주는 WebLogic Server의 트랜잭션 모니터입니다.
Spring의 WebLogicJtaTransactionManager
는 WebLogic Server의 트랜잭션 매니저의 모든 기능을 Spring 기반의 애플리케이션에 효과적으로 노출합니다. 따라서 Spring트랜잭션 경계 설정은 EJB CMT에 필적할 만한 대안이 되며 동일한 수준의 트랜잭션 지원을 제공합니다.
트랜잭션을 일시 중지할 필요가 있거나 WebLogic의 JTA 확장자를 이용해야 할 경우에만 WebLogic에 맞게 JTA를 설정하면 됩니다. PROPAGATION_REQUIRED
나 PROPAGATION_SUPPORTS
와 같은 표준 트랜잭션 경계 설정에 대해서는 표준 JTA 설정을 사용할 수 있습니다.
위에서 설명한 것과 같이 POJO에 대한 Spring의 선언적 트랜잭션 경계 설정은 기존의 EJB CMT의 대안으로 사용할 수 있습니다. 하지만 Spring과 EJB가 상호 배타적인 것은 아닙니다. Spring 애플리케이션 컨텍스트를 EJB facade 배후에서 백엔드로 사용하여, 데이터 액세스 객체(DAO)나 다른 정교한 비즈니스 객체를 관리할 수 있습니다.
EJB 시나리오에서 트랜잭션은 EJB CMT에 의해 수행됩니다. Spring의 데이터 액세스 지원 기능을 통해 자동으로 해당 환경이 감지되며 적절히 적응이 이루어집니다. 예를 들어 Spring에서 수행되는 트랜잭션과 마찬가지로 EJB에서 수행되는 트랜잭션에서도 Spring의 Hibernate 지원 기능을 통해 암시적 리소스 관리가 제공됩니다. 동일한 의미가 제공되므로 DAO 코드를 변경할 필요가 없습니다.
Spring은 실제 런타임 환경에서 DAO 구현을 효과적으로 분리합니다. DAO는 Spring 트랜잭션(백엔드로 실행되는 모든 트랜잭션 전략으로도)과 EJB CMT 트랜잭션에 모두 사용할 수 있습니다. 다른 환경에서 재사용할 수 있을 뿐 아니라 J2EE 컨테이너 외부에서 테스트용으로도 손쉽게 사용할 수 있습니다.
Spring Framework는 J2EE와 비J2EE 환경에서 완전한 트랜잭션 경계 설정 기능을 제공합니다. 따라서 EJB가 없어도 유연하고 안전한 방법으로 편리하게 트랜잭션 경계 설정을 수행할 수 있습니다. EJB와는 달리 트랜잭션 POJO 애플리케이션 객체는 J2EE 컨테이너 외부에서도 쉽게 테스트하고 재사용할 수 있습니다.
Spring은 J2EE서버의 트랜잭션 코디네이터에 위임하는 JtaTransactionManager
와 단일 JDBC DataSource(단일 대상 데이터베이스)에 대해 트랜잭션을 실행하는 JDBC DataSourceTransactionManager
처럼 즉시 이용할 수 있는 다양한 트랜잭션 전략을 제공합니다. Spring은 백엔드 구성을 변경함으로써 다른 환경에 맞게 트랜잭션 전략을 쉽게 변경합니다.
표준 JTA 지원 외에도 Spring은 WebLogic Server의 JTA 익스텐션과의 정교한 통합 기능을 제공함으로써 트랜잭션 모니터링과 트랜잭션별 격리 수준 같은 고급 기능을 지원합니다. WebLogic Server에 대한 이런 특별한 지원을 바탕으로 Spring 기반의 애플리케이션에서 WebLogic 트랜잭션 매니저의 모든 기능을 사용할 수 있습니다.
Spring 트랜잭션 경계 설정은 특히 POJO 기반의 lightweight 아키텍처와 함께 사용할 경우 EJB CMT에 필적할 만한 대안이 됩니다. 단순히 선언적 트랜잭션 때문에 Local Sateless Session Bean을 선택해야 하는 경우, Spring 기반의 POJO 서비스 모델을 선택하면 훨씬 더 수준 높은 유연성, 테스트 편리성 및 재사용성이 제공됩니다.
SSISO Community