데이터베이스 — 트랜잭션과 ACID
개요 — 트랜잭션이 게임 서버에서 중요한 이유
섹션 제목: “개요 — 트랜잭션이 게임 서버에서 중요한 이유”온라인 게임 서버에서는 매 초 수천 건의 데이터 변경이 발생합니다.
- 두 플레이어 간 아이템 거래가 진행 중에 서버가 다운된다면?
- 같은 희귀 아이템을 두 명이 동시에 구매하려고 한다면?
- 경험치를 얻었는데 레벨업 처리가 실패하면?
- 결제가 완료됐는데 아이템 지급이 누락된다면?
이런 상황을 방지하는 핵심 메커니즘이 바로 트랜잭션(Transaction)입니다. ACID 속성을 이해하면 데이터 무결성을 보장하는 게임 DB를 설계할 수 있습니다.
1. 트랜잭션 (Transaction)
섹션 제목: “1. 트랜잭션 (Transaction)”트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위를 이루는 연산의 집합입니다. 트랜잭션 내의 모든 연산은 완전히 성공하거나 완전히 실패해야 합니다.
-- 아이템 거래 트랜잭션 예시BEGIN TRANSACTION;
-- 1. 판매자의 인벤토리에서 아이템 제거UPDATE inventorySET item_count = item_count - 1WHERE player_id = 'seller' AND item_id = 'sword_001';
-- 2. 구매자의 인벤토리에 아이템 추가UPDATE inventorySET item_count = item_count + 1WHERE player_id = 'buyer' AND item_id = 'sword_001';
-- 3. 판매자 골드 증가UPDATE players SET gold = gold + 5000 WHERE player_id = 'seller';
-- 4. 구매자 골드 감소UPDATE players SET gold = gold - 5000 WHERE player_id = 'buyer';
-- 모두 성공 시 확정COMMIT;
-- 하나라도 실패 시 모두 취소-- ROLLBACK;트랜잭션 상태
섹션 제목: “트랜잭션 상태”[Active] -> [Partially Committed] -> [Committed] | | | └-> [Failed] -> [Aborted] └-> [Failed] -> [Aborted]
Active: 트랜잭션 실행 중Partially Committed: 마지막 연산 완료, 커밋 전Committed: 영구적으로 DB에 반영됨Failed: 오류 발생Aborted: 롤백 완료, 트랜잭션 전 상태로 복원2. ACID 속성
섹션 제목: “2. ACID 속성”ACID는 트랜잭션이 안전하게 수행되기 위한 4가지 속성입니다.
A — 원자성 (Atomicity)
섹션 제목: “A — 원자성 (Atomicity)”트랜잭션의 모든 연산은 완전히 실행되거나 전혀 실행되지 않아야 합니다. “All or Nothing”의 원칙입니다.
아이템 거래 시나리오:1. 판매자 아이템 제거 - 성공2. 구매자 아이템 추가 - 성공3. 판매자 골드 증가 - 성공4. 구매자 골드 감소 - 실패! (잔액 부족)
원자성 보장 시: 1, 2, 3 모두 롤백 -> 거래 없었던 것으로원자성 미보장 시: 구매자가 아이템을 받고 골드도 내지 않는 버그 발생구현 방법 — WAL (Write-Ahead Log): 변경 사항을 DB에 쓰기 전에 먼저 로그에 기록합니다. 장애 발생 시 로그를 보고 완료된 트랜잭션은 재실행(Redo), 미완료 트랜잭션은 취소(Undo)합니다.
C — 일관성 (Consistency)
섹션 제목: “C — 일관성 (Consistency)”트랜잭션 실행 전후에 DB는 항상 일관된 상태를 유지해야 합니다. 정의된 제약 조건과 규칙이 항상 만족되어야 합니다.
-- 일관성 제약 예시CREATE TABLE players ( player_id VARCHAR(50) PRIMARY KEY, gold INT NOT NULL CHECK (gold >= 0), -- 골드는 0 이상 level INT NOT NULL CHECK (level BETWEEN 1 AND 100));
-- 골드가 음수가 되는 트랜잭션은 제약 위반으로 자동 실패UPDATE players SET gold = gold - 99999 WHERE player_id = 'poor_player';-- Error: CHECK constraint failed: gold >= 0I — 격리성 (Isolation)
섹션 제목: “I — 격리성 (Isolation)”동시에 실행되는 트랜잭션들은 서로에게 영향을 주지 않아야 합니다. 각 트랜잭션은 혼자 실행되는 것처럼 동작해야 합니다.
동시 처리 문제 예시 (격리성 미보장):
시간 트랜잭션 A (플레이어 X가 아이템 구매) 트랜잭션 B (GM이 골드 확인)T1 READ gold = 1000T2 READ gold = 1000 <-- A의 미완료 작업 읽음T3 WRITE gold = 0T4 COMMIT
B가 읽은 값 1000은 최종 값 0과 다름 -> 더티 리드(Dirty Read) 문제D — 지속성 (Durability)
섹션 제목: “D — 지속성 (Durability)”커밋된 트랜잭션의 결과는 영구적으로 보존되어야 합니다. 시스템 장애가 발생해도 손실되지 않아야 합니다.
지속성 보장 방법:1. WAL(Write-Ahead Log): 디스크에 변경 로그 먼저 기록2. 체크포인트(Checkpoint): 주기적으로 메모리의 변경 내용을 디스크에 플러시3. 데이터베이스 복제(Replication): 여러 서버에 동일 데이터 유지
게임 서버 적용:서버가 갑자기 다운되어도 "COMMIT 완료"된 거래 내역은 복구 후에도 유지3. 트랜잭션 격리 수준
섹션 제목: “3. 트랜잭션 격리 수준”격리성을 100% 보장하면 성능이 크게 저하됩니다. 실제로는 격리 수준을 선택해 성능과 정확성 사이의 균형을 맞춥니다.
격리 수준별 허용되는 이상 현상
섹션 제목: “격리 수준별 허용되는 이상 현상”| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 성능 |
|---|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 | 가장 빠름 |
| READ COMMITTED | 방지 | 발생 | 발생 | 빠름 |
| REPEATABLE READ | 방지 | 방지 | 발생 | 보통 |
| SERIALIZABLE | 방지 | 방지 | 방지 | 가장 느림 |
이상 현상 설명
섹션 제목: “이상 현상 설명”더티 리드 (Dirty Read):
-- 트랜잭션 AUPDATE players SET gold = 9999 WHERE player_id = 'hacker';-- 아직 COMMIT 안 함
-- 트랜잭션 B (READ UNCOMMITTED 격리 수준)SELECT gold FROM players WHERE player_id = 'hacker';-- 9999 읽음! (커밋 안 된 데이터)
-- 트랜잭션 AROLLBACK; -- A가 롤백하면 B가 읽은 9999는 존재하지 않는 데이터반복 불가능한 읽기 (Non-Repeatable Read):
-- 트랜잭션 ASELECT gold FROM players WHERE player_id = 'user1'; -- 결과: 1000
-- 트랜잭션 B (중간에 커밋)UPDATE players SET gold = 500 WHERE player_id = 'user1';COMMIT;
-- 트랜잭션 A (같은 트랜잭션 내 재조회)SELECT gold FROM players WHERE player_id = 'user1'; -- 결과: 500 (달라짐!)팬텀 리드 (Phantom Read):
-- 트랜잭션 ASELECT COUNT(*) FROM items WHERE owner_id = 'user1'; -- 결과: 5
-- 트랜잭션 B (중간에 커밋)INSERT INTO items (owner_id, item_name) VALUES ('user1', 'new_sword');COMMIT;
-- 트랜잭션 A (재조회)SELECT COUNT(*) FROM items WHERE owner_id = 'user1'; -- 결과: 6 (행이 추가됨!)4. 락 (Lock)과 동시성 제어
섹션 제목: “4. 락 (Lock)과 동시성 제어”비관적 락 (Pessimistic Lock)
섹션 제목: “비관적 락 (Pessimistic Lock)”충돌이 자주 발생한다고 가정하고 데이터 접근 시 즉시 락을 걸어 다른 트랜잭션의 접근을 차단합니다.
-- SELECT FOR UPDATE: 조회와 동시에 행에 배타적 락BEGIN TRANSACTION;
SELECT gold FROM playersWHERE player_id = 'user1'FOR UPDATE; -- 다른 트랜잭션이 이 행 수정 불가
-- 비즈니스 로직 처리...
UPDATE players SET gold = gold - 100 WHERE player_id = 'user1';COMMIT;
-- 적합한 경우: 실시간 경매, 동시 아이템 구매낙관적 락 (Optimistic Lock)
섹션 제목: “낙관적 락 (Optimistic Lock)”충돌이 드물다고 가정하고 락 없이 읽은 후, 업데이트 시 데이터가 변경됐는지 확인합니다.
-- 버전 번호(version) 컬럼을 이용한 낙관적 락BEGIN TRANSACTION;
-- 현재 버전 확인하며 읽기SELECT gold, version FROM players WHERE player_id = 'user1';-- gold = 1000, version = 5
-- 업데이트 시 버전 확인UPDATE playersSET gold = 900, version = version + 1WHERE player_id = 'user1' AND version = 5; -- 버전이 변경됐으면 0행 업데이트
-- 영향받은 행이 0이면 다른 트랜잭션이 먼저 수정한 것 -> 롤백 후 재시도COMMIT;5. 게임 서버 DB 설계 패턴
섹션 제목: “5. 게임 서버 DB 설계 패턴”아이템 거래 트랜잭션
섹션 제목: “아이템 거래 트랜잭션”-- 안전한 아이템 거래 프로시저CREATE PROCEDURE TradeItem( seller_id VARCHAR(50), buyer_id VARCHAR(50), item_id VARCHAR(50), price INT)BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Trade failed'; END;
START TRANSACTION;
-- 판매자 검증 (FOR UPDATE로 동시 거래 방지) SELECT 1 FROM inventory WHERE player_id = seller_id AND item_id = item_id AND quantity >= 1 FOR UPDATE;
-- 구매자 골드 검증 SELECT 1 FROM players WHERE player_id = buyer_id AND gold >= price FOR UPDATE;
-- 거래 실행 UPDATE inventory SET quantity = quantity - 1 WHERE player_id = seller_id AND item_id = item_id;
UPDATE inventory SET quantity = quantity + 1 WHERE player_id = buyer_id AND item_id = item_id;
UPDATE players SET gold = gold + price WHERE player_id = seller_id; UPDATE players SET gold = gold - price WHERE player_id = buyer_id;
-- 거래 로그 기록 INSERT INTO trade_log (seller_id, buyer_id, item_id, price, trade_time) VALUES (seller_id, buyer_id, item_id, price, NOW());
COMMIT;END;중복 방지 패턴 (Idempotency)
섹션 제목: “중복 방지 패턴 (Idempotency)”-- 네트워크 재전송으로 인한 중복 요청 방지CREATE TABLE item_grants ( request_id VARCHAR(100) PRIMARY KEY, -- 클라이언트가 생성한 고유 ID player_id VARCHAR(50), item_id VARCHAR(50), granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- 아이템 지급 시INSERT INTO item_grants (request_id, player_id, item_id)VALUES ('req_abc123', 'user1', 'sword_001')ON DUPLICATE KEY UPDATE request_id = request_id; -- 이미 있으면 무시
-- 클라이언트가 같은 request_id로 재전송해도 중복 지급 없음6. MVCC (Multi-Version Concurrency Control)
섹션 제목: “6. MVCC (Multi-Version Concurrency Control)”대부분의 현대 DB(PostgreSQL, MySQL InnoDB, Oracle)는 락 기반 대신 MVCC를 사용해 읽기와 쓰기가 서로를 차단하지 않도록 합니다.
MVCC 동작 원리:
트랜잭션 A (READ, 스냅샷 시점 T1) 트랜잭션 B (WRITE)T1: A가 스냅샷 획득 (gold=1000) T2: B가 gold=500으로 UPDATE T3: B COMMITT4: A가 gold 재조회 -> 스냅샷(T1 기준) 버전 읽음 = 1000 (B의 변경 안 보임) -> LOCK 없이 일관된 읽기 가능!
구현 방식:- 각 행에 버전 번호(xmin, xmax) 저장- UPDATE 시 기존 행 무효화 + 새 버전 행 삽입- 트랜잭션은 자신의 시작 시점 이전 커밋된 버전만 읽음- 오래된 버전은 VACUUM(PostgreSQL) 또는 Purge(MySQL)로 정리MVCC와 격리 수준
섹션 제목: “MVCC와 격리 수준”-- PostgreSQL에서 REPEATABLE READ 설정 시 MVCC 동작BEGIN;SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 스냅샷 획득 (이후 다른 트랜잭션의 변경이 보이지 않음)SELECT gold FROM players WHERE player_id = 'user1'; -- 1000
-- 다른 트랜잭션이 gold를 500으로 변경하고 COMMIT해도:SELECT gold FROM players WHERE player_id = 'user1'; -- 여전히 1000-- 같은 스냅샷을 사용하므로 Non-Repeatable Read 없음COMMIT;7. SAVEPOINT — 부분 롤백
섹션 제목: “7. SAVEPOINT — 부분 롤백”트랜잭션 내에서 중간 지점을 설정해 전체 롤백 없이 일부만 취소할 수 있습니다.
BEGIN TRANSACTION;
-- 1단계: 플레이어 골드 차감UPDATE players SET gold = gold - 5000 WHERE player_id = 'buyer';
SAVEPOINT after_gold_deduct; -- 중간 저장점
-- 2단계: 아이템 추가 시도INSERT INTO inventory (player_id, item_id, quantity)VALUES ('buyer', 'rare_sword', 1);
-- 만약 인벤토리가 가득 찼다면?-- ROLLBACK TO SAVEPOINT after_gold_deduct; -- 골드 차감은 유지, 아이템만 취소
-- 3단계: 거래 로그 기록INSERT INTO trade_log (buyer_id, item_id, gold, time)VALUES ('buyer', 'rare_sword', 5000, NOW());
COMMIT;
-- 게임 서버 활용:-- 퀘스트 보상 지급 시 아이템 A 지급 성공, 아이템 B 지급 실패 ->-- SAVEPOINT로 A만 유지하고 B 관련 작업만 롤백8. 교착상태 (Deadlock) 방지 전략
섹션 제목: “8. 교착상태 (Deadlock) 방지 전략”-- 교착상태 발생 시나리오-- 트랜잭션 A: user1 -> user2 순서로 락-- 트랜잭션 B: user2 -> user1 순서로 락
-- 해결책 1: 항상 동일한 순서로 락 획득 (player_id 오름차순)BEGIN;-- 낮은 ID를 먼저 잠금SELECT * FROM players WHERE player_id IN ('user1', 'user2')ORDER BY player_id -- 정렬로 일관된 락 순서 보장FOR UPDATE;-- 이후 업데이트 진행COMMIT;
-- 해결책 2: 락 타임아웃 설정SET innodb_lock_wait_timeout = 5; -- MySQL: 5초 후 타임아웃-- SET lock_timeout = '5s'; -- PostgreSQL
-- 해결책 3: 데드락 감지 후 자동 롤백 (DB가 자동 처리)-- MySQL/PostgreSQL은 데드락 감지 시 한 트랜잭션을 자동으로 희생(victim)으로 선택해 롤백-- 애플리케이션은 데드락 에러(ERROR 1213)를 받으면 재시도 로직 필요
-- 게임 서버 재시도 패턴 (Python/C# 의사 코드)-- for attempt in range(3):-- try:-- execute_trade_transaction()-- break-- except DeadlockException:-- sleep(random(0.1, 0.5)) # 랜덤 백오프-- continue9. NoSQL과 ACID
섹션 제목: “9. NoSQL과 ACID”전통적 RDBMS (MySQL, PostgreSQL):- 완전한 ACID 보장- 강력한 일관성 (Strong Consistency)- 수직 확장 중심
NoSQL (MongoDB, Cassandra, DynamoDB):- BASE (Basically Available, Soft state, Eventually consistent)- 최종 일관성 (Eventual Consistency): 잠시 다른 값이 보일 수 있음- 수평 확장에 유리- 최근에는 ACID 트랜잭션을 지원하는 NoSQL도 증가 (MongoDB 4.0+: 다중 문서 트랜잭션 지원)
게임 서버 선택 기준:- 결제, 아이템 거래, 랭킹 등 데이터 정확성이 중요한 경우: RDBMS + ACID- 채팅 로그, 분석 데이터, 세션 정보 등 유연한 스키마가 필요한 경우: NoSQL
분산 트랜잭션 (2PC, Two-Phase Commit):- 여러 DB 서버에 걸친 트랜잭션을 조율하는 프로토콜- Phase 1 (Prepare): 모든 참여자에게 커밋 준비 여부 확인- Phase 2 (Commit/Abort): 모두 준비 완료 시 커밋, 하나라도 실패 시 전체 롤백- 성능 비용이 크고 조율자(coordinator) 장애 시 블로킹 문제 발생10. 면접 핵심 정리
섹션 제목: “10. 면접 핵심 정리”트랜잭션이란? 데이터베이스에서 하나의 논리적 작업 단위를 이루는 연산의 집합입니다. 모든 연산이 완전히 성공하거나 모두 실패(롤백)해야 합니다.
ACID란?
- 원자성(Atomicity): All or Nothing
- 일관성(Consistency): 트랜잭션 전후 DB 제약 조건 유지
- 격리성(Isolation): 동시 트랜잭션 간 간섭 없음
- 지속성(Durability): 커밋된 데이터는 영구 보존
격리 수준 중 어느 것을 선택해야 하는가? MySQL InnoDB의 기본값은 REPEATABLE READ입니다. 게임 서버에서는 대부분의 조회가 READ COMMITTED로도 충분하지만, 재고 차감 등 정확한 동시성 제어가 필요한 경우 비관적/낙관적 락을 함께 사용합니다.
교착상태(Deadlock)란? 두 트랜잭션이 서로가 가진 락을 기다려 영원히 진행이 안 되는 상태입니다. 항상 동일한 순서로 락을 획득하거나, 타임아웃을 설정해 해결합니다.
참고 자료
섹션 제목: “참고 자료”- Database System Concepts (Silberschatz)
- MySQL InnoDB Transactions
- PostgreSQL Transaction Isolation