CS/Database

트랜잭션과 격리 수준

olsohee 2024. 1. 16. 17:47

트랜잭션

트랜잭션은 ACID라 해는 다음 4가지 성질을 만족시켜야 한다.

  • 원자성(Atomicity): 한 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
  • 일관성(Consistency): 트랜잭션 처리 후에도 데이터의 상태는 일관되어야 한다. 즉, 트랜잭션 처리 후에도 데이터베이스의 제약 조건이나 규칙을 만족해야 한다. 
  • 격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.
  • 지속성(Durability): 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 DB에 오류가 발생해도 로그 등을 사용해서 트랜잭션 내용을 복구해야 한다.

트랜잭션 격리 수준

트랜잭션 간에 격리성을 완벽하게 보장하기 위해서는 각 트랜잭션이 차례대로 하나씩 수행되어야 한다. 그러나 그렇게 하면 동시성 처리 성능이 매우 나빠진다. 따라서 완벽한 격리가 아닌 완화된 수준의 격리가 필요하고, 동시성 처리 속도와 데이터 정합성에 대한 트레이드 오프를 고려하여 다음 4가지의 트랜잭션 격리 수준이 제공된다.

  • READ UNCOMMITTED(커밋되지 않은 읽기)
  • READ COMMITTED(커밋된 읽기)
  • REPEATABLE READ(반복 가능한 읽기)
  • SERIALIZABLE

뒤로 갈수록 각 트랜잭션 간의 데이터 격리 정도가 높아지며, 동시 처리 성능도 떨어지는 것이 일반적이다.

 

DIRTY READ라고도 불리는 READ UNCOMMITTED는 일반적인 데이터베이스에서는 거의 사용하지 않고, SERIALIZABLE 또한 동시성이 중요한 데이터베이스에서는 거의 사용되지 않는다. 일반적인 온라인 서비스 용도의 데이터베이스는 READ COMMITTED와 REPEATABLE READ 중 하나를 사용한다. 오라클은 주로 READ COMMITTED를 많이 사용하며, MySQL에서는 REPEATABLE READ를 주로 사용한다. 

 

격리 수준이 높아질수록 MySQL 서버의 처리 성능이 많이 떨어질 것이라고 생각할 수 있지만 SERIALIZABLE 격리 수준이 아니라면 크게 성능 개선이나 저하가 발생하지 않는다.

READ UNCOMMITTED(커밋되지 않은 읽기)

  • 각 트랜잭션에서의 변경 내용이 커밋이나 롤백 여부에 상관없이 다른 트랜잭션에서 보인다. 따라서 위 그림에서 사용자 B는 사용자 A가 insert한 데이터를 커밋 전에 볼 수 있다. 
  • 문제점 - 더티 리드(Dirty read) 
    •  트랜잭션에서 처리한 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상이다.
    • 더티 리드를 유발하는 REDA UNCOMMITTED는 RDBMS 표준에서는 트랜잭션 격리 수준으로 인정되지 않을 정도로 정합성에 문제가 많은 격리 수준이다. 따라서 최소한 READ COMMITTED 이상의 격리 수준을 사용할 것이 권장된다.

 

READ COMMITTED(커밋된 읽기)

  • 커밋된 데이터만 다른 트랜잭션에서 조회할 수 있다. 따라서 더티 리드가 발생하지 않는다.
  • 오라클 DBMS에서 기본으로 사용하는 격리 수준이며, 온라인 서비스에서 가장 많이 사용되는 격리 수준이다.
  • 커밋되기 전의 데이터를 읽을 수 있는 이유는 언두 로그(Undo Log) 덕분이다. 즉, 한 트랜잭션이 커밋하기 전에 다른 트랜잭션에서 데이터를 읽으려고 하면, 언두 영역의 데이터가 반환되어 커밋되지 않은 데이터는 읽을 수 없다.
  • 문제점 - 반복 가능하지 않은 읽기(NON-REPEATABLE READ)
    • 하나의 트랜잭션에서 동일한 SELECT 쿼리를 실행했을 때 다른 결과가 나타나는 현상이다.

REPEATABLE READ(반복 가능한 읽기)

  • MySQL의 InnoDB 스토리지 엔진에서 기본으로 사용하는 격리 수준이다.
  • 반복 가능하지 않은 읽기 문제를 해결하는 격리 수준으로, 커밋된 데이터만 읽을 수 있으면서 자신보다 낮은 트랜잭션 번호를 갖는 트랜잭션에서 커밋한 데이터만 읽을 수 있는 격리수준이다.
  • 이를 가능하게 하는 것이 언두 로그이다.
    • 모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호를 가지며, 언두 영역에 백업된 레코드에는 변경을 발생시킨 트랜잭션 번호가 포함되어 있다. 
    • 언두 영역에 백업된 데이터는 InnoDB 스토리지 엔진에 의해 불필요하다고 판단되는 시점에 주기적으로 삭제된다. REPEATABLE READ에서는 실행 중인 트랜잭션 중 가장 오래된 트랜잭션 번호보다 트랜잭션 번호가 앞선 언두 영역의 데이터는 삭제할 수 없다. 
    • 이전에 살펴본 커밋된 읽기도 MVCC를 이용해 언두 영역을 이용해서 커밋되기 전의 데이터를 보여준다. 커밋된 읽기와 반복 가능한 읽기의 차이는 언두 영역에 백업된 레코드의 여러 버전 중 몇 번째 버전가지 찾아 들어가야 하느냐에 있다. 반복 가능한 읽기는 자신보다 낮은 트랜잭션 번호가 커밋한 데이터만 읽을 수 있다.
  • 문제점 - 유령 읽기(PHANTOM READ)
    • PHANTOM READ는 하나의 트랜잭션 내에서 여러 번 실행되는 동일한 SELECT 쿼리에 대해 결과 레코드 수가 달라지는 형상이다.
    • SELECT FOR UPDATE를 수행할 경우 발생할 수 있다. SELECT FOR UPDATE 쿼리는 레코드에 대해 쓰기 락을 거는데 언두 영역에는 락을 걸 수 없다(언두 영역은 변경 전 데이터를 저장하고 관리하는 용도일 뿐). 따라서 SELECT FOR UPDATE와 같이 락을 걸어야 하는 경우에는 언두 영역이 아닌 실제 테이블 값을 조회해온다.   

SERIALIZABLE

  • 가장 엄격한 격리 수준으로, 단순한 SELECT 쿼리를 실행할 때도 읽기 락(공유 락)을 획득한다. 따라서 한 트랜잭션에서 읽기만 하고 있는 레코드도 다른 트랜잭션에서 접근이 불가하다. 
  • 일반적인 DBMS에서 일어나는 PHANTOM READ 문제가 발생하지 않는다.
  • 문제점 - 동시성 처리 성능

JPA와 트랜잭션 격리 수준

JPA를 사용하면 1차 캐시를 통해 REPEATABLE READ 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공받을 수 있다.

 

1차 캐시는 영속성 컨텍스트 내에서 엔티티를 보관하는 장소이다. 트랜잭션을 시작하면 영속성 컨텍스트를 초기화하고, 같은 트랜잭션 내에서는 같은 영속성 컨텍스트를 사용하며, 데이터베이스에서 한 번 조회한 엔티티는 영속성 컨텍스트의 1차 캐시 안에 보관된다. 따라서 1차 캐시 덕분에 PHANTOM READ가 발생하지 않는다.

  1. 트랜잭션 A와 트랜잭션 B가 있고, DB에 회원의 나이가 10살로 저장되어 있다고 가정하자.
  2. 트랜잭션 A가 DB에서 회원을 조회해와서 회원의 나이를 10살에서 20살로 변경했다.
  3. 이때 트랜잭션 B도 DB에서 회원을 조회해오고 회원의 나이는 10살이다.
  4. 트랜잭션 A가 변경 내역을 커밋한다. 따라서 DB에 저장된 회원의 나이가 20살로 변경되었다.
  5. 트랜잭션 B는 이전에 DB에서 회원을 조회해왔으므로, 1차 캐시에 엔티티가 저장되어 있다. 따라서 트랜잭션 B가 회원을 다시 조회할 때 DB가 아닌 1차 캐시에서 엔티티를 조회해오고, 여전히 회원의 나이는 10살이다.
  6. 즉, 1차 캐시 덕분에 PHANTOM READ가 발생하지 않는다.

Reference

  • 자바 ORM 표준 JPA 프로그래밍, 김영한
  • 위키북스, Real MySQL 8.0