Skip to content

Conversation

@YeeunJ
Copy link
Collaborator

@YeeunJ YeeunJ commented Nov 8, 2025

Description

이번 PR에서는 선착순 구매(First-Come Purchase) 기능의 핵심 흐름과,
이를 지원하기 위한 Quartz 기반 재고 초기화 Job을 구현했습니다.

상품 타입이 FIRST_COME인 경우, 구매 시 Redis를 통해 재고를 실시간으로 제어하도록 처리했습니다.
Quartz Job을 통해 매일 새벽 KST 02:00에 Redis에 재고와 상품 정보를 미리 세팅(TTL=2일) 합니다.
구매 요청은 Kafka 이벤트(first-come-created)로 발행되어 Consumer에서 DB에 영속화됩니다.

Quartz 재고 초기화 Sequence diagram

mermaid-diagram-2025-11-13-214032
  • redis 접근으로 구매 관련 모든 정보를 알 수 있도록 상품 정보를 함께 redis에 저장해두었습니다.
  • Quartz Scheduler는 UTC 기준으로 동작하도록 해서, KST 실행 시점을 맞추기 위해 UTC 17:00으로 설정되어 있습니다.
  • TTL은 2일로 설정되어 있어, 신규 버전 전환 후 하루 이후까지 Redis 데이터가 유지됩니다.
  • 현재 추후 로그와 Slack / 메트릭 알림 연동 검토 예정입니다.

선착순 구매 Sequence diagram

mermaid-diagram-2025-11-13-212642
  • Kafka Consumer 실패 정책은 아직 적용되지 않아, 메시지 처리 중 예외가 발생할 경우 DLQ나 재시도 없이 kafka 성공 기준으로만 동작하고 있습니다.

  • Quartz 기반 상품 상태 점검 Job은 미구현 상태로, 상품 상태(INACTIVE_SOLD_OUT, INACTIVE_PERIOD) 변경을 주기적으로 수행하는 스케줄이 추가될 예정입니다. (sold out은 재고 부족 시 자동으로 처리할 예정이지만 제대로 처리가 되지 않았을 경우를 대비한 스케쥴을 추가할 예정입니다.)

  • 테스트 코드와 장애 복구 시나리오(Kafka 전송 실패, Redis 연결 단절과 같은 상황에 대한 fallback 로직)이 추가될 예정입니다.

파일 관련 추가 설명

PurchaseService.kt: 상품 타입 기반으로 구매 전략 선택

PurchaseStrategyFactory.kt: FIRST_COME 전략 분기 및 주입

FirstComePurchaseStrategy.kt: Redis 재고 확인, 감소, Kafka 이벤트 발행

StockService.kt: Redis에 접근하는 모든 상품과 재고 관련 로직 (상품 캐시(getProductCache..), 재고(getStock, decrementStock, recoverStock..)..)

KafkaProducer.kt: PurchaseCreatedEvent 발행 (topic="first-come-created")

PurchaseCreatedEventConsumer.kt: groupId="nearpick-group"으로 이벤트 수신 후 DB 저장

Related Issues :
#24

Test cases

Redis, kafka를 이용한 선착순 구매 로직의 정상 동작을 확인했습니다.

Kafka 이벤트 발행(purchase.created) 후 비동기 구매 처리 정상 수행 여부 확인했습니다.

Need additional checks?

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update (changes to docs or README)
  • Refactoring (code changes that neither fix a bug nor add a feature)
  • Test update (adding or updating tests)

Additional Context

이후 이 부분 관련한 테스트 코드 작성과 타임아웃, 중복 방지, 예외 발생 시 재시도 등 예외 처리를 추가할 예정입니다.

@YeeunJ YeeunJ requested a review from f-lab-black November 8, 2025 09:44
@YeeunJ YeeunJ self-assigned this Nov 8, 2025
var productType: ProductType,

var reservationDeadline: LocalDateTime? = null,
var startDt: LocalDateTime? = null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3. createdAt 과 같은 네이밍 규칙이 존재하는데, startedAt이 아닌 이유가 있을까요?


) {
fun update(request: UpdateProductRequest) {
if (this.status == ProductStatus.ACTIVE &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3. 업데이트시에 어떤 정책들을 통해 검증하고 있는지 유닛 테스트가 필요해보이네요.

override fun getStockVersionKey(productId: String): String =
"stock:v${LocalDate.now()}:$productId"

@Transactional(readOnly = true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q. 여기에 트랜잭션 어노테이션이 있는 경우, 어떤 역할을 수행하게 되나요?


fun initializeForVersion(products: List<ProductEntity>, version: String) {
val stockMap = products.associate { product ->
val key = "stock:$version:${product.id}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1. 현재 레디스의 키들을 만드는 로직들이 전반적으로 서비스 레이어에 파편화 되어 있는 것 같습니다. 이 부분을 별도의 Pure 객체를 정의하고, 이를 토대로 의미론적으로 key들을 반환받도록 리팩토링할 필요가 있어보입니다. 예를 들어, InMemoryStock 과 같이 별도의 도메인 객체를 만들고, 전달 받은 파라미터들을 토대로 key를 생성하는 등 일관되게 관리할 수 있도록 정책 객체를 따로 두어야할 것 같아요.


fun initializeProductCache(products: List<ProductEntity>, version: String) {
val cacheMap: Map<String, String> = products.associate { product ->
val key = "product:${version}:${product.id}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3. product과 stock을 stockService에서 제어하고 있는데, 이 부분이 역할적인 측면에서 맞을까요?

end
""".trimIndent()

val result = redisTemplate.execute(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3. 실행시 발생할 수 있는 예외들을 재정의할 필요가 있어보입니다. 예를 들어, 찾고자 하는 stock이 없거나, quantity의 값이 음수가 될 수 있는 등 다양한 케이스들에 대해 예외들을 잘 정의해야할 것 같아요.

…into feature/purchase-create-detail

# Conflicts:
#	app/src/main/kotlin/com/nearpick/app/domain/purchase/controller/PurchaseController.kt
#	domain/src/main/kotlin/com/nearpick/app/domain/purchase/service/PurchaseService.kt
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants