본문 바로가기

DB

트랜잭션 격리성

데이터베이스 트랜잭션은 4가지 특성을 가지고 있다. 4가지 중 3가지에 대해서는 글로만 읽더라도 쉽게 이해가 되었는데 격리성에 대해서는 이해하기 어려워 이를 정리하고자 한다.

격리성(Isolation)

여러 트랜잭션이 동일한 데이터를 다루다 보면 이상 현상이 발생할 수 있다.

예를 들어 트랜잭션-1은 데이터 A의 값을 50에서 10으로 변경하고, 트랜잭션-2는 A의 값을 50에서 100으로 변경한다고 가정해보자.

1. 트랜잭션-1 : A의 값 조회 (결과 : A = 50)

2. 트랜잭션-2 : A의 값 조회 (결과 : A = 50)

3. 트랜잭션-1 : A의 값 변경 (결과 : A = 10)

4. 트랜잭션-1 : 커밋

5. 트랜잭션-2 : A의 값 변경 (결과 : A = 100)

6. 트랜잭션-2 : 커밋

최종 결과 A = 100이라는 값을 가지게 되었다.

 

이런 경우를 트랜잭션-1이 수행한 결과가 무시되었다고 해서 Lost Update라고 한다. 간단히 하나의 예시만 말했지만, 상당히 많은 경우의 수로 이상 현상이 발생할 수 있다. 이러한 문제를 해결하기 위해 격리성이라는 원칙을 만들게 되었다.

 

즉 격리성이란 여러 트랜잭션이 동시에 같은 데이터를 조회하거나 변경할 때, 하나의 트랜잭션이 먼저 실행 중이라면 다른 트랜잭션들은 연산에 끼어들지 못하도록 하는 원칙이다.

대표적인 3가지 상황

격리 수준은 크게 아래 3가지 상황을 가지고 수준을 분류한다. 물론 3가지 상황 외에도 위에서 설명한 Lost Update를 비롯하여, Read Skew, Write Skew, Dirty Write 등 많은 상황이 존재하지만 대표적으로 3가지 상황에 대해 정리하려고 한다.

Dirty Read

Dirty Read란 트랜잭션-1이 데이터 A를 변경하고 커밋하지 않은 상황에서, 트랜잭션-2가 데이터 A의 값을 조회한 상황을 말한다.

예를 들어 데이터 A가 100, 데이터 B가 20이라는 값을 가졌다고 해보자

1. 트랜잭션-1 : A의 값 조회 (결과 : A = 100)

2. 트랜잭션-1 : A의 값 50으로 변경 (결과 : A = 50)

3. 트랜잭션-2 : A의 값 조회 (결과 : A = 50)

4. 트랜잭션-2 : B의 값 조회 (결과 : B = 20)

5. 트랜잭션-2 : B에 A를 더한다 (결과 : B = B(20) + A(50) )

6. 트랜잭션-2 : 커밋 

7. 트랜잭션-1 : 롤백 (A = 50 --> A = 100)

최종 결과는 A = 100, B = 70을 가지게 되었다.

 

원래대로라면 트랜잭션-1이 정상 커밋되어 A = 50, B = 70이 나와야 하지만 트랜잭션-1이 롤백을 하는 바람에 A의 최종 값은 100이 되었다. 

Non-Repeatable Read

Non-Repeatable Read란 한 트랜잭션이 동일한 조건으로 두 번 이상 조회했는데 처음 조회한 값과는 다른 값이 나온 상황을 말한다.

예를 들어 데이터 A가 100이라는 값을 가졌다고 해보자

1. 트랜잭션-1 : A의 값 조회 (결과 : A = 100)

2. 트랜잭션-2 : A의 값 조회 (결과 : A = 100)

3. 트랜잭션-2 : A의 값을 100에서 20으로 조회 (결과 : A = 20)

4. 트랜잭션-2 : 커밋

5. 트랜잭션-1 : A의 값 조회 (결과 : A = 20)

최종 결과는 A = 20을 가지게 되었다.

 

이처럼 트랜잭션-1이 처음 조회했을 때 A의 값은 100이었는데, 두 번째 조회했을 때는 예상과 다르게 20이 나온 상황을 말한다.

Phantom Read

Phantom Read란 한 트랜잭션이 동일한 조건으로 두 번 이상 조회했는데 처음 조회한 행보다 더 많은 행의 결과가 나온 상황을 말한다.

예를 들어 count > 10 인 데이터를 조회한다고 해보자

1. 트랜잭션-1 : count > 10 조회 (결과 : A)

2. 트랜잭션-2 : B의 count를 11로 변경 (결과 : B의 count = 11)

3. 트랜잭션-2 : 커밋

4. 트랜잭션-1 : count > 10 조회 (결과 : A, B)

최종 결과는 A, B를 가지게 되었다.

 

이처럼 트랜잭션-1이 처음 조회했을 때 count > 10 결과는 A였지만, 두 번째 조회했을 때는 예상과 다르게 A와 B가 나온 상황을 말한다.

 

Non-Repeatable Read와 Phantom Read는 얼추 비슷해 보이지만 서로 전혀 다른 상황에서 발생한다.

Non-Repeatable Read은 단일 행에 대해서만 말한다. (A라는 값이 100에서 20으로 변경되어 조회됨)

Phantom Read는 여러 행이 추가되는 상황을 말한다. (처음 조회했을 때는 A만 조회됐지만, 두 번째 조회했을 때는 A, B가 조회됨 )

 

위의 3가지 상황을 보고 4단계로 결정한 것이 격리 수준이다.

격리 수준(Isolation Level)

격리성을 지키기 위해 한 트랜잭션이 수행을 마치는 동안 여러 트랜잭션이 수행을 기다리게 된다면 자원을 낭비하게 될 것이다. 이런 자원의 낭비를 막기 위해 격리 수준을 크게 4단계로 나누어 보다 효율적으로 트랜잭션을 관리할 수 있게 만들었다. (각기 다른 RDBMS 마다 격리 수준에 대해 동작이 달라 결과가 다르게 나올 수 있다.)

 

격리 수준은 단계가 높을 수록 데이터를 보다 엄격하게 격리시켜 다른 트랜잭션으로부터 영향을 받지 않도록 한다. 하지만 그만큼 동시에 실행될  있는 동시성이 떨어지기 때문에 최고의 퍼포먼스를   없다. 그렇다고 단계를 낮추게 되면 동시성은 높아지지만, 여러 트랜잭션에서 데이터를 변경하다 보면 잘못된 결과가 나올  있다.

 

격리 수준 4단계

Isolation Level Dirty Read Non-Repeatable Read Phamtom Read
Read Uncommitted O O O
Read Committed X O O
Repeatable Read X X O
Serializable X X X

Level-0 (Read Uncommitted)

Level-0은 위의 3가지 상황을 모두 허용하는 단계다. Level-0을 사용할 경우 데이터의 정합성에 문제가 발생되기 때문에 사용하지 않는 것을 권장한다.

Level-1 (Read Committed)

Level-1은 커밋된 데이터만을 읽는 단계다. 즉 커밋되기 전의 변경된 데이터들은 읽을 수 없다.

예를 들어 데이터 A가 100이라는 값을 가졌다고 해보자

1. 트랜잭션-2 : A의 값을 100에서 50으로 변경 (결과 : A = 50)

2. 트랜잭션-1 : A의 값 조회 (결과 : A = 100)

3. 트랜잭션-2 : 커밋

4. 트랜잭션-1 : A의 값 조회 (결과 : A = 50)

 

위와 같이 트랜잭션-2가 커밋한 다음과 커밋하기 전의 값이 다르게 나올 수 있다.

Level-2 (Repeatable Read)

Level-2는 트랜잭션 시작 시간을 기준으로 그전에 커밋한 데이터를 읽는 단계이다. 즉 여러 번 조회를 하더라도 처음 조회했던 데이터만 가져온다.(사실 RDBMS 마다 데이터를 읽어오는 기준이 다르다. 어떤 제품은 트랜잭션 시작 시간을 기준으로 하고, 어떤 제품은 처음 조회 시간을 기준으로 한다.)

예를 들어 데이터 A가 100이라는 값을 가졌다고 해보자

1. 트랜잭션-2 : A의 값을 100에서 50으로 변경 (결과 : A = 50)

2. 트랜잭션-1 : A의 값 조회 (결과 : A = 100)

3. 트랜잭션-2 : 커밋

4. 트랜잭션-1 : A의 값 조회 (결과 : A = 100)

 

Level-1과 같은 상황이었지만, 격리 수준이 다르기 때문에 다른 결과가 나온 것을 알 수 있다.

Level-3 (Serializable)

Level-3은 트랜잭션-1가 수행 중에 데이터 A를 수정하거나 조회한 상황이라면 다른 트랜잭션은 데이터 A를 수정하기 위해 트랜잭션-1을 한없이 기다렸다가 트랜잭션-1이 커밋되었을 때 수행되는 단계를 말한다. 그만큼 데이터의 정합성이 보장되지만, 동시성은 줄어든다.

예를 들어 데이터 A가 100이라는 값을 가졌다고 해보자

1. 트랜잭션-2 : A의 값을 100에서 50으로 변경 (결과 : A = 50)

2. 트랜잭션-1 : A의 값 조회 (트랜잭션-2가 A의 값을 변경 중이어서 대기)

3. 트랜잭션-2 : 커밋

4. 트랜잭션-1 : A의 값 조회 (결과 : A = 50)

 

위처럼 트랜잭션-1은 트랜잭션-2가 종료될 때까지 기다렸다 결과를 가져온다.

마치며

3가지 상황과 4가지의 격리 수준을 알아보았다. 격리성이 어려운 이유 중 하나는 RDBMS마다 각기 다른 정책을 가지고 있다는 것이다.

예를 들면 Repeatable Read 단계를 가진 MySQL과 PostgreSQL이 있다고 가정해보자

 

MySQL

1. 트랜잭션-1 : A의 값을 조회 (Read-Lock으로 다른 트랜잭션에서 조회 불가)

2. 트랜잭션-2 :  A의 값을 조회 (Lock이 걸린 상태이므로 대기)

3. 트랜잭션-1 : A의 값을 변경 후 커밋

4. 트랜잭션-2 :  A의 값을 조회 (트랜잭션-1의 Read-Lock이 Unlcok 되었으므로 조회 가능)

 

PostgreSQL

1. 트랜잭션-1 : A의 값을 조회 (Read-Lock으로 다른 트랜잭션에서 조회 불가)

2. 트랜잭션-2 :  A의 값을 조회 (Lock이 걸린 상태이므로 대기)

3. 트랜잭션-1 : A의 값을 변경 후 커밋

4. 트랜잭션-2 :  A의 값을 조회하지 못하고 롤백(A의 값을 트랜잭션-1 먼저 조회하고 변경하여 트랜잭션-2는 롤백을 한다.)

 

위처럼 MySQL, PostgreSQL가 서로 다른 정책을 가진 것처럼 각각의 RDBMS마다 다른 정책을 가지고 있기 때문에 현재 사용하고 있는 RDBMS의 공식 문서를 잘 확인해봐야한다.


참고 자료

https://youtube.com/playlist?list=PLcXyemr8ZeoREWGhhZi5FZs6cvymjIBVe