트랜잭션(Transaction)의 개념과 ACID 원칙
Series: 데이터베이스
들어가며
데이터베이스를 다루다 보면 '이 작업은 반드시 함께 성공해야 한다'라는 상황을 자주 마주하게 된다. 단순히 데이터를 하나 수정하는 수준이 아니라, 여러 개의 작업이 하나의 흐름으로 묶여야 하는 경우이다. 대표적인 예로 은행 송금을 생각해보자. A 계좌에서 B 계좌로 10,000원을 이체한다고 하면 내부적으로는 최소 두 가지 작업이 필요하다.
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';이 두 작업은 반드시 함께 성공해야 한다. 만약 A의 잔액만 줄어들고 B의 잔액이 증가하지 않는다면 돈이 사라지는 문제가 발생한다. 반대로 B의 잔액만 증가하고 A의 잔액이 줄어들지 않아도 문제가 된다. 이처럼 여러 작업을 하나의 묶음으로 처리하고, 그 묶음이 안전하게 실행되도록 보장하는 개념이 바로 트랜잭션(Transaction)이다. 그리고 이 트랜잭션이 제대로 동작하기 위해 지켜야 하는 대표적인 원칙이 ACID이다. 이번 글에서는 트랜잭션이 무엇인지, 왜 필요한지, 그리고 ACID 원칙이 각각 어떤 의미를 가지는지 실제 예시를 통해 자세히 살펴보겠다.
1. 트랜잭션이란?
트랜잭션은 데이터베이스에서 실행되는 여러 작업을 하나의 논리적인 단위로 묶은 것이다. 이 묶음은 '전부 성공하거나, 전부 실패해야 한다'는 특징을 가진다. 데이터베이스에서는 보통 다음과 같은 흐름으로 트랜잭션을 사용한다.
BEGIN;
-- 여러 작업 실행 // [!code highlight]
COMMIT;작업 중 문제가 발생하면 다음과 같이 되돌릴 수 있다.
ROLLBACK;이 구조는 단순하지만 매우 중요하다. 트랜잭션이 없다면 여러 쿼리가 중간 상태로 남을 수 있기 때문이다.
2. 트랜잭션이 필요한 이유
트랜잭션이 왜 필요한지는 실제 예시를 보면 쉽게 이해할 수 있다.
2.1. 주문 생성 예시
사용자가 상품을 주문하는 상황을 생각해보자. 주문이 생성될 때 내부적으로는 여러 작업이 함께 수행된다.
flowchart TB
A[주문 데이터 생성] --> B[주문 상품 저장]
B --> C[재고 차감]
C --> D[결제 정보 저장]
C --> E[오류 발생]
E --> F[중간 상태 발생]
F --> G1[주문은 생성됨]
F --> G2[주문 상품도 저장됨]
F --> G3[재고는 그대로]
F --> G4[결제는 없음]-- 1. 주문 데이터 생성
INSERT INTO orders (id, user_id, status) VALUES (1, 10, 'PAID');
-- 2. 주문 상품 저장
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 100, 2);
-- 3. 재고 차감
UPDATE products SET stock = stock - 2 WHERE id = 100;
-- 4. 결제 정보 저장
INSERT INTO payments (order_id, amount, status) VALUES (1, 30000, 'PAID');만약 재고 차감 단계에서 오류가 발생하면 어떻게 될까? 이 작업을 트랜잭션 없이 처리하면 다음과 같은 문제가 생길 수 있다.
flowchart LR
subgraph 정상 흐름
A1[주문 생성] --> B1[주문 상품 저장] --> C1[재고 차감] --> D1[결제 저장] --> E1[COMMIT]
end
subgraph 실패 흐름 (트랜잭션 없음)
A2[주문 생성] --> B2[주문 상품 저장] --> C2[재고 차감 실패]
C2 --> F2[주문은 생성됨]
C2 --> G2[주문 상품 저장됨]
C2 --> H2[재고는 그대로]
C2 --> I2[결제 없음]
end즉, 데이터가 중간 상태로 남게 된다. 이 문제를 해결하기 위해 트랜잭션으로 묶는다.
BEGIN;
INSERT INTO orders (id, user_id, status) VALUES (1, 10, 'PAID');
INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 100, 2);
UPDATE products SET stock = stock - 2 WHERE id = 100;
INSERT INTO payments (order_id, amount, status) VALUES (1, 30000, 'PAID');
COMMIT;중간에 하나라도 실패하면 전체를 롤백한다.
ROLLBACK;flowchart TB
A[트랜잭션 시작] --> B[주문 생성]
B --> C[주문 상품 저장]
C --> D[재고 차감]
D --> E[결제 저장]
E --> F[COMMIT]
D --> G[오류 발생]
G --> H[ROLLBACK]
H --> I[모든 작업 취소]이렇듯 트랜잭션은 여러 작업을 하나의 단위로 묶어서 데이터의 정합성을 유지해준다.
3. ACID 원칙
트랜잭션이 안전하게 동작하기 위해서는 몇 가지 조건이 필요하다. 이를 정리한 것이 바로 ACID 원칙이다. ACID는 다음 네 가지로 구성된다.
- Atomicity (원자성)
- Consistency (일관성)
- Isolation (격리성)
- Durability (지속성)
이 네 가지는 각각 다른 관점에서 트랜잭션의 안정성을 보장한다.
4. Atomicity: 원자성
원자성은 트랜잭션 안의 작업들이 모두 성공하거나, 모두 실패해야 한다는 원칙이다. 앞서 본 송금 예시를 다시 보자.
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
COMMIT;이 중 하나라도 실패하면 전체를 되돌린다.
ROLLBACK;원자성은 자판기로 쉽게 이해할 수 있다.
flowchart TB
A[돈을 넣는다] --> B[음료가 나온다]
B --> C[거래 완료]
B --> D[문제 발생]
D --> E[돈 반환]이 돈을 넣고, 음료가 나오는 작업은 항상 함께 성공해야 한다. 돈만 들어가고 음료가 나오지 않거나, 돈을 넣지도 않았는데 음료가 나오면 문제가 된다. 트랜잭션도 마찬가지다. 여러 작업이 묶여 있다면, 중간 상태는 허용되지 않는다.
5. Consistency: 일관성
일관성은 트랜잭션이 실행된 후에도 데이터가 '정해진 규칙을 만족해야 한다'는 원칙이다. 예를 들어 계좌 잔액은 음수가 될 수 없다는 규칙이 있다고 해보자.
ALTER TABLE accounts ADD CONSTRAINT positive_balance CHECK (balance >= 0);이 규칙이 있다면 어떤 트랜잭션도 잔액을 음수로 만들 수 없다. 또 다른 예로 재고를 생각해보자.
UPDATE products SET stock = stock - 1 WHERE id = 100 AND stock >= 1;이 쿼리는 재고가 1개 이상일 때만 감소한다. 결과적으로 재고는 음수가 되지 않는다. 한가지 중요한 점은 일관성은 데이터베이스만으로 완전히 보장되지 않는다는 것이다. 데이터베이스는 NOT NULL, UNIQUE, FOREIGN KEY 같은 제약 조건을 통해 기본적인 무결성을 보장하지만, 서비스에서 요구하는 모든 비즈니스 규칙까지 자동으로 보장해주지는 않는다. 예를 들어 WHERE절을 통해 재고가 음수가 되지 않아야 한다거나, 결제 완료된 주문만 배송할 수 있다는 규칙은 애플리케이션 로직에서 직접 검증해야 한다. 따라서 일관성은 데이터베이스와 애플리케이션이 함께 책임지는 영역이라고 볼 수 있다.
6. Isolation: 격리성
격리성은 여러 트랜잭션이 동시에 실행될 때 서로 영향을 주지 않도록 분리하는 원칙이다. 재고가 1개 남은 상품을 두 명이 동시에 주문한다고 해보자.
sequenceDiagram
participant A as 사용자 A
participant DB as DB
participant B as 사용자 B
A->>DB: 재고 조회 → 1
B->>DB: 재고 조회 → 1
A->>DB: 재고 차감
B->>DB: 재고 차감
DB-->>A: 성공
DB-->>B: 성공결과적으로 재고는 하나인데 주문은 두 개가 성공한다. 이런 문제를 막기 위해 격리성이 필요하다. 격리성을 어떻게 보장할지는 격리 수준(Isolation Level)에 따라 달라진다.
flowchart LR
A[Read Uncommitted] --> B[Read Committed]
B --> C[Repeatable Read]
C --> D[Serializable]격리 수준이 높을수록 안전하지만, 성능은 떨어질 수 있다. 즉, 격리성은 항상 강하게 가져가는 것이 아니라 상황에 맞게 선택해야 한다.
7. Durability: 지속성
지속성은 트랜잭션이 성공적으로 커밋되면, 그 결과는 절대 사라지지 않아야 한다는 원칙이다.
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 1;
COMMIT;이후 서버가 꺼지거나 장애가 발생하더라도, PAID 상태는 유지되어야 한다. 데이터베이스는 로그(WAL)를 사용해서 지속성을 보장한다.
WAL: Write-Ahead Logging, 데이터를 쓰기 전에, 먼저 로그를 쓰는 방식을 의미한다.
flowchart TB
A[데이터 변경] --> B[로그에 먼저 기록]
B --> C[COMMIT]
C --> D[데이터 파일 반영]
C --> E[장애 시 로그로 복구]즉, 커밋이 완료된 순간부터는 장애가 발생하더라도 데이터를 복구할 수 있다.
8. ACID를 하나로 이해하기
ACID는 각각 따로 떨어진 개념이 아니라, 하나의 트랜잭션 안에서 함께 작동한다.
flowchart TB
T[트랜잭션] --> A[Atomicity<br/>모두 성공/실패]
T --> C[Consistency<br/>규칙 유지]
T --> I[Isolation<br/>동시성 제어]
T --> D[Durability<br/>영구 저장]예를 들어 주문 생성 트랜잭션을 다시 보면 ACID가 함께 적용되어 있는 것을 알 수 있다.
- 원자성 → 모든 작업이 함께 성공해야 함
- 일관성 → 재고 음수 금지, FK 유지
- 격리성 → 동시에 주문해도 문제 없어야 함
- 지속성 → 커밋 후 데이터는 사라지지 않음
9. 실무에서의 관점
ACID는 데이터베이스가 제공하는 매우 강력한 보장이다. 이 네 가지 원칙 덕분에 우리는 데이터가 쉽게 깨지지 않을 것이라는 기본적인 신뢰를 가지고 시스템을 설계할 수 있다. 하지만, ACID가 있다고 해서 모든 문제가 자동으로 해결되는 것은 아니다. 오히려 실무에서는 'ACID를 어떻게 사용하는가'에 따라 시스템의 안정성이 크게 달라진다. 데이터베이스는 트랜잭션이라는 도구를 제공할 뿐이고, 그 도구를 올바르게 사용하는 책임은 결국 개발자에게 있다. 즉, ACID는 '보장'이라기보다는 '보장할 수 있는 기반'에 가깝다.
-
원자성은 트랜잭션으로 묶었을 때만 의미가 있다. 여러 쿼리를 순차적으로 실행하면서 트랜잭션을 사용하지 않는다면, 중간에 하나라도 실패했을 때 데이터는 그대로 중간 상태로 남게 된다. 데이터베이스는 이러한 작업들이 하나의 논리적인 작업이라는 사실을 알지 못하기 때문에, 개발자가 직접 트랜잭션으로 묶어주지 않으면 원자성은 보장되지 않는다.
-
일관성도 마찬가지다. 데이터베이스는
NOT NULL,UNIQUE,FOREIGN KEY같은 기본적인 제약 조건은 지켜주지만, 서비스의 비즈니스 규칙까지 이해하지는 못한다. 예를 들어 재고는 음수가 되면 안 된다거나, 결제 완료된 주문만 배송할 수 있다는 규칙은 데이터베이스가 자동으로 판단해주지 않는다. 이런 부분은 조건을 포함한 쿼리나 애플리케이션 로직으로 직접 보완해야 한다. 즉, 잘못된 쓰기 쿼리(Write Query,INSERT,UPDATE,DELETE) 하나만으로도 일관성은 쉽게 깨질 수 있다. -
격리성은 더 미묘한 문제다. 트랜잭션을 사용하고 있다고 해서 동시성 문제가 자동으로 사라지는 것은 아니다. 기본 격리 수준에서는 동시에 실행되는 트랜잭션 간에 간섭이 발생할 수 있고, 이로 인해 재고가 음수가 되거나 같은 데이터를 중복 수정하는 문제가 생길 수 있다. 이런 상황에서는 단순히 트랜잭션을 사용하는 것만으로는 부족하고, 조건부 UPDATE, 락(Lock), 혹은 적절한 격리 수준 설정까지 함께 고려해야 한다.
-
지속성 역시 'COMMIT 했으니 안전하다'라고 단순하게 생각할 수 있는 문제가 아니다. 데이터베이스는 WAL 같은 메커니즘을 통해 지속성을 보장하지만, 설정에 따라 동기화 시점이나 디스크 기록 방식이 달라질 수 있다. 또한 백업 전략이 없다면 물리적인 장애 상황에서는 데이터를 복구하지 못할 수도 있다. 즉, 지속성은 데이터베이스 내부 기능뿐만 아니라 운영 환경과 설정까지 포함된 문제다.
결국 실무에서 중요한 것은 단지 ACID를 알고 있는 것만이 전부가 아니라, 어떤 상황에서 어떤 원칙이 깨질 수 있는지 이해하고, 그에 맞게 설계하는 것이다. 예를 들어 다음과 같은 질문을 스스로 던질 수 있어야 한다.
- 이 작업은 하나의 트랜잭션으로 묶어야 하는가?
- 트랜잭션 범위는 어디까지가 적절한가?
- 이 UPDATE 쿼리는 동시성 상황에서도 안전한가?
- 이 데이터는 어떤 규칙을 반드시 지켜야 하는가?
- 장애가 발생했을 때 이 데이터는 어떻게 복구되는가?
이러한 고민 없이 단순히 트랜잭션을 사용하거나 ORM에 맡겨버리면, 겉으로는 정상 동작하는 것처럼 보여도 실제 운영 환경에서는 데이터가 깨지는 문제가 발생할 수 있다. 그래서 실무에서는 다음과 같은 태도가 중요하다.
트랜잭션을 언제 사용할지, 어디까지 묶을지, 어떤 방식으로 데이터를 수정할지까지 의도적으로 설계하는 것
ACID는 그 설계를 위한 기준이다. 결국 좋은 시스템은 ACID를 믿는 시스템이 아니라, ACID가 깨질 수 있는 지점을 이해하고 방어하는 시스템이라고 볼 수 있다.
마치며
트랜잭션은 데이터베이스에서 여러 작업을 하나로 묶어 안전하게 처리하기 위한 핵심 개념이다. 그리고 ACID는 이 트랜잭션이 신뢰할 수 있게 동작하도록 만들어주는 네 가지 원칙이다. 원자성은 '모두 성공하거나 모두 실패'를 보장하고, 일관성은 데이터가 항상 규칙을 만족하도록 만든다. 격리성은 동시에 실행되는 트랜잭션 간의 간섭을 제어하고, 지속성은 커밋된 결과가 사라지지 않도록 보장한다.
이 개념들은 단순한 이론이 아니라, 실제 서비스에서 데이터 정합성을 지키기 위한 가장 기본적인 토대이다. 특히 결제, 주문, 재고, 포인트, 계좌 같은 중요한 도메인에서는 트랜잭션과 ACID를 제대로 이해하는 것이 필수적이다. 결국 중요한 것은 '데이터가 깨지지 않도록 어떻게 설계할 것인가?'이다. ACID는 그 설계를 위한 가장 기본적인 기준이라고 볼 수 있다.