Database
수정·

트랜잭션의 격리 수준

by Dohoon Kim · 24년 11월 20일 18:55:06

트랜잭션의 격리 수준

개요

관계형 데이터베이스에서 트랜잭션 격리 수준은 동시에 여러 트랜잭션이 동일한 데이터에 대한 읽기 및 쓰기 접근을 하는 경우
트랜잭션이 다른 트랜잭션의 데이터 변경 사항을 언제 관찰할 수 있는지를 제어한다. 가시성에 대한 제어를 한다고 볼 수 있다. Lock과 유사한 측면이 있지만 목적이 다르다고 볼 수 있다.

동시성과 가시성

동시성(Concurrency)은 다음과 같이 정의할 수 있다.

여러 트랜잭션의 실행 스케줄을 관리하는 것

읽기 요청 처리 시간이 500ms이고, 현재 조회수가 1인 게시글에 대한 읽기 요청이 있다고 가정하자
두개의 읽기 요청이 1ms 차이로 순서대로 들어왔을 때, 각 트랜잭션은 조회수를 각각 2로 업데이트 할 것이고, 최종 조회수는 2가 될 것이다. 하지만 우리가 원하는 값은 3이다. 이런 부분을 해결 할 때, Lock을 사용하거나 Database Trigger를 사용하는 등 방식을 선택할 수 있다.
동시성 제어가 되지 않는다면 위와 같은 문제가 발생한다.

가시성(Visibility)은 다음과 같이 정의할 수 있다.

다른 트랜잭션의 데이터 변경 사항을 관찰할 수 있는 정도

한 데이터에 대해 두개의 트랜잭션이 존재한다고 가정해보자. 트랜잭션 A가 데이터에 대한 변경을 한 상태에서 커밋을 하지 않았는데 트랜잭션 B가 해당 데이터의 변경을 포함하여 조작한다면 데이터의 일관성이 깨지게 될 수도 있다. 이런 문제를 방지하기 위해 트랜잭션 간의 격리 레벨을 설정하여 한 트랜잭션 내에서 일관성을 보장한다

즉, Lock은 동시성 제어가 목적이며 격리 수준은 가시성 제어의 목적이 강하다.
다만 레벨에 따라 내부적으로 Lock이 사용되므로 동시성에도 영향을 미친다,

트랜잭션의 격리 레벨

트랜잭션의 격리 수준은 네가지가 존재한다.

  1. Read Uncommitted - 커밋되지 않은 데이터를 볼 수 있다.
  2. Read Committed - 커밋된 데이터만 볼 수 있다.
  3. Repeatable Read - 동일 트랜잭션에서 동일 데이터를 반복해서 읽으면 동일한 값이 나타난다.
  4. Serializable - 다른 트랜잭션의 변경 작업이 원천적으로 차단된다.

위로 갈 수록 가시성이 높아지고 아래로 내려갈수록 가시성이 감소한다. 마찬가지로 위로 갈 수록 처리량은 높지만 데이터의 일관성 보장 수준이 낮아진다. 아래로 내려갈 수록 처리량은 낮아지고 데이터의 일관성 보장 수준이 높아진다.

Read Uncommitted 그리고 발생할 수 있는 문제점

Read Uncommitted는 동일한 데이터에 대해 두 개 이상의 트랜잭션이 접근할 때 발생할 수 있다. 한 트랜잭션에 의해 값이 변경된 후 아직 커밋 되지 않은 상태에서 다른 트랜잭션이 이 값을 읽을 때 많은 문제가 발생할 수 있다.

이로 인해 발생하는 문제는 Dirty Read가 대표적이다. Dirty Read는 두 트랜잭션 중 한 트랜잭션에서 데이터를 수정하고 다른 트랜잭션이 해당 변경 데이터를 참조한 상태에서 변경 트랜잭션이 롤백 되어 버렸을 때 발생한다.

Dirty Read

위 그림을 예로 들어보자. 하나의 계좌에 대해 3의입금트랜잭션과1의 입금 트랜잭션과 1 출금 트랜잭션이 수행된다.

  1. 트랜잭션 A는, 트랜잭션 B에서 1달러 출금이 완료된 이후 커밋되지 않은 데이터(잔액)을 읽는다.
  2. 이 때 잔액은 0이며,이값에3이며, 이 값에 3를 추가하게 된다.
  3. 최종적으로 트랜잭션 A의 결과는 잔고가 3$가 되게 된다.
  4. 이 때 트랜잭션 B가 실패하고 롤백된다. 이 때 결과는 잔액 1$가 된다.

두 트랜잭션의 최종 결과 모두, 실제 우리가 원하는 결과인 4$와 일치하지 않는다.

이 외에도 Dirty Write 문제가 발생하는데 상황은 아래와 같다.

Dirty Write

  1. 트랜잭션 A가 시작하는 시점에 트랜잭션 B에 의해 이미 잔액이 0이 된 상황이고, 트랜잭션 B가 먼저 Abort 잔고는 1$로 롤백된다.
  2. 트랜잭션 A는 초기 잔액은 트랜잭션 B가 먼저 변경한 0$를 잔액 초기값으로 인식한다.
  3. 트랜잭션 B가 롤백 된 이후, 트랜잭션 A가 롤백되면서 0$로 잔액이 저장된다.

위 경우에도, 일관성이 보장되어야 하는 상황이라면 1$가 되어야 하기 때문에 문제가 발생한다.

Read Commited 이상의 격리 수준을 사용하면 위와같은 Dirty Read/Write 문제를 해결 할 수 있다.

Read Committed 그리고 발생할 수 있는 문제점

Read Committed는 하나의 데이터에 접근하는 두 개 이상의 트랜잭션이 있을 때, 반드시 커밋된 데이터만을 읽을 수 있는 격리 레벨이다.

Read Committed 격리 수준에서 위 Dirty Read 예시와 동일 트랜잭션을 수행할 때의 모습을 살펴보면 아래 그림과 같다.

Read Commited 에서 Dirty R/W 해결

반드시 커밋된 데이터만 읽어오기 때문에 트랜잭션 A에서 초기 잔액을 0가아닌1가 아닌 1로 인식하여 문제가 발생하지 않는다.

하지만 Read Commited에서도 데이터 불일치가 발생하는 이상현상들이 존재한다. Non Repeatable Read 라는 문제인데 아래 그림을 통해 살펴보자

Non Repeatable Read

트랜잭션 A 내에서 데이터의 값이 변경을 하는 작업이 없다. 다만 하나의 값을 두번 읽는데 두번째 읽는 값이 변경되어 있다. 결과는 일치하기 때문에 문제가 없다라고 볼 수 있지만 트랜잭션 격리라는 관점에서 보면
한 트랜잭션 내에서 데이터의 일관성이 깨진 상태이기 때문에 문제가 된다.

또 Read Skew / Write Skew라는 문제가 여전히 존재한다. 먼저 Read Skew를 살펴보자

Read Skew

Non Repeated Read와 유사해 보이지만 한 레코드를 중복해서 읽는 것이 아닌, 서로 다른 레코드를 서로 다른 타이밍에 읽어 발생하는 문제이다. 트랜잭션 A 에서는 통장 A,B 에 있는 잔고 금액의 합이 100로일치하지만트랜잭션B에서는트랜잭션A완료이후통장B의잔고를읽어합이130로 일치하지만 트랜잭션 B 에서는 트랜잭션 A 완료 이후 통장 B의 잔고를 읽어 합이 130가 되어 일관성이 깨진 모습이다.

다음은 Write Skew의 예시이다.

Write Skew

위 그림에서 A, B 둘 중 최소 한명은 근무를 해야하는 근무 시스템이라고 하자
트랜잭션 A에서 A를 퇴근 상태로 변경하려하고
트랜잭션 B에서 B를 퇴근 상태로 변경하려고 한다. 각 트랜잭션에서는 다른 한명이 근무 중이기 때문에 현재 요청을 처리하는데 문제가 없어
트랜잭션을 처리한다. 두 트랜잭션은 각각
update set is_working=true where name = 'A'
update set is_working=true where name = 'B' 쿼리가 발생한다.

결국 각 트랜잭션 상에서 결과는 문제가 없지만, 커밋된 최종 데이터가 저장된 형태는
빨간색으로 표기한 것과 같이 제약 조건을 위반한다.

마지막으로 Lost Update 문제가 있다.

Lost Update

한 게시물을 조회할 때 조회 수가 1씩 증가하는 비지니스 로직이 있다고 가정해보자.
거의 동시에 한 게시물에 대한 두 개의 조회 요청이 왔다면 어떻게 될까?
서버에서는 해당 게시물 ID를 주고 제목, 내용, 게시자, 날짜, 조회수 등을 데이터베이스에서 조회하여 가져오고 조회수를 조회수 + 1 로 업데이트 할 것이다.

위 상황은 그런 경우에 발생하는 갱신 유실 문제를 보여주고 있다.
거의 동시에 들어온 두 요청은 다른 트랜잭션이 조회수를 업데이트 하고 최초의 조회 수 값을 읽어오게 되고, 결과적으로 같은 조회 수 + 1 로 업데이트하게 되면서 실제 조회 수는 1이 유실되어 버린다.

위 문제들은 Write Skew 그리고 Lost Update를 제외하면 Repeatable Read 격리 수준으로 해결할 수 있다.

Repeatable Read 그리고 발생할 수 있는 문제점

Repeatable Read는 한 트랜잭션 내에서 한 레코드의 값을 반복해서 읽더라도 항상 같은 값을 내도록(그 트랜잭션에서 변경 작업이 없었다면) 하는 격리 수준이다. 따라서 Read Commited 격리 수준에서 발생했던 Non Repeatable Read를 해결할 수 있다.

Read Committed에서 예를 든 Non Repeatable Read의 경우를 Repeatable Read 격리 수준에서 동작시키면 아래 그림과 같을 것이다.

Repeatable Read 격리 수준의 동작

하지만 이 격리 수준은 접근하는 단일 레코드에 대한 일관성은 보장하지만, 여러 건을 조회하여 나온 결과 집합에 대한 일관성을 보장하지 못한다. 따라서 여러건의 데이터를 조회하는 경우 결과 값이 달라지는 Phantom Read 현상이 나타날 수 있다.

Phantom Read

위의 경우 트랜잭션 A의 공지 목록이 공지 추가/수정 연산이 없음에도 불구하고 한 트랜잭션 내의 조회 결과가 달라진 것을 확인할 수 있다. 이는 Repeatable Read 격리 수준이 데이터 집합의 삽입/삭제와 같은 구조적인 변화를 제어하지 않기 때문이다. Repeatable Read에서 역시 Lost Update 문제, Write Skew에서 자유롭지 못하다.

Serializable

Serializable 격리 레벨에서는 모든 문제가 해결되지만 동시성이 극도로 떨어져 처리량이 좋지 않다.

트랜잭션 격리 수준별 발생하는 이상현상

문제Read UncommittedRead CommittedRepeatable ReadSerializable
Dirty ReadOXXX
Non-Repeatable ReadOOXX
Phantom ReadOOOX
Lost UpdateOOOX
Write SkewOOOX
Read SkewOOXX