Redisson의 MultiLock으로 구현하는 완벽한 분산 락 - 원자성과 일관성 보장하기

궁금하면 500원·2025년 1월 27일
0

미생의 개발 이야기

목록 보기
27/58
post-thumbnail

서론

이번에는 Redisson에서 제공하는 또 다른 대표적인 분산 락 구현체인 MultiLock에 집중해보겠습니다.
MultiLock의 핵심 아이디어는 여러 개의 독립적인 RLock에 대해 동시에 잠금 또는 해제 작업을 수행하는 것입니다.
모든 락이 성공적으로 획득되어야만 전체 잠금이 완료되며, 하나라도 실패할 경우 전체 작업이 롤백됩니다.
이러한 "한 번에 모두 잠그거나 모두 풀거나"의 방식은 높은 수준의 요구사항을 가진 분산 비즈니스 시나리오에 더 적합합니다.

소개

분산 환경에서 데이터를 여러 Redis 인스턴스, 클러스터 또는 서로 다른 키로 분할할 때, "N개의 자원을 한 번에 모두 잠가야만 자원을 사용할 수 있다"는 시나리오를 자주 마주치게 됩니다.
Redisson의 MultiLock은 이런 요구사항의 구현을 크게 단순화합니다.
여러 RLock을 하나로 통합한 추상화를 제공하여 외부에는 단일 락 인터페이스로 노출됩니다. 사용자 입장에서는 하나의 락을 다루는 것처럼 사용하지만, 내부적으로는 여러 락의 조합 작업이 이루어집니다.

주요 활용 사례

  • 여러 다른 Redis 키를 동시에 잠가서 "모두 잠금 성공하거나 모두 잠금 실패"를 보장
  • 여러 데이터센터나 Redis 노드에 걸친 상호 배제 요구사항, 특히 비동기 및 분산 작업 스케줄링 시 여러 락을 다루는 번거로움 감소

이제 소스코드를 직접 살펴보면서 RedissonMultiLock이 어떻게 이 로직을 구현하는지 알아보겠습니다.

잠금 획득 (Lock)

Redisson 소스코드에서 MultiLock의 주요 구현체는 org.redisson.RedissonMultiLock 클래스입니다.
핵심 속성은 함께 잠글 모든 락을 저장하는 List입니다. 본질적으로 java.util.concurrent.locks.Lock 인터페이스를 구현하므로 lock()과 tryLock() 등의 메서드를 가집니다.
다음은 핵심적인 잠금 획득 로직입니다 (이해를 돕기 위해 일부 내용 간소화)

public class RedissonMultiLock implements Lock {

    private final List<RLock> locks = new ArrayList<>();

    public RedissonMultiLock(RLock... locks) {
        this.locks.addAll(Arrays.asList(locks));
    }

    @Override
    public void lock() {
        lock(-1, null);
    }

    @Override
    public void lock(long leaseTime, TimeUnit unit) {
        boolean locked = tryLock(leaseTime, unit);
        if (!locked) {
            throw new IllegalStateException("잠금을 획득할 수 없습니다");
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        List<RLock> acquiredLocks = new ArrayList<>();
        for (RLock lock : locks) {
            // 남은 대기 시간 계산
            long elapsed = System.currentTimeMillis() - startTime;
            long remain = time - elapsed;
            if (remain <= 0 && time != -1) {
                // 타임아웃 발생, 이미 획득한 모든 락 롤백
                unlockInner(acquiredLocks);
                return false;
            }

            // 락 획득 시도
            boolean success;
            if (time == -1) {
                success = lock.tryLock(); // 무제한 대기 시간
            } else {
                success = lock.tryLock(remain, unit);
            }

            if (!success) {
                // 락 획득 실패, 롤백 수행
                unlockInner(acquiredLocks);
                return false;
            }
            acquiredLocks.add(lock);
        }
        return true;
    }

    private void unlockInner(List<RLock> locks) {
        for (RLock lock : locks) {
            try {
                lock.unlock();
            } catch (Exception e) {
                // 예외 처리, 일반적으로 로그 기록 또는 무시
            }
        }
    }

    // ...
}

이 코드에서 확인할 수 있는 중요한 부분

  1. MultiLock은 "함께 잠글" 여러 RLock을 하나의 List에 캡슐화합니다.
  2. tryLock 호출 시 각 RLock을 하나씩 획득하려고 시도합니다.
  3. 모든 락이 성공적으로 획득되어야만 true를 반환합니다.
  4. 중간에 하나라도 락 획득에 실패하거나 타임아웃이 발생하면 unlockInner 메서드를 호출하여 이미 성공적으로 획득한 락을 차례로 해제하고, 초기 상태로 롤백하여 분산 환경에서의 원자성을 유지합니다.

이렇게 "모든 락이 성공적으로 잠기거나 아니면 하나도 잠기지 않는" 상태를 보장하여 일관성이 없는 상태의 위험을 제거합니다.

잠금 해제 (Unlock)

MultiLock의 해제 로직도 마찬가지로 "모두 해제" 또는 "모두 해제하지 않음"입니다. unlock() 메서드의 구현을 살펴보겠습니다 (간소화 버전)

public void unlock() {
    // 모든 locks를 한 번에 unlock 합니다
    for (RLock lock : locks) {
        try {
            lock.unlock();
        } catch (Exception e) {
            // 락이 현재 스레드에 속하지 않는 등의 상황에서 발생할 수 있는 예외 처리
        }
    }
}

이 코드를 보면, unlock()은 내부적으로 모든 RLock을 순회하면서 해제 작업을 수행합니다.
이는 중간에 어떤 락이 "현재 스레드가 소유하지 않음" 등의 이유로 오류가 발생하더라도 나머지 락의 해제를 계속 진행한다는 의미입니다.

가능한 한 많은 락을 해제하여 "일부 락이 해제되지 않는" 상황으로 인한 데드락 위험을 최소화하려는 노력을 보여줍니다.

이는 MultiLock의 일관된 접근 방식을 보여줍니다.
모두 잠그거나 모두 해제하여 여러 분산 락을 논리적으로 "하나로 묶는" 것입니다.

일관성을 어떻게 보장하는가?

1. 획득 실패 시 즉시 롤백

tryLock 호출 시 어떤 분산 락이라도 잠금 획득에 실패하면 즉시 롤백합니다.
이는 다중 락의 원자성을 보장하는 핵심입니다.

2. 재진입 가능 의미론은 구체적인 RLock에 의존

여러 RLock 중 일부가 재진입 가능한 락이라면, 같은 스레드에서 반복해서 획득할 때 블록되지 않습니다.
MultiLock은 재진입 로직을 추가로 재정의하지 않습니다.
각 RLock 자체의 재진입 구현에 의존합니다.

3. 통일된 타임아웃 제어

tryLock은 각 락마다 남은 시간을 점차 줄여가며, 특정 락의 획득이 너무 오래 걸려서 전체 프로세스가 멈추는 것을 방지합니다.

4. 해제 과정에서 각 락에 대한 책임

잠금 해제 중 예외가 발생하더라도 MultiLock은 다른 락의 해제를 계속 진행하여 위험과 영향을 최소화합니다.

요약

RedissonMultiLock은 여러 RLock을 하나의 "조합 락"으로 패키징하여 사용자가 프로그래밍 시 "모든 락을 획득했는가? 모든 락이 해제되었는가?"에만 집중할 수 있게 합니다.

내부적으로는 순차적 잠금 획득과 롤백 전략을 통해 원자성을 보장하고, 분산 환경에서 흔히 발생하는 "잠금 불일치" 문제를 방지합니다.

이전에 다룬 공정 락(Fair Lock) 등 다른 락 구현과 달리, MultiLock은 단일 Redis 인스턴스에서 Lua 스크립트로 구현되지 않고, 여러 락 객체를 캡슐화하여 "함께 성공하거나 함께 실패하도록" 보장합니다.
주로 "여러 자원을 한 번에 잠그는" 시나리오에 사용되며, 단일 락보다 일관성과 원자성 요구사항이 더 높은 분산 비즈니스 시나리오에 더 적합합니다.

다른 Redisson 락과 마찬가지로, 소스 코드를 읽는 과정은 분산 환경에서 안전성과 효율성을 어떻게 보장하는지 더 잘 이해하는 데 도움이 되며, 사용자 정의 분산 컴포넌트를 설계할 때 "조합" 사고방식으로 복잡함을 단순화하는 방법에 대한 영감을 줄 수 있습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글