트랜잭션 격리 수준
을 Prisma
를 통해서 실제로 어떻게 동작하는지 확인해 보자!
개요
RDBMS
에서 중요한 개념
으로 트랜잭션
이 있습니다.
트랜잭션
은 데이터베이스
의 무결성
을 유지하는 데 중요한 역할을 하며, 복잡한 데이터 작업을 안전하고 일관되게 처리하는 데 사용될 수 있습니다.
근데 무심코 사용하던 트랜잭션
에도 격리 수준(isolation level)
이 있다는 것을 아시나요? 저는 동시성
이 있는 데이터를 어떻게 안전하게 처리할 수 있는지, Lock
말고는 방법이 없는지 찾아보다가 알게 되었습니다.
이 격리 수준
을 이용하여 성능
과 데이터 일관성
사이의 트레이드 오프
를 다룰 수 있습니다.
높은 격리 수준
은 더 일관된 데이터를 제공하지만 동시성
이 저하
될 수 있고, 낮은 격리 수준
은 더 높은 동시성
을 제공하지만 데이터 무결성 문제
가 발생할 수 있습니다.
그래서 각 격리 수준
을 이해하고 사용할 수 있어야 하는데 이론
도 중요하지만 실제로 어떻게 동작되는지를 보고 싶었습니다.
이에 따라 각 격리 수준
에 대한 실제 동작을 TS
+ Prisma
를 통해 만들어 보았습니다.
격리 수준
수행 동작을 보기 앞서 SQL 표준
에서 정의한 주요 격리 수준
의 동작에 대해서 정리하고 가겠습니다.
DBMS
는 SQL Server
(MS SQL) 을 기준으로 작성했습니다.
Read Uncommitted
가장 낮은
격리 수준으로, 다른 트랜잭션에서 커밋 되지 않은 변경사항을 읽을 수 있습니다.
이 수준에서는 Dirty Read
가 발생할 수 있습니다.
Read Committed
대부분의 데이터베이스 시스템에서 기본으로 사용되는 격리 수준입니다.
커밋 된 데이터만 읽을 수 있으므로 Dirty Read
는 방지되지만, Non-Repeatable Read
는 발생할 수 있습니다.
Repeatable Read
한 트랜잭션이 시작된 후 읽은 데이터는 트랜잭션이 종료될 때까지 다른 트랜잭션에 의해 변경되지 않습니다.
이로 인해 Non-Repeatable Read
는 방지되지만, Phantom Read
는 여전히 발생할 수 있습니다.
Serializable
가장 높은 격리 수준으로, 트랜잭션이 마치 순차적으로 실행되는 것처럼 보장합니다.
이 수준에서는 모든 읽기 문제
가 방지
되지만, 성능 저하
의 가능성이 가장 높습니다.
격리 수준에 따라 발생할 수 있는 문제
더티 리드 (Dirty Read)
특정 트랜잭션
에 의해 데이터가 변경되었지만, 아직 커밋 되지 않은 상황에서 다른 트랜잭션이 해당 변경 사항을 조회할 수 있는 문제를 말합니다.
이 문제는 트랜잭션 A
가 데이터를 변경하고 커밋 하지 않은 시점에 트랜잭션 B
가 변경된 데이터를 읽어온 상황에서, 트랜잭션 A
가 변경 내용을 커밋 하지 않고 롤백
한 상황에서 치명적입니다.
이렇게 되면 트랜잭션 B
는 무효가 된 값
을 읽고 처리하므로 문제가 발생합니다.
반복 불가능한 조회 (Non-Repeatable Read)
같은 트랜잭션 내에서 같은 데이터를 여러 번 조회했을 때 읽어온 데이터가 다른 경우를 의미합니다.
한 트랜잭션이 동일한 데이터를 여러 번 조회했을 때, 각 조회 사이에 다른 트랜잭션이 데이터를 변경하면 일관성 없는 결과를 얻게 되는데 이는 특히 데이터 분석이나 보고서 생성과 같은 작업에서 큰 문제가 될 수 있습니다.
이는 읽은 데이터에 대해서 수정(삭제 포함)을 막지 않기 때문에 발생하며 트랜잭션 세션 안에서 컨디션에 따라 조회된 데이터가 다를 수 있음으로 레이스 컨디션이 발생할 수 있습니다.
가상 읽기 (Phantom reads, 팬텀 리드)
Repeatable Read
격리 수준인 경우 읽은 데이터에 대해서는 데이터 변경(삭제 포함)
을 방지
하지만 새로운 데이터의 삽입
으로 인한 문제가 발생할 수 있습니다.
ChatGPT
의 말을 인용하자면 아래와 같습니다.
"Phantom Reads"는 데이터베이스 트랜잭션에서 발생할 수 있는 문제 중 하나입니다. 이 문제는 일반적으로 트랜잭션이 동일한 쿼리를 두 번 실행할 때, 첫 번째 쿼리 실행 후 두 번째 쿼리 실행 전에 다른 트랜잭션이 새로운 레코드를 삽입하거나 삭제하는 경우 발생합니다.
예를 들어, 트랜잭션이 데이터베이스에서 특정 조건에 맞는 레코드를 모두 찾는 쿼리를 실행한다고 가정해 보겠습니다. 이 트랜잭션이 처음에는 100개의 레코드를 찾았다고 합시다. 그런데 이 트랜잭션이 동일한 쿼리를 다시 실행하면, 동일한 조건에 따라 이번에는 101개의 레코드를 찾을 수 있습니다. 이 새로운 레코드(즉, "phantom" 레코드)는 첫 번째 쿼리 실행 후 두 번째 쿼리 실행 전에 다른 트랜잭션에 의해 삽입되었습니다.
이런 현상은 데이터베이스에서 읽기 일관성(read consistency)을 유지하는 데 문제를 일으킬 수 있습니다. 트랜잭션은 일관성 있는 데이터 집합을 바탕으로 작업을 수행하려고 하는데, 이러한 "phantom" 레코드 때문에 트랜잭션의 결과가 예기치 않게 변할 수 있기 때문입니다.
실습: 격리 수준 차이 확인 ✨
코드는 parkgang/prisma-transaction-isolation-level-examples 에 존재합니다.
각각의 격리 수준
에 대한 차이를 느끼려면 낮은 격리 수준
에서 높은 격리 수준
으로 올라갈 때 어떤 현상하는지 보면 됩니다.
그래서 샘플
도 각각의 격리 수준
에 따라서 발생한 문제에 대해서 파일 단위
로 분리했으니 편하게 살펴보실 수 있습니다.
info
아참, 그리고 RDBMS
마다 격리 수준
이 조금씩 다르니 이점을 유의해서 봐주세요.
ORM
을 사용하니까 Docker Image
만 바꾸면 손쉽게 테스트해 보실 수 있을 겁니다. ✌️
Insight
Repeatable Read
걸리면 트랜잭션
이 끝날 때까지 다른 작업이 대기함
Repeatable Read
격리 수준의 트랜잭션이 시작하자마자 Select
쿼리를 넣으니까 동작이 달라지는 것을 확인했습니다.
원래는 비동기 작업
에서 쿼리 시작 후 바로 수행이 완료되었다는 console.log
가 찍혔는데 Repeatable Read
트랜잭션 시작하자마자 Select
쿼리를 추가하니 비동기 작업의 쿼리가 Repeatable Read
의 트랜잭션이 끝날 때까지 수행이 안되고 있는 것을 볼 수 있었습니다.
생각을 해보니까 읽은
데이터에 대해서 lock
을 걸어서 격리
시킨다는 말이 떠올랐고 Repeatable Read
트랜잭션 시작하자마다 쿼리를 수행하니 해당 row
가 lock
이 걸린 것이고 그래서 비동기 작업에서 해당 row
에 대해서 계속 수정을 하고 있지 않아서 대기 중이었던 것으로 알게 되었습니다.
트랜잭션
이 시작되자마자 트랜잭션
이 끝날 때까지 락이 걸리는 것이 아니었다
트랜잭션
이 시작되면 해당 세션
동안은 격리 수준
에 맞게 락
과 같은 동작이 되는 줄 알았습니다.
근데 실제로 그렇지 않았고 조회
를 해야지 해당 row
에 대해서 락
이 걸리더군요.
생각을 해보니 당연한 원리 같긴 합니다. 제가 이전에 생각한 대로 이면 트랜잭션 시작하자마다 모든 Table lock
이랑 다를 게 없기 때문입니다.
해당 동작은 Repeatable Read
일 때와 아닐 때의 동작이 어떻게 다른지 확인하면 차이를 알 수 있을 것입니다.
Read Committed
인 경우 조회 시 Commit
를 바라봐야 하는 트랜잭션
이 끝날 때까지 대기한다
Read Committed
인 경우 조회 시 Commit
이전으로 가져오는 줄 알았는데 Commit
를 바라봐야 하는 트랜잭션이 끝날 때까지 대기하는 것을 볼 수 있었습니다.
근데 해당 동작은 DBMS
혹은 ORM Client
마다 다를 수 있다는 생각이 들었습니다.
무한정 대기하기에는 성능 이슈
가 크고 굳이 기다리지 않고 내부적으로 처리할 수 있는 방법이 있어 보이기 때문입니다.
Serializable
는 Select
한 테이블 단위
가 아닌 데이터
에 대해서만 생성을 포함한 변경을 막는다
Serializable
격리 수준인 경우 Select
시 해당 테이블에 대해서 모두 생성
이 불가능한 것으로 알고 있었습니다.
하지만, 실제로 해보니 그렇지 않았으며 Select
한 데이터에 대해서만 수정
이 안 되는 것이며 만약, select * from
으로 모두 Select
한다면 해당 테이블에 대해서 생성이 불가능하도록 바뀌는 것이었습니다.
고로, Repeatable Read
와 큰 차이는
Repeatable Read
은 Select 여부와 무관하게 생성된 데이터가 조회된다는 것이고Serializable
는 Select 한 데이터에 대해서는 다시 조회하더라도Phantom reads
가 발생하지 않도록 해줍니다.- 사실
Select all
말고 든Serializable
와Repeatable Read
의 차이를 증명할 수 있나 싶습니다.
Repeatable Read
와 Serializable
의 차이
Repeatable Read
는 Serializable
과는 달리 팬텀 읽기
를 방지하지 않습니다.
Repeatable Read
는 새로운 데이터의 삽입은 막지 않습니다. 그래서 팬텀 읽기
가 발생할 수 있습니다.
트랜잭션 격리 수준은 읽기와 관련이 있다
SQL Server
의 learn.microsoft.com의 SQL Server 문서 를 알게 된 것으로 Read Committed
와 Repeatable Read
는 row
단위 락이라고 나와서 명확하게 설명해 줘서 좋았습니다.
마무리
이론
이 아닌 실제 격리 수준
에 따른 동작을 보니 더 이해도 쉬워지고 오해하고 몰랐던 부분도 알게 되어서 좋았습니다.
트랜잭션
을 꼭 롤백
단위로만 생각했는데 이번에 배우면서 그런 것이 아니라는 것을 느낄 수 있었고 트랜잭션의 ACID
에 대해서 생각해 보고 공부해야겠다는 생각을 해볼 수 있었습니다.
그리고 트랜잭션 in 트랜잭션
이면 또 여러 상황을 고려해야 하므로 알아야 할 것이 많습니다.
저와 같은 가려움을 가지고 계셨던 분들이 해당 들을 통해 시원해졌으면 좋겠습니다.
읽어주셔서 감사합니다.