Transaction Propagation and Isolation in Spring @Transactional

강민욱·2023년 2월 22일
0

springboot

목록 보기
2/2

1. 목표

@Transactional 어노테이션의 isolation과 propagation 세팅을 알아보는게 목적

2. What Is @Transactional?

(1) @Transactional Implementation Details
Spring은 생성, 커밋, 롤백을 관리하기 위해 proxy를 생성하거나 byte-code클래스를 조작한다. proxy의 경우에는 내부 메소드에서의 호출을 무시한다.

createTransactionIfNecessary();
	try {
    	callMethod();
        commitTransactionAfterReturning();
   	} catch (exception) {
        completeTransactionAfterThrowing();
        throw exception;
   	}

Simply put, if we have a method like callMethod and we mark it as @Transactional, Spring will wrap some transaction management code around the invocation@Transactional method called

위와 같이 callMethod를 @Transactional로 설정했을 경우 callMethod가 위의 코드처럼 createTransactionIfNecessary()로 감싸지기 때문에 내부에서 callMethod()를 호출할 경우 createTransactionIfNecessary()을 호출할 수 없기 때문에 commitTransactionAfterReturning()같은 다른 메소드들이 호출되지 않기 떄문에 Transactional이 적용되지 않는다 정도로 이해

(2) How to Use @Transactional
위치는 interface, class, 메소드위 어디에서든 선언할 수 있다. interspace, superclass, class, interface 메소드, superclass 메소드, class 메소드 순으로 재정의 된다. 클래스에서 @Transactional을 선언했을 경우, 해당 클래스의 public 메소드에서는 따로 선언하지 않아도 적용된다. 그러나 private, protected 메소드에서는 적용되지 않고 스프링은 에러없이 해당 메소드들을 무시한다.
보통 interface에서의 @Transactional은 추천되지 않지만, @Repository같이 spring data를 다루는 경우에는 가능하다. class에 @Transactional을 선언하고 interface, superclass의 트랜잭션을 재정의 할 수도 있다.

@Transactional
public interface TransferService {
   void transfer(String user1, String user2, double val);
}
@Service
@Transactional
public class TransferServiceImpl implements TransferService {
   @Override
   @Transactional
   public void transfer(String user1, String user2, double val) {
      // ...
   }
}
위의 코드를 보면 interface TransferService 에 @Transactional을 선언하였기 때문에 transfer()에 자동으로 전파됐다, 하지만 TransferServiceImpl 클래스에서 transfer()를 재정의 하면서 @Transactional선언해서 transaction도 재정의 될 수 있다, 다음 transaction Propagation, transaction Isolation을 재정의 할 수 있다는걸 얘기하기 위해 든 예시 같다.

3. What Is transaction Propagation

Propagation은 비즈니스 로직에서 트랜잭션 경계를 정의한다. 스프링은 Propagation설정을 통해 트랜잭션의 시작과 끝을 관리한다. 스프링은 propagation(전파레벨)에 따라 트랜잭션을 가져오거나 생성하기 위해 TransactionManager의 getTransaction()을 호출한다. 모든 종류의 TransactionManager의 몇몇 propagation(전파레벨) 지원한다. 그중 몇몇은 특정 TransactionManager를 구현했을때만 지원된다.

(1) REQUIRED Propagation
REQUIRED는 default Propagation이다, sping은 active 상태의 transaction이 있는지 확인하고 없으면 새로 생성하고 있다면 비즈니스 로직을 active 상태의 transaction에 추가한다.

@Transactional
public void requiredExample(String user) {
      // ... 
}

default이므로 아무것도 적지 않으면 자동으로 선언된다.

if (isExistingTransaction()) {
      if (isValidateExistingTransaction()) {
          validateExisitingAndThrowExceptionIfNotValid();
      }
      return existing;
}
return createNewTransaction();

일단 Transaction이 있는지 체크, 없으면 새로 만들어서 return, 유효성 체크 하고 유효하지 않다면 exception 발생

(2) SUPPORTS Propagation
spring은 처음 transaction의 존재여부를 체크하고 있으면 기존의 transaction이 사용되지만 없으면 non-transaction 상태로 실행된다.

if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return emptyTransaction;

기존의 transaction이 있는지 확인하고 있다면 유효성을 체크하고 있는걸 사용 없으면 transaction을 비우고 수행

(3) MANDATORY Propagation
spring transaction의 존재여부를 체크하고 있으면 사용하지만 없으면 exception을 발생시킨다.

if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
throw IllegalTransactionStateException;

(4) NEVER Propagation
기존의 존재하는 transaction이 있으면 exception을 발생시킨다.

if (isExistingTransaction()) {
    throw IllegalTransactionStateException;
}
return emptyTransaction;

(5) NOT_SUPPORTED Propagation
기존의 존재하는 transaction이 있으면 중지시키고(suspend) transaction없이 비즈니스 로직을 수행한다.

The JTATransactionManager supports real transaction suspension out-of-the-box. Others simulate the suspension by holding a reference to the existing one and then clearing it from the thread context

JTATransactionManager는 실제 transaction을 중지시키지만 다른 경우네는 thread context에서 삭제해서 트랜잭션을 중지 효과를 준다.

(6) REQUIRES_NEW Propagation
spring 현재 transaction이 존재한다면 이를 중지시키고 새로운 transaction을 생성한다. 이렇게만 보면 (5)의 NOT_SUPPORTED Propagation와 마찬가지로 실제 transaction을 중지하고 싶다면 JTATransactionManager가 필요하다.

(7) NESTED Propagation
spring은 존재유무를 체크, 존재한다면 save point를 표시해두고 만약 비즈니스 로직에서 에러가 발생된다면 transaction을 save point로 롤백한다. 만약 transaction이 존재하지 않는다면 REQUIRED와 같다. DataSourceTransactionManager가 이를 지원하는데 별도의 설치없이 바로 사용가능하다(out-of-the-box) JTATransactionManager 몇몇 구현체 또한 지원한다. JpaTransactionManager는 JDBC connection 관련부분만 NESTED를 지원한다. 그러나 nestedTransactionAllowed를 true로 설정하고 JDBC 드라이버가 save point를 지원한다면 JPA transaction의 JDBC access 코드에서도 작동한다.

4. Transaction Isolation

Isolation은 일반적인 ACID(Atomicity, Consistency, Isolation and Durablity) 프로퍼티중 하나이다. Isolation은 동시 트랜잭션(concurrent transaction)에 의해 변경된 사항이 서로에게 어떻게 보여지는 지를 설명한다.
각각의 Isolation 레벨은 transaction에서의 0개 이상의 동시성 부작용을 막는다. 부작용은 다음과 같다.

  • Dirty read : commit 되지 않은 다른 트랜잭션의 정보도 읽을 수 있다,
  • Nonrepeatable read : 한 트랜잭션에서 같은 쿼리를 두번 실했을 때 다른값이 read된다
  • Phantom read : 다른 transaction의 추가, 삭제로 인해 다수결과(range) 쿼리의 결과값이 다르다
    우리는 sping에서 @Transactional(isolation=xxx)으로 설정할 수 있다. DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE 5가지가 있다.

(1) Isolation Management in Spring
DEFAULT 기본 Isolation이다. 결과적으로 spring이 새로운 transaction을 생성할 때, 새로운 transaction의 level은 DEFAULT란 얘기다, 그러므로 database를 변경할 경우 주의하여야 한다.
또 하나의 주의사항은 다른 Isolation에서 서로 연관된 method를 호출할 경우이다. 보통의 처리에서는 Isolation은 transaction생성할 때만 적용된다, 따라서 다른 Isolation에서 method가 실해되길 원치 않는 경우 TransactionManager의 setValidateExistingTransaction true로 설정한다.

(2) READ_UNCOMMITTED Isolation
READ_UNCOMMITTED는 가장 낮은 level의 Isolation이어서 대부분의 다른 transaction의 접근을 허용한다. 따라서 앞에서 얘기한 3가지 부작용이 모두 발생할 수 있다. 이 Isolation의 transaction은 다른 transaction들의 commit 되지 않는 data를 읽는다, 따라서 다시 읽거나 query를 실행할 때 다른 결과값을 얻을 수 있다.
Postgres는 READ_UNCOMMITTED isolation을 지원하지 않고 READ_COMMITED를 대신 사용한다. Oracle역시 마찬가지다.

(3) READ_COMMITTED Isolation
두번째 level의 Isolation이다. Dirty read는 방지할 수 있지만 나머지 부작용은 발생할 수 있다, commit되지 않은 변화는 우리에게 영향을 줄 수 없지만 commit이 된 경우에는 결과값이 달라질 수 있다.
Postgres, SQL, Oracle에서는 READ_COMMITTED가 디폴트이다.

(4) REPEATABLE_READ Isolation
세번째 level의 Isolation이다. Dirty read, Nonrepeatable read를 방지할 수 있다. 그래서 commit되지 않은 변경은 영향을 받지 않는다,

Also, when we re-query for a row, we don't get a different result. However, in the re-execution of range-queries, we may get newly added or removed rows.

단일행의 쿼리는 같은 값을 얻지만, 다행쿼리의 경우 재실행 하면 새로 변화된 결과값도 포함된다.

lost update를 방지하기 위한 가장 낮은 level이다. update 손실이란 하나 이상의 transaction이 같은 row를 수정하거나 읽을 때 발생하는 에러를 말한다, REPEATABLE_READ 동시접속을 허용하지 않는다, 따라서 lost update는 발생하지 않는다.
REPEATABLE_READ는 mysql에서 기본 레벨이다, oracle은 지원하지 않는다.

(5) SERIALIZABLE Isolation
SERIALIZABLE 가장 높은 level의 Isolation이다. 모든 언급된 부작용을 막는다. 동시 호출을 순차적으로 수행하기 떄문에 동시접속률(concurrent access rate)은 낮을 수 있다. 따라서 모든 transaction의 결과값은 같다.

reference
https://www.baeldung.com/spring-transactional-propagation-isolatione/

profile
백엔드 개발자

0개의 댓글