락을 통한 동시성 제어 - 설명편

최창효·2024년 1월 6일
0
post-custom-banner

들어가기 앞서

이번 포스팅의 주제는 락을 통한 동시성 제어와 관련된 내용입니다.

이번 프로젝트에서 기술적으로 가장 많이 고민했던, 그리고 가장 많은 에러와 수정이 발생했던 부분이 바로 동시성과 관련된 부분이었습니다.

또한 다른 동기들이 ‘이거도 글로 정리해서 올려주시나요?’를 가장 많이 물어본, 나름 관심이 높았던 주제였던 만큼 다른 분들에게도 도움이 될 수 있게 글로 잘 한번 정리해보려고 합니다.

어디선가 ‘synchronized키워드가 사용된 프로젝트를 담당해 달라는 부탁을 받으면 도망가라(?)’와 같은 우스갯소리를 들었던 기억이 있습니다. 아마도 동시성이 그만큼 어려운 주제라는 농담인거 같은데, 비록 작은 프로젝트지만 직접 동시성을 고려해보니 적어도 저에게는 어려운 주제가 맞았습니다.

해당 포스팅에서 소개할 제 방법과 코드가 열심히 고민해서 생각해낸 나름 최선의 결과는 맞지만, 분명 제가 생각지 못한 틀린 부분이 있을 거라 생각합니다. 이점 꼭 고려해서 읽어주시고, 만약 틀린 부분을 발견해서 알려주신다면 감사하겠습니다!

우리 쇼핑몰은

Blooming Blooms(이하 BB)는 온라인 화훼 쇼핑몰 플랫폼입니다. BB에는 쇼핑몰이라면 빠질 수 없는 재고 그리고 쿠폰이 존재합니다.

재고는 주문이 발생했을 때 차감되며 하나의 주문에는 여러 가게의 여러 상품이 포함될 수 있습니다. 또한 가게사장이 직접 자기 가게에 있는 상품의 재고를 수정할 수 있습니다.

쿠폰은 한정 수량으로 발급해 선착순으로 지급되며, 같은 종류의 쿠폰에 대해 1인당 1장만 지급됩니다.

  • 100장의 쿠폰을 발급하기로 했다면 99장이 발급되어도 안되고, 101장이 발급되어도 안됩니다.
  • 쿠폰은 먼저 신청한 100명의 고객에게 발급되어야 합니다. 99번째 순서로 신청한 고객이 쿠폰을 받지 못하고 101번째 순서로 신청한 고객이 쿠폰을 받는 경우가 발생하면 안됩니다.
  • 유저1이 A쿠폰을 이미 발급받았다면 유저1은 더 이상 A쿠폰을 추가로 발급받을 수 없습니다.

저는 재고의 경우 Redisson을 이용한 분산락을, 쿠폰은 Redis의 Set자료구조를 이용한 분산락을 사용해 동시성 문제를 해결했습니다.

선택의 구체적인 이유, 구현 방법과 같은 자세한 내용은 다음 글들에서 별도로 다룰 예정입니다. 이번 글에서는 락과 레디스에 대한 짧은 설명과 함께 분산락을 선택한 이유 정도만 얘기해 보겠습니다. 설명이 짧기 때문에 락과 레디스에 대한 내용을 함께 찾아보면서 읽는 걸 추천드립니다!

락은 동시성을 제어하기 위한 기술입니다. 우리가 흔히 사용하는 락의 종류로는 낙관락, 비관락, 분산락이 있습니다.

낙관락과 비관락

낙관락은 애플리케이션 단위에서 버전을 통해 동시성을 제어합니다. 여러 스레드가 동시에 값을 가져가 변경시키는 건 가능하지만 먼저 전달 받은 하나의 변경사항만 반영하고 이후에는 버전이 달라져 나머지 요청은 반영되지 않습니다.

BB는 '사장님이 직접 수량을 변경하는 기능'이 존재합니다. 오늘 아침에 50개의 장미가 가게에 들어왔다면 이를 사장님이 시스템에 반영하는 것이죠.

만약 이 시스템에 사장님과 알바생이 동시에 접근해서 수량을 변경하면 어떻게 될까요? 사장님이 알바생에게 수량을 전산에 입력하라고 지시했는데 이를 깜빡하고 본인 휴대폰으로 수량 변경을 시도하는 것이죠. 그런데 공교롭게도 사장님과 알바생이 수량을 변경하는 시간까지 정확히 동일했던 겁니다!

  • 사장과 알바생이 동시에 재고에 접근합니다. 둘 다 동일한 버전의 데이터를 가져가게 됩니다.
  • 사장이 먼저 수량을 변경한 뒤 이를 반영합니다. 사장의 요청이 정상적으로 처리되며 해당 재고의 버전은 올라갑니다.
  • 이후 알바생이 수량 변경을 요청합니다. 하지만 이전 사장의 요청으로 버전이 올라갔고, 처음 자신이 데이터를 가져갔을 때의 버전과 달라 알바생의 요청은 실패하게 됩니다.

사실 이런 일은 자주 발생하지 않습니다. 또한 둘 중 하나의 수정만 반영되는 게 더 자연스럽기 때문에 이럴 때는 낙관락이 적합합니다.

하지만 상품 주문과 관련된 재고 차감에서 낙관락을 사용하면 어떤 문제가 발생할까요?

A와 B, 그리고 C라는 사람이 동일한 가게에서 동시에 '장미꽃다발'을 주문했습니다. 이런 일은 비교적 흔히 발생할 수 있으며 가게의 재고가 충분하다면 A, B, C 셋 다 재고를 차감하고 구매를 완료할 수 있어야 합니다.

이때 낙관락을 사용하고 있다면 여러 요청 중 먼저 들어온 하나는 성공하지만 나머지 요청은 실패하게 됩니다. A만 구매에 성공하고 나머지 B와 C는 재고가 충분히 남아있는 상황임에도 불구하고 구매에 실패하는 문제가 발생할 수 있습니다.

비관락은 DB레벨에서 락을 걸어 해당 row들에 대한 접근을 일시적으로 막습니다. 비관락을 사용하면 위 예제와 같은 동시 주문 상황에서도 데이터의 정합성을 잘 보장할 수 있습니다.

만약 BB의 재고와 쿠폰 시스템은 분산락이 아니라 낙관락을 사용했어도 아마 잘 동작했을 겁니다. 하지만 비관락은 DB에서 락을 걸기 때문에 단일 DB일 때만 동시성을 제어할 수 있습니다. 만약 DB가 Scale Out되어 여러 Replication이 존재하는 상황이라면 정합성을 보장하지 못합니다. 또한 선착순이 아닌 다른 로직들도 락이 해제될 때까지 대기해야 한다는 문제가 있습니다.

분산락을 선택한 이유

BB가 ‘여러 DB로 Scale Out되어있는 환경이냐?’라고 묻는다면 그렇지는 않습니다. 지금과 같은 작은 규모의 프로젝트라면 직접 실험해보지는 않았지만 Redis를 활용한 분산락보다 비관락을 사용하는 게 성능적으로도 더 우수할 수도 있습니다. (참고자료)

그럼에도 불구하고 재고와 쿠폰 모두 Redis를 활용한 분산락을 선택한 이유는 다음과 같습니다.

  1. MSA의 장점인 유연한 확장 가능성이 저해되지 않길 원했습니다. 비관락을 사용하면 추후 DB의 Scale Out을 고민할 때 걸림돌이 될 수 있다고 판단했습니다.
  2. 다른 MSA서비스에서 Redis의 사용이 예정되어 있어 이미 Redis를 사용할 수 있는 환경이 갖춰져 있었습니다.
  3. 재고가 아닌 선착순 쿠폰은 1인 1발급을 위해 설계 단계부터 Redis의 Set 자료구조를 이용해 구현할 계획을 세워둔 상태였습니다.
  4. 분산락을 직접 설계하고 구현해보고 싶었습니다. 앞으로 단기간에 트래픽이 몰려 특정 DB만 빠르게 Scale Out해야 할 상황을 마주할 경우가 많을 거라 생각했습니다.

정리해보면 저는 쿠폰은 처음부터 Redis를 활용할 계획을 가지고 설계를 진행했습니다. 또한 재고는 Redis를 활용할 수 있는 환경이 이미 구축되어 있는 상태에서 굳이 비관락을 사용해 확장 가능성을 줄이길 원치 않아서 분산락을 선택했습니다.
분산락이 낙관락과 비관락보다 무조건 더 좋은건 아니기 때문에 자신의 환경과 상황에 맞는 적절한 선택이 필요합니다.

만약 Redis를 Clustering방식으로 운영하며 RoundRobin과 같이 key가 같은 데이터라도 다른 Cluster에 저장될 가능성이 있다면 이때는 Redis를 통한 분산락 전략을 활용할 수 없습니다. 이때는 Setinel방식으로 운영을 변경하거나, 동시성 제어만을 위한 별도의 Standalone방식의 Redis를 새롭게 확보하는 방법을 이용해야 합니다.

분산락과 Redis의 Thread-safe

분산락은 공통된 DB를 이용해 어떤 자원이 사용 중인지를 확인하는 방법입니다. 사실 분산락을 반드시 Redis를 통해 구현해야 하는건 아닙니다. 하지만 대부분 Redis를 통한 분산락을 사용하는 이유는 Redis가 클라이언트의 명령을 Single Thread로 하나씩 처리하며 Incr, Setnx와 같은 명령어로 Atomicity를 보장할 수 있기 때문입니다.

References

profile
기록하고 정리하는 걸 좋아하는 개발자.
post-custom-banner

0개의 댓글