Dev

[Spring Boot] Transaction Propagation and Isolation 에 대한 정리

Ryan Woo 2019. 12. 24. 20:46

본 포스팅은 아래 사이트를 해석 및 정리한 글입니다.

https://www.baeldung.com/spring-transactional-propagation-isolation

 

Transaction Propagation and Isolation in Spring @Transactional | Baeldung

Learn about the isolation and propagation settings in Spring's @Transactional

www.baeldung.com

 

1. @Transactional 어노테이션이란?

@Transactional을 사용하여 트랜잭션에 대한 전파, 격리, 시간 초과, 읽기 전용 및 롤백 조건을 설정할 수 있다. 

Spring은 프록시 생성 또는 클래스 바이트 코드를 조작하여 트랜잭션 생성, 커밋 및 롤백을 관리한다. 프록시의 경우 Spring은 내부 메소드 호출에서 @Transactional을 무시한다. 간단히 말해서 callMethod() 메소드가 있고 @Transactional로 표시하면 Spring은 메소드 호출을 중심으로 트랜잭션 관리 코드를 랩핑한다.

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

@Transactioanl 어노테이션은 인터페이스, 클래스, 메소드에 선언할 수 있다. 여기에 우선순위가 있는데 낮음 -> 높음 순으로 정리하면 인터페이스, 슈퍼클래스, 클래스, 인터페이스 메소드, 슈퍼클래스 메소드, 클래스 메소드 순이다. 

또한 클래스 레벨에서 @Transactional 어노테이션을 사용하면 메소드에 일일이 @Transactional을 선언하지 않아도 해당 클래스의 모든 public 메소드에 @Transactional을 적용한다. 그러나 private나 protected 메소드에 @Transactional을 사용하면 Spring은 오류없이 어노테이션을 무시한다.

일반적으로 @Repository with Spring Data를 제외하고 @Transactional 어노테이션을 인터페이스에 사용하는것은 추천하지 않는다.

2. Transaction Propergation

Propergation은 비즈니스 로직의 트랜잭션 바운더리를 정의한다. Spring은 Propergation 설정에 따라 트랜잭션을 시작하고 일시 중지를 관리한다. Spring은 TransactionManager :: getTransaction을 호출하여 전파에 따라 트랜잭션을 얻거나 만든다. 

2.1. REQUIRED

Required는 default propergation이다. Active Transaction이 있으면 참여하고, 없으면 새로 만든다. pseudo-code는 다음과 같다.

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

 

2.2. SUPPORTED

Supported는 Transaction이 있으면 참여하고, 없으면 non-transactional로 실행된다. pseudo-code는 다음과 같다.

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

 

2.3. MANDATORY

Mandatory는 Transaction이 있으면 참여하고, 없으면 Exception을 발생시킨다. pseudo-code는 다음과 같다.

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

 

2.4. NEVER

Never는 Transaction이 있으면 Exception을 발생시킨다. pseudo-code는 다음과 같다.

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

 

2.5. NOT_SUPPORTED

Not_Supported는 Transaction이 있으면 일시중지한 뒤, 비즈니스 로직을 Transaction 없이 실행한다. JTATransactionManager가 일시중지를 지원한다.

 

2.6. REQUIRES_NEW

Requires_New는 Transaction이 있으면 일시중지한 뒤, 새 Transaction을 생성한다. JTATransactionManager가 일시중지를 지원한다. pseudo-code는 다음과 같다.

if (isExistingTransaction()) {
    suspend(existing);
    try {
        return createNewTransaction();
    } catch (exception) {
        resumeAfterBeginException();
        throw exception;
    }
}
return createNewTransaction();

 

2.7. NESTED

Spring은 Transaction이 존재하는지 확인한 다음 존재하면 저장 점을 표시한다. 즉, 비즈니스 로직 실행에서 Exception이 발생하면 Transaction이 저장 점으로 롤백된다. Transaction이 없으면 REQUIRED처럼 작동한다.

DataSourceTransactionManager는 Nested를 기본적으로 지원한다. 또한 JTATransactionManager의 일부 구현에서 지원할 수 있다.

JpaTransactionManager는 JDBC 연결에 대해서만 NESTED를 지원한다. 그러나 nestedTransactionAllowed 플래그를 true로 설정하면 JDBC 드라이버가 savepoints를 지원하는 경우 JPA Transaction의 JDBC 액세스 코드에도 작동한다.

Nested는 Transaction이 존재하면 중첩 Transaction을 만드는 것이다. 중첩 Transaction은 먼저 시작된 부모의 commit과 rollback에는 영향을 받지만, 자신의 commit이나 rollback이 부모의 Transaction에는 영향을 주지 않는 특징이 있다.

 

3. Transaction Isolation

격리는 일반적인 ACID 속성 중 하나이다(Atomicity, Consistency, Isolation, and Durability : 원자성, 일관성, 격리성, 내구성). Isolation은 동시 트랜잭션에 의해 적용된 변경 사항이 서로 어떻게 표시되는지를 설명한다.

각 격리 수준은 Transaction에서 0 개 이상의 동시성 부작용을 방지한다.

Dirty read : commit되지 않은 동시 Transaction 변경을 읽는다.
Nonrepeatable read : 동시 Transaction이 동일한 행을 update하고 commit하는 경우, 행을 다시 읽을 때 다른 값을 얻는다.
Phantom read : 다른 Transaction이 범위의 일부 행을 추가 또는 제거하고 commit하는 경우 범위 쿼리를 다시 실행 하면 다른 행을 가져온다.

@Transactional :: isolation을 통해 트랜잭션의 격리 수준을 설정할 수 있다. Spring에는 DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE의 5 가지가 있다.

 

3.1. Spring의 Isolation Management

기본 격리 수준은 DEFAULT이다. 따라서 Spring이 새로운 Transaction을 생성 할 때 격리 수준은 RDBMS의 Default를 따른다. 따라서 데이터베이스를 변경할 때는주의해야한다.

또한 격리 수준이 다른 일련의 메소드를 호출 할 때도 고려해야한다. 정상적인 흐름에서는 격리가 새 트랜잭션이 생성 될 때만 적용된다. 따라서 어떤 이유로 든 격리 수준이 다른 메소드를 실행하지 못하게하려면 TransactionManager :: setValidateExistingTransaction을 true로 설정해야한다. pseudo-code는 다음과 같다.

if (isolationLevel != ISOLATION_DEFAULT) {
    if (currentTransactionIsolationLevel() != isolationLevel) {
        throw IllegalTransactionStateException
    }
}

 

3.2. READ_UNCOMMITTED

READ_UNCOMMITTED는 가장 낮은 격리 수준이며 대부분의 동시 액세스를 허용한다. Dirty read, Repeatable read, Phantom read가 모두 발생할 수 있다. 따라서 행을 다시 읽거나 범위 쿼리를 다시 실행할 때 다른 결과를 얻을 수 있다.

Postgres는 READ_UNCOMMITTED 격리를 지원하지 않으며 대신 READ_COMMITED로 폴백한다. 또한 Oracle은 READ_UNCOMMITTED를 지원하지 않는다.

 

3.3 READ_COMMITED

Dirty read를 방지한다. Repeatable read, Phantom read는 여전히 발생한다. 동시 Transaction에서 commit되지 않은 변경은 영향을 주지 않지만, commit이 되면 다시 읽었을 때 결과값이 변경될 수 있다.

READ_COMMITTED는 Postgres, SQL Server 및 Oracle의 기본 수준이다. 

 

3.4. REPEATABLE_READ

REPEATABLE_READ는 Dirty read, Repeatable read, 방지한다. 따라서 동시 Transaction의 commit되지 않은 변경에 영향을받지 않는다. 또한 행을 다시 쿼리 할 때 다른 결과를 얻지 못한다. 그러나 범위 쿼리를 다시 실행하면 새로 추가되거나 제거 된 행이 생길 수 있다.

또한 업데이트 손실을 방지하는 데 필요한 가장 낮은 수준이다. 손실 된 업데이트는 둘 이상의 동시 트랜잭션이 동일한 행을 읽고 업데이트 할 때 발생한다. REPEATABLE_READ는 행에 대한 동시 액세스를 전혀 허용하지 않는다. 따라서 손실 된 업데이트가 발생할 수 없다.

REPEATABLE_READ는 Mysql의 기본 수준이다. Oracle은 REPEATABLE_READ를 지원하지 않는다.

 

3.5. ERIALIZABLE

SERIALIZABLE은 최고 수준의 격리이다. 언급 된 모든 동시성 부작용을 방지하지만 동시 호출을 순차적으로 실행하기 때문에 가장 낮은 동시 액세스 속도로 이어질 수 있다.

다시 말해, 일련 화 가능한 Transaction 그룹의 동시 실행은 Transaction을 직렬로 실행하는 것과 동일한 결과를 갖는다.