ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이벤트 응모 시스템으로 배우는 Redis 분산 락(Distributed Lock) 해결 가이드 🚀
    Project 2026. 1. 21. 23:14

    안녕하세요! 오늘은 백엔드 개발자라면 한 번쯤은 마주하게 되는, 혹은 면접 단골 질문이기도 한 '동시성 제어'에 대해 이야기해보려 합니다.

    선착순 이벤트나 한정판 상품 판매 시스템을 구축할 때, 수만 명의 사용자가 동시에 '구매' 버튼을 누르면 어떤 일이 벌어질까요? 단순히 DB의 값을 업데이트하는 방식으로는 데이터의 원자성(Atomicity)을 보장하기 어렵습니다.

    오늘은 그 해결책 중 하나인 Redis 분산 락을 활용해 문제를 해결하는 방법을 알아보겠습니다! 👨‍💻


    1. 왜 DB 락(Lock)만으로는 부족할까? 🤔

    일반적으로 관계형 데이터베이스(RDBMS)가 제공하는 Pessimistic Lock(비관적 락)이나 Optimistic Lock(낙관적 락)을 떠올릴 수 있습니다. 하지만 다음과 같은 상황에서는 한계가 명확하죠.

    • 성능 저하: DB 자체에 락을 걸면 트랜잭션이 길어질수록 DB 커넥션 점유 시간이 늘어나 전체 처리량이 떨어집니다.
    • 분산 환경: 여러 대의 서버 인스턴스가 동작하는 MSA 환경에서는 각 서버의 로컬 락(e.g. Java의 synchronized)이 무용지물이 됩니다.

    이때 활용하기 좋은 도구가 바로 Redis입니다. 메모리 기반이라 빠르고, 모든 명령어가 싱글 스레드로 동작하여 원자성을 보장하기 때문이죠.


    2. Redisson 라이브러리를 이용한 분산 락 구현 🛠️

    Redis를 이용해 직접 SETNX 명령어로 락을 구현할 수도 있지만, 타임아웃 처리나 재시도 로직을 직접 짜기엔 위험 요소가 많습니다. 그래서 Java 진영에서는 Redisson 라이브러리를 주로 사용합니다.

    💻 핵심 코드 예제 (Spring Boot)

    재고를 감소시키는 로직에 분산 락을 적용해 보겠습니다.

    @Service
    @RequiredArgsConstructor
    public class StockService {
    
        private final RedissonClient redissonClient;
        private final StockRepository stockRepository;
    
        public void decreaseStock(Long itemId, Long quantity) {
            // 1. Lock 객체 가져오기 (고유한 키 값 설정)
            RLock lock = redissonClient.getLock("ITEM_LOCK:" + itemId);
    
            try {
                // 2. 락 획득 시도 (waitTime: 락 획득 대기 시간, leaseTime: 락 점유 시간)
                boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
    
                if (!available) {
                    throw new RuntimeException("락 획득에 실패했습니다. 잠시 후 다시 시도해주세요.");
                }
    
                // 3. 비즈니스 로직 수행 (재고 감소)
                Stock stock = stockRepository.findById(itemId).orElseThrow();
                stock.decrease(quantity);
                stockRepository.save(stock);
    
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 4. 반드시 락 해제 (획득한 스레드인지 확인 필수)
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    
    

    💡 코드 포인트 설명

    • tryLock: 무한정 기다리지 않고 지정된 시간 동안만 락 획득을 시도합니다. 시스템 전체의 행(Hang) 걸림을 방지하죠.
    • isHeldByCurrentThread: 락을 획득하지 못한 스레드가 다른 스레드의 락을 해제하는 불상사를 막아줍니다.
    • Pub/Sub 방식: Redisson은 락이 해제될 때 대기 중인 스레드에게 알림을 주는 방식을 사용하여 Redis에 주는 부하를 줄입니다.

    3. 주의할 점: 트랜잭션의 커밋 시점 ⚠️

    분산 락을 사용할 때 가장 많이 하는 실수 중 하나는 @Transactional과 분산 락의 순서입니다.

    성공적인 동시성 제어를 위해서는 반드시 아래의 실행 순서를 지켜야 합니다:

    1. 락 획득 (Lock Acquire)
    2. 트랜잭션 시작 & 비즈니스 로직 수행
    3. 트랜잭션 커밋 (Transaction Commit)
    4. 락 해제 (Lock Release)

    왜 그럴까요? 만약 트랜잭션이 커밋되기 전에 락이 해제되어 버리면, 대기하던 다른 스레드가 즉시 락을 획득하고 데이터를 읽어갑니다. 하지만 이전 스레드의 변경 사항이 아직 DB에 최종 반영(Commit)되지 않았기 때문에, 다른 스레드는 업데이트 전의 데이터를 읽어 '재고 초과'와 같은 정합성 오류를 일으키게 됩니다.

    따라서 실무에서는 보통 서비스 레이어 외부에서 락을 잡고 내부 서비스를 호출하는 Facade 패턴을 주로 사용합니다.


    4. 요약 및 인사이트 📝

    백엔드 개발자로서 기술을 선택할 때는 항상 트레이드오프(Trade-off)를 고민해야 합니다.

    비교 항목 DB Lock (Pessimistic) Redis (Redisson)

    속도 상대적으로 느림 매우 빠름 (In-memory)
    복잡도 단순함 (SQL 수준) 별도 인프라(Redis) 필요
    효율성 커넥션 점유로 부하 발생 가능 Pub/Sub 방식으로 부하 적음
    권장 상황 충돌이 적고 단일 DB일 때 초당 요청이 많고 분산 환경일 때

    ✅ 한 줄 요약

    분산 환경에서 데이터 정합성을 확실히 지키고 싶다면, 트랜잭션 범위보다 넓게 설계된 Redis 분산 락을 적극 검토해보자! 🚀


    🔗 함께 보면 좋은 글

    • [Redis Pub/Sub 구조 깊이 알아보기]
    • [Spring Boot에서 Facade 패턴으로 분산 락 깔끔하게 관리하기]
    • [DB 격리 수준(Isolation Level)과 동시성 제어의 관계]

    참고 자료:

    • Redisson 공식 가이드: https://redisson.org/
    • 마틴 클레프만 저, <데이터 중심 애플리케이션 설계> (동시성 파트)
Designed by Tistory.