개발놀이터

@Transactional (Propagation과 Isolation의 관점에서) 본문

JPA/JPA

@Transactional (Propagation과 Isolation의 관점에서)

마늘냄새폴폴 2023. 3. 2. 14:59

우리는 보통 @Transactional을 많이 사용합니다. 트랜잭션을 wrapping 해주는 역할로서 많이 사용하죠. 

 

이번 포스팅에선 @Transactional이 무엇인지, @Transactional에서 주로 사용하는 옵션인 Propagation, Isolation에 대해서 알아보도록 하겠습니다. 

 

 

@Transactional

우선 @Transactional에 대해서 알아봐야겠죠? 

 

@Transactional은 스프링이 만든 AOP중 하나입니다. @Transactional이 붙어있는 메서드에 한해서 트랜잭션의 원자성을 보장해주고 트랜잭션 시작, 커밋, 롤백을 수행해줍니다. 

 

@Transactional의 등장으로 트랜잭션 코드와 비즈니스 로직을 분리하여 개발자들은 좀 더 순수한 비즈니스 로직을 구현할 수 있게 되었는데요. 

 

@Transactional을 사용하는 방법

우리는 이 어노테이션을 인터페이스, 클래스, 메서드에 붙일 수 있습니다. 하지만 공식문서에서는 인터페이스에 붙이는 것을 추천하지는 않습니다. 이 인터페이스가 Spring Data JPA에 의해 만들어진 인터페이스가 아니라면 붙이는 것을 추천하고 있지 않죠. 

 

이 어노테이션을 클래스 레벨에 붙이게 되면 해당 클래스의 public 메서드에 전부 붙게됩니다. private, protected는 어떻게 될까요? 그냥 에러 없이 무시됩니다. 

 

@Service
@Transactional
public class TransferServiceImpl implements TransferService {
    @Override
    public void transfer(String user1, String user2, double val) {
        // ...
    }
}

이런식으로 클래스 레벨에 붙여도 되고?

 

@Transactional
public void transfer(String user1, String user2, double val) {
    // ...
}

메서드 레벨에 붙여도 됩니다. 

 

 

Propagation

Propagation(이하 전파단계)는 우리의 비즈니스 로직의 경계를 정의합니다. 스프링은 전파 단계의 정도에 따라 트랜잭션을 시작할수도, 중지할수도 있습니다. 

 

스프링은 TransactionManager에 getTransaction을 통해 트랜잭션을 얻거나 만듭니다. 이렇게 만들어진 트랜잭션은 TransactionManager의 모든 종류의 전파단계를 지원합니다. 

 

이제부터 JPA의 전파단계에 대해서 하나씩 알아보도록 하겠습니다. 

 

REQUIRED

REQUIRED는 디폴트 전파단계입니다. 스프링은 현재 실행중인 트랜잭션이 있는지 체크하고 만약 존재하지 않는다면 새로운 것을 만듭니다. 비즈니스 로직은 현재 실행중인 트랜잭션에 붙어서 실행됩니다. 

 

SUPPORTS

스프링은 먼저 현재 실행중인 트랜잭션이 존재하는지 체크합니다. 만약 트랜잭션이 존재하면 존재하는 트랜잭션이 사용될 것입니다. 만약 트랜잭션이 존재하지 않았다면 트랜잭션이 없는 상태로 실행됩니다. 

NOT_SUPPORTED

만약 현재 트랜잭션이 있으면 스프링은 그 트랜잭션을 중지하고 비즈니스 로직이 트랜잭션 없이 실행됩니다. 

 

REQUIRES_NEW

REQUIRES_NEW는 현재의 트랜잭션이 존재하면 중지합니다. 그리고 새로운 것을 만듭니다. NOT_SUPPORTED와 비슷한데, 이때 트랜잭션을 중지하기 위해서는 JTATransactionManager가 필요합니다. 

 

NESTED

스프링은 트랜잭션이 존재하는지 체크하고 세이브 포인트를 만듭니다. 세이브 포인트를 만든다는 것의 의미는 만약 우리의 비즈니스 로직의 실행이 예외를 만나게 되면 트랜잭션은 이 세이브 포인트로 롤백합니다.

 

만약 실행중인 트랜잭션이 없다면 REQUIRED 처럼 동작합니다. 

 

DataSourceTransactionManager는 이 NESTED 전파단계를 별도의 설치 없이 실행할 수 있도록 지원해줍니다. 또한 JTATransactionManager의 몇몇 구현체들도 이 NESTED단계를 지원합니다. 

 

JpaTransactionManager는 오직 JDBC 커넥션을 위해 NESTED를 지원합니다. 그러나 만약 우리가 nestedTransactionAllowed의 속성을 true로 준다면 JDBC 드라이버가 저장지점을 원하는 경우, JPA 트랜잭션의 JDBC 접근 코드에 대해서도 작동합니다. 

 

MANDATORY

만약 실행중인 트랜잭션이 있다면 그 트랜잭션이 사용될 것입니다. 만약 현재 실행중인 트랜잭션이 없다면 스프링은 예외를 던집니다. 

 

 

여기까지 전파단계에 대해서 알아봤습니다. 이제 다음은 트랜잭션 격리수준과 JPA에서는 어떻게 트랜잭션 격리수준을 지원하는지 알아보죠

 

 

Transaction Isolation

격리수준은 흔히 ACID의 속성을 따릅니다. 격리수준은 동시에 실행되는 트랜잭션에 대해 어떻게 변하는지 시각적으로 보여줍니다. 

 

데이터베이스 격리수준은 아래의 링크에 자세한 내용이 나와있으므로 중요한 부분만 짚고 넘어가도록 하겠습니다.

https://coding-review.tistory.com/196

 

데이터베이스 격리수준 (isolation level)

우선 데이터베이스 격리수준을 들어가기 전에 우리는 트랜잭션에 대해서 간단한 이해가 필요합니다. 트랜잭션 트랜잭션은 데이터의 정합성을 보장하기 위한 기능입니다. 트랜잭션은 꼭 여러개

coding-review.tistory.com

 

각각의 격리수준에서 생기는 문제에 대해서만 빠르게 짚고 넘어가죠

 

  • Dirty Read : Read UnCommitted 격리수준에서 생기는 부정합 문제로 커밋을 하지 않아도 상대방 트랜잭션의 값을 볼 수 있는 경우였죠. 심한 경우 롤백을 하더라도 롤백하기 전 데이터로 값을 변경할 수 있었던 가장 낮은 수준의 격리수준입니다. 
  • Non-Repeatable Read : Read Committed 격리수준에서 생기는 부정합 문제로 같은 쿼리에서 같은 결괏값이 나와야 한다는 Repeatable Read 정합성 문제에 어긋나는 부정합 문제였습니다. 
  • Phantom Read : Repeatable Read 격리수준에서 생기는 부정합 문제로 Non-Repeatable Read와 비슷하지만 Phantom Read는 같은 쿼리를 두번 날리는게 아닌 재실행했을 때 레코드가 보였다 안보였다 하는 문제였습니다. 

 

스프링에서는 이 격리수준을 관리할 수 있는 속성들을 제공합니다. 하지만 디폴트값은 각각의 데이터베이스가 기본적으로 세팅해 둔 격리수준을 따릅니다. 

 

또한 JPA에서는 지원하는 격리수준이지만 몇몇의 격리수준은 데이터베이스에서 지원하지 않는 격리수준도 있기 때문에 데이터베이스를 바꾸게 된다면 꼭 조심해야합니다. 

 

READ_UNCOMMITTED 

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void log(String message) {
    // ...
}

위의 방법처럼 사용하면 되지만 READ_UNCOMMITTED는 그냥 사용하지 않는것이 좋습니다. 기본적으로 생기는 부정합 문제인 Dirty Read도 그렇지만 Non-Repeatable Read나 Phantom Read도 발생할 수 있기 때문에 그냥 사용하지 않는 것이 좋습니다. 

 

뭐 그런 자질구레한 이유 말고도 PostgreSQL이나 Oracle 같은 경우에는 READ_UNCOMMITTED 격리수준은 아예 지원을 안합니다. 그래서 READ_UNCOMMITTED로 설정하면 자동으로 디폴트 값인 READ_COMMITTED로 바뀌게 됩니다. 

 

 

READ_COMMITTED

@Transactional(isolation = Isolation.READ_COMMITTED)
public void log(String message){
    // ...
}

READ_COMMITTED는 앞서 발생했던 Dirty Read 부정합 문제를 해결하는 두번째 격리수준입니다. 하지만 READ_COMMITTED는 같은 쿼리에서 같은 결괏값이 나와야 한다는 Repeatable Read 정합성에 어긋납니다. 

 

READ_COMMITTED는 Oracle과 PostgreSQL, SQL Server에서 디폴트 격리수준으로 사용하고 있습니다. 

 

 

REPEATABLE_READ

 

@Transactional(isolation = Isolation.REPEATABLE_READ) 
public void log(String message){
    // ...
}

세번째 격리수준인 REPEATABLE_READ입니다. REPEATABLE_READ는 앞서 생긴 부정합 문제인 Dirty Read와 Non-Repeatable Read가 생기지 않습니다. 따라서 Uncommitted에 의한 동시다발적인 트랜잭션의 변화에대해 완전히 면역을 가지고 있습니다. 

 

같은 쿼리에서 같은 결괏값을 보여주긴 하지만 재실행을 했을 때는 레코드가 보였다 안보였다 하는 Phantom Read가 발생합니다. 

 

REPEATABLE_READ는 Mysql에서 디폴트 격리수준으로 사용하고 있습니다. 또한 Oracle에서는 이 격리수준을 지원하지 않습니다. 

 

 

SERIALIZABLE 

@Transactional(isolation = Isolation.SERIALIZABLE)
public void log(String message){
    // ...
}

마지막 격리수준인 SERIALIZABLE입니다. 가장 강력한 격리수준 중 하나로 동시에 일어날 수 있는 모든 부정합 문제가 발생하지 않습니다. 

 

하지만 변경 작업은 그렇다쳐도 읽기 작업까지 lock을 획득해야 하기 때문에 성능상 굉장히 불리함을 가지고 있습니다. SERIALIZABLE 격리수준 이외의 모든 격리수준은 성능이 다 비슷비슷한데 해당 격리수준만 처참한 성능을 가지고 있죠 때문에 대부분의 데이터베이스가 READ_UNCOMMITTED와 마찬가지로 사용하고 있지 않습니다. 

 

 

여기까지 JPA의 전파단계가 무엇인지, 전파단계에는 어떤 것이 있는지, 격리수준은 어떤것을 지원하는지에 대해서 알아봤습니다. 그런데 이 글을 다 읽고 궁금증이 하나 들 수 있습니다. 

 

Q. 그럼 Oracle, PostgreSQL, SQL Server는 Non-Repeatable Read 문제를 안고 가는건가요? Mysql은 Phantom Read 부정합 문제를 안고 가는건가요? 

 

A. 그렇지 않습니다. 다 해결방법이 존재합니다.

 

보통의 경우 Non-Repeatable Read같은 부정합 문제는 MVCC를 통해 해결을 하고, Phantom Read 부정합 문제는 gap-locking이라는 방법을 통해 해결합니다. 

 

이 두가지에 대해서는 추후에 데이터베이스 카테고리에서 자세히 다룰 예정이니 그때 다시 언급하도록 하겠습니다. 

 

전파단계는 참 어렵네요 이해하는데 참 오래 걸렸던 것 같습니다. 전파단계를 실무에서는 어느레벨까지 사용할지 감이 잘 안잡히네요 REQUIRED나 REQUIRES_NEW정도는 많이 사용할 것 같은데... 다른 것들은 잘 사용할지 모르겠습니다. 

 

아무래도 전파단계를 사용하는 목적이 서로 다른 두 트랜잭션을 병합하거나 이어붙이기 위해서 사용하는 느낌이 강하게 듭니다. 때문에 우리가 보통 토이프로젝트 레벨에서는 싱글트랜잭션을 사용하게 되니 디폴트 값만 사용하면 될 것 같습니다. 

 

하지만 실무에서는 여러대의 데이터베이스를 묶어서 하나의 트랜잭션으로 처리하고 싶은 상황이 생길 수도 있으니 이럴 때 글로벌 트랜잭션과 함께 묶어서 전파단계를 설정하면 그럴듯하게 사용할 수 있을 것 같습니다. 

 

여기까지 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

 

 

출처

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

https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth

 

Spring Transaction Management: @Transactional In-Depth

You can use this guide to get a simple and practical understanding of how Spring's transaction management with the @Transactional annotation works.

www.marcobehler.com