글 작성자: Key Ryung

개요

이전 포스팅에서 진행했듯이 Lua Script를 적용함으로써 Redis의 Atomic한 재고 관리를 수행할 수 있었습니다. 곧바로 nGrinder를 통해 트래픽을 발생시켜 이전보다 개선되었는지 확인하고 싶은 마음이 굴뚝같았으나 다시 한번 고민에 빠졌습니다.

 

왜 항상 Database는 구매 시마다 차감되는 재고를 기록해야될까?

 

A. [#10] 재고 관리 Integrity 문제 포스팅을 해결하는 시점에서는

  • 재고를 실시간으로 차감하여 실물상 재고와 서비스 상의 재고를 일치시켜 고객 경험을 높이기 위해서 구매 시마다 Database에서의 실시간 재고 차감이 필요합니다.

B. [#11] 재고 관리는 어떻게 해야 될까? - 1. Redis Transaction 포스팅부터 [#11] 재고 관리는 어떻게 해야될까? - 2. Lua Script를 적용하는 시점에서는

  • 재고는 Cache Layer에서 실물상 재고와 서비스 상의 재고를 일치시키므로 Database에서의 실시간 재고 차감은 불필요합니다.

 

처음 문제를 정의하고 해결 방법들을 고안한 시점과는 다르게 이제 Database는 실시간으로 재고 차감이 필요 없다고 생각됐습니다. 비즈니스 요구 사항(실물과 서비스 재고의 일치)을 Cache Layer의 책임으로 옮김으로써 Database는 더 이상 해당 요구 사항에 대한 책임을 지지 않아도 됩니다.

 

현재 구매 주문 과정을 Sequence Diagram으로 표현해본다면 다음과 같을 것입니다.

 

구매주문 과정 Sequence Diagram


0. 상위 레이어인 PurchaseOrderAssembleService에서 주문 접수

 

1. 하위 레이어인 ProductOptionReadService에서 해당 구매 물품 Entity 획득

- Repository Layer를 통해 Database에서 SELECT 실행

 

2. 하위 레이어인 UserReadService에서 구매자 Entity를 획득

- Repository Layer를 통해 Database에서 SELECT 실행

 

3. 하위 레이어인 PurchaseCreateService에서 구매 주문서 작성(저장)

- Repository Layer를 통해 Database에서 INSERT 실행

 

4. 하위 레이어인 ProductOptionStockService에서 구매 상품의 재고 차감

- Cache Layer를 통해 Redis에서 재고 차감
- Repository Layer를 통해 Database에서 UPDATE 실행(Lock)


위와 같은 순서대로 실행된다면 Database도 실시간으로 재고를 차감하며 비즈니스 요구 사항에 대한 책임을 함께 수행하게 됩니다.

Database Node Monitoring / 위 - DB Lock, 아래 - Distributed-Lock

이전에도 언급했듯이 Database에서의 UPDATE는 Lock을 동반하므로 IO wait를 동반합니다. 그렇다면 결국 테스트를 해보지 않아도 이전과 같은 결과가 나올 것이 예상되었습니다.

 

Consistency에도 종류가 있다

앞선 일련의 고민들에서 대해서 조사를 하던 중 Consistency의 종류에 대해서 알게 되었습니다. Strong, Weak, Eventually Consistent 이외에도 더 자세히 분류할 수도 있지만 현재는 이 정도만 언급하겠습니다.

 

직역해보면 강한 정합성, 약한 정합성, 궁극적 정합성입니다. 강한 정합성은 매 순간 정합성을 맞추는 방식, 약한 정합성은 정합성이 안 맞아도 넘어가는 방식, 궁금적 정합성은 특정 타이밍, 시간대 동안은 정합성이 안 맞을 수 있으나 궁극적으로는 정합성이 맞게 되는 방식으로 간략하게 설명할 수 있습니다.

 

현재까지 설계한 구조는 Strong Consistency 하므로 실시간으로 정합성이 보장되지만 이로 인해 TPS Performance는 Trade-Off 하게 되었습니다. 하지만 Cache Layer를 Database 앞단에 배치함으로써 Cache를 통해서는 Strong Consistency를 만족하고 있습니다. Database는 데이터의 영속성을 위해 사용되므로 최종적으로는 실제 재고를 갖고 있어야 하고 실시간으로는 재고가 틀려도 서비스에는 무관합니다. Eventaully Consistency!

 

이제 다른 고민이 시작되었습니다. 어떻게 Eventually Consistency 하게 만들 수 있을까?

 

Publish/Subscribe 패턴을 Spring에서

Eventually Consistency를 구현하는 여러 가지 방법을 생각하던 중 디자인 패턴의 Pub/Sub 패턴이 가장 먼저 생각났습니다.

Event Publish(?)

Event를 발행하는 Publisher와 Event를 수신하는 Subscriber는 구조적으로 떨어져 있다는 부분을 실마리로 생각했습니다. Publisher에서는 구매 주문에 대한 요청만 진행하고 실제 차감은 구매 주문 요청을 받아들이는 Subscriber에서 진행한다면 Subscriber에서 처리가 완료될 때까지는 정합성이 맞지 않지만 완료가 된 이후에는 정합성이 맞게 될 것입니다.

 

가장 빠르게 Pub/Sub을 구현하는 방향으로 선정을 진행했습니다. 그 결과 바로 Spring Event Publisher/Listener입니다. ApplicationContext가 상속하고 있는 기본 Feature이기 때문에 추가적인 Component(MQ, Streaming)가 필요 없고 Spring 기능을 충실히 사용하여 Over-Engineering을 하지 말자는 프로젝트의 취지와도 부합한다고 판단했습니다.

 

Spring Event Publisher를 사용했을 때의 Class Diagram을 구상해보았습니다. 이전의 구조와는 다르게 재고 수량을 업데이트하는 Service는 구매 주문을 요청 시 필요 없으므로 관계가 필요 없게 됩니다. 상위 Service Layer인 PurchaseOrderAssembleService는 구매 주문서를 작성하는 것과 Cache Layer에서 재고를 관리하는 Service와 Event Publisher를 Composition 관계로 연결되어 있습니다.

 

구매 주문을 수행하는 Service의 Class Diagram

 

구매 주문이 수행되는 과정을 Sequence diagram 중 Listener 부분만 나타내면 다음과 같습니다.

 

EventListener에서 특징적인 부분은 @Async와 @TransactionalEventListener입니다.

@Slf4j
@RequiredArgsConstructor
@Component
public class SellProductEventListener {

	private final ProductOptionUpdateService productOptionUpdateService;
	private final PurchaseOrderUpdateService purchaseOrderUpdateService;

	@Async
	@TransactionalEventListener
	public void handleSellProductEvent(SellProductEvent sellProductEvent) {
		productOptionUpdateService.deductProductOptionCount(
			sellProductEvent.getSoldProductOptionId(),
			sellProductEvent.getPurchaseAmount()
		);

		purchaseOrderUpdateService.acceptPurchaseOrderStatus(sellProductEvent.getPurchaseOrderId());
	}
}

 

@TransactionEventListener는 Publish가 포함되어 있는 Transaction이 Commit 된 이후에 Event를 Listen 하여 처리합니다. 만약 구매 주문이 정상적으로 접수되지 않으면 Event를 수신하는 측에서도 해당 Event에 대한 처리를 진행하지 않습니다. 그리고 Event가 수신되었을 때 Asynchronous 하게 처리하도록 @Async를 추가하였습니다.

 

Lua Script와 Pub/Sub 구조 적용 후 Test

Pub/Sub 구조까지 적용을 완료한 뒤 테스트를 진행했습니다. VUser는 이전과 마찬가지로 500으로 설정하였습니다.

  도입 전 도입 후
TPS 411.8 1651.4

이전의 구조에 비해서 4배 정도의 TPS 성능이 올라간 것을 확인할 수 있었습니다. Database의 지표도 살펴보겠습니다.

 

위 - 도입 전 / 아래 - 도입 후

도입 전과 도입 후를 비교해보면 Busy wait(RED)/Busy user(BLUE)가 테스트 시작 후 특정 시점 이후에 점유율의 추세가 달라지는 것을 확인할 수 있습니다. 재고의 차감은 지속적으로 수행되고 있으며 이전에 응답은 반환됩니다. 추가적으로 Eventually Consistency 하게 되므로 주문의 상태가 Database에 반영될 때 정상적으로 주문이 접수되었다는 것을 사용자에게 보여줄 필요성을 느껴 구매 주문의 상태를 분류했습니다.

 

public enum PurchaseOrderStatus {
	ACCEPTED,
	CANCELLED,
	IN_PROGRESS
}

 

초기에 주문이 접수되면 IN_PROGRESS 상태로 저장되고 재고가 차감된 이후에 ACCEPTED로 전환되어 사용자가 자신이 한 주문의 상태를 판별할 수 있는 부분을 Enum을 통해서 표현하도록 했습니다. Status를 Enum으로 표현함으로써 추후 비즈니스 요구 사항에 배송 지연됨, 결제 중, 결제 완료와 같은 부분이 추가되는 상황에서도 기존의 로직은 유지하고 확장할 수 있도록 구성했습니다.

 

Conclusion

이번 포스팅을 마지막으로 현재까지 진행한 YOUSINSA의 개선 과정은 모두 포스팅을 통해 완료했습니다!

 

하지만 아는 만큼 보인다고 개선을 진행할수록 개선이 필요한 부분들이 더 많아지는 것으로 보이고 아쉬운 부분들이 더 커져갔습니다. 처음 시작은 계획했던 Version 3까지는 완료하고 이직 준비를 진행하자라고 생각했지만 Naver Cloud Platform에서 지원해주는 금액이 고갈되어 가는 부분과 실서비스에서 트래픽을 만나보고 싶은 열망이 더 커지게 되어 남은 Version 3와 Version 4는 이직 후에 이어나갈 계획입니다. 

그동안 다양한 테스트를 위해 사용한 783,970원의 내역들
나중에 다시 만나자

 

해당 프로젝트는 네이버 클라우드를 활용하여 진행했습니다.

 

REF

https://www.orchard.co.uk/blog/are-too-many-messages-turning-off-consumers-15191.aspx

 

Are too many messages turning off consumers?

× Are too many messages turning off consumers?08/09/2017 In a survey it was revealed that nearly two-thirds of adults in the UK say they receive too many digital marketing promotions. Consumers revealed that they wouldn’t think twice about unsubscribing

www.orchard.co.uk