Spring에서 선언형 방식으로 트랜잭션을 적용하는 방법은 크게 두 가지이다.
@Configuration
@EnableTransactionManagement
public class JpaConfig{
...
...
// (1)
@Bean
public DataSource dataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
...
...
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(){
JpaTransactionManager transactionManager
= new JpaTransactionManager(); // (2)
transactionManager.setEntityManagerFactory(
entityManagerFactoryBean().getObject() );
return transactionManager;
}
}
트랜잭션은 기본적으로 데이터베이스와의 인터랙션과 관련이 있기 때문에 (1)과 같이 데이터베이스 커넥션 정보를 포함하고 있는 Datasource가 필요하다.
Spring에서 트랜잭션은 기본적으로 PlatformTransactionManager
에 의해 관리되며, PlatformTransactionManager
인터페이스를 구현해서 해당 데이터 액세스 기술에 맞게 유연하게 트랜잭션을 적용 할 수 있도록 추상화 되어 있다.
현재 사용하는 데이터 액세스 기술이 JPA이기 때문에 (2)와 같이 PlatformTransactionManager
의 구현 클래스인 JpaTransactionManager
를 사용한다.
Spring에서 트랜잭션을 적용하는 가장 간단한 방법은 @Transactional
이라는 애너테이션을 트랜잭션이 필요한 영역에 추가해 주는 것이다.
@Transactional
적용import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // (1)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
return memberRepository.save(member);
}
...
...
}
(1)과 같이 @Transactional
애너테이션을 클래스 레벨에 추가하면 기본적으로 해당 클래스에서 MemberRepository의 기능을 이용하는 모든 메서드에 트랜잭션이 적용된다.
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
...
...
logging: # (1)
level:
org:
springframework:
orm:
jpa: DEBUG
애플리케이션을 실행시키기 전에 트랜잭션이 어떻게 적용되는지 로그로 확인할 수 있도록 JPA의 로그 레벨을 application.yml
에 추가한다.
로그 레벨을 ‘DEBUG
’ 레벨로 설정하면 JPA 내부에서 ‘DEBUG
’ 로그 레벨을 지정한 부분의 로그를 확인할 수 있다.
// (1) 트랜잭션 생성
Creating new transaction with name [com.codestates.member.service.MemberService.
createMember]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
...
...
Initiating transaction commit
...
...
// (2) 커밋
Committing JPA transaction on EntityManager [SessionImpl(1508768151<open>)]
// (3) 트랜잭션 종료
Not closing pre-bound JPA EntityManager **after transaction**
// (4) EntityManager 종료
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
MemberService
의 createMember()
메서드가 호출되며 새로운 트랜잭션이 생성EntityManager
를 종료함을 확인@Service
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
Member resultMember = memberRepository.save(member);
if (true) { // (1)
throw new RuntimeException("Rollback test");
}
return resultMember;
}
...
...
}
createMember()
메서드에서 회원 정보를 저장하고 메서드가 종료되기 전에 (1)과 같이 강제로 RuntimeException
이 발생하도록 수정한 뒤, 회원 정보를 저장한다.
Initiating transaction rollback // (1)
// (2)
Rolling back JPA transaction on EntityManager [SessionImpl(1632113004<open>)]
Not closing pre-bound JPA EntityManager after transaction
...
...
java.lang.RuntimeException: Rollback test
...
...
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
로그를 확인해보면 rollback이 정상적으로 동작하는 것을 확인할 수 있다.
이는 H2 웹 콘솔을 통해서 MEMBER 테이블을 조회해도 저장된 회원 정보가 없는 것을 확인할 수 있다.
체크 예외의 경우, @Transactional
애너테이션만 추가해서는 rollback이 되지 않는다.
따라서, 캐치(catch)한 후에 해당 예외를 복구할지 회피할지 등의 적절한 예외 전략을 고민해 볼 필요가 있다.
만약 별도의 예외 전략을 짤 필요가 없다면 @Transactional(rollbackFor = {SQLException.class, DataFormatException.class})
와 같이 해당 체크 예외를 직접 지정해주거나 언체크 예외(unchecked exception)로 감싸서 rollback이 동작하도록 할 수 있다.
@Transactional
적용import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional // (1)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// (2)
@Transactional(readOnly = true)
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
...
...
}
클래스 레벨의 @Transactional
애너테이션 이외에 (2)와 같이 메서드 레벨에 @Transactional(readOnly = true)
를 추가했다.
이런 경우, 해당 메서드는 읽기 전용 트랜잭션이 적용된다.
@Transactional(readOnly = true)
로 설정하면 JPA 내부적으로 영속성 컨텍스트를 flush하지 않는다. 그리고 읽기 전용 트랜잭션일 경우, 변경 감지를 위한 스냅샷 생성도 하지 않는다.
이처럼 flush 처리를 하지 않고, 스냅샷을 생성하지 않으므로 불필요한 추가 동작을 줄일 수 있다.
따라서, 조회 메서드에서 @Transactional(readOnly = true)
로 설정하면 JPA가 자체적으로 성능 최적화 과정을 거치도록 할 수 있다.
@Transactional
이 적용된 경우@Transactional
애너테이션이 메서드에 일괄 적용@Transactional
애너테이션이 적용@Transactional
애너테이션이 적용되지 않았을 경우, 클래스 레벨의 @Transactional
애너테이션이 적용ACID 원칙에서 트랜잭션은 다른 트랜잭션에 영향을 주지 않고, 독립적으로 실행되어야 하는 격리성이 보장되어야 하는데 Spring은 이런 격리성을 조정할 수 있는 옵션을 @Transactional
애너테이션의 isolation
애트리뷰트를 통해 제공하고 있다.
트랜잭션의 격리 레벨을 일반적으로 데이터베이스나 데이터 소스에 설정된 격리 레벨을 따르는 것이 권장된다.
Isolation.DEFAULT
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
AOP를 이용해 트랜잭션을 적용하면 @Transactional
애너테이션 조차도 비즈니스 로직에 적용하지 않고, 트랜잭션을 적용할 수 있다.
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.interceptor.*;
import java.util.HashMap;
import java.util.Map;
// (1)
@Configuration
public class TxConfig {
private final TransactionManager transactionManager;
// (2)
public TxConfig(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Bean
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource txAttributeSource =
new NameMatchTransactionAttributeSource();
// (3)
RuleBasedTransactionAttribute txAttribute =
new RuleBasedTransactionAttribute();
txAttribute.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// (4)
RuleBasedTransactionAttribute txFindAttribute =
new RuleBasedTransactionAttribute();
txFindAttribute.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRED);
txFindAttribute.setReadOnly(true);
// (5)
Map<String, TransactionAttribute> txMethods = new HashMap<>();
txMethods.put("find*", txFindAttribute);
txMethods.put("*", txAttribute);
// (6)
txAttributeSource.setNameMap(txMethods);
// (7)
return new TransactionInterceptor(transactionManager, txAttributeSource);
}
@Bean
public Advisor txAdvisor() {
// (8)
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* com.codestates.coffee.service." +
"CoffeeService.*(..))");
return new DefaultPointcutAdvisor(pointcut, txAdvice()); // (9)
}
}
AOP 방식으로 트랜잭션을 적용하기 위한 Configuration
클래스 정의
TransactionManager
DI
트랜잭션 어드바이스용 TransactionInterceptor
빈 등록
Spring에서는 TransactionInterceptor
를 이용해서 대상 클래스 또는 인터페이스에 트랜잭션 경계를 설정하고 트랜잭션을 적용할 수 있다.
메서드 이름 패턴
에 따라 구분해서 적용 가능하기 때문에 (3), (4)와 같이 트랜잭션 애트리뷰트를 설정할 수 있음메서드 이름 패턴
으로 지정해서 각각의 트랜잭션 애트리뷰트를 추가txAttributeSource.setNameMap(txMethods)
으로 전달TransactionInterceptor
객체 생성TransactionInterceptor
의 생성자 파라미터로 transactionManager
와 txAttributeSource
를 전달Advisor 빈 등록
TransactionInterceptor
를 타겟 클래스에 적용하기 위해 포인트컷을 지정AspectJExpressionPointcut
객체를 생성한 후, 포인트컷 표현식을 이용해 타겟 클래스로 지정DefaultPointcutAdvisor
의 생성자 파라미터로 포인트컷과 어드바이스를 전달