본문으로 건너뛰기

트랜잭션 격리 수준 분석: Prisma를 활용한 실습 가이드

 · reading-time-plural · 

트랜잭션 격리 수준Prisma 를 통해서 실제로 어떻게 동작하는지 확인해 보자!

개요

RDBMS 에서 중요한 개념 으로 트랜잭션 이 있습니다.

트랜잭션데이터베이스무결성 을 유지하는 데 중요한 역할을 하며, 복잡한 데이터 작업을 안전하고 일관되게 처리하는 데 사용될 수 있습니다.

근데 무심코 사용하던 트랜잭션 에도 격리 수준(isolation level) 이 있다는 것을 아시나요? 저는 동시성 이 있는 데이터를 어떻게 안전하게 처리할 수 있는지, Lock 말고는 방법이 없는지 찾아보다가 알게 되었습니다.

격리 수준 을 이용하여 성능데이터 일관성 사이의 트레이드 오프 를 다룰 수 있습니다.

높은 격리 수준 은 더 일관된 데이터를 제공하지만 동시성저하 될 수 있고, 낮은 격리 수준더 높은 동시성 을 제공하지만 데이터 무결성 문제 가 발생할 수 있습니다.

그래서 각 격리 수준 을 이해하고 사용할 수 있어야 하는데 이론 도 중요하지만 실제로 어떻게 동작되는지를 보고 싶었습니다.

이에 따라 각 격리 수준 에 대한 실제 동작을 TS + Prisma 를 통해 만들어 보았습니다.

격리 수준

수행 동작을 보기 앞서 SQL 표준 에서 정의한 주요 격리 수준 의 동작에 대해서 정리하고 가겠습니다.

DBMSSQL 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 트랜잭션 시작하자마다 쿼리를 수행하니 해당 rowlock 이 걸린 것이고 그래서 비동기 작업에서 해당 row 에 대해서 계속 수정을 하고 있지 않아서 대기 중이었던 것으로 알게 되었습니다.

트랜잭션 이 시작되자마자 트랜잭션 이 끝날 때까지 락이 걸리는 것이 아니었다

트랜잭션 이 시작되면 해당 세션 동안은 격리 수준 에 맞게 과 같은 동작이 되는 줄 알았습니다.

근데 실제로 그렇지 않았고 조회 를 해야지 해당 row 에 대해서 이 걸리더군요.

생각을 해보니 당연한 원리 같긴 합니다. 제가 이전에 생각한 대로 이면 트랜잭션 시작하자마다 모든 Table lock 이랑 다를 게 없기 때문입니다.

해당 동작은 Repeatable Read 일 때와 아닐 때의 동작이 어떻게 다른지 확인하면 차이를 알 수 있을 것입니다.

Read Committed 인 경우 조회 시 Commit 를 바라봐야 하는 트랜잭션 이 끝날 때까지 대기한다

Read Committed 인 경우 조회 시 Commit 이전으로 가져오는 줄 알았는데 Commit 를 바라봐야 하는 트랜잭션이 끝날 때까지 대기하는 것을 볼 수 있었습니다.

근데 해당 동작은 DBMS 혹은 ORM Client 마다 다를 수 있다는 생각이 들었습니다.

무한정 대기하기에는 성능 이슈 가 크고 굳이 기다리지 않고 내부적으로 처리할 수 있는 방법이 있어 보이기 때문입니다.

SerializableSelect테이블 단위 가 아닌 데이터 에 대해서만 생성을 포함한 변경을 막는다

Serializable 격리 수준인 경우 Select 시 해당 테이블에 대해서 모두 생성 이 불가능한 것으로 알고 있었습니다.

하지만, 실제로 해보니 그렇지 않았으며 Select 한 데이터에 대해서만 수정 이 안 되는 것이며 만약, select * from 으로 모두 Select 한다면 해당 테이블에 대해서 생성이 불가능하도록 바뀌는 것이었습니다.

고로, Repeatable Read 와 큰 차이는

  • Repeatable Read 은 Select 여부와 무관하게 생성된 데이터가 조회된다는 것이고
  • Serializable 는 Select 한 데이터에 대해서는 다시 조회하더라도 Phantom reads 가 발생하지 않도록 해줍니다.
  • 사실 Select all 말고 든 SerializableRepeatable Read 의 차이를 증명할 수 있나 싶습니다.

Repeatable ReadSerializable 의 차이

Repeatable ReadSerializable 과는 달리 팬텀 읽기 를 방지하지 않습니다.

Repeatable Read 는 새로운 데이터의 삽입은 막지 않습니다. 그래서 팬텀 읽기 가 발생할 수 있습니다.

트랜잭션 격리 수준은 읽기와 관련이 있다

SQL Serverlearn.microsoft.com의 SQL Server 문서 를 알게 된 것으로 Read CommittedRepeatable Readrow 단위 락이라고 나와서 명확하게 설명해 줘서 좋았습니다.

마무리

이론 이 아닌 실제 격리 수준 에 따른 동작을 보니 더 이해도 쉬워지고 오해하고 몰랐던 부분도 알게 되어서 좋았습니다.

트랜잭션 을 꼭 롤백 단위로만 생각했는데 이번에 배우면서 그런 것이 아니라는 것을 느낄 수 있었고 트랜잭션의 ACID 에 대해서 생각해 보고 공부해야겠다는 생각을 해볼 수 있었습니다.

그리고 트랜잭션 in 트랜잭션 이면 또 여러 상황을 고려해야 하므로 알아야 할 것이 많습니다.

저와 같은 가려움을 가지고 계셨던 분들이 해당 들을 통해 시원해졌으면 좋겠습니다.

읽어주셔서 감사합니다.