DB

DB 트랜잭션 격리 수준 [ isolation level ]

martial 2022. 4. 20. 20:34

안녕하세요 여러분은 트랜잭션의 격리 수준에 대해 알고계신가요?
그전의 DB 트랜잭션에 대해 잠깐 말하자면

데이터들이 처리되는 작업의 단위입니다.
예를 들어 상점에서 한가지의 물품을 사려합니다. 이때 일어나는 쿼리들을 살펴볼까요?

  • 사용자의 계좌에서 물품의 가격만큼 차감하는 쿼리
  • 상품의 재고가 한개 차감하는 쿼리

이정도로 예를 들어볼게요.
이렇게 쿼리들로 모여진 한 작업의 단위를 트랜잭션이라고 표현합니다.


이제 트랜잭션을 알았다는 가정하에 각설하고 본문을 설명해보겠습니다.

 

그래서 트랜잭션 격리 수준이란게 뭘까?

바로 트랜잭션들끼리 얼마나 고립되어 있는지 그 수준을 레벨로 나눈것입니다.

 

트랜잭션 격리 레벨은

  • Read Uncommitted - 0 단계
  • Read Committed - 1단계
  • Repetable Read - 2단계
  • Serializable - 3단계

으로 이루어져 있습니다.

 

자세히 하나하나 살펴볼게요

 

Read Uncommitted

  • SELECT절이 수행될 때 동안 해당 데이터에 대해 Shared Lock이 작동하지 않습니다.
  • 커밋 되지 않은 트랜잭션들을 읽는것이 허용됩니다.

한가지의 예시를 들어서 설명할게요.

 

멤버들의 나이를 20살로 바꾸는 트랜잭션A가 존재한다.

또한 멤버테이블에는 "마샬"이라는 이름을 가진 멤버가 존재하고 "마샬"의 나이는 19살이다.

또한 멤버의 나이를 조회하는 쿼리가 2개가 존재하는 트랜잭션B가 수행되다가

트랜잭션 A의 개입이 있으면 "마샬"의 나이는 어떻게 조회 될까요?

 

  • 트랜잭션의 수행조건과 순서

트랜잭션A의 첫번째 SELECT 쿼리가 실행되고
트랜잭션B가 수행되어 나이를 20살로 UPDATE
다시 트랜잭션A의 두번째 SELECT 쿼리가 실행되는 로직으로 실행한다고 가정할게요.

트랜잭션B의 결과 트랜잭션A가 정상적으로 COMMIT 되었을때 트랜잭션A가 COMMIT되지 않았을때
트랜잭션B의 첫번째 나이 조회 쿼리 19 19
트랜잭션B의 두번째 나이 조회 쿼리 20 20

어라? 왜 트랜잭션A 가 Commit 되지 않았는데도 두번째 조회에서는 왜 20살로 조회되는거지? 라고 생각하실 수 있어요

바로 이것이 Read Uncommitted의 단점입니다.

바로 Commit을 읽지 않아요 단어에도 써있죠, Commit을 읽지 않기 때문에 Read Uncommitted단계에서는

두번째로 조회했던 리소스에 대해 Commit의 성공여부를 판단하지 않고 조회했던 그대로 반환하기 때문이에요.

 

이런 경우를 통틀어 Dirty Read라고 표현합니다.

또한 Dirty Read가 발생한다면 데이터의 정합성, 일관성등이 보장이 전혀 안되겠죠.

 

그럼 다음 레벨로 가볼게요.

 

 

 

하지만 그전에! Shared Lock에 대해 설명을 해드릴게요.

Shared Lock

Shared Lock이란 공유잠금을 뜻하는 말이에요.
쉽게 말하면 모든 트랜잭션에서 동시에 특정 Row를 조회할 수 있게 할 수 있는 Lock이에요.

이로인해 다른 트랜잭션에서 해당 Row를 수정 할수 없어요. 그 이유는

Shared Lock이 Exclusive Lock을 해당 Row에 설정할 수 없게 만들거든요 (해당 Row가 트랜잭션이 일어나고 있을때)

Exclusive Lock에 대해 궁금하시다면 해당 키워드를 구글링 하시길 바래요!

 

 

 

그럼 이제 다음 레벨 Read Committed로 넘어가 볼게요.

Read Committed

  • SELECT절이 수행될 때 동안 해당 데이터에만 Shared Lock이 작동합니다.
  • 커밋이 된 트랜잭션만 참조를 함.
  • Oracle같은 DBMS에서 Read Committed 격리 레벨을 수용합니다. 

Read Committed는 말 그대로 커밋을 읽습니다.

위에서 들었던 예를 그대로 가져올게요.

 

멤버들의 나이를 20살로 바꾸는 트랜잭션A가 존재한다.

또한 멤버테이블에는 "마샬"이라는 이름을 가진 멤버가 존재하고 "마샬"의 나이는 19살이다.

또한 멤버의 나이를 조회하는 쿼리가 2개가 존재하는 트랜잭션B가 수행되다가

트랜잭션 A의 개입이 있으면 "마샬"의 나이는 어떻게 조회 될까요?

 

  • 트랜잭션의 수행조건과 순서

트랜잭션A의 첫번째 SELECT 쿼리가 실행되고
트랜잭션B가 수행되어 나이를 20살로 UPDATE
다시 트랜잭션A의 두번째 SELECT 쿼리가 실행되는 로직으로 실행한다고 가정할게요.

트랜잭션B의 결과 트랜잭션 A가 정상적으로 COMMIT 되었을때 트랜잭션 A가 COMMIT되지 않았을때
트랜잭션B의 첫번째 나이 조회 쿼리 19 19
트랜잭션B의 두번째 나이 조회 쿼리 20 19

Read Committed 레벨은 커밋을 읽기 때문에 "마샬"의 나이가 20살로 Update 쿼리가 나가고나서 해당 트랜잭션이 Commit에 실패했다면, 두번째 SELECT쿼리에서 해당 UPDATE 연산이 일어났던 트랜잭션에 대해 참조를 하지 않아요.

즉 Read Committed 단계는 Commit을 성공하면 성공한 그 트랜잭션에 대해 참조를 하지만, Commit을 실패하면 해당 트랜잭션은 

지우개로 지운것과 같아요.

 

그렇기 때문에 트랜잭션A가 Commit되지 않았다면 트랜잭션B의 두번째 쿼리 조회결과는 20이었겠지만 Commit에 실패하였기 때문에

첫번째로 조회한 스냅샷인 19를 반환하게 되는 것이죠, 이렇게 Commit에 실패 했을때 복구하는 과정을 Consistent Read라고 해요

물론 Consistent Read에서 복구하는 스냅샷의 기준은 Commit이 된 스냅샷이어야 해요.

 

그렇다면 Read Committed의 문제는 무엇일까요?
바로 한 트랜잭션 안의 두개의 쿼리가 각각 다른 스냅샷을 갖는다는 것이에요. 이를 Non-Repetable-Read 문제라고 해요.

 

Non-Repetable-Read란 한 트랜잭션안에 각각의 한 데이터에 대해 여러개의 스냅샷을 갖는것을 말하고

해당 예제같은 경우는 트랜잭션B의 첫번째 SELECT쿼리와 두번째 SELECT쿼리의 조회값이 다른거와 같이
같은 쿼리지만 조회되는 스냅샷들이 다르게 조회되는 경우를 Non-Repetable-Read라고 말해요
근데 Non-Repetable-Read? 좀 익숙하지 않아요? 바로 격리레벨 2단계 Repetable-Read의 반대의 의뫄과도 같아요

 

즉 격리 레벨 단계 Repetable-Read 의 특징과 반대되기 때문이에요 

그럼 다음 레벨 Repetable Read를 살펴볼게요!

 

Repetable Read

 

  • SELECT절이 수행될 때 동안 모든 데이터에 Shared Lock이 작동합니다.
  • Oracle DBMS에서 사용하는 격리 레벨이에요.
  • 한 트랜잭션 안에서 반환되는 결과가 모두 같아요.

위에서 들었던 에시를 또다시 한번만 그대로 가져올게요 ㅋㅋㅋㅋ

 

멤버들의 나이를 20살로 바꾸는 트랜잭션A가 존재한다.

또한 멤버테이블에는 "마샬"이라는 이름을 가진 멤버가 존재하고 "마샬"의 나이는 19살이다.

또한 멤버의 나이를 조회하는 쿼리가 2개가 존재하는 트랜잭션B에서 "마샬"의 나이는 어떻게 조회 될까요?

 

  • 트랜잭션의 수행조건과 순서

트랜잭션A의 첫번째 SELECT 쿼리가 실행되고
트랜잭션B가 수행되어 나이를 20살로 UPDATE
다시 트랜잭션A의 두번째 SELECT 쿼리가 실행되는 로직으로 실행한다고 가정할게요.

트랜잭션B의 결과 트랜잭션 A가 정상적으로 COMMIT 되었을때 트랜잭션 A가 COMMIT되지 않았을때
트랜잭션B의 첫번째 나이 조회 쿼리 19 19
트랜잭션B의 두번째 나이 조회 쿼리 19 19

이제까지 제 포스팅을 쭉 봐왔다면 이상하다고 느끼실거 같아요, 어? 왜 커밋이 되었는데도 19로 조회되지? 그 이유는 다음과 같아요

Repetable Read 레벨은 처음 조회를 했을때 그 결과에 대해 스냅샷을 찍고 조회 되었던 시간을 기록해놔요 그 뒤로 같은 SELECT 쿼리가 나가도 저장해놨던 첫번째 SELECT 쿼리가 날라갔던 시점을 바탕으로 해당 스냅샷에 Consistent Read해요. 

근데 좀 이상한점이 보이지 않나요?

이게 단순 쿼리를 몇개 날리고 끝이 아닌 24시간 돌아가는 온라인 스토어로 가정해볼게요

상품 가격을 두번 조회하는 트랜잭션이 있어요, 첫번째로 상품을 조회했을때는 10만원이 나왔는데 다른 트랜잭션에서 해당 아이템이

20만원으로 업데이트를 하고 커밋을 하여서 데이터베이스 상에는 20만원으로 남아있을거에요

하지만 가격을 두번 조회했던 트랜잭션에서는 두번째에서는 여전히 10만원일 거에요 그러면 이 트랜잭션을 수행하게 한 유저의 클라이언트에서는 해당 상품의 가격이 10만원으로 보이겠죠?

 

이런 문제점을 Phantom Read라고 불러요

Phantom Read란 한 트랙잭션에서 발생한 UPDATE 쿼리가 유실되는 경우를 말해요, 방금 제가 말했던 상황과 같죠

유실 되었기 때문에 마찬가지로 조회를 했을때도 UPDATE한 정보가 없는것이죠


그러면 이제 진짜 마지막!

 

Serializable

한마디로 정리할게요 Serializable 쓰지 마세요 ㅋㅋㅋㅋ 

무작정 쓰지말라는게 아니라 설명만 들으면 Serializalbe은 최상의 고립성과 일관성을 제공하지만 성능이 어마어마하게 안나와요.

쓰지마세요.

 

 

자 이제 모든 트랜잭션 격리 수준에 대해 알아보았어요.

트랜잭션 격리 수준은 수준이 높을수록 고립성은 높아지고 동시성은 낮아져요.
고립성은 안정성이라고 보시면 되고 동시성은 성능이라고 보시면 되요!

 

여러 레퍼런스를 참조했지만 제가 잘못된 사실을 포함했을 수도 있으니 잘못된 정보가 있을경우
댓글로 알려주시기 바랍니다 훈수, 피드백은 언제나 환영입니다!

 

자 그럼 이제 상황에 맞는 격리 수준을 수용하고 한층 더 성장한 개발자가 되길 바래요!