Transaction이란?
트랜잭션(Transaction)은 하나의 작업 단위를 의미한다. 데이터베이스에 저장되어 있는 데이터를 CRUD(Create/Read/Update/Delete)를 통해 제어하는데, 단건이 아니라 여러 변경사항을 하나의 작업으로 취급하기 위해 트랜잭션을 사용한다. 예를 들어, 은행 시스템에서 돈을 송금하는 로직을 구현할 때, A 계좌에서는 돈을 차감하고 B 계좌에서는 돈을 증가 시키는 작업은 하나의 단위로 취급되어야한다. 즉, All or Nothing에 대한 개념이 도입되는 것이고 일괄성을 보장하기 위한 용도로 트랜잭션이 사용되는 것이다. 그런데 일관성을 보장하기 위해서는 병행성을 Trade-Off로 내어줘야한다.
당연한 결과이다. 데이터를 일관되게 보장하기 위해서 하나의 session에서 데이터를 제어할 때, 다른 session에서 데이터를 제어할 수 없도록 막는 것이 일관성을 보장하는 방법이다. 그래서 위의 표 처럼 격리에 대한 수준이 올라가면 일관성을 높아지지만, 병행성은 낮아진다. 이러한 격리에 대한 수준의 정도를 표현한 것이 Transaction Isolation Level이다.
Transaction Isolation Level
Transaction Isolation Level은 Database의 이론적인 내용이다. 각각 DBMS 별로 구현하는 방식도 다르고 기본적으로 따르고 있는 Isolation Level도 다르지만, 일반적으로 아래와 같이 4가지 Level로 나눠서 구분한다.
- 0 - Read Uncommitted
- 1 - Read Committed
- 2 - Repeatable Read
- 3 - Serializable
일관성 100% 보장을 위해 Level 3인 Serializable을 항상 사용하면 좋겠지만, 과도한 Lock을 잡게되어 대기하는 시간이 많아지면 사용자 입장에서는 매우 불편한 시스템이 될 수 있다. 그래서 서비스의 요구사항에 따라 이런 Isolation에 대한 정도를 결정해야할 것이고 더 나아가서는 트랜잭션을 정말 사용해야되는지까지 고민해볼 필요도 있다.
일관성 정도에 따라 아래와 같은 문제들이 발생할 수 있다.
- Dirty Read
- Non-Repeatable Read
- Phantom Read
아래는 Isolation Level에 따라 위의 문제들이 발생할 수 있는 여부에 대한 표이다.
각각 발생할 수 있는 문제와 Isolation Level에 대해서 알아보겠다.
Isolation Level 0 - Read Uncommitted
Uncommitted Isolation Level은 아직 Commit되지 않은 데이터를 다른 Transaction에서 읽는 것을 허용하는 레벨이다. 사실상 Transaction이 아니고, All or Nothing이 보장되지 않아 Rollback된 데이터를 조회할 수 있다. 이러한 조건으로 발생할 수 있는 가장 첫번 째 문제는 Dirty Read이다.
Dirty Read
Dirty Read는 아직 commit되지 않은 변경사항을 다른 Transaction에서 읽을 수 있는 것 이다. Transaction 1이 Rollback되어도 Transaction 2는 변경된 값을 읽을 것이며 All or Nothing이 보장되지 않는다. Dirty Read에 대한 문제는 Read Commited Isolation Level에서 해결된다.
Isolation Level 1 - Read Committed
Read Committed Level에서는 Commit된 데이터만 읽을 수 있도록 허용한다. 즉, Dirty Read는 발생하지 않는다. 대부분은 DBMS에서 Read Committed Level을 기본 Isolation Level로 설정하고 구현 방식은 조금씩 다르다. SQL Server나 Sysbase에서는 Lock을 통해서 구현하고 Oracle이나 MySQL은 변경 전 블록에 대한 정보를 기록하는 Undo Log를 통해서 구현한다고 한다.
그런데 Transaction 2번이 끝나지도 않았는데, 같은 데이터에 대해서 조회 했을 때, Transaction 1번의 영향으로 다른 데이터를 반환하는 것이 맞을까? Transaction은 하나의 작업 단위이기 때문에 시작점을 기준으로 항상 같은 결과를 반환해야한다는 의미가 맞을 거라는 의견이다. 이렇게 동일한 Read 쿼리를 여러번 수행 했을 때, 다른 트랜잭션의 영향으로 다른 결과를 반환하는 문제를 Non Repeatable Read라고 한다. 위의 그림의 흐름만 봤을 때는, "이게 뭐가 문제라는거지?" 싶은 생각이들 수도 있다. Non Repeatable Read로 발생할 수 있는 문제를 아래에서 확인해보자.
Non Repeatable Read
결혼 전, 지출을 줄이기 위해 사이 좋은 커플이 생활비 목적으로 통장을 만들어서 함께 사용하고 있다고 가정해보겠다. 이 커플은 우연히 각각 다른 은행 지점에서 현금을 인출하려고한다. B(Transaction 2)가 A(Transaction 1)보다 먼저 현금인출기에서 잔고를 확인하고 인출 작업을 진행하고 있는데, 손이 빠른 A는 B보다 늦게 시작했지만 1분만에 빠르게 4000원을 인출했다. 반면에 B는 현금인출기를 사용하는 방법이 익숙하지 않아서 인출기의 모든 안내를 꼼꼼히 읽어보느라 5000원을 최종적으로 인출하려고하는데 3분 정도 시간이 소요되었다. A는 정상적으로 4천원을 인출했고, B는 인출 가능 금액으로 5000원을 확인했는데, 최종적으로 인출할 때는 실패 오류가 발생되어 패닉에 빠진다.
Non Repeatable Read가 발생하면 위와 같이 사용자에게 좋지 못한 경험을 안겨줄 수 있다. Read Commited Level을 사용해서 Commit된 정보만 보여주도록 했지만, 선행되는 트랜잭션에 대해서 변경작업을 막지 않았기 때문에 이런 문제가 발생할 수 있다. 이러한 일괄성의 위반되는 문제를 Repeatable Read Isolation Level을 사용하면 해결할 수 있다.
Isolation Level 2 - Repeatable Read
위의 커플통장 상황을 이번에는 Repeatable Read Isolation Level에서 재현한 그림이다. 동일하게 B가 먼저 잔고를 확인하고 인출을 시작했고 A가 조금 늦게 시작했지만, 최종적으로 먼저 인출하는 단계에 돌입했다. 그런데 이번에는, Repeatable Read Isolation Level에 따라, B가 잔고를 Read 할 때, Lock을 잡아서 A가 인출하려고할 때 대기하게된다. 그래서 안내사항을 꼼꼼히 읽어본 B는 조금 느리게 진행했지만 결국 5000원을 인출할 수 있게 되었다. A는 5000원 잔고를 확인했지만, B가 먼저 인출을 시작했기 때문에 B가 인출에 성공하고 나서 잔고가 부족하여 인출을 실패했다.
Repeatable Read Isolation Level은 선행되는 트랜잭션이 읽은 데이터가 트랜잭션 종료될 때까지, 후행 트랜잭션이 변경하는 작업을 막음으로써 트랜잭션 안에서 여러번 수행한 Read 쿼리에 대해서 항상 일관성있는 결과를 보장할 수 있다. 즉, Non Repeatable Read가 발생하지 않고 Repeatable Read를 보장한다. 해당 Isolation Level은 따로 제공되어 Lock으로 구현하는 DBMS도 있고 Oracle 처럼 Repeatable Read Level을 제공하지 않아 Select For Update 구문을 사용해서 구현할 수 있다.
Repeatable Read는 Dirty Read와 Non Repeatable Read 일관성 문제를 해결할 수 있지만, 여전히 발생할 수 있는 문제가 남아있다.
Phantom Read
Update나 Delete 처럼 기존에 존재하는 데이터에 대해서는 Repeatable Read에서 Lock을 걸어서 변경할 수 없도록 제어할 수 있다. 하지만 범위 형태로 데이터를 읽을 때는, Insert 되는 데이터에 대해서 막지 않는다. Phantom Read는 범위에 대한 Read Query를 여러번 수행할 때, 없던 데이터가 갑자기 나타나는 현상을 의미한다.
Phantom Read를 방지하는 방법은 마지막 Serializable Isolation Level 통해 가능하다.
Isolation Level 3 - Serializable
우선 모든 DBMS에서 Lock을 통해 Serializable을 구현하는 것은 아니다. 단지 이해를 돕기 위해 그림에서는 Lock으로 표현했다. Repeatable Read의 경우 Select For Update 구문이 Lock을 거는 형태라 그림으로 표현이 가능한데 Serializable은 꼭 위의 그림 형태로 구현되는 것은 아닐 수 있다. Oracle의 경우 Isolation을 Lock으로 구현하지 않는다. Read Committed는 Undo Log를 통해서 구현하고 Serializable은 SCN 번호를 기준으로 Transaction 시작점에 존재했던 데이터만을 대상으로 한다.
여하튼, Lock으로 구현해서 쿼리하는 동안 새로운 데이터가 들어갈 수 없도록 하든, Oracle 처럼 SCN 번호를 사용하든, Serializable Isolation Level은 선행되는 트랜잭션이 읽은 데이터가 트랜잭션 종료될 때까지, 후행 트랜잭션이 변경하는 작업뿐만 아니라 삽입까지 막음으로써 트랜잭션 안에서 여러번 수행한 범위 Read 쿼리에 대해서 항상 일관성있는 결과를 보장한다. 즉, 완벽한 읽기 일관성을 제공하는 레벨이다.
결론
- 데이터베이스에서 Transaction에 대한 일관성을 제어하기 위해 Isolation Level이 존재한다.
- 일관성이 보장될수록 병행성은 떨어진다.
- 비즈니스 로직, 서비스의 요구사항에 따라 Isolation Level을 정한다.
- Isolation Level을 고려하기 전에 Transaction이 꼭 필요한지도 생각해본다.