# 트랜잭션

# 트랜잭션이란?

트랜잭션은 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우 원래 상태로 복구하여 작업의 일부만 적용되는 현상(Partial update)을 막아준다.

또한 트랜잭션은 하나의 논리적인 작업 셋의 쿼리 개수와 관계없이 논리적인 작업 셋 자체가 전부 적용(COMMIT)되거나 아무것도 적용되지 않는 것(ROLLBACK)을 보장해주는 것이다. 결국 트랜잭션은 여러 개의 변경 작업을 수행하는 쿼리가 조합 됐을 때만 의미 있는 개념은 아니다.

트랜잭션은 시작이 존재하는 절차(script)이다. 트랜잭션은 이러한 절차의 시작과 끝을 단위화하는 것이다. 단위화된 트랜잭션은 반드시 원자성이 보장되어야 한다.

TPS Transactions per second
트랜잭션은 논리적인 작업 셋 단위화한 것이다. 초당 트랜잭션의 실행 수를 의미하는 TPS는 성능을 측정하는 지표로 사용된다.

# 트랜잭션의 특성 ACID

# Atomicity (원자성)

  • 트랜잭션은 DB에 모두 반영되거나, 전혀 반영되지 않아야 한다.
  • 완료되지 않은 트랜잭션의 중간 상태를 데이터베이스에 반영 해서는 안 된다.

# Consistency (일관성)

  • 트랜잭션 작업 처리 결과는 항상 일관성을 유지해야 한다.
  • 데이터베이스는 항상 일관된 상태를 유지해야 한다. 일관적인 데이터베이스 상태는 다양한 제약 조건을 만족해야 한다는 것이다.

# Isolation (격리성)

  • 둘 이상의 트랜잭션이 동시 실행되고 있을 때, 어떤 트랜잭션도 다른 트랜잭션 연산에 끼어들 수 없다.
  • 각각의 트랜잭션은 서로 간섭 없이 독립적으로 이루어져야 한다.

# Durability (지속성)

  • 트랜잭션이 성공적으로 완료 되었으면 결과는 영구적으로 반영되어야 한다.
  • 이것을 위해 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 한다.

보통 데이터가 영속 되었다는 것은 HDD나 SSD와 같이 디스크에 저장되는 것을 의미한다. 특정 트랜잭션에서 기존에 존재하는 데이터를 수정할 경우 바로 디스크에 접근하여 영속된 데이터를 수정하는 것이 아니라 COMMIT이 일어나는 시점에 비로소 메인 메모리에 존재하는 수정된 데이터가 디스크에 반영된다.

Transaction 1이 영속된 데이터를 수정한다고 가정한다. 아직 COMMIT되기 이전 이기 때문에 디스크에는 반영되어 있지 않은 상태이다. 그 때 또 다른 Transaction 2가 해당 데이터를 읽기 위해 시도하고 있다. 트랜잭션 2는 현재 영속되어 있는 데이터를 읽기 때문에 트랜잭션 1에서 수정된 변경 사항을 확인하지 못한 채 이전 데이터를 읽을 것이다. 결국 Transaction 1COMMIT되면 Transaction 2는 잘못된 데이터를 조회한 것과 다름없다.

과연 이것은 온전히 트랜잭션의 특성을 보장한다고 말할 수 있을까? 하나의 데이터를 두 트랜잭션이 접근하려 시도할 경우 각각의 트랜잭션을 순차적으로 실행하지 않는 한 두 트랜잭션은 완전히 격리 되었다고 판단할 수 없을 것이다.

하지만 모든 트랜잭션을 순차적으로 처리할 경우 동시 처리 성능이 매우 나빠질 것이다. 이러한 ACID는 트랜잭션이 이론적으로 보장해야 하는 성질과 같다. 실제로는 트랜잭션 격리 수준에 따라 성능을 위해 ACID 특성을 보장하는 것을 완화하여 적용할 수 있다.

# 트랜잭션 격리 수준

트랜잭션 격리 수준은 트랜잭션이 동시에 데이터베이스에 접근할 때 그 접근을 어떻게 제어 할지에 대한 설정을 다룬다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

데이터의 정합성은 성능은 반비례한다. 아래로 갈수록 격리 수준과 데이터 정합성은 높아 지지만 성능은 떨어진다.

# READ UNCOMMITTED

커밋 전의 트랜잭션의 데이터 변경 내용을 다른 트랜잭션이 읽는 것을 허용한다.

Transaction 1에서 INSERT로 추가된 Member가 COMMIT되기 이전에 Transaction 2에서 조회를 진행하고 있다.

하지만 Transaction 1에서 모종의 이유로 ROLLBACK된다고 가정한다. Transcation 2ROLLBACK 여부를 확인하지 못하고 정상적인 Member라 생각하고 계속 진행될 것이다.

이처럼 트랜잭션에서 처리한 작업이 완료 되지 않아도 볼 수 있는 현상을 Dirty Read라고 한다. 이러한 Dirty Read가 허용되는 격리 수준은 READ UNCOMMITTED이다.

# READ COMMITTED

COMMIT이 완료된 트랜잭션의 변경사항만 다른 트랜잭션에서 조회가 가능하다. 대부분의 RDB에서 기본적으로 사용하고 있는 격리 수준이며, Dirty Read가 발생하지 않는다. 데이터의 변경이 일어나면 변경 전 데이터는 언두 영역으로 복사된다. 다른 트랜잭션에서 접근하면 변경된 테이블 데이터를 조회하는 것이 아니라 언두 영역에 복사된 레코드를 조회한다.

하지만 READ COMMITTEDNON REPEATABLE READ가 발생하게 된다.

Transaction 2에서 Transaction 1 COMMIT 이전 데이터를 조회하면 1건이 조회된다. 하지만 Transaction 2에서 Transaction 1이 정상적으로 COMMIT 된 이후 똑같은 SELECT 쿼리를 사용하여 다시 조회할 경우 0건이 조회된다. 이것은 하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행 했을 때 항상 같은 결과를 가져와야 하는 REPEATABLE READ의 정합성에 어긋난다.

# REPEATABLE READ

트랜잭션 범위 내에서 조회한 내용이 항상 동일함을 보장한다. 이 격리 수준에서는 READ COMMITTED에서 발생한 NON REPEATABLE READ 부정합이 발생하지 않는다.

InnoDB를 사용하는 MySQL의 경우 트랜잭션에 순차적인 고유한 번호를 가진다. 언두 영역에 백업된 레코드에는 변경을 발생 시킨 트랜잭션의 번호를 포함하고 있다.

InnoDB를 사용하는 MySQL에서 REPEATABLE READ를 보장하기 위해 자신의 트랜잭션 번호보다 작은 트랜잭션 번호에서 변경한 것만 읽도록 한다.

위 그림을 살펴보면 Transaction 1의 트랜잭션 번호는 6번이다. Transaction 2의 번호는 5번이다. Transaction 1은 특정 데이터를 수정한 뒤 COMMIT을 진행 했지만, 트랜잭션 번호가 더 낮은 Transaction 2는 자신의 트랜잭션 번호보다 작은 언두 영역에 백업된 레코드를 조회한다.

READ COMMITTED와 유사해보이지만 가장 큰 차이점은 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가는지 이다.

언두 영역의 백업된 데이터는 여러 개일 수 있다. InnoDB가 불필요하다 판단하면 주기적으로 삭제한다. 언두 영역에 데이터가 늘어나면 MySQL 처리 성능이 떨어질 수 있기 때문이다.

SQL-92 혹은 SQL-99 표준에 따르면 REPEATABLE READ에서도 문제는 발생한다. 바로 PHANTOM READ이다. PHANTOM READ란 다른 트랜잭션에서 수행한 변경 작업으로 인하여 데이터가 보였다가 안보였다가 하는 현상을 의미한다.

하지만 InnoDB를 사용하는 MySQL은 넥스트 키 락 덕분에 단순한 SELECT에서는 PHANTOM READ가 발생하지 않는다고 한다. (자세한 내용은 추후 공부할 예정이다.)

다만 SELECT … FOR UPDATE, SELECT … FOR SHARE와 같이 조회 시 잠금을 설정할 경우 언두 영역의 레코드는 잠금을 걸 수 없기 때문에 PHANTOM READ가 발생할 수 있다.

앞서 언급한 것 처럼 언두 영역의 레코드에 잠금을 걸 수 없기 때문에 변경 전 데이터를 가져오는 것이 아닌 현재 레코드의 값을 가져오기 때문에 이전에 4건의 레코드를 조회하는 것과 달리 총 5건의 레코드가 조회된다.

# SERIALIZABLE

가장 단순하고 엄격한 격리 수준이다. 단순한 SELECT 문도 트랜잭션이 끝날 때 까지 잠금이 설정되어 다른 트랜잭션에서 절대 접근할 수 없게 된다.

SERIALIZABLEPHANTOM READ를 문제가 발생하지 않는다. 다만 InnoDB를 사용하는 MySQL의 경우 이미 REPEATABLE-READ에서 PHANTOM READ가 발생하지 않도록 보장하기 때문에 SERIALIZABLE 격리 수준은 더욱 불필요해 보인다.

# 격리 수준 비교

# 회고

지금까지 트랜잭션에 대한 개념 및 격리 수준에 대해 알아보았다. 트랜잭션에 대해 막연한 개념만 가지고 있던 중, 각각의 격리 수준에 따른 접근 제어에 대한 궁금증으로 정리하게 되었다. 공부를 하며 가장 인상 깊었던 부분은 MySQL의 InnoDB 스토리지 엔진의 독특한 특성으로 상대적으로 낮은 격리 수준에서 PHANTOM READ를 예방할 수 있다는 것이다.

아직 내부 동작 구조 및 아키텍처 등 부족한 지식이 많아 잘못된 개념이 있을 수 있다. 이번에 공부하며 추가적으로 얻은 키워드를 기반으로 차근차근 정리할 예정이다.

# References.

백은빈, 이성욱, 『Real MySQL 8.0』, 위키북스(2021), p176 ~ 183.
🙈[DB이론] 트랜잭션(transaction)과 ACID 특성을 보장하는 방법🐵 (opens new window)
트랜잭션의 격리 수준(isolation Level)이란? (opens new window)
MySQL 트랜잭션 Isolation Level로 인한 장애 사전 예방법 (opens new window)
DB 트랜잭션에 대해서 말해보세요. (신입 면접용) (opens new window)
InnoDB Next-Key Locking (phantom row 방지 기법) (opens new window)
Non-Repeatable Read VS Phantom Read (opens new window)

#우아한테크코스 #트랜잭션 #transaction #ACID
last updated: 10/8/2022, 11:45:47 AM