트랜잭션의 개념
계좌 이체 중 서버가 죽으면? — 돈은 빠져나갔는데 안 들어온 상황을 트랜잭션이 막아줍니다. 데이터 일관성의 핵심입니다. 트랜잭션은 관계형 데이터베이스가 신뢰할 수 있는 시스템으로 기능하는 근본적인 메커니즘이며, 금융, 의료, 전자상거래 등 데이터 정확성이 생명인 모든 시스템의 기반입니다.
트랜잭션이란
트랜잭션(Transaction)은 하나의 논리적 작업 단위를 구성하는 SQL 문들의 집합입니다. "전부 성공하거나, 전부 취소되거나" — 중간 상태는 허용하지 않습니다. 데이터베이스를 하나의 일관된 상태에서 다른 일관된 상태로 변환하는 과정이라 정의할 수 있습니다.
원자성 (Atomicity)
원자성은 트랜잭션 안의 작업이 전부 성공하거나 전부 취소되어야 한다는 뜻입니다. 계좌 A에서 돈을 빼는 UPDATE와 계좌 B에 돈을 넣는 UPDATE가 한 트랜잭션이라면, 둘 중 하나만 최종 결과로 남아서는 안 됩니다.
원자성은 주로 Undo 로그와 ROLLBACK 동작으로 보장됩니다. 아직 커밋되지 않은 변경은 장애나 명시적 롤백이 발생했을 때 시작 전 상태로 되돌릴 수 있어야 합니다.
일관성 (Consistency)
일관성은 트랜잭션이 실행되기 전과 후에 데이터베이스가 일관된 상태를 유지해야 한다는 것입니다. "일관된 상태"란 모든 제약 조건(기본키, 외래키, CHECK, 도메인 등)이 만족되는 상태를 의미합니다.
일관성의 책임은 DBMS와 애플리케이션 양쪽에 있습니다. DBMS는 선언적 제약 조건(PRIMARY KEY, FOREIGN KEY, CHECK, NOT NULL)을 자동으로 검사합니다. 그러나 이체 전후 총합이 같아야 한다는 비즈니스 규칙은 애플리케이션 로직이나 트리거에서 보장해야 합니다.
격리성 (Isolation)
격리성은 동시에 실행되는 여러 트랜잭션이 서로의 미완료 상태에 함부로 영향을 받지 않도록 제한하는 성질입니다. 이상적으로는 각 트랜잭션이 혼자 실행된 것처럼 보이지만, 실제 DBMS는 격리 수준에 따라 허용되는 간섭 범위를 조절합니다.
격리성이 없으면 어떤 문제가 발생하는지 살펴보겠습니다.
격리성은 락(Lock)이나 MVCC(Multi-Version Concurrency Control)로 구현됩니다. 락은 충돌 가능성이 있는 읽기·쓰기 범위를 제한하는 방식이며, 공유락과 배타락처럼 종류에 따라 막는 동작이 다릅니다. MVCC는 데이터의 여러 버전을 유지하여 읽기와 쓰기가 서로 덜 방해하도록 만들지만, 쓰기 충돌이나 범위 조건에서는 여전히 락이나 재시도가 필요할 수 있습니다.
격리 수준(Isolation Level)에 따라 격리성의 정도를 조절할 수 있으며, 이는 다음 절에서 상세히 다룹니다.
지속성 (Durability)
지속성은 트랜잭션이 성공적으로 커밋되면 그 결과가 장애 이후에도 복구 가능해야 한다는 것입니다. 실제 내구성은 WAL/Redo flush, 동기 커밋 설정, 스토리지 안정성에 영향을 받습니다.
지속성은 보통 Redo 로그(Redo Log)와 WAL(Write-Ahead Logging) 원칙으로 보장됩니다. WAL 원칙은 데이터 파일보다 로그를 먼저 안정적인 저장소에 기록한다는 것입니다. 이렇게 하면 시스템 장애 시 로그를 이용하여 커밋된 변경 사항을 재적용(Redo)할 수 있습니다.
각 속성의 비용
ACID를 보장하는 것은 성능 비용이 따릅니다. 데이터베이스가 안전성을 위해 치르는 대가입니다.
| 속성 | 비용 | 설명 |
|---|---|---|
| 원자성 | Undo 로그 기록 | 모든 변경 전 원래 값을 기록 |
| 일관성 | 제약 조건 검사 | INSERT/UPDATE마다 CHECK, FK 검증 |
| 격리성 | 락 대기, 버전 관리 | 동시성↓ 또는 메모리↑ |
| 지속성 | Redo/WAL 기록 | 동기 커밋·group commit 설정에 따라 비용 발생 |
이 비용 때문에 모든 상황에서 가장 강한 격리 수준과 가장 보수적인 동기화 정책을 쓰는 것은 비현실적입니다. 예를 들어, SNS의 좋아요 카운트처럼 약간의 지연이나 재계산이 허용되는 경우에는 낮은 격리 수준, 비동기 집계, eventual consistency를 선택할 수 있습니다. 반면 금융 거래처럼 정확성이 중요한 경우에는 강한 제약 조건과 충분한 격리 수준이 필요합니다.
트랜잭션 SQL 명령어
트랜잭션을 제어하는 SQL 명령어를 TCL(Transaction Control Language)이라 합니다.
명령어 이름은 DBMS와 드라이버에 따라 조금씩 다릅니다. 예를 들어 MySQL은 START TRANSACTION 또는 BEGIN, PostgreSQL은 BEGIN, SQL Server는 BEGIN TRANSACTION을 자주 사용합니다. 예제의 핵심은 문법 암기가 아니라 트랜잭션 경계를 명확히 열고 닫는 것입니다.
COMMIT과 ROLLBACK
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 1000000
WHERE account_id = 'A';
UPDATE accounts SET balance = balance + 1000000
WHERE account_id = 'B';
COMMIT; -- 두 UPDATE 모두 영구 반영
-- 커밋 로그가 안전하게 기록된 뒤에는 장애 복구 대상이 됨BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 1000000
WHERE account_id = 'A';
-- B 계좌가 동결 상태라면?
UPDATE accounts SET balance = balance + 1000000
WHERE account_id = 'B'; -- 실패!
ROLLBACK; -- A 계좌의 출금도 취소됨
-- 데이터베이스는 트랜잭션 시작 전 상태로 복원오류가 난 SQL 문을 만났을 때 트랜잭션을 계속 진행할 수 있는지는 DBMS마다 다릅니다. 예를 들어 PostgreSQL은 오류 후 트랜잭션이 실패 상태가 되어 ROLLBACK 또는 SAVEPOINT 처리가 필요합니다.
SAVEPOINT
SAVEPOINT는 트랜잭션 내부에 중간 저장 지점을 설정하는 명령입니다. ROLLBACK TO SAVEPOINT를 사용하면 해당 지점까지만 되돌릴 수 있습니다.
BEGIN TRANSACTION;
INSERT INTO orders (order_id, user_id, total)
VALUES (1001, 42, 50000);
SAVEPOINT sp_order_created; -- 중간 저장 지점
INSERT INTO order_items (order_id, product_id, qty)
VALUES (1001, 'P001', 2);
INSERT INTO order_items (order_id, product_id, qty)
VALUES (1001, 'P002', 1); -- 재고 부족으로 실패!
ROLLBACK TO SAVEPOINT sp_order_created;
-- order_items INSERT만 취소, orders INSERT는 유지
-- 대안 상품으로 재처리
INSERT INTO order_items (order_id, product_id, qty)
VALUES (1001, 'P003', 1);
COMMIT; -- orders + 수정된 order_items 영구 반영SAVEPOINT는 복잡한 트랜잭션에서 부분 롤백이 필요할 때 유용합니다. 그러나 과도한 SAVEPOINT 사용은 트랜잭션 로직을 복잡하게 만들므로 신중하게 사용해야 합니다.
트랜잭션의 자동 커밋
DBMS마다 트랜잭션 시작 방식이 다릅니다.
MySQL은 기본적으로 autocommit 모드입니다. 명시적 트랜잭션 밖의 각 SQL 문은 자동으로 하나의 트랜잭션으로 처리됩니다. 여러 문장을 하나로 묶으려면 START TRANSACTION 또는 BEGIN을 사용해야 합니다.
-- Auto Commit ON (기본값)
INSERT INTO users (name) VALUES ('김철수');
-- 이 한 줄이 자동으로 트랜잭션 + COMMIT
-- Auto Commit 끄기
SET autocommit = 0;
INSERT INTO users (name) VALUES ('이영희');
INSERT INTO users (name) VALUES ('박민수');
COMMIT; -- 명시적 커밋 필요
-- 또는 명시적 트랜잭션
START TRANSACTION;
INSERT INTO users (name) VALUES ('최지은');
INSERT INTO users (name) VALUES ('정현우');
COMMIT;Oracle은 일반적으로 DML(INSERT, UPDATE, DELETE)을 명시적으로 COMMIT할 때까지 확정하지 않습니다. 다만 클라이언트 도구의 autocommit 설정은 별도로 확인해야 합니다. 또한 DDL(CREATE, ALTER, DROP)은 묵시적 COMMIT을 발생시킨다는 점이 중요합니다.
-- Oracle에서 DDL은 자동 COMMIT
INSERT INTO temp_data VALUES (1, 'data'); -- 커밋 안 함
CREATE TABLE new_table (id INT); -- DDL 실행
-- → 위의 INSERT도 묵시적으로 COMMIT됨!
-- → ROLLBACK 불가!PostgreSQL은 클라이언트가 autocommit 상태이면 명시적 BEGIN 밖의 각 문장이 개별 트랜잭션으로 실행됩니다. 또한 많은 DDL을 트랜잭션 안에서 롤백할 수 있다는 점이 Oracle과 다릅니다. 단, CREATE DATABASE처럼 트랜잭션 블록에서 실행할 수 없는 명령도 있습니다.
BEGIN;
CREATE TABLE test_table (id INT);
INSERT INTO test_table VALUES (1);
ROLLBACK;
-- → test_table 생성도 취소됨트랜잭션 설계 원칙
실무에서 트랜잭션을 설계할 때 지켜야 하는 원칙이 있습니다.
트랜잭션은 짧게
트랜잭션이 오래 실행되면 락을 오랫동안 점유하여 다른 트랜잭션이 대기하게 됩니다. 이는 시스템의 동시 처리 능력을 심각하게 저하시킵니다.
BEGIN TRANSACTION;
SELECT * FROM orders WHERE user_id = 42;
-- 사용자 화면에 주문 목록 표시
-- ... 사용자가 5분 동안 화면을 보고 있음 ...
-- ... 이 동안 트랜잭션과 관련 리소스가 계속 열린 상태 ...
UPDATE orders SET status = '취소'
WHERE order_id = 1001;
COMMIT;
-- DBMS와 격리 수준에 따라 락, 스냅샷, Undo 보존 비용이 커질 수 있음-- 조회는 트랜잭션 바깥에서
SELECT * FROM orders WHERE user_id = 42;
-- 사용자 화면에 주문 목록 표시
-- 변경이 필요할 때만 짧은 트랜잭션
BEGIN TRANSACTION;
UPDATE orders SET status = '취소'
WHERE order_id = 1001 AND status = '주문완료';
-- 낙관적 락: 상태가 이미 변경되었으면 0행 수정 → 충돌 감지
COMMIT;트랜잭션 내에서 사용자 입력을 기다리지 않기
트랜잭션 도중에 사용자에게 확인을 요청하는 것은 절대 금물입니다. 사용자가 화면을 열어두고 자리를 비우면 트랜잭션이 몇 시간이고 열린 채로 유지될 수 있습니다.
필요한 최소한의 데이터만 잠그기
전체 테이블을 잠그는 것보다 특정 행만 잠그는 것이 효율적입니다. 행 단위 락(Row-Level Lock)을 사용하면 서로 다른 행을 수정하는 트랜잭션은 동시에 실행될 수 있습니다.
일관된 순서로 리소스에 접근
데드락을 예방하기 위해 여러 테이블이나 행에 접근할 때는 항상 동일한 순서로 접근해야 합니다.
암묵적 트랜잭션과 명시적 트랜잭션
프로그래밍에서 트랜잭션을 사용하는 두 가지 방식이 있습니다.
명시적 트랜잭션(Explicit Transaction)은 BEGIN/COMMIT/ROLLBACK을 직접 작성하는 방식입니다. 트랜잭션의 범위가 명확하고 제어가 정확합니다.
START TRANSACTION;
INSERT INTO orders (user_id, total) VALUES (42, 50000);
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, qty)
VALUES (@order_id, 'P001', 2);
UPDATE products SET stock = stock - 2
WHERE product_id = 'P001' AND stock >= 2;
-- 애플리케이션 코드에서 영향 행 수를 확인
-- 0행이면 재고 부족으로 ROLLBACK, 1행이면 COMMIT
COMMIT;암묵적 트랜잭션(Implicit Transaction)은 프레임워크나 ORM이 트랜잭션을 자동으로 관리하는 방식입니다. Spring의 @Transactional, Django의 ATOMIC_REQUESTS 등이 이에 해당합니다.
보상 트랜잭션
분산 시스템이나 마이크로서비스 환경에서는 여러 서비스에 걸친 트랜잭션을 하나의 ACID 트랜잭션으로 묶기 어렵습니다. 이 경우 보상 트랜잭션(Compensating Transaction) 패턴을 사용합니다.
보상 트랜잭션은 Undo 로그에 의한 자동 롤백이 아니라, 역방향 트랜잭션을 새로 실행하여 논리적으로 되돌리는 방식입니다. 출금의 보상은 입금이고, 재고 차감의 보상은 재고 추가입니다.
보상 트랜잭션은 실패와 재시도를 전제로 설계해야 합니다. 이미 보상한 작업을 다시 실행해도 결과가 깨지지 않도록 멱등성(idempotency) 키를 두고, 원 작업과 보상 작업의 상태를 이벤트 로그나 사가 상태 테이블에 남기는 방식이 일반적입니다.
트랜잭션과 동시성
트랜잭션의 격리성과 동시성은 상충 관계(Trade-off)입니다. 격리 수준이 높을수록 데이터 정확성은 높아지지만 동시 처리 능력은 떨어집니다.
| 격리 수준 | 동시성 경향 | 주의할 수 있는 현상 |
|---|---|---|
| READ UNCOMMITTED | 높음 | Dirty Read가 허용될 수 있음 |
| READ COMMITTED | 높음 | Non-repeatable Read가 발생할 수 있음 |
| REPEATABLE READ | 보통 | DBMS 구현에 따라 Phantom Read 대응이 다름 |
| SERIALIZABLE | 낮음 | 가장 강한 격리, 대기·재시도 비용이 커질 수 있음 |
각 격리 수준에서 발생할 수 있는 동시성 문제는 다음 절에서 상세히 다룹니다.
이 표는 표준 개념을 단순화한 것입니다. 실제 동작은 DBMS마다 다릅니다. PostgreSQL은 READ UNCOMMITTED를 사실상 READ COMMITTED처럼 처리하고, InnoDB의 REPEATABLE READ는 MVCC와 next-key lock 때문에 상황에 따라 Phantom을 막기도 합니다. SERIALIZABLE도 DBMS에 따라 대기, 실패, 재시도 방식이 다르므로 실행 계획만큼이나 재시도 설계가 중요합니다.
실무에서의 트랜잭션
실무에서 트랜잭션은 단순히 COMMIT과 ROLLBACK을 넘어서 시스템 설계의 핵심 요소입니다. 몇 가지 실무적 고려사항을 정리합니다.
프레임워크의 트랜잭션 전파(Propagation)는 기존 트랜잭션이 있는 상태에서 새로운 트랜잭션이 시작될 때 어떻게 처리할지를 결정합니다. Spring에서는 REQUIRED(기존 트랜잭션에 참여), REQUIRES_NEW(새 트랜잭션 생성), MANDATORY(기존 트랜잭션 필수) 등의 전파 속성을 제공합니다.
읽기 전용 트랜잭션(Read-Only Transaction)은 SELECT만 수행하는 트랜잭션임을 DBMS와 프레임워크에 알려 주는 힌트이자 제한입니다. DBMS에 따라 쓰기 금지, 스냅샷 최적화, 라우팅 최적화 등에 활용될 수 있지만, 모든 락이나 로그 비용이 사라진다는 뜻은 아닙니다.
-- MySQL
START TRANSACTION READ ONLY;
SELECT * FROM orders WHERE user_id = 42;
SELECT * FROM order_items WHERE order_id = 1001;
COMMIT; -- 변경이 없으므로 커밋 비용이 최소화됨
-- DBMS별 관리 비용은 남지만, 쓰기 트랜잭션보다 가벼울 수 있음타임아웃(Timeout)은 트랜잭션이나 SQL 문이 지정된 시간 안에 끝나지 않을 때 중단시키는 안전장치입니다. DBMS와 설정에 따라 전체 트랜잭션이 롤백되기도 하고, 현재 문장만 실패하거나 트랜잭션이 오류 상태로 남기도 합니다. 그래서 애플리케이션은 타임아웃을 잡으면 명시적으로 ROLLBACK하고 재시도 가능 여부를 판단해야 합니다.
배치 작업에서의 트랜잭션은 대량의 데이터를 처리할 때 한 번의 트랜잭션으로 모든 것을 처리하면 Undo/Redo 로그, 락 보유 시간, 복제 지연이 크게 증가할 수 있습니다. 업무 원자성을 해치지 않는 범위에서 1000건 또는 10000건 단위로 COMMIT하는 방식을 검토합니다.
-- 100만 건 업데이트를 1000건씩 분할하는 개념 예시
SET @batch_size = 1000;
SET @processed = 0;
REPEAT
START TRANSACTION;
UPDATE large_table SET status = 'processed'
WHERE status = 'pending'
LIMIT @batch_size;
SET @processed = @processed + ROW_COUNT();
COMMIT;
UNTIL ROW_COUNT() = 0 END REPEAT;위 코드는 개념을 보여 주는 의사 코드입니다. 실제 MySQL에서는 반복문이 저장 프로그램 안에서만 가능하고, PostgreSQL/Oracle/SQL Server는 반복 처리 문법과 행 제한 문법이 다릅니다. 실무에서는 배치 키 범위, 재시작 지점, 중복 실행 안전성까지 함께 설계해야 합니다.
트랜잭션과 로그의 관계 요약
트랜잭션의 ACID 속성에 어떤 로그와 제어 메커니즘이 관여하는지 한눈에 정리합니다.
Redo 로그는 커밋된 변경을 다시 적용하는 역할이고, Undo 로그는 커밋 안 된 변경을 되돌리는 역할입니다. 이 두 로그가 함께 작동하여 장애 복구 시 데이터베이스를 일관된 상태로 정리합니다.
트랜잭션은 단순한 SQL 명령이 아니라, 데이터베이스의 신뢰성을 지탱하는 근본 메커니즘입니다. 원자성으로 부분 실행을 방지하고, 일관성으로 규칙 위반을 차단하며, 격리성으로 동시 접근 문제를 해결하고, 지속성으로 영구 보존을 보장합니다. 다음 절에서는 동시에 실행되는 트랜잭션 간의 간섭 문제와 격리 수준(Isolation Level)을 다루겠습니다.