웅글웅글
article thumbnail

들어가면서 현재 프로젝트의 상품 주문 기능 중 상품 재고 차감 로직에서 한 번에 많은 트래픽이 몰릴 경우를 대비해 Redis 분산 락을 이용하여 동시성을 제어하고 있습니다. 코드를 팀원이 작성하였기 때문에 코드를 이해하기 위해 분산 락에 대해 공부 중이었습니다. 그러다 문득 든 생각은 Redis 분산 락을 구현할 때 항상 완벽하게 동시성 제어가 가능할까였습니다. 그렇게 찾아보던 중 분산 락으로 항상 완벽하게 동시성 제어를 할 수 없다는 것을 알게 되었고 글로 정리해 보려고 합니다.


왜 항상 완벽하게 동시성 제어를 할 수 없을까?

Redis 분산 락을 공부하던 중 공식 문서에서 Redis 분산 락이 보장해야하는 세 가지 속성에 대해 알게 되었습니다.

그중 첫 번째인 Safety Property라는 속성이 있는데 공식 문서의 내용은 아래와 같습니다.

  • Safety Property : Mutual exclusion. At any given moment, only one client can hold a lock.

의미는 어떤 순간에도 단 하나의 클라이언트만이 특정 리소스에 대한 락을 보유할 수 있다는 것입니다. 상호 배제성(Mutual Exclusion)은 동시성 제어의 가장 기본적인 원칙 중 하나로, 분산 시스템에서 이를 보장하기 위해, 락 획득 시도를 성공한 클라이언트만 해당 리소스에 접근할 수 있어야 합니다.

 

하지만 여기서 의문점이 하나 생겼었습니다. 단 하나의 클라이언트만이 특정 리소스에 대한 락을 보유할 수 있다고 했는데 이는 단 하나의 클라이언트만이 특정 리소스에 접근할 수 있다는 뜻입니다. 하지만 Redis 분산 락을 Redisson(예시)로 구현할 때 Redisson Lease Time이 존재하는데 만약 트랜잭션이 실행되는 시간보다 Lease Time이 더 짧다면 어떻게 될까요??

 

Lease Time이 끝나면 락이 해제되고 다음 대기 중인 트랜잭션이 락을 획득하게 됩니다. 그렇게 되면 두 개의 트랜잭션이 하나의 리소스에 대해 race condition 상태가 되게 됩니다.

 

이러면 또다시 데이터의 정합성이 맞지 않는 현상이 발생하게 됩니다. 그렇다면 어떻게 해결할 수 있을까요?


어떻게 해결할까?

생각해 본 방법은 MySQL의 네임드 락을 사용하거나 Redis 분산 락과 낙관적 락을 함께 사용하는 방법이 있습니다.

 

네임드 락 (Named Lock)

네임드 락을 사용하여 해당 문제를 해결할 수 있습니다. 네임드 락은 TTL의 개념이 존재하지 않기 때문에 락을 획득하면 커넥션이 종료될 때까지 락을 획득하고 있는 상태가 됩니다. 그렇기 때문에 TTL 때문에 락이 해제되어 다른 트랜잭션이 락을 획득할 일이 발생하지 않습니다.

 

하지만 락에 타임 아웃이 존재하기 때문에 기본 값인 30초안에 락을 획득하지 못하면 실패하게 됩니다. 그리고 네임드 락은 DB 커넥션을 사용하기 때문에 별도의 커넥션을 관리해야 하고 대기 상태일 때도 커넥션을 획득한 채로 대기하기 때문에 많은 트래픽이 몰릴 경우 병목 현상이 발생할 수 있습니다.

 

그렇기 때문에 네임드 락이 아닌 Redis 분산 락과 낙관적 락을 함께 사용하여 동시성을 제어했습니다.

Redis 분산 락 + 낙관적 락

Redis 분산 락과 낙관적 락을 어떻게 함께 사용하여 해결할 수 있을까요?

Redis 분산 락에서 아까와 같이 트랜잭션보다 Redisson Lease Time이 먼저 종료되어 다른 트랜잭션과 함께 한 리소스에 접근하는 상황이 발생할 때 낙관적 락을 사용하여 한 번더 동시성 처리를 하게 됩니다.

 

이렇게 되면 트랜잭션이 길어져서 두 개 이상의 트랜잭션이 함께 한 리소스에 접근하는 상황이 발생해도 한번 더 동시성 제어를 하기 때문에 데이터 정합성을 맞출 수 있습니다. 여기서 Version이 일치하지 않는 경우 예외를 던져줘도 되고 Retry 로직을 넣어서 다시 시도할 수 있습니다. 현재 기능의 경우 상품 재고를 차감하는 기능이고 주문에 성공한 사용자의 재고는 보장해 주는 것이 맞는 것 같기 때문에 Retry 로직을 추가해야 할 것 같습니다.

추후에 알게 된 내용이지만 Retry 로직은 순서를 보장하지 않기 때문에 주문 같은 선착순 로직에서는 적합하지 않을 수도 있다고 합니다. ㅎㅎ

낙관적 락 대신 비관적 락을 사용할 수 있지만 트랜잭션이 길어지는 상황이 많지 않을 것이기 때문에 비교적 무거운 비관적 락보다는 낙관적 락이 적합하다고 판단했습니다.

마무리

이번에 분산 락을 공부하면서 동시성을 제어하기 위해서는 고려해야 하는 상황이 정말 많은 것 같습니다. 분산 락을 적용해서 동시성 제어 끝났다가 아닌 그 뒤에 발생할 수 있는 경우까지 생각하는 게 쉽지 않은 것 같습니다. 아직 분산 락에 대해 잘 모르기도 하고 공부하면서 쓴 글이기 때문에 올바르지 않은 부분이 있을 수 있습니다. 제가 생각하지 못한 방법도 존재할 수 있구요. 편하게 댓글로 알려주시거나 피드백해주시면 감사하겠습니다!