← 문제로

2. DB 트랜잭션·ACID, 인덱스 기본, 게임 데이터 영속화

난이도 중
내 답안
모범답안

모범답안 — DB 트랜잭션·ACID, 인덱스 기본, 게임 데이터 영속화

난이도: 중

핵심 답변

ACID

  • Atomicity(원자성): 트랜잭션 안의 작업은 전부 성공하거나 전부 취소(롤백)된다. 부분 적용이 없다.
  • Consistency(일관성): 트랜잭션 전후로 DB가 정의된 제약(무결성 규칙)을 항상 만족한다.
  • Isolation(격리성): 동시에 실행되는 트랜잭션이 서로의 중간 상태를 보지 못한다(격리 수준에 따라 정도가 다름).
  • Durability(지속성): 커밋된 트랜잭션은 장애가 나도 살아남는다(보통 WAL/로그를 디스크에 먼저 기록).

"골드 차감 + 아이템 지급"을 한 트랜잭션으로 묶으면, 원자성 덕분에 "골드는 빠졌는데 아이템은 안 들어가는" 중간 상태가 사라진다(둘 다 되거나 둘 다 안 됨).

인덱스: 정렬된 보조 자료구조(보통 B-tree)로, 전체를 훑지 않고 원하는 행으로 바로 점프할 수 있어 조회가 빨라진다. 단, 인덱스는 데이터가 바뀔 때마다 같이 갱신해야 하므로 INSERT/UPDATE/DELETE가 느려지고, 저장공간도 더 쓴다. 그래서 자주 조회 조건으로 쓰는 컬럼에만 선별적으로 건다.

즉시 저장 vs 주기적 저장: 즉시 저장은 변경 즉시 DB에 반영해 손실이 없지만 부하가 크다. 주기적 저장은 메모리에서 모았다가 한 번에 flush해 부하가 작지만, flush 사이에 죽으면 그만큼 손실된다. 중요·재화성 데이터는 즉시(트랜잭션), 자주 바뀌고 손실 허용되는 데이터는 주기적으로.

깊이 있는 설명 (왜, 트레이드오프)

격리성과 게임의 동시성. 같은 아이템을 두 요청이 동시에 거래하거나, 같은 골드를 두 번 쓰려는 경쟁(double-spend)이 발생할 수 있다. 격리 수준(READ COMMITTED, REPEATABLE READ, SERIALIZABLE)이 낮으면 빠르지만 더티 리드/팬텀 같은 이상 현상이 가능하고, 높으면 안전하지만 락 경합과 성능 저하가 생긴다. 재화 차감 같은 곳은 UPDATE ... WHERE gold >= cost처럼 조건부 갱신 + 영향 행 수 확인으로 원자적 검증을 하거나, 적절한 락/낙관적 동시성으로 보호한다.

인덱스의 본질적 트레이드오프. 인덱스는 "읽기 최적화를 위해 쓰기와 공간을 희생"하는 구조다. 쓰기가 매우 많은 테이블(로그성)에 인덱스를 남발하면 쓰기 throughput이 급감한다. 또한 카디널리티가 낮은 컬럼(예: 성별)에 단독 인덱스는 효과가 거의 없다. 복합 인덱스는 컬럼 순서(가장 선택적인/등호 조건 우선)가 성능을 좌우한다.

영속화의 본질. 게임 서버는 사실상 "메모리가 1차 저장소, DB가 2차 백업"인 구조에 가깝다. RAM은 빠르지만 휘발성이고 DB는 느리지만 영속적이다. 모든 변경을 동기로 DB에 쓰면 게임 틱이 DB 지연에 묶인다. 그래서 메모리를 권위 상태로 두고, DB 쓰기는 비동기/배치로 분리하는 것이 일반적이다. 핵심은 "어떤 데이터는 절대 잃으면 안 되는가"로 정책을 가르는 것.

응용/실무 연결 (게임서버에서)

(a) 주기적 저장의 위험: 마지막 flush 이후의 변경은 크래시 시 전부 손실. 따라서 **결제/구매로 얻은 유료 재화, 거래 결과, 핵심 진행도(퀘스트 완료 보상 등)**는 이 방식으로만 두면 안 된다. 사용자 분쟁·환불·운영 사고로 직결된다.

(b) 데이터 등급 분리 설계 예시

  • 티어1(즉시·트랜잭션 필수): 결제 다이아몬드, 현금성 재화, 거래소 결과, 과금 아이템. → 동기 트랜잭션으로 커밋 확인 후 클라이언트에 성공 응답.
  • 티어2(준실시간 flush): 골드, 인벤토리 변동, 레벨/경험치. → 변경 시 dirty 표시, 짧은 주기(또는 이벤트 단위) flush.
  • 티어3(주기적/손실 허용): 좌표, 버프 잔여시간 등 자주 바뀌고 재계산/허용 가능한 상태. → 30초 주기 또는 로그아웃 시점 저장.

경험치 +50은 티어2로 30초 손실 정도는 허용 가능하지만, 다이아몬드 1000개 지급은 티어1로 결제 성공과 같은 트랜잭션 흐름에서 즉시 영속화 + 멱등 처리해야 한다.

(c) 손실 위험 완화

  • 이벤트 로그/저널 우선 기록(WAL 사상): 메모리 반영과 동시에 "무엇을 했는지" append-only 로그를 빠르게 기록해두면, 크래시 후 로그 재생(replay)으로 마지막 flush 이후 변경을 복구할 수 있다.
  • 중요 이벤트는 즉시 트랜잭션 분리: 재화 변동만 별도 동기 커밋.
  • flush 주기 단축 + 그레이스풀 셧다운 시 강제 flush.
  • MQ/이벤트 소싱으로 변경 명령을 내구성 있는 큐에 먼저 넣고 비동기로 DB 반영.

흔한 오답·함정

  • Consistency를 "데이터가 항상 최신"으로 오해: ACID의 C는 "제약 위반 없음"이지, CAP의 일관성(모든 노드가 같은 값)과 다른 개념이다.
  • "인덱스는 많을수록 좋다": 쓰기 비용·공간·옵티마이저 혼란을 유발한다.
  • "트랜잭션으로 묶으면 동시성 문제까지 자동 해결": 격리 수준과 락 전략을 함께 고려해야 한다.
  • 모든 데이터를 동일 정책으로 저장: 부하 또는 데이터 손실 중 하나를 반드시 키운다.
  • 클라이언트에 "성공" 응답을 DB 커밋 전에 보냄: 티어1 재화에서 절대 금물(미반영 상태에서 성공 통보 → 분쟁).

꼬리질문 대비

  • Q: WAL(Write-Ahead Log)이 Durability를 어떻게 보장하나요? A: 실제 데이터 페이지를 디스크에 쓰기 전에 변경 내용을 로그에 먼저 순차 기록(fsync)한다. 크래시 후 로그를 재생해 커밋된 변경을 복원한다.

  • Q: 클러스터형 인덱스와 보조 인덱스의 차이는? A: 클러스터형은 데이터 행 자체를 인덱스 키 순서로 저장(테이블당 1개). 보조 인덱스는 별도 구조로 클러스터 키(또는 행 위치)를 가리켜 한 번 더 조회(lookup)가 필요할 수 있다.

  • Q: 낙관적 락과 비관적 락 중 게임 재화에는? A: 경합이 드물면 낙관적(version/CAS, WHERE gold>=cost)이 빠르고, 경합이 잦은 핫 아이템은 비관적 락이나 단일 처리 주체로 직렬화하는 편이 안전하다.