@Transactional 란?
@Transactional
어노테이션은 스프링에서 많이 사용되는 선언적 트랜잭션 방식이다.
해당 어노테이션은 getConnection(), setAutoCommit(false), 예외 발생 시 롤백, 정상 종료 시 커밋 등의 필요한 코드를 삽입해준다.
@Transactional 사용 방법
레거시 스프링에선 해당 어노테이션을 사용하기 위해선 PlatformTransactionManager
와 어노테이션 활성화 설정이 필요하다.
스프링 부트에선 @EnableTransactionManagement
설정이 되어 있어서 자동으로 사용할 수 있으며 입맛에 맞게 클래스 또는 메서드에 @Transactional 어노테이션을 적용하면 된다.
PlatformTransactionManager
는 스프링이 제공하는 TransactionManager의 최상위 인터페이스로, 환경에 맞는 클래스를 주입할 수 있도록 구성되어 있다. DataSourceTransactionManger, JpaTransactionManager 등 필요한 정보를 Bean으로 등록하고 DI를 받아 사용한다.- 스프링 컨테이너는 @Transactional 어노테이션이 있으면, 해당 타겟 빈을 상속받은 프록시 객체를 생성한다. 따라서 private 메서드는 상속이 불가하기 때문에 어노테이션을 붙여도 동작하지 않는다.
@Configuration
@EnableTransactionManagement
public class MemberConfig {
@Bean
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("oracle.jdbc.OracleDriver");
ds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
ds.setUsername("아이디");
ds.setPassword("비밀번호");
ds.setInitialSize(2);
ds.setMaxActive(10);
ds.setTestWhileIdle(true);
ds.setMinEvictableIdleTimeMillis(60000 * 3);
ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
}
단, @Transcation 어노테이션을 사용할 때 상속 불가능한 private 메서드의 경우 트랜잭션이 동작하지 않는다.
@Transcational 동작 원리
@Transactional은 Spring AOP를 통해 프록시 객체를 생성하여 사용된다.
스프링에서 Target 객체를 직접 참조하지 않고, 프록시 객체를 사용하는 이유는, Aspect 클래스에서 제공하는 부가 기능을 사용하기 위해서이다. Target 객체를 직접 참조하는 경우, 원하는 위치에서 직접 Aspect 클래스를 호출해야하기 때문에 유지보수가 어려워진다.
스프링에서 사용하는 프록시 구현체는 JDK Proxy(Dynamic Proxy), CGLib 두 가지가 있다.
- AOP 프록시 생성과정에서
Target
객체의 인터페이스 구현 여부에 따라 다음과 같이 나뉜다. - JDK Dynamic Proxy : Target 클래스가 인터페이스 구현체일 경우 생성되며, 구현 클래스가 아닌 인터페이스를 프록시 객체로 구현해서 코드에 끼워넣는 방식이다.
- CGLib Proxy : 스프링에서 사용하는 디폴트 프록시 생성방식으로, Target 클래스를 프록시 객체로 생성하여 코드에 끼워넣는 방식이다.
JDK 방식은 java.lang.Reflection 을 이용해서 동적으로 프록시를 생성해준다. 해당 방식의 단점은 AOP 적용을 위해 반드시 인터페이스를 구현해야된다는 점, 리플렉션은 private 접근이 가능하다는 점 때문에 스프링 부트에선 기본 방식으로 CGLib 방식을 채택하였다. (스프링 레거시는 JDK 기본 동작)
CGLib는 바이트 코드를 조작하여 프록시 객체를 생성한다. 직접 원본 객체를 호출하지 않고 MethodInterceptor 와 같은 프록시와 원본 객체 사이에 인터셉터를 두어 메서드 호출을 조작할 수 있도록 도와준다.
- Target에 대한 호출이 오면, AOP 프록시가 인터셉터 체인을 통해 가로채온 후 Transaction Advisor에게 전달한다.
- Transaction Advisor는 트랜잭션을 생성한다.
- Custom Advisor가 있다면, 실행한 후 비즈니스 로직을 호출한다.
- Transaction Advisor는 커밋 또는 롤백 등의 트랜잭션 결과를 반환한다.
트랜잭션 인터셉터
트랜잭션은 커밋이나 롤백 등 어떤 메서드가 호출될지 모르기 때문에 어드바이스 메서드를 지정할 수 없고, aspect가 아닌 advisor로 aop를 설정해야 한다.
메서드 별로 다른 트랜잭션을 적용하려면 곧 어드바이스의 기능을 확장해야 한다. 스프링에선 메서드 패턴에 따라 경계 설정을 할 수 있도록 TransactionInterceptor
를 제공 한다.
TransactionInterceptor는 PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 갖고 있으며, 그 중 Properties가 transactionAttributes로 트랜잭션 속성을 정의한 프로퍼티다.
<bean id="transactionAdvice"
class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly,timeout_30</prop>
<prop key="upgrade*">PROPAGATION_REQUIRES_NEW,ISOLATION_SERIALIZABLE</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
- 메서드 패턴을 이용한 트랜잭션 속성 지정으로, PROPAGATION_XXX, ISOLATION_XXX, readOnly, timout_XXX 등을 정의할 수 있다. 이 중에서 PROPAGATION(전파 방식)은 필수로 등록해야 한다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30"/>
<tx:method name="upgrade*" propagation="REQUIRES_NEW" isolation="SERIALIZABLE"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
- tx 네임스페이스를 이용한 설정 방법으로 tx 스키마의 태그를 이용하여 정의할 수 있다.
<tx:method>
속성 종류name
: 트랜잭션이 적용될 메서드 패턴 지정read-only
: 읽기 전용 여부 지정 (default : false)no-rollback-for
: 롤백하지 않는 경우 지정rollback-for
: 롤백할 경우 지정propagation
: 트랜잭션 전파 범위 지정- REQUIRED (기본값) : 이미 진행 중인 트랜잭션이 없으면 새로 시작하고, 있다면 기존 트랜잭션에 참여
- REQUIRED_NEW : 트랜잭션 존재 유무와 상관 없이 항상 새로운 트랜잭션 시작 (독립 보장)
- MANDATORY : 진행 중인 트랜잭션이 없으면 예외를 발생하고, 있으면 참여한다. (독립 트랜잭션 생성 안됨을 보장)
- SUPPORTS : 메소드가 트랜잭션을 필요로 하지는 않지만, 진행 중인 트랜잭션이 존재하면 트랜잭션을 사용한다는 것을 의미한다. 진행 중인 트랜잭션이 존재하지 않더라도 메소드는 정상적으로 동작한다.
- NESTED : 이미 실행 중인 트랜잭션이 존재하면, 자식 트랜잭션을 만든다.(부모의 커밋과 롤백에는 영향 받지만, 자식의 결과는 부모에게 영향을 주지 않는다.), 실행 중인 트랜잭션이 없다면, 새로운 트랜잭션을 생성해서 사용한다.
- NEVER : 트랜잭션을 사용하지 않도록 강제한다. 트랜잭션이 존재하면 예외를 발생한다.
트랜잭션 사용 시 주의사항
- private 메서드는 트랜잭션의 대상이 될 수 없다.
public void test1() {
test2();
}
@Transactional
public void test2() {
// 상위 메서드인 test1이 타깃 오브젝트가 되어 트랜잭션 적용 안됨
}
- 스프링에서 트랜잭션은 처음으로 호출하는 메서드나 클래스의 속성을 따라가게 되어있다. 따라서 동일한 빈 안에서 하위 메서드에서만 트랜잭션이 설정되어 있다면, 전이되지 않는다. (반대로, 상위에 적용되면 하위 메서드는 트랜잭션 설정이 없어도 전이가 된다.) 따라서 클래스를 분리하거나, 상위 메서드에 트랜잭션을 설정해야한다.
RuntimeException
이나Error
의 경우에만 실패시 롤백이 된다. Exception의 경우 rollbackFor 옵션을 주어 처리하는 방법도 있다.
REFERENCES
- 토비의 스프링 3.1
- https://steady-coding.tistory.com/610
- https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC
- https://minkukjo.github.io/framework/2021/05/23/Spring/
- https://moonhy7.tistory.com/entry/Spring-7%EC%9E%A5-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%B2%98%EB%A6%AC
- https://devbelly.tistory.com/181