티스토리 뷰
- 목표
- 트랜잭션의 ACID 속성에 대해 설명할 수 있다.
- 트랜잭션의 Commit/Abort 기능이 애플리케이션에 주는 이점을 설명할 수 있다.
- 동시성 문제의 종류를 이해하고 이를 방지할 수 있는 격리 수준의 종류에 대해 설명할 수 있다.
Transaction
트랜잭션: 애플리케이션에서 여러 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법, 한 연산으로 실행 됨 전체가 성공(커밋) 하거나 실패(어보트) 함
트랜잭션을 사용함으로써 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있음(all or nothing, 부분적 결과를 허용하지 않음)
트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도 할 수 있다는 점.
ACID?
너무 흔하게 들어보는 개념이지만. .DB 마다 ACID 구현이 제각각이다 ..
원자성(Atomicity): 어보트 능력(abortability) 에 더 가깝다, 여러 읽기/쓰기 작업이 하나의 원자적인 트랜잭션으로 묶이게 되고, 작업이 완료(커밋)되지 못하면 어보트됨 → 애플리케이션 입장에서는 트랜잭션이 어보트 되었다면 이 트랜잭션이 어떤 것도 변경하지 않았음을 알 수 있음. 애플리케이션이 부분 실패를 고려할 필요가 없게 해줌.
일관성(Consistency): 항상 진실이어야 하는 데이터에 관한 정의(불변식)이 있다는 것(e.g. 회계 시스템에서 모든 계좌의 대변과 차변은 항상 맞아떨어져야 함) → BUT 이것은 애플리케이션의 속성에 가까움 DB는 불변식을 위반하는 데이터를 막을 수는 있지만 (e.g. 외래키 재약 조건 ..), 일관성을 지키기 위해 DB만으로 되는 것이 아님
격리성(Isolation): 동시에 실행되는 트랜잭션은 서로 격리됨. 한 트랜잭션은 다른 트랜잭션의 내용을 전부 볼 수 있던지 아니면 아무것도 볼 수 없든지 둘 중 하나여만 한다. 직렬성 과 유사한 의미로, 여러 트랜잭션이 동시에 실행되더라도 트랜잭션이 순차적으로 실행된 결과와 동일함을 보장함. 추후에 다룰 격리수준에서 더 자세히 설명 예정.
지속성(Durability): 트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생했거나 데이터베이스가 죽더라도 기록한 모든 데이터는 손실되지 않는다는 보장. 물론 완벽한 지속성은 존재하지 않는다 (하드웨어와 백업이 동시에 파괴되어 버릴 경우..)
Isolation Level
각 격리 수준은 트랜잭션 간 방지해야 하는 현상에 따라 정의됨
Preventable Read Phenomena by Isolation Level
Isolation Level Dirty Read Nonrepeatable Read(fuzzy read) Phantom Read
| Read uncommitted | Possible | Possible | Possible |
| Read committed | Not possible | Possible | Possible |
| Repeatable read | Not possible | Not possible | Possible |
| Serializable | Not possible | Not possible | Not possible |
- Dirty Read: 다른 트랜잭션에 의해 변경된 커밋되지 않은 데이터를 읽음
- Non-Repeatable Read: 같은 쿼리를 동일 트랜잭션 내에서 두 번 실행했을 때 결과가 달라지는 현상 (다른 트랜잭션에 의해 데이터가 수정되고 커밋되었을 경우)
- Phantom Read: 어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 현상
Read committed
가장 기본적인 수준의 트랜잭션 격리로, 두 가지를 보장함.
- 더티 읽기 방지: 데이터베이스에서 읽을 때 커밋한 데이터만 보게 됨
- 더티 쓰기 방지: 데이터베이스에 쓸 때 커밋한 데이터만 덮어쓰게 됨
오라클 11g 기본 설정
트랜잭션 1이 쿼리 도중 트랜잭션 2가 데이터를 변경하거나 추가해서 커밋하게 되면, 쿼리 결과가 달라지는 fuzzy read나, 없던 row가 튀어나오는 phantom read 가 생길 수 있음
데이터베이스는 로우 수준 잠금 을 이용해 더티쓰기를 방지
오라클 기준 Row lock(TX lock) 이용, INSERT, UPDATE, DELETE, MERGE, or SELECT ... FOR UPDATE 수행 시 해당 row 에 대한 잠금을 획득하여 트랜잭션이 커밋되거나 어보트 될 때까지 잠금을 보유
오직 한 트랜잭션만 잠금을 보유할 수 있고 다른 트랜잭션이 쓰기를 원한다면 잠금을 보유한 트랜잭션이 커밋되거나 어보트 된 후에 잠금을 얻을 수 있음
오라클 기준 다른 트랜잭션이 변경 중이면 그 변경 전 버전을 Undo Log 를 이용해 읽음
Repeatable read
스냅샷 격리라고도 함.
각 트랜잭션은 데이터베이스의 일관된 스냅샷 으로부터 읽음
데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 봄
스냅샷 격리의 핵심 원리는 읽는 쪽에서 쓰는 쪽을 차단하지도 않고, 쓰는 쪽에서 읽는 쪽을 차단하지도 않는다는 것
스냅샷 격리는 MVCC(multi-version concurrency control) 로 구현
mySQL InnoDB 에서는 트랜잭션 ID + undo log 를 사용해서 구현되고,
각 row 마다 3개의 field 가 저장된다
- DB_TRX_ID: insert나 update 를 수행한 가장 마지막 트랜잭션 id
- DB_ROLL_PTR: undo log 의 레코드를 가리킴, update 전의 내용을 다시 가져오는데 필요한 정보가 포함
- DB_ROW_ID: 새 행이 삽입될 때 증가되는 행 id

트랜잭션이 update나 delete 를 하면 변경 전 데이터는 undo log 에 저장되고, 현재 데이터는 덮어씀. 다른 트랜잭션이 자기보다 나중에 커밋된 데이터를 보게 되면 undo log 를 참조해서 트랜잭션 시작 시점에 맞게 버전을 재구성
undo log는 더이상 이전 버전의 row를 빌드하는데 필요한 스냅샷이 할당된 트랜잭션이 없는 경우에 삭제됨.
스냅숏 격리는 각 데이터베이스에서 다른 이름을 사용, 오라클에서는 직렬성, postgreSQL 과 mySQL 에서는 반복 읽기(repeatable read) 하고 함
갱신 손실 문제
두 트랜잭션이 동시에 쓰기를 실행한다면? 두 트랜잭션이 데이터베이스에서 값을 읽고 변경한 후, 변경한 값을 다시 쓴다면 (e.g. 카운터를 증가시킴), 동시에 트랜잭션 작업이 이루어질 경우 두번째 변경이 첫번째 변경을 포함하지 않으므로 변경 중 하나가 손실될 수 있음
이 문제를 해결하는 방법?
- 명시적 잠금: 데이터 수정 전에 SELECT .. FOR UPDATE 를 이용하여 질의에 의해 반환된 모든 로우에 대해 Exclusive Lock (배타 락) 을 건다
- 오라클 Lock 모드
- Shared lock mode: 여러 트랜잭션이 동시에 공유 잠금을 획득할 수 있음, 공유 잠금이 걸린 데이터에 대해 쓰기 작업은 불가능(shared lock 이 걸릴 경우 exclusive lock은 획득 불가). 보통은 일관된 읽기를 제공하는 MVCC 로 해결
- Exclusive lock mode: 리소스를 배타적으로 잠그는 첫번째 트랜잭션은 배타적 잠금이 해제될 때까지 데이터를 변경할 수 있는 유일한 트랜잭션임, 다른 트랜잭션이 변경하려고 하면 blocking 됨. 단 MVCC 때문에 이전 데이터는 읽을 수 (SELECT) 있음
- 오라클 Lock 모드
- 갱신 손실 자동 감지: 스냅숏 격리와 결합해 Oracle 의 경우 갱신 손실이 발생하면 감지하여 문제가 되는 트랜잭션을 어보트 시킴

왼: Session1, 오: Session2
Session2의 트랜잭션 시작 이후 Session1이 같은 데이터에 대해 수정해서 커밋했기 때문에,
Session2가 같은 데이터에 대해 UPDATE 시 ORA-08177: can't serialize access for this transaction 에러가 발생
애플리케이션에서 특별한 데이터베이스 기능을 사용하지 않아도 갱신 손실이 자동 감지되기 때문에 오류가 덜 발생하게 해줌
Oracle Database permits a serializable transaction to modify a row only if changes to the row made by other transactions were already committed when the serializable transaction began. The database generates an error when a serializable transaction tries to update or delete data changed by a different transaction that committed after the serializable transaction began:
ORA-08177: Cannot serialize access for this transaction
쓰기 스큐와 팬텀
쓰기 스큐: 두 트랜잭션이 동시에 조건을 확인하고 충돌 없이 각자 업데이트했지만, 전체적으로 무결성이 깨지는 상황
팬텀:
단순히 새 행 삽입이 아니라 아래 조건 역시 팬텀
변화 유형 예
| INSERT | 새로 조건을 만족하는 행 삽입됨 |
| UPDATE | 기존 행이 조건을 만족하도록 바뀜 |
| DELETE | 조건 범위의 행이 사라져서 COUNT 등 조건이 달라짐 |
아래 예시와 같이 동시에 같은 조건의 행을 읽고, 각각 조건을 만족한다고 판단 후 수정
결과적으로 트랜잭션 도중 다른 트랜잭션에 의해 조건의 결과가 깨지게 됨

쓰기 스큐는 두 트랜잭션이 같은 객체를 읽어서 그 중 일부를 갱신할 때 나타날 수 있음
쓰기 스큐 방지 방법?
- Next Key Lock: MySQL InnoDB 엔진에서 REPEATBLE READ 격리 수준에 사용되는 방식으로 인덱스의 레코드 (record lock) + 레코드의 바로 인접한 사이의 간격(gap lock) 을 잠궈 팬텀을 방지
- 색인 범위 잠금이란? 색인(인덱스)을 기준으로 특정 범위에 대해 공유 잠금 또는 배타 잠금을 거는 방식
//users 테이블에 age 로 index 가 설정되었다고 가정
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
// InnoDB는 age 인덱스를 따라 20 ~ 30 사이 각 row + 그 사이의 gap에 대해
// next key lock을 걸게됨
// 다른 트랜잭션은 해당 범위 안 데이터에 대해 INSERT, UPDATE, DELETE 를 할 수 없음
- 직렬성 격리수준 이용
Serializable
직렬성 트랜잭션을 구현하는 3가지 방법
- 트랜잭션 순서대로 실행하기: 각 트랜잭션 실행 시간이 짧고 트랜잭션 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 처리량 낮아야 함
- 2단계 잠금(Two-Phased Locking): 직렬성을 구현하는 가장 표준적인 방법. 비관적(pessimistic) 동시성 제어로 다음과 같이 동작
- 트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 쓰기를 원한다면 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다
- 트랜잭션 A가 객체를 쓰고 트랜잭션 B가 객체를 읽기 원한다면 마찬가지로 B는 A가 커밋되거나 어보트 될 때까지 기다려야 한다
- 직렬성 스냅숏 격리(SSI): 낙관적 동시성 제어 과거의 읽기에 영향을 미치는 쓰기 감지 & 오래된 MVCC 버전을 읽었는지 감지, 트랜잭션이 커밋을 원할 때 실행이 직렬적이지 않으면 어보트 시킴기본적으로 스냅샷 격리를 실행하지만 트랜잭션 간의 읽기-쓰기 충돌을 모니터링하여(트랜잭션 간 의존성 그래프를 이용) 트랜잭션 그래프에서 동시에 실행되는 트랜잭션의 anomaly 를 식별하고 anomaly가 발생하지 않도록 트랜잭션을 롤백
PostgreSQL 의 Serializable 에서 구현됨
읽기는 쓰기를 차단하지 않고, 쓰기도 읽기를 차단하지 않음 → 2PL 보다 성능 우수
//session 1
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 1;
INSERT INTO mytab VALUES (2, 30);
//session 2
BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 2;
//session 1에서 읽은 데이터를 session 2에서 변경
INSERT INTO mytab VALUES (1, 300);
//session2 에서 정상적으로 커밋
COMMIT;
//session 1
//session 1에서 커밋 시, ERROR 발생
//트랜잭션을 커밋하려고 할 때 데이터베이스는 무시된 쓰기 중 커밋된게 있는지 확인 후
//커밋된 것이 있으면 어보트 시킴
COMMIT;
ERROR: could not serialize access
due to read/write dependencies
among transactions
DETAIL: Cancelled on identification
as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
출처:
https://docs.oracle.com/cd/E28271_01/server.1111/e25789/consist.htm#BABEIHGJ
https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html
https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/
https://docs.oracle.com/cd/E17952_01/mysql-5.7-en/innodb-next-key-locking.html#:~:text=To prevent phantoms%2C InnoDB uses,the index records it encounters.
https://wiki.postgresql.org/wiki/SSI
https://wiki.postgresql.org/wiki/Serializable#Cahill2009
데이터 중심 애플리케이션 설계 7장
'공부 > Database' 카테고리의 다른 글
| [Desinging Data-Intensive Applications] 02장. 데이터 모델과 질의 언어 (0) | 2025.02.16 |
|---|---|
| [Desinging Data-Intensive Applications] 03장. 저장소와 검색 (5) | 2025.02.08 |