[기타] 분산락, 분산 환경에서 동시성 제어하기

KIM Jongwan·2025년 5월 9일
0

기타

목록 보기
10/10
post-thumbnail

ℹ️ 이 글은 항해99 백엔드 플러스 과정 6주차 회고를 위해 작성되었습니다.
분산 락의 개념과 도입 시 고려할 점들을 실제 예제와 함께 정리해보았습니다.

6주차 목표

  • 분산 락을 도입하는 이유에 대해 알아보고 E-Commerce 서비스에서 적절한 적용 지점을 찾아봅니다.
  • Redis를 활용하여 데이터 캐시를 적용하고 성능을 분석해봅니다.

경쟁 상태(Race condition)와 동시성 제어

대부분의 웹 서비스는 여러 사용자가 동시에 이용하게 됩니다. 예를 들어, 이커머스 서비스에서는 하나의 상품을 여러 명이 동시에 주문하고, 콘서트 예매 서비스에서는 하나의 좌석을 여러 명이 동시에 예매하려고 시도합니다. 이러한 요청은 모두 하나의 데이터베이스에서 동일한 데이터를 읽거나 수정하려고 하기 때문에 충돌이 발생할 수 있습니다.

두 명의 사용자가 재고가 1개 남은 상품을 동시에 주문하는 상황을 가정해봅시다. 재고가 1개뿐이라면 두 명 중 한 명은 주문에 실패해야 하지만, 동시 요청에 대한 처리가 없다면 두 사용자 모두 주문 성공 처리가 되는 상황이 발생할 수 있습니다. 이처럼 동시에 같은 자원에 대한 접근으로 인해 발생하는 상황을 경쟁 상태(Race Condition)라고 합니다.

이러한 문제를 방지하기 위해서는 동시 요청에 대해 순서를 정해 차례대로 처리하는 장치, 즉 동시성 제어가 필요합니다.

동시성 제어 기법

경쟁 상태를 막기 위해 사용되는 것이 바로 동시성 제어 기법입니다. 대표적으로 사용되는 기법들은 다음과 같습니다:

  • 비관적 락(Pessimistic Lock): 충돌이 발생할 가능성이 높다고 판단하여 데이터를 사용하는 시점부터 락을 걸어 다른 접근을 차단하는 방식입니다. 주로 SELECT ... FOR UPDATE와 같은 데이터베이스의 락 기능을 사용합니다.
  • 낙관적 락(Optimistic Lock): 충돌이 드물다고 가정하고 작업을 먼저 수행한 뒤, 최종 저장 시점에 데이터가 변경되지 않았는지 확인합니다. 변경 감지는 버전 정보나 타임스탬프 등을 활용합니다.
  • 큐 기반 직렬 처리: 요청을 큐에 저장하고 하나씩 순차적으로 처리하여 순서를 보장하는 방식입니다. 실시간성이 크게 요구되지 않으면서도 동시 처리가 위험한 상황에서 자주 사용됩니다.
  • 분산 락(Distributed Lock): 여러 서버나 인스턴스에서 동시에 자원에 접근할 수 있는 환경에서는 Redis, ZooKeeper 등의 외부 시스템을 활용한 분산 락이 필요합니다. 단일 서버 환경에서는 고려하지 않아도 되지만, 마이크로서비스나 클라우드 환경에서는 필수적입니다.
  • 이벤트 기반 비동기 처리: 요청을 즉시 처리하지 않고 이벤트를 큐에 넣어 비동기로 처리함으로써 자원 접근 시점을 조율하는 방식입니다. 주문 처리나 알림 발송 같은 후처리 작업에서 자주 사용됩니다.

동시성 제어는 시스템의 안정성과 직결되는 요소이며, 서비스의 특성, 처리량, 인프라 구조에 따라 적절한 전략을 선택해야 합니다. 각 기법의 특성과 한계를 이해하고 이를 설계에 반영하는 것이 중요합니다.

분산 락의 개념

앞서 학습한 비관적 락과 낙관적 락은 데이터베이스를 기반으로 한 동시성 제어 기법입니다. 이들은 단일 데이터베이스 환경에서 매우 효과적이지만, 시스템의 규모가 커지면서 서버와 데이터베이스가 여러 대로 구성되는 분산 환경에서는 한계가 발생합니다.

이때 고려할 수 있는 대안이 바로 분산 락(Distributed Lock)입니다.

분산 락은 여러 서버나 인스턴스가 동시에 하나의 자원에 접근하지 못하도록 제어하는 락입니다. 단일 서버에서는 synchronized 블록이나 데이터베이스 락으로 충분할 수 있지만, 다중 인스턴스 환경에서는 서로 다른 서버가 같은 데이터를 동시에 변경할 수 있기 때문에, 이들 사이에서 공통으로 사용할 수 있는 락의 기준점이 필요합니다.

이런 기준점 역할을 하는 도구로는 Redis, ZooKeeper, Etcd 등이 있으며, 특히 Redis는 Redisson 라이브러리를 통해 분산 락 구현을 간편하게 할 수 있습니다. 예를 들어, 하나의 재고 데이터를 여러 서버가 동시에 처리할 수 있는 구조라면, 해당 재고에 대한 락을 Redis에 설정하여 하나의 인스턴스만 작업을 수행하도록 만들 수 있습니다.

다음과 같은 상황에서는 분산 락의 도입을 고려할 수 있습니다:

  • 여러 서버가 동일한 데이터를 동시에 갱신할 수 있는 구조일 때
  • 중복 처리가 절대 발생해서는 안 되는 중요한 작업이 있을 때 (예: 쿠폰 발급, 재고 차감 등)
  • 이벤트 기반 또는 비동기 처리 환경에서 여러 컨슈머 간 작업 순서를 제어할 필요가 있을 때

단, 분산 락은 일반적인 락보다 구현이 복잡하며, 잘못 사용할 경우 병목이나 데드락을 유발할 수 있으므로 사용 여부를 신중하게 판단해야 합니다.

💡 왜 Redisson인가?

Redisson은 Redis를 기반으로 동작하는 Java 클라이언트 라이브러리로, 단순한 키-값 저장소를 넘어서 분산 락, 세마포어, 큐, 캐시 등 다양한 동기화 도구를 제공합니다. 특히 여러 서버에서 동시에 같은 자원에 접근할 수 있는 분산 환경에서 Redisson은 락의 획득과 해제를 안정적으로 처리할 수 있도록 고수준 API를 제공하여 개발자가 복잡한 동시성 제어 로직을 직접 구현하지 않아도 되도록 도와줍니다.

💡 예제 시나리오

재고가 1개 남은 상품에 대해 여러 사용자가 동시에 주문할 때, 중복 주문이 발생하지 않도록 분산 락을 활용하여 동시에 하나의 서버만 재고 차감 작업을 진행하도록 합니다.

@Service
public class OrderService {

    private final RedissonClient redissonClient;
    private final ProductRepository productRepository;

    public OrderService(RedissonClient redissonClient, ProductRepository productRepository) {
        this.redissonClient = redissonClient;
        this.productRepository = productRepository;
    }

    public void placeOrder(Long productId) {
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 락 획득 시도: 최대 3초 대기, 락을 잡으면 5초 후 자동 해제
            if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                Product product = productRepository.findById(productId)
                        .orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다."));

                if (product.getStock() <= 0) {
                    throw new IllegalStateException("상품이 품절되었습니다.");
                }

                product.decreaseStock(1); // 재고 차감
                productRepository.save(product);
            } else {
                throw new IllegalStateException("다른 사용자가 주문을 처리 중입니다. 잠시 후 다시 시도해주세요.");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("락 획득 중 인터럽트 발생", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock(); // 반드시 락 해제
            }
        }
    }
}

📘 코드 설명

  • lockKey: 상품 ID 기반으로 고유한 락 키를 설정하여, 상품 단위로 락을 분리합니다.
  • tryLock(대기시간, 자동해제시간, 단위): 락이 해제될 때까지 최대 3초간 기다리고, 락을 획득하면 5초 뒤 자동으로 해제됩니다. 첫 번째 인자는 대기 시간, 두 번째는 락 유지 시간입니다.
  • finally 블록에서의 락 해제는 필수입니다. 락을 잡은 스레드만 해제할 수 있으므로 조건을 확인해야 합니다.

✅ 마무리하며

Race Condition과 동시성 제어는 모든 웹 서비스에서 한 번쯤은 마주하게 되는 주제입니다. 이번 글에서는 그 개념부터 분산 환경에서의 해결책인 분산 락까지, 실무에서 자주 사용되는 방식들을 중심으로 정리해보았습니다. 특히 Redisson을 활용한 분산 락 구현은 마이크로서비스 환경에서의 실질적인 대안이 될 수 있습니다.

서비스가 커지면 동시성 제어는 선택이 아닌 필수가 됩니다. 상황에 맞는 적절한 방법을 이해하고 활용할 수 있다면, 안정성과 확장성을 모두 갖춘 구조를 만들 수 있습니다.

profile
3년차 백앤드 개발자입니다.

0개의 댓글