쿼카러버의 기술 블로그

[데이터 베이스] 트랜잭션 시리즈 #4 : Isolation Level deep dive (이상현상들) 본문

[Database]

[데이터 베이스] 트랜잭션 시리즈 #4 : Isolation Level deep dive (이상현상들)

quokkalover 2023. 1. 1. 18:34

데이터베이스를 사용하고 있는 많은 개발자들은 DBMS의 기본 isolation level을 사용하는 경우가 많다. 하지만 지금 내 서비스에 적합한 isolation level을 지정해주지 않으면 여러 요청이 동시에 처리될때 예상치 못한 동작이 발생할 수 있다. 따라서 본 글에서는 Isolation level에 대해 알아볼 것이다. 그치만 그전에, 동시에 트랜잭션이 처리될 때 어떤 이상 현상(Concurrency Problem)들이 발생할 수 있는지에 대해 알아보려고 한다. SQL표준에서 제시한 이상현상 외에도 다른 이상현상들도 알아볼 것이다. 다양한 이상현상들을 공부해두면 트랜잭션을 사용할 때 본인 서비스에 치명적일 수 있는 이상현상(Concurrency Problem)들을 미연에 방지할 수 있고, 문제가 발생했을 때 원인 파악에 도움이 될 수 있기 때문이다. 따라서 표준 + 추가 이상현상들에 대해 알아보고, 그 뒤에 Isolation level을 소개하면서, 이들이 어떤 이상현상들을 방지할 수 있는지에 대해 알아보도록 하겠다.

 

Concurrency Problems (이상 현상)

Dirty Read

Dirty Read란 다른 트랜잭션에서 변경했지만 아직 commit되지 않은 데이터를 읽는 경우를 말한다. 이게 문제가 되는 이유는 데이터를 변경하고 있는 트랜잭션이 실패될 수도 있기 때문이다. 따라서 아직 커밋되지 않은 데이터를 읽고 처리했는데, 트랜잭션이 롤백되게 되면 유효하지 않은 데이터를 처리한게 된다. 한번 예를 들어보자.

 

위 예를 보면 T2는 T1이 업데이트한 X값을 읽고 처리를 했지만 T1은 롤백을 한 경우, T2는 유효하지 않은 데이터로 처리후 커밋을 하기 때문에 데이터베이스에 유효하지 않은 데이터가 저장된다.

 

Non repeatable read

non repeatable readfuzzy read라고도 표현하는데, 이름에서도 힌트가 있지만 한 트랜잭션 내에서 같은 값을 다시 읽었을 때 그 값이 다른 경우를 의미한다. non repeatable read는 커밋된 데이터를 읽는 경우에도 발생할 수 있는데, 이게 무슨말인지 모르겠다면 아래 예를 보자.

 

위에서 보면 알 수 있듯, T1이 먼저 시작한 트랜잭션임에도 불구하고, T2가 커밋이 되고나자 T1에서 해당 값을 다시 읽었을 때 그 값이 바뀌었다.

트랜잭션의 ACID의 특성중 하나인 Isolation은 여러 트랜잭션이 동시에 실행된다고 하더라도, 혼자서 실행되는 것처럼 동작해야 한다. 하지만 위 경우는 커밋된 데이터를 읽었음에도 불구하고 T1에 T2가 영향을 주었기 때문에, 여전히 Isolation을 위배한다.

이게 어떤 경우에 문제가 되는지를 한번 예로 들어보겠다.

 

아래와 같이 판매 기록을 관리하는 Sales 테이블이 있다고 해보자.

 

1) SELECT ID, quantity * price From Sales

먼저 상품별 총 판매 금액을 계산했다.

 

2) UPDATE Sales SET quanity = quantity + 5 Where ID = 1

1번 상품이 5개가 더 팔려서 5를 더해줬다

 

3) SELECT SUM(quanity * price) From Sales

전체 상품 판매금액을 조회한다

 

위 케이스에서의 문제는 1) 읽었을 때의 값으로 전체 상품 판매 금액을 조회했을 때랑 3)에서 읽었을때의 전체 상품 판매금액이 다를 것이라는 점이다. Dirty Read와는 다르게 커밋된 데이터를 읽었음에도 불구하고 이렇게 예상치 못한 현상이 발생할 수 있는 것이다.

 

Phantom read

phantom은 유령이라는 뜻으로, 이름에서도 힌트가 있지만 한 트랜잭션 내에서 없던 데이터가 생기거나 있던 데이터가 사라지는 경우를 의미한다. 다시 말해 트랜잭션 시작 후 특정 조건의 데이터를 읽었을 때 없었던 데이터가 다시 같은 조건으로 데이터를 읽어들였더니 존재하는 현상을 의미한다.

 

예를 들어보면 특정 날짜 사이의 이벤트를 조회하는 쿼리를 실행해 불러왔을 때 없었던 이벤트가 다시 조회하니까 생기는 케이스를 예로 들 수 있다.

 

트랜잭션의 실행 순서로 예를 들어보겠다.

위 예를 보면 T1이 처음에 x=10인 튜플들을 조회했을 때 B가 있었다. 하지만 그 뒤에 T2가 x=10을 가진 A를 입력한 후 커밋을 했다. 그 뒤에 T1에서 다시 x=10인 튜플들을 조회했을 때 원래 B밖에 없었는데, A, B가 조회된다. 이 또한 Isolation에 위배된다. 하나의 트랜잭션 내에서 같은 조건의 데이터를 두번 읽었을 뿐인데 그 결과가 다르기 때문이다.

 

위 케이스를 보면 커밋된 데이터를 읽었기 때문에 Dirty Read는 아니다. Non Repeatable Read와는 다르게 읽었던 데이터가 변경된건 아니고 없던 데이터가 생긴 것이기 때문에 Non Repeatable Read도 아니다. 즉 위와 같이 기존에 없던 데이터가 갑자기 생기는 현상을 Phantom read라고 표현한다.

 

이상 현상 ‘중간’ 정리

앞서 소개한 세가지 이상 현상인 Dirty Read, Non-repeatable Read, Phantom read는 SQL 표준에서 제시된 이상현상들이다. 그리고 이들은 Isolation Level을 조정함을 통해서 발생하지 않게 만들 수 있다. Isolation level이 높아질수록, 즉 격리 수준이 높아질수록 위 테이블에서 보이는 것처럼 이상 현상을 더 많이 제어할 수 있다. 물론 더 엄격해질수록 동시에 처리 가능한 트랜잭션 수가 줄어들기 때문에 DB전체 처리량은 나빠진다. 따라서 내 서비스의 특성에 따라 일부 이상 현상들은 허용해 성능을 높일 수 있도록 Isolation level이라는 격리 수준을 둔 것이다.

 

현재 대부분의 DBMS에서 정의되는 Isolation level은 위 이상 현상을 얼마나 제어하냐에 따라 결정된다. SQL표준 문서에서는 위 이상현상들만 소개하지만 사실 위 세 가지 이상현상 외에도 더 많은 이상 현상이 존재한다. 물론 Isolation level을 조정함에 따라서 방지할 수 있긴한데, 그래도 어떤 이상현상이 있을 수 있는지에 대해서는 알아둘 필요가 있다. 따라서 Isolation level에 대해 더 알아보기전에 어떤 이상 현상이 더 있을 수 있는지 추가로 더 알아보자

 

Dirty Write

Dirty Write는 트랜잭션 T1이 특정 item을 수정하고 있는데, 그 뒤에 실행된 트랜잭션 T2가 T1이 커밋되거나 롤백되기 전에 item을 수정하게될 때 발생한다. 만약 T1이나 T2가 롤백되게 되면 어떤 값이 정확한 값인지 파악할 수 없다.

 

예를 들어 x = 0 이었을때 T1은 x를 10으로 쓰고, T2는 10으로 바뀐 x를 읽고 x를 100으로 썼다고 해보자. 그리고 T1과 T2를 롤백하면 0으로 돌아가야되는데, T2가 뒤에 롤백되게되면 10으로 돌아가게 되는 경우가 발생할 수 있다. 즉 트랜잭션의 ACID한 특성을 지키기 위해서는 recoverability또한 보장해야 하기 때문에 dirty write를 허용해선 안된다.

 

Lost Update

Lost Update은 이미 먼저 실행된 트랜잭션 T1이 delete, insert, update 을 통해 값을 수정하고 있는데, 그 뒤에 실행된 트랜잭션 T2이 값을 수정하고 커밋하게되어 T1이 커밋되게 되면 T2의 값이 사라지게 되는 경우를 의미한다. 아래 예시와 같은 케이스가 발생할 수 있는 것이다.

 

Read Skew

Read Skew는 다른 시점의 데이터베이스의 상태를 읽을때 주로 발생된다.

 

예를 들어보면 아래와 같다.

x=20와 y=80라는 데이터가 있을때

T1이 x를 읽었고(x= 20), T2는 x(x=x-20)와 y(y=y+20)의 값을 수정했다. 그 뒤에 T1이 y(y=100)를 읽었는데 T1이 x를 읽었을때 당시의 y(이 때는 80)값과, y를 읽을때의 y값은 서로 다른 시점에 읽다보니 값이 다른 것이다.

 

다시 말해 x에서 20을 빼 y에 더해주는 연산을 했으면 초기의 x+y값(100)과 그 뒤의 x+y값(120)이 같아야 하는데 위 경우엔 다르다. 이렇게 커밋된 데이터를 읽었음에도 불구하고 데이터를 읽는 시점이 달라 inconsistent한 데이터를 읽는 경우를 Read Skew라고 한다.

Write Skew

예를 들어 병원에서 당직 근무가 최소 1명 이상은 있어야 하고, 1명 이상이 있을 경우에 당직 근무에 빠질 수 있도록 데이터베이스에 제약사항을 만들었다고 해보자. 그리고 현재 당직 근무자가 2명이다. 그럼 지금 1명이 당직에서 빠질 수 있다. 근데 두개의 트랜잭션이 동시에 시작됐다.

T1은 Alice의사를 당직근무에서 빼려고 하고

T2는 Bob의사를 당직근무에서 빼려고 한다.

 

위와 같은 케이스는 아래와 같이 진행된다.

이 둘이 동시에 실행됐을때, 당직근무자가 2명이었기 때문에, 2명이라고 인지한 상태에서 각 트랜잭션 모두 당직근무자 1명은 뺄 수 있는 상태일테니 둘다 뺄 것이고, 그로 인해 테이블의 제약사항이 깨져버리는 이상현상이 발생한다. 이를 Write Skew라고 한다.

 

참고로 Write Skew는 repeatable read isolation level에서도 탐지할 수 없는 이상현상이다. 이제 바로 이어서 isolation level들에 대해 설명하도록 하겠다.

 

Isolation level

참고로 위에서 설명한 이상현상들 중, SQL에서 Isolation level을 정하는 기준이 되는 이상현상은 Dirty Write, Non Repeatable Read, 그리고 Phantom이다. 하지만 본 글은 시험을 보기 위해 암기형 내용을 소개 하는게 아니라 실무에서 어떤 이상현상들이 있을 수 있는지, 그리고 실무에서 어떤 Isolation level들이 있고 어떻게 사용되는지를 다루는데 주 목적이 있다. 따라서 SQL 표준에서 소개하는 Isolation level뿐 아니라 Snapshot Isolation level도 소개할 것이다. 또한 DBMS의 종류마다 정해놓은 Isolation level의 격리수준 또한 같은 이름이어도 동작방식이 일부 다르다. 그래서 사실 Isolation level과 이상현상들에 대한 개념을 이해한 후에, 내가 사용하는 DBMS의 격리수준을 다시 공부해야 한다. 그럼에도 일단 표준에 대해 공부를 마쳐놓아야 문서를 읽을 때 이해할 수 있기 때문에 가장 흔히들 소개되는 Isolation level들에 대해 설명해보겠다.

 

Isolation level은 위에서 설명한 이상 현상을 허용할 수 있는 몇 가지 level을 만들어, 개발자가 격리 수준을 선택할 수 있도록 한다.

우선 SQL 표준에서 정의한 이상 현상 세가지중 무엇을 허용하는지를 기준으로 Isolation level을 정할 수 있다.

위 테이블을 보면 격리 수준이 더 엄격해질 수록 이상 현상을 더 방지할 수 있다는 것을 알 수 있다. 일단 하나하나 설명해보자.

 

Read Uncommitted

가장 낮은 수준의 isolation이다. 사실상 거의 아무것도 해주지 않는 수준으로, 커밋되지 않은 데이터까지 읽으면서 Dirty Read, Non Repeatable Read, Phantom Read 세 가지 현상 모두 발생할 수 있다.

 

사실 Read Uncommitted를 지원하지 않는 DBMS도 있다. 예를 들어 오라클의 경우 Read Uncommitted level은 제공하지 않는다고 한다. postgreSQL의 경우도 READ UNCOMMITTED라는 레벨은 있지만 실제로는 READ COMMITTED라는 다음 레벨로 된다.

 

Read Committed

데이터를 읽을 때 커밋되지 않은 데이터는 읽지 않고 오직 커밋된 데이터만 읽는 방식으로 Dirty Read를 방지할 수 있다. 하지만 Non Repetable ReadPhantom Read는 발생할 수 있다. 예를 들어보겠다.

T1이 먼저 트랜잭션이 시작돼서 X를 수정하려고 하고 있기 때문에 T2가 중간에 시작되면서 X를 읽으려고하면 읽을 수 없는 상태가 된다. 따라서 Dirty Read를 방지할 수 있다.

하지만 Non Repeatable Read케이스에서 봤듯이, 커밋된 데이터를 읽더라도 읽었던 값을 다시 읽었을 때 수정되거나, 없었던 데이터가 생기는 Phantom Read는 여전히 방지할 수 없다.

 

Repeatable Read

사실상 가장 많이 쓰이는 Isolation level이다. 이름에서 알 수 있듯이 non repeatable read를 방지할 수 있다. Isolation level에서는 Read 연산만 있다고 하더라도 다른 트랜잭션으로 하여금 데이터를 읽거나 쓰는걸 막는다.

하지만 위와 같은 케이스 말고, 직접적으로 X를 읽는게 아니라, range 쿼리를 한다든지 하는 케이스에서는 여전히 Phantom Read가 발생할 수 있다. (헷갈리면 위로 올라가서 다시 한번보자)따라서 Snapshot Isolation 부터 Phantom Read를 막을 수 있다.

 

참고로 Repeatable Read격리 수준 부터는 어플리케이션에서 트랜잭션이 실패할 수 있기 때문에 재시도 처리를 고려해야 한다.

 

Snapshot Isolation

Snapshot Isolation은 앞서 말한 dirty read, non-repeatable read, phantom read와 같은 이상 현상을 방지하면서 concurrent하게 트랜잭션을 실행할 수 있도록 하기 위해 설정된 격리 수준이다. Snapshot 격리수준은 트랜잭션이 데이터를 읽을 때 다른 트랜잭션의 영향을 받지 않는 일관성을 유지하기 위한 격리 수준이다. 쉽게 말해 트랜잭션은 자신이 실행되기 전에 커밋된 데이터 내용만 인식할 수 있는 것이다. 현재 트랜잭션이 실행되고 있는데, 다른 트랜잭션에서 데이터를 수정한다하더라도, 현재 트랜잭션에는 영향을 주지 않는다. 즉 트랜잭션 시작 당시 커밋된 데이터의 스냅숏을 가져오는 것과 같은 의미에서 Snapshot이라는 용어를 사용한듯 싶다.

 

Lock-based isolation level의 경우 하나의 트랜잭션이 락을 통해 같은 데이터를 읽는 다른 데이터의 실행을 방지하는 식으로 동시성을 제한하기 때문에 시스템의 성능을 떨어트리는 문제가 있다. 이러한 문제를 해결하기 위해 snapshot isolationMVCC의 일환으로 소개된 것이고(MVCC는 추후 글에서 별도로 다룰 예정), row-versiong이라는 테크닉을 사용하는데 row versioning 이 어떤 것인지 설명해보겠다.

 

row versioning은 트랜잭션에 의해 수정된 데이터들의 히스토리를 버전 번호와 함께 temp DB에 저장된다. insert, update, delete와 같은 write연산들에 대해 매번 수행된다. 만약 다른 트랜잭션이 동일한 데이터를 읽을 경우, 커밋된 버전 중에 해당 트랜잭션이 실행된 시점의 버전을 읽게 된다. 따라서 데이터를 읽는 트랜잭션이 다른 트랜잭션을 block하지 않도록 하는 것이다. 물론 쓰기 연산을 하는 트랜잭션의 경우 다른 쓰기 연산을 하는 트랜잭션과 update conflict이 발생할 경우, 둘 중 하나의 트랜잭션을 롤백되거나 취소하는데, 주로 first committer win, 즉 먼저 커밋한 트랜잭션만 허용해준다.

 

Snapshot Isolation이 Read operation에 대해 lock을 걸지 않는다는 점에서 Serializable 격리수준보다 동시성을 더 보장한다는 장점이 있지만 데이터 update가 있는 모든 트랜잭션들의 대해 row versioning을 하기 위해 temp DB에 데이터를 저장해야 하는 오버헤드가 문제고, 또한 write operation에 대해서는 block이 여전히 있긴하다.

 

Serializable

DBMS에서 가장 높은 격리수준이다. 엄격한만큼 데이터베이스의 동시처리 성능도 떨어진다. 여러 트랜잭션이 동시에 요청될 수는 있지만, serial하게 처리되도록 한다. 이러한 효과는 트랜잭션은 모든 read / write operation에 대해 lock을 통해 얻을 수 있다.

트랜잭션이 시작되면 처리하고 있는 데이터에 대해 key-range lock을 걸어 두어 현재 트랜잭션이 읽고 있는 데이터의 key range에 포함되는 데이터의 경우 현재 트랜잭션이 끝날때까지 다른 트랜잭션에서 데이터를 update하거나 insert하지 못한다. 이를 통해 Repeatable Read에서 막지못하는 phantom read현상을 방지할 수 있다.

 

물론 locking은 serializable level을 얻기 위한 하나의 방법이고, DBMS마다 사용되는 방식이 다르기 때문에, 사용하기 전에 내가 사용하는 DB에서 어떻게 구현됐는지 알아두는게 좋다.

 

PosrgresSQL의 Isolation level

필자는 주로 postgreSQL을 사용하기 때문에, postgresSQL의 특징들 몇 개만 나열해보자면 아래와 같다.

  • 명시적으로는 네 가지 표준 transaction isolation level이 있지만 내부적으로는 세가지 별도의 isolation level만 구현된다.
  • 또한 postgreSQL의 경우는 Repetable read가 phantom read를 허용하지 않는다.
  • 또한 serializable과 repeatable read의 차이는 repeatable read와 동일하게 동작하면서 트랜잭션들이 순서대로 실행중인지 모니터링하는 정도다.

 

 

 

참고자료

https://vladmihalcea.com/a-beginners-guide-to-read-and-write-skew-phenomena/

https://stackoverflow.com/questions/60972340/what-are-dirty-writes-what-happens-if-they-are-not-allowed

https://seunghyunson.tistory.com/12

https://prasad-jayakumar.medium.com/oracle-db-transactions-read-skew-d3957ecd59e2

https://retool.com/blog/isolation-levels-and-locking-in-relational-databases/

https://ssudan16.medium.com/database-isolation-levels-explained-61429c4b1e31

https://dev.to/techschoolguru/understand-isolation-levels-read-phenomena-in-mysql-postgres-c2e#serializable-isolation-level-in-mysql

Comments