데이터베이스 — 트랜잭션과 ACID
개요 — 트랜잭션이 게임 서버에서 중요한 이유
Section titled “개요 — 트랜잭션이 게임 서버에서 중요한 이유”온라인 게임 서버에서는 매 초 수천 건의 데이터 변경이 발생합니다.
- 두 플레이어 간 아이템 거래가 진행 중에 서버가 다운된다면?
- 같은 희귀 아이템을 두 명이 동시에 구매하려고 한다면?
- 경험치를 얻었는데 레벨업 처리가 실패하면?
- 결제가 완료됐는데 아이템 지급이 누락된다면?
이런 상황을 방지하는 핵심 메커니즘이 바로 트랜잭션(Transaction)입니다. ACID 속성을 이해하면 데이터 무결성을 보장하는 게임 DB를 설계할 수 있습니다.
1. 트랜잭션 (Transaction)
Section titled “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;트랜잭션 상태
Section titled “트랜잭션 상태”[Active] -> [Partially Committed] -> [Committed] | | | └-> [Failed] -> [Aborted] └-> [Failed] -> [Aborted]
Active: 트랜잭션 실행 중Partially Committed: 마지막 연산 완료, 커밋 전Committed: 영구적으로 DB에 반영됨Failed: 오류 발생Aborted: 롤백 완료, 트랜잭션 전 상태로 복원2. ACID 속성
Section titled “2. ACID 속성”ACID는 트랜잭션이 안전하게 수행되기 위한 4가지 속성입니다.
A — 원자성 (Atomicity)
Section titled “A — 원자성 (Atomicity)”트랜잭션의 모든 연산은 완전히 실행되거나 전혀 실행되지 않아야 합니다. “All or Nothing”의 원칙입니다.
아이템 거래 시나리오:1. 판매자 아이템 제거 - 성공2. 구매자 아이템 추가 - 성공3. 판매자 골드 증가 - 성공4. 구매자 골드 감소 - 실패! (잔액 부족)
원자성 보장 시: 1, 2, 3 모두 롤백 -> 거래 없었던 것으로원자성 미보장 시: 구매자가 아이템을 받고 골드도 내지 않는 버그 발생구현 방법 — WAL (Write-Ahead Log): 변경 사항을 DB에 쓰기 전에 먼저 로그에 기록합니다. 장애 발생 시 로그를 보고 완료된 트랜잭션은 재실행(Redo), 미완료 트랜잭션은 취소(Undo)합니다.
C — 일관성 (Consistency)
Section titled “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)
Section titled “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)
Section titled “D — 지속성 (Durability)”커밋된 트랜잭션의 결과는 영구적으로 보존되어야 합니다. 시스템 장애가 발생해도 손실되지 않아야 합니다.
지속성 보장 방법:1. WAL(Write-Ahead Log): 디스크에 변경 로그 먼저 기록2. 체크포인트(Checkpoint): 주기적으로 메모리의 변경 내용을 디스크에 플러시3. 데이터베이스 복제(Replication): 여러 서버에 동일 데이터 유지
게임 서버 적용:서버가 갑자기 다운되어도 "COMMIT 완료"된 거래 내역은 복구 후에도 유지3. 트랜잭션 격리 수준
Section titled “3. 트랜잭션 격리 수준”격리성을 100% 보장하면 성능이 크게 저하됩니다. 실제로는 격리 수준을 선택해 성능과 정확성 사이의 균형을 맞춥니다.
격리 수준별 허용되는 이상 현상
Section titled “격리 수준별 허용되는 이상 현상”| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 성능 |
|---|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 | 가장 빠름 |
| READ COMMITTED | 방지 | 발생 | 발생 | 빠름 |
| REPEATABLE READ | 방지 | 방지 | 발생 | 보통 |
| SERIALIZABLE | 방지 | 방지 | 방지 | 가장 느림 |
이상 현상 설명
Section titled “이상 현상 설명”더티 리드 (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)과 동시성 제어
Section titled “4. 락 (Lock)과 동시성 제어”비관적 락 (Pessimistic Lock)
Section titled “비관적 락 (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)
Section titled “낙관적 락 (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 설계 패턴
Section titled “5. 게임 서버 DB 설계 패턴”아이템 거래 트랜잭션
Section titled “아이템 거래 트랜잭션”-- 안전한 아이템 거래 프로시저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)
Section titled “중복 방지 패턴 (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. 면접 핵심 정리
Section titled “6. 면접 핵심 정리”트랜잭션이란? 데이터베이스에서 하나의 논리적 작업 단위를 이루는 연산의 집합입니다. 모든 연산이 완전히 성공하거나 모두 실패(롤백)해야 합니다.
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