본문 바로가기
CS/데이터베이스

트랜잭션 격리 수준

by LDY3838 2024. 3. 18.
반응형

트랜잭션이 가져야 하는 특성으로는 ACID가 존재합니다. 우선 이에 대해서 간략히 살펴보도록 하겠습니다.

A(Atomicity)

원자성을 의미합니다. All or Nothing으로 하나의 트랜잭션은 내부에서 하나의 실패라도 발생한다면 전부 실패한 것으로 보고 트랜잭션이 시작하기 이전의 상태로 복구해야 합니다.

Spring에서 java를 이용하여 트랜잭션을 진행할 때 RuntimeException이 발생하거나 rollbackFor 옵션을 주어서 해당 Exception이 발생했을 때 commit을 하지 않고 rollback을 진행하는 것이 이러한 특성을 지키기 위한 것입니다.

C(Consistency)

일관성을 의미합니다. 트랜잭션에 의해서 데이터의 일관적인 상태가 바뀌면 안 된다는 것을 의미합니다. 데이터 무결성과 관련된 속성으로 기존의 데이터가 Integer 형식이었는데 트랜잭션이 끝난 후 해당 column에 해당하는 값이 String과 같이 변하면 안 된다는 것을 의미합니다.

이 외에도 유저가 회원탈퇴를 했을 때 전체 유저의 수를 기록하는 다른 테이블의 column이 있다고 한다면 유저의 수가 줄어듦에 따라서 전체 유저의 수를 기록하는 column의 값도 감소해야 합니다. 위와 같은 내용을 잘 지켜야 한다는 속성이 일관성에 해당합니다.

I(Isolation)

고립성에 해당합니다. 이 포스트에서 아래에 자세히 다룰 내용으로 트랜잭션은 다른 트랜잭션들과 서로 영향을 주고 받지 않고 고립되어 있어야 한다는 의미입니다.

게시판에 유저 2명이 동시에 글을 게시할 때 어느 한 사람의 게시글이 다른 사람의 게시글에 의해서 영향을 받아 제대로 게시가 되지 않거나 상태가 변한다면 Isolation이 제대로 지켜지지 않는 예시입니다.

트랜잭션의 고립성을 지키기 위해서 트랜잭션 격리 수준은 read uncommitted, read committed, repeatable read, serializable로 나누어 적용할 수 있습니다.

D(Durability)

지속성에 해당합니다. 트랜잭션이 끝난 후 결과는 지속적으로 반영되어야 한다는 것을 의미합니다. 이를 위하여 DB에 변경값을 반영하고 이후 시스템 장애가 발생하여 데이터가 사라지더라고 이를 다시 복구할 수 있도록 덤프, 즉 복사본 데이터베이스를 만들어두거나 로그(redo, undo)를 만들어 두는 방식을 사용합니다.


이제 위에서 설명드린 Isolation에 해당하는 트랜잭션 격리 수준에 대해서 알아보도록 하겠습니다.

트랜잭션의 격리 수준은 read uncommitted, read committed, repeatable read, serializable이 존재합니다. 뒤로 갈수록 격리 수준이 높아지고 앞 즉 read uncommitted 쪽으로 갈수록 격리 수준이 낮아집니다. 이에 대해서 하나씩 알아보도록 하겠습니다.


Read Uncommitted

이 격리 수준에 대해서 알아보기 위해서 아래와 같은 테이블이 있다고 가정하겠습니다.

이제 해당 테이블에 2개의 트랜잭션이 각각 접근해보도록 하겠습니다.

TID 5번에 해당하는 화살표가 먼저 쿼리를 날렸다는 것을 표현하기 위해서 TID가 10인 화살표가 테이블에 더 아래에 존재하게 하였습니다. 화살표가 더 아래에 존재하는 경우 더 늦게 실행된 쿼리로 생각하시면 좋을 거 같습니다. TID는 트랜잭션 id를 의미하고 TID가 더 빠르면 더 먼저 생성된 트랜잭션으로 이해해 주시면 좋을 거 같습니다. TID가 다른 경우 서로 다른 트랜잭션을 의미합니다.

위와 같은 쿼리를 날렸을 때 결과는 위에 보이는 사진에 있는 result와 같습니다. select만 했을 때는 사실 순서가 중요하지 않습니다. 하지만 update와 같은 쿼리를 날렸을 때는 순서가 중요하게 됩니다.

위와 같은 상황은 TID가 10인 트랜잭션에서 select 쿼리를 날렸는데 TID가 5번인 트랜잭션이 테이블을 update하고 이후 TID가 10인 트랜잭션이 똑같은 쿼리를 날렸을 때 다른 결과가 나온 모습입니다.

read uncommitted 격리 수준에서는 TID가 5인 트랜잭션이 commit을 하지 않더라도 변경한 내용이 다른 트랜잭션에서 보이게 됩니다. 따라서 위와 같은 상황이 발생하는 것으로 해당 트랜잭션에서 commit을 하지 않았는데 수정 내용이 다른 트랜잭션에서 보이는 문제를 dirty read라고 합니다.

dirty read가 문제가 되는 이유는 아래와 같습니다.

위의 상황은 TID가 update로 값을 수정했으나 commit하지 않은 경우입니다. 위와 같은 경우에서 TID가 10인 트랜잭션은 select를 할 때마다 값이 계속 달라지는 문제가 발생합니다. 이는 데이터의 정합성을 신뢰할 수 없는 큰 문제입니다. 따라서 read uncommitted는 거의 사용하지 않습니다.

참고로 read uncommitted에서는 아래에서 설명드릴 non repeatable read, phantom read와 같은 문제가 발생합니다.


Read Committed

이제 read committed 격리 수준을 살펴보도록 하겠습니다. 해당 격리 수준은 read uncommitted에 비해서 격리 수준이 강화된 것으로 commit이 완료된 결과만 읽게 됩니다.

해당 격리 수준에서는 undo라는 log를 이용합니다. undo는 테이블에서 변경이 일어났을 때 변경이 일어나기 전의 데이터를 저장한다고 생각하시면 됩니다. 이와 관련해서는 이 격리 수준에 대해서 알아보면서 더 설명드리도록 하겠습니다.

undo와 같이 사용하는 log에는 redo가 있는데 undo는 변경이 발생했을 때 변경 이전의 데이터를 저장한다고 할 때 redo는 변경 이후의 데이터를 저장하고 있습니다.

undo 로그를 이용하기 위해서 이제 테이블에 해당 row를 쓴 트랜잭션의 ID를 같이 저장합니다.

위의 그림이 read committed를 잘 나타내는 그림이라고 생각합니다. TID가 5인 트랜잭션에서는 id가 2인 row의 name을 남으로 변경합니다. 이때 TID가 5인 트랜잭션에서 변경 사항을 commit 하지 않는 경우에는 TID가 10인 트랜잭션에서 select 쿼리를 날렸을 때 member 테이블이 아니라 undo에 있는 데이터를 가져옵니다. 이 이유는 commit을 해야만 변경 사항이 member 테이블에 완전히 반영되기 때문입니다.

이때 undo에 TID가 2, ID가 2, name이 현으로 저장된 이유는 변화가 일어나기 전의 상태에 대한 로그가 undo 로그에 저장되기 때문입니다.

위와 같이 read committed 격리 수준을 이용하면 read uncommitted 격리 수준에서 발생하는 문제였던 dirty read는 해결이 되었습니다.

하지만 모든 문제가 해결된 것은 아닙니다. 위의 그림에서는 똑같은 row에 대한 select 쿼리가 연속으로 DB에 날아갔습니다. 하지만 결과는 '현'에서 '남'으로 변경되었습니다. 이는 트랜잭션이 다른 트랜잭션의 실행에 따라서 영향을 받는 모습입니다. 위와 같이 같은 row 들에 대한 select를 했는데 결과가 달라지는 문제는 non repeatable read라고 합니다.


Repeatable Read

그럼 위에서 설명드린 read committed 격리 수준에서도 계속 발생하는 non repeatable read를 해결하기 위해서는 어떻게 해야 하는지 알아보도록 하겠습니다. 이 문제를 해결한 격리 수준이 repeatable read입니다.

위의 그림이 repeatable read의 격리 수준입니다. 왼쪽에서 실행되고 있던 트랜잭션의 TID를 15로 변경해주었습니다. 위에서 설명했을 때도 사실 먼저 시작한 트랜잭션의 ID가 더 작은 값을 가지기 때문에 우측의 트랜잭션의 아이디가 더 작은 상황이 맞습니다.

다시 repeatable read의 격리 수준에 대해서 말씀드리도록 하겠습니다. repeatable read에서는 read committed와 비슷하지만 해당 row를 작성한 TID를 확인해 줍니다. TID를 확인한 후 해당 row의 TID가 자신의 TID보다 크다면 해당 트랜잭션이 시작된 후 이 값이 변경된 것이기 때문에 undo 로그를 확인하여 변경되기 이전의 값을 가져오게 됩니다.

위와 같이 여러 버전의 데이터가 한번에 존재한다고 하여 MVCC(Multi Version Concurrency Control)라고도 부릅니다.

위와 같이 TID를 확인하여 자신의 트랜잭션 이전의 값만을 확인하는 방식으로 non repeatable read를 해결하였습니다.

하지만 해당 격리 수준에서도 phantom read라는 문제를 해결하지 못하였습니다. 하지만 MySQL InnoDB의 경우 gap lock을 이용하여 이를 해결하였습니다.

우선 phantom read에 대해서 알아보도록 하겠습니다.

위의 그림과 같이 repeatable read의 격리 수준에서 다른 트랜잭션에서 member 테이블에 값을 추가했을 때 select를 하는 경우 TID를 확인 후 undo에서 값을 가져오면 되기 때문에 기존의 결과와 같은 결과가 나옵니다.

하지만 위의 사진에서 아래의 쿼리와 같이 Lock을 걸게 되면 이처럼 undo 로그의 값을 가져오는 것이 아니라 테이블에 잠금을 해야 하기 때문에 추가로 row가 검색됩니다.

위와 같이 row를 가져왔을 때 기존에 없던 row가 추가로 검색되는 상황을 phantom read라고 합니다. phantom read는 위처럼 for update를 이용하여 X-lock을 걸거나 for share, lock in share mode와 같이 S-lock을 거는 경우에 발생합니다.

이를 막기 위해서 MySQL InnoDB에서 사용하는 개념이 gap lock인데 이에 대해서 알아보도록 하겠습니다.

gap lock은 위와 같이 범위 질의를 하였을 때 row가 없는 곳에도 잠금을 거는 것을 의미합니다. 위의 그림을 보면 ID가 2보다 크거나 같은 row 들에 대해서 X-lock을 얻는 쿼리가 진행됩니다. 이와 같은 쿼리가 나오는 경우 id가 2, 3이 있는 row에는 해당 row에 걸리는 record lock이 걸립니다.

하지만 id >=  2에 해당하는 부분은 위에서 말한 id가 2, 3인 경우 뿐만이 아니라 id가 4 이상인 경우도 있을 것입니다. 이와 같이 비어있는 공간에도 lock을 걸어주는 개념이 gap lock입니다. 보통 record lock과 gap lock을 합쳐서 next key lock이라고 부릅니다.

이 lock은 commit 또는 rollback을 한 이후 풀리기 때문에 위의 그림에서 commit을 해준 이후 gap lock이 풀린 모습을 볼 수 있습니다. commit 이후의 select 쿼리는 TID가 10인 트랜잭션이 아니라 새로운 트랜잭션에서 진행한 것으로 이해하셔도 좋을 것 같습니다. lock을 거는 범위를 줄이기 위해서 index에 대해서도 생각해야 하지만 이와 관련해서는 다른 포스트에서 다루도록 하겠습니다.


Serializable

serializable 격리 수준은 select가 진행되는 모든 record에 대해서 lock을 걸고 해당 트랜잭션이 끝나면 다른 트랜잭션을 허용하겠다는 방식입니다.

이 방식에서는 어떠한 데이터 부정합 문제도 발생하지 않으나 순차적으로 트랜잭션이 진행되기 때문에 성능이 매우 좋지 않습니다. 따라서 매우 높은 수준의 안정성이 필요하지 않으면 잘 사용하지 않는 것이 좋습니다.

 

참고 자료

https://mangkyu.tistory.com/299

 

[MySQL] 트랜잭션의 격리 수준(Isolation Level)에 대해 쉽고 완벽하게 이해하기

이번에는 트랜잭션 격리 수준(Isolation Level)에 대해 알아보도록 하겠습니다. 아래의 내용은 RealMySQL과 MySQL 공식 문서 등을 참고하여 작성하였으며, 모든 내용은 InnoDB를 기준으로 설명합니다. 해당

mangkyu.tistory.com

https://loosie.tistory.com/527

 

[DB] REDO와 UNDO 동작 과정을 이해해보자 (지속성을 구현하기 위해)

REDO와 UNDO를 배워야 하는 이유 트랜잭션이 가지고 있어야 하는 특성에 'ACID'라는 것이 있다. 이 특성을 구현하기 위해서 REDO와 UNDO는 빠질 수 없으므로 이들을 배울 필요가 있다. REDO와 UNDO에 대한

loosie.tistory.com

 

반응형

댓글