-
Notifications
You must be signed in to change notification settings - Fork 44
[7주차] 이벤트 기반 아키텍처, 카프카 - 최호석 #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ghtjr410
wants to merge
32
commits into
Loopers-dev-lab:ghtjr410
Choose a base branch
from
ghtjr410:volume-7
base: ghtjr410
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
7994f6a
feat: ApplicationEvent 기반 부가 로직 분리
ghtjr410 4aa948b
feat: Transactional Outbox Pattern 구현
ghtjr410 646f4e0
feat: Kafka Consumer 멱등 처리 + 메트릭 집계
ghtjr410 c5ed60d
feat: Kafka 기반 선착순 쿠폰 발급
ghtjr410 a68c233
fix: AsyncConfig에 AsyncUncaughtExceptionHandler 안전망 추가
ghtjr410 ff29345
feat: DLQ 구성 + Consumer Group 분리 + 배치 처리
ghtjr410 d897d3a
feat: likeCount Reconciliation 스케줄러 + LikeApiE2ETest 비동기 대응
ghtjr410 bb79d3e
refactor: Outbox Relay를 즉시 발행 + Scheduled 보완 + 셀프컨슘으로 전환
ghtjr410 075e27d
refactor: Outbox INSERT를 BEFORE_COMMIT 리스너에서 Facade 직접 호출로 전환
ghtjr410 3492930
feat: Consumer 운영 가시성 추가 (EventLog + ConsumerMetrics)
ghtjr410 396d98f
fix: Consumer 셀프컨슘으로 Outbox SENT 마킹 + Javadoc 수정
ghtjr410 e2394f1
fix: OutboxRelayScheduler를 .get() 동기 방식으로 전환, backoff 제거
ghtjr410 d32c0f8
refactor: 토픽 상수 중앙화 + TransactionHelper 적용
ghtjr410 d8e5194
refactor: 셀프컨슘 제거, Kafka ACK 기준 SENT 마킹으로 통일
ghtjr410 9838ef9
refactor: SENT 마킹을 @Modifying + @Transactional로 단순화
ghtjr410 cd9fc59
feat: OutboxEventService에 @Transactional(MANDATORY) 추가
ghtjr410 b8b10ec
test: EDA 단위 테스트 5개 추가
ghtjr410 aeeb877
test: commerce-api 통합 테스트 3개 추가
ghtjr410 9367061
test: commerce-streamer 통합 테스트 2개 추가
ghtjr410 aac48c3
test: Consumer 파싱/분기 단위 테스트 + streamer 통합 테스트 추가
ghtjr410 6755f9a
test: 스케줄러 통합 테스트 2개 추가
ghtjr410 58ba3f2
test: CouponAsyncApiE2ETest 추가
ghtjr410 c55b3eb
refactor: 쿠폰 발급을 네이티브 쿼리에서 JPA Entity + JPQL로 전환 + 통합 테스트
ghtjr410 c591b89
test: CouponIssueConcurrencyTest 추가
ghtjr410 057d32c
refactor: Reconciliation을 product_metrics에서 likes COUNT(*)로 전환 + 통합 테스트
ghtjr410 a130ebf
chore: 리소스 풀 설정을 EC2 large(2vCPU, 8GB) 환경에 맞게 조정
ghtjr410 21886fc
fix: findPending에 stale 조건 추가 + PR 문서 트레이드오프 보강
ghtjr410 518ff3f
fix: Kafka Consumer 설정 현재 트래픽 기준 조정 + PR 문서 피드백 반영
ghtjr410 9db92dc
docs: product_metrics Reconciliation 부재 근거 + RETENTION_DAYS 7일 SLA 주석 추가
ghtjr410 7fe545c
docs: PR 문서 가독성 개선 — 목차 추가 + 시각 자료 전 요약 선행
ghtjr410 cb01af5
chore: PR.md, blog.md 삭제
ghtjr410 fb0d229
feat: CREATED 주문 만료 스케줄러 추가 — 10분 경과 시 자동 취소 + 재고/쿠폰 복원
ghtjr410 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponIssueRequestInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.loopers.application.coupon; | ||
|
|
||
| import com.loopers.domain.coupon.CouponIssueRequest; | ||
| import com.loopers.domain.coupon.CouponIssueStatus; | ||
|
|
||
| public record CouponIssueRequestInfo( | ||
| Long requestId, | ||
| Long couponId, | ||
| Long userId, | ||
| CouponIssueStatus status, | ||
| String rejectReason | ||
| ) { | ||
| public static CouponIssueRequestInfo from(CouponIssueRequest request) { | ||
| return new CouponIssueRequestInfo( | ||
| request.getId(), | ||
| request.getCouponId(), | ||
| request.getUserId(), | ||
| request.getStatus(), | ||
| request.getRejectReason() | ||
| ); | ||
| } | ||
| } |
5 changes: 5 additions & 0 deletions
5
apps/commerce-api/src/main/java/com/loopers/application/event/CouponIssueRequestedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| public record CouponIssueRequestedEvent(String eventId, Long couponId, Long userId) { | ||
| } |
61 changes: 61 additions & 0 deletions
61
apps/commerce-api/src/main/java/com/loopers/application/event/LikeCountEventHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.loopers.application.product.ProductService; | ||
| import com.loopers.confg.kafka.KafkaTopics; | ||
| import com.loopers.infrastructure.product.ProductCacheManager; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.kafka.core.KafkaTemplate; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.event.TransactionPhase; | ||
| import org.springframework.transaction.event.TransactionalEventListener; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class LikeCountEventHandler { | ||
|
|
||
| private final ProductService productService; | ||
| private final ProductCacheManager productCacheManager; | ||
| private final KafkaTemplate<Object, Object> kafkaTemplate; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handleLiked(ProductLikedEvent event) { | ||
| try { | ||
| productService.incrementLikeCount(event.productId()); | ||
| productCacheManager.evictDetail(event.productId()); | ||
| publishToKafka("product.liked", event.productId(), event); | ||
| } catch (Exception e) { | ||
| log.error("좋아요 집계 실패: productId={}", event.productId(), e); | ||
| } | ||
| } | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handleUnliked(ProductUnlikedEvent event) { | ||
| try { | ||
| productService.decrementLikeCountIfPositive(event.productId()); | ||
| productCacheManager.evictDetail(event.productId()); | ||
| publishToKafka("product.unliked", event.productId(), event); | ||
| } catch (Exception e) { | ||
| log.error("좋아요 취소 집계 실패: productId={}", event.productId(), e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 비핵심 지표(좋아요)는 Outbox 없이 직접 Kafka fire-and-forget. | ||
| * 유실돼도 비즈니스 불변식이 깨지지 않는다. product_metrics 근사치로 충분. | ||
| */ | ||
| private void publishToKafka(String eventType, Long productId, Object event) { | ||
| try { | ||
| String payload = objectMapper.writeValueAsString(event); | ||
| kafkaTemplate.send(KafkaTopics.CATALOG_EVENTS, String.valueOf(productId), payload); | ||
| } catch (Exception e) { | ||
| log.warn("Kafka 직접 발행 실패 (fire-and-forget): eventType={}, productId={}", eventType, productId, e); | ||
| } | ||
| } | ||
| } | ||
5 changes: 5 additions & 0 deletions
5
apps/commerce-api/src/main/java/com/loopers/application/event/PaymentCanceledEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| public record PaymentCanceledEvent(Long paymentId, Long orderId, Long userId) { | ||
| } |
7 changes: 7 additions & 0 deletions
7
apps/commerce-api/src/main/java/com/loopers/application/event/PaymentCompletedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
| public record PaymentCompletedEvent(Long paymentId, Long orderId, Long userId, BigDecimal amount) { | ||
| } |
5 changes: 5 additions & 0 deletions
5
apps/commerce-api/src/main/java/com/loopers/application/event/PaymentFailedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| public record PaymentFailedEvent(Long paymentId, Long orderId, Long userId, String reason) { | ||
| } |
5 changes: 5 additions & 0 deletions
5
apps/commerce-api/src/main/java/com/loopers/application/event/ProductLikedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| public record ProductLikedEvent(Long userId, Long productId) { | ||
| } |
5 changes: 5 additions & 0 deletions
5
apps/commerce-api/src/main/java/com/loopers/application/event/ProductUnlikedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
|
|
||
| public record ProductUnlikedEvent(Long userId, Long productId) { | ||
| } |
4 changes: 4 additions & 0 deletions
4
apps/commerce-api/src/main/java/com/loopers/application/event/ProductViewedEvent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
| public record ProductViewedEvent(Long userId, Long productId) { | ||
| } |
55 changes: 55 additions & 0 deletions
55
apps/commerce-api/src/main/java/com/loopers/application/event/UserActivityEventHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.loopers.application.event; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.event.TransactionPhase; | ||
| import org.springframework.transaction.event.TransactionalEventListener; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| public class UserActivityEventHandler { | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handleProductViewed(ProductViewedEvent event) { | ||
| try { | ||
| log.info("상품 조회: userId={}, productId={}", event.userId(), event.productId()); | ||
| } catch (Exception e) { | ||
| log.error("상품 조회 로깅 실패: productId={}", event.productId(), e); | ||
| } | ||
| } | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handlePaymentCompleted(PaymentCompletedEvent event) { | ||
| try { | ||
| log.info("결제 완료: paymentId={}, orderId={}, userId={}, amount={}", | ||
| event.paymentId(), event.orderId(), event.userId(), event.amount()); | ||
| } catch (Exception e) { | ||
| log.error("결제 완료 로깅 실패: paymentId={}", event.paymentId(), e); | ||
| } | ||
| } | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handlePaymentFailed(PaymentFailedEvent event) { | ||
| try { | ||
| log.info("결제 실패: paymentId={}, orderId={}, userId={}, reason={}", | ||
| event.paymentId(), event.orderId(), event.userId(), event.reason()); | ||
| } catch (Exception e) { | ||
| log.error("결제 실패 로깅 실패: paymentId={}", event.paymentId(), e); | ||
| } | ||
| } | ||
|
|
||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Async | ||
| public void handlePaymentCanceled(PaymentCanceledEvent event) { | ||
| try { | ||
| log.info("결제 취소: paymentId={}, orderId={}, userId={}", | ||
| event.paymentId(), event.orderId(), event.userId()); | ||
| } catch (Exception e) { | ||
| log.error("결제 취소 로깅 실패: paymentId={}", event.paymentId(), e); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
publishToKafka()publishes the raw ProductLiked/UnlikedEvent JSON directly tocatalog-events, but CatalogEventConsumer expects an envelope witheventId/eventType/payload. This contract mismatch will cause parsing failures (or empty eventType) in the streamer and prevent metrics updates. Consider publishing the same envelope format used by the consumers (and generating an eventId), or changing consumers to match the produced schema.