[CS Study] Database - Transaction

Frye 'de Bacon·2023년 12월 1일
0

Computer Science(CS)

목록 보기
27/40
post-thumbnail

트랜잭션(Transaction)

개요

CS에서 말하는 트랜잭션(Transaction)은 '분할이 불가능한 업무 처리의 단위'를 말한다. 그리고 트랜잭션이라는 용어가 가장 많이 사용되는 Database 분야에서는 트랜잭션을 흔히 'DB의 상태를 변화시키는 논리적 기능을 수행하기 위한 작업의 단위'라고 정의한다. 여기서 'DB의 상태를 변화시킨다'는 것은 DB에 데이터를 생성하거나, 수정하거나, 삭제하거나 읽어오는 등의 작업을 말한다.

분할할 수 없다는 것은 어떤 의미일까? 많이 드는 예시로서 '계좌이체'의 예를 살펴보자.

A와 B가 비대면 중고거래를 하였다. B가 택배 송장을 A에게 보냈고, 이를 확인한 A는 B에게 물건 대금을 송금한 뒤 B에게 송금하였음을 알리는 문자를 보냈다. 문자를 확인한 B는 해당 금액을 출금하였다.
그런데 어떤 이유(은행의 전산 오류 등)로 인해 A의 계좌에서는 물건 대금이 차감되었으나, B의 계좌에는 물건 대금이 입금되지 않았다.

위의 계좌이체의 예에서 계좌이체(송금)라는 행위는 '인출(A의 통장으로부터)'과 입금(B의 통장으로)'의 두 과정으로 구성된다. 만약 이 중 하나라도 실패한다면? 계좌이체라는 작업 자체가 성립하지 않는다. 따라서 두 개의 작업은 동시에 성공하거나, 그렇지 않으면 동시에 실패해야 한다. 이처럼 '한꺼번에 수행되어야 할 연산'을 트랜잭션이라고 한다.

Transaction의 사전적 정의는 '거래'이다. DB에서 데이터의 '거래'를 성공적으로 수행하기 위한 방법이 바로 트랜잭션인 것이다.

트랜잭션의 특징

트랜잭션의 특징을 간단히 정리하면 다음과 같다.

  • 트랜잭션은 DBMS에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이다.
  • 트랜잭션은 사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업 단위이다.
  • 하나의 트랜잭션은 Commit되거나 Rollback된다.

현재로서는 이해가 갈 듯하면서도 이게 무슨 소리인가 싶은데, 트랜잭션에 대해 좀 더 자세히 살펴보자.


트랜잭션의 성질(ACID)

트랜잭션의 특징은 크게 4가지로 구분된다.

원자성(Atomic)

  • All or Nothing으로 나타낼 수 있는 성질
  • 하나의 연산은 데이터베이스에 '모두 반영'되든지 아니면 '전혀 반영되지 않아야' 한다.
  • 트랜잭션으로 묶인 모든 명령(연산)은 모두 완벽하게 수행되어야 하며, 그중 하나라도 제대로 수행되지 않고 오류가 발생할 경우 트랜잭션 전부가 취소되어야 한다.

일관성(Consistency)

  • 트랜잭션이 성공적으로 실행되면 데이터베이스의 상태는 언제나 일관성 있는 상태여야 한다.
    • 명시적 일관성 : 기본키, 외래키 등의 무결성 제약조건
    • 비명시적 일관성 : 위의 계좌이체 예시에서 두 계좌의 잔고 합은 계좌이체 전후가 동일해야 한다는 사항 등

독립성(Isolation)

  • 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 어느 하나의 트랜잭션 실행 중 다른 트랜잭션의 연산이 끼어들 수 없다.
  • 어떤 트랜잭션이 수행 중인 경우 그 작업이 완전히 완료되기 전까지 다른 트랜잭션에서 해당 트랜잭션의 수행 결과를 참조할 수 없다.

영구성(Durability)

  • 트랜잭션이 성공적으로 완료되었다면 그 결과는 '시스템이 고장 나더라도' 영구적으로 반영되어야 한다.

트랜잭션의 연산 및 상태

트랜잭션 연산

  1. Commit
    Commit 연산은 하나의 트랜잭션에 대한 작업이 성공적으로 완료되고 데이터베이스가 일관된 상태에 있을 때, 이 트랜잭션이 행한 연산이 완료되었음을 트랜잭션 관리자에게 알려주는 연산을 말한다. 이 연산이 수행되면 수행했던 트랜잭션이 로그에 저장되며, 이후 Rollback 연산 시 Commit 연산이 수행되었던 시점을 단위로 동작하게 된다.
  2. Rollback
    Rollback 연산은 트랜잭션이 비정상적으로 종료되어 데이터베이스의 일관성을 깨뜨렸을 때, 이 트랜잭션의 모든 연산을 취소(Undo)하는 연산이다. 비록 이 트랜잭션의 일부가 정상적으로 처리되었다고 하더라도 트랜잭션의 모든 작업이 정상적으로 수행되지 않았다면 트랜잭션의 원자성을 깨뜨릴 수 있으므로 해당 작업들도 DB에 반영하지 않는 것이다. Rollback 명령을 트랜잭션 관리자에 전달하면 트랜잭션이 시작되기 이전의 상태(즉, 마지막 Commit 상태)로 돌아가며, Rollback 시에는 비정상적으로 종료되었던 트랜잭션을 재시작하거나 폐기하게 된다.

트랜잭션의 상태

  • 활동(Activate) : 트랜잭션이 정상적으로 실행 중인 상태
  • 실패(Failed) : 트랜잭션 실행에 오류가 발생하여 중단된 상태
  • 철회(Aborted) : 트랜잭션이 비정상적으로 종료되어 Rollback 연산을 수행한 상태
  • 부분 완료(Partially committed) : 트랜잭션의 마지막 연산까지 실행하고 Commit 연산을 실행하기 전의 상태
  • 완료(Committed) : 트랜잭션이 성공적으로 종료되어 Commit 연산을 실행한 수의 상태

부분 완료 vs 완료
설계된 트랜잭션대로 연산이 성공적으로 수행되었다고 해도 이를 즉시 DB에 반영하는 것이 아니라 '부분 완료' 상태로 대기한다. 그리고 설계자의 최종 승인(Commit)이 주어지면 그때 해당 연산 결과를 DB에 반영한다. 즉, 부분 완료는 commit 요청이 들어온 시점을 의미하고 완료는 commit 연산이 성공적으로 수행된 상태를 의미힌다.


트랜잭션의 격리 수준

개요

트랜잭션의 격리 수준이란 '동시에 여러 개의 트랜잭션이 처리될 때 트랜잭션끼리 얼마나 고립(격리)되어 있는가'에 대한 수준을 말한다. 즉, 여러 트랜잭션이 병렬적으로 처리될 때, 특정 트랜잭션이 변경 혹은 조회하고 있는 데이터에 대하여 다른 트랜잭션이 조회하는 것을 허용할지 여부를 결정하는 것이다.

트랜잭션 격리 수준의 필요성

그런데 조금 이상하다. 앞에서 살펴본 트랜잭션의 성질(ACID) 중 '격리성(Isolation)'에 대한 내용을 다시 되짚어보자.

어떤 트랜잭션이 수행 중인 경우 그 작업이 완전히 완료되기 전까지 다른 트랜잭션에서 해당 트랜잭션의 수행 결과를 참조할 수 없다.

조금 헷갈리지만, 기본적으로는 트랜잭션 수행 중일 때는 서로 끼어들지 못하도록 격리성을 확보하되, 단순히 각 트랜잭션들을 서로 완전히 격리시키는 것이 아니라 격리 수준을 나누어 그 고립의 정도를 나누게 된다. 왜 그럴까?

트랜잭션 중간에 다른 트랜잭션이 완전히 끼어들 수 없다면 어떨까. 모든 트랜잭션은 순차적으로 처리되고, 당연히 데이터의 정확성이 보장될 것이다. 그러나 속도 면에서는 어떨까? 트랜잭션이 많아지고, 특히 중간에 처리 속도가 긴 트랜잭션이 섞일 경우 처리를 기다리는 트랜잭션이 쌓이게 되고, 전체적인 서비스의 속도는 느려진다. 이는 서비스 운영에 치명적인 하자를 야기할 수도 있을 것이다.
결국 트랜잭션의 격리 수준을 나누는 것은 트랜잭션 처리 속도를 확보하기 위함이다. 즉 속도와 정확성의 트레이드 오프를 통해 서비스에 적합한 처리가 가능하게끔 해 주는 것이 트랜잭션의 격리 수준 설정인 것이다.

다중 버전 동시성 제어(Multi-Version Concurrency Control, MVCC)

먼저 다중 버전 동시성 제어(Multi-Version Concurrency Control, MVCC)에 대해 알아보고 다음으로 넘어가자. 일반적으로 RDBMS는 변경 전의 레코드를 Undo 공간에 백업해 둔다. 그렇게 하면 변경 전후의 데이터가 모두 존재하므로 동일 레코드에 대해 여러 버전의 데이터가 존재하게 되며 이를 '다중 버전 동시성 제어'라고 한다. 이를 이용해 트랜잭션의 롤백 시 데이터의 복원도 가능하고, 트랜잭션의 격리 수준에 따라 서로 다른 트랜잭션 간 접근 가능한 데이터를 조절할 수도 있다.
각 트랜잭션은 순차 증가하는 고유한 트랜잭션 번호가 존재하며, 백업 레코드에는 어느 트랜잭션에 의해 백업되었는지를 나타내기 위하여 트랜잭션 번호가 함께 저장된다. 그리고 해당 데이터가 불필요하다고 판단될 경우 주기적으로 백그라운드 스레드를 이용해 삭제하게 된다.

트랜잭션 격리 수준에 따라 발생 가능한 문제들

트랜잭션의 격리 수준에 앞서 트랜잭션 격리 수준에 따라 발생할 수 있는 문제들을 간단하게 정리하고 넘어가자.

  1. 더티 리드(Dirty read)
    더티 리드는 특정 트랜잭션으로 인해 데이터가 변경되었으나 아직 커밋되지 않은 상황에서, 다른 트랜잭션이 해당 변경 사항을 조회할 수 있는 문제를 말한다. 예를 들어 트랜잭션 A가 데이터를 변경하고 커밋하지 않은 상태에서 트랜잭션 B가 변경된 데이터를 참조하여 연산을 수행하였다고 할 때, 만약 트랜잭션 A가 변경 내역을 커밋하지 않고 롤백한다면 트랜잭션 B는 무효가 된 변경사항을 바탕으로 연산을 수행하는 것이므로 치명적인 문제가 발생할 수 있다.
  2. 반복 불가능한 조회(Non-repeatable read)
    반복 불가능한 조회란 동일한 트랜잭션 내에서 같은 데이터를 여러 번 조회했을 때 조회한 데이터의 값이 서로 다른 경우를 의미한다. 예를 들어 트랜잭션 A가 연산을 수행하면서 어떤 데이터의 값을 계속해서 변경하고, 그때 트랜잭션 B가 데이터를 반복적으로 조회한다면 조회할 때마다 데이터의 값이 달라지게 될 것이다.
  3. 팬텀 리드(Phantom read)
    팬텀 리드는 '반복 불가능한 조회'의 한 종류로서, 조회한 결과의 행이 새로 생기거나 아예 없어지는 현상을 말한다. 반복 불가능한 조회가 '반복적으로 조회하는 데이터의 값이 달라지는 것'을 의미한다면, 팬텀 리드는 조회하는 데이터의 값은 달라지지 않지만 조회할 때마다 새로운 레코드가 생겼다 사라지는 등의 일이 발생하는 것(유령처럼)이라는 차이가 있다.

트랜잭션 격리 수준 구분

트랜잭션 격리 수준은 일반적으로 4단계로 분류되며, 격리 수준이 낮은 순으로 'Read uncommitedRead commitedRepeatable readSerializable'로 구성된다. 각 격리 수준에 따른 상세 내용은 DBMS마다 차이가 있으므로 실제 DB 구성 시에는 사용하는 DBMS의 공식 문서를 참조해야 한다.

  1. Read uncommited
    다른 트랜잭션에서 커밋되지 않은 데이터에 접근하는 것을 허용하는 격리 수준으로, 가장 저수준의 격리 수준으로서 일반적으로 사용되지 않는 격리 수준이다. 속도는 당연히 가장 빠르지만, 데이터 부정합 문제가 발생할 확률이 높다.
    '더티 리드', '반복 불가능한 조회', '팬텀 리드' 등의 문제가 모두 발생할 수 있으며, Oracle에서는 아예 지원하지 않는 수준이고 RDBMS 표준에서도 인정되지 않는 격리 수준이다.

  2. Read commited
    커밋이 완료된 트랜잭션의 변경 사항에 대해서만 타 트랜잭션에서 조회할 수 있도록 허용하는 격리 수준이다. 특, 특정 트랜잭션이 수행되는 동안에는 다른 트랜잭션은 해당 데이터에 접근할 수 없으며, 이 경우 이전 커밋의 데이터, 즉 트랜잭션이 시작되기 전의 데이터를 읽어온다.
    이를 통해 '더티 리드' 문제는 해결하였으나, '반복 불가능한 조회'나 '팬텀 리드' 등의 문제는 여전히 발생한다.

  3. Repeatable read
    특정 행을 조회했을 때 항상 같은 데이터가 반환되는 것을 보장하는 격리 수준이다. 커밋된 데이터만을 읽되, 자신보다 낮은 트랜잭션 번호를 갖는 트랜잭션에서 커밋한 데이터만을 읽을 수 있도록 함으로써 '반복 불가능한 조회'를 해결한다. 그러나 INSERT는 허용되므로 '팬텀 리드' 현상은 여전히 발생할 수 있다.
    MySQL의 InnoDB 엔진의 기본 격리 수준에 해당한다.

MVCC 덕분에 일반적인 조회에서 팬텀 리드 현상이 발생하지는 않는다(자신보다 나중에 실행된 트랜잭션이 추가한 레코드는 무시하면 되므로). 그러나 '잠금'이 사용되는 경우와 같이 특수한 케이스에서는 팬텀 리드 현상이 발생할 수 있다.
일반적인 RDBMS의 경우 SELECT ... FOR UPDATE 구문을 통해 쓰기 잠금을 걸 경우 잠금이 있는 읽기가 되어 Undo 공간이 아닌 테이블에서 수행되고, 이 경우에는 팬텀 리드가 발생할 수 있다. 다음 그림을 보면 이해하기 쉬울 것이다.


다만 MySQL에서는 갭 락이라는 특수한 락이 존재하므로 위와 같은 문제는 발생하지 않는다. 따라서 MySQL의 Repeatable read에서는 (극히 제한적인 경우를 제외하면) 팬텀 리드가 발생하지 않는다고 보아도 좋다.
  1. Serializable
    특정 트랜잭션이 사용 중인 테이블의 모든 행에 대하여 다른 트랜잭션이 접근할 수 없도록 잠그는 방식이다. 가장 높은 수준의 데이터 정합성을 확보할 수 있으나 당연히 성능은 가장 떨어진다. 이 수준에서는 단순한 SELECT 쿼리가 실행되더라도 DB에 락이 걸려 다른 트랜잭션에서 데이터에 접근할 수 없게 된다.

앞서 언급했듯, 트랜잭션 격리 수준은 동시성과 정합성의 트레이드 오프 관계를 띤다. 즉, 수준이 너무 낮으면 속도는 빨라지지만 일관성을 보장하지 못하고, 수준이 너무 높으면 일관성은 완벽하게 보장하지만 속도가 느려지게 된다.

따라서 서비스 및 상황에 적합하도록 격리 수준을 설정해야 하며, 이때 격리 수준에 따라 발생할 수 있는 문제들도 고려하여야 할 것이다.


참고 자료

profile
AI, NLP, Data analysis로 나아가고자 하는 개발자 지망생

0개의 댓글