Propagation은 우리의 비즈니스 로직에서의 트랜잭션 범위를 정의한다.
@Transactional을 쓸 때, 스프링은 트랜잭션의 시작과 중지를 propagation 세팅을 바탕으로 관리한다.
스프링이 트랜잭션을 만들 때 TransactionManager::getTransaction을 호출하며, 이때 propagation 세팅이 영향을 준다.
REQUIRED는 별도 옵션을 설정하지 않을 경우 default로 설정되는 속성이다.
만약 현재 활성화된 트랜잭션이 존재하면, 해당 트랜잭션에 비즈니스 로직을 append한다.
그러나 활성화된 트랜잭션이 없다면, 새로운 트랜잭션을 만들어준다.
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return createNewTransaction();
SUPPORTS는 활성화된 트랜잭션이 존재하는지 확인하고, 존재하면 해당 트랜잭션을 사용한다.
그러나 활성화된 트랜잭션이 없다면, 트랜잭션 없이 로직을 수행한다.
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return emptyTransaction;
MANDATORY는 활성화된 트랜잭션이 존재하는지 확인하고, 존재하면 해당 트랜잭션을 사용한다.
그러나 활성화된 트랜잭션이 없다면, 예외를 발생시킨다.
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
throw IllegalTransactionStateException;
NEVER로 옵션을 설정할 경우, 활성화된 트랜잭션이 존재하면 예외를 발생시킨다.
활성화된 트랜잭션이 없다면 트랜잭션을 사용하지 않는다.
if (isExistingTransaction()) {
throw IllegalTransactionStateException;
}
return emptyTransaction;
NOT_SUPPORTED는 활성화된 트랜잭션이 존재할 경우, 이를 보류시킨다.
그리고 비즈니스 로직을 트랜잭션 없이 실행하도록 한다.
REQUIRES_NEW를 사용할 경우, 항상 새로운 트랜잭션을 시작한다.
그러나 이미 진행중인 트랜잭션이 존재하면, 해당 트랜잭션을 잠시 보류시킨다.
트랜잭션이 존재할 때 보류시킨다는 점은 NOT_SUPPORTED와 유사하다.
if (isExistingTransaction()) {
suspend(existing);
try {
return createNewTransaction();
} catch (exception) {
resumeAfterBeginException();
throw exception;
}
}
return createNewTransaction();
NESTED는 활성화된 트랜잭션이 존재할 경우, 해당 트랜잭션의 세이브 포인트를 기록한다.
이후 해당 트랜잭션 내에 중첩 트랜잭션을 만들어 작업을 하게 되는데, 만약 그 과정에서 비즈니스 로직에 예외가 발생하면 세이브 포인트로 롤백한다.
즉, 하위 트랜잭션은 상위 트랜잭션에 영향을 받지만, 상위 트랜잭션은 하위 트랜잭션에 영향을 덜 받도록 구축할 수 있다.
만약 활성화된 트랜잭션이 없다면, REQUIRED처럼 작동한다.
isolation(고립, 격리)은 앞서의 포스트에서 다뤘던 ACID의 요소 중 하나이다.
isolation은 동시에 여러 트랜잭션에 의한 변경 사항이 어떻게 적용되는지를 설정한다.
각각의 격리 수준은 아래의 동시성 부작용을 방지한다.
만약 isolation level을 설정하지 않았다면, DEFAULT로 설정된다. 이 경우, 트랜잭션의 isolation level은 RDBMS가 지정한 값으로 설정된다. 그렇기 때문에, DB를 바꿀 경우 주의해야 할 필요가 있다.
READ_UNCOMMITTED는 가장 낮은 isolation level로, 커밋되지 않는 데이터(트랜잭션 처리중인 데이터)에 대한 읽기를 허용한다. 이는 가장 많은 동시 엑세스를 허용한다.
이 경우, 위에서 언급된 세 가지 동시성의 부작용이 모두 발생하게 된다.
그렇기 때문에 Postgres는 READ_UNCOMMITTED를 허용하지 않고 READ_COMMITTED를 사용하도록 하며, Oracle은 READ_UNCOMMITTED를 지원하거나 허용하지 않고 있다.
READ_COMMITTED는 트랜잭션에서 커밋된 데이터만 읽을 수 있기 때문에, 위의 세 가지 동시성 부작용 중 Dirty read를 방지한다. 풀어서 말하자면, 어떤 데이터가 변경되는 중일 경우(변경사항이 반영되지 않은 경우) 해당 데이터는 접근할 수 없게 된다.
그러나 나머지 동시성 부작용을 해결하지 못하기 때문에, 아직 문제의 여지가 남아있다.
READ_COMMITTED는 Postgres와 SQL Server 및 Oracle의 기본 수준이다.
REPEATABLE_READ는 트랜잭션이 완료될 때까지 SELECT문이 사용하는 모든 데이터에 shared lock을 걸게 된다.
그렇기 때문에 select 쿼리를 여러 번 같은 트랜잭션 내에서 사용하더라도 같은 값을 조회하게 되고, 해당 값들에 대한 변경이나 삭제가 불가해진다.
그렇기 때문에 동시성 부작용 중 Dirty read와 Nonrepeatable read를 방지한다.
REPEATABLE_READ는 Mysql의 기본 수준이며, Oracle은 REPEATABLE_READ를 지원하지 않는다.
SERIALIZABLE은 최고 수준의 isolation level로, 세 가지 동시성 부작용을 모두 방지한다.
그러나 동시 호출을 순차적으로 실행하기 때문에, 동시 엑세스 속도가 가장 낮을 수 있다.
즉, SERIALIZABLE를 사용하는 트랜잭션 그룹의 동시 실행은 순차적으로 실행하는 것과 동일한 결과를 가진다.
그러한 원리에서, SERIALIZABLE은 MVCC(다중 버전 동시성 제어, Multi-Version Concurrency Control)을 지원하지 않는다.
MVCC는 동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법으로, 데이터베이스의 스냅샷을 여러 트랜잭션이 읽게 되고 이를 바탕으로 commit된 데이터들을 비교해 마지막 버전의 데이터를 만들게 된다.
이는 일반적인 RDBMS보다 빠르게 작동하지만, 사용하지 않는 데이터가 쌓이고 데이터 버전이 충돌할 때 어플리케이션에서 해결할 필요가 생긴다고 한다.
@Transactional을 쓸 때, readOnly라는 속성을 true/false로 설정해 줄 수 있다. (defalut : false)
만약 readOnly = true로 설정하게 되면, 스프링은 해당 트랜잭션의 FlushMode를 NEVER로 설정한다.
이 경우 flush가 일어나지 않으므로 비용이 절감되며, 또한 생성/수정/삭제가 일어나지 않으므로 별도의 스냅샷을 만들 필요가 없어 성능상 이점이 생긴다.
이 옵션을 설정한 경우, 해당 트랜잭션에서 예외 발생 시 롤백을 수행한다.
반대로 예외가 전혀 발생하지 않은 경우, 정상적으로 커밋한다.
이때 특정 예외를 지정해서 롤백하도록 할 수도 있으며, 반대로 특정 예외 발생시 롤백하지 않도록 지정할 수도 있다.
default는 rollbackFor = {RuntimeException.class, Error.class}이다.
지정한 시간 내에 해당 트랜잭션의 수행이 완료되지 않은 경우, 롤백을 수행하게 된다.
값은 정수형으로 설정하게 되는데, 만약 -1로 설정할 경우 timeout 설정을 해제한다.
default는 -1이다.