데이터베이스의 트랜잭션 격리 수준
1️⃣ Read Uncommitted (읽기 미완료)
가장 낮은 격리 수준으로, 하나의 트랜잭션이 아직 커밋되지 않은 데이터임에도 다른 트랜잭션이 읽을 수 있다
이 시점에서는 Dirty Read 라고 하여, B트랜잭션이 아직 커밋되지 않은 A트랜잭션의 데이터를 읽어버렸는데 A트랜잭션이 롤백되었다면 잘못된 데이터를 읽은 것이 되버린다. 이 문제는 Read Committed 단계에서 해결할 수 있다
2️⃣ Read Committed (읽기 완료)
한 트랜잭션이 커밋된 데이터만 읽은 수 있다. 커밋되지 않은 데이터는 다른 트랜잭션이 접근하지 못한다. 그러나 동일 트랜잭션에서 동일 데이터를 두 번 읽을 때, 다른 트랜잭션이 데이터를 수정해버렸다면 읽은 값이 달라져버린다. 이를 Non-repeatable Read 라고 한다. 따라서 해결을 원한다면 Repeatable Read 격리 수준을 이용한다
3️⃣ Repeatable Read (반복 가능 읽기)
트랜잭션이 시작될 때 읽은 데이터는 트랜잭션이 종료될 때까지 변경하지 않는다. 즉, 동일한 데이터를 두 번 읽을 지 언정, 최초에 읽은 데이터 값을 유지하여 읽는다 그러나, 해당 트랜잭션 중간에 다른 트랜잭션이 새로운 데이터 행을 삽입했다고 하자. 새로 생긴 데이터에 대해서는 해결책이 없이 처음엔 없는 줄 알았던 데이터를 읽어버린다 이를 Phantom Read 문제라고 한다.
(그림에서는 트랜잭션3이 마치 DB에 쿼리를 보내지 않는 것처럼 보이지만, 그것은 아니고 [undo영역]에서 데이터를 조회한다)
4️⃣ Serializable (직렬화 가능)
각 트랜잭션이 직렬적으로 실행되는 것을 보장한다. 즉, 병렬처리가 아니라 하나 하나 끝날 때까지 대기를 타는 모양이다. 상호간섭은 완전히 차단할 수 있으나, 성능저하가 심히 우려된다.
또 다른 해결책, MVCC
앞서 본 것과 같이 일일이 다 해결하려고 하니 직렬화 단계까지 오면 성능저하가 걱정된다. 이를 균형적으로 해결하기 위해 MVCC, Multi-Version concurrency Control이 존재한다
이는 데이터를 수정할 때 새로운 버전을 생성하고, 트랜잭션 간 각 다른 버전의 데이터를 읽게 하여 Locking 없이도 트랜잭션의 충돌을 최소화하도록 한다 마치 스냅샷을 생성하여 작업을 진행하는 것인데, 이를 통해 충돌을 최소화하면서도 격리 수준을 보다 높게 설정할 수 있다. 하지만 한 데이터가 여러 버전이 생김으로써 디스크 공간이 증가한다. 이를 또 보완하기위해 Garbage Collection으로 오래된 데이터 버전을 삭제하지만 이 작업 자체도 성능에 영향을 줄 수도 있으니 튜닝이 필요하다. 어쨌거나 Postgre 기준으로 이에 대한 방법을 알아보면 다음과 같다
PostgreSQL의 MVCC
Postgre의 MVCC는 기본적으로 내장되어 있고, 별도 설정 없이도 PostgreSQL에서 자동으로 동작한다. 이를 통해 데이터베이스는 트랜잭션 간의 충돌을 최소화하며 동시에 데이터 일관성을 유지한다. 따라서 특별한 MVCC 활성화 없이도 트랜잭션과 관련된 쿼리에서 이를 활용할 수 있다.
위처럼 xmin, xmax라는 컬럼을 추가로 검색하면 실제로 만든 column이 아님에도 결과값이 추출된다. 이는 고유한 트랜잭션 ID로서, 해당 행이 생성된 트랜잭션ID와 삭제된 트랜잭션 Id를 나타낸다 이 ID를 통해 현재 트랜잭션이 접근 가능한 버전인지에 대한 판단을 하고, 이를 통해 트랜잭션이 끝날 때까지 동일하게 유지할 수 있는 것이다.
가비지 컬렉션
MVCC는 여러 버전의 행을 유지하므로, 시간이 지날수록 불필요한 이전 버전들이 쌓인다. PostgreSQL은 이를 관리하기 위해 Autovaccum 프로세스가 있고, 이를 통해 더 이상 참조되지 않는 행을 Garbage Collection으로 제어한다.
종합적으로 보면, 동시성이 증가하고 일관성도 유지할 수 있어 좋긴 하나 앞선 언급하였듯이 여러 버전을 관리하기 위한 저장 공간에 대한 염려와 가비지 컬렉션에 의한 성능 영향도를 고려해야 하는 복잡성이 있다.