트랜잭션의 격리수준, ACID

Posted by , March 05, 2023
데이터베이스트랜잭션ACID
Series ofDatabase Core Concept

트랜잭션(Transaction) 의 성질 : ACID

트랜잭션하면 빼놓을 수 없는 키워드가 바로 ACID 라는 4가지 성질입니다. 이들이 무엇인지 간단히 되짚어보겠습니다.

Atomicity (원자성)

우선 트랜잭션은 원자성이라는 성격을 띠고있습니다. 트랜잭션 안의 내용은 모두 DB에 반영되거나 또는 전혀 반영되서는 안된다는 뜻이죠. 트랜잭션 안의 중간 어딘가까지만 실행되고 갑자기 종료되는 일이 있어서는 안되는 것입니다.

원자성이 보장되면 구매자의 계좌에서 돈이 빠져나갔는데, 판매자의 계좌에 돈이 들어오지 않는 일은 없을겁니다.

Consistency (일관성)

또 트랜잭션은 일관된 데이터베이스 상태를 유지해야 한다는 뜻입니다.** 즉, 트랜잭션은 DB 에 여러 제약조건에 맞는 상태를 보장해준다는 것이죠.** 만약 마이너스 통장(통장 잔금이 0원 이하)을 허락하지 않는 DB의 조건이 있다면, 이 조건이 위배될 때 트랜잭션은 바로 종료됩니다.

Isolation (독립성)

다음으로 독립성이라는 성질입니다. 트랜잭션은 각각의 독립성을 보장해야 하는 것이죠. 즉, 둘 이상의 트랜잭션이 동시에 실행되고 있을때 그 어떤 트랜잭션도 다른 트랜잭션의 연산에 끼어들 수 없다는 뜻입니다. 구매자의 계좌에서 돈이 빠져나가고 판매자의 계좌에 돈이 아직 들어가지 않는 DB 상황, 즉 송금+출금에 관한 트랜잭션이 완전히 끝나기전의 DB 상황을 다른 트랙잭션이 조회해서는 안됩니다.

Durability (지속성)

마지막으로 지속성입니다. 트랜잭션이 성공했을 경우, 그 해당 결과가 DB에 영구적으로 반영되어야 한다는 것입니다. 한번 송금에 성공했다면 은행 시스템에 문제가 발생하더라도 송금이 성공한 상태로 복구할 수 있어야합니다.


왜 격리수준을 알아야하는가?

위와 같은 ACID 성질은 트랜잭션이 이론적으로 보장해야하는 성질이며, 실제로는 성능을 위해 손실보장이 완화되기도 합니다.

예를들어 독립성을 완벽히 보장하려고 하면 동일한 데이터에 100개의 연결이 접근했을 떄, 이 100개의 연결을 순차적으로 해결해야하죠.

동시성(동시간대에 처리하는 양에 대한 성능)이 매우 떨어지는 문제가 발생하게 되는데, 동시성을 얻기위한 1가지 방법으로 트랜잭션의 격리수준(레벨) 설정이 있습니다.

동시성애 대한 성능을 향상시키기 위해, 격리수준을 이해하자.


트랜잭션 격리수준(isolation level)

트랜잭션 격리수준(isolation level)이란 동시에 DB에 접근할 떄, 그 접근을 어떻게 제외할지에 대한 설정입니다.

좀 더 자세히 말해보면, 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 것입니다. 즉, 특정 트랙잭션이 다른 트랜잭션이 변경한 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것입니다.

격리수준은 크게 아래와 같은 4가지 단계로 나뉩니다.

  • READ UNCOMMITED
  • READ COMMITED
  • REPEATABLE READ
  • SERIALIZABLE

아래로 내려갈수록 각 트랜잭션간의 고립 정도가 높아지며, 성능이 떨어지는 특징이 있습니다. 데이터 정합성과 성능이 반비례하므로, 각 상황에 알맞는 격리수준을 잘 선택하는 것이 중요합니다.

참고로 일반적인 DB 서비스는 READ COMMITED 또는 REPEATABLE READ 중 하나를 선택합니다. (oracle = READ COMMITED, mysql = REPEATABLE READ)


READ-UNCOMMITED

첫번째 격리레벨은 격리수준이 가장 낮은 READ_UNCOMMITED 레벨입니다. 한 트랜잭션이 아직 커밋되지 않은 상태임에도 불구하고, 다른 트랜잭션에서 읽을 수 있는 격리수준입니다.

즉 트랙잭션 A의 변경내용이 커밋이나 롤백을 얼만큼 진행한 것과 상관없이 다른 트랜잭션B, C, ... 에게 노출되는 것이죠.

더티 리드(Dirty Read) 발생

이 격리수준은 아래와 같은 문제가 발생할 수 있습니다.

    1. 트랙잭션 A에서 10번 사원의 나이를 30살에서 31살으로 바꾼다.
    1. 아직 커밋하지는 않은 상태이다.
    1. 트랜잭션 B에서 10번 사원의 나이를 조회한다.
    1. 30살이 조회된다. => 이를 더티 리드(Dirty Read)라고 합니다.
    1. 트랜잭션 A 에서 문제가 발생해 롤백합니다.
    1. 트랜잭션 B 는 10번 사원이 여전히 30살이라고 생각하고 로직을 수행합니다.

이런식으로 데이터 정합성에 문제가 많기 때문에, RDBMS 표준에서는 격리수준으로 인정하지도 않습니다.

또한 READ-UNCOMMITED 격리 레벨에서는 Dirty Read 현상뿐만 아니라 Repeatable Read 와 Phantom Read 문제도 발생할 수 있습니다. 이는 추후 다른 격리레벨에서 설명하겠습니다.


READ-COMMITED

두번째는 READ_COMMITED 격리수준입니다. 어떤 트랜잭션의 변경내용이 커밋이 완료된 데이터만 다른 트랜잭션에서 조회 가능합니다. 트랜잭션이 이루어지는동안 다른 사용자는 해당 데이터에 접근이 불가능하죠.

트랜잭션 A에서 아직 커밋이 완료되지 않은 상태라면 트랜잭션 B는 시작전의 값을 읽어오고, 반대로 A에서 커밋이 완료된 상태라면 B는 변경된 데이터를 읽어옵니다.

이 격리수준은 Oracle DBMS 에서 기본으로 사용하고 있으며, 대중적으로 가장 많이 선택되는 격리수준입니다.

NON-REPEATABLE READ 발생

그러나 이 격리수준도 데이터에 대한 정합성 문제가 발생할 수 있습니다.

  • 트랜잭션 B에서 10번 사원의 나이를 조회한다.
  • 30살이 조회되었다! 여기까지는 문제없다.
  • 트랜잭션 A 에서 10번 사원의 나이를 30살에서 31살로 바꾸기를 시도한다.
  • 트랜잭션 A에서 나이 변경내용을 커밋(COMMIT) 하기전에, B 에서 10번 사원의 나이를 다시 조회한다.
  • 그 결과는 30살이 조회된다!
  • 트랜잭션 A 가 커밋을 완료하고나서, 트랜잭션 B는 다시 나이를 조회해본다. 그 결과는 31살이 조회된다.
  • 이로써 같은 트랜잭션 내에서 select 쿼리를 2번 조회했는데, 두 값이 나이값이 조회되는 데이터 불일치 문제가 발생한다.

이는 하나의 트랜잭션내에서 똑같은 SELECT를 수행했을 경우 항상 같은 결과를 반환해야 한다는 REPEATABLE READ 정합성에 어긋나는 것이죠.

일반적인 웹 애플리케이션이라면 크게 문제될 것 없지만, 결제 기능과 같은 금전 적인 처리와 연결된 기능이라면 문제가 발생할 수 있습니다. 예를들어 여러 트랜잭션에서 입/출금 처리가 계속 진행되는 트랜잭션들이 있고, 오늘의 입금 총 합을 보여주는 트랜잭션이 있다고하면, 총합을 계산하는 SELECT 쿼리는 실행될 떄 마다 다른 결과값을 가져올겁니다.


REPETABLE READ

3번쨰 격리수준은 REPEATABLE READ 입니다. 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준입니다. 트랜잭션이 완료될 떄 까지 SELECT 쿼리가 사용되는 모든 데이터에 Shared Lock(공유 락)이 걸리는 계층이죠.

트랜잭션 범위 내에서 조회한 데이터 내용이 항상 동일함을 보장하며, 다른 사용자는 트랜잭션 영역에 해당되는 데이터에 대한 수정이 불가능합니다.

MySQL DBMS에서 기본으로 사용하고 있고, 이 격리수준에서는 NON-REPETABLE READ 부정합이 발생하지 않습니다.

READ COMMITED 마찬가지로 커밋이 완료된 데이터만 읽을 수 있는데, 차이점은 뭘까요? 어떤 한 트랜잭션 A가 조회한 데이터는 해당 트랜잭션이 종료될 때까지 다른 트랜잭션이 변경하거나 삭제하는 것을 막으므로, 한번 조회한 데이터는 반복적으로 조회해도 같은 값을 반환합니다.

Phantom Read 발생

하지만 이 격리수준도 센텀 리드(Phantom Read) 문제가 발생할 수 있습니다.

센텀 리드는 Non-repeatable Read 의 한 종류로, 조건이 걸렸든 안 걸렸든 select 문을 쓸때 나타날 수 있는 현상입니다. 해당 쿼리로 읽히는 데이터에 들어가는 행이 새로 생기거나 없어져있는 현상이죠.

즉, 한 트랜잭션 내에서 같은 쿼리를 2번실행시, 첫번쨰 쿼리에서 없었던 레코드 (유령,Phantom) 가 2번쨰 쿼리에서 발생하는 현상입니다. INSERT, DELETE 쿼리등에 대해 쓰기 잠금을 거는 경우, 다른 트랜잭션에서 수행한 변경작업 내용에 의해 레코드가 보였다가 안보였다가하는 현상입니다.


SERIALIZABLE

마지막으로 격리 레벨이 가장 높은 SERIALIZABLE 입니다. 한 트랜잭션에서 사용하는 데이터를 다른 트랜잭션에서 절대 접근할 수 없습니다.트랜잭션의 ACID 성질이 엄격하게 지켜지지만, 성능은 가장 떨어집니다.

단순 SELECT 쿼리만으로도 트랜잭션이 커밋될때까지 모든 데이터에 잠금(lock) 이 설정되어 다른 트랜잭션에서 해당 데이터를 변경할 수 없게 됩니다.

  • 트랜잭션이 완료될때까지 SELECT 쿼리가 사용되는 모든 데이터에 Shared Lock(공유 락) 이 걸리는 계층

  • 가장 엄격힌 격리수준으로, 완벽한 읽기 일관성 모드를 제공한다.

  • 다른 사용자는 트랜잭션 영역에 해당되는 데이터에 대한 수정 및 입력 불가능


참고