NDM

4. 재고 처리 로직 동시성 이슈 해결 일지 본문

[프로젝트] Slow Delivery

4. 재고 처리 로직 동시성 이슈 해결 일지

ndm.jr 2022. 8. 31. 10:21

배달 어플을 구현하는 SlowDelivery 프로젝트에서 재고를 처리하는데 동시성 이슈가 있었습니다

 

더보기

하지만 사실 재고 도메인은 필요가 없었는지도 모릅니다. 일반적인 쇼핑몰과 다르게

배달 어플은 판매중인 상품의 재고를 숫자로 나타내지 않고 각 가게마다 유연하게 판매 가능 / 판매 불가능 상태값을 바꿔주는 로직만 작성해주면 굳이 재고의 증가/감소를 구현할 필요는 없었을 수도 있습니다.

 

하지만 이 프로젝트는 순수하게 학습용이고, 취업 시 [이런것도 구현하고 고민해 보았다] 를 어필하고 싶었고, 무엇보다 이커머스 분야에서 일하고 싶다면 한번쯤은 경험해보는게 낫겠다 싶어 억지로(?) 끼워넣은 도메인입니다

 

때문에 프로젝트 주제나 도메인에 있어 어색함이 있을 수 있습니다

 

현재 제 프로젝트의 주문 로직은 주문접수 -> 재고 확인 -> 재고 감소 -> 주문 처리 -> 결제 -> 배달의 순서로 진행됩니다.

하지만 재고 감소 이전에 다른 스레드에서 주문이 또 들어온다면 같은 재고값을 읽을 것이기 떄문에, 이러한 동시성 이슈를 해결할 방법이 필요했습니다.

 

결과적으로는 Redis를 이용해 해결했으나, Redis라는 해결 방안에 도달하기까지의 생각을 정리하는 포스팅입니다.


# Synchronized

 

Java Application 레벨에서 동시성을 해결하는 거의 유일한 방법입니다. 한 Thread가 점유중인 작업에서 다른 Thread의 접근에 대한 동시성을 반드시 보장해줍니다. 하지만 몇가지 문제점이 있었습니다.

 

  • 첫번쨰는 성능입니다.

openJDK의 소스를 발췌했습니다

왼쪽은 Java의 ConcurrentHashMap, 오른쪽은 HashTable 클래스의 Put 메소드를 발췌한 것입니다.

두 클래스는 모두 Hash함수를 이용해 키를 설정하고, 버킷에 데이터를 저장하는 자료구조이며 동시성을 보장합니다.

두 클래스 모두 Synchronized를 사용하고 있습니다만 차이점이 존재합니다.

 

HashTable은 메소드 자체에 Synchronized를 걸었고, ConcurrentHashMap은 메소드 내부에 블럭을 생성해 걸었습니다.

Synchronized를 사용하면 Java의 멀티스레드라는 특성을 제한해 강제로 싱글스레드처럼 동작하게 만들어버리므로, 동시성은 해결할 수 있지만 그만큼 성능이 떨어지는 것을 뜻합니다.

만약 메소드 레벨에 건다면 메소드 자체에 한 스레드밖에 진입할 수 없다는 걸 뜻하는 것이며, 이를 조금이라도 완화하고자 ConcurrentHashMap에서는 내부에 블럭을 생성해서 건 것이죠.

 

  • 두 번째는 서버 한 대로 운영할 경우에만 동시성을 보장한다는 것입니다.

 

간단하게 위와 같이 서버가 2대라면, 이 경우에도 동시성을 보장할까요?

그렇지 않습니다. Synchronized는 어플리케이션을 기준으로 동작하기 때문에 다른 서버에서의 동시성까지 막아주지는 않습니다. 트래픽이 분산되는 환경에서 두 대의 서버에 거의 동시적으로 사용자가 접근한다면, 같은 메소드가 동시에 호출될 것이고 DB의 데이터에 동시 접근해 다른 사용자가 같은 재고값을 확인하는 결과가 충분히 발생할 수 있습니다.

 

때문에 Synchronized는 배제하게 되었습니다.

 


# DB Lock

Optimistic Lock과 Pessimistic Lock 모두를 고려했으나 두 가지 모두 적합하지 않다는 판단을 내렸습니다.

 

Pessimistic Lock의 경우, 공유자원에 대해 동시 접근이 일어날 것을 예상하고 거는 락입니다.

때문에 격리성이 매우 높은 대신, 그만큼 성능을 포기한다는 특성이 있습니다.

재고 처리의 경우 반드시 충돌이 일어나는 로직인 만큼 고려해 보았지만, 사실상 Synchronized를 거는 것과 별 다른 성능을 기대할 수 없었기에 배제하게 되었습니다. 데드락의 가능성도 무시할 수 없었습니다.

 

Optimistic Lock의 경우, 동시성 문제가 터지면 그때 가서 해결을 하자는 방법론에서 비롯된 락입니다.

실패할 경우 롤백 처리를 일일히 해줘야 하며, Retry로직도 구현을 해 줘야합니다.

즉, Retry까지 고려한다면 재고 동시성 문제가 뻔하게 터지는 상황에서 DB에 가해지는 부하가 매우 심할 것이라고 예상했습니다.

 

또한 두 가지 방법 모두 결국은 RDB에 요청을 보내야 한다는 특징이 있습니다.(낙관적 락의 경우는 더 심할것입니다)

[재고 확인 / 주문 시 재고 감소 / 실패 시 재고 원복] 은 매우 빈번하게 호출되는 메소드입니다. 저는 DB에 가해지는 부하가 매우 심각할 것이라고 여겼습니다.

또한, [주문 접수 -> 재고 확인 -> 재고 감소 -> 주문 처리 -> 주문 완료 -> 결제] 로 진행되는 주문 로직에서, 재고 확인 / 재고 감소까지 RDB에서 해결한다면 하나의 트랜잭션이 너무 긴 로직에 물려있다고 생각했습니다. 매우 빈번하게 호출되는 주문 로직에 이렇게 길게 트랜잭션이 물려있다면, Connection Time out이 나올 확률도 있다고 생각했습니다.

 

때문에 DB Lock을 이용한 방법을 배제하게 되었고, 재고의 동시성 문제를 해결할 수 있으며 RDB의 트랜잭션이 로직을 수행하는 시간도 줄일 수 있는 방법을 찾던 중 Redis를 생각해보게 되었습니다.

 

2편에서 이어집니다

https://ndm-tech.tistory.com/88

 

재고 처리 로직 동시성 이슈 해결 일지 2

동시성을 해결하기 위해 Redis를 선택하게 된 생각의 과정은 앞선 포스팅에서 정리하였습니다. https://ndm-tech.tistory.com/34 4. 재고 처리 로직 동시성 이슈 해결 일지 배달 어플을 구현하는 SlowDelivery

ndm-tech.tistory.com