[Spring Docs] Transaction Management #1
공식문서로 트랜잭션을 배워볼까요..?
이번글은 공식 문서에서 소개하는 Transaction Management에 대해 정리했습니다.
범위 : Advantages of the Spring Framework’s Transaction Support Model ~
Declarative Transaction Management(Configuring Different Transactional Semantics for Different Beans)
Spring에서 트랜잭션 지원 모델의 장점이 무엇일까요?
Advantages of the Spring Framework's Transaction Support Model
EE 애플리케이션 개발자들은 트랜잭션 관리를 위해서 글로벌 트랜잭션과 로컬 트랜잭션 두 가지 선택지만 존재했습니다.
하지만, 이 두가지 방식 모두 심각한 한계를 가지고 있습니다.
Global 트랜잭션과 Local 트랜잭션은 무엇일까요?
Global Transaction
글로벌 트랜잭션은 일반적으로 관계형 데이터베이스와 메시지 큐 처럼 여러 개의 트랜잭션 리소스를 함께 다룰 수 있게 해줍니다.
글로벌 트랜잭션은 애플리케이션 서버가 JTA를 통해서 관리를 하는데, JTA는 예외 처리 모델 때문에 사용하기에 번거로운 API 입니다.
또한, JTA의 UserTransaction 객체는 보통 JNDI에서 조회해야 합니다.
즉, JTA를 사용하기 위해선 JDNI도 함께 사용해야 합니다.
이러한 글로벌 트랜잭션 방식은 애플리케이션 코드의 재사용성에 제약을 줍니다.
JTA는 보통 애플리케이션 서버 환경에서만 제공되기 때문입니다.
과거엔 글로벌 트랜잭션을 사용하는 대표적인 방법으로 EJB CMT(컨테이너 관리 트랜잭션)가 선호되었습니다.
CMT는 선언적 트랜잭션 관리 방식의 한 형태로 (코드로 직접 트랜잭션을 다루는 프로그래밍 방식과는 구분됩니다) EJB CMT 덕분에 트랜잭션과 관련된 JDNI 조회 작업이 필요 없어지게 됩니다.
하지만, 아이러니하게도 EJB 자체를 사용하기 위해선 결국 JNDI를 사용해야 합니다.
게다가, CMT는 트랜잭션을 제어하기 위한 자바 코드를 대부분 제거해주지만, 완전히 없애주지는 못합니다.
무엇보다 큰 문제는, CMT는 JTA와 애플리케이션 서버 환경에 묶여 있다는 점입니다.
또한, CMT를 쓰려면 비즈니스 로직을 반드시 EJB로 구현하거나, 최소한 트랜잭션 EJB를 전면에 두는 방식으로 구성해야 합니다.
그런데, EJB 자체의 단점이 워낙 심각해서, 이 방식은 사실상 매력적인 선택지가 되기 어렵습니다.
특히, 요즘엔 훨씬 더 매력적인 선언적 트랜잭션 관리 대안들이 많이 나왔기 때문에 더욱 그렇습니다.
Local 트랜잭션은 무엇일까요?
Local Transactions
로컬 트랜잭션은 특정 리소스에 종속된 트랜잭션을 의미합니다.
예를 들어, JDBC 커넥션에 연결된 트랜잭션이 대표적인 예시입니다.
로컬 트랜잭션은 다루기 쉬울 수 있지만, 여러 트랜잭션 리소스를 걸쳐서 사용할 수 없다는 치명적인 단점이 존재합니다.
예를 들어, JDBC 커넥션을 사용해 트랜잭션을 관리하는 코드는 글로벌 JTA 트랜잭션 안에서 실행될 수 없습니다.
왜냐하면, 이 경우 애플리케이션 서버가 트랜잭션 관리에 관여하지 않기 때문에 여러 리소스를 걸친 트랜잭션의 정합성 보장을 도와줄 수 없기 때문입니다.
또한, 로컬 트랜잭션은 프로그래밍 모델에 침투적이라는 단점도 있습니다.
즉, 트랜잭션 처리를 위한 코드가 비즈니스 로직과 뒤섞여 들어가야 한다는 뜻입니다.
Spring 프레임워크는 이 문제를 어떻게 해결했을까요?
Spring Framework’s Consistent Programming Model
스프링은 글로벌 트랜잭션과 로컬 트랜잭션들이 가진 단점을 해결해줍니다.
스프링을 사용하면, 애플리케이션 개발자는 환경에 관계없이 일관된 프로그래밍 모델을 사용할 수 있습니다.
즉 한 번만 코드를 작성하면, 서로 다른 환경에서 각기 다른 트랜잭션 관리 전략의 혜택을 누릴 수 있습니다.
스프링 프레임워크는 선언적 트랜잭션 관리와 프로그래밍 방식의 트랜잭션 관리를 모두 지원합니다.
대부분의 사용자들은 선언적 트랜잭션 관리를 선호하며, 스프링도 이를 권장합니다.
프로그래밍 방식의 트랜잭션 관리는 개발자가 스프링 프레임워크의 트랜잭션 추상화를 직접 다루게 됩니다.
이 추상화는 어떤 종류의 트랜잭션 인프라에서도 동작할 수 있게 설계 되었습니다.
선언적 트랜잭션 관리는 트랜잭션 관리를 위한 코드를 거의 작성하지 않아도 됩니다.
그 결과로 개발자는 스프링 프레임워크의 트랜잭션 API나 그 외의 어떤 트랜잭션 관련 API에 종속되지 않게 됩니다.
쉽게 말해, 선언적 트랜잭션 방식은 어노테이션 @Transactional을 사용하는 방식을 의미하고, 프로그래밍 방식은 PlatformTransactionManger 클래스를 사용해서 코드로 직접 사용하는 것을 의미합니다.
과거에는, EJB CMT나 JTA를 쓰지 않는다면 JDBC 커넥션 같은 로컬 트랜잭션 코드를 직접 작성해야 했고, 글로벌 트랜잭션 환경으로 옮기려면 엄청난 수준의 코드 수정이 필요했습니다. 하지만 스프링에서는, 트랜잭션 전략을 변경할 때 코드가 아닌 설정 파일의 빈 정의 몇 개만 바꾸면 됩니다. 이 덕분에, 기존 비즈니스 로직은 건드릴 필요조차 없게 되었습니다.
Spring의 트랜잭션 추상화란 무엇일까요?
Understanding the Spring Framework Transaction Abstraction
스프링 트랜잭션 추상화의 핵심은 트랜잭션 전략(transaction strategy)이라는 개념입니다.
트랜잭션 전략은 TransactionManager에 의해 정의됩니다.
구체적으로 다음 두 인터페이스가 각각의 트랜잭션을 관리합니다.
- org.springframework.transaction.PlatformTransactionManager : 동기(Imperative) 방식 트랜잭션 관리용
- org.springframework.transaction.ReactiveTransactionManager : 리액티브(Reactive) 방식 트랜잭션 관리용
다음은 PlatformTransactionManager API의 정의 예시입니다.
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
이 인터페이스는 서비스 제공자 인터페이스(SPI) 역할을 주로 합니다. 하지만, 애플리케이션 코드에서 직접 프로그래밍 방식으로 호출하는 것도 가능합니다.
PlatformTransactionManager는 인터페이스이기 때문에, 목(mock) 객체나 스텁(stub)으로 쉽게 대체할 수 있습니다.
또한, JNDI 같은 조회 전략(lookup strategy)에 묶여있지 않습니다.
서비스 제공자 인터페이스는 서비스 제공자(Service Provider), 즉 서비스를 실제로 제공하는 쪽이 구현해야 하는 인터페이스를 의미합니다. 예시로 JDBC 드라이버, JPA 구현체가 있습니다.
스프링의 철학에 맞게, PlatformTransactionManager 인터페이스의 모든 메서드에서 던질 수 있는 TransactionException은 언체크 예외(unchecked exception)입니다. 즉, java.lang.RuntimeException을 상속받습니다.
그 이유는 트랜잭션 인프라가 실패하는 경우는 거의 치명적(fatal)이기 때문입니다.
이런 경우, 애플리케이션이 정상적으로 복구할 가능성은 매우 낮습니다.
물론, 아주 드물게 애플리케이션 코드가 트랜잭션 실패에서 복구할 수 있는 경우도 있을 수 있습니다.
그런 경우, 개발자는 원한다면 TransactionException을 직접 catch하고 처리할 수도 있습니다.
하지만, 핵심은 스프링이 개발자에게 반드시 예외를 잡도록 강요하지 않는다는 점입니다.
getTransaction(..) 메서드는 TransactionDefinition 파라미터를 받아서, 그 정의에 맞는 TransactionStatus 객체를 반환합니다.
이때 반환되는 TransactionStatus는 새로운 트랜잭션일 수도 있고, 이미 현재 호출 스택(call stack)에 존재하는 트랜잭션을 나타낼 수도 있습니다.
두 번째 경우는, 이미 열려있는 트랜잭션이 있는 경우 같은 트랜잭션 컨텍스트를 계속 사용한다는 의미입니다. 즉, 스프링의 트랜잭션 컨텍스트는 실행 중인 스레드에 귀속됩니다. 같은 스레드 안에서 트랜잭션을 계속 이어받아서 사용할 수 있다는 뜻입니다.
스프링은 리액티브 애플리케이션을 위한 트랜잭션 관리 추상화도 제공합니다.
이 추상화는 리액티브 타입(Reactive Types)이나 코틀린 코루틴(Kotlin Coroutines)을 사용하는 경우에 적합합니다.
다음은 ReactiveTransactionManager API의 정의 예시입니다.
public interface ReactiveTransactionManager extends TransactionManager {
Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}
리액티브 트랜잭션 매니저는 기본적으로 서비스 제공자 인터페이스(SPI) 역할을 합니다. 하지만, 필요하다면 애플리케이션 코드에서 직접 프로그래밍 방식으로 호출할 수도 있습니다. 그리고, ReactiveTransactionManager는 인터페이스이기 때문에 테스트 시에 목(mock) 객체나 스텁(stub) 객체로 쉽게 대체할 수 있습니다.
TransactionDefine 인터페이스는 어떤 것을 지정할까요?
The TransactionDefinition interface specifies
트랜잭션 전파(Propagation)
일반적으로 트랜잭션 범위 내의 모든 코드는 동일한 트랜잭션에서 실행됩니다. 그러나, 이미 존재하는 트랜잭션 컨텍스트 내에서 트랜잭션 메서드가 실행될 경우의 동작을 지정할 수 있습니다.
예를 들어, 기존 트랜잭션에서 계속 실행하도록 할 수도 있고(가장 일반적인 경우), 기존 트랜잭션을 일시 중단하고 새로운 트랜잭션을 생성하도록 지정할 수도 있습니다.
격리 수준(Isolation)
해당 트랜잭션이 다른 트랜잭션의 작업으로부터 얼마나 격리되는지를 나타냅니다.
예를 들어, 다른 트랜잭션이 커밋하지 않은 데이터를 이 트랜잭션이 조회할 수 있는지를 결정합니다.
타임 아웃(Time out)
해당 트랜잭션이 얼마나 오랫동안 실행될 수 있는지를 지정합니다. 지정된 시간 안에 완료되지 않으면, 해당 트랜잭션은 기본 트랜잭션 인프라에 의해 자동으로 롤백됩니다.
읽기 전용 상태(Read-Only)
코드가 데이터를 읽기만 하고 수정하지 않을 경우, 읽기 전용 트랜잭션을 사용할 수 있습니다. 읽기 전용 트랜잭션은 Hibernate와 같은 경우 성능 최적화를 위해 유용할 수 있습니다.
TransactionStatus 인터페이스는 어떤 역할을 할까요?
TransactionStatus 인터페이스는 트랜잭션 코드가 트랜잭션 실행을 제어하고 트랜잭션 상태를 조회할 수 있는 단순한 방법을 제공합니다. 다음은 TransactionStatus 인터페이스의 정의입니다.
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
@Override
boolean isNewTransaction();
boolean hasSavepoint();
@Override
void setRollbackOnly();
@Override
boolean isRollbackOnly();
void flush();
@Override
boolean isCompleted();
}
Spring에서 선언적 트랜잭션 관리를 선택하든 프로그래밍 방식의 트랜잭션 관리를 선택하든, 올바른 TransactionManager 구현체를 정의하는 것은 매우 중요합니다. 이 구현체는 보통 의존성 주입을 통해 정의합니다.
TransactionManager 구현체는 자신이 동작하는 환경(JDBC, JTA, Hibernate 등)에 대한 정보를 필요로 합니다.
다음은 로컬 PlatformTransactionManager 구현체를 정의하는 예제이며, 여기서는 순수 JDBC 환경을 기준으로 합니다.
JDBC DataSource는 다음과 같이 빈(bean)을 생성하여 정의할 수 있습니다.
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
관련된 PlatformTransactionManager 빈 정의는 DataSource 정의를 참조해야 합니다. 다음은 그 예시입니다.
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
Hibernate에서 Transaction Setup은 어떻게 할까요?
Hibernate Transaction Setup
Hibernate 로컬 트랜잭션도 다음 예시와 같이 쉽게 사용할 수 있습니다.
이 경우, Hibernate LocalSessionFactoryBean을 정의해야 하며, 애플리케이션 코드는 이를 통해 Hibernate Session 인스턴스를 얻을 수 있습니다.
DataSource 빈 정의는 앞서 보여준 로컬 JDBC 예제와 유사합니다.
이 경우, txManager 빈은 HibernateTransactionManager 타입입니다. DataSourceTransactionManager가 DataSource를 참조해야 하는 것과 동일하게, HibernateTransactionManager는 SessionFactory를 참조해야 합니다.
다음은 sessionFactory와 txManager 빈을 선언하는 예제입니다.
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
트랜잭션에서 리소스 동기화는 어떻게 할까요?
Synchronizing Resources with Transactions
이제 다양한 트랜잭션 매니저를 생성하는 방법과, 각 매니저가 트랜잭션과 동기화되어야 하는 관련 리소스와 어떻게 연결되는지(예: DataSourceTransactionManager는 JDBC DataSource와 연결되고, HibernateTransactionManager는 Hibernate SessionFactory와 연결되는 방식)가 명확해졌을 것입니다.
이 섹션에서는, 애플리케이션 코드가 (JDBC, Hibernate, JPA 같은 퍼시스턴스 API를 직접 사용하거나 간접적으로 사용할 때) 이러한 리소스를 어떻게 생성하고, 재사용하고, 적절히 정리하는지를 설명합니다.
또한, 관련 트랜잭션 매니저를 통해 트랜잭션 동기화를 (선택적으로) 어떻게 트리거하는지도 다룹니다.
High-level Synchronization Approach
권장되는 접근 방식은 스프링의 가장 상위 수준 템플릿 기반 퍼시스턴스 통합 API를 사용하거나, 트랜잭션 인식이 가능한 팩토리 빈 또는 프록시를 통해 네이티브 ORM API를 사용하는 것입니다.
이런 트랜잭션 인식 솔루션은 내부적으로 다음을 자동으로 처리합니다
- 리소스 생성 및 재사용
- 리소스 정리
- 선택적인 트랜잭션 동기화
- 예외 매핑
따라서, 사용자 데이터 액세스 코드에서는 이런 반복 작업(보일러플레이트 코드)을 신경 쓸 필요 없이, 순수하게 비즈니스 로직에 집중할 수 있습니다. 일반적으로, ORM 환경에서는 네이티브 ORM API를 사용하고, JDBC 환경에서는 JdbcTemplate을 사용하는 템플릿 접근 방식을 선택합니다.
Low-level Synchronization Approach
JDBC의 DataSourceUtils, JPA의 EntityManagerFactoryUtils, Hibernate의 SessionFactoryUtils 같은 클래스들은 더 낮은 수준에서 존재합니다. 만약 애플리케이션 코드가 네이티브 퍼시스턴스 API의 리소스 타입을 직접 다뤄야 하는 경우, 이 클래스들을 사용해 다음을 보장할 수 있습니다.
- 올바른 스프링 프레임워크 관리 인스턴스를 획득
- (선택적으로) 트랜잭션과 동기화
- 발생하는 예외를 일관된 스프링 예외로 변환
예를 들어, JDBC의 경우 전통적인 방식으로 DataSource에서 getConnection()을 직접 호출하는 대신,
스프링의 org.springframework.jdbc.datasource.DataSourceUtils 클래스를 다음과 같이 사용할 수 있습니다.
Connection conn = DataSourceUtils.getConnection(dataSource);
이미 존재하는 트랜잭션에 연결된(동기화된) 커넥션이 있으면, 해당 커넥션 인스턴스를 반환합니다. 만약 동기화된 커넥션이 없다면, 새로운 커넥션을 생성하고, (선택적으로) 기존 트랜잭션에 동기화한 후, 같은 트랜잭션에서 이후에도 재사용할 수 있도록 준비합니다.
앞서 설명한 것처럼, 이 과정에서 발생하는 SQLException은 스프링 프레임워크의 CannotGetJdbcConnectionException으로 감싸져서 던져집니다. 이 예외는 스프링의 DataAccessException 계층에 속하는 언체크 예외입니다. 이런 방식은 단순히 원래의 SQLException만으로는 얻기 어려운 추가 정보를 제공하며, 데이터베이스 종류가 달라져도, 심지어 다른 퍼시스턴스 기술로 바뀌어도
코드의 이식성을 보장하는 데 도움이 됩니다.
이 방식(DataSourceUtils 같은 유틸리티를 활용해서 커넥션을 얻는 방식)은 반드시 스프링의 트랜잭션 관리가 필요한 것은 아닙니다. 트랜잭션 동기화는 선택 사항이기 때문에, 스프링의 트랜잭션 관리를 사용하든 사용하지 않든 자유롭게 이 방식을 사용할 수 있습니다.
물론, 일단 스프링의 JDBC 지원, JPA 지원, Hibernate 지원을 사용해 본다면, 대부분의 경우 DataSourceUtils나 그 외의 헬퍼 클래스들을 직접 사용하는 대신, 스프링이 제공하는 추상화를 통해 작업하는 쪽을 더 선호하게 됩니다.
예를 들어, 스프링의 JdbcTemplate이나 jdbc.object 패키지를 사용해 JDBC 작업을 단순화하면, 커넥션을 올바르게 가져오는 작업은 내부적으로 자동 처리되므로, 개발자가 특별한 코드를 작성할 필요가 없습니다.
TransactionAwareDataSourceProxy
가장 낮은 수준에는 TransactionAwareDataSourceProxy 클래스가 있습니다. 이 클래스는 대상 DataSource를 감싸는 프록시이며,
대상 DataSource에 스프링 트랜잭션 인식 기능을 추가합니다. 그러나, 이 클래스는 거의 사용할 일이 없으며, 대부분의 경우 직접 사용할 필요도, 사용할 이유도 없습니다.
단, 기존 코드가 이미 작성되어 있고, 그 코드가 표준 JDBC DataSource 인터페이스 구현체를 반드시 필요로 하는 경우에는
이 프록시를 사용할 수 있습니다. 이렇게 하면, 기존 코드는 그대로 두면서도 해당 DataSource가 스프링이 관리하는 트랜잭션에 참여하도록 만들 수 있습니다. 하지만 새로운 코드를 작성할 때는, 앞서 설명한 상위 수준 추상화들을 사용하는 것이 훨씬 권장됩니다.
Declarative Transaction Management
대부분의 스프링 프레임워크 사용자들은 선언적 트랜잭션 관리를 선택합니다.
이 방식은 애플리케이션 코드에 미치는 영향이 가장 적기 때문에, 스프링이 추구하는 비침투적(non-invasive)이고 경량 컨테이너라는 이상과 가장 잘 맞습니다.
스프링 프레임워크의 선언적 트랜잭션 관리는 스프링 AOP(Aspect-Oriented Programming)를 통해 구현됩니다.
그러나, 트랜잭션 처리를 위한 어드바이스(Aspect) 코드가 이미 스프링 프레임워크에 내장되어 제공되며, 개발자는 이를 정형화된 방식(boilerplate)으로 바로 사용할 수 있습니다. 따라서, 효과적으로 선언적 트랜잭션 관리를 사용하기 위해 AOP 개념을 반드시 깊이 이해할 필요는 없습니다.
스프링 프레임워크의 선언적 트랜잭션 관리는 EJB CMT와 유사합니다. 각 메서드 수준에서 트랜잭션 동작(또는 트랜잭션이 필요 없는 경우)을 세부적으로 지정할 수 있기 때문입니다.
또한, 필요할 경우 트랜잭션 컨텍스트 내에서 setRollbackOnly() 호출도 가능합니다. 두 트랜잭션 관리 방식(EJB CMT와 스프링 선언적 트랜잭션 관리) 사이의 차이점은 다음과 같습니다:
- EJB CMT는 JTA에 강하게 묶여 있지만, 스프링 프레임워크의 선언적 트랜잭션 관리는 환경에 관계없이 동작합니다.
설정 파일만 조정하면, JTA 트랜잭션뿐만 아니라, JDBC, JPA, Hibernate를 사용하는 로컬 트랜잭션도 지원할 수 있습니다. - 스프링 선언적 트랜잭션 관리는 EJB처럼 특별한 클래스에만 국한되지 않고, 아무 클래스에나 적용할 수 있습니다.
- 스프링은 선언적 롤백 규칙을 제공합니다. 이 기능은 EJB CMT에는 없는 기능입니다. 스프링에서는 롤백 규칙을 코드로 지정하는 방법과 설정으로 선언하는 방법을 모두 지원합니다.
스프링은 AOP를 활용해, 트랜잭션 동작을 커스터마이징할 수 있습니다.
예를 들어, 트랜잭션이 롤백될 때 특정 로직을 추가할 수도 있고, 트랜잭션 어드바이스 외에도 임의의 어드바이스를 추가할 수도 있습니다.
반면, EJB CMT는 setRollbackOnly()를 제외하고는 컨테이너의 트랜잭션 관리를 사용자가 제어할 방법이 없습니다.
스프링 프레임워크는 원격 호출 간의 트랜잭션 컨텍스트 전파는 지원하지 않습니다.
이 기능은 일부 고급 애플리케이션 서버에서 제공하는 기능입니다.
만약 이 기능이 꼭 필요하다면, EJB를 사용하는 것이 더 적합합니다. 그러나, 원격 호출에 트랜잭션을 걸어야 하는 경우는
극히 드물기 때문에, 신중히 판단하고 사용하는 것이 좋습니다. 보통은, 원격 호출을 포함하는 트랜잭션을 구성하지 않는 것이 바람직합니다.
원격 호출(Remote Call)은 다른 서버나 다른 JVM에서 동작하는 서비스 메서드를 호출하는 것을 말합니다.
쉽게 말해, MSA 환경을 생각하면 됩니다.
롤백 규칙(rollback rules)이라는 개념은 매우 중요합니다. 이 규칙을 통해, 어떤 예외(또는 throwable)가 발생했을 때 자동으로 롤백할지를 지정할 수 있습니다. 이 규칙은 자바 코드가 아닌 설정 파일에서 선언적(declarative)으로 지정할 수 있습니다.
물론, 여전히 TransactionStatus 객체에서 setRollbackOnly()를 호출해 수동으로 롤백할 수도 있습니다.
하지만, 대부분의 경우 "MyApplicationException이 발생하면 항상 롤백 같은 규칙을 미리 지정해두는 방식이 더 많이 사용됩니다.
이 방식의 중요한 장점은, 비즈니스 객체가 트랜잭션 인프라에 의존하지 않아도 된다는 점입니다. 즉, 비즈니스 객체는 스프링의 트랜잭션 API나 기타 스프링 API를 import할 필요가 없습니다.
Understanding the Spring Framework’s Declarative Transaction Implementation
단순히 클래스에 @Transactional 애노테이션을 붙이고, 설정에 @EnableTransactionManagement를 추가하면 된다고만 설명하는 것은 충분하지 않습니다. 이 섹션에서는, 스프링 프레임워크의 선언적 트랜잭션 인프라가 내부적으로 어떻게 동작하는지, 트랜잭션과 관련된 문제들과 함께 보다 깊이 있게 설명합니다.
스프링 프레임워크의 선언적 트랜잭션을 이해할 때, 반드시 알아야 할 가장 중요한 개념은 다음 두 가지입니다:
- 이 기능은 AOP 프록시를 통해 활성화된다는 점
- 트랜잭션 어드바이스는 메타데이터에 의해 제어된다는 점 (현재 XML 또는 애노테이션 기반 설정 지원)
AOP와 트랜잭션 메타데이터가 결합되면, 메서드 호출을 감싸는 AOP 프록시가 만들어지고, 이 프록시는 적절한 TransactionManager 구현체와 함께 TransactionInterceptor를 사용해 트랜잭션 처리를 수행합니다.
스프링 프레임워크의 TransactionInterceptor는 동기(Imperative) 및 리액티브(Reactive) 프로그래밍 모델에 대한 트랜잭션 관리를 제공합니다. 이 인터셉터는 메서드의 반환 타입을 검사함으로써, 어떤 방식의 트랜잭션 관리가 필요한지를 자동으로 판별합니다.
- 메서드 반환 타입이 Publisher 또는 Kotlin Flow(혹은 그 하위 타입)이면, 리액티브 트랜잭션 관리 경로로 처리합니다.
- 그 외의 모든 반환 타입(예: void, String, int 등)은 동기 트랜잭션 관리 경로로 처리합니다.
@Transactional은 일반적으로 PlatformTransactionManager가 관리하는 스레드에 바인딩된 트랜잭션과 함께 동작합니다.
이 트랜잭션은 현재 실행 중인 스레드 내에서 수행되는 모든 데이터 액세스 작업에 공유됩니다.
단, 이 트랜잭션은 메서드 내에서 새로 시작된 스레드로는 전파되지 않습니다.
ReactiveTransactionManager로 설정된 경우, 트랜잭션 범위로 선언된 모든 메서드는 반드시 리액티브 파이프라인을 반환해야 합니다.
(Mono, Flux, Flow 등) 만약 메서드 반환 타입이 void이거나, 일반 객체를 반환하는 경우에는
이 메서드는 리액티브 트랜잭션이 아니라, 일반적인 PlatformTransactionManager와 연결되어야 합니다.
이 경우, @Transactional 선언에서 transactionManager 속성을 사용해 명시적으로 지정할 수 있습니다.
다음 이미지는 트랜잭션 프록시에서 메서드를 호출하는 개념적 흐름을 보여줍니다:
(1) 호출자는 실제 객체가 아닌 프록시를 호출합니다.
- 실제 객체의 메서드를 직접 호출하는 게 아닙니다.
- 스프링이 만든 AOP 프록시를 호출합니다.
(2) 메서드 진입 시 트랜잭션 생성, 종료 시 커밋 또는 롤백합니다.
- 트랜잭션은 메서드 진입할 때 생성됩니다.
- 메서드 실행이 끝나고, 프록시가 다시 제어를 받을 때, 트랜잭션은 커밋하거나, 예외 발생 시 롤백합니다.
- 이 흐름은 "열고 → 실행하고 → 닫는다"는 전형적인 트랜잭션 라이프사이클입니다.
(3) 사용자 정의 인터셉터는 트랜잭션 어드바이저 전후로 실행될 수 있습니다.
- 트랜잭션 어드바이저 외에도, 개발자가 직접 추가한 커스텀 어드바이저가 있을 수 있습니다.
- 이런 커스텀 어드바이저는 필요에 따라 트랜잭션 어드바이저 앞 또는 뒤에 실행 가능합니다.
- 예) 로깅 어드바이저, 모니터링 어드바이저 등
(4) 비즈니스 로직 호출합니다.
- 트랜잭션이 열리고, 필요한 모든 어드바이저가 실행된 후, 비즈니스 메서드가 실행됩니다.
- 이 단계는 실제 데이터 처리나 비즈니스 로직 수행과 직결됩니다.
(5) 처리 결과는 다시 인터셉터 체인을 거쳐 호출자에게 반환합니다.
- 비즈니스 로직이 끝나면, 결과가 바로 반환되는 게 아니라,
- 호출할 때 지나온 프록시, 어드바이저 체인을 역순으로 다시 거쳐서 최종 반환됩니다.
- 이 과정에서 후처리 로직, 예외 변환 처리 등이 이루어질 수도 있습니다.
Example of Declarative Transaction Implementation
다음은 FooService 인터페이스와 해당 구현 클래스에 대한 설명입니다. 이 예제에서는 특정 도메인 모델에 집중하지 않고 트랜잭션 사용에 초점을 맞출 수 있도록 Foo 및 Bar 클래스를 플레이스홀더(자리 표시자)로 사용합니다.
이 예제에서는 DefaultFooService 클래스가 각 구현된 메서드 본문에서 UnsupportedOperationException을 던지는 것이 좋습니다. 그 동작을 통해 UnsupportedOperationException이 발생할 때 트랜잭션이 생성되고 롤백되는 모습을 확인할 수 있습니다.
다음 목록은 FooService 인터페이스를 보여줍니다.
// the service interface that we want to make transactional
package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}
다음 예제는 앞서 나온 인터페이스의 구현을 보여줍니다.
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
FooService 인터페이스의 첫 번째 두 메서드, getFoo(String) 및 getFoo(String, String)는 읽기 전용(read-only) 의미를 가지는 트랜잭션 컨텍스트에서 실행되어야 하며, 나머지 메서드인 insertFoo(Foo) 및 updateFoo(Foo)는 읽기-쓰기(read-write) 의미를 가지는 트랜잭션 컨텍스트에서 실행되어야 한다고 가정합니다.
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>
<!-- don't forget the DataSource -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
</bean>
<!-- similarly, don't forget the TransactionManager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
앞서 나온 설정을 살펴보십시오. 이 설정은 fooService 빈을 트랜잭션이 적용된 서비스 객체로 만들고자 한다는 가정을 기반으로 합니다.
적용할 트랜잭션의 의미(semantics)는 <tx:advice/> 정의에 캡슐화되어 있습니다. <tx:advice/> 정의는 다음과 같이 해석할 수 있습니다.
- 모든 "get"으로 시작하는 메서드는 읽기 전용 트랜잭션 컨텍스트에서 실행됩니다.
- 그 외의 모든 메서드는 기본 트랜잭션 의미(default transaction semantics)로 실행됩니다.
또한 <tx:advice/> 태그의 transaction-manager 속성은 트랜잭션을 관리할 TransactionManager 빈의 이름을 지정합니다. 이 경우, 트랜잭션을 제어하는 빈은 txManager입니다.
<aop:config/> 정의는 txAdvice 빈에 의해 정의된 트랜잭션 어드바이스가 프로그램의 적절한 지점에서 실행되도록 보장합니다.
먼저, FooService 인터페이스에 정의된 모든 연산을 실행하는 지점을 매칭하는 포인트컷(pointcut)을 정의합니다.(fooServiceOperation).
그런 다음, 어드바이저(advisor)를 사용하여 이 포인트컷을 txAdvice와 연결합니다.
그 결과, fooServiceOperation이 실행될 때, txAdvice에 의해 정의된 어드바이스가 실행됩니다.
<aop:pointcut/> 요소 내에서 정의된 표현식은 AspectJ 포인트컷 표현식입니다.
Spring에서의 포인트컷 표현식에 대한 자세한 내용은 AOP 섹션을 참조하십시오.
일반적인 요구 사항 중 하나는 서비스 레이어 전체를 트랜잭션 처리하는 것입니다.
이를 수행하는 가장 좋은 방법은 포인트컷 표현식을 변경하여 서비스 레이어의 모든 연산을 매칭하도록 하는 것입니다.
다음 예제는 이를 수행하는 방법을 보여줍니다.
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
이제 설정을 분석했으므로, "이 모든 설정이 실제로 무엇을 하는가?" 라는 질문을 할 수도 있습니다.
앞서 제시된 설정은 fooService 빈 정의로 생성된 객체 주위에 트랜잭션 프록시(transactional proxy)를 생성하는 데 사용됩니다.
이 프록시는 트랜잭션 어드바이스(txAdvice)로 구성되어 있으며, 프록시에서 적절한 메서드가 호출될 때, 해당 트랜잭션 설정에 따라 트랜잭션이 시작, 일시 정지, 읽기 전용으로 설정되는 등의 동작을 수행합니다.
다음은 앞서 제시된 설정을 테스트하는 프로그램입니다.
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
FooService fooService = ctx.getBean(FooService.class);
fooService.insertFoo(new Foo());
}
}
앞서 실행한 프로그램의 출력 결과는 다음과 유사해야 합니다.
(Log4J 출력과 DefaultFooService 클래스의 insertFoo(..) 메서드에서 발생한 UnsupportedOperationException의 스택 트레이스는 가독성을 위해 생략되었습니다.)
<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]
<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
반응형 트랜잭션 관리를 사용하려면 코드가 반응형 타입을 사용해야 합니다.
다음 목록은 이전에 사용된 FooService의 수정된 버전을 보여주며, 이번에는 코드가 반응형 타입을 사용합니다.
// the reactive service interface that we want to make transactional
package x.y.service;
public interface FooService {
Flux<Foo> getFoo(String fooName);
Publisher<Foo> getFoo(String fooName, String barName);
Mono<Void> insertFoo(Foo foo);
Mono<Void> updateFoo(Foo foo);
}
다음 예제는 앞서 나온 인터페이스의 구현을 보여줍니다.
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Flux<Foo> getFoo(String fooName) {
// ...
}
@Override
public Publisher<Foo> getFoo(String fooName, String barName) {
// ...
}
@Override
public Mono<Void> insertFoo(Foo foo) {
// ...
}
@Override
public Mono<Void> updateFoo(Foo foo) {
// ...
}
}
Rolling Back a Declarative Transaction
이 섹션에서는 XML 구성에서 간단하고 선언적인 방식으로 트랜잭션 롤백을 제어하는 방법을 설명합니다.
@Transactional 애너테이션을 사용하여 롤백 의미론(rollback semantics)을 선언적으로 제어하는 방법에 대한 자세한 내용은@Transactional Settings을 참조하세요.
Spring Framework의 트랜잭션 인프라에 트랜잭션 작업을 롤백해야 함을 알리는 권장 방법은 트랜잭션 컨텍스트 내에서 실행 중인 코드에서 예외(Exception)를 발생시키는 것입니다.
Spring Framework의 트랜잭션 인프라는 처리되지 않은 예외(unhandled Exception)를 호출 스택을 따라 전파되는 과정에서 감지한 후, 해당 트랜잭션을 롤백 대상으로 지정할지 결정합니다.
Spring Framework의 트랜잭션 인프라는 기본 설정에서 런타임 예외(RuntimeException) 또는 그 하위 클래스(=Unchecked Exception)가 발생한 경우에만 트랜잭션을 롤백 대상으로 표시합니다.
즉, 체크 예외(Checked Exception)는 기본적으로 롤백되지 않으며, 예외가 RuntimeException의 인스턴스이거나 하위 클래스인 경우에만 롤백이 수행됩니다.
(또한, 기본적으로 Error의 인스턴스가 발생한 경우에도 트랜잭션이 롤백됩니다.)
기본 설정에서는 Vavr의 Try 메서드를 지원하여 Failure를 반환할 때 트랜잭션이 자동으로 롤백되도록 설정할 수 있습니다.
이를 통해, 함수형 스타일의 예외 처리를 Try를 사용하여 수행할 수 있으며, 실패(Failure) 발생 시 트랜잭션이 자동으로 롤백됩니다.
Vavr의 Try에 대한 자세한 내용은 공식 Vavr 문서를 참조하세요. 다음은 트랜잭션 메서드에서 Vavr의 Try를 사용하는 예제입니다.
@Transactional
public Try<String> myTransactionalMethod() {
// If myDataAccessOperation throws an exception, it will be caught by the
// Try instance created with Try.of() and wrapped inside the Failure class
// which can be checked using the isFailure() method on the Try instance.
return Try.of(delegate::myDataAccessOperation);
}
Spring Framework 6.1부터는 CompletableFuture (및 일반적인 Future) 반환 값에 대한 특별한 처리가 추가되었습니다.
이는 원래 메서드에서 반환될 때 CompletableFuture가 예외적으로 완료(exceptionally completed)된 경우, 해당 핸들(handle)에 대해 트랜잭션을 롤백하도록 합니다.
이 기능은 주로 @Async 메서드를 위한 것으로,
- 실제 메서드 구현이 CompletableFuture 시그니처를 준수해야 하는 경우를 고려합니다.
- 런타임에서 @Async 처리에 의해 프록시(proxy) 호출을 위한 비동기 핸들(async handle)로 자동 변환됩니다.
- 기존처럼 예외를 다시 던지는(rethrowing an exception) 방식보다, 반환된 핸들에서 예외를 노출하는 방식을 선호합니다.
@Transactional @Async
public CompletableFuture<String> myTransactionalMethod() {
try {
return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
}
catch (DataAccessException ex) {
return CompletableFuture.failedFuture(ex);
}
}
트랜잭션 메서드에서 발생하는 체크 예외(Checked Exception)는 기본 설정에서 롤백되지 않습니다.
롤백 규칙(rollback rules)을 지정하면, 체크 예외를 포함하여 어떤 예외 유형이 트랜잭션을 롤백하도록 할지 정확하게 설정할 수 있습니다.
롤백 규칙(Rollback rules)은 특정 예외(Exception)가 발생했을 때 트랜잭션을 롤백할지 여부를 결정하며, 예외 유형(Exception types) 또는 예외 패턴(Exception patterns)을 기반으로 설정됩니다.
XML에서 rollback-for 및 no-rollback-for 속성을 사용하여 롤백 규칙을 구성할 수 있으며, 이를 통해 패턴으로 규칙을 정의할 수 있습니다.
@Transactional을 사용할 경우, rollbackFor/noRollbackFor 및 rollbackForClassName/noRollbackForClassName 속성을 통해 예외 유형 또는 패턴을 기반으로 롤백 규칙을 설정할 수 있습니다.
예외 유형으로 롤백 규칙을 정의할 경우, 해당 유형은 발생한 예외의 타입과 그 상위 타입(Super Types)까지 일치하는지 확인하며,
이를 통해 타입 안정성을 보장하고 패턴을 사용할 때 발생할 수 있는 의도치 않은 매칭을 방지할 수 있습니다.
예를 들어, jakarta.servlet.ServletException.class 값을 사용하면, jakarta.servlet.ServletException 및 그 하위 클래스(Subclasses)에서 발생한 예외만 매칭됩니다.
예외 패턴으로 롤백 규칙을 정의할 경우, 해당 패턴은 예외 유형의 정규화된(fully qualified) 클래스명 또는 해당 클래스명의 부분 문자열이 될 수 있습니다.
(현재 와일드카드(wildcard)는 지원되지 않습니다.)
예를 들어, "jakarta.servlet.ServletException" 또는 "ServletException" 값을 설정하면, jakarta.servlet.ServletException 및 그 하위 클래스의 예외가 롤백 대상으로 지정됩니다.
패턴이 얼마나 구체적인지, 그리고 패키지 정보를 포함할지 여부를 신중하게 고려해야 합니다. (패키지 정보는 필수 사항이 아닙니다.)
예를 들어, "Exception"이라는 패턴은 거의 모든 예외와 일치하게 되므로, 다른 규칙을 가릴 가능성이 높습니다.
만약 모든 체크 예외(Checked Exception)에 대한 규칙을 정의하려는 것이라면, "java.lang.Exception"이 올바른 선택입니다.
반면, "BaseBusinessException"과 같이 고유한 예외 이름을 사용하는 경우,
해당 예외 패턴을 정의할 때 완전한 클래스명을 사용할 필요가 없을 가능성이 큽니다.
또한, 패턴 기반 롤백 규칙은 유사한 이름을 가진 예외나 중첩 클래스(Nested Class)에 대해 의도치 않은 매칭을 초래할 수도 있습니다.
이는 예외가 발생했을 때, 해당 예외의 이름이 롤백 규칙에 설정된 예외 패턴을 포함하고 있으면, 해당 패턴과 일치하는 것으로 간주되기 때문입니다.
예를 들어, "com.example.CustomException"을 기반으로 설정된 규칙이 있는 경우, 이 규칙은 다음과 같은 예외에도 일치할 수 있습니다.
- com.example.CustomExceptionV2 (CustomException과 같은 패키지에 있지만, 접미사가 추가된 예외)
- com.example.CustomException$AnotherException (CustomException 내에 선언된 중첩 클래스 예외)
다음 XML 코드 조각은 rollback-for 속성을 통해 예외 패턴을 지정하여, 특정한 체크 예외(Checked Exception)에 대해 롤백을 구성하는 방법을 보여줍니다.
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
예외가 발생했을 때 트랜잭션이 롤백되지 않도록 하려면 '롤백 제외(no rollback)' 규칙을 지정할 수도 있습니다.
다음 예제는 InstrumentNotFoundException이 발생하더라도 Spring Framework의 트랜잭션 인프라가 트랜잭션을 커밋하도록 설정하는 방법을 보여줍니다.
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
Spring Framework의 트랜잭션 인프라는 예외를 감지한 후, 구성된 롤백 규칙을 확인하여 트랜잭션을 롤백할지 여부를 결정합니다.
이 과정에서 가장 강력하게 일치하는 규칙(strongest matching rule)이 우선 적용됩니다.
따라서, 다음과 같은 설정이 있는 경우:
- InstrumentNotFoundException을 제외한 모든 예외는 트랜잭션을 롤백합니다.
- 즉, InstrumentNotFoundException이 발생하면 트랜잭션이 롤백되지 않고 커밋됩니다.
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
프로그래밍 방식으로 트랜잭션 롤백을 요구할 수도 있습니다.
이 방식은 간단하지만, 코드가 Spring Framework의 트랜잭션 인프라에 강하게 결합(Tightly Coupled)되는 단점이 있습니다.
다음 예제는 프로그래밍 방식으로 트랜잭션 롤백을 요구하는 방법을 보여줍니다.
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
가능하다면 선언적 방식(declarative approach)으로 롤백을 설정하는 것이 강력히 권장됩니다.
프로그래밍 방식의 롤백(programmatic rollback)은 필요할 경우 사용할 수 있지만,
이를 사용하면 깨끗한 POJO 기반 아키텍처(Clean POJO-based Architecture)를 유지하는 데 반하는 방식이 될 수 있습니다.
Configuring Different Transactional Sematics for Different Beans
여러 개의 서비스 레이어 객체(service layer objects)가 있고, 각각에 대해 완전히 다른 트랜잭션 설정(transactional configuration)을 적용하려는 상황을 가정해 보세요. 이 경우, 서로 다른 pointcut 및 advice-ref 속성 값을 가진 개별적인 <aop:advisor/> 요소를 정의하여 이를 구현할 수 있습니다.
비교를 위한 기준점으로, 먼저 모든 서비스 레이어 클래스(service layer classes)가 x.y.service 루트 패키지에 정의되어 있다고 가정해 보세요. 이제, 해당 패키지(또는 하위 패키지)에 정의된 클래스의 인스턴스 중에서 이름이 "Service"로 끝나는 모든 빈(Bean)에 기본 트랜잭션 설정(default transactional configuration)을 적용하려면, 다음과 같이 작성할 수 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* x.y.service..*Service.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<!-- these two beans will be transactional... -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<bean id="barService" class="x.y.service.extras.SimpleBarService"/>
<!-- ... and these two beans won't -->
<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
The following example shows how to configure two distinct beans with totally different transactional settings:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="defaultServiceOperation"
expression="execution(* x.y.service.*Service.*(..))"/>
<aop:pointcut id="noTxServiceOperation"
expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>
<aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>
<aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>
</aop:config>
<!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this bean will also be transactional, but with totally different transactional settings -->
<bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>
<tx:advice id="defaultTxAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<tx:advice id="noTxAdvice">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
참고 문헌 : https://docs.spring.io/spring-framework/reference/index.html