☁️ 트랜잭션
트랜잭션이란 우리가 하나
라고 생각하는 작업이다. 예를 들어 나에게 “샤워를 한 뒤 바디로션을 바른다.”는 하나의 작업이다. 엄밀하게 따지면 “샤워를 한다.” 와 “바디로션을 바른다.”로 행동이 나눠지지만 그냥 하나의 작업으로 묶는 것이다.
트랜잭션도 동일하다. “A테이블을 읽고 a레코드가 존재한다면, A테이블의 a레코드를 수정한다.” 라는 두 작업을 하나의 작업으로 인식하게 하는 것이다. 하나의 작업은 반드시 성공(커밋)하거나 하나라도 실패 시 롤백되어야 한다. A테이블을 읽는 작업을 성공하고 a레코드를 수정하는 작업에서 오류가 발생하였다면 롤백되어야 한다.
유의할 점
그런데 여기서 유의해야할 사항이 있다. “과연 A테이블을 읽고 a레코드가 존재한다면, A테이블의 a레코드를 수정한다” 라는 작업을 트랜잭션으로 선언할 필요가 있을까? A테이블을 읽는 작업은 롤백이 필요한가? 필요 없을 것이다. 그럼 A테이블의 a레코드를 수정한다. 라는 하나의 작업은 트랜잭션으로 선언될 필요가 있는가? 아닐 것이다.
트랜잭션은 활성화 시간이 길수록 언두 로그도 쌓이게 된다. 언두 로그가 쌓일수록 버퍼 풀의 메모리를 차지하게 되고 이는 데이터베이스 성능을 감소시킬 수 있기에 트랜잭션은 최대한 빨리 사용하고 결과를 커밋하거나 롤백해야 한다.
단순 읽기 작업은 트랜잭션으로 선언할 필요가 없고, 꼭 하나의 트랜잭션으로 관리되어야 하는 쓰기 작업만 트랜잭션으로 선언하도록 하자. 추가로 메일 전송과 같이 네트워크를 통해 원격 서버와 통신하는 작업은 어떻게 해서든 트랜잭션 내에서 제거하는 것이 좋다. 프로그램이 실행되는 동안 외부 서버와의 통신에서 예기치 못한 오류가 발생한다면 웹 서버 뿐만 아니라 DBMS 서버에 까지 영향을 끼칠 수 있기 때문이다.
☁️ 격리 수준 (Isolation level)
트랜잭션의 격리 수준이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
격리 수준은 크게 READ-UNCOMMITED
, READ-COMMITED
, REPEATABLE-READ
, SERIALIZABLE
4가지로 나뉜다. READ-UNCOMMITED와 SERIALIZABLE은 일반적인 데이터베이스에서는 거의 사용하지 않는다. SERIALIZABLE격리 수준을 제외하고는 성능이 거의 동일하다고 한다.
READ UNCOMMITED
커밋되지 않은 데이터를 읽는
격리 수준이다. 말 그대로 쓰기 트랜잭션이 변경하고 커밋되지 않은 데이터를 다른 트랜잭션이 읽을 수 있는 격리 수준이다.
만약 쓰기 트랜잭션이 데이터를 변경한 뒤 읽기 트랜잭션이 변경된 데이터를 읽고 쓰기 트랜잭션이 롤백 되었다면, 읽기 트랜잭션은 이미 읽어버린 변경된 내용을 정상적인 데이터라고 생각하고 처리할 것이다. 이렇게 아직 정상적으로 처리되지 않은 데이터를 읽는 현상을 더티리드라고 한다.
이는 정합성을 지키지 못하는 큰 문제를 발생시키므로 책에서는 사용을 권장하지 않는다고 한다.
READ COMMITED
커밋된 데이터를 읽는
격리 수준이다. 오라클에서 기본적으로 사용되는 격리 수준이다.
트랜잭션은 커밋된 데이터를 읽는다. 쓰기 트랜잭션이 데이터를 변경하면 변경 전의 데이터가 언두 로그에 기록되는데, 읽기 트랜잭션은 이 언두 로그에 접근하여 데이터를 읽어가기에 쓰기 트랜잭션의 Commit, Rollback 여부에 관계없이 동일한 데이터를 읽는다.
하지만 여기서도 Non-Repeatable Read 라는 문제가 있다. 연속된 읽기가 불가능하다 라는 의미인데, A 트랜잭션이 테이블을 두 번 읽는 작업을 수행할 때, 두 읽기 작업 사이에 쓰기 트랜잭션이 하나의 레코드를 추가한 뒤 커밋한 상황에 발생한다. A트랜잭션은 같은 트랜잭션에 동일한 읽기 작업을 수행했음에도 두 읽기 작업은 레코드 수의 차이가 존재하게 된다. READ COMMITED 수준에서의 문제 예시들은 아래와 같다.
예약 시스템에서의 문제:
- 여러 사용자가 동시에 호텔 예약을 시도합니다.
- 각각의 예약 트랜잭션은 현재의 예약 가능 상태를 확인하고 예약을 시도합니다.
- 동시에 다른 사용자가 예약을 취소하고 커밋하면, 예약 트랜잭션들은 이전 상태를 참조하게 됩니다.
- 결과적으로, 예약이 중복되거나 예약이 되지 않는 문제가 발생할 수 있습니다.
주문 및 결제 시스템에서의 문제:
- 고객이 장바구니에 상품을 담고 결제를 시도합니다.
- 동시에 관리자가 고객 장바구니에 있는 특정 상품의 가격을 변경하고 COMMIT 합니다.
- 고객이 상품 결재를 위해 SELECT 작업을 수행하고 합산 결제 금액을 확인하는 작업을 수행합니다.
- 관리자에 의해 변경된 금액을 합산하기에 결제 금액의 부정합을 발생시킬 수 있습니다.
REPEATABLE READ
일관된 읽기
라는 격리 수준이다. MySQL InnoDB에서 기본으로 사용된다.
READ COMMITED 수준은 트랜잭션의 두 번의 읽기 작업 사이에 커밋된 데이터가 추가된다면 데이터 추가 후의 읽기 작업이 커밋된 데이터도 읽어 두 읽기 결과의 정합성을 깨뜨리는 NON-REPEATABLE READ 문제가 있었다. REPEATABLE READ는 이러한 문제를 해결하여 트랜잭션의 두 번의 읽기 작업 사이에 커밋된 데이터가 있더라도 이를 무시하고 일관적인 데이터를 읽는다.
MVCC를 통한 버저닝:
이러한 데이터 조회가 가능한 이유는 MVCC 방식을 사용하여 언두 로그에 여러 버전의 데이터를 저장해놓고 트랜잭션의 버전에 맞는 데이터를 일관적으로 조회하기 때문이다. READ COMMITED도 MVCC를 통해 버저닝을 수행하는데 어떤 점이 달라서 읽기 작업에 대해 이런 차이가 발생할까?
먼저 REPEATABLE READ 수준에서는 가장 오래된 트랜잭션에 의해 생성된 언두 로그가 존재한다면 그 앞에 생성된 언두 로그는 삭제하지 않는다. 하지만 READ COMMIED 수준에서는 트랜잭션이 커밋된다면 해당 언두 로그를 삭제한다. 이러한 차이에서 데이터 정합성 문제가 발생하는 것이다. 간단하게 그림으로 확인하면 아래와 같다.
PHANTOM READ:
두 트랜잭션 작업에서 한 트랜잭션의 변경으로 인해 다른 트랜잭션의 조회 쿼리의 데이터 정합성이 보장되지 않는 것을 팬텀리드라 한다.
위의 그림과 같이 REPEATABLE READ에서는 SELECT 작업에 대해서 MVCC를 통한 데이터 정합성을 보장한다. 하지만 이런 상황에서도 PANTHOM READ가 발생할 수 있는데 SELECT ~ FOR UPDATE 혹은 SELECT ~ LOCK IN SHARE MODE 쿼리를 사용할 때이다.
위의 쿼리는 읽기 작업을 수행하는데 자신이 읽는 레코드에 쓰기 락을 건다. 여기서 쓰기 락을 거는데 문제가 발생한다. 바로 언두 로그에는 락을 걸 수 없다는 것. 따라서 InnoDB는 인덱스에 락을 건다. 이는 즉, 언두 로그가 아닌 테이블의 커밋된 데이터들이 저장되는 페이지에 락이 걸리는다는 것이고, 이는 위의 쿼리를 수행하는 트랜잭션이 언두 로그가 아닌 커밋된 데이터를 읽는다는 것이다.
이렇게 커밋된 데이터를 읽어버리면, 나 이후에 수행된 트랜잭션이 이미 INSERT한 레코드가 존재할 수도 있다. 이렇게 트랜잭션이 언두로그가 아닌 버퍼 풀의 테이블 데이터를 참조해버리면서 팬텀 리드가 발생할 수 있다.
SERIALIZABLE
읽기 작업 또한 잠금을 획득하여야 하는 가장 엄격한 격리 수준이다.
InnoDB 테이블에서 순수한 읽기 작업은 아무런 잠금 없이 실행될 수 있다. 하지만 SERIALIZATION 으로 설정된다면, 읽기 작업도 잠금을 획득해야만 한다. 동시에 다른 트랜잭션은 해당 레코드를 변경하거나 읽기 위해 잠금을 기다려야 한다.
'Database > Real MySQL' 카테고리의 다른 글
인덱스: B-Tree (1) | 2024.01.29 |
---|---|
MySQL 엔진과 InnoDB 엔진의 잠금 (1) | 2024.01.27 |
InnoDB 스토리지 엔진 - 지원 기능 (1) | 2024.01.26 |
InnoDB 스토리지 엔진 - 버퍼 풀과 리두, 언두 로그 (1) | 2024.01.26 |
쿼리 실행 구조와 MySQL 8.0 변경점 (2) | 2024.01.25 |