트랜잭션의 격리수준

고라니·2023년 2월 19일
0
post-thumbnail

트랜잭션은 논리적인 일의 단위로서 데이터의 정합성을 보장하기 위해 사용됩니다.

트랜잭션은 흔히 ACID라고 하는 4가지 특성을 가지고 있습니다.

  1. Atomicity(원자성): 트랜잭션 내 모든 작업은 모두 성공(COMMIT)하거나 모두 실패(ABORT)해야 합니다.

  2. Consistency(일관성): 트랜잭션 전후로 데이터베이스의 상태를 일관성있게 유지되어야 합니다. 예를들면, 무결성 제약 조건을 위배할 수 없습니다. 다른 말로 Correctness라고도 합니다.

  3. Isolation(격리성): 하나의 트랜잭션안에서 다른 트랜잭션으로부터 영향을 받을 수 없습니다. 이후에 살펴볼 격리수준과 관련이 있습니다.

  4. Durability(지속성): 완료된 트랜잭션의 효과는 영속적이며 모두에게 드러나야합니다.

완료된 트랜잭션의 상태를 COMMIT 또는 ROLLBACK으로 가짐으로써 데이터의 정합성을 보장하게 됩니다.

이렇게 보면 트랜잭션은 완벽한 개념처럼 보이지만, 격리성으로 인한 현실적인 문제가 존재합니다.

트랜잭션은 각각 격리되어야하기 때문에, 같은 데이터를 접근하는 두개의 트랜잭션은 순차적으로 처리되어야합니다.

하지만 순차적으로 처리한다면, 동시성이 떨어지게 되어 성능 저하를 야기합니다.

그래서 트랜잭션의 격리수준을 나누어서 동시성과 정합성을 조율할 수 있도록 하는 것입니다.

4가지 격리수준

ANSI/ISO 표준에서는 트랜잭션의 격리수준을 4가지의 레벨로 나누어서 정의하고 있습니다.

  1. Read Uncommitted: 커밋되지 않은 변경 내용까지 읽기
  2. Read Committed: 커밋된 내용들만 읽기 (ORACLE 기본 설정)
  3. Repeatable Read: 반복가능한 읽기, 즉 조회했던 레코드들의 정합성을 보장함 (MySQL InnoDB, JPA* 기본 설정)
  4. Serializable: 최고의 격리수준으로, 2개의 트랜잭션이 동시에 실행되어도 순차적으로 실행된 것과 같은 결과를 보장

*JPA의 1차 캐시로 DB가 아닌 애플리케이션 레벨에서 Repeatable Read 수준의 격리성을 제공합니다.

MySQL 8.0 공식문서에 따르면 Repeatable Read, Read Committed, Read Uncommitted, Serializable 순으로 많이 사용된다고 합니다.

Serializable이 가장 이상적이지만, 현실적으로 DB 운영에 사용되기엔 성능 이슈가 존재합니다.

그래서 보통 Read Committed, Repeatable Read 수준을 유지하게 되는데, 격리수준을 낮춤으로써 문제가 발생할 수 있습니다.

  1. Dirty Read: 커밋되지 않은 변경내용을 다른 트랜잭션에서 읽는 현상
  2. Non-Repeatable Read: SELECT로 조회했던 레코드가 수정되거나 삭제되어, 다시 쿼리를 실행했을 때 동일한 결과가 나오지 않는 현상
  3. Phantom Read: 동일한 2개의 SELECT 쿼리 사이에 레코드가 생성되어서 2번째 쿼리를 실행했을 때 새로운 레코드가 조회되는 현상

일반적으로 각 격리수준마다 나타날 수 있는 현상은 다음과 같습니다.

Isolation LevelDirty ReadNon-Repeatable ReadPhantom Read
Read UncommittedOOO
Read CommittedXOO
Repeatable ReadXXO*
SerializableXXX

*예외적으로 MySQL InnoDB의 Repeatable Read에서는 MVCC 방식으로 인하여 Phantom Read가 발생하지 않습니다.

눈으로 확인해보자

각 격리수준에서 발생할 수 있는 문제 상황들을 실제로 확인해보겠습니다.

사용할 DB는 MySQL 8.0이며 Storage Engine은 InnoDB 입니다.

테스트에 앞서, 아주 간단한 테이블을 생성하겠습니다.

CREATE TABLE ACCOUNT(
    id INT primary key,
    money INT
)

그리고 샘플 데이터 2개를 만들어보겠습니다.

INSERT INTO ACCOUNT(id, money) VALUES(1, 1000);

INSERT INTO ACCOUNT(id, money) VALUES(2, 5000);

SELECT * FROM ACCOUNT;
+----+-------+
| id | money |
+----+-------+
|  1 |  1000 |
|  2 |  5000 |
+----+-------+

그리고 2개의 트랜잭션이 동시에 접근해야 의미가 있기 때문에 2개의 세션을 생성해주겠습니다.

Read Uncommitted에서 Dirty Read

TX 1TX 2

TX 1에서 변경한 값이 TX 2에서도 조회되는 것을 확인할 수 있습니다.
이 때, TX2에서 커밋되지 않은 값으로 로직이 수행되고 TX1이 롤백된다면 큰 문제가 생길 수 있습니다.

Read Committed에서 Non-Repeatable Read

TX 1TX 2

READ COMMITTED 수준에서는 커밋된 변경 내용을 읽을 수 있기 때문에 트랜잭션 중간에 다른 트랜잭션의 커밋이 발생되면 같은 쿼리라도 결과가 달라질 수 있습니다.

Repeatable Read에서 Phantom Read

MySQL InnoDB 엔진에서는 Repeatable Read에서 Phantom Read가 발생하지 않습니다.

Tx 1Tx 2

그러면 테이블의 엔진을 MyISAM으로 변경 후, 결과를 살펴보겠습니다.

ALTER TABLE ACCOUNT ENGINE = MyISAM;
Tx 1Tx 2

새로운 레코드 삽입으로 인해서 Phantom Read가 발생하고 쿼리의 결과가 달라지는 것을 확인할 수 있습니다.

정리하며

트랜잭션의 격리수준과 각 격리수준에서 발생할 수 있는 문제점들을 살펴보았습니다.

특히, MySQL의 InnoDB의 MVCC방식으로 인해서 예외적인 경우가 존재합니다.

따라서, 실제 사용하는 DBMS의 공식 문서를 보는 것을 추천드립니다.

0개의 댓글