티스토리 뷰

DB 작업의 단위인 트랜잭션은 ACID라는 특징을 보장해야 하는데요, ACID 중 I는 Isolation의 약자입니다. Isolation의 특징을 보장해야 한다는 것은 하나의 트랜잭션이 데이터를 처리 하고 있을때, 다른 트랜잭션이 끼어들어 이 데이터를 훼손하지 못하도록 만드는 것을 말합니다.

먼저 Isolation을 보장하지 않는다면 어떤 문제가 발생하는지 보겠습니다.

그림과 같이 DB에 A = 1이라는 데이터가 저장되어 있다고 가정하겠습니다. 그리고 Transaction 1은 DB에서 꺼낸 X에 + 1을 수행하고 Transaction 2는 꺼낸 X에 -1을 수행한다고 가정합니다.

먼저 Transaction 1이 수행된다면, Transaction 1이 DB에 접근해 A 데이터를 가져오고, X = X + 1 연산을 시작할 것입니다.

바로 이어서 Transaction 2가 수행되면, Transaction 2도 DB에 접근해 A 데이터를 가져오고, X = X - 1 연산을 시작합니다.

Transaction 1의 작업이 끝나면 변경 사항을 DB에 저장합니다. 이때 X = 1이였고, 연산 결과는 2이므로 A = 2로 갱신이 됩니다.

Transaction 2도 작업이 끝나면 변경 사항을 DB에 저장합니다. 이때 꺼내온 X = 1이였고, 연산 결과는 0이므로 A = 0으로 갱신이 됩니다.

초기 A의 값은 1이였고, +1 -1 연산을 각각 한번씩 수행했습니다. 그러면 A = 1로 유지가 될 것 같지만 하나의 Transaction이 실행되는 동안 다른 Transaction이 동시에 실행되었고 A는 0이라는 잘못된 값으로 갱신이 되었습니다.

이는 Transaction 1과 Transaction 2가 동시에 쓰기 작업을 수행해서 발생하는 문제인데, '갱신 손실 문제'라고 부르기도 합니다. 그리고 이를 해결하는 가장 간단한 방법은 '락(Lock)'을 거는 방법입니다.

락은 운영체제에서도 등장하는 용어인데요, 운영체제에서 락은 아래의 그림과 같이 동작합니다.

하나의 스레드가 임계구역을 사용중이면, 다른 스레드는 임계구역을 사용할 수 없고 계속 대기합니다. 이 그림에서 스레드를 Transaction으로 바꾸면 공유 자원을 최대 1개의 Transaction만 사용할 수 있음 보장할 수 있습니다. 그리고 더이상 '갱신 손실 문제'는 발생하지 않게 됩니다. 코드로 Lock을 구현하면 아래와 같습니다.

acquire() {
    while (!lock);
    lock = false;
}

먼저 Lock을 획득하는 코드입니다. lock이 false면 계속 무한 루프를 돌게 되고, 루프가 끝난다면 자신이 공유 자원을 사용하고 있음을 표시하기 위해 lock을 false로 바꿉니다.

release() {
    lock = true;
}

공유 자원을 전부 사용하고 작업이 끝난 후 lock을 해제하는 코드입니다. 공유 자원의 사용이 끝났다면, 다른 트랜잭션이 공유 자원에 접근할 수 있도록 lock을 true로 바꿉니다.

운영체제와 같은 방식으로 DB에도 락을 구현할 수 있습니다. 그러면 위에서 살펴봤던 동시에 2개의 Transaction이 공유 자원을 변경하는 상황에서 '갱신 손실 문제'가 발생하지 않고 데이터 일관성도 유지할 수 있습니다.

하지만, 데이터베이스에 락을 걸면 공유 자원에 접근할 수 있는 트랜잭션들의 대기 시간이 길어지고 이는 성능 저하로 이어질 수 있습니다.
그래서 '격리 수준(Isolation Level)'이 등장합니다. 격리 수준은 무조건 락을 사용하는 방식이 아니고, 여러 트랜잭션이 얼마나 서로 고립되어 있을지 수준을 나타내며 트랜잭션 격리 수준은 다음과 같은 현상을 기준으로 정의됩니다.

Dirty Read

Transaction이 아직 커밋되지 않는 데이터를 읽는 현상을 말합니다. 
예를들어 Transaction 1이 어떤 작업을 수행중이라고 할때, Transaction 2가 작업을 시작했다고 가정합니다.
Transaction 1이 어떤 공유 자원을 update 하면 Transaction 2는 update 된 공유 자원을 읽을 수 있습니다. 하지만, Transaction 1에 문제가 생겨 Rollback을 수행하게 되었습니다. 이때 Transaction 2이 읽은 자원은 사실상 존재하지 않은 자원이 되어버리는 현상입니다.

Non Repeatable Read

한 Transaction 에서 select 쿼리를 두번 수행한다고 할때, select 쿼리 사이에 다른 트랜잭션이 값을 수정하여 결과값이 서로 다른 현상을 말합니다.

@Transaction
public void query() {
    A = select();
    B = select();
}

예를들어 위와같은 Transaction이 있을때 A와 B에서 조회한 결과가 다른 상황을 말합니다. 

Phantom Read

Phantom Read는 Non Repeatable Read와 굉장히 유사합니다.
한 Transacion에서 select 쿼리를 두번 수행할때, select 쿼리 사이에 다른 트랜잭션이 값을 추가하거나 삭제하여 결과값이 서로 다른 현상을 말합니다.

@Transaction
public void query() {
    A = select();
    B = select();
}


이러한 현상들을 얼마나 허용할 것인가를 기준으로 DB 격리 수준을 4가지로 정의할 수 있습니다.

1. Read Uncommitted
Read Uncommitted는 이름 그대로 어떤 트랜잭션의 변경 사항이 Commit 되지 않았음에도 읽을 수 있는 격리 수준입니다.

Transaction이 수행되는 DB 자원은 위의 그림과 같이 Memory로 불러와 작업 후 commit 을 통해 다시 DB에 저장하게 됩니다.
Commit 되지 않은 자원에 대해 접근할 수 있다면 위의 상황에서 Transaction 2가 실행 되었을때, Transaction 2는 A가 1이라고 판단하게 됩니다. 하지만 Transaction 1의 작업 중 Rollback이 일어났다고 가정해보겠습니다.

그러면 Transaction 2 접근한 A는 잘못된 값이 되는 것이지요. 하지만 Transaction 2는 이 사실을 모르고 A = 1에 대해 비즈니스 로직을 처리할 수 있습니다. 
이러한 Dirty Read가 발생하는 것을 허용하는 격리 수준이 바로 Read Uncommitted입니다.

2. Read Committed
Read Committed도 이름에서 격리 수준을 유추할 수 있는데요, Read Uncommitted와는 반대로 Commit 된 공유 자원에만 접근할 수 있음을 보장하는 격리 수준입니다.

다시 이 그림으로 돌아와서 Read Committed가 어떻게 동작하는지 예상해보면 Transaction 2는 commit된 자원에만 접근할 수 있으므로 Transaction 1이 실행되는 중이더라도 A = 0을 불러오게 됩니다.
이제 Transaction 1이 Rollback 되더라도 잘못된 값을 불러오지 않기 때문에 Dirty Read 문제를 해결할 수 있게 되었습니다.

하지만, 여전히 문제는 남아있는데요, 아래의 그림을 보겠습니다.

Transaction 2의 비즈니스 로직이 A를 두번 불러오는 로직이라고 가정하겠습니다.

만약 Transaction이 처리되는 순서가 Transaction 1 실행 -> Transaction 2 첫번째 X Read -> Transaction 1 commit -> Transaction 2 두번째 X Read라고 해봅시다. 그러면 Transaction 2에서 불러온 A의 값이 각각 0, 1이기 때문에 일치하지 않습니다. 

위에서 봤던 Non Repeatable Read이 발생하는 것이지요. 따라서 Read Committed는 Non Repeatable Read 이 발생하는 것을 허용하는 격리 수준입니다.

3. Repeatable Read
Repeatable Read는 MySQL InnoDB에서 채택한 격리 수준인데요, Transaction이 시작되기 전 Commit된 데이터만 사용하도록 보장하는 격리 수준입니다.

MySQL의 공식 문서에 따르면 Repeatable Read는 위의 그림과 같이 동작합니다.
MySQL InnoDB 다중 버전 관리를 사용하여 특정 시점의 데이터베이스 스냅샷을 쿼리에 제공합니다. 이 시점의 기준은 Transaction마다 할당된 Transaction ID를 기준으로 판별하고, 해당 시점 이전에 커밋된 SNAPSHOT을 기준으로 데이터를 읽습니다.

몇몇 블로그에서 Repeatable Read에서 Phantom Read가 발생할 수 있다고 하지만, 이는 잘못된 정보입니다. 기본적으로 Repeatable Read 방식을 사용하는 MySQL InnoDB에서는 MVCC 모델을 채택했기 때문에 Phantom Read가 발생하지 않습니다.

즉 Repeatable Read에서 Phantom Read가 무조건적으로 발생하지 않고, DB 엔진이 무엇이냐에 따라 이 문제는 다르게 해석되며, Repeatable Read는 Phantom Read이 발생한다와 같은 공식은 존재하지 않습니다.

4. Serializable
단순하지만, 가장 엄격한 Isolation Level 입니다.


Isolation Level이 Serializable로 설정되면, 읽기 작업을 수행할때도 Lock을 획득해야 합니다. 
쉽게 해석하면 하나의 공유 자원에 접근하기 위해서는 무조건 Lock을 획득해야 하고, 한번에 하나의 Transaction만 접근할 수 있는 방식입니다. 따라서 앞서 살펴본 Dirty Read, Non Repeatable Read, Phantom Read와 같은 문제들이 절대 발생하지 않습니다.

하지만, Transaction 동시 처리가 불가능하기 때문에 다른 Isolation Level 보다 성능이 떨어지게 되고, 대부분의 DB가 사용하지 않는 방식입니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함