본문 바로가기
프로그래밍/Spring Boot

@Transactional 어노테이션의 다양한 옵션 활용

by 카프카뮈 2021. 6. 17.

본 글은 아래의 글에서 이어지는 글이다.

https://kafcamus.tistory.com/30

 

@Transactional 어노테이션의 이해

나는 보통 서비스 코드에 @Transactional 어노테이션을 활용해준다. 그런데 사실 뜻도 잘 모르고 좋다고 그래서 쓴거라...지나고 보니 정확히 설명하기가 어려웠다. 그런고로, 해당 어노테이션의 작

kafcamus.tistory.com

앞서 @Transactional 어노테이션을 사용할 경우 어떻게 트랜잭션이 구현되는지를 간단히 다뤘다.

그런데, 막상 @Transactional 어노테이션을 써보려고 하니 Isolation이나 propagation과 같은 parameter에 대해 잘 몰라서 난감한 마음이다.

그런 이유로, 이번 글은 @Transactional 어노테이션 사용 시 인자를 통해 어떻게 속성을 조절할 수 있는지에 대해 다룬다.


Transaction Propagation

Propagation은 우리의 비즈니스 로직에서의 트랜잭션 범위를 정의한다. 

@Transactional을 쓸 때, 스프링은 트랜잭션의 시작과 중지를 propagation 세팅을 바탕으로 관리한다.

스프링이 트랜잭션을 만들 때 TransactionManager::getTransaction을 호출하는데, 이때 propagation 세팅이 영향을 준다고 한다.

이제 종류별로 알아보자.

REQUIRED

REQUIRED는 별도 옵션을 설정하지 않을 경우 default로 설정되는 속성이다.

만약 현재 활성화된 트랜잭션이 존재하면, 해당 트랜잭션에 비즈니스 로직을 append한다.

그러나 활성화된 트랜잭션이 없다면, 새로운 트랜잭션을 만들어준다.

//REQUIRED pseudo-code
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return createNewTransaction();

SUPPORTS

SUPPORTS는 활성화된 트랜잭션이 존재하는지 확인하고, 존재하면 해당 트랜잭션을 사용한다.

그러나 활성화된 트랜잭션이 없다면, 트랜잭션 없이 로직을 수행한다.

//SUPPORTS pseudo-code
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return emptyTransaction;

MANDATORY

MANDATORY는 활성화된 트랜잭션이 존재하는지 확인하고, 존재하면 해당 트랜잭션을 사용한다.

그러나 활성화된 트랜잭션이 없다면, 예외를 발생시킨다.

//MANDATORY pseudo-code
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
throw IllegalTransactionStateException;

NEVER

NEVER로 옵션을 설정할 경우, 활성화된 트랜잭션이 존재하면 예외를 발생시킨다.

활성화된 트랜잭션이 없다면 트랜잭션을 사용하지 않는다.

//NEVER pseudo-code
if (isExistingTransaction()) {
    throw IllegalTransactionStateException;
}
return emptyTransaction;

NOT_SUPPORTED

NOT_SUPPORTED는 활성화된 트랜잭션이 존재할 경우, 이를 보류시킨다.

그리고 비즈니스 로직을 트랜잭션 없이 실행하도록 한다.

REQUIRES_NEW

REQUIRES_NEW를 사용할 경우, 항상 새로운 트랜잭션을 시작한다.

그러나 이미 진행중인 트랜잭션이 존재하면, 해당 트랜잭션을 잠시 보류시킨다.

트랜잭션이 존재할 때 보류시킨다는 점은 NOT_SUPPORTED와 유사하다.

//REQUIRES_NEW pseudo-code
if (isExistingTransaction()) {
    suspend(existing);
    try {
        return createNewTransaction();
    } catch (exception) {
        resumeAfterBeginException();
        throw exception;
    }
}
return createNewTransaction();

NESTED

NESTED는 활성화된 트랜잭션이 존재할 경우, 해당 트랜잭션의 세이브 포인트를 기록한다.

이후 해당 트랜잭션 내에 중첩 트랜잭션을 만들어 작업을 하게 되는데, 만약 그 과정에서 비즈니스 로직에 예외가 발생하면 세이브 포인트로 롤백한다.

즉, 하위 트랜잭션은 상위 트랜잭션에 영향을 받지만, 상위 트랜잭션은 하위 트랜잭션에 영향을 덜 받도록 구축할 수 있다.

만약 활성화된 트랜잭션이 없다면, REQUIRED처럼 작동한다.


Transaction Isolation

isolation(고립, 격리)은 앞서의 포스트에서 다뤘던 ACID의 요소 중 하나이다.

isolation은 동시에 여러 트랜잭션에 의한 변경 사항이 어떻게 적용되는지를 설정한다.

각각의 격리 수준은 아래의 동시성 부작용을 방지한다.

 

  • Dirty read: 변경사항이 반영되지 않은 값을 다른 트랜잭션에서 읽는 경우
  • Nonrepeatable read: 한 트랜잭션 내에서 값을 조회할 때, 동시성 문제로 인하여 같은 쿼리가 다른 결과를 반환하는 경우. 즉 트랜잭션이 끝나기 전에 수정사항이 반영되어, 트랜잭션 내에서 쿼리 결과가 일관성을 가지지 못하는 경우
  • phantom read: 외부에서 수행되는 삽입/삭제 작업으로 인해 트랜잭션 내에서의 동일한 쿼리가 다른 값을 반환하는 경우

이제 각각의 옵션을 종류별로 알아보자.

DEFAULT: 별도 옵션이 없는 경우

만약 isolation level을 설정하지 않았다면, DEFAULT로 설정된다. 이 경우, 트랜잭션의 isolation level은 RDBMS가 지정한 값으로 설정된다. 그렇기 때문에, DB를 바꿀 경우 주의해야 할 필요가 있다.

 

READ_UNCOMMITTED

READ_UNCOMMITTED는 가장 낮은 isolation level로, 커밋되지 않는 데이터(트랜잭션 처리중인 데이터)에 대한 읽기를 허용한다. 이는 가장 많은 동시 엑세스를 허용한다.

이 경우, 위에서 언급된 세 가지 동시성의 부작용이 모두 발생하게 된다.

그렇기 때문에 Postgres는 READ_UNCOMMITTED를 허용하지 않고 READ_COMMITTED를 사용하도록 하며, Oracle은 READ_UNCOMMITTED를 지원하거나 허용하지 않고 있다.

READ_COMMITTED

READ_COMMITTED는 트랜잭션에서 커밋된 데이터만 읽을 수 있기 때문에, 위의 세 가지 동시성 부작용 중 Dirty read를 방지한다. 풀어서 말하자면, 어떤 데이터가 변경되는 중일 경우(변경사항이 반영되지 않은 경우) 해당 데이터는 접근할 수 없게 된다.

그러나 나머지 동시성 부작용을 해결하지 못하기 때문에, 아직 문제의 여지가 남아있다.

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

REPEATABLE_READ

REPEATABLE_READ는 트랜잭션이 완료될 때까지 SELECT문이 사용하는 모든 데이터에 shared lock을 걸게 된다.

그렇기 때문에 select 쿼리를 여러 번 같은 트랜잭션 내에서 사용하더라도 같은 값을 조회하게 되고, 해당 값들에 대한 변경이나 삭제가 불가해진다.

그렇기 때문에 동시성 부작용 중 Dirty read와 Nonrepeatable read를 방지한다.

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

SERIALIZABLE

SERIALIZABLE은 최고 수준의 isolation level로, 세 가지 동시성 부작용을 모두 방지한다.

그러나 동시 호출을 순차적으로 실행하기 때문에, 동시 엑세스 속도가 가장 낮을 수 있다.

즉, SERIALIZABLE를 사용하는 트랜잭션 그룹의 동시 실행은 순차적으로 실행하는 것과 동일한 결과를 가진다.

그러한 원리에서, SERIALIZABLE은 MVCC(다중 버전 동시성 제어, Multi-Version Concurrency Control)을 지원하지 않는다.

MVCC

MVCC는 동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법으로, 데이터베이스의 스냅샷을 여러 트랜잭션이 읽게 되고 이를 바탕으로 commit된 데이터들을 비교해 마지막 버전의 데이터를 만들게 된다.

이는 일반적인 RDBMS보다 빠르게 작동하지만, 사용하지 않는 데이터가 쌓이고 데이터 버전이 충돌할 때 어플리케이션에서 해결할 필요가 생긴다고 한다.


readOnly

@Transactional을 쓸 때, readOnly라는 속성을 true/false로 설정해 줄 수 있다. (defalut : false)

만약 readOnly = true로 설정하게 되면, 스프링은 해당 트랜잭션의 FlushMode를 NEVER로 설정한다.

이 경우 flush가 일어나지 않으므로 비용이 절감되며, 또한 생성/수정/삭제가 일어나지 않으므로 별도의 스냅샷을 만들 필요가 없어 성능상 이점이 생긴다.


ETC

rollbackFor

이 옵션을 설정한 경우, 해당 트랜잭션에서 예외 발생 시 롤백을 수행한다.

반대로 예외가 전혀 발생하지 않은 경우, 정상적으로 커밋한다.

이때 특정 예외를 지정해서 롤백하도록 할 수도 있으며, 반대로 특정 예외 발생시 롤백하지 않도록 지정할 수도 있다.

default는 rollbackFor = {RuntimeException.class, Error.class}이다.

timeout

지정한 시간 내에 해당 트랜잭션의 수행이 완료되지 않은 경우, 롤백을 수행하게 된다.

값은 정수형으로 설정하게 되는데, 만약 -1로 설정할 경우 timeout 설정을 해제한다.

default는 -1이다. 


참고한 페이지는 다음과 같다.

Baeldung: transactional propagation and isolation: 설명이 친절하고 디테일이 깨알같아 참고하기 좋았다.

[Spring] Transactional 정리 및 예제: 평소에도 많이 참고하는 블로그인데, 설명을 너무 친절하게 잘해주셔서 막히는 부분에서 큰 도움을 받았다.

joinc의 MVCC: MVCC가 무엇인지에 대한 설명이 있다. 읽어보시길 추천.

우아한형제들 기술 블로그 "응? 이게 왜 롤백되는거지?" : 위의 propagation과 rollback 문제의 예시. 읽어보면 넘 재밌다.

반응형

댓글