트랜잭션
트랜잭션은 하나의 작업 단위로 취급되는 SQL 쿼리들의 모음입니다. 보통 트랜잭션은 데이터를 변경하고 수정하는 데 사용되는데요. 일관성을 유지하기 위해 읽기 전용 트랜잭션을 가질 수도 있습니다. 또한, 사용자가 정의하든 암시적으로 정의되든 간에 모든 쿼리는 트랜잭션 내에서 실행됩니다.
생애주기
- 새로운 트랜잭션을 시작하겠다는 BEGIN으로 시작됩니다.
- COMMIT을 통해 변경사항을 반영하여 디스크에 영구적으로 저장합니다.
- ROLLBACK을 통해 변경사항을 영구적으로 저장하지 않고 돌려놓습니다.
- 트랜잭션이 비정상적으로 종료됐을 때, ROLLBACK 해야합니다.
예시
계좌 입금을 사례로 들겠습니다. 먼저, SELECT로 충분한 돈이 있는지 확인해야 합니다. UPDATE로 계좌 돈을 차감하며, 또다시 UPDATE로 계좌 돈을 입금합니다. 이것은 한 트랜잭션 안에서 수행되어야 합니다. 곧이어 말할 트랜잭션의 성질 덕분에 계좌에서 돈이 차감되고, 입금이 안 되는 일은 없을 테니까요. 수행 과정을 SQL문으로 나타내면 다음과 같습니다.
- BEGIN TX1
- SELECT BALANCE FROM ACCOUNT WHERE ID = 1
- BALANCE > 100
- UPDATE ACCOUNT SET BALANCE = BALANCE - 100 WHERE ID = 1
- UPDATE ACCOUNT SET BALANCE = BALANCE + 100 WHERE ID = 2
- COMMIT TX1
원자성(Atomicity)
트랜잭션 내의 모든 쿼리는 성공해야 합니다. 즉, 한 쿼리가 실패하면 트랜잭션은 즉시 롤백되어야 합니다. 또한, 데이터베이스가 커밋되기 전에 다운된다면 해당 트랜잭션도 즉시 롤백되어야 합니다.
예시
- 계좌 입금 시 Account 1의 계좌에서 100달러를 차감하고, 데이터베이스가 다운됐습니다.
- 만약 원자성이 보장되지 않는다면, 단순히 100달러를 잃어버리게 됩니다.
- 원자적 트랜잭션은 하나 이상의 쿼리가 실패할 경우 모든 쿼리를 롤백시킵니다.
- 데이터베이스를 복구 및 재시작 후 트랜잭션을 정리(롤백) 해야 합니다.
고립성(Isolation)
여러 트랜잭션이 동시에 실행되어도 각 트랜잭션은 독립적으로 실행되는 것처럼 보이게 하는 것입니다. 데이터베이스의 성능을 위해서 고립성을 완전히 지키기는 사실상 어렵습니다. 그래서 고립 수준과 읽기 현상을 정의할 수 있습니다.
읽기 현상(Read Phenomena)
읽기 현상은 여러 트랜잭션이 처리되면서 발생할 수 있는 이상 현상입니다. 읽기 현상에는 Dirty Reads, Non-repeatable Reads, Phantom Reads, Lost Updates가 있습니다. 다음 Sales 테이블을 기준으로 예시와 함께 살펴봅시다.
| PID | QNT | PRICE |
| Product 1 | 10 | 5 |
| Product 2 | 20 | 4 |
Dirty Reads
나의 트랜잭션이 다른 트랜잭션에서 아직 커밋되지 않은 것을 읽을 수 있는 읽기 현상입니다. 이 경우 방금 읽은 내용이 커밋될 수도 있지만, 데이터베이스 충돌 및 에러 등의 이유로 롤백될 수도 있습니다.
| BEGIN TX1 | BEGIN TX2 |
| SELECT PID, QNT*PRICE FROM SALES | |
| UPDATE SALES SET QNT = QNT + 5 WHERE PID = 1 | |
| SELECT SUM(QNT*PRICE) FROM SALES | |
| COMMIT TX1 | ROLLBACK TX2 |
위의 예시의 경우 TX1에서 처음 데이터를 조회했을 때는 (50, 80)의 결과가 나왔으나 TX2 업데이트의 영향을 받아 합이 155로 나오게 됩니다. 심지어, TX2는 롤백하여 정확한 정보도 아니게 되었습니다.
Non-repeatable Reads
나의 트랜잭션이 무언가를 읽은 후에 동일한 값을 다시 읽으려고 할 때 (집계), 다른 트랜잭션의 커밋에 의해 그 값이 변경(UPDATE) 되는 경우를 말합니다.
| BEGIN TX1 | BEGIN TX2 |
| SELECT PID, QNT*PRICE FROM SALES | |
| UPDATE SALES SET QNT = QNT + 5 WHERE PID = 1 | |
| COMMIT TX2 | |
| SELECT SUM(QNT*PRICE) FROM SALES | |
| COMMIT TX1 |
위의 예시의 경우 TX1에서 처음 데이터를 조회했을 때는 (50, 80)의 결과가 나왔으나 TX2 업데이트의 영향을 받아 합이 155로 나오게 됩니다. 커밋한 결과이긴 하나 커밋 이전의 값 집계를 원했다면 원하는 결과는 아닐 것입니다.
Phantom Reads
나의 트랜잭션이 무언가를 읽은 후에 동일한 값을 다시 읽으려고 할 때 (집계), 다른 트랜잭션의 커밋에 의해 데이터가 삽입(INSERT) 혹은 삭제(DELETE) 되는 경우를 말합니다.
| BEGIN TX1 | BEGIN TX2 |
| SELECT PID, QNT*PRICE FROM SALES | |
| INSERT INTO SALES VALUES (’Product 3’, 10, 1) | |
| COMMIT TX2 | |
| SELECT SUM(QNT*PRICE) FROM SALES | |
| COMMIT TX1 |
위의 예시의 경우 TX1에서 처음 데이터를 조회했을 때는 (50, 80)의 결과가 나왔으나 TX2 삽입의 영향을 받아 합이 140으로 나오게 됩니다. 커밋한 결과이긴 하나 커밋 이전의 값 집계를 원했다면 원하는 결과는 아닐 것입니다.
Lost Updates
상대 트랜잭션이 커밋하기 전에 값을 읽어 업데이트한 내용이 사라지는 경우입니다.
| BEGIN TX1 | BEGIN TX2 |
| UPDATE SALES SET QNT = QNT + 10 WHERE PID = 1 | |
| UPDATE SALES SET QNT = QNT + 5 WHERE PID = 1 | |
| COMMIT TX2 | |
| SELECT SUM(QNT*PRICE) FROM SALES | |
| COMMIT TX1 |
위의 예시의 경우 TX1에서 원본에 10을 더하고, TX2에서 원본에 5를 더하여 원본 값은 손실됩니다. TX2를 커밋한 이후 TX1에서 집계를 했을 때 TX1은 180을 기대했겠지만, TX2에 의해 155를 받게 됩니다.
고립 수준(Isolation Levels)
고립 수준은 어떤 트랜잭션이 데이터를 조회할 때, 다른 트랜잭션이 해당 데이터를 변경했을 때의 영향 수준입니다.
- READ UNCOMMITTED - 고립성이 없습니다. 외부에서의 모든 변경 사항은 커밋되었는지 여부에 상관없이 트랜잭션에게 보입니다. Dirty Reads가 발생할 수 있습니다.
- READ COMMITTED - 트랜잭션 내의 각 쿼리는 트랜잭션에 의해 커밋된 변경사항만을 볼 수 있습니다.
- REPEATABLE READ - Non-repeatable Reads를 해결하기 위해 고안된 고립 수준입니다. 동일한 트랜잭션 내에 있는 한에 반복하여 읽어도 행이 변경되지 않도록 보장됩니다.
- SNAPSHOT - 각 쿼리는 트랜잭션의 시작 시점까지 커밋된 변경 사항만을 볼 수 있습니다. 그것은 그 순간 전체 데이터베이스의 스냅샷과 같습니다. 모든 읽기 현상을 제거하는 것이 보장됩니다.
- SERIALIZABLE - 더 이상 동시성이 없습니다. 거의 물리적으로 각 트랜잭션이 데이터베이스에 연이어 직렬화된 것처럼 구현됩니다.

데이터베이스 구현
- 각 DBMS는 고립 수준을 서로 다르게 구현합니다.
- 비관적 접근 방식(Pessimistic)에는 행 레벨 잠금, 테이블 잠금, 페이지 잠금 등이 있습니다.
- 낙관적 접근 방식(Optimistic)에는 잠금을 사용하지 않습니다. 상황에 맞게 처리하다가 실제로 트랜잭션이 서로 충돌하기 시작하면, 그 순간에 트랜잭션을 실패시킵니다.
- REPEATABLE READ는 읽고 있는 행을 잠급니다. 많은 행을 읽는 경우 비용이 많이 들 수 있습니다. Postgres는 이것을 SNAPSHOT으로 구현합니다. 따라서 Postgres에서는 Phantom Reads가 발생하지 않습니다.
- SERIALIZABLE은 일반적으로 적극적인 동시성 제어로 구현됩니다. 실제로 직렬화를 하면 데이터베이스가 너무 느려지기 때문입니다. SELECT FOR UPDATE를 사용하여 비관적으로 구현할 수 있습니다.
일관성(Consistency)
트랜잭션 처리 전과 처리 후 데이터 모순이 없는 상태를 유지하는 것을 의미합니다. 일관성에는 데이터의 일관성과 읽기의 일관성이 있습니다.
데이터의 일관성(Consistency In Data)
사용자에 의해 정의되며, 참조 무결성과 외래키를 강제하는 것이 중요합니다. 원자성, 고립성이 기본적으로 보장되어야 합니다.
| ID (PK) | BLOB | LIKES |
| 1 | xx | 5 |
| 2 | xx | 1 |
| USER (PK) | PICTURE_ID (FK) |
| Jon | 1 |
| Edmond | 1 |
| Jon | 2 |
| Edmond | 4 |
다음은 두 테이블의 관계를 보면 일관성이 없고 참조 무결성이 깨진 것을 볼 수 있습니다. 먼저, 사진 1에는 5개의 좋아요가 있으나 실제로 2개의 좋아요만을 받았습니다. 또한, Edmond가 사진 4에 좋아요를 눌렀으나 사진 4는 없습니다.
읽기의 일관성(Consistency In Reads)
트랜잭션이 변경을 커밋했다면, 새로운 트랜잭션은 즉시 변경사항을 볼 수 있어야 합니다. 이것이 보장되지 않는다면 데이터 불일치로 인해 시스템 전체에 피해를 줄 수 있습니다. 최종 일관성은 지금은 일관성이 없지만, 결국은 일관성을 갖게 되도록 보장한다는 의미입니다. 분산 컴퓨팅에서 고가용성을 달성하기 위해 사용되는 일관성 모델입니다.
지속성(Durability)
커밋된 트랜잭션에 의해 변경된 것들은 SSD나 하드 드라이브와 같은 지속성이 있는 비휘발성 저장소에 저장되어야 합니다. 지속성 기술로는 WAL, 비동기 스냅샷, AOF 등이 있습니다. WAL은 Write-Ahead Logging으로 데이터 파일에 실제로 반영하기 전에 모든 변경사항을 로그에 먼저 기록하는 방식입니다. 비동기 스냅샷은 백그라운드에서 비동기적으로 한꺼번에 모든 것을 디스크에 스냅샷하는 기술입니다. AOF는 Append Only File로 모든 변경 연산을 순차적으로 파일에 기록하는 방식입니다. 주로 Redis와 같은 인메모리 데이터 저장소에서 사용됩니다.
OS Cache
실제로 OS에 쓸 때 데이터베이스가 디스크에 쓰도록 윈도우나 리눅스 같은 운영 체제에 요청하면, OS는 디스크에 쓰지 않고 자신의 메모리 캐시에 씁니다. 이러면 시스템 충돌이 발생했을 때 데이터를 손실할 수 있습니다. 이때, Fsync 명령어를 통해 OS 캐시를 우회할 수 있습니다. 하지만 Fsync는 비용이 많이 들고 성능을 저하시킬 수 있습니다.
출처
https://www.udemy.com/course/database-engineering-korean/
이상으로 트랜잭션과 ACID 정리를 마칩니다. 감사합니다.
'Database' 카테고리의 다른 글
| [Database] PostgreSQL이란? (vs NoSQL, 타 RDBMS) (0) | 2025.10.24 |
|---|---|
| [Database] PostgreSQL 입문 (with 생활코딩) - (3) CRUD (1) | 2023.10.08 |
| [Database] PostgreSQL 입문 (with 생활코딩) - (2) Database, Schema, Table 생성 (4) | 2023.10.07 |
| [Database] PostgreSQL 입문 (with 생활코딩) - (1) 개념, 설치, 접속 (0) | 2023.10.03 |