비관적 락(Pessimistic Lock), 낙관적 락(Optimistic Lock), 분산 락(Distributed Lock)

동시성 제어(Concurrency Control)는 여러 요청이 동일한 데이터에 동시에 접근할 때 데이터의 일관성과 무결성을 보장하기 위한 핵심 기술이다.

온라인 게임 서버, 포인트 정산, 결제 시스템, 매칭 시스템처럼 여러 사용자의 요청이 동시에 처리되는 환경에서는 동시성 문제가 빈번하게 발생한다. 이를 해결하기 위해 사용하는 대표적인 방법이 비관적 락, 낙관적 락, 분산 락이다.

각 방식은 동시 접근을 제어하는 위치와 전략이 다르다. 따라서 데이터의 특성, 충돌 빈도, 처리량, 서버 구조를 고려해 적절한 방식을 선택해야 한다.

동시성 문제가 발생하는 이유

동일한 데이터에 여러 요청이 동시에 접근하면 하나의 요청이 수행한 변경 사항을 다른 요청이 덮어쓰거나, 데이터가 중복 처리되는 문제가 발생할 수 있다.

예를 들어 사용자의 포인트가 100점인 상황에서 두 요청이 동시에 30점을 사용한다고 가정하자.

  • 요청 A가 포인트 100점을 조회한다.
  • 요청 B도 포인트 100점을 조회한다.
  • 요청 A가 30점을 차감하여 70점으로 저장한다.
  • 요청 B도 30점을 차감하여 70점으로 저장한다.

두 요청이 정상적으로 처리된 것처럼 보이지만 실제로는 60점이 차감되어야 한다. 결과적으로 포인트가 70점으로 남는 갱신 손실(Lost Update)이 발생한다.

이러한 문제를 방지하려면 데이터의 조회와 변경 과정에서 동시 접근을 제어해야 한다.

비관적 락(Pessimistic Lock)

비관적 락은 여러 요청이 동시에 동일한 데이터에 접근하면 충돌이 발생할 가능성이 높다고 가정하는 방식이다.

데이터를 변경하기 전에 먼저 잠금을 획득하고, 잠금을 보유한 트랜잭션만 해당 데이터를 수정하도록 제한한다. 다른 트랜잭션은 잠금이 해제될 때까지 대기한다.

일반적으로 관계형 데이터베이스의 행 잠금(Row Lock)을 이용해 구현한다.

동작 원리

데이터베이스에서 SELECT ... FOR UPDATE를 사용하면 조회한 행에 쓰기 잠금을 설정할 수 있다. 정확한 잠금 범위와 동작은 데이터베이스 종류와 격리 수준에 따라 달라질 수 있다.

BEGIN;

SELECT points
FROM users
WHERE id = 1
FOR UPDATE;

UPDATE users
SET points = points - 30
WHERE id = 1;

COMMIT;

트랜잭션이 해당 행의 잠금을 획득하면 다른 트랜잭션은 같은 행에 대한 충돌하는 작업을 즉시 수행하지 못하고 대기한다. 트랜잭션이 커밋되거나 롤백되면 잠금이 해제된다.

실제 서비스에서는 포인트 부족 여부를 검증한 후 차감해야 하며, 트랜잭션을 불필요하게 오래 유지하지 않아야 한다.

장점

  • 충돌이 빈번한 상황에서 데이터의 일관성을 유지하기 쉽다.
  • 잠금을 획득한 트랜잭션이 작업을 수행하는 동안 다른 트랜잭션의 충돌하는 변경을 제한한다.
  • 재시도 로직 없이도 데이터베이스의 잠금 메커니즘으로 동시 접근을 제어할 수 있는 경우가 많다.

단점

  • 다른 트랜잭션이 잠금 해제를 기다리므로 응답 시간이 증가할 수 있다.
  • 잠금 대기가 길어지면 처리량이 감소한다.
  • 여러 자원을 서로 다른 순서로 잠그면 교착 상태(Deadlock)가 발생할 수 있다.
  • 잠금 경합이 심한 환경에서는 데이터베이스의 부하가 커질 수 있다.

적합한 사용 사례

  • 동일한 계정의 포인트 및 잔액 차감
  • 재고 수량 변경
  • 제한된 수량의 아이템 지급
  • 충돌이 빈번하고 데이터 변경의 정확성이 중요한 작업

비관적 락은 충돌을 사전에 통제하는 대신 잠금 대기 비용을 감수하는 전략이다.

낙관적 락(Optimistic Lock)

낙관적 락은 대부분의 요청이 서로 충돌하지 않는다고 가정하는 방식이다.

데이터를 조회할 때 잠금을 장시간 유지하지 않는다. 대신 데이터를 수정하는 시점에 다른 트랜잭션이 해당 데이터를 먼저 변경했는지 검사한다.

일반적으로 버전 번호(Version)나 수정 시각을 이용해 구현한다. 이 중 버전 번호를 사용하는 방식이 데이터의 변경 여부를 명확하게 검증하기에 유리하다.

동작 원리

데이터에 버전 컬럼을 추가한다.

CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    points INT NOT NULL,
    version INT NOT NULL DEFAULT 0
);

데이터를 조회할 때 현재 버전을 함께 읽는다.

SELECT points, version
FROM users
WHERE id = 1;

포인트를 수정할 때 조회한 버전이 여전히 동일한지 확인한다.

UPDATE users
SET points = points - 30,
    version = version + 1
WHERE id = 1
  AND version = 0;

업데이트된 행이 있다면 변경에 성공한 것이다. 반대로 변경된 행이 없다면 다른 트랜잭션이 먼저 데이터를 수정했거나 대상 데이터가 존재하지 않는 것이다.

이 경우 최신 데이터를 다시 조회하고 작업을 재시도하거나, 충돌을 호출자에게 반환해야 한다.

위 SQL은 버전이 0인 상태에서 실행하는 예시다. 실제 애플리케이션에서는 조회한 버전 값을 매개변수로 전달해야 한다.

장점

  • 데이터를 조회하는 동안 잠금을 장시간 유지하지 않는다.
  • 충돌이 적은 환경에서 높은 처리량을 기대할 수 있다.
  • 충돌 여부를 명시적으로 확인할 수 있다.
  • 데이터베이스 잠금 대기로 인한 병목을 줄일 수 있다.

단점

  • 충돌이 발생하면 작업을 재시도하거나 실패를 처리해야 한다.
  • 충돌이 빈번한 환경에서는 재시도 비용이 커질 수 있다.
  • 버전 검증을 누락하면 동시성 제어가 정상적으로 동작하지 않는다.
  • 여러 행에 걸친 복잡한 불변 조건은 단일 행의 버전 검증만으로 보장되지 않을 수 있다.

적합한 사용 사례

  • 사용자 프로필 수정
  • 게임 설정 및 캐릭터 정보 변경
  • 게시글 수정
  • 충돌 빈도가 낮은 일반적인 데이터 갱신
  • 동시에 편집할 수 있는 문서나 설정 데이터

낙관적 락은 충돌이 발생했을 때 이를 감지하고 복구하는 대신, 평상시 잠금 비용을 줄이는 전략이다.

분산 락(Distributed Lock)

분산 락은 여러 서버 프로세스나 인스턴스가 동일한 공유 자원에 접근할 때 상호 배제를 보장하기 위한 잠금 방식이다.

비관적 락과 낙관적 락이 주로 데이터베이스의 동시성 제어 방식으로 설명되는 것과 달리, 분산 락은 여러 실행 주체 사이의 조정을 다룬다.

예를 들어 게임 서버가 여러 인스턴스로 실행되고 있다고 가정하자. 각 서버가 동일한 사용자의 일일 보상을 지급할 수 있다면, 서버 내부의 일반적인 뮤텍스(Mutex)만으로는 전체 서버의 동시 접근을 제어할 수 없다.

각 인스턴스의 메모리는 서로 독립적이기 때문이다.

이때 Redis와 같은 공유 시스템을 이용해 분산 락을 구현할 수 있다.

Redis를 이용한 분산 락

Redis에서는 SET 명령어의 NX와 PX 옵션을 활용해 락을 획득할 수 있다.

SET lock:user:123 unique-token NX PX 10000

각 옵션의 의미는 다음과 같다.

  • NX: 해당 키가 존재하지 않을 때만 생성한다.
  • PX 10000: 락의 유효 시간을 밀리초 단위로 설정한다.
  • unique-token: 락을 획득한 요청을 식별하는 고유 토큰이다.

명령이 성공하면 해당 요청이 락을 획득한 것이다. 이미 다른 요청이 락을 보유하고 있다면 획득에 실패한다.

락을 해제할 때는 반드시 소유권을 확인해야 한다. 단순히 DEL 명령어를 실행하면 다른 요청이 새로 획득한 락을 삭제할 수 있기 때문이다.

따라서 토큰이 일치할 때만 삭제하는 Lua 스크립트를 사용하는 것이 일반적이다.

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

이 방식은 락을 획득한 요청만 해당 락을 해제할 수 있도록 한다.

분산 락의 주요 고려 사항

락 만료 시간

서버가 비정상 종료되거나 락 해제 과정이 실행되지 않더라도 일정 시간이 지나면 락이 만료되도록 설정해야 한다.

다만 작업이 락의 유효 시간보다 오래 걸리면 락이 만료된 뒤 다른 요청이 같은 자원에 진입할 수 있다. 따라서 작업 시간에 맞는 만료 정책이나 안전한 락 갱신 전략이 필요하다.

소유권 검증

락을 획득한 요청만 락을 해제하도록 해야 한다. 고유 토큰 없이 잠금을 해제하면 다른 요청의 락을 삭제할 수 있다.

네트워크 장애

Redis와 애플리케이션 사이의 통신이 끊기거나 타임아웃이 발생하면 락 획득 여부가 불확실할 수 있다. 이런 경우 무조건 작업을 진행해서는 안 되며, 시스템의 일관성 요구에 맞게 실패를 처리해야 한다.

분산 환경의 안전성

Redis에 락을 설정했다는 사실만으로 데이터 변경의 정확성이 완전히 보장되는 것은 아니다. 네트워크 지연, 프로세스 중단, 락 만료 등으로 인해 이전 소유자가 락을 잃은 뒤에도 작업을 계속할 수 있기 때문이다.

정확성이 중요한 시스템에서는 데이터베이스의 조건부 갱신, 고유 제약 조건, 멱등성 처리 등을 함께 적용해야 한다. 필요한 경우 펜싱 토큰(Fencing Token)처럼 오래된 락 소유자의 쓰기를 거부할 수 있는 추가적인 보호 장치도 고려해야 한다.

장점

  • 여러 서버 인스턴스 사이에서 공유 자원의 접근을 조정할 수 있다.
  • 특정 사용자나 게임 방 단위로 작업을 직렬화할 수 있다.
  • 여러 서버에서 동시에 시작될 수 있는 작업을 제어하는 데 유용하다.

단점

  • Redis 등 락 관리 시스템에 대한 의존성이 생긴다.
  • 네트워크 지연과 장애에 대한 대응이 필요하다.
  • 락 만료 시간과 작업 실행 시간이 맞지 않으면 중복 실행이 발생할 수 있다.
  • 잘못 설계하면 교착 상태와 유사한 대기 문제 또는 처리량 저하가 발생할 수 있다.

적합한 사용 사례

  • 여러 게임 서버에서 동일한 보상을 지급하는 작업
  • 분산 환경의 중복 스케줄러 실행 방지
  • 특정 사용자 단위의 작업 직렬화
  • 공유 자원에 대한 여러 서버의 동시 접근 제어

분산 락은 여러 실행 주체 사이의 조정 수단이다. 실제 데이터의 정합성을 보장하려면 데이터베이스의 제약 조건과 트랜잭션을 함께 설계해야 한다.

세 가지 락의 차이

구분비관적 락낙관적 락분산 락
핵심 전략먼저 잠금변경 시 충돌 검증여러 실행 주체의 접근 조정
주요 구현DB 행 잠금버전 번호, 조건부 갱신Redis 등 공유 시스템
충돌 처리잠금 대기충돌 감지 후 재시도 또는 실패락 획득 대기, 실패 또는 재시도
주요 비용잠금 대기충돌 및 재시도네트워크 통신과 락 관리
적합한 환경충돌 빈도가 높음충돌 빈도가 낮음여러 서버가 같은 자원을 공유함

세 방식은 반드시 서로 배타적인 선택지가 아니다. 분산 락으로 여러 서버의 접근을 조정하면서 데이터베이스 트랜잭션과 조건부 갱신으로 최종적인 데이터 무결성을 보호할 수 있다.

게임 서버에서의 활용 전략

실시간 멀티플레이 게임 서버에서는 모든 데이터에 동일한 락을 적용하기보다 데이터의 특성에 따라 전략을 분리하는 것이 효율적이다.

포인트 및 재화 차감

데이터베이스 트랜잭션과 조건부 갱신 또는 비관적 락을 사용한다. 포인트가 음수가 되지 않도록 데이터베이스 수준에서도 제약 조건을 검토한다.

일일 보상 지급

지급 여부를 나타내는 고유 제약 조건과 트랜잭션을 우선적으로 고려한다. 여러 서버가 동일한 작업을 수행할 수 있다면 분산 락을 보조적으로 활용할 수 있다.

게임 방 상태 변경

하나의 게임 방을 하나의 서버 프로세스가 독점적으로 처리하도록 설계하면 불필요한 분산 락을 줄일 수 있다. 서버 간 소유권 이전이나 공유 상태 변경이 필요한 경우에는 별도의 동시성 제어가 필요하다.

캐릭터 정보 수정

충돌 빈도가 낮다면 버전 번호를 이용한 낙관적 락이 적합할 수 있다. 충돌이 발생하면 최신 상태를 확인한 뒤 재시도하거나 사용자에게 충돌을 알려야 한다.

매칭 및 랭킹 시스템

Redis 자료구조와 원자적 명령을 활용하면 일부 작업에서 별도의 분산 락을 피할 수 있다. 여러 단계에 걸친 작업의 중복 실행을 막아야 한다면 멱등성 키와 상태 전이 검증을 함께 고려한다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다