database

데이터 정합성을 위한 동시성 제어 기법

gototech 2025. 4. 25. 16:07

안녕하세요, 오늘은 분산 시스템과 데이터베이스에서 데이터 정합성을 보장하기 위한 여러 가지 동시성 제어 기법에 대해 이야기해보려고 합니다. 현대의 서비스들은 대부분 여러 사용자가 동시에 접근하고, 때로는 여러 서버에 분산되어 있는 데이터에 접근합니다. 이런 환경에서 데이터 정합성을 유지하는 것은 상당히 까다로운 문제인데요, 오늘은 그 해결책으로 사용되는 분산락, 뮤텍스, 트랜잭션 고립성, 그리고 낙관적/비관적 락에 대해 알아보겠습니다.

데이터 정합성이란?

먼저 데이터 정합성이 무엇인지 짚고 넘어가겠습니다. 데이터 정합성(Data Consistency)은 데이터가 모든 시점에서 정의된 규칙을 준수하고 일관된 상태를 유지하는 것을 의미합니다. 예를 들어, 은행 계좌의 잔액이 음수가 되지 않아야 한다거나, 재고 시스템에서 상품의 재고량이 0 미만이 되지 않아야 한다는 규칙이 있을 수 있습니다.

여러 사용자나 프로세스가 동시에 같은 데이터에 접근하면 '경쟁 상태(Race Condition)'가 발생할 수 있고, 이는 데이터 정합성을 해칠 수 있습니다. 이제 이러한 문제를 해결하기 위한 기법들을 살펴보겠습니다.

뮤텍스(Mutex): 가장 기본적인 동시성 제어

뮤텍스(Mutex)는 'Mutual Exclusion'의 줄임말로, 공유 자원에 대한 접근을 동기화하는 가장 기본적인 방법입니다. 뮤텍스는 한 번에 하나의 스레드만 특정 임계 구역에 진입할 수 있도록 보장합니다.

 
java
public class BankAccount {
    private double balance;
    private final Object lock = new Object();
    
    public void withdraw(double amount) {
        synchronized(lock) {
            if (balance >= amount) {
                // 실제 은행 시스템에서는 이 작업이 더 오래 걸릴 수 있습니다
                balance -= amount;
            } else {
                throw new InsufficientFundsException();
            }
        }
    }
}

위 예제에서 synchronized 블록은 Java에서 뮤텍스를 구현하는 방법입니다.

이렇게 하면 한 번에 하나의 스레드만 withdraw 메서드의 임계 구역에 접근할 수 있습니다.

분산락(Distributed Lock): 여러 서버에서의 동시성 제어

단일 서버 환경에서는 뮤텍스가 잘 작동하지만, 여러 서버에 걸쳐 있는 분산 시스템에서는 분산락(Distributed Lock)이 필요합니다. 분산락은 여러 서버에 걸쳐 있는 프로세스들 간에 리소스 접근을 조정하는 매커니즘입니다.

분산락을 구현하는 방법은 여러 가지가 있는데, 대표적으로 Redis, ZooKeeper, etcd 등을 사용할 수 있습니다.

 
java
// Redis를 이용한 분산락 구현 예시 (Redisson 클라이언트 사용)
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getLock("myLock");

try {
    // 5초 동안 락 획득을 시도하고, 획득하면 10초 동안 유지
    boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
    if (isLocked) {
        try {
            // 임계 구역: 공유 자원에 접근
            processSharedResource();
        } finally {
            lock.unlock(); // 작업 완료 후 반드시 락 해제
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

분산락을 사용할 때 주의할 점은 네트워크 파티션이나 서버 다운과 같은 문제가 발생했을 때 데드락을 방지하기 위해 타임아웃을 설정하는 것이 중요합니다.

트랜잭션 고립성(Transaction Isolation): 데이터베이스에서의 정합성

데이터베이스에서는 트랜잭션과 그 고립성 수준을 통해 데이터 정합성을 보장합니다. 트랜잭션은 ACID(원자성, 일관성, 고립성, 지속성) 속성을 가진 데이터베이스 작업의 논리적 단위입니다.

SQL 표준은 4가지 트랜잭션 고립성 수준을 정의합니다:

  1. READ UNCOMMITTED: 가장 낮은 고립성 수준으로, 다른 트랜잭션이 커밋하지 않은 변경사항도 볼 수 있습니다(더티 리드).
  2. READ COMMITTED: 커밋된 데이터만 읽을 수 있지만, 한 트랜잭션 내에서 같은 쿼리를 두 번 실행하면 다른 결과가 나올 수 있습니다(넌-리피터블 리드).
  3. REPEATABLE READ: 트랜잭션 시작 시점의 데이터 스냅샷을 기준으로 일관된 읽기를 보장하지만, 팬텀 리드 문제가 발생할 수 있습니다.
  4. SERIALIZABLE: 가장 높은 고립성 수준으로, 완전한 데이터 정합성을 보장하지만 성능 저하가 발생할 수 있습니다.
 
sql
-- MySQL에서 트랜잭션 고립성 수준 설정 예시
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

START TRANSACTION;
-- 데이터 조회 및 수정 작업
SELECT balance FROM accounts WHERE account_id = 123;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
COMMIT;

트랜잭션 고립성 수준을 선택할 때는 데이터 정합성과 성능 사이의 균형을 고려해야 합니다.

낙관적 락(Optimistic Locking)과 비관적 락(Pessimistic Locking)

마지막으로, 데이터베이스에서 동시성을 제어하는 두 가지 주요 접근 방식인 낙관적 락과 비관적 락에 대해 알아보겠습니다.

비관적 락(Pessimistic Locking)

비관적 락은 데이터 접근 시 즉시 락을 획득하는 방식입니다. 다른 트랜잭션이 같은 데이터에 접근하면 대기해야 합니다. 이는 '충돌이 발생할 것'이라고 가정하는 비관적인 접근 방식입니다.

 
sql
-- MySQL에서 비관적 락 사용 예시
START TRANSACTION;
SELECT * FROM products WHERE id = 123 FOR UPDATE;  -- 배타적 락 획득
-- 데이터 수정 작업
UPDATE products SET quantity = quantity - 1 WHERE id = 123;
COMMIT;

FOR UPDATE 구문은 해당 레코드에 대한 배타적 락을 획득합니다. 이 락은 트랜잭션이 커밋되거나 롤백될 때까지 유지됩니다.

낙관적 락(Optimistic Locking)

낙관적 락은 처음부터 락을 걸지 않고, 데이터를 수정할 때 다른 트랜잭션이 그 사이에 데이터를 수정했는지 확인합니다. 이는 '충돌이 거의 발생하지 않을 것'이라고 가정하는 낙관적인 접근 방식입니다.

낙관적 락은 주로 버전 번호나 타임스탬프를 이용해 구현합니다:

 
java
// JPA/Hibernate에서 낙관적 락 사용 예시
@Entity
public class Product {
    @Id
    private Long id;
    
    private int quantity;
    
    @Version  // 낙관적 락을 위한 버전 필드
    private int version;
    
    // getters and setters
}
 
java
// 낙관적 락 사용 예시
try {
    Product product = entityManager.find(Product.class, 123L);
    product.setQuantity(product.getQuantity() - 1);
    entityManager.flush();  // 변경 사항 저장
} catch (OptimisticLockException e) {
    // 다른 트랜잭션이 이미 데이터를 수정한 경우 예외 처리
    handleConcurrencyConflict();
}

낙관적 락은 충돌이 적을 것으로 예상되는 상황에서 좋은 성능을 보이지만, 충돌이 발생했을 때 이를 해결하는 추가 로직이 필요합니다.

어떤 방식을 선택해야 할까?

각 접근 방식은 장단점이 있으며, 시스템의 특성과 요구사항에 따라 적절한 방식을 선택해야 합니다:

  • 뮤텍스: 단일 프로세스 내에서 간단한 동시성 제어에 적합
  • 분산락: 여러 서버에 걸친 분산 시스템에서 필요
  • 트랜잭션 고립성: 데이터베이스 수준에서의 정합성 보장에 중요
  • 비관적 락: 충돌이 자주 발생하거나 데이터 정합성이 매우 중요한 경우에 적합
  • 낙관적 락: 충돌이 드물고 성능이 중요한 경우에 적합

실제 시스템에서는 이러한 기법들을 조합하여 사용하는 경우가 많습니다. 예를 들어, 주문 시스템에서 재고 확인은 낙관적 락을 사용하고, 결제 처리는 비관적 락을 사용하는 방식으로 구현할 수 있습니다.