우리는 지금까지 웹 애플리케이션(Was + DB)의 동시성을 제어하는 방법을 알아보았다.
- 애플리케이션에서 @Version을 이용한 낙관적 락
- DB+JPA에서 제공하는 @Lock + FOR UPDATE을 이용한 비관적 락
하지만
낙관적 락은 실패 시 예외처리+재시도로 인한 성능 저하의 우려가 있고,
비관적 락은 row(행) 자체에 락을 걸기 때문에 성능 저하와 처리량 이슈가 있다.
또한 락에 대한 쿼리가 발생하기에 DB 커넥션 비용증가까지 발생한다.

이미지와 같이 DB인스턴스를 여러개로 나눈 환경이나
하나의 리소스를 여러 프로세스가 DB만으로 경합을 조절할 경우,
분산 환경에서 제어하기가 버거워진다.

이러한 생각으로 분산 락(distributed lock)이 등장했다.
“경합을 DB 바깥(예: Redis)으로 빼서, 잡을 수 있으면 잡고
못 잡으면 빠르게 포기(or 우회)한다.”
이렇게 하면 대기(블로킹) 시간을 줄이고, 락 범위를 더 작게 유지할 수 있다.
즉, 시스템 내에서 자원을 공유하거나 접근 경쟁이 발생하는 거의 모든 상황에서 분산락은 유효하게 사용될 수 있다.
또한 분산락은 DB에 도달하기 이전에 비즈니스 로직 단에서 먼저 락으로 제어할 수 있다는 점에서 유연성과 효율성, 비용감소(DB 커넥션) 측면까지에서 매우 강력한 도구가 된다.
1. Zookeeper
분산 서버 관리시스템으로 분산 서비스 내 설정 등을 공유해주는 시스템이다.
하지만 추가적인 인프라 구성이 필요하고 성능 튜닝을 위한 러닝커브가 존재한다.
다양한 기능을 제공하지만, 분산락만 쓰기에는 과한 느낌이 있다고 한다.
2. MySQL (네임드 락)
MySQL은 추가적인 인프라 구성없이 문자열로 거는 UserLevel Lock(네임드 락)으로 분산락을 직접 구현할 수 있다고 한다.
MySQL의 메모리에 저장하고 락 획득 시 타임아웃을 설정할 수도 있다.
네임드 락은 비관적 락과 다르게 커넥션에 락을 걸기 때문에 레코드의 존재 여부와 관계없이 Lock 이름을 문자열로 지정하여 락을 걸 수 있다고 한다.
하지만 아래의 우려점들이 있다.
배민에서는 MySQL로 분산락을 구현하기도 하였다.
https://techblog.woowahan.com/2631/
3. Redis
Zookeeper와 마찬가지로 별도의 인프라를 구축하고 관리해야하지만
보통 기존 캐시저장소로도 사용되고 있고, 아래의 장점이 있다.
보통 아래와 같이 3개 중에 하나를 사용하는 것 같다.
❓ 레디스 클라이언트
애플리케이션(JVM)에서 Redis 서버와 통신(TCP/RESP 프로토콜)하는 라이브러리
Jedis: 오래된 전통의 동기/블로킹 스타일
Lettuce: Netty 기반 비동기/논블로킹·Reactive 지원(스프링 기본)
Redisson: Redis를 분산 자료구조/동시성 도구로 추상화(분산락/맵/큐 등 고수준 API)
보통 Lettuce와 Redisson을 고민하다가 "Redisson"을 선택하는데 다음과 같은 이유가 있다.

Lettuce로 분산락을 사용하기 위해서는 setnx, setex 등을 이용해 분산락을 직접 구현해야한다는 어려움도 있고,
직접 구현을 떠나서도 Lettuce로 분산 락을 구현하려면 스핀락의 형태로 구현하게 되는데 Reids에 부하를 주는 스핀락의 요청방식도 부담이 된다.

반면에 Redisson은 Lock interface를 지원해서 락에 대해 타임아웃과 같은 설정을 지원하기에 락을 보다 안전하게 사용할 수 있고,
Redis에 부하를 주지않고 리소스를 절약할 수 있는 Pub/Sub 기반 락기능을 제공한다는 것도 장점이다.
스핀락은 "계속해서 물어보는 방식" : "락 있나요?" -> "없어요" -> 잠깐 쉬었다가 -> "락 있나요?" -> ...
Pub/Sub은 "테이블링 예약하고 기다리는 방식" : "락 있나요?" -> "없으니 대기번호 받고 기다리세요" -> 알림 울릴때까지 대기 → 알림이 오면 한번만 더 시도
Redisson은 레드락 알고리즘으로 구현되었다. 레드락은 상당히 좋은 알고리즘이고, 레디스는 현재 분산락으로 레드락의 사용을 권장하고 있다.
하지만 레드락은 절대적으로 안전하지는 않으며, 시스템 클록에 의한 clock drift 현상이나 애플리케이션 중지에 의해 충분히 문제가 생길 수 있다. 물론 이러한 문제가 쉽게 발생하지는 않기 때문에 충분히 사용해도 되지만, 이러한 부분에 대해 충분히 인지할 필요가 있다.
만약 정말로 완전한 정확성이 필요하다면 Redlock 대신 ZooKeeper와 같은 시스템을 사용하는 것이 좋은 선택이 될 수도 있다.
성능 최적화, 복잡성 감소, 그리고 데이터 일관성 때문이다.
Redis 6.0 부터 클라이언트로부터
전송된 네트워크를 읽는 부분과 전송하는 I/O 부분은 멀티 스레드를 지원한다고 한다.
많은 기술블로그에서 "물류 시스템" 환경에서 동시성 제어를 하기위해 분산락(Redisson)을 사용하는 것 같았다.
많은 쓰기작업이 동시에 발생하는 환경이므로, DB 커넥션 비용을 줄이고 동시성제어도 함께 하기 위한 분산락이 쓰이기에 딱 좋은 환경같다는 느낌이 든다.

실제 상품이 발주되면 중앙물류기지로 입고가 된다.
각 지역거점센터(피킹패킹)로 재고이관이 되고,
실제 B마트에 고객이 주문하면 지역거점센터에서 상품이 출고된다.
재고이관 과정을 집중적으로 살펴보자.

피킹패킹센터로 재고이관하기 위해서는 이관요청서가 필요하다.

이관할 상품들을 결정했다면, 상품들을 선점(할당)해야 한다.

재고를 할당하게 되면 할당수량과 재고위치가 결정된다.
할당은 WMS 내부 로직에서 자동으로 이루어진다.
위는 이관요청서가 할당상태를 의미하는 이미지이다.

이관요청서가 부분할당상태를 가진다는 의미는 다음과 같다.
사과는 할당되었고, 케이크/고기는 부분할당이라고 하며, 할당수량이 0인 휴지는 미할당이라고 한다.
용어 정리를 해보자.
(1) 할당은 피킹패킹센터로 재고이관하기 위해 상품을 선점하는 작업이고,
(2) 취소는 미할당상태(할당수량 0)의 상품을 취소하는 작업이라고 하자.
할당과 취소를 동시에 요청하게 된다면 어떻게 될까?

재고할당은 되었는데, 상태는 취소가된 상황이 발생해버렸다..
기존 할당 API는 재고 할당과 할당 상태변경의 트랜잭션이 분리되어 있었다.
트랜잭션 분리와 함께 상태변경에 분산락을 적용한 이유는
하나의 이관요청서 안에 있는 많은 상품들을 동시에 할당상태를 변경하게 되면
상태가 변경되는 시점에 따라 할당상태가 부분할당으로 남을 수 있기 때문에
분산락을 통해 순차적으로 할당상태를 변경하고 있었다고 한다.

문제는 분산락안에서 사과의 할당상태를 변경하고 있는 도중에
취소 요청이 들어오고, 할당상태가 미할당인 사과상태를 확인한다.
이후에 분산락 안에서 할당상태로 변경하고 커밋이 된다.
그리고, 취소트랜잭션은 사과상태를 취소상태로 변경하였던 것이다.
→ 경쟁상태가 발생하게 된다.
할당 로직은 다음과 같다.
재고할당(allocateItem) 이후에 할당상태(updateStatus)를 변경한다.
할당상태(updateStatus) 로직을 자세히 보면
분산락 키는 이관요청서ID로 되어있다. 즉, 분산락 단위는 이관요청서이다.
그리고 순차적으로 분산락 안에서 할당결과에 따라 상태변경을 한다.
취소 로직은 다음과 같다.
할당취소(updateCancelStatus) 로직은 미할당상태이면 취소상태로 변경하는 간단한 로직이다.
✨ 동시성 이슈원인은 다음과 같았다.
취소 작업에는 분산락이 걸려 있지 않기 때문이었다.
취소로직에 분산락을 적용하지 않은 이유는
이관요청서 할당 상태만 변경하고 할당과 관련된 작업은 하지 않기 때문이었다.
그럼 취소요청에도 분산락 단위를 이관요청서로 구현하면 되겠네!?
다음과 같이 할당요청시 분산락 시점을 앞당기고, 취소요청에도 분산락을 추가하였다.

이제 할당요청과 취소요청이 들어와도, 먼저 들어온 할당요청에 분산락이 걸려있기 때문에 취소요청은 락을 획득하지 못하고 기다리게 되면서 동시성문제를 해결하였다.
당연히 몇가지 다른 문제가 발생하였다.
1. 이관요청서에 일괄할당하고자 할 때
2. 락 획득을 대기할 때
1. 이관요청서에 일괄할당하고자 할 때
기존에는 일괄할당할 때, 재고할당이 먼저 되고 마지막에 상태변경이 되었기 때문에
위와 같은 일괄할당이 되었었다.

할당요청 분산락 시점을 앞당기니 요청/취소에 대한 동시성문제는 해결했지만
일괄할당 경우에 부분할당으로 종료되는 문제점이 발생하였다.
2. 락 획득까지 대기하도록 해보자
락 획득 로직을 넣어 부분할당으로 종료되는 문제점은 해결했지만,
락 획득 대기시간만큼 할당요청이 지연되는 문제가 발생하였다.

문제는 이관요청서 하나에 상품이 4-5개가 아니라면..? 😇
할당요청 처리시간은 기하급수적으로 늘어나는 문제가 발생한다.
3. 현재 처리중인 상태값을 별도의 키로 관리하고, 동일유형(이관요청서ID)의 경우에는 병렬로 처리할 수 있도록 해보자!
할당/취소요청에는 상태값 설정 분산락을 추가한다.

할당요청이 먼저 상태값 설정(할당)하였으므로 취소요청은 종료된다.
재고할당에는 분산락을 적용하지 않는다.
✨ 재고할당은 할당요청 중에 가장 오래걸리는 작업이다.
2번문제점에서 봤듯이 일괄요청 시에 지연문제를 해결하기 위해
분산락 밖에서 병렬처리로 처리하도록 한다.
(마지막) 다수의 할당요청과 취소요청이 들어온다!

❓ 사과요청과 케이크요청의 할당상태변경 시점이 같다면?
분산락은 사과가 먼저 얻었으므로, 케이크는 기다려야 한다.
상태변경의 시간은 짧게 이루어지므로 기다리는 시간은 굉장히 적을 것으로 판단된다.
출처 블로그를 통해 다양한 구현방법의 코드를 확인하자!
분산락은 분산환경 및 물류시스템과 같은 시스템에서 자주 사용되는 것 같다.
실제 업무에서는 분산락을 낙관적 락과 같이 사용하여 DB 커넥션 비용은 줄이고,
데이터의 일관성과 동시성제어를 할 수 있겠다라는 생각도 든다.
상황에 따라 각각의 락에 대한 장/단점을 알고 있어야
합리적인 기술적 의사결정과 함께 시스템 안정성과 비즈니스 성장에도 도움이 될 것이라 생각된다!
분산락을 적용해 동시성 문제 해결하기
[Redis] 분산락을 깊게 다뤄보자. (RedLock, Redisson)
분산락으로 해결하는 동시성 문제(이론편)
[Redis] 레디스가 제공하는 분산락(RedLock)의 특징과 한계
[매일메일] Redis가 싱글 스레드로 만들어진 이유
[마켓컬리] 풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson
[SSG] SSG 자동화센터 운영시스템에서 분산 락을 사용하는 방법
[우아한형제들] WMS 재고 이관을 위한 분산 락 사용기
좋은 정보 얻고 갑니다. 🙂