Skip to content

데이터베이스 — 트랜잭션과 ACID

개요 — 트랜잭션이 게임 서버에서 중요한 이유

Section titled “개요 — 트랜잭션이 게임 서버에서 중요한 이유”

온라인 게임 서버에서는 매 초 수천 건의 데이터 변경이 발생합니다.

  • 두 플레이어 간 아이템 거래가 진행 중에 서버가 다운된다면?
  • 같은 희귀 아이템을 두 명이 동시에 구매하려고 한다면?
  • 경험치를 얻었는데 레벨업 처리가 실패하면?
  • 결제가 완료됐는데 아이템 지급이 누락된다면?

이런 상황을 방지하는 핵심 메커니즘이 바로 트랜잭션(Transaction)입니다. ACID 속성을 이해하면 데이터 무결성을 보장하는 게임 DB를 설계할 수 있습니다.


트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위를 이루는 연산의 집합입니다. 트랜잭션 내의 모든 연산은 완전히 성공하거나 완전히 실패해야 합니다.

-- 아이템 거래 트랜잭션 예시
BEGIN TRANSACTION;
-- 1. 판매자의 인벤토리에서 아이템 제거
UPDATE inventory
SET item_count = item_count - 1
WHERE player_id = 'seller' AND item_id = 'sword_001';
-- 2. 구매자의 인벤토리에 아이템 추가
UPDATE inventory
SET item_count = item_count + 1
WHERE 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: 롤백 완료, 트랜잭션 전 상태로 복원

ACID는 트랜잭션이 안전하게 수행되기 위한 4가지 속성입니다.

트랜잭션의 모든 연산은 완전히 실행되거나 전혀 실행되지 않아야 합니다. “All or Nothing”의 원칙입니다.

아이템 거래 시나리오:
1. 판매자 아이템 제거 - 성공
2. 구매자 아이템 추가 - 성공
3. 판매자 골드 증가 - 성공
4. 구매자 골드 감소 - 실패! (잔액 부족)
원자성 보장 시: 1, 2, 3 모두 롤백 -> 거래 없었던 것으로
원자성 미보장 시: 구매자가 아이템을 받고 골드도 내지 않는 버그 발생

구현 방법 — WAL (Write-Ahead Log): 변경 사항을 DB에 쓰기 전에 먼저 로그에 기록합니다. 장애 발생 시 로그를 보고 완료된 트랜잭션은 재실행(Redo), 미완료 트랜잭션은 취소(Undo)합니다.

트랜잭션 실행 전후에 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 >= 0

동시에 실행되는 트랜잭션들은 서로에게 영향을 주지 않아야 합니다. 각 트랜잭션은 혼자 실행되는 것처럼 동작해야 합니다.

동시 처리 문제 예시 (격리성 미보장):
시간 트랜잭션 A (플레이어 X가 아이템 구매) 트랜잭션 B (GM이 골드 확인)
T1 READ gold = 1000
T2 READ gold = 1000 <-- A의 미완료 작업 읽음
T3 WRITE gold = 0
T4 COMMIT
B가 읽은 값 1000은 최종 값 0과 다름 -> 더티 리드(Dirty Read) 문제

커밋된 트랜잭션의 결과는 영구적으로 보존되어야 합니다. 시스템 장애가 발생해도 손실되지 않아야 합니다.

지속성 보장 방법:
1. WAL(Write-Ahead Log): 디스크에 변경 로그 먼저 기록
2. 체크포인트(Checkpoint): 주기적으로 메모리의 변경 내용을 디스크에 플러시
3. 데이터베이스 복제(Replication): 여러 서버에 동일 데이터 유지
게임 서버 적용:
서버가 갑자기 다운되어도 "COMMIT 완료"된 거래 내역은 복구 후에도 유지

격리성을 100% 보장하면 성능이 크게 저하됩니다. 실제로는 격리 수준을 선택해 성능과 정확성 사이의 균형을 맞춥니다.

격리 수준별 허용되는 이상 현상

Section titled “격리 수준별 허용되는 이상 현상”
격리 수준Dirty ReadNon-Repeatable ReadPhantom Read성능
READ UNCOMMITTED발생발생발생가장 빠름
READ COMMITTED방지발생발생빠름
REPEATABLE READ방지방지발생보통
SERIALIZABLE방지방지방지가장 느림

더티 리드 (Dirty Read):

-- 트랜잭션 A
UPDATE players SET gold = 9999 WHERE player_id = 'hacker';
-- 아직 COMMIT 안 함
-- 트랜잭션 B (READ UNCOMMITTED 격리 수준)
SELECT gold FROM players WHERE player_id = 'hacker';
-- 9999 읽음! (커밋 안 된 데이터)
-- 트랜잭션 A
ROLLBACK; -- A가 롤백하면 B가 읽은 9999는 존재하지 않는 데이터

반복 불가능한 읽기 (Non-Repeatable Read):

-- 트랜잭션 A
SELECT 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):

-- 트랜잭션 A
SELECT 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 (행이 추가됨!)

충돌이 자주 발생한다고 가정하고 데이터 접근 시 즉시 락을 걸어 다른 트랜잭션의 접근을 차단합니다.

-- SELECT FOR UPDATE: 조회와 동시에 행에 배타적 락
BEGIN TRANSACTION;
SELECT gold FROM players
WHERE player_id = 'user1'
FOR UPDATE; -- 다른 트랜잭션이 이 행 수정 불가
-- 비즈니스 로직 처리...
UPDATE players SET gold = gold - 100 WHERE player_id = 'user1';
COMMIT;
-- 적합한 경우: 실시간 경매, 동시 아이템 구매

충돌이 드물다고 가정하고 락 없이 읽은 후, 업데이트 시 데이터가 변경됐는지 확인합니다.

-- 버전 번호(version) 컬럼을 이용한 낙관적 락
BEGIN TRANSACTION;
-- 현재 버전 확인하며 읽기
SELECT gold, version FROM players WHERE player_id = 'user1';
-- gold = 1000, version = 5
-- 업데이트 시 버전 확인
UPDATE players
SET gold = 900, version = version + 1
WHERE player_id = 'user1' AND version = 5; -- 버전이 변경됐으면 0행 업데이트
-- 영향받은 행이 0이면 다른 트랜잭션이 먼저 수정한 것 -> 롤백 후 재시도
COMMIT;

-- 안전한 아이템 거래 프로시저
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;
-- 네트워크 재전송으로 인한 중복 요청 방지
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로 재전송해도 중복 지급 없음

트랜잭션이란? 데이터베이스에서 하나의 논리적 작업 단위를 이루는 연산의 집합입니다. 모든 연산이 완전히 성공하거나 모두 실패(롤백)해야 합니다.

ACID란?

  • 원자성(Atomicity): All or Nothing
  • 일관성(Consistency): 트랜잭션 전후 DB 제약 조건 유지
  • 격리성(Isolation): 동시 트랜잭션 간 간섭 없음
  • 지속성(Durability): 커밋된 데이터는 영구 보존

격리 수준 중 어느 것을 선택해야 하는가? MySQL InnoDB의 기본값은 REPEATABLE READ입니다. 게임 서버에서는 대부분의 조회가 READ COMMITTED로도 충분하지만, 재고 차감 등 정확한 동시성 제어가 필요한 경우 비관적/낙관적 락을 함께 사용합니다.

교착상태(Deadlock)란? 두 트랜잭션이 서로가 가진 락을 기다려 영원히 진행이 안 되는 상태입니다. 항상 동일한 순서로 락을 획득하거나, 타임아웃을 설정해 해결합니다.