나의 독학은

트랜잭션 격리 수준을 통해 낙관적, 비관적 락을 `왜` 사용해야 하는지를 이해하기 본문

카테고리 없음

트랜잭션 격리 수준을 통해 낙관적, 비관적 락을 `왜` 사용해야 하는지를 이해하기

안종혁 2024. 6. 24. 02:23

비관적 락과 낙관적 락에 대한 공부 자료는 저 이외에도 충분하다고 판단하여, 해당 글에서는 설명하지 않습니다.
락에 대해 학습하고 있는 중이기 때문에, 틀린 부분이 있을 수 있습니다. 발견하신다면 댓글로 남겨주시면 감사하겠습니다.

 

트랜잭션과 격리 수준을 이해해서 낙관적, 비관적 락을 왜 사용하는지에 대해 알아봅니다.

1. 트랜잭션의 격리 수준(Isolation level) 이해하기

트랜잭션의 격리 수준(isolation)이란?

  • 데이터의 일관성을 유지 하기 위해 여러 트랜잭션이 서로에게 영향을 미칠 수 있는 범위를 어느 정도로 제한할 지를 레벨로 나눈 것입니다.

예를 들면, A트랜잭션에서 id=1 에 대한 레코드를 조회 할 때, B트랜잭션에서 id=1 에 대한 레코드를 변경했다고 가정합니다.

이 때, A트랜잭션의 결과가 B트랜잭션에 의해 변경된다면 "두 트랜잭션은 격리가 되지 않았다"고 말합니다.

즉, 트랜잭션 격리 수준에 따라 데이터의 일관성이 지켜질 수도, 깨질 수도 있습니다.

 

그렇다면 트랜잭션의 격리 수준이 어떨 때, 어떤 문제가 발생해서 데이터의 일관성이 지켜지지 않을까요?

2번에서 알아보겠습니다.


2. 트랜잭션의 격리 수준 4단계와 이에 따라 발생하는 문제 3가지

SQL 표준에서는 트랜잭션이 동시에 진행 될 때,

발생할 수 있는 문제의 종류에 따라 트랜잭션의 격리 수준을 아래의 표처럼 나누었습니다.

  •  Isolation level 에 따라 발생할 수 있는 문제는 "O" 이고, 발생하지 않으면 "X" 입니다.
Isolation Level Dirty Read Problem Non Repeatable Read Problem Phantom Read Problem
Read Uncommitted O O O
Read Committed X O O
Repeatable Read X X O
Serializable X X X

트랜잭션이 동시에 진행될 때, 발생할 수 있는 문제를 알아보겠습니다.

문제 1. Dirty Read Problem

  •  한 트랜잭션에서 변경한 값을 다른 트랜잭션에서 읽을 때 발생합니다.
  • 예를 들어, 아래 표에서 T1이 a 값을 변경하고 롤백할 경우, a 값은 실제로 DB에 반영되지 않아야 합니다.
  • 그러나 T2는 이 변경된 a 값을 읽어 마치 DB에 반영된 것처럼 사용할 수 있습니다.
  • 트랜잭션 격리 수준을 Read Committed로 올리면, 커밋되거나 롤백된 트랜잭션만 읽을 수 있게 되어 이 문제를 방지할 수 있습니다.

문제 2. Non-repeatable Read Problem

  • 한 트랜잭션에서 같은 값을 두 번 읽었을 때 각각 다른 값이 읽히는 경우를 뜻합니다.
  • 예를 들어, T1이 처음에 a 값을 읽었는데, T2가 그 사이에 a 값을 삭제하고 변경 사항을 DB에 반영한 경우, T1이 다시 a 값을 읽으려 할 때 a 값은 더 이상 존재하지 않게 됩니다. 따라서 T1이 처음 읽은 a 값과 나중에 읽은 a 값이 달라집니다.
  • 트랜잭션 격리 수준을 Repeatable Read 로 올려서 트랜잭션이 시작된 후, 다른 트랜잭션의 커밋변 변경 사항을 읽지 않음으로써 이 문제를 방지할 수 있습니다.

문제 3. Phantom Read Problem

  • 한 트랜잭션에서 한 값을 두 번 읽을 때, 없던 값이 생겨서 데이터 수가 변하는 경우입니다.
  • 예시는 Non-Repeatable Read Problem 과 유사하며,
  • Non-Repeatable Read 은 같은 데이터의 변경으로 인해 발생하는 문제라면, Phantom Read 는 데이터의 삽입에 의해 찾고자 하는 데이터의 개수가 달라졌을 때의 의미입니다.
이렇게 트랜잭션끼리 격리 되지 않는다면,
데이터 정합성에 문제가 생기기 때문에 트랜잭션 끼리 영향을 끼치지 않도록 트랜잭션들을 격리 시켜야만 합니다.


물론, 이런 문제들이 모두 발생하지 않게 할 수 있지만,
그만큼 제약사항이 많아져 동시 처리 가능한 트랜잭션 수가 줄어들어 결국 DB의 전체 처리량이 하락하게 됩니다.( == 성능이 나빠집니다)


그래서, 일부 이상한 현상은 허용하는 몇 가지 level 을 만들어서 사용자 필요에 따라 선택하도록 한 것이 트랜잭션 격리 수준 4단계입니다.

이제, 이 문제들을 막을 수 있는 트랜잭션의 격리 수준 4단계를 알아보겠습니다.

격리 수준 1단계 : Read Uncommitted

  • 가장 낮은 격리 수준으로, 하나의 트랜잭션이 다른 트랜잭션의 커밋되지 않은 데이터를 읽을 수 있습니다.
  • 이로 인해, Dirty Read, Non-Repeatable Read, Phantom Read 문제가 발생합니다.
  • 커밋되지 않은 데이터를 읽을 수 있기 때문에 4단계 격리 수준 중 성능이 가장 빠르지만(트랜잭션 처리량이 가장 많지만), 데이터의 일관성을 보장할 수 없습니다.

격리 수준 2단계 : Read committed

  • 트랜잭션이 데이터를 읽을 때, 다른 트랜잭션의 커밋된 데이터만 읽습니다.
  • Non-Repeatable Read 와 Phantom Read 는 여전히 발생합니다.

격리 수준 3단계 : Repeatable Read

  • 하나의 트랜잭션 내에서 동일한 데이터를 여러 번 읽을 때마다 같은 결과를 보장합니다.
  • Phantom Read 는 여전히 발생합니다.

격리 수준 4단계 : Serializable

  • 가장 높은 격리 수준으로, 트랜잭션이 순차적으로 실행되는 것처럼 보장합니다.
  • 그렇기 때문에, 데이터 일관성을 유지할 수 있지만 성능이 매우 떨어집니다.

3. 트랜잭션 격리 수준과 "Lock" 의 관계 

이렇게 4단계의 트랜잭션 격리 수준을 DB 에서는

read Lock(S-Lock, shared Lock) 과 write Lock(X-Lock, exclusive Lock) 를 이용해서 각 격리 수준을 구분하였고,

동시성을 제어했습니다.

  • read Lock (S-Lock, shared Lock)
    • 한 트랜잭션이 id=3 에 대한 레코드에 read Lock 을 획득하면, 다른 트랜잭션은 id=3 에 대한 레코드를 읽기만 할 수 있고, 변경할 수 없습니다.
  • write Lock (X-Lock, exclusive Lock)
    • 한 트랜잭션이 id=3 에 대한 레코드에 write Lock 을 획득하면, 다른 트랜잭션은 id=3 에 대한 레코드를 읽을 수도 변경할 수도 없습니다.

하지만, read Lock 과 write Lock 을 이용해서 동시성을 제어하고, 데이터의 일관성을 유지하는 방법에는 단점이 있었습니다.

 

바로 A 트랜잭션이 id=3 인 레코드를 변경하기 위해 write Lock 을 획득하면,  B 트랜잭션이 id=3인 레코드를 읽고만 싶어도 read Lock 을 획득 할 수 없었습니다.

즉, read Lock 과 write Lock 을 동시에 획득하지 못하고 업데이트 중에는 읽기가 블록된다는 것이었습니다.
  read Lock write Lock
read Lock O X
write Lock X X

4. MVCC(Mulit Version Concurrency Control) 의 등장

  • 데이터의 일관성을 유지하기 위해 Lock 을 사용한 방식은 트랜잭션의 전체 처리량을 낮게 만들었고, 각 DBMS 는 이 문제를 해결하기 위해 노력했습니다.
  • 그리고 노력의 결과로 데이터의 일관성을 유지하기 위해 MVCC 방식이 등장하게 되었습니다.

MVCC 란?

  • MVCC의 목적은 잠금을 사용하지 않고 일관된 읽기는 제공하는 데 있습니다.
  • 트랜잭션의 격리 수준을 지키기 위해 Lock 이 아닌 스냅샷을 이용하는 방식입니다.

MVCC의 등장으로 인해, 잠금 없이 일관된 데이터를 읽을 수 있게 되어 트랜잭션의 전체 처리량이 높아지게 되었습니다.


5. MySQL 의 MVCC와 낙관적, 비관적 락의 관계

그렇다면, MySQL 은 MVCC 를 어떻게 활용하고 있을까요? 

 

MySQL 8.0 부터 default 트랜잭션 격리 수준은 Repeatable Read 이며, MVCC를 이용하여 데이터의 읽기 일관성을 지원해주고 있습니다.

  • MySQL 의 Repeatable Read 격리 수준에서는 MVCC를 사용해서 동시성을 제어하고 있기 때문에, Dirty Read, Non-Repeatable Read, Phantom Read 의 문제는 발생하지 않습니다.

그러나, MVCC 기법을 사용하더라도 Lost Update 와 같이 여러 트랜잭션이 하나의 데이터에 대한 쓰기 문제를 막을 수는 없었습니다.

  • 사실 두 트랜잭션이 동시에 진행되면 일관된 데이터를 읽지 못해 발생하는 Dirty Read, Non-Repeatable read, Phantom Read 말고도 다양한 문제들이 생기고, 그 중 하나가 Lost Update 문제 입니다.

Lost Update

  • A트랜잭션도 id=3 레코드에 대해 쓰기작업을 하고, B 트랜잭션도 id=3 레코드에 대해 쓰기작업을 할 때, A트랜잭션의 결과가 B트랜잭션 결과에 의해 덮어씌어져서 A트랜잭션의 결과가 DB에 반영되지 않는 것입니다.

그렇기에 두 트랜잭션이 하나의 데이터에 대해 쓰기 작업을 할 때,

개발자는 해당 레코드에 Lock 을 걸어줌으로써 데이터의 일관성을 지켜야 합니다.

 

이 때, 사용할 수 있는 여러 방법들 중 한 방법이 낙관적 Lock or 비관적 Lock 이며,

저희는 이 Lock 들을 사용해서 데이터의 일관성을 보장해야만 합니다.

 

뿐만 아니라,

  • DBMS 마다 데이터의 일관성을 유지하기 위해 default로 설정된 격리 수준다양한 문제들(Lost Update, Phantom read) 을 막아서 동시성을 제어하는 방식이 다르기 때문에
  • 사용할 DBMS 의 default 격리 수준은 무엇이고, 데이터의 일관성을 어떻게 유지하고 있는지 아는 것은 매우 중요합니다.

출처