Java/Spring

[Spring] @Transactional isolation level, propagation level, timeout

SeungbeomKim 2024. 9. 23. 18:23

Transactional annotation

 

오늘은 @Transactional annotation의 isolation level, propagation level, timeout에 대해 설명드리도록 하겠습니다. 

 

먼저 @Transactional annotation이 무엇인지에 대해 알아본 후에, 핵심적인 내용들을 설명드리겠습니다. 

 

@Transacational

  • @Transactional은 DB 작업에서 Transaction을 관리하는 데 사용되는 annotation
  • Transaction: 단일 작업 단위로 실행되는 하나 이상의 데이터베이스 작업 시퀀스, 더 이상 쪼갤 수 없는 최소 작업 단위
  • Transcation은 commit으로 성공하거나 rollback으로 작업을 취소하고 이전 상태로 복원해야 합니다.
  • commit: Transaction이 끝나는 시점에 모든 작업을 확정짓습니다.
  • rollback: Transaction이 끝나는 시점에 모든 작업을 무효화합니다.

Transcational's  ACID

  • Atomicity (원자성)
    • 원자적이어서 분할할 수 없는 단일 작업 단위로 취급
    • Transaction의 모든 작업이 완료되거나 완료되지 않아야 됩니다.
    • Transaction의 한 시점에서 실패하면 rollback이 되어야 합니다.
  • Consistency (일관성)
    • Transaction 전후에 데이터베이스가 일관성을 유지해야 합니다.
    • Transaction이 실행되기 전과 후에 모두 유효한 상태여야 합니다.
    • PK, FK 제약조건을 위반할 경우, rollback 되어야 합니다.
  • Isolation (격리성)
    • Transaction은 서로 간섭할 수 없도록 격리되어야 합니다.
    • 다중 사용자 환경에서는 Transaction이 동시에 실행되므로 각 Transaction이 격리되어 다른 Transaction의 결과에 영향을 미치면 안 됩니다.
  • Durability (영속성)
    • Transaction이 commit 되면, DB에 영구적으로 저장되어야 합니다.

 

기본적인 Transaction의 개념에 대해 알아보았으니, Transaction의 시작, 종료 부분과 물리 트랜잭션, 논리 트랜잭션에 대해서도 짚고 넘어가도록 하겠습니다.

 

 

Transaction의 시작 부분

  • Transaction은 하나의 Connection을 가져와 사용하고 닫는 사이에서 발생합니다.
  • Transaction의 시작과 종료는 Connection 객체를 통해 발생합니다.
  • JDBC에서는 Autocommit option이 활성화되어 있습니다.

 

Transaction의 종료 부분

  • 하나의 Transaction이 시작되면, commit() 또는 rollback()이 호출될 때 까지가 하나의 Transaction으로 묶입니다.
  • Transaction의 경계설정: setAutocommit(false)로 Transaction의 시작을 선언하고, commit, rollback으로 트랜잭션을 종료하는 작업입니다.

 

Physical Transaction, Logical Transaction

  • Transaction은 DB에서 제공하는 기술이므로 Connection 객체를 통해 처리하기에 하나의 Transaction을 사용하는 것은 하나의 Connection 객체를 사용한다는 의미입니다.
  • Physical Transaction: 실제 DB에 적용되는 Transaction, Connection을 통해 commit/rollback 하는 단위
  • Logical Transaction: Spring의 TransactionManager를 통해 Transaction을 처리하는 단위
  • Principle
    • 모든 Logical Transaction이 commit 되어야만, Physical Transaction이 commit 됩니다.
    • 하나의 Logical Transaction이라도 rollback 되면, Physical Transaction이 rollback 됩니다.

 

이제 핵심 내용은 propagation, isolation level에 대해 알아보도록 하겠습니다. (앞서 설명드린 내용들은 숙지한 상태라고 가정합니다)

 

1. REQUIRED (default, @Transactional(propagation = Propagation.REQUIRED))

  • 스프링이 제공하는 default 전파 옵션 (별도의 설정이 없으면 REQUIRED 적용)
  • 2개의 논리 트랜잭션을 묶어 1개의 물리 트랜잭션을 사용
  • 내부 트랜잭션 (논리 트랜잭션)은 기존에 존재하는 외부 트랜잭션 (물리 트랜잭션)에 참여
  • 트랜잭션 매니저에 의해 관리되는 논리 트랜잭션이 존재하므로, 커밋은 내부에서 1회, 외부에서 1회 총 2회 실행됩니다.
  • 즉시 커밋되지 않으며, 외부 트랜잭션이 커밋될 때 실제로 커밋이 됩니다.

 

2. REQUIRED_NEW (@Transactional(propagation = Propagation.REQUIRES_NEW))

  • 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하는 전파 옵션
  • 2개의 물리 트랜잭션이 사용되며 각각 트랜잭션 별로 커밋과 롤백이 반복됩니다.
  • 2개는 서로 다른 외부 트랜잭션이므로, 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 미치지 않습니다
  • 1개의 HTTP 요청에 2개의 DB Connection이 사용되기에 (별도의 DB Connection이 사용되기 때문) 커넥션이 고갈될 수 있습니다. 이러한 이유로 조심해서 사용해야 합니다.

 

3. MANDATORY (@Transactional(propagation = Propagation.MANDATORY))

  • 트랜잭션이 의무입니다. (반드시 필요)
  • 기존에 트랜잭션이 없는 경우 IllegalTransactionStateException이 발생하고, 있는 경우에는 기존 트랜잭션에 참여합니다.

 

4. SUPPORTS (@Transactional(propagation = Propagation.SUPPORTS))

  • 트랜잭션이 선택입니다. (반드시 필요 X)
  • 기존에 트랜잭션이 없는 경우 그대로 진행하고, 있는 경우에는 트랜잭션에 참여합니다.

 

5. NOT_SUPPORTED (@Transactional(propagation = Propagation.NOT_SUPPORTED))

  • 트랜잭션을 지원하지 않습니다.
  • 기존에 트랜잭션이 없는 경우 그대로 진행하고, 있는 경우에는 트랜잭션을 보류하고 트랜잭션 없이 진행합니다.

 

6. NEVER (@Transactional(propagation = Propagation.NEVER))

  • 트랜잭션을 허용하지 않습니다.
  • 기존에 트랜잭션이 없는 경우 그대로 진행하고, 있는 경우에는 IllegalTransactionStateException이 발생합니다.

 

7. NESTED (@Transactional(propagation = Propagation.NESTED))

  • 중첩 (자식) 트랜잭션을 생성합니다
  • 기존에 트랜잭션이 없는 경우 새로운 트랜잭션을 생성하고, 있는 경우에는 중첩 (자식) 트랜잭션을 생성합니다

 

이제 일관성 있는 데이터를 보장하기 위해 설정하는 Isolation Level에 대해 설명드리겠습니다.

Transaction Isolation level

DEFAULT

  • 사용하는 데이터 액세스 기술 또는 DB Driver의 default 설정을 따릅니다.
  • 대부분의 DB는 READ_COMMITTED 격리 수준을 가집니다.

 

1. @Transactional(isolation = Isolation.READ_UNCOMMITTED)

  • 가장 낮은 격리 수준
  • Dirty Read 현상 발생
    • Dirty Read: 커밋되지 않은 변경 내용을 읽게 되는 현상

 

2. @Transactional(isolation = Isolation.READ_COMMITTED)

  • Spring은 기본 속성이 DEFAULT이며, DB는 일반적으로 READ_COMMITED가 기본 속성이므로 가장 많이 사용됩니다.
  • READ_UNCOMMITED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수가 없습니다.
  • Non-Repeatable Read 현상 발생
    • Non-Repeatable Read: 트랜잭션이 동일한 행을 업데이트하고 커밋한 후, 행을 다시 가져올 때 다른 값을 읽게 되는 현상

 

3. @Transactional(isolation = Isolation.REPEATABLE_READ)

  • 하나의 트랜잭션이 읽은 행을 다른 트랜잭션이 수정할 수 없도록 막아주지만, 새로운 행을 추가하는 것은 막지 못합니다.
  • 추가된 행을 발견할 수 있습니다.
  • Dirty Read, Non-Repeatable Read 현상 방지
  • Phantom Read 현상 발생
    • Phantom Read: 다른 트랜잭션이 범위의 일부 행을 추가 또는 제거하고 커밋하는 경우, 다른 행을 가져오는 현상

 

4. @Transactional(isolation = Isolation.SERIALIZABLE)

  • 가장 강력한 트랜잭션 격리 수준으로 트랜잭션을 순차적으로 실행시켜 줍니다.
  • 여러 트랜잭션이 동시에 같은 테이블의 정보를 Access 할 수 없습니다.
  • 성능이 매우 떨어지기에, 사용 X

 

Transaction Timeout

  • 모든 트랜잭션에 대해 @Transactional 어노테이션을 사용하여 트랜잭션 timeout을 제공할 수 있습니다.
  • @Transactional(timeout = 10)을 정의하면 트랜잭션이 주어진 시간 내에 완료되어야 되며, 시간 내에 완료가 안되면 rollback 후에 TransactionTimedOutException 예외 가 발생합니다
  • default: -1 (시간제한 없음, 지정된 시간 제한 없이 무한정 실행될 수 있습니다)
    • Timeout 지정: 트랜잭션이 설정된 시간 내에 완료되지 않으면 자동으로 롤백되기에, 리소스가 장기간 점유되는 것을 방지할 수 있지만, 너무 짧게 설정하면 정상적인 작업도 중단될 수 있습니다.
    • Timeout 미지정: 트랜잭션이 완료될 때까지 무한정 기다리므로, 리소스가 장시간 점유될 수 있습니다. 긴 작업에서 오랜 시간이 소요될 수 있습니다.