HomeAbout
DATA
Lock 기반의 DataBase Transaction의 Isolation Level
NASA1515
NASA1515
July 03, 2024
5 min

목차

01
개요
02
Transaction?
03
Transaction Isolation Level

개요

실제로 업무 중에 파이프라인 로직을 개발하며, 데이터 수집 과정에서 Source DB의 transaction 부하 문제가 빈번하게 발생했었는데,
그럴 때 마다, 나는 회사의 다른 DBA가 isolation level의 패턴을 설정을 변경하는 것을 단순히 따라하기만 했지, 근본적으로 왜 그렇게 해야하는지 몰랐다.
그래서 매번 업무를 할 때마다 찝찝함을 남기게 하는 요소가 됐었는데, 이번 기회에 transaction의 isolation level에 대한 근본적인 내용을 정리했다.


Transaction?

트랜잭션(Transaction) : 데이터베이스의 데이터를 조작하는 작업의 단위(unit of work)입니다.
여럿 블로그에서도 은행의 송금을 예로 많이 설명하는데, 아래 과정을 트랙잭션이라고 합니다. (두 과정중에 하나라도 실패한다면, 아예 데이터의 반영이 되지 않아야 하니까요)

  • (1)의 계좌에서 (2)의 계좌로 돈을 송금.
  • (2)의 계좌에서 (1)의 입금을 확인.

일반적으로 이론상 트랜잭션 = ACID 원칙을 보장해야한다 라고 이해하고 있는데, 조금 더 파고 들어서 ACID의 개념을 송금 시스템을 예시로 정리해보겠습니다.

  • Atomicity(원자성): transaction이 부분 성공하는 일이 없도록 보장. (ex. 송금 계좌의 돈은 인출됐으나, 입금 계좌에 돈이 들어오지 않는 일이 일어나지 않게 보장.)
  • Consistency(일관성): transaction 종료 시 DB의 제약 조건에 맞는 상태를 보장. (ex. 송금 계좌 잔고에 있는 돈만 송금이 가능하게 보장. (-가 될 수는 없다)
  • Isolation(독립성): transaction 진행 중 데이터를 다른 transaction이 간섭하지 못하게 보장. (ex. 트랙잭션이 완료되지 않은 데이터를 다른 Transaction의 개입하지 못함을 보장)
  • Durability(영구성): transaction 성공의 경우, 결과가 영구적으로 적용되는 것을 보장, (ex. 송금/입금이 한번 완료되면, 은행에 장애로 인해서 송금 전 상황으로 돌아가지 않게 보장.

근데 ACID를 전부 만족하면서 DataBase를 운영하기엔, 동시성 처리 성능이 많이 떨어질 수 밖에 없습니다.

따라서 DB 엔진에서는 Isolation Level라고 하는 ACID를 어느정도 포기하면서 동시성을 늘리고자 하는 개념을 제공하고 있습니다.
위에서 언급한 Isolation 원칙을 덜 지키는 Level을 사용하면 다른 문제가 발생할 가능성은 높아지지만, 동시에 더 높은 동시성 처리 성능을 얻을 수 있게 됩니다.
이때, Consistent Read로 DB의 Snapshot 기능을 통해서 현재 값이 아닌 특정 시점의 값을 읽을 수 있기 때문에, Level에 따라 위에서 설명한 각기 다른 성질을 보장하게 됩니다.
각 isolation level에 대해서 정리하기 전, DB 엔진 단에서의 ACID와 동시성 보장을 위해 locking 전략을 사용하는데 그 부분부터 이해하고 넘어가도록 하겠습니다.


DB Lock range

우선 DB의 잠금 범위는 Global Lock, DataBase, Table, Record(Row-level) 등으로 나눌 수 있습니다.
Transaction 포스트에서 잠금 수준을 모두 설명하기 보다는 큰 맥락에서의 정리만 하고 넘어가면 좋을 것 같아, 간단하게 정리해보도록 하겠습니다.

잠금 수준은 큰 맥락에서 Exclusive Lock(x Lock)과 Shared Lock(s Lock)로 구분된다.

가장 기본적인 수준의 Lock인데 보통 Table의 Row Level ~ Record 단위에서 걸리는 Lock 입니다.

  • Shared lock(S lock)Data read에 대한 lock 입니다.
    일반적으로 사용하는 SELECT 쿼리는 lock을 사용않는데, SELECT ... FOR SHARE 등 특정 SELECT 쿼리는 read 작업을 수행할 때 각 row에 S lock을 걸게 됩니다.
  • Exclusive lock(X lock)write에 대한 lock 입니다.
    SELECT ... FOR UPDATEUPDATE, DELETE등의 수정과 관련된 쿼리를 날릴 때 row에 걸리는 lock 입니다.

여기서 궁금한 점!! S lock과 X lock에 규칙이 있을까?

보통 두 가지 종류의 lock에는 아래와 같은 규칙들이 존재합니다.

    1. 여러 Transaction이 동시에 한 Row에 S lock을 걸 수 있습니다. 즉, 여러 Transaction이 동시에 하나의 Row를 읽는 것을 보장합니다.
    1. 이미 S lock이 걸려있는 row는 다른 Transaction에서 X lock을 걸 수 없습니다. 즉, 다른 Transaction이 읽고 있는 데이터를 수정하거나 삭제 할 수 없음을 보장합니다.
    1. X lock이 걸려있는 row에는 다른 Transaction에서 S,X lock 둘다 걸 수 없습니다. 즉, 다른 Transaction이 수정하고 있는 Row에는 다른 동작을 할 수 없음을 보장합니다.

요약하자면, Data를 Read는 여러 Transaction에서 수행이 가능하나, 수정과 관련된 Transaction이 동작중인라면 어떤 쿼리도 접근이 불가능합니다.


Transaction Isolation Level


0단계 Uncommited Read

READ UNCOMMITTED은 SELECT 쿼리를 실행할 때 아직 commit 되지 않은 데이터를 읽어올 수 있습니다.. 예를 들어, 다음과 상황 일어날 수 있습니다.

  • Transaction (1) 에서 nasa1515 row를 삽입.
  • READ UNCOMMITTED transaction (2)가 nasa1515 row를 읽어서 가져간다.
  • Transaction (1)가 rollback되서 nasa1515 row가 사라진다.

위와 같은 경우를 Dirty Read라고 하는데, (2) Transaction은 실제로 DB에 한번도 Commit 되지 않은 (존재한 적 없는) 데이터를 읽은 것입니다.


1단계 commited Read

READ COMMITTED은 commit이 완료된 데이터만 확인되는 수준의 isolation을 보장하는 level 입니다.
동작을 간단하게 설명하면, read operation 동작이 완료되었을 때 마다 DB의 snapshot을 다시 뜨고 저장합니다.
그 덕분에 모든 쿼리 결과가 commit 된 이후의 데이터 결과를 읽을 수 있습니다.
왜? 이런 당연한 상황에 대한 Level이 있는거지?? 라는 의구심이 들거나, Select 쿼리마다 Snapshot을 다시 뜨고 저장을 왜 하는거야?? 라고 생각이 드는데.
이 경우는 위의 (Uncommited Read)로 설명이 가능합니다. 즉, 실제 DB에는 아직 Commit 되지 않은 경우의 쿼리도 적용된 상태이기 때문입니다.
따라서 1단계 Level에서는 여러 Read 동작 과정 중에 Commit 된 데이터만을 읽어오기 위해서 Commit 되지 않은 데이터들을 솎아 내는 과정이 필요하게 되고,
이 과정에서 Consistent read를 수행해야합니다. 간단하게 요약하면 아래와 같이 정리가 가능하겠네요.

  • 한 Transaction에서 Read 할 때 마다 Snapshot을 새로 뜬다. 그래서 Commit 후의 데이터 결과만을 읽을 수 있다.
  • Unrepeatable Read : 그러나 한 Transaction의 SELECT 결과는 동일하지 않다. 두번의 Select에서 그 사이의 다른 트랜잭션의 Commit으로 데이터 변화 있다면 결과가 다를 수 있다.

2단계 Repeatable Read

o 반복해서 여러번 read operation을 수행해도, 항상 동일 값을 읽는 것을 보장하는 level 입니다.
처음으로 read(SELECT) operation 시점의 Transaction ID를 기록해놓고, 그 이후 read operation 마다 해당 시점을 기준의 데이터를 읽어오는 consistent read를 수행합니다.
결과적으론 다른 transaction의 결과가 commit 되더라도 새롭게 commit 된 데이터는 조회할 수 없습니다. (처음 read 기준의 snapshot을 뜨고 이후는 뜨지 않기 때문)
이 특성을 간단하게 요약하면 아래와 같이 정리가 가능할 것 같습니다.

  • Transaction의 처음 Read Operation 기준으로 Snapsht을 뜨고 그 뒤의 Snapshot은 뜨지 않는다. (일관성 보장)
  • 일관성을 보장하지만, 다른 Transaction의 결과를 무시하기 때문에, 현재의 데이터가 아닌 과거의 데이터 결과를 반환할 가능성이 높습니다.
  • 일관성을 보장하기 위해서, 한 Transaction에서 SELECT를 실행 시 대상 Record 단에 Shared Lock이 발생합니다.

3단계 Serializable

SERIALIZABLE transaction은 REPEATABLE READ와 동작이 동일 합니다. 다만, SELECT 쿼리가 전부 SELECT ... FOR SHARE로 자동 변환 됩니다.
즉, 여러 Transaction이 동일한 Record에 접근할 수 없게되고, 어떤 데이터의 부정합 문제가 발생하지 않습니다, 다만 Transaction이 Serializable하게 처리되야 하므로 동시성 처리 성능이 떨어지게 됩니다.
아까 위에서도 설명했듯이, 이 경우 Repeatable Read에서 발생하는 몇가지 문제들을 방지할 수 있는데, 정말 쉽게 정리해주신 블로그가 있어서 아래의 예로 설명을 하겠습니다.

1 (A-1) SELECT state FROM account WHERE id = 1;
2 (B-1) SELECT state FROM account WHERE id = 1;
3 (B-2) UPDATE account SET state = ‘rich’, money = money * 1000 WHERE id = 1;
4 (B-3) COMMIT;
5 (A-2) UPDATE account SET state = ‘rich’, money = money * 1000 WHERE id = 1;
6 (A-3) COMMIT;

(출처 : Ditto 님 블로그)

우선, 위의 쿼리의 동작을 하나씩 나열해서 설명하면 다음과 같습니다.

  • (A-1)번 SELECT 쿼리가 SELECT … FOR SHARE로 변경되면서 id = 1 인 row에 S lock이 걸리게됩니다.
  • 이후 동일하게 (B-1)번의 SELECT 쿼리도 역시 FOR SHARE로 변경되면서 id = 1인 row에 S lock을 걸게 됩니다.
  • 이 상황에서 transaction A와 B가 각각 2번 UPDATE 쿼리를 실행하려고 하면 해당 row에 X lock을 걸려는 시도를 하게 됩니다.
  • 하지만 이미 해당 row에는 S lock이 걸려있는 상태입니다.
  • 따라서 Transaction 들은 deadlock 상황에 걸리게 됩니다.
  • 결과적으로는 두개의 transaction은 deadlock으로 인해 모두 timeout 실패로 처리될 것이고. Update는 반영되지 않은 채 money 값은 1로 남게됩니다.

Deadlock(데드락)

여기서 Deadlock을 설명하고 넘어가자면, 위처럼 A라는 Transaction으로 lock이 걸려있는 Row를, B Transaction에서 쓰기를 시도하면 발생하는 현상입니다.
버그가 아니고, 단지 동시성을 보장하기 위해 발생되는 현상일 뿐입니다. 보통의 경우 DeadLock을 방지하기 위해서 Read-Commit 방식의 Level을 사용하는 것으로 권장합니다.

위와 같은 예로 알아봤듯이, SERIALIZABLE Level에서는 데이터의 일관성을 안전하게 보장하지만, 굉장히 간단하게 DEAD LOCK에 걸리게 됩니다. (조심해야 합니다.)
참고로, 위에서 설명한 4가지의 isolation Level은 모두 Auto Commit 기능이 False인 전제에서만 발생합니다.


Serializable을 사용하지 않았을 때 Update, Delete는 데이터 부정합을 보장하나?

여기서 중요하게 기억해야 하는 부분은, Select 쿼리와는 다르게, Update, Delete 쿼리는 Consistent read의 적용이 불가능하다는 것입니다.
어떤 의미일까요? 만약 같은 Where 조건문으로 데이터를 가져온다고 하더라도, Select 로 읽어온 Row Data를 수정하기 위해 Update쿼리를 실행시키게 되면
우리가 읽어온 Row Data와 실제로 Update가 적용 되는 Row는 서로 상이할 수 있다는 것이죠.

분명 위에선 이런 얘기는 없었잖아!!!…조금 더 자세히 REPEATABLE READ의 예시로 이해해보겠습니다.

  • A Transaction - READ COMMITTED
(1) SELECT COUNT(name) FROM table WHERE name = 'nasa1515';
(2) DELETE FROM table WHERE name = 'nasa1515';
(3) COMMIT;
  • B Transaction - READ COMMITTED
(Transaction B - READ COMMITTED)
(1) INSERT INTO table(name, value) VALUES('nasa1515', 15), ('nasa1515', 16), ('nasa1515', 17);
(2) COMMIT;

(A)쿼리는 간단하게 where 절로 특정 Name의 Count를 집계하는 쿼리 이후에, 해당 데이터들을 삭제하는 쿼리이고, (B)쿼리는 새로운 데이터를 넣는 간단한 쿼리입니다.
위의 (A,B) Transaction이 아래와 같이 실행된다는 가정에서 설명을 이어 나가겠습니다.

(A-1) SELECT COUNT(name) FROM table WHERE name = 'nasa1515';
(B-1) INSERT INTO table(name, value) VALUES('nasa1515', 15), ('nasa1515', 16), ('nasa1515', 17);
(B-2) COMMIT;
(A-2) DELETE FROM table WHERE name = 'nasa1515';
(A-3) COMMIT;
  • (A-1)에서 테이블 데이터가 없다면 값은 0으로 나올것입니다.
    이때 실행했던 Select 쿼리는 Serializable와는 다른 Level 이기에 Non-locking으로 동작, Row에 Lock 걸리지 않습니다.
  • 때문에 Transaction B는 자유롭게 동일한 조건에 Data Value의 삽입이 가능하게 됩니다.
  • 따라서 가장 (A-2)에서는 분명히 초기에 조회했던 nasa1515 User는 0명이었지만, (B-1)에서 새로 삽입된 3개의 Row Data가 삭제되게 됩니다.

만약 위처럼 consistent read에서는 확인할 수 없는 데이터에 UPDATEDELETE의 영향을 받은 경우, 해당 시점 이후로는 해당 row가 transaction에서 보이기 시작합니다.

결과론적으로는 Isolation Level에 대한 내용을 정리하면 다음과 같습니다.

  • READ UNCOMMITTED는 부정합 문제가 지나치게 발생하게되고.
  • SERIALIZABLE은 동시성이 상당히 떨어지게 됩니다.
  • 즉, 요약해서 실제 개발 환경에서는 READ COMMITTED 또는 REPEATABLE READ를 사용이 효율적입니다. (보통 Mysql, Oraclel의 기본 격리 레벨이 둘입니다.)
    • 가령 예를 들면 row를 읽기만 할 때는 REPEATABLE READ로, row를 삽입/수정/삭제할 때는 SERIALIZABLE로 isolation level을 지정하는 방식으로요.

Tags

#DATA#DataBase
NASA1515

NASA1515

Data Engineer

Hello I'M Wonseok aka NASA1515

Expertise

Public Cloud
k8s/Docker
Python

Social Media

instagramwebsitelinkedingithub

Related Posts

Star Schema, Snow Flake 모델링과 데이터 정규화
Star Schema, Snow Flake 모델링과 데이터 정규화
2024-07-02
3 min

Topics

CloudDevelop

Social Media