트랜잭션(Transaction 이하 트랜잭션)이란, 데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위를 뜻한다. 코드에서 선언을 하여서 분기별로 데이터베이스 작업이 이뤄질때 추가작업을 지시 가능합니다. 아래의 그림을 보면 이해하실겁니다.
간단하게 Transaction을 사용할 수 있는 메타 어노테이션입니다.
ElementType.TYPE
(class, interface등), ElementType.METHOD
의 단계에서 사용가능하며, 옵션들을 이용해서 커스텀이 가능합니다. 옵션에 대한 설명은 하단에서 하겠습니다.
@Transactional // 여기에 옵션을 넣어서 Transaction이 발생할때 무엇을 하고 싶은지 커스텀 가능합니다.
public void increaseCount(String name) {
Hello hello = findHello(name);
if(hello == null) jdbcTemplate.update("insert into hello values(?, ?)", name, 1);
else jdbcTemplate.update("update hello set count = ? where name = ?", hello.getCount()+1, name);
}
AOP와연동시켜 매번 @Transactional을 선언하지 않고 일괄적용이 가능합니다.
AOP의 대한 설명은 여기서 확인 가능합니다.
tx:
는 필요한 context schema를 namespace로 선언하셔야합니다.
<beans xmlns:tx="http://www.springframework.org/schema/tx"
// ...생략
>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/> //dbclass를 잡아주셔야합니다. JDBC가 선언되어있는 context의 bean name을 ref에 쓰시면 됩니다.
</bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" rollback-for="java.lang.RuntimeException, java.lang.NullpointExceiption" />
</tx:attributes>
</tx:advice>
// tx:attributes는 @Transactional의 옵션과 같고 선언하여 동일하게 이용가능 합니다.
// tx:method는 select, insert등.. 쿼리문 안에 뭐랑 반응할지 써주시면 됩니다. *은 모두
// rollback-for은 어떤상황에서 롤백하는지를 넣어주시면 됩니다. 여러개의 Exception이라면 ,로 구분합니다.
<aop:config>
<aop:pointcut id="requiredTx" expression="execution(* com.service..*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx"/>
</aop:config>
// AOP 설명은 제 다른 글을 보시면 됩니다.
<tx:annotation-driven transaction-manager="txManager"/>
마찬가지로 XML방법이 아닌 Java에서 Bean 어노테이션으로 사용가능합니다.
JAVA 방식은 매서드들의 이름이 직관적이라 설명안드려도 이해가 가실겁니다.
@Component
public class MyTransactionManager {
private static final String AOP_TRANSACTION_METHOD_NAME="*";
private static final String AOP_TRANSACTION_EXPRESSION="execution(* com.service..*.*(..))";
public MyTransactionManager(DataSource dataSource) {
DataSourceTransactionManager txMgr = new DataSourceTransactionManager();
txMgr.setDataSource(dataSource);
this.transactionManager = txMgr;
}
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor transactionAdvice(){
MatchAlwaysTransactionAttributeSource source = new MatchAlwaysTransactionAttributeSource();
RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute();
transactionAttribute.setName(AOP_TRANSACTION_METHOD_NAME);
transactionAttribute.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
source.setTransactionAttribute(transactionAttribute);
return new TransactionInterceptor(transactionManager, source);
}
@Bean
public Advisor transactionAdviceAdvisor(){
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_TRANSACTION_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, transactionAdvice());
}
}
사진과 같이 적용전에는 에러가나도 1로 업데이트 되지만, 적용 후에는 업데이트가 안되는걸 보실 수 있습니다.
Spring 에서 @Transactional 을 사용할 때 지정할 수 있는 옵션들을 알아봅니다.
사용법은 다음과 같습니다.
@Transactional(
propagation = Propagation.REQUIRED
, isolation = Isolation.DEFAULT
, rollbackFor = {IllegalAccessException.class, OutOfMemoryError.class}
)
옵션의 종류들입니다.
기본값 : DEFAULT
데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질 네가지가 존재합니다. (ACID)
@Transactional 의 isolation 은 동시에 여러 사용자가 데이터에 접근할 때 어디까지 허용할까? 를 정하는 옵션이라고 생각하면 됩니다.
사용하는 DB 의 기본 격리 수준을 따름
한 트랜잭션이 처리 중인 커밋되지 않은 데이터를 다른 트랜잭션에서 접근 가능합니다.
DB 에 커밋하지 않은, 즉 존재하지 않는 데이터를 읽는 현상을 Dirty Read 라고 합니다.
데이터 정합성에 문제가 많아서 웬만하면 권장되지 않고 아예 지원하지 않는 경우도 있습니다.
Dirty Read 가 가능하기 때문에 잘못된 데이터를 읽을 수 있습니다.
▪ A 트랜잭션이 데이터 1 을 조회하여 2 로 변경하고 아직 커밋하지 않음
▪ B 트랜잭션이 동일한 데이터를 조회해서 2 라는 값을 받음 (Dirty Read)
▪ A 트랜잭션에서 오류가 발생해서 데이터를 롤백 (2 -> 1)
▪ 실제 데이터는 1 이지만 B 트랜잭션은 2 라는 잘못된 데이터를 읽은 셈
트랜잭션은 커밋한 데이터만 읽을 수 있습니다.
A 트랜잭션이 데이터를 변경해도 커밋하기 전이라면 B 트랜잭션은 변경되기 전의 데이터를 조회할 수 있습니다.
이 때, B 트랜잭션은 Undo 영역에서 데이터를 가져옵니다. (MVCC - Multi Version Concurrency Control 참조)
매 조회 시마다 새로운 스냅샷을 뜨기 때문에 다른 트랜잭션이 커밋한 후 다시 조회하면 변경된 데이터를 볼 수 있습니다.
대부분의 DB 기본 격리 수준이며 REPEATABLE_READ 와 함께 가장 많이 사용되는 방식입니다.
Non-Repeatable Read 현상이 발생할 수 있습니다.
트랜잭션에서 조회한 데이터가 트랜잭션이 끝나기 전에 다른 트랜잭션에 의해 변경되면 다시 읽었을 때 새로운 값이 읽히며 데이터 불일치하는 현상을 말합니다.
하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때 항상 같은 결과를 가져와야 한다는 REPEATABLE READ 정합성 정의에 어긋납니다.
▪ A 트랜잭션이 데이터 (row) 를 읽음
▪ B 트랜잭션이 같은 데이터를 수정하고 커밋
▪ A 트랜잭션이 다시 같은 데이터를 읽었는데 데이터는 수정전 처음 데이터
간단히 말하면 하나의 트랜잭션은 하나의 스냅샷만 사용하는 겁니다.
A 트랜잭션이 시작하고 처음 조회한 데이터의 스냅샷을 저장하고 이후에 동일한 쿼리를 호출하면 스냅샷에서 데이터를 가져옵니다.
따라서 중간에 B 트랜잭션이 새로 커밋해도 A 트랜잭션이 조회하는 데이터는 변하지 않습니다.
Phantom Read 라는 다른 트랜잭션에서 수행한 작업에 의해 안보였던 데이터가 보이는 현상이 발생할 수 있습니다.
REPEATABLE_READ 격리 수준은 조회한 데이터에 대해서만 Shared Lock 이 걸리기 때문에 다른 트랜잭션이 새로운 데이터를 추가할 수 있습니다.
▪ A 트랜잭션이 조회한 데이터는 0 건
▪ B 트랜잭션이 새로운 데이터를 추가하고 커밋
▪ A 트랜잭션이 같은 쿼리로 다시 조회했더니 B 트랜잭션이 추가한 데이터까지 같이 조회됨
가장 단순하고 엄격한 격리 수준입니다.
이름 그대로 순차적으로 트랜잭션을 진행시키며 읽기 작업에도 잠금을 걸어 여러 트랜잭션이 동시에 같은 데이터에 접근하지 못합니다.
가장 안전하지만 성능 저하가 발생하기 때문에 극도의 안정성을 필요로 하지 않으면 자주 사용되지 않습니다.
기본값 : REQUIRED
현재 진행중인 트랜잭션 (부모 트랜잭션) 이 존재할 때 새로운 트랜잭션 메소드를 호출하는 경우 어떤 정책을 사용할 지에 대한 정의입니다.
예를 들어, 기존 트랜잭션에 참여해서 그대로 이어갈 수도 있고, 새로운 트랜잭션을 생성할 수도 있으며 non-transactional 상태로 실행할 수도 있습니다.
처음에 non-transactional 상태로 실행한다라는 개념에 대해 착각을 했었는데 트랜잭션은 존재하지만 커밋, 롤백이 되지 않는 상태입니다.
그래서 NOT_SUPPORTED
같은 트랜잭션은 TransactionSynchronizationManager.getCurrentTransactionName() 메소드로 조회했을 때 이름이 존재하지만 JPA Dirty Checking 은 동작하지 않습니다.
Spring 의 @Transactional
에서는 다음과 같은 propagation 옵션을 제공합니다.
REQUIRED
: 기본값이며 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 새 트랜잭션을 시작
SUPPORTS
: 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 non-transactional 상태로 실행
MANDATORY
: 부모 트랜잭션이 있으면 참여하고 없으면 예외 발생
REQUIRES_NEW
: 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성
NOT_SUPPORTED
: non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 일시 정지시킴
NEVER
: non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 예외 발생
NESTED
: 부모 트랜잭션과는 별개의 중첩된 트랜잭션을 만듬
부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않음.
부모 트랜잭션이 없는 경우 새로운 트랜잭션을 만듬 (REQUIRED
와 동일)
DB 가 SAVEPOINT 를 지원해야 사용 가능 (Oracle)
JpaTransactionManager 에서는 지원하지 않음
기본값 : false
기본값은 false 이며 true 로 세팅하는 경우 트랜잭션을 읽기 전용으로 변경합니다.
만약 읽기 전용 트랜잭션 내에서 INSERT, UPDATE, DELETE 작업을 해도 반영이 되지 않거나 DB 종류에 따라서 아예 예외가 발생하는 경우도 있습니다.
성능 향상을 위해 사용하거나 읽기 외의 다른 동작을 방지하기 위해 사용하기도 합니다.
JPA 에는 Dirty Checking 이라는 기능이 있습니다.
개발자가 임의로 UPDATE 쿼리를 사용하지 않아도 트랜잭션 커밋 시에 1차 캐시에 저장되어 있는 Entity 와 스냅샷을 비교해서 변경된 부분이 있으면 UPDATE 쿼리를 날려주는 기능입니다.
하지만 readOnly = true 옵션을 주면 스프링 프레임워크가 하이버네이트의 FlushMode 를 MANUAL 로 설정해서 Dirty Checking 에 필요한 스냅샷 비교 등을 생략하기 때문에 성능이 향상됩니다.
기본값 : RuntimeException
, Error
기본적으로 트랜잭션은 종료 시 변경된 데이터를 커밋합니다.
하지만 @Transactional 에서 rollbackFor 속성을 지정하면 특정 Exception 발생 시 데이터를 커밋하지 않고 롤백하도록 변경할 수 있습니다.
기본값은 {} 라고 나와있지만 사실 RuntimeException 과 Error 가 세팅되어 있습니다.
내부 로직으로 들어가 설명을 보면 둘 다 예측 불가능한 예외 상황이기 때문에 기본값으로 들어가 있다고 합니다.
중요한 점은 이 값은 그냥 기본값이 아니라 아예 지정된 값이기 때문에 rollbackFor 속성으로 다른 Exception 을 추가해도 RuntimeException 이나 Error 는 여전히 데이터를 롤백합니다.
만약 강제로 데이터 롤백을 막고 싶다면 noRollbackFor 옵션으로 지정해주면 됩니다.
기본값 : -1
지정한 시간 내에 해당 메소드 수행이 완료되이 않은 경우 JpaSystemException 을 발생시킵니다.
JpaSystemException 은 RuntimeException 을 상속받기 때문에 데이터 역시 롤백 처리 됩니다.
초 단위로 지정할 수 있으며 기본값인 -1 인 경우엔 timeout 을 지원하지 않습니다.
지정된 timeout 을 초과하면 다음과 같은 에러 로그를 보여줍니다.
org.springframework.orm.jpa.JpaSystemException: transaction timeout expired; nested exception is org.hibernate.TransactionException: transaction timeout expired
noRollbackFor = {RuntimeException.class, JpaSystemException.class} 옵션을 추가하고 타임아웃 테스트를 해보았습니다.
Exception 은 발생하지만 롤백 처리가 됩니다.
참고자료들___
(참고) DB Transaction 의 특징과 Spring Boot @Transactional 옵션
(참고) @Transaction과 AOP를 이용해서 트랜잭션 처리하기 & 차이점