InnoDB를 사용하면서 동시성(Concurrency)을 고려하지 않고 개발을 하면 큰 문제가 생길 수 있다. 아래에 게임 속 플레이어 간 골드를 넘기는 간단한 로직을 살펴보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test', 'bookworm', '');

try {
    $pdo->beginTransaction();
    foreach ($pdo->query('SELECT gold FROM players WHERE id = 1') as $row) {
        if ($row['gold'] < 1000) {
            echo "Not enough gold.\n";
            exit;
        }
    }
    $pdo->query('UPDATE players SET gold = gold - 1000 WHERE id = 1');
    $pdo->query('UPDATE players SET gold = gold + 1000 WHERE id = 2');
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
}

$pdo = null;
?>

위 예제 코드는 한 플레이어의 골드를 다른 플레이어에게 주는 로직을 간략하게 한 것이다. 이 코드가 동시에 하나씩 실행되면 문제가 없지만, 그렇지 않은 경우 버그가 발생한다. 우선 한 플레이어의 골드가 옮기려는 1000 골드 이상인지 확인 후 1000 골드를 차감한다. 그 후 받을 플레이어의 골드를 1000 증가시킨 후 트랜잭션을 커밋한다. 만약 커밋이나 롤백을 하기 전에 다른 프로세스가 플레이어 1번의 골드를 조회하는 경우 차감 전의 원래 골드 값을 돌려받아 버그가 생기게 된다.

이를 방지하기 위해서는 InnoDB의 기본 ‘Isolation Level’를 변경 할 수 있겠지만, 전체 쿼리 중 동시성을 고려해야 하는 경우가 많지 않다면 간단한 잠금 쿼리를 이용해서 처리 할 수 있다. InnoDB의 잠금은 두가지로 나눌 수 있다. 하나는 LOCK IN SHARE MODE와 다른 하나는 FOR UPDATE다. 이 둘의 용도는 약간 다르기 때문에 자신의 용도에 따라 신중히 선택을 해야 한다.

LOCK IN SHARE MODESELECT를 한 후에 트랜잭션이 끝날 때까지 해당 ROW 값이 변경되지 않을 것을 보장한다. 바꿔 말하면 해당 ROW를 UPDATE 하거나 DELETE 하려는 쿼리는 잠김 상태가 되어 트랜잭션이 끝날 때까지 대기하게 된다. 하지만, SELECT는 얼마든지 여러 세션이 동시에 수행하는 것이 가능하다. 기존 SELECT 쿼리문 맨 뒤에 LOCK IN SHARE MODE 문장을 추가하는 것만으로 사용이 가능하지만, 트랜잭션이 끝나기 전까지만 유효하므로 auto_commit을 꺼야 한다.

1
SELECT gold FROM players WHERE id = 1 LOCK IN SHARE MODE;

FOR UPDATESELECT로 가져 온 데이터를 변경을 하려고 할 때 사용한다.

1
SELECT gold FROM players WHERE id = 1 FOR UPDATE;

FOR UPDATESELECT를 가져온 이후로 해당 ROW에 대해 다른 세션의 SELECT, UPDATE, DELETE 등의 쿼리가 모두 잠김 상태가 된다. 즉, FOR UPDATE를 한 세션 외에 다른 세션들은 모두 해당 ROW에 접근을 할 수 없게 되고, 모두 대기 상태가 된다. FOR UPDATELOCK IN SHARE MODE처럼 트랜잭션이 끝나는 시점에서 풀린다. 예로 든 플레이어 간 골드 이전과 같은 경우에 알맞는 잠금이다.

웹 프로그래밍에서는 쓰레드 프로그래밍처럼 세밀히 동시성을 고민 할 필요는 없지만, 웹이 기본적으로 다수 사용자가 동시에 같은 데이터에 접근을 하는 환경이므로 이로 인해 주의가 필요하다. 이 외에 이런 문제를 해결하기 위해 트리거나 저장 프로시저 등을 이용 할 수 있지만, 최근 웹서비스 초기에는 MySQL을 단순 key-value 저장소처럼 사용하다 추후 서비스 성장에 맞춰 noSQL을 도입하는 경우가 종종 보이므로, RDBMS의 기능을 너무 적극적으로 활용하지 않는 것이 낫지 않을까 싶다.