diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 53feb3cfa..7848bd950 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -2,6 +2,7 @@ dependencies {
     // add-ons
     implementation(project(":modules:jpa"))
     implementation(project(":modules:redis"))
+    implementation(project(":modules:kafka"))
     implementation(project(":supports:jackson"))
     implementation(project(":supports:logging"))
     implementation(project(":supports:monitoring"))
@@ -25,6 +26,7 @@ dependencies {
     // test-fixtures
     testImplementation(testFixtures(project(":modules:jpa")))
     testImplementation(testFixtures(project(":modules:redis")))
+    testImplementation(testFixtures(project(":modules:kafka")))
 
     // awaitility (비동기 테스트)
     testImplementation("org.awaitility:awaitility:4.2.0")
diff --git a/apps/commerce-api/docker-compose.yml b/apps/commerce-api/docker-compose.yml
deleted file mode 100644
index 0e1310f0b..000000000
--- a/apps/commerce-api/docker-compose.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-version: '3.8'
-services:
-  app:
-    build: .
-    ports:
-      - "8080:8080"
-    environment:
-      - SPRING_PROFILES_ACTIVE=local
-      - product.data.generate=true
-    deploy:
-      resources:
-        limits:
-          cpus: '0.5'
-          memory: 512M
-        reservations:
-          cpus: '0.25'
-          memory: 256M
-    restart: unless-stopped
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
index ee54b238f..fe3e9ce66 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
@@ -9,7 +9,6 @@
 import com.loopers.domain.product.ProductCacheService;
 import com.loopers.domain.product.ProductEntity;
 import com.loopers.domain.product.ProductService;
-import com.loopers.domain.tracking.UserBehaviorTracker;
 import com.loopers.domain.user.UserEntity;
 import com.loopers.domain.user.UserService;
 import com.loopers.support.error.CoreException;
@@ -34,7 +33,6 @@ public class LikeFacade {
     private final UserService userService;
     private final LikeService likeService;
     private final ProductCacheService cacheService;
-    private final UserBehaviorTracker behaviorTracker;
 
 
     /**
@@ -58,14 +56,8 @@ public LikeInfo upsertLike(String username, Long productId) {
         // 3. 좋아요 등록/복원 (도메인 엔티티에서 이벤트 발행)
         LikeResult result = likeService.upsertLike(user, product);
 
-        // 4. 유저 행동 추적 (좋아요 액션)
-        if (result.changed()) {
-            behaviorTracker.trackLikeAction(
-                    user.getId(),
-                    productId,
-                    "LIKE"
-            );
-        }
+        // 4. 유저 행동 추적은 도메인 이벤트(LikeChangedEvent)에서 처리됨
+        // 중복 이벤트 방지를 위해 UserBehaviorTracker 호출 제거
 
         // 5. DTO 변환 후 반환
         return LikeInfo.of(result.entity(), product, user);
@@ -93,13 +85,7 @@ public void unlikeProduct(String username, Long productId) {
         // 3. 좋아요 취소 (도메인 엔티티에서 이벤트 발행)
         boolean isChanged = likeService.unlikeProduct(user, product);
 
-        // 4. 유저 행동 추적 (좋아요 취소 액션)
-        if (isChanged) {
-            behaviorTracker.trackLikeAction(
-                    user.getId(),
-                    productId,
-                    "UNLIKE"
-            );
-        }
+        // 4. 유저 행동 추적은 도메인 이벤트(LikeChangedEvent)에서 처리됨
+        // 중복 이벤트 방지를 위해 UserBehaviorTracker 호출 제거
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderEventHandler.java
index e31cd3599..4326d9848 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderEventHandler.java
@@ -1,13 +1,25 @@
 package com.loopers.application.order;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
 
+import com.loopers.domain.event.EventTypes;
+import com.loopers.domain.event.EventVersions;
+import com.loopers.domain.event.outbox.OutboxEventEntity;
+import com.loopers.domain.event.outbox.OutboxRepository;
+import com.loopers.domain.order.OrderEntity;
+import com.loopers.domain.order.OrderItemEntity;
+import com.loopers.domain.order.OrderService;
 import com.loopers.domain.payment.event.PaymentCompletedEvent;
 import com.loopers.domain.payment.event.PaymentFailedEvent;
 import com.loopers.domain.payment.event.PaymentTimeoutEvent;
+import com.loopers.infrastructure.event.DomainEventEnvelopeFactory;
+import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -20,29 +32,44 @@
 @Slf4j
 @RequiredArgsConstructor
 public class OrderEventHandler {
+    private static final String ORDER_EVENTS_TOPIC = "order-events";
+
     private final OrderFacade orderFacade;
+    private final OrderService orderService;
+    private final DomainEventEnvelopeFactory envelopeFactory;
+    private final OutboxRepository outboxRepository;
 
     private void executeSafely(String action, Long orderId, Long userId, Runnable task) {
         if (orderId == null || userId == null) {
-            log.warn("이벤트 무시 - 필수 값 누락 action={}, orderId={}, userId={}", action, orderId, userId);
+            log.warn("이벤트 무시 - 필수 값 누락 action={}, orderNumber={}, userId={}", action, orderId, userId);
             return;
         }
         try {
-            log.debug("이벤트 처리 시작 action={}, orderId={}, userId={}", action, orderId, userId);
+            log.debug("이벤트 처리 시작 action={}, orderNumber={}, userId={}", action, orderId, userId);
             task.run();
-            log.info("이벤트 처리 성공 action={}, orderId={}, userId={}", action, orderId, userId);
+            log.info("이벤트 처리 성공 action={}, orderNumber={}, userId={}", action, orderId, userId);
         } catch (Exception e) {
-            log.error("이벤트 처리 실패 action={}, orderId={}, userId={}", action, orderId, userId, e);
+            log.error("이벤트 처리 실패 action={}, orderNumber={}, userId={}", action, orderId, userId, e);
         }
     }
 
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
     public void handlePaymentCompleted(PaymentCompletedEvent event) {
-        Long orderId = event.orderId();
+        Long orderId = event.orderNumber();
         Long userId = event.userId();
+
+        // 1. 주문 확정 처리
         executeSafely("PAYMENT_COMPLETED", orderId, userId,
                 () -> orderFacade.confirmOrderByPayment(orderId, userId));
+
+        // 2. Kafka 이벤트용 Outbox 저장
+        try {
+            savePaymentSuccessToOutbox(event);
+        } catch (Exception e) {
+            log.error("결제 완료 이벤트 Outbox 저장 실패 - orderNumber={}, userId={}",
+                    orderId, userId, e);
+        }
     }
 
     @Async
@@ -62,4 +89,96 @@ public void handlePaymentTimeout(PaymentTimeoutEvent event) {
         executeSafely("PAYMENT_TIMEOUT", orderId, userId,
                 () -> orderFacade.cancelOrderByPaymentFailure(orderId, userId));
     }
+
+    /**
+     * 결제 완료 이벤트를 Outbox에 저장
+     * 원자성과 멱등성을 보장하기 위해 상품별로 개별 이벤트를 생성하여 일괄 저장
+     */
+    private void savePaymentSuccessToOutbox(PaymentCompletedEvent event) {
+        final Long orderNumber = event.orderNumber();
+        final Long userId = event.userId();
+
+        if (orderNumber == null || userId == null) {
+            log.warn("Outbox 저장 스킵 - 필수값 누락 orderNumber={}, userId={}", orderNumber, userId);
+            return;
+        }
+
+        final OrderEntity order = orderService.getOrderByOrderNumberAndUserId(orderNumber, userId);
+        final List<OrderItemEntity> orderItems = orderService.getOrderItemsByOrderId(order);
+
+        if (orderItems.isEmpty()) {
+            log.warn("Outbox 저장 스킵 - 주문 상품이 없음 orderId={}", order.getId());
+            return;
+        }
+
+        // 상품별로 개별 이벤트 생성 및 일괄 저장 (원자성 보장)
+        try {
+            savePaymentSuccessEventsAtomically(order, orderItems);
+            log.info("결제 완료 이벤트 Outbox 일괄 저장 완료 - orderId={}, itemCount={}",
+                    order.getId(), orderItems.size());
+        } catch (Exception e) {
+            log.error("결제 완료 이벤트 Outbox 저장 실패 - orderId={}, itemCount={}",
+                    order.getId(), orderItems.size(), e);
+            throw e; // 상위로 전파하여 전체 트랜잭션 롤백
+        }
+    }
+
+    /**
+     * 상품별 결제 완료 이벤트를 원자적으로 Outbox에 저장
+     * 모든 상품 이벤트가 성공하거나 모두 실패하도록 보장
+     * 새로운 구조: 각 상품별로 주문 컨텍스트 정보를 포함한 개별 이벤트 생성
+     */
+    private void savePaymentSuccessEventsAtomically(OrderEntity order, List<OrderItemEntity> orderItems) {
+        final List<OutboxEventEntity> outboxEvents = new ArrayList<>();
+        final long baseTimestamp = System.currentTimeMillis();
+
+        // 각 상품별로 개별 이벤트 생성
+        for (int i = 0; i < orderItems.size(); i++) {
+            final OrderItemEntity orderItem = orderItems.get(i);
+
+            // 상품별 개별 페이로드 생성 (주문 컨텍스트 정보 포함)
+            final PaymentSuccessPayloadV1 payload = new PaymentSuccessPayloadV1(
+                    order.getId(),              // 주문 내부 ID (PK)
+                    order.getOrderNumber(),     // 주문 번호 (비즈니스 키)
+                    order.getUserId(),          // 주문한 사용자 ID
+                    orderItem.getProductId(),   // 상품 ID (개별 이벤트)
+                    orderItem.getQuantity(),    // 구매 수량 (개별 이벤트)
+                    orderItem.getUnitPrice(),   // 단가
+                    orderItem.getTotalPrice()   // 해당 상품의 총 금액
+            );
+
+            // 각 상품별로 고유한 이벤트 ID와 타임스탬프 생성 (순서 보장)
+            final var envelope = envelopeFactory.create(
+                    EventTypes.PAYMENT_SUCCESS,
+                    EventVersions.V1,
+                    payload
+            );
+
+            // 복합 파티션 키 사용: "orderId-productId" (동일 주문의 동일 상품은 동일 파티션)
+            final String partitionKey = String.format("%d-%d", order.getId(), orderItem.getProductId());
+
+            final OutboxEventEntity outboxEvent = OutboxEventEntity.ready(
+                    envelope.eventId(),
+                    ORDER_EVENTS_TOPIC,
+                    partitionKey, // 주문ID-상품ID 복합 키로 파티션 분산
+                    envelope.eventType(),
+                    envelope.version(),
+                    baseTimestamp + i, // 순서 보장을 위한 타임스탬프 조정
+                    envelope.payloadJson()
+            );
+
+            outboxEvents.add(outboxEvent);
+
+            log.debug(
+                    "상품별 결제 완료 이벤트 생성 - orderId={}, orderNumber={}, productId={}, quantity={}, unitPrice={}, totalPrice={}, eventId={}",
+                    order.getId(), order.getOrderNumber(), orderItem.getProductId(),
+                    orderItem.getQuantity(), orderItem.getUnitPrice(), orderItem.getTotalPrice(), envelope.eventId());
+        }
+
+        // 모든 이벤트를 일괄 저장 (원자성 보장)
+        outboxRepository.saveAll(outboxEvents);
+
+        log.info("상품별 결제 완료 이벤트 일괄 저장 완료 - orderId={}, orderNumber={}, eventCount={}",
+                order.getId(), order.getOrderNumber(), outboxEvents.size());
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
index 40bc019e3..4f7a5c0ec 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
@@ -11,6 +11,7 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import com.loopers.application.order.OrderFacadeDtos.CardOrderCreateCommand;
 import com.loopers.application.payment.PaymentCommand;
 import com.loopers.application.payment.PaymentFacade;
 import com.loopers.application.payment.PaymentInfo;
@@ -21,6 +22,7 @@
 import com.loopers.domain.order.OrderService;
 import com.loopers.domain.order.OrderStatus;
 import com.loopers.domain.order.dto.OrderCreationResult;
+import com.loopers.domain.payment.PaymentType;
 import com.loopers.domain.point.PointService;
 import com.loopers.domain.product.ProductEntity;
 import com.loopers.domain.product.ProductService;
@@ -30,6 +32,8 @@
 
 import lombok.RequiredArgsConstructor;
 
+import jakarta.validation.Valid;
+
 /**
  * 주문 Facade
  * <p>
@@ -63,7 +67,7 @@ public class OrderFacade {
      * @throws IllegalArgumentException 재고 부족 또는 주문 불가능한 경우
      */
     @Transactional
-    public OrderFacadeDtos.OrderInfo createOrderByPoint(@jakarta.validation.Valid OrderFacadeDtos.OrderCreateCommand command) {
+    public OrderFacadeDtos.OrderInfo createOrderByPoint(@Valid OrderFacadeDtos.OrderCreateCommand command) {
         // 1. 주문자 정보 조회 (락 적용)
         UserEntity user = userService.findByUsernameWithLock(command.username());
 
@@ -138,11 +142,11 @@ public OrderFacadeDtos.OrderInfo createOrderByPoint(@jakarta.validation.Valid Or
      */
     @Transactional
     public OrderFacadeDtos.OrderInfo createOrderByPoint(
-            @jakarta.validation.Valid OrderFacadeDtos.PointOrderCreateCommand command) {
+            @Valid OrderFacadeDtos.PointOrderCreateCommand command) {
         OrderFacadeDtos.OrderCreateCommand legacy = OrderFacadeDtos.OrderCreateCommand.builder()
                 .username(command.username())
                 .orderItems(command.orderItems())
-                .paymentType(com.loopers.domain.payment.PaymentType.POINT)
+                .paymentType(PaymentType.POINT)
                 .build();
         return createOrderByPoint(legacy);
     }
@@ -159,7 +163,7 @@ public OrderFacadeDtos.OrderInfo createOrderByPoint(
      */
     @Transactional
     public OrderFacadeDtos.OrderInfo createOrderForCardPayment(
-            @jakarta.validation.Valid OrderFacadeDtos.OrderCreateCommand command) {
+            @Valid OrderFacadeDtos.OrderCreateCommand command) {
         // 1. 주문자 정보 조회 (락 적용)
         UserEntity user = userService.findByUsernameWithLock(command.username());
 
@@ -238,7 +242,7 @@ public OrderFacadeDtos.OrderInfo createOrderForCardPayment(
      */
     @Transactional
     public OrderFacadeDtos.OrderWithPaymentInfo createOrderWithCardPayment(
-            @jakarta.validation.Valid OrderFacadeDtos.OrderCreateCommand command) {
+            @Valid OrderFacadeDtos.OrderCreateCommand command) {
         // 1. 주문 생성 (재고 차감, 쿠폰 사용, 포인트 차감 안 함)
         OrderFacadeDtos.OrderInfo orderInfo = createOrderForCardPayment(command);
 
@@ -265,7 +269,7 @@ public OrderFacadeDtos.OrderWithPaymentInfo createOrderWithCardPayment(
      */
     @Transactional
     public OrderFacadeDtos.OrderWithPaymentInfo createOrderWithCardPayment(
-            @jakarta.validation.Valid OrderFacadeDtos.CardOrderCreateCommand command) {
+            @Valid CardOrderCreateCommand command) {
         OrderFacadeDtos.OrderCreateCommand.CardPaymentInfo legacyCard = new OrderFacadeDtos.OrderCreateCommand.CardPaymentInfo(
                 command.cardInfo().cardType(),
                 command.cardInfo().cardNo(),
@@ -274,7 +278,7 @@ public OrderFacadeDtos.OrderWithPaymentInfo createOrderWithCardPayment(
         OrderFacadeDtos.OrderCreateCommand legacy = OrderFacadeDtos.OrderCreateCommand.builder()
                 .username(command.username())
                 .orderItems(command.orderItems())
-                .paymentType(com.loopers.domain.payment.PaymentType.CARD)
+                .paymentType(PaymentType.CARD)
                 .cardInfo(legacyCard)
                 .build();
         return createOrderWithCardPayment(legacy);
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacadeDtos.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacadeDtos.java
index 84314c377..4eeddd328 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacadeDtos.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacadeDtos.java
@@ -167,7 +167,6 @@ public static OrderSummary from(OrderEntity order, int itemCount) {
      * @since 2025. 11. 14.
      */
     @Builder
-    @Deprecated
     public record OrderCreateCommand(
             @NotBlank
             String username,
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
index 33757aca6..50c3ad916 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
@@ -7,6 +7,7 @@
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
+import com.loopers.cache.CacheStrategy;
 import com.loopers.domain.brand.BrandEntity;
 import com.loopers.domain.brand.BrandService;
 import com.loopers.domain.like.LikeService;
@@ -14,7 +15,6 @@
 import com.loopers.domain.product.dto.ProductSearchFilter;
 import com.loopers.domain.tracking.UserBehaviorTracker;
 import com.loopers.domain.user.UserService;
-import com.loopers.infrastructure.cache.CacheStrategy;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -66,34 +66,38 @@ public Page<ProductInfo> getProducts(ProductSearchFilter productSearchFilter) {
      */
     @Transactional(readOnly = true)
     public ProductDetailInfo getProductDetail(Long productId, String username) {
-        // 1. 캐시 조회
+        // 1. 사용자 정보 및 좋아요 상태 조회
+        Long userId = null;
+        Boolean isLiked = false;
+
+        if (username != null) {
+            userId = userService.getUserByUsername(username).getId();
+            isLiked = likeService.findLike(userId, productId)
+                    .map(like -> like.getDeletedAt() == null)
+                    .orElse(false);
+        }
+
+        // 2. 캐시 조회
         Optional<ProductDetailInfo> cachedDetail = productCacheService.getProductDetailFromCache(productId);
 
-        Boolean isLiked = username != null
-                ? likeService.findLike(userService.getUserByUsername(username).getId(), productId)
-                .map(like -> like.getDeletedAt() == null)
-                .orElse(false)
-                : false;
+        ProductDetailInfo result;
 
-        // 캐시 히트 시 사용자 좋아요 상태 동기화 후 반환
         if (cachedDetail.isPresent()) {
             log.debug("상품 상세 캐시 히트 - productId: {}", productId);
-            return ProductDetailInfo.fromWithSyncLike(cachedDetail.get(), isLiked);
-        }
+            result = ProductDetailInfo.fromWithSyncLike(cachedDetail.get(), isLiked);
+        } else {
+            log.debug("상품 상세 캐시 미스 - productId: {}", productId);
 
-        log.debug("상품 상세 캐시 미스 - productId: {}", productId);
+            // 3. MV 엔티티 조회
+            ProductMaterializedViewEntity productMaterializedViewEntity = mvService.getById(productId);
+            result = ProductDetailInfo.from(productMaterializedViewEntity, isLiked);
 
-        // 2. MV 엔티티 조회
-        ProductMaterializedViewEntity productMaterializedViewEntity = mvService.getById(productId);
-
-        ProductDetailInfo productDetail = ProductDetailInfo.from(productMaterializedViewEntity, isLiked);
-
-        // 3. 캐시 저장
-        productCacheService.cacheProductDetail(productId, productDetail);
+            // 4. 캐시 저장
+            productCacheService.cacheProductDetail(productId, result);
+        }
 
-        // 4. 유저 행동 추적 (상품 조회)
-        if (username != null) {
-            Long userId = userService.getUserByUsername(username).getId();
+        // 5. 유저 행동 추적 (상품 조회) - 캐시 히트/미스와 관계없이 항상 이벤트 발행
+        if (userId != null) {
             behaviorTracker.trackProductView(
                     userId,
                     productId,
@@ -101,8 +105,7 @@ public ProductDetailInfo getProductDetail(Long productId, String username) {
             );
         }
 
-        // 5. 사용자 좋아요 상태 적용
-        return productDetail;
+        return result;
     }
 
     /**
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponEventHandler.java
index c2cf2c3c0..dc4402b24 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponEventHandler.java
@@ -8,7 +8,6 @@
 
 import java.util.Objects;
 
-import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
@@ -41,12 +40,12 @@ public void handleCouponConsume(CouponConsumeEvent event) {
         Objects.requireNonNull(event, "쿠폰 이벤트가 null 입니다.");
 
         CouponEntity coupon = couponService.getCouponByIdAndUserId(event.couponId(), event.userId());
-        log.debug("쿠폰 사용 처리 시작 - orderId={}, userId={}, couponId={}",
-                event.orderId(), coupon.getUserId(), coupon.getId());
+        log.debug("쿠폰 사용 처리 시작 - orderNumber={}, userId={}, couponId={}",
+                event.orderNumber(), coupon.getUserId(), coupon.getId());
 
         couponService.consumeCoupon(coupon);
 
-        log.debug("쿠폰 사용 처리 완료 - orderId={}",
-                event.orderId());
+        log.debug("쿠폰 사용 처리 완료 - orderNumber={}",
+                event.orderNumber());
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/event/CouponConsumeEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/event/CouponConsumeEvent.java
index 7941d9d1e..7411d3805 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/event/CouponConsumeEvent.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/event/CouponConsumeEvent.java
@@ -7,6 +7,6 @@
 public record CouponConsumeEvent(
         Long couponId,
         Long userId,
-        Long orderId
+        Long orderNumber
 ) {
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/EventTypes.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/EventTypes.java
new file mode 100644
index 000000000..6590b5280
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/EventTypes.java
@@ -0,0 +1,16 @@
+package com.loopers.domain.event;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public final class EventTypes {
+    private EventTypes() {
+    }
+
+    public static final String PAYMENT_SUCCESS = "PAYMENT_SUCCESS";
+    public static final String PRODUCT_VIEW = "PRODUCT_VIEW";
+    public static final String LIKE_ACTION = "LIKE_ACTION";
+    public static final String ORDER_CREATE = "ORDER_CREATE";
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/EventVersions.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/EventVersions.java
new file mode 100644
index 000000000..828bd9503
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/EventVersions.java
@@ -0,0 +1,13 @@
+package com.loopers.domain.event;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public final class EventVersions {
+    private EventVersions() {
+    }
+
+    public static final String V1 = "v1";
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxEventEntity.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxEventEntity.java
new file mode 100644
index 000000000..657358cf3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxEventEntity.java
@@ -0,0 +1,133 @@
+package com.loopers.domain.event.outbox;
+
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+import lombok.Getter;
+
+import jakarta.persistence.*;
+
+@Entity
+@Getter
+@Table(name = "outbox_event")
+public class OutboxEventEntity {
+
+    @Id
+    @Column(name = "event_id", length = 36, nullable = false)
+    private String eventId;
+
+    @Column(name = "topic", nullable = false, length = 200)
+    private String topic;
+
+    @Column(name = "message_key", nullable = false, length = 200)
+    private String messageKey;
+
+    @Column(name = "event_type", nullable = false, length = 100)
+    private String eventType;
+
+    @Column(name = "version", nullable = false, length = 20)
+    private String version;
+
+    @Column(name = "occurred_at_epoch_millis", nullable = false)
+    private long occurredAtEpochMillis;
+
+    @Lob
+    @Column(name = "payload_json", nullable = false)
+    private String payloadJson;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "status", nullable = false, length = 20)
+    private OutboxStatus status;
+
+    @Column(name = "retry_count", nullable = false)
+    private int retryCount;
+
+    @Column(name = "created_at", nullable = false)
+    private ZonedDateTime createdAt;
+
+    @Column(name = "published_at")
+    private ZonedDateTime publishedAt;
+
+    protected OutboxEventEntity() {
+    }
+
+    private OutboxEventEntity(
+            final String eventId,
+            final String topic,
+            final String messageKey,
+            final String eventType,
+            final String version,
+            final long occurredAtEpochMillis,
+            final String payloadJson
+    ) {
+        // 유효성 검증
+        Objects.requireNonNull(eventId, "이벤트 ID는 필수입니다.");
+        if (eventId.trim().isEmpty() || eventId.length() != 36) {
+            throw new IllegalArgumentException("이벤트 ID는 36자리 UUID 형식이어야 합니다.");
+        }
+
+        Objects.requireNonNull(topic, "토픽은 필수입니다.");
+        if (topic.trim().isEmpty() || topic.length() > 200) {
+            throw new IllegalArgumentException("토픽은 필수이며 200자를 초과할 수 없습니다.");
+        }
+
+        Objects.requireNonNull(messageKey, "메시지 키는 필수입니다.");
+        if (messageKey.trim().isEmpty() || messageKey.length() > 200) {
+            throw new IllegalArgumentException("메시지 키는 필수이며 200자를 초과할 수 없습니다.");
+        }
+
+        Objects.requireNonNull(eventType, "이벤트 타입은 필수입니다.");
+        if (eventType.trim().isEmpty() || eventType.length() > 100) {
+            throw new IllegalArgumentException("이벤트 타입은 필수이며 100자를 초과할 수 없습니다.");
+        }
+
+        Objects.requireNonNull(version, "버전은 필수입니다.");
+        if (version.trim().isEmpty() || version.length() > 20) {
+            throw new IllegalArgumentException("버전은 필수이며 20자를 초과할 수 없습니다.");
+        }
+
+        if (occurredAtEpochMillis <= 0) {
+            throw new IllegalArgumentException("발생 시간은 0보다 커야 합니다.");
+        }
+
+        Objects.requireNonNull(payloadJson, "페이로드 JSON은 필수입니다.");
+        if (payloadJson.trim().isEmpty()) {
+            throw new IllegalArgumentException("페이로드 JSON은 필수입니다.");
+        }
+
+        this.eventId = eventId;
+        this.topic = topic;
+        this.messageKey = messageKey;
+        this.eventType = eventType;
+        this.version = version;
+        this.occurredAtEpochMillis = occurredAtEpochMillis;
+        this.payloadJson = payloadJson;
+
+        this.status = OutboxStatus.READY;
+        this.retryCount = 0;
+        this.createdAt = ZonedDateTime.now();
+    }
+
+    public static OutboxEventEntity ready(
+            final String eventId,
+            final String topic,
+            final String messageKey,
+            final String eventType,
+            final String version,
+            final long occurredAtEpochMillis,
+            final String payloadJson
+    ) {
+        return new OutboxEventEntity(eventId, topic, messageKey, eventType, version, occurredAtEpochMillis, payloadJson);
+    }
+
+    public void markSent() {
+        this.status = OutboxStatus.SENT;
+        this.publishedAt = ZonedDateTime.now();
+    }
+
+    public void markFailed() {
+        this.status = OutboxStatus.FAILED;
+        this.retryCount += 1;
+    }
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRelayScheduler.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRelayScheduler.java
new file mode 100644
index 000000000..cbbf339ff
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRelayScheduler.java
@@ -0,0 +1,107 @@
+package com.loopers.domain.event.outbox;
+
+import java.util.List;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Outbox 이벤트 배치 처리 스케줄러
+ * <p>
+ * 순수 Outbox 패턴:
+ * - 모든 이벤트는 Outbox에 READY 상태로 저장
+ * - 이 스케줄러가 배치 단위로 Kafka 발송 담당
+ * - 실패한 이벤트는 재시도 처리
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class OutboxRelayScheduler {
+
+    private final OutboxService outboxService;
+    private static final int MAX_RETRY_COUNT = 5;
+    private static final int BULK_COUNT = 1000; // 배치 처리 단위
+
+    /**
+     * READY 상태 이벤트 배치 처리 (1초마다)
+     * 순수 Outbox 패턴 - 모든 이벤트를 스케줄러가 처리
+     */
+    @Scheduled(fixedDelay = 1000)
+    public void processReadyEvents() {
+        final List<OutboxEventEntity> readyEvents =
+                outboxService.findTopNByStatusOrderByCreatedAtAsc(OutboxStatus.READY, BULK_COUNT);
+
+        if (readyEvents.isEmpty()) {
+            return;
+        }
+
+        log.info("READY 이벤트 배치 처리 시작 - 처리할 이벤트 수: {} (배치크기: {})",
+                readyEvents.size(), BULK_COUNT);
+
+        int successCount = 0;
+        int failCount = 0;
+
+        for (OutboxEventEntity event : readyEvents) {
+            boolean isSuccess = outboxService.processEvent(event);
+            if (isSuccess) {
+                successCount++;
+                log.debug("이벤트 발송 성공 - eventId: {}, type: {}",
+                        event.getEventId(), event.getEventType());
+            } else {
+                failCount++;
+            }
+        }
+
+        log.info("READY 이벤트 배치 처리 완료 - 성공: {}, 실패: {} (배치크기: {})",
+                successCount, failCount, BULK_COUNT);
+    }
+
+    /**
+     * 실패한 이벤트 재시도 (FAILED 상태)
+     * 30초마다 실행 - 실패한 이벤트만 재시도
+     */
+    @Scheduled(fixedDelay = 30000)
+    public void retryFailedEvents() {
+        final List<OutboxEventEntity> failedEvents =
+                outboxService.findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+                        OutboxStatus.FAILED, MAX_RETRY_COUNT, BULK_COUNT);
+
+        if (failedEvents.isEmpty()) {
+            return; // 로그 없이 조용히 리턴
+        }
+
+        log.info("실패 이벤트 재시도 시작 - 처리할 이벤트 수: {} (배치크기: {})",
+                failedEvents.size(), BULK_COUNT);
+
+        int retrySuccessCount = 0;
+        int retryFailCount = 0;
+        int maxRetryReachedCount = 0;
+
+        for (OutboxEventEntity event : failedEvents) {
+            if (event.getRetryCount() >= MAX_RETRY_COUNT) {
+                maxRetryReachedCount++;
+                log.warn("최대 재시도 횟수 초과 - eventId: {}, retryCount: {}",
+                        event.getEventId(), event.getRetryCount());
+                continue;
+            }
+
+            boolean isSuccess = outboxService.processEvent(event);
+            if (isSuccess) {
+                retrySuccessCount++;
+                log.info("재시도 성공 - eventId: {}, retryCount: {}",
+                        event.getEventId(), event.getRetryCount());
+            } else {
+                retryFailCount++;
+            }
+        }
+
+        log.info("실패 이벤트 재시도 완료 - 성공: {}, 실패: {}, 최대재시도초과: {} (배치크기: {})",
+                retrySuccessCount, retryFailCount, maxRetryReachedCount, BULK_COUNT);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRepository.java
new file mode 100644
index 000000000..d82062bc3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxRepository.java
@@ -0,0 +1,43 @@
+package com.loopers.domain.event.outbox;
+
+import java.util.List;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+public interface OutboxRepository {
+    /**
+     * READY 상태 이벤트 조회
+     */
+    List<OutboxEventEntity> findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus outboxStatus);
+
+    /**
+     * 지정된 개수만큼 상태별 이벤트 조회 (배치 크기 조절 가능)
+     */
+    List<OutboxEventEntity> findTopNByStatusOrderByCreatedAtAsc(OutboxStatus status, int limit);
+
+    /**
+     * 재시도 가능한 실패 이벤트 조회
+     *
+     * @param status        상태 (FAILED)
+     * @param maxRetryCount 최대 재시도 횟수
+     * @return 재시도 가능한 이벤트 목록
+     */
+    List<OutboxEventEntity> findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            OutboxStatus status, int maxRetryCount);
+
+    /**
+     * 지정된 개수만큼 재시도 가능한 실패 이벤트 조회 (배치 크기 조절 가능)
+     */
+    List<OutboxEventEntity> findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            OutboxStatus status, int maxRetryCount, int limit);
+
+    OutboxEventEntity save(OutboxEventEntity ready);
+
+    /**
+     * 여러 이벤트를 일괄 저장 (원자성 보장)
+     */
+    List<OutboxEventEntity> saveAll(List<OutboxEventEntity> events);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxService.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxService.java
new file mode 100644
index 000000000..8cc58fae6
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxService.java
@@ -0,0 +1,89 @@
+package com.loopers.domain.event.outbox;
+
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.loopers.infrastructure.event.DomainEventEnvelope;
+import com.loopers.infrastructure.event.DomainEventPublisher;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class OutboxService {
+    private final OutboxRepository outboxRepository;
+    private final DomainEventPublisher domainEventPublisher;
+
+    /**
+     * 개별 이벤트 처리 (독립적인 트랜잭션)
+     *
+     * @param event 처리할 이벤트
+     * @return 성공 여부
+     */
+    @Transactional
+    public boolean processEvent(OutboxEventEntity event) {
+        try {
+            final DomainEventEnvelope envelope = new DomainEventEnvelope(
+                    event.getEventId(),
+                    event.getEventType(),
+                    event.getVersion(),
+                    event.getOccurredAtEpochMillis(),
+                    event.getPayloadJson()
+            );
+
+            // Kafka 발송
+            domainEventPublisher.publish(event.getTopic(), event.getMessageKey(), envelope);
+
+            // 성공 시 상태 업데이트
+            event.markSent();
+            outboxRepository.save(event);
+
+            log.debug("이벤트 발송 성공 - eventId: {}, topic: {}", event.getEventId(), event.getTopic());
+            return true;
+
+        } catch (Exception ex) {
+            // 실패 시 상태 업데이트
+            event.markFailed();
+            outboxRepository.save(event);
+
+            log.warn("이벤트 발송 실패 - eventId: {}, topic: {}, retryCount: {}, error: {}",
+                    event.getEventId(), event.getTopic(), event.getRetryCount(), ex.getMessage());
+            return false;
+        }
+    }
+
+    public List<OutboxEventEntity> findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus status) {
+        return outboxRepository.findTop100ByStatusOrderByCreatedAtAsc(status);
+    }
+
+    /**
+     * 지정된 개수만큼 READY 상태 이벤트 조회 (배치 크기 조절 가능)
+     */
+    public List<OutboxEventEntity> findTopNByStatusOrderByCreatedAtAsc(OutboxStatus status, int limit) {
+        return outboxRepository.findTopNByStatusOrderByCreatedAtAsc(status, limit);
+    }
+
+    public List<OutboxEventEntity> findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(OutboxStatus outboxStatus,
+                                                                                             int maxRetryCount) {
+        return outboxRepository.findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+                outboxStatus, maxRetryCount);
+    }
+
+    /**
+     * 지정된 개수만큼 재시도 가능한 실패 이벤트 조회 (배치 크기 조절 가능)
+     */
+    public List<OutboxEventEntity> findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            OutboxStatus status, int maxRetryCount, int limit) {
+        return outboxRepository.findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+                status, maxRetryCount, limit);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxStatus.java
new file mode 100644
index 000000000..22a1283fc
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/event/outbox/OutboxStatus.java
@@ -0,0 +1,7 @@
+package com.loopers.domain.event.outbox;
+
+public enum OutboxStatus {
+    READY,
+    SENT,
+    FAILED
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentEntity.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentEntity.java
index 6db4cd627..d0643c17a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentEntity.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentEntity.java
@@ -178,7 +178,6 @@ public void completeWithEvent() {
     }
 
 
-
     /**
      * 결제 실패 처리 (도메인 이벤트 발행)
      */
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentCompletedEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentCompletedEvent.java
index 368ef80c4..e29f5aad8 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentCompletedEvent.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/event/PaymentCompletedEvent.java
@@ -8,7 +8,7 @@
  */
 public record PaymentCompletedEvent(
         String transactionKey,
-        Long orderId,
+        Long orderNumber,
         Long userId,
         BigDecimal amount,
         String cardType
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCacheService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCacheService.java
index a388f91ef..e12a4f347 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCacheService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCacheService.java
@@ -15,9 +15,9 @@
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.loopers.application.product.ProductDetailInfo;
+import com.loopers.cache.CacheKeyGenerator;
+import com.loopers.cache.CacheStrategy;
 import com.loopers.domain.product.dto.ProductSearchFilter;
-import com.loopers.infrastructure.cache.CacheKeyGenerator;
-import com.loopers.infrastructure.cache.CacheStrategy;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -61,29 +61,6 @@ public void cacheProductDetail(Long productId, ProductDetailInfo productDetail)
     }
 
 
-    public void batchCacheProductDetails(java.util.List<ProductDetailInfo> productDetails) {
-        if (productDetails == null || productDetails.isEmpty()) {
-            log.debug("배치 캐시 저장 대상 없음");
-            return;
-        }
-
-        int successCount = 0;
-        int failCount = 0;
-
-        for (ProductDetailInfo productDetail : productDetails) {
-            try {
-                cacheProductDetail(productDetail.id(), productDetail);
-                successCount++;
-            } catch (Exception e) {
-                failCount++;
-                log.warn("배치 캐시 저장 실패 - productId: {}", productDetail.id());
-            }
-        }
-
-        log.info("배치 캐시 완료 - 성공: {}, 실패: {}", successCount, failCount);
-    }
-
-
     public Optional<ProductDetailInfo> getProductDetailFromCache(Long productId) {
         try {
             String key = cacheKeyGenerator.generateProductDetailKey(productId);
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMVService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMVService.java
index eb498a116..b669db185 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMVService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMVService.java
@@ -12,8 +12,8 @@
 import org.springframework.transaction.annotation.Transactional;
 
 import com.loopers.application.product.BatchUpdateResult;
+import com.loopers.cache.CacheStrategy;
 import com.loopers.domain.product.dto.ProductSearchFilter;
-import com.loopers.infrastructure.cache.CacheStrategy;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMaterializedViewEntity.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMaterializedViewEntity.java
index 25945ec5a..87f93aa7a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMaterializedViewEntity.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductMaterializedViewEntity.java
@@ -359,10 +359,6 @@ public static boolean hasActualChangesFromDto(ProductMaterializedViewEntity mv,
 
         // 좋아요 수 변경
         Long dtoLikeCount = dto.getLikeCount() != null ? dto.getLikeCount() : 0L;
-        if (!mv.getLikeCount().equals(dtoLikeCount)) {
-            return true;
-        }
-
-        return false;
+        return !mv.getLikeCount().equals(dtoLikeCount);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/tracking/UserBehaviorTracker.java b/apps/commerce-api/src/main/java/com/loopers/domain/tracking/UserBehaviorTracker.java
index 5695ba993..f4d08333e 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/tracking/UserBehaviorTracker.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/tracking/UserBehaviorTracker.java
@@ -98,10 +98,10 @@ public void trackOrderCreate(
             );
 
             eventPublisher.publishEvent(event);
-            log.debug("주문 생성 추적 - userId: {}, orderId: {}", userId, orderId);
+            log.debug("주문 생성 추적 - userId: {}, orderNumber: {}", userId, orderId);
 
         } catch (Exception e) {
-            log.warn("주문 생성 추적 실패 - userId: {}, orderId: {}, error: {}",
+            log.warn("주문 생성 추적 실패 - userId: {}, orderNumber: {}, error: {}",
                     userId, orderId, e.getMessage());
         }
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheUpdateStrategy.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheUpdateStrategy.java
deleted file mode 100644
index e49142a67..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheUpdateStrategy.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.loopers.infrastructure.cache;
-
-/**
- * 캐시 갱신 전략
- */
-public enum CacheUpdateStrategy {
-
-    /**
-     * 배치 갱신 (주기적으로 미리 갱신)
-     * - 적용: Hot 데이터 (상품 목록, 인기 상품 등)
-     */
-    BATCH_REFRESH,
-
-    /**
-     * Cache-Aside (조회 시 캐시 미스 발생 시 DB에서 로드)
-     * - 적용: Warm 데이터 (개별 상품 상세정보 등)
-     */
-    CACHE_ASIDE,
-
-    /**
-     * 캐시 미사용 (항상 DB 직접 조회)
-     * - 적용: Cold 데이터 (조회 빈도 낮은 데이터)
-     */
-    NO_CACHE
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClient.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClient.java
index 3f32364de..3b27cf64f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClient.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClient.java
@@ -31,8 +31,8 @@ public class DataPlatformClient {
     public boolean sendOrderData(OrderDataDto orderData) {
         try {
             // Fake API 호출 시뮬레이션
-            log.info("[DATA PLATFORM] 주문 데이터 전송 시작 - orderId: {}, eventType: {}",
-                    orderData.orderId(), orderData.eventType());
+            log.info("[DATA PLATFORM] 주문 데이터 전송 시작 - orderNumber: {}, eventType: {}",
+                    orderData.orderNumber(), orderData.eventType());
 
             // 실제로는 HTTP 요청을 보냄
             // restTemplate.postForObject("https://data-platform.api/orders", orderData, String.class);
@@ -47,7 +47,7 @@ public boolean sendOrderData(OrderDataDto orderData) {
             }
 
         } catch (Exception e) {
-            log.error(" [DATA PLATFORM] 주문 데이터 전송 실패 - orderId: {}, error: {}",
+            log.error(" [DATA PLATFORM] 주문 데이터 전송 실패 - orderNumber: {}, error: {}",
                     orderData.orderId(), e.getMessage());
             return false;
         }
@@ -70,7 +70,7 @@ public boolean sendPaymentData(PaymentDataDto paymentData) {
 
             // 성공 시뮬레이션 (90% 성공률)
             if (Math.random() < 0.9) {
-                log.info("[DATA PLATFORM] 결제 데이터 전송 성공 - transactionKey: {}, orderId: {}",
+                log.info("[DATA PLATFORM] 결제 데이터 전송 성공 - transactionKey: {}, orderNumber: {}",
                         paymentData.transactionKey(), paymentData.orderId());
                 return true;
             } else {
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformEventHandler.java
index a9cd98da3..d39eb1dbb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformEventHandler.java
@@ -43,30 +43,30 @@ public class DataPlatformEventHandler {
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
     public void handleOrderConfirmed(OrderConfirmedEvent event) {
         try {
-            log.debug("주문 확정 데이터 플랫폼 전송 시작 - orderId: {}", event.orderId());
+            log.debug("주문 확정 데이터 플랫폼 전송 시작 - orderNumber: {}", event.orderNumber());
 
             OrderDataDto orderData = new OrderDataDto(
-                event.orderId(),
-                event.orderNumber(),
-                event.userId(),
-                OrderStatus.CONFIRMED,
-                event.originalTotalAmount(),
-                event.discountAmount(),
-                event.finalTotalAmount(),
-                ZonedDateTime.now(),
-                "ORDER_CONFIRMED"
+                    event.orderId(),
+                    event.orderNumber(),
+                    event.userId(),
+                    OrderStatus.CONFIRMED,
+                    event.originalTotalAmount(),
+                    event.discountAmount(),
+                    event.finalTotalAmount(),
+                    ZonedDateTime.now(),
+                    "ORDER_CONFIRMED"
             );
 
             boolean success = dataPlatformClient.sendOrderData(orderData);
 
             if (success) {
-                log.info("주문 확정 데이터 플랫폼 전송 성공 - orderId: {}", event.orderId());
+                log.info("주문 확정 데이터 플랫폼 전송 성공 - orderNumber: {}", event.orderNumber());
             } else {
-                log.warn("주문 확정 데이터 플랫폼 전송 실패 - orderId: {}", event.orderId());
+                log.warn("주문 확정 데이터 플랫폼 전송 실패 - orderNumber: {}", event.orderNumber());
             }
 
         } catch (Exception e) {
-            log.error("주문 확정 데이터 플랫폼 전송 중 예외 발생 - orderId: {}", event.orderId(), e);
+            log.error("주문 확정 데이터 플랫폼 전송 중 예외 발생 - orderNumber: {}", event.orderNumber(), e);
         }
     }
 
@@ -77,30 +77,30 @@ public void handleOrderConfirmed(OrderConfirmedEvent event) {
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
     public void handleOrderCancelled(OrderCancelledEvent event) {
         try {
-            log.debug("주문 취소 데이터 플랫폼 전송 시작 - orderId: {}", event.orderId());
+            log.debug("주문 취소 데이터 플랫폼 전송 시작 - orderNumber: {}", event.orderId());
 
             OrderDataDto orderData = new OrderDataDto(
-                event.orderId(),
-                event.orderNumber(),
-                event.userId(),
-                OrderStatus.CANCELLED,
-                event.originalTotalAmount(),
-                event.discountAmount(),
-                event.finalTotalAmount(),
-                ZonedDateTime.now(),
-                "ORDER_CANCELLED"
+                    event.orderId(),
+                    event.orderNumber(),
+                    event.userId(),
+                    OrderStatus.CANCELLED,
+                    event.originalTotalAmount(),
+                    event.discountAmount(),
+                    event.finalTotalAmount(),
+                    ZonedDateTime.now(),
+                    "ORDER_CANCELLED"
             );
 
             boolean success = dataPlatformClient.sendOrderData(orderData);
 
             if (success) {
-                log.info("주문 취소 데이터 플랫폼 전송 성공 - orderId: {}", event.orderId());
+                log.info("주문 취소 데이터 플랫폼 전송 성공 - orderNumber: {}", event.orderId());
             } else {
-                log.warn("주문 취소 데이터 플랫폼 전송 실패 - orderId: {}", event.orderId());
+                log.warn("주문 취소 데이터 플랫폼 전송 실패 - orderNumber: {}", event.orderId());
             }
 
         } catch (Exception e) {
-            log.error("주문 취소 데이터 플랫폼 전송 중 예외 발생 - orderId: {}", event.orderId(), e);
+            log.error("주문 취소 데이터 플랫폼 전송 중 예외 발생 - orderNumber: {}", event.orderId(), e);
         }
     }
 
@@ -114,15 +114,15 @@ public void handlePaymentCompleted(PaymentCompletedEvent event) {
             log.debug("결제 완료 데이터 플랫폼 전송 시작 - transactionKey: {}", event.transactionKey());
 
             PaymentDataDto paymentData = new PaymentDataDto(
-                event.transactionKey(),
-                event.orderId(),
-                event.userId(),
-                PaymentStatus.COMPLETED,
-                event.amount(),
-                event.cardType(),
-                ZonedDateTime.now(),
-                "PAYMENT_COMPLETED",
-                null
+                    event.transactionKey(),
+                    event.orderNumber(),
+                    event.userId(),
+                    PaymentStatus.COMPLETED,
+                    event.amount(),
+                    event.cardType(),
+                    ZonedDateTime.now(),
+                    "PAYMENT_COMPLETED",
+                    null
             );
 
             boolean success = dataPlatformClient.sendPaymentData(paymentData);
@@ -148,15 +148,15 @@ public void handlePaymentFailed(PaymentFailedEvent event) {
             log.debug("결제 실패 데이터 플랫폼 전송 시작 - transactionKey: {}", event.transactionKey());
 
             PaymentDataDto paymentData = new PaymentDataDto(
-                event.transactionKey(),
-                event.orderId(),
-                event.userId(),
-                PaymentStatus.FAILED,
-                event.amount(),
-                event.cardType(),
-                ZonedDateTime.now(),
-                "PAYMENT_FAILED",
-                event.reason()
+                    event.transactionKey(),
+                    event.orderId(),
+                    event.userId(),
+                    PaymentStatus.FAILED,
+                    event.amount(),
+                    event.cardType(),
+                    ZonedDateTime.now(),
+                    "PAYMENT_FAILED",
+                    event.reason()
             );
 
             boolean success = dataPlatformClient.sendPaymentData(paymentData);
@@ -182,15 +182,15 @@ public void handlePaymentTimeout(PaymentTimeoutEvent event) {
             log.debug("결제 타임아웃 데이터 플랫폼 전송 시작 - transactionKey: {}", event.transactionKey());
 
             PaymentDataDto paymentData = new PaymentDataDto(
-                event.transactionKey(),
-                event.orderId(),
-                event.userId(),
-                PaymentStatus.TIMEOUT,
-                event.amount(),
-                event.cardType(),
-                ZonedDateTime.now(),
-                "PAYMENT_TIMEOUT",
-                "결제 콜백 타임아웃 (10분 초과)"
+                    event.transactionKey(),
+                    event.orderId(),
+                    event.userId(),
+                    PaymentStatus.TIMEOUT,
+                    event.amount(),
+                    event.cardType(),
+                    ZonedDateTime.now(),
+                    "PAYMENT_TIMEOUT",
+                    "결제 콜백 타임아웃 (10분 초과)"
             );
 
             boolean success = dataPlatformClient.sendPaymentData(paymentData);
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java
new file mode 100644
index 000000000..19114903f
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java
@@ -0,0 +1,15 @@
+package com.loopers.infrastructure.event;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+public record DomainEventEnvelope(
+        String eventId,
+        String eventType,
+        String version,
+        long occurredAtEpochMillis,
+        String payloadJson
+) {
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelopeFactory.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelopeFactory.java
new file mode 100644
index 000000000..7b3a22591
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelopeFactory.java
@@ -0,0 +1,39 @@
+package com.loopers.infrastructure.event;
+
+import java.time.Instant;
+import java.util.UUID;
+
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+@Component
+@RequiredArgsConstructor
+public class DomainEventEnvelopeFactory {
+    private final ObjectMapper objectMapper;
+
+    public DomainEventEnvelope create(final String eventType, final String version, final Object payload) {
+        final String payloadJson;
+        try {
+            payloadJson = objectMapper.writeValueAsString(payload);
+        } catch (Exception e) {
+            throw new IllegalStateException("이벤트 payload 직렬화에 실패했습니다.", e);
+        }
+
+        return new DomainEventEnvelope(
+                UUID.randomUUID().toString(),
+                eventType,
+                version,
+                Instant.now().toEpochMilli(),
+                payloadJson
+        );
+    }
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventPublisher.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventPublisher.java
new file mode 100644
index 000000000..f80c6d9c3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/DomainEventPublisher.java
@@ -0,0 +1,27 @@
+package com.loopers.infrastructure.event;
+
+import java.util.Objects;
+
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+
+@Component
+@RequiredArgsConstructor
+public class DomainEventPublisher {
+    private final KafkaTemplate<Object, Object> kafkaTemplate;
+
+    public void publish(final String topic, final String key, final DomainEventEnvelope envelope) {
+        Objects.requireNonNull(topic, "topic은 필수입니다.");
+        Objects.requireNonNull(key, "key는 필수입니다.");
+        Objects.requireNonNull(envelope, "envelope은 필수입니다.");
+        kafkaTemplate.send(topic, key, envelope);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventJpaRepository.java
new file mode 100644
index 000000000..a30668db6
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventJpaRepository.java
@@ -0,0 +1,41 @@
+package com.loopers.infrastructure.event.outbox;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import com.loopers.domain.event.outbox.OutboxEventEntity;
+import com.loopers.domain.event.outbox.OutboxStatus;
+
+public interface OutboxEventJpaRepository extends JpaRepository<OutboxEventEntity, String> {
+
+    List<OutboxEventEntity> findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus status);
+
+    /**
+     * 지정된 개수만큼 상태별 이벤트 조회 (배치 크기 조절 가능)
+     */
+    @Query(value = "SELECT * FROM outbox_event WHERE status = :status ORDER BY created_at ASC LIMIT :limit",
+            nativeQuery = true)
+    List<OutboxEventEntity> findTopNByStatusOrderByCreatedAtAsc(
+            @Param("status") String status, @Param("limit") int limit);
+
+    /**
+     * 재시도 가능한 실패 이벤트 조회
+     *
+     * @param status        상태 (FAILED)
+     * @param maxRetryCount 최대 재시도 횟수
+     * @return 재시도 가능한 이벤트 목록 (최대 50개)
+     */
+    List<OutboxEventEntity> findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            OutboxStatus status, int maxRetryCount);
+
+    /**
+     * 지정된 개수만큼 재시도 가능한 실패 이벤트 조회 (배치 크기 조절 가능)
+     */
+    @Query(value = "SELECT * FROM outbox_event WHERE status = :status AND retry_count < :maxRetryCount ORDER BY created_at ASC LIMIT :limit",
+            nativeQuery = true)
+    List<OutboxEventEntity> findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            @Param("status") String status, @Param("maxRetryCount") int maxRetryCount, @Param("limit") int limit);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventRepositoryImpl.java
new file mode 100644
index 000000000..6b87384c5
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/outbox/OutboxEventRepositoryImpl.java
@@ -0,0 +1,57 @@
+package com.loopers.infrastructure.event.outbox;
+
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+
+import com.loopers.domain.event.outbox.OutboxEventEntity;
+import com.loopers.domain.event.outbox.OutboxRepository;
+import com.loopers.domain.event.outbox.OutboxStatus;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+
+@Component
+@RequiredArgsConstructor
+public class OutboxEventRepositoryImpl implements OutboxRepository {
+    private final OutboxEventJpaRepository outboxEventJpaRepository;
+
+    @Override
+    public List<OutboxEventEntity> findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus outboxStatus) {
+        return outboxEventJpaRepository.findTop100ByStatusOrderByCreatedAtAsc(outboxStatus);
+    }
+
+    @Override
+    public List<OutboxEventEntity> findTopNByStatusOrderByCreatedAtAsc(OutboxStatus status, int limit) {
+        return outboxEventJpaRepository.findTopNByStatusOrderByCreatedAtAsc(status.name(), limit);
+    }
+
+    @Override
+    public List<OutboxEventEntity> findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+            OutboxStatus status, int maxRetryCount) {
+        return outboxEventJpaRepository.findTop50ByStatusAndRetryCountLessThanOrderByCreatedAtAsc(status, maxRetryCount);
+    }
+
+    @Override
+    public List<OutboxEventEntity> findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(OutboxStatus status,
+                                                                                            int maxRetryCount,
+                                                                                            int limit) {
+        return outboxEventJpaRepository.findTopNByStatusAndRetryCountLessThanOrderByCreatedAtAsc(
+                status.name(), maxRetryCount, limit);
+    }
+
+    @Override
+    public OutboxEventEntity save(OutboxEventEntity ready) {
+        return outboxEventJpaRepository.save(ready);
+    }
+
+    @Override
+    public List<OutboxEventEntity> saveAll(List<OutboxEventEntity> events) {
+        return outboxEventJpaRepository.saveAll(events);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java
new file mode 100644
index 000000000..4599c3dba
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java
@@ -0,0 +1,14 @@
+package com.loopers.infrastructure.event.payloads;
+
+/**
+ * 좋아요 액션 이벤트 페이로드 V1
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record LikeActionPayloadV1(
+        Long productId,
+        Long userId,
+        String action  // "LIKE" or "UNLIKE"
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java
new file mode 100644
index 000000000..216c12548
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java
@@ -0,0 +1,21 @@
+package com.loopers.infrastructure.event.payloads;
+
+import java.math.BigDecimal;
+
+/**
+ * 결제 완료 이벤트 페이로드 V1
+ * 상품별 개별 이벤트로 발행되며 주문 컨텍스트 정보를 포함합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 17.
+ */
+public record PaymentSuccessPayloadV1(
+        Long orderId,           // 주문 내부 ID (PK)
+        Long orderNumber,       // 주문 번호 (비즈니스 키)
+        Long userId,            // 주문한 사용자 ID
+        Long productId,         // 상품 ID (개별 이벤트)
+        Integer quantity,       // 구매 수량 (개별 이벤트)
+        BigDecimal unitPrice,   // 단가
+        BigDecimal totalPrice   // 해당 상품의 총 금액
+) {
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java
new file mode 100644
index 000000000..fba41ebe0
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java
@@ -0,0 +1,13 @@
+package com.loopers.infrastructure.event.payloads;
+
+/**
+ * 상품 조회 이벤트 페이로드 V1
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record ProductViewPayloadV1(
+        Long productId,
+        Long userId
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeEventHandler.java
index 8b2fde232..104f3dcdf 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeEventHandler.java
@@ -5,8 +5,14 @@
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
 
+import com.loopers.domain.event.EventTypes;
+import com.loopers.domain.event.EventVersions;
+import com.loopers.domain.event.outbox.OutboxEventEntity;
+import com.loopers.domain.event.outbox.OutboxRepository;
 import com.loopers.domain.like.event.LikeChangedEvent;
 import com.loopers.domain.product.ProductMVService;
+import com.loopers.infrastructure.event.DomainEventEnvelopeFactory;
+import com.loopers.infrastructure.event.payloads.LikeActionPayloadV1;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -24,25 +30,29 @@
 @RequiredArgsConstructor
 @Slf4j
 public class LikeEventHandler {
+    private static final String CATALOG_EVENTS_TOPIC = "catalog-events";
 
     private final ProductMVService productMVService;
+    private final DomainEventEnvelopeFactory envelopeFactory;
+    private final OutboxRepository outboxRepository;
 
     /**
      * 좋아요 변경 이벤트 처리
      * <p>
      * AFTER_COMMIT + @Async로 좋아요 트랜잭션과 완전 분리
-     * 집계 업데이트 실패가 좋아요 처리에 영향 주지 않음
+     * 1. MV 테이블 집계 업데이트
+     * 2. Kafka 이벤트용 Outbox 저장
      *
      * @param event 좋아요 변경 이벤트
      */
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
     @Async
     public void handleLikeChanged(LikeChangedEvent event) {
+        // 1. MV 테이블 집계 업데이트
         try {
             log.debug("좋아요 집계 업데이트 시작 - productId: {}, action: {}, delta: {}",
                     event.productId(), event.action(), event.countDelta());
 
-            // MV 테이블의 좋아요 카운트 업데이트
             productMVService.updateLikeCount(event.productId(), event.countDelta());
 
             log.debug("좋아요 집계 업데이트 완료 - productId: {}, delta: {}",
@@ -53,5 +63,48 @@ public void handleLikeChanged(LikeChangedEvent event) {
             log.error("좋아요 집계 업데이트 실패 - productId: {}, action: {}, delta: {}",
                     event.productId(), event.action(), event.countDelta(), e);
         }
+
+        // 2. Kafka 이벤트용 Outbox 저장
+        try {
+            saveToOutbox(event);
+        } catch (Exception e) {
+            log.error("좋아요 이벤트 Outbox 저장 실패 - productId: {}, action: {}",
+                    event.productId(), event.action(), e);
+        }
+    }
+
+    /**
+     * 좋아요 이벤트를 Outbox에 저장
+     */
+    private void saveToOutbox(LikeChangedEvent event) {
+        final Long productId = event.productId();
+        final Long userId = event.userId();
+        final String action = event.action().name(); // LIKE or UNLIKE
+
+        if (productId == null || userId == null || action == null) {
+            log.warn("Outbox 저장 스킵 - 필수값 누락 productId={}, userId={}, action={}",
+                    productId, userId, action);
+            return;
+        }
+
+        final LikeActionPayloadV1 payload = new LikeActionPayloadV1(productId, userId, action);
+        final var envelope = envelopeFactory.create(EventTypes.LIKE_ACTION, EventVersions.V1, payload);
+        final String partitionKey = String.valueOf(productId);
+
+        // Outbox에 저장 (READY 상태)
+        OutboxEventEntity outboxEvent = OutboxEventEntity.ready(
+                envelope.eventId(),
+                CATALOG_EVENTS_TOPIC,
+                partitionKey,
+                envelope.eventType(),
+                envelope.version(),
+                envelope.occurredAtEpochMillis(),
+                envelope.payloadJson()
+        );
+
+        outboxRepository.save(outboxEvent);
+
+        log.info("좋아요 이벤트 Outbox 저장 완료 - type={}, productId={}, userId={}, action={}",
+                EventTypes.LIKE_ACTION, productId, userId, action);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductMVBatchScheduler.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductMVBatchScheduler.java
index 0473c2d7b..cb9c9b147 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductMVBatchScheduler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductMVBatchScheduler.java
@@ -11,13 +11,13 @@
 import org.springframework.stereotype.Component;
 
 import com.loopers.application.product.BatchUpdateResult;
+import com.loopers.cache.CacheStrategy;
 import com.loopers.domain.brand.BrandEntity;
 import com.loopers.domain.brand.BrandService;
 import com.loopers.domain.product.ProductCacheService;
 import com.loopers.domain.product.ProductMVService;
 import com.loopers.domain.product.ProductMaterializedViewEntity;
 import com.loopers.domain.product.dto.ProductSearchFilter;
-import com.loopers.infrastructure.cache.CacheStrategy;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -85,12 +85,12 @@ public void syncMaterializedView() {
     }
 
     /**
-     * Hot 캐시 갱신 배치 작업 (50분마다)
+     * Hot 캐시 갱신 배치 작업 (20분마다)
      * <p>
      * 배치 갱신으로 캐시 스탬피드 방지
      * ProductMVRepository를 직접 사용하여 likeCount 정렬 보장
      */
-    @Scheduled(fixedRate = 50 * 60 * 1000, initialDelay = 60 * 1000)
+    @Scheduled(fixedRate = 20 * 60 * 1000, initialDelay = 60 * 1000)
     public void refreshHotCache() {
         log.info("Hot 캐시 갱신 시작");
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/UserBehaviorEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/UserBehaviorEventHandler.java
index 47397b97e..ddf140067 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/UserBehaviorEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/UserBehaviorEventHandler.java
@@ -5,7 +5,13 @@
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
 
+import com.loopers.domain.event.EventTypes;
+import com.loopers.domain.event.EventVersions;
+import com.loopers.domain.event.outbox.OutboxEventEntity;
+import com.loopers.domain.event.outbox.OutboxRepository;
 import com.loopers.domain.tracking.event.UserBehaviorEvent;
+import com.loopers.infrastructure.event.DomainEventEnvelopeFactory;
+import com.loopers.infrastructure.event.payloads.ProductViewPayloadV1;
 import com.loopers.infrastructure.tracking.client.AnalyticsClient;
 
 import lombok.RequiredArgsConstructor;
@@ -24,18 +30,23 @@
 @Slf4j
 @RequiredArgsConstructor
 public class UserBehaviorEventHandler {
+    private static final String CATALOG_EVENTS_TOPIC = "catalog-events";
 
     private final AnalyticsClient analyticsClient;
+    private final DomainEventEnvelopeFactory envelopeFactory;
+    private final OutboxRepository outboxRepository;
 
     /**
      * 유저 행동 이벤트 처리
      * <p>
      * AFTER_COMMIT + @Async로 완전한 트랜잭션 분리
-     * 분석 시스템 전송 실패가 비즈니스 로직에 영향 주지 않음
+     * 1. 분석 시스템으로 데이터 전송
+     * 2. PRODUCT_VIEW인 경우 Kafka 이벤트용 Outbox 저장
      */
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
     public void handleUserBehavior(UserBehaviorEvent event) {
+        // 1. 분석 시스템으로 데이터 전송
         try {
             log.debug("유저 행동 분석 데이터 전송 시작 - eventType: {}, userId: {}, targetId: {}",
                     event.eventType(), event.userId(), event.targetId());
@@ -58,5 +69,48 @@ public void handleUserBehavior(UserBehaviorEvent event) {
 
             // TODO: 실패한 이벤트를 재처리 큐에 넣거나 로컬 저장소에 백업
         }
+
+        // 2. PRODUCT_VIEW인 경우 Kafka 이벤트용 Outbox 저장
+        if ("PRODUCT_VIEW".equals(event.eventType())) {
+            try {
+                saveProductViewToOutbox(event);
+            } catch (Exception e) {
+                log.error("상품 조회 이벤트 Outbox 저장 실패 - userId: {}, targetId: {}",
+                        event.userId(), event.targetId(), e);
+            }
+        }
+    }
+
+    /**
+     * 상품 조회 이벤트를 Outbox에 저장
+     */
+    private void saveProductViewToOutbox(UserBehaviorEvent event) {
+        final Long productId = event.targetId();
+        final Long userId = event.userId();
+
+        if (productId == null || userId == null) {
+            log.warn("Outbox 저장 스킵 - 필수값 누락 productId={}, userId={}", productId, userId);
+            return;
+        }
+
+        final ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, userId);
+        final var envelope = envelopeFactory.create(EventTypes.PRODUCT_VIEW, EventVersions.V1, payload);
+        final String partitionKey = String.valueOf(productId);
+
+        // Outbox에 저장 (READY 상태)
+        OutboxEventEntity outboxEvent = OutboxEventEntity.ready(
+                envelope.eventId(),
+                CATALOG_EVENTS_TOPIC,
+                partitionKey,
+                envelope.eventType(),
+                envelope.version(),
+                envelope.occurredAtEpochMillis(),
+                envelope.payloadJson()
+        );
+
+        outboxRepository.save(outboxEvent);
+
+        log.info("상품 조회 이벤트 Outbox 저장 완료 - type={}, productId={}, userId={}",
+                EventTypes.PRODUCT_VIEW, productId, userId);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/client/AnalyticsClient.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/client/AnalyticsClient.java
index 50ccc4a04..4adc80187 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/client/AnalyticsClient.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/tracking/client/AnalyticsClient.java
@@ -48,7 +48,7 @@ public boolean sendBehaviorData(UserBehaviorEvent event) {
                             event.targetId(), event.userId());
                     case "LIKE_ACTION" -> log.info("[ANALYTICS] 좋아요 액션 추적 완료 - productId: {}, userId: {}, action: {}",
                             event.targetId(), event.userId(), props.get("action"));
-                    case "ORDER_CREATE" -> log.info("[ANALYTICS] 주문 생성 추적 완료 - orderId: {}, userId: {}, amount: {}",
+                    case "ORDER_CREATE" -> log.info("[ANALYTICS] 주문 생성 추적 완료 - orderNumber: {}, userId: {}, amount: {}",
                             event.targetId(), event.userId(), props.get("totalAmount"));
                     default -> log.info("[ANALYTICS] 유저 행동 추적 완료 - eventType: {}, userId: {}",
                             event.eventType(), event.userId());
diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml
index a49d7c762..8f3c1fbdf 100644
--- a/apps/commerce-api/src/main/resources/application.yml
+++ b/apps/commerce-api/src/main/resources/application.yml
@@ -21,8 +21,8 @@ spring:
     import:
       - jpa.yml
       - redis.yml
+      - kafka.yml
       - logging.yml
-      - monitoring.yml
   cloud:
     openfeign:
       client:
@@ -31,6 +31,9 @@ spring:
             connect-timeout: 500    # 연결 타임아웃 500ms
             read-timeout: 500       # 읽기 타임아웃 500ms
             logger-level: full
+management:
+  server:
+    port: 8099
 
 pg:
   simulator:
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductCacheStrategyIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductCacheStrategyIntegrationTest.java
index d95a74823..5520d0191 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductCacheStrategyIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductCacheStrategyIntegrationTest.java
@@ -14,13 +14,13 @@
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 
+import com.loopers.cache.CacheStrategy;
 import com.loopers.domain.brand.BrandRepository;
 import com.loopers.domain.product.ProductCacheService;
 import com.loopers.domain.product.ProductMVService;
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.product.dto.ProductSearchFilter;
 import com.loopers.fixtures.ProductTestFixture;
-import com.loopers.infrastructure.cache.CacheStrategy;
 import com.loopers.utils.DatabaseCleanUp;
 import com.loopers.utils.RedisCleanUp;
 
@@ -89,7 +89,7 @@ void should_cache_first_page_as_hot_and_second_page_as_warm() {
             // When: 두 번째 페이지 조회
             Pageable page1 = PageRequest.of(1, 20);
             ProductSearchFilter filter1 = new ProductSearchFilter(null, null, page1);
-            Page<ProductInfo> result1 = productFacade.getProducts(filter1);
+            productFacade.getProducts(filter1);
 
             // Then: 캐시에 저장됨 (WARM 전략)
             Optional<List<Long>> cachedIds1 = cacheService.getProductIdsFromCache(
@@ -110,7 +110,7 @@ void should_cache_brand_first_2_pages_as_hot_data() {
             // When: 브랜드별 첫 번째 페이지 조회
             Pageable page0 = PageRequest.of(0, 20);
             ProductSearchFilter filter0 = new ProductSearchFilter(brandId, null, page0);
-            Page<ProductInfo> result0 = productFacade.getProducts(filter0);
+            productFacade.getProducts(filter0);
 
             // Then: 캐시에 저장됨 (Hot 전략)
             Optional<List<Long>> cachedIds0 = cacheService.getProductIdsFromCache(
@@ -121,7 +121,7 @@ void should_cache_brand_first_2_pages_as_hot_data() {
             // When: 브랜드별 두 번째 페이지 조회
             Pageable page1 = PageRequest.of(1, 20);
             ProductSearchFilter filter1 = new ProductSearchFilter(brandId, null, page1);
-            Page<ProductInfo> result1 = productFacade.getProducts(filter1);
+            productFacade.getProducts(filter1);
 
             // Then: 캐시에 저장됨 (Hot 전략)
             Optional<List<Long>> cachedIds1 = cacheService.getProductIdsFromCache(
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductIntegrationTest.java
index 016385d9b..eccbad169 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductIntegrationTest.java
@@ -120,16 +120,16 @@ void get_product_pagination() {
             // given
             // given
             BrandEntity brand = BrandTestFixture.createAndSave(brandRepository, "Test Brand", "Test Description");
-           for(int i=0; i<10; i++) {
-               ProductTestFixture.createAndSave(
-                       productRepository,
-                       brand,
-                       "Test Product " + i,
-                       "Product Description " + i,
-                       new BigDecimal("10000"),
-                       100
-               );
-           }
+            for (int i = 0; i < 10; i++) {
+                ProductTestFixture.createAndSave(
+                        productRepository,
+                        brand,
+                        "Test Product " + i,
+                        "Product Description " + i,
+                        new BigDecimal("10000"),
+                        100
+                );
+            }
 
             productMVService.syncMaterializedView();
 
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java b/apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
index ea4b4d15a..4ef7cfc68 100644
--- a/apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
+++ b/apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java
@@ -1,11 +1,12 @@
 package com.loopers;
 
-import jakarta.annotation.PostConstruct;
+import java.util.TimeZone;
+
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
 
-import java.util.TimeZone;
+import jakarta.annotation.PostConstruct;
 
 @ConfigurationPropertiesScan
 @SpringBootApplication
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventEntity.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventEntity.java
new file mode 100644
index 000000000..f780d0255
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventEntity.java
@@ -0,0 +1,38 @@
+package com.loopers.domain.event;
+
+import java.time.ZonedDateTime;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+@Entity
+@Table(name = "event_handled")
+@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
+@Getter
+public class EventEntity {
+    @Id
+    @Column(name = "event_id", length = 36, nullable = false)
+    private String eventId;
+
+    @Column(name = "handled_at", nullable = false)
+    private ZonedDateTime handledAt;
+
+    private EventEntity(final String eventId, final ZonedDateTime handledAt) {
+        this.eventId = eventId;
+        this.handledAt = handledAt;
+    }
+
+    public static EventEntity create(final String eventId) {
+        return new EventEntity(eventId, ZonedDateTime.now());
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventRepository.java
new file mode 100644
index 000000000..d6236a04a
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/event/EventRepository.java
@@ -0,0 +1,13 @@
+package com.loopers.domain.event;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public interface EventRepository {
+    EventEntity save(EventEntity eventEntity);
+
+
+    boolean existsById(String eventId);
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/MetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/MetricsService.java
new file mode 100644
index 000000000..0171318e5
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/MetricsService.java
@@ -0,0 +1,216 @@
+package com.loopers.domain.metrics;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.springframework.stereotype.Component;
+
+import com.loopers.domain.event.EventRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * ConcurrentHashMap 기반 동시성 안전한 메트릭 서비스
+ * <p>
+ * 상품별 메모리 락을 사용하여 동일한 상품에 대한 동시 업데이트를 제어합니다.
+ * Redis 분산락 대신 메모리 기반 락을 사용하여 성능을 대폭 향상시킵니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class MetricsService {
+    private final EventRepository eventHandledRepository;
+    private final ProductMetricsService metricsTransactionService;
+
+    // 상품별 메모리 락 관리
+    private final ConcurrentHashMap<Long, ReentrantLock> productLocks = new ConcurrentHashMap<>();
+
+    // 락 획득 설정 (빠른 처리를 위해 짧게 설정)
+    private static final long LOCK_WAIT_TIME_MS = 100; // 100ms 대기
+    private static final int LOCK_CLEANUP_THRESHOLD = 10000; // 락 정리 임계값
+
+    // 메모리 기반 멱등성 체크 (성능 최적화)
+    private final ConcurrentHashMap<String, Boolean> processedEvents = new ConcurrentHashMap<>();
+    private static final int PROCESSED_EVENTS_CLEANUP_THRESHOLD = 50000; // 처리된 이벤트 정리 임계값
+
+    /**
+     * 멱등성 체크 - 메모리 기반으로 성능 최적화
+     * 예외 기반이 아닌 조회 기반으로 중복 체크를 수행하여 성능을 향상시킵니다.
+     */
+    public boolean tryMarkHandled(String eventId) {
+        // 1. 메모리 캐시 먼저 확인 (빠른 경로)
+        if (processedEvents.containsKey(eventId)) {
+            log.debug("이미 처리된 이벤트 (메모리 캐시): {}", eventId);
+            return false;
+        }
+
+        // 2. DB에서 확인 (느린 경로)
+        if (eventHandledRepository.existsById(eventId)) {
+            // DB에 있으면 메모리 캐시에도 추가
+            processedEvents.put(eventId, true);
+            log.debug("이미 처리된 이벤트 (DB 확인): {}", eventId);
+            return false;
+        }
+
+        // 3. 새로운 이벤트 - 트랜잭션 서비스를 통해 안전하게 저장
+        boolean saved = metricsTransactionService.saveEventHandled(eventId);
+        if (saved) {
+            processedEvents.put(eventId, true);
+            return true;
+        } else {
+            // 동시성으로 인해 다른 스레드가 먼저 저장한 경우
+            processedEvents.put(eventId, true);
+            log.debug("동시성으로 인한 중복 이벤트: {}", eventId);
+            return false;
+        }
+    }
+
+    /**
+     * 조회수 증가 (메모리 락 적용)
+     */
+    public void incrementView(Long productId, long occurredAtEpochMillis) {
+        ReentrantLock lock = getProductLock(productId);
+
+        try {
+            if (lock.tryLock(LOCK_WAIT_TIME_MS, TimeUnit.MILLISECONDS)) {
+                try {
+                    metricsTransactionService.incrementViewWithTransaction(productId, occurredAtEpochMillis);
+                    log.debug("조회수 업데이트 성공: productId={}", productId);
+                } finally {
+                    lock.unlock();
+                }
+            } else {
+                log.warn("조회수 업데이트 스킵 - 락 획득 실패: productId={}", productId);
+                // 락 획득 실패 시 이벤트 스킵 (성능 우선)
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("조회수 업데이트 중단 - 스레드 인터럽트: productId={}", productId);
+        }
+    }
+
+
+    /**
+     * 좋아요 수 변경 (메모리 락 적용)
+     */
+    public void applyLikeDelta(final Long productId, final int delta, long occurredAtEpochMillis) {
+        ReentrantLock lock = getProductLock(productId);
+
+        try {
+            if (lock.tryLock(LOCK_WAIT_TIME_MS, TimeUnit.MILLISECONDS)) {
+                try {
+                    metricsTransactionService.applyLikeDeltaWithTransaction(productId, delta, occurredAtEpochMillis);
+                    log.debug("좋아요 수 업데이트 성공: productId={}, delta={}", productId, delta);
+                } finally {
+                    lock.unlock();
+                }
+            } else {
+                log.warn("좋아요 수 업데이트 스킵 - 락 획득 실패: productId={}, delta={}", productId, delta);
+                // 락 획득 실패 시 이벤트 스킵 (성능 우선)
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("좋아요 수 업데이트 중단 - 스레드 인터럽트: productId={}, delta={}", productId, delta);
+        }
+    }
+
+
+    /**
+     * 판매량 증가 (메모리 락 적용)
+     */
+    public void addSales(final Long productId, final int quantity, long occurredAtEpochMillis) {
+        ReentrantLock lock = getProductLock(productId);
+
+        try {
+            if (lock.tryLock(LOCK_WAIT_TIME_MS, TimeUnit.MILLISECONDS)) {
+                try {
+                    metricsTransactionService.addSalesWithTransaction(productId, quantity, occurredAtEpochMillis);
+                    log.debug("판매량 업데이트 성공: productId={}, quantity={}", productId, quantity);
+                } finally {
+                    lock.unlock();
+                }
+            } else {
+                log.warn("판매량 업데이트 스킵 - 락 획득 실패: productId={}, quantity={}", productId, quantity);
+
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("판매량 업데이트 중단 - 스레드 인터럽트: productId={}, quantity={}", productId, quantity);
+        }
+    }
+
+    /**
+     * 재고 소진 이벤트 처리 (캐시 갱신 중심)
+     */
+    public void handleStockDepleted(Long productId, Long brandId, Integer remainingStock, long occurredAtEpochMillis) {
+        // 재고 소진은 메트릭 업데이트보다는 캐시 갱신이 주 목적
+        // 락 없이 바로 트랜잭션 서비스 호출
+        metricsTransactionService.handleStockDepletedWithTransaction(productId, brandId, remainingStock, occurredAtEpochMillis);
+        log.info("재고 소진 이벤트 처리 완료: productId={}, brandId={}, remainingStock={}", productId, brandId, remainingStock);
+    }
+
+    /**
+     * 상품별 락 획득 (없으면 생성)
+     */
+    private ReentrantLock getProductLock(Long productId) {
+        return productLocks.computeIfAbsent(productId, k -> new ReentrantLock());
+    }
+
+    /**
+     * 락 상태 모니터링 및 정리 (메모리 누수 방지)
+     */
+    public void cleanupUnusedLocks() {
+        if (productLocks.size() > LOCK_CLEANUP_THRESHOLD) {
+            log.info("락 정리 시작 - 현재 락 수: {}", productLocks.size());
+
+            // 사용하지 않는 락 제거 (락이 걸려있지 않은 것들)
+            productLocks.entrySet().removeIf(entry -> {
+                ReentrantLock lock = entry.getValue();
+                return !lock.isLocked() && !lock.hasQueuedThreads();
+            });
+
+            log.info("락 정리 완료 - 정리 후 락 수: {}", productLocks.size());
+        }
+    }
+
+    /**
+     * 처리된 이벤트 캐시 정리 (메모리 누수 방지)
+     */
+    public void cleanupProcessedEvents() {
+        if (processedEvents.size() > PROCESSED_EVENTS_CLEANUP_THRESHOLD) {
+            log.info("처리된 이벤트 캐시 정리 시작 - 현재 캐시 수: {}", processedEvents.size());
+
+            // 오래된 이벤트 캐시 절반 정도 제거 (LRU 방식은 아니지만 메모리 절약)
+            int targetSize = PROCESSED_EVENTS_CLEANUP_THRESHOLD / 2;
+            int currentSize = processedEvents.size();
+            int toRemove = currentSize - targetSize;
+
+            processedEvents.entrySet().stream()
+                    .limit(toRemove)
+                    .map(Map.Entry::getKey)
+                    .forEach(processedEvents::remove);
+
+            log.info("처리된 이벤트 캐시 정리 완료 - 정리 후 캐시 수: {}", processedEvents.size());
+        }
+    }
+
+    /**
+     * 락 상태 정보 조회 (모니터링용)
+     */
+    public void logLockStatus() {
+        int totalLocks = productLocks.size();
+        long lockedCount = productLocks.values().stream()
+                .mapToLong(lock -> lock.isLocked() ? 1 : 0)
+                .sum();
+
+        if (totalLocks > 0) {
+            log.debug("메트릭 락 상태 - 총 락: {}, 사용 중: {}", totalLocks, lockedCount);
+        }
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java
new file mode 100644
index 000000000..a07933520
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsEntity.java
@@ -0,0 +1,81 @@
+package com.loopers.domain.metrics;
+
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+
+@Entity
+@Getter
+@Table(name = "product_metrics")
+@AllArgsConstructor
+@NoArgsConstructor
+public class ProductMetricsEntity {
+    @Id
+    @Column(name = "product_id", nullable = false)
+    private Long id;
+
+    @Column(name = "view_count", nullable = false)
+    private long viewCount = 0L;
+
+    @Column(name = "like_count", nullable = false)
+    private long likeCount = 0L;
+
+    @Column(name = "sales_count", nullable = false)
+    private long salesCount = 0L;
+
+    @Column(name = "order_count", nullable = false)
+    private long orderCount = 0L;
+
+    @Column(name = "last_event_at")
+    private ZonedDateTime lastEventAt;
+
+
+    private ProductMetricsEntity(final Long productId) {
+        Objects.requireNonNull(productId);
+        this.id = productId;
+    }
+
+    public static ProductMetricsEntity create(final Long productId) {
+        return new ProductMetricsEntity(productId);
+    }
+
+
+    public void incrementView(ZonedDateTime eventTime) {
+        this.viewCount += 1;
+        this.lastEventAt = eventTime;
+    }
+
+    public void applyLikeDelta(final int delta, ZonedDateTime eventTime) {
+        final long next = this.likeCount + delta;
+
+        // 좋아요 수는 0 미만으로 내려가지 않도록 보장
+        this.likeCount = Math.max(0, next);
+
+        this.lastEventAt = eventTime;
+    }
+
+
+    public void addSales(final int quantity, ZonedDateTime eventTime) {
+        if (quantity <= 0) {
+            return;
+        }
+        this.salesCount += quantity;
+        this.orderCount += 1; // 주문 건수도 함께 증가
+        this.lastEventAt = eventTime;
+    }
+
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
new file mode 100644
index 000000000..ae4bfb12d
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
@@ -0,0 +1,16 @@
+package com.loopers.domain.metrics;
+
+import java.util.Optional;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public interface ProductMetricsRepository {
+    ProductMetricsEntity save(ProductMetricsEntity metrics);
+
+    Optional<ProductMetricsEntity> findById(Long productId);
+
+    void deleteAll();
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
new file mode 100644
index 000000000..759c3fc43
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsService.java
@@ -0,0 +1,112 @@
+package com.loopers.domain.metrics;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.loopers.domain.event.EventRepository;
+import com.loopers.domain.metrics.repository.MetricsRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 메트릭 트랜잭션 처리 서비스
+ * <p>
+ * Spring AOP Self-Invocation 문제를 해결하기 위해 분리된 트랜잭션 서비스입니다.
+ * MetricsService에서 @Transactional 메서드를 호출할 때 발생하는 문제를 방지합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class ProductMetricsService {
+
+    private final MetricsRepository metricsRepository;
+    private final EventRepository eventHandledRepository;
+
+    /**
+     * 조회수 증가 (트랜잭션 적용)
+     */
+    @Transactional
+    public void incrementViewWithTransaction(Long productId, long occurredAtEpochMillis) {
+        try {
+            metricsRepository.incrementView(productId, occurredAtEpochMillis);
+            log.debug("조회수 증가 완료: productId={}", productId);
+        } catch (Exception e) {
+            log.error("조회수 증가 실패: productId={}", productId, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 좋아요 수 변경 (트랜잭션 적용)
+     */
+    @Transactional
+    public void applyLikeDeltaWithTransaction(Long productId, int delta, long occurredAtEpochMillis) {
+        try {
+            metricsRepository.applyLikeDelta(productId, delta, occurredAtEpochMillis);
+            log.debug("좋아요 수 변경 완료: productId={}, delta={}", productId, delta);
+        } catch (Exception e) {
+            log.error("좋아요 수 변경 실패: productId={}, delta={}", productId, delta, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 판매량 증가 (트랜잭션 적용)
+     */
+    @Transactional
+    public void addSalesWithTransaction(Long productId, int quantity, long occurredAtEpochMillis) {
+        try {
+            metricsRepository.addSales(productId, quantity, occurredAtEpochMillis);
+            log.debug("판매량 증가 완료: productId={}, quantity={}", productId, quantity);
+        } catch (Exception e) {
+            log.error("판매량 증가 실패: productId={}, quantity={}", productId, quantity, e);
+            throw e;
+        }
+
+    }
+
+
+    /**
+     * 재고 소진 이벤트 처리 (트랜잭션 적용)
+     * 주로 캐시 갱신을 담당합니다.
+     */
+    @Transactional
+    public void handleStockDepletedWithTransaction(Long productId, Long brandId, Integer remainingStock,
+                                                   long occurredAtEpochMillis) {
+        try {
+            // 재고 소진 시 캐시 갱신 처리
+            metricsRepository.handleStockDepleted(productId, brandId, remainingStock, occurredAtEpochMillis);
+            log.debug("재고 소진 처리 완료: productId={}, brandId={}, remainingStock={}", productId, brandId, remainingStock);
+        } catch (Exception e) {
+            log.error("재고 소진 처리 실패: productId={}, brandId={}, remainingStock={}", productId, brandId, remainingStock, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 이벤트 처리 완료 마킹 (트랜잭션 적용)
+     * 예외 기반이 아닌 조회 기반으로 중복 체크를 수행합니다.
+     */
+    @Transactional
+    public boolean saveEventHandled(String eventId) {
+        try {
+            // 트랜잭션 내에서 다시 한번 확인 (동시성 안전)
+            if (eventHandledRepository.existsById(eventId)) {
+                log.debug("트랜잭션 내 중복 확인: {}", eventId);
+                return false;
+            }
+
+            eventHandledRepository.save(com.loopers.domain.event.EventEntity.create(eventId));
+            log.debug("이벤트 처리 완료 저장: {}", eventId);
+            return true;
+        } catch (Exception e) {
+            // 동시성으로 인한 중복 저장 시도 (Unique 제약 조건 위반 등)
+            log.debug("동시성으로 인한 이벤트 저장 실패: {}", eventId, e);
+            return false;
+        }
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/repository/MetricsRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/repository/MetricsRepository.java
new file mode 100644
index 000000000..7837bddaf
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/repository/MetricsRepository.java
@@ -0,0 +1,32 @@
+package com.loopers.domain.metrics.repository;
+
+/**
+ * 메트릭 업데이트를 위한 Repository 인터페이스
+ * <p>
+ * 동시성 안전한 메트릭 업데이트 작업을 담당합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+public interface MetricsRepository {
+
+    /**
+     * 조회수 증가
+     */
+    void incrementView(Long productId, long occurredAtEpochMillis);
+
+    /**
+     * 좋아요 수 변경 (증가/감소)
+     */
+    void applyLikeDelta(Long productId, int delta, long occurredAtEpochMillis);
+
+    /**
+     * 판매량 증가
+     */
+    void addSales(Long productId, int quantity, long occurredAtEpochMillis);
+
+    /**
+     * 재고 소진 이벤트 처리 (캐시 갱신 중심)
+     */
+    void handleStockDepleted(Long productId, Long brandId, Integer remainingStock, long occurredAtEpochMillis);
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductCacheService.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductCacheService.java
new file mode 100644
index 000000000..8133f922c
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/cache/ProductCacheService.java
@@ -0,0 +1,61 @@
+package com.loopers.infrastructure.cache;
+
+import org.springframework.stereotype.Component;
+
+import com.loopers.cache.BaseCacheService;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 상품 캐시 관리 서비스 (Streamer용)
+ * <p>
+ * 메트릭 변화 시 관련 캐시를 무효화합니다.
+ * 공통 모듈의 BaseCacheService를 위임하여 사용합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ProductCacheService {
+
+    private final BaseCacheService baseCacheService;
+
+    /**
+     * 특정 상품의 캐시를 무효화
+     */
+    public void evictProductCache(Long productId) {
+        baseCacheService.evictProductCache(productId);
+    }
+
+    /**
+     * 상품 목록 관련 캐시들을 무효화
+     */
+    public void evictProductListCaches() {
+        baseCacheService.evictProductListCaches();
+    }
+
+    /**
+     * 판매량 변화 시 호출 - 인기 상품 순위가 변경될 수 있음
+     */
+    public void onSalesCountChanged(Long productId) {
+        baseCacheService.onSalesCountChanged(productId);
+    }
+
+
+    /**
+     * 브랜드별 상품 목록 캐시 무효화
+     */
+    public void evictBrandProductListCache(Long brandId) {
+        baseCacheService.evictBrandProductListCache(brandId);
+    }
+
+    /**
+     * 상품 상세 캐시의 재고 정보만 업데이트
+     */
+    public void updateProductStock(Long productId, Integer newStock) {
+        baseCacheService.updateProductStock(productId, newStock);
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java
new file mode 100644
index 000000000..e3fb05df1
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/DomainEventEnvelope.java
@@ -0,0 +1,16 @@
+package com.loopers.infrastructure.event;
+
+/**
+ * 도메인 이벤트 봉투 - commerce-api와 동일한 구조
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record DomainEventEnvelope(
+        String eventId,
+        String eventType,
+        String version,
+        long occurredAtEpochMillis,
+        String payloadJson
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventDeserializer.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventDeserializer.java
new file mode 100644
index 000000000..81ea5cd57
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventDeserializer.java
@@ -0,0 +1,77 @@
+package com.loopers.infrastructure.event;
+
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.loopers.infrastructure.event.payloads.LikeActionPayloadV1;
+import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1;
+import com.loopers.infrastructure.event.payloads.ProductViewPayloadV1;
+import com.loopers.infrastructure.event.payloads.StockDepletedPayloadV1;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 이벤트 역직렬화 유틸리티
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class EventDeserializer {
+
+    private final ObjectMapper objectMapper;
+
+    public DomainEventEnvelope deserializeEnvelope(Object kafkaValue) {
+        try {
+            if (kafkaValue instanceof String json) {
+                return objectMapper.readValue(json, DomainEventEnvelope.class);
+            }
+            // Map 형태로 온 경우 JSON으로 변환 후 역직렬화
+            String json = objectMapper.writeValueAsString(kafkaValue);
+            return objectMapper.readValue(json, DomainEventEnvelope.class);
+        } catch (JsonProcessingException e) {
+            log.error("Failed to deserialize event envelope: {}", kafkaValue, e);
+            return null;
+        }
+    }
+
+    public ProductViewPayloadV1 deserializeProductView(String payloadJson) {
+        try {
+            return objectMapper.readValue(payloadJson, ProductViewPayloadV1.class);
+        } catch (JsonProcessingException e) {
+            log.error("Failed to deserialize ProductViewPayloadV1: {}", payloadJson, e);
+            return null;
+        }
+    }
+
+    public LikeActionPayloadV1 deserializeLikeAction(String payloadJson) {
+        try {
+            return objectMapper.readValue(payloadJson, LikeActionPayloadV1.class);
+        } catch (JsonProcessingException e) {
+            log.error("Failed to deserialize LikeActionPayloadV1: {}", payloadJson, e);
+            return null;
+        }
+    }
+
+    public PaymentSuccessPayloadV1 deserializePaymentSuccess(String payloadJson) {
+        try {
+            return objectMapper.readValue(payloadJson, PaymentSuccessPayloadV1.class);
+        } catch (JsonProcessingException e) {
+            log.error("Failed to deserialize PaymentSuccessPayloadV1: {}", payloadJson, e);
+            return null;
+        }
+    }
+
+    public StockDepletedPayloadV1 deserializeStockDepleted(String payloadJson) {
+        try {
+            return objectMapper.readValue(payloadJson, StockDepletedPayloadV1.class);
+        } catch (JsonProcessingException e) {
+            log.error("Failed to deserialize StockDepletedPayloadV1: {}", payloadJson, e);
+            return null;
+        }
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventJpaRepository.java
new file mode 100644
index 000000000..072ae9aef
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventJpaRepository.java
@@ -0,0 +1,13 @@
+package com.loopers.infrastructure.event;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.loopers.domain.event.EventEntity;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public interface EventJpaRepository extends JpaRepository<EventEntity, String> {
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventRepositoryImpl.java
new file mode 100644
index 000000000..d3785269f
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/EventRepositoryImpl.java
@@ -0,0 +1,29 @@
+package com.loopers.infrastructure.event;
+
+import org.springframework.stereotype.Component;
+
+import com.loopers.domain.event.EventEntity;
+import com.loopers.domain.event.EventRepository;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+@Component
+@RequiredArgsConstructor
+public class EventRepositoryImpl implements EventRepository {
+    private final EventJpaRepository eventJpaRepository;
+
+    @Override
+    public EventEntity save(EventEntity eventEntity) {
+        return eventJpaRepository.save(eventEntity);
+    }
+
+    @Override
+    public boolean existsById(String eventId) {
+        return eventJpaRepository.existsById(eventId);
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java
new file mode 100644
index 000000000..4599c3dba
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/LikeActionPayloadV1.java
@@ -0,0 +1,14 @@
+package com.loopers.infrastructure.event.payloads;
+
+/**
+ * 좋아요 액션 이벤트 페이로드 V1
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record LikeActionPayloadV1(
+        Long productId,
+        Long userId,
+        String action  // "LIKE" or "UNLIKE"
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java
new file mode 100644
index 000000000..2e4b130f8
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/PaymentSuccessPayloadV1.java
@@ -0,0 +1,21 @@
+package com.loopers.infrastructure.event.payloads;
+
+import java.math.BigDecimal;
+
+/**
+ * 결제 완료 이벤트 페이로드 V1
+ * 상품별 개별 이벤트로 발행되며 주문 컨텍스트 정보를 포함합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record PaymentSuccessPayloadV1(
+        Long orderId,           // 주문 내부 ID (PK)
+        Long orderNumber,       // 주문 번호 (비즈니스 키)
+        Long userId,            // 주문한 사용자 ID
+        Long productId,         // 상품 ID (개별 이벤트)
+        Integer quantity,       // 구매 수량 (개별 이벤트)
+        BigDecimal unitPrice,   // 단가
+        BigDecimal totalPrice   // 해당 상품의 총 금액
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java
new file mode 100644
index 000000000..fba41ebe0
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/ProductViewPayloadV1.java
@@ -0,0 +1,13 @@
+package com.loopers.infrastructure.event.payloads;
+
+/**
+ * 상품 조회 이벤트 페이로드 V1
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+public record ProductViewPayloadV1(
+        Long productId,
+        Long userId
+) {
+}
\ No newline at end of file
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/StockDepletedPayloadV1.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/StockDepletedPayloadV1.java
new file mode 100644
index 000000000..b9338d10e
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/event/payloads/StockDepletedPayloadV1.java
@@ -0,0 +1,16 @@
+package com.loopers.infrastructure.event.payloads;
+
+/**
+ * 재고 소진 이벤트 페이로드 V1
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+public record StockDepletedPayloadV1(
+        Long productId,         // 재고 소진된 상품 ID
+        Long brandId,           // 브랜드 ID (브랜드별 캐시 무효화용)
+        String productName,     // 상품명 (로깅용)
+        Integer remainingStock, // 남은 재고 (0이어야 함)
+        Long warehouseId        // 창고 ID (선택적)
+) {
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/MetricsRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/MetricsRepositoryImpl.java
new file mode 100644
index 000000000..7296bc600
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/MetricsRepositoryImpl.java
@@ -0,0 +1,136 @@
+package com.loopers.infrastructure.metrics;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import org.springframework.stereotype.Repository;
+
+import com.loopers.domain.metrics.ProductMetricsEntity;
+import com.loopers.domain.metrics.ProductMetricsRepository;
+import com.loopers.domain.metrics.repository.MetricsRepository;
+import com.loopers.infrastructure.cache.ProductCacheService;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 메트릭 Repository 구현체
+ * <p>
+ * ProductMetricsRepository를 사용하여 실제 메트릭 업데이트를 수행합니다.
+ * 존재하지 않는 상품의 경우 새로 생성하여 처리합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Repository
+@RequiredArgsConstructor
+@Slf4j
+public class MetricsRepositoryImpl implements MetricsRepository {
+
+    private final ProductMetricsRepository productMetricsRepository;
+    private final ProductCacheService productCacheService;
+
+    @Override
+    public void incrementView(Long productId, long occurredAtEpochMillis) {
+        ZonedDateTime eventTime = convertToZonedDateTime(occurredAtEpochMillis);
+
+        Optional<ProductMetricsEntity> existingMetrics = productMetricsRepository.findById(productId);
+
+        long newViewCount;
+        if (existingMetrics.isPresent()) {
+            ProductMetricsEntity metrics = existingMetrics.get();
+            metrics.incrementView(eventTime);
+            productMetricsRepository.save(metrics);
+            newViewCount = metrics.getViewCount();
+        } else {
+            // 새로운 상품 메트릭 생성
+            ProductMetricsEntity newMetrics = ProductMetricsEntity.create(productId);
+            newMetrics.incrementView(eventTime);
+            productMetricsRepository.save(newMetrics);
+            newViewCount = newMetrics.getViewCount();
+        }
+
+
+        log.debug("조회수 증가 완료: productId={}, eventTime={}", productId, eventTime);
+    }
+
+    @Override
+    public void applyLikeDelta(Long productId, int delta, long occurredAtEpochMillis) {
+        ZonedDateTime eventTime = convertToZonedDateTime(occurredAtEpochMillis);
+
+        Optional<ProductMetricsEntity> existingMetrics = productMetricsRepository.findById(productId);
+
+        if (existingMetrics.isPresent()) {
+            ProductMetricsEntity metrics = existingMetrics.get();
+            metrics.applyLikeDelta(delta, eventTime);
+            productMetricsRepository.save(metrics);
+        } else {
+            // 새로운 상품 메트릭 생성 (좋아요가 음수가 되지 않도록 처리)
+            if (delta > 0) {
+                ProductMetricsEntity newMetrics = ProductMetricsEntity.create(productId);
+                newMetrics.applyLikeDelta(delta, eventTime);
+                productMetricsRepository.save(newMetrics);
+            } else {
+                log.debug("새로운 상품에 대한 좋아요 감소 무시: productId={}, delta={}", productId, delta);
+                return; // 캐시 무효화 불필요
+            }
+        }
+
+        // 좋아요는 캐시 무효화하지 않음 (실시간 반영 불필요)
+
+        log.debug("좋아요 수 변경 완료: productId={}, delta={}, eventTime={}", productId, delta, eventTime);
+    }
+
+    @Override
+    public void addSales(Long productId, int quantity, long occurredAtEpochMillis) {
+        if (quantity <= 0) {
+            log.debug("잘못된 판매량 무시: productId={}, quantity={}", productId, quantity);
+            return;
+        }
+
+        ZonedDateTime eventTime = convertToZonedDateTime(occurredAtEpochMillis);
+
+        Optional<ProductMetricsEntity> existingMetrics = productMetricsRepository.findById(productId);
+
+        if (existingMetrics.isPresent()) {
+            ProductMetricsEntity metrics = existingMetrics.get();
+            metrics.addSales(quantity, eventTime);
+            productMetricsRepository.save(metrics);
+        } else {
+            // 새로운 상품 메트릭 생성
+            ProductMetricsEntity newMetrics = ProductMetricsEntity.create(productId);
+            newMetrics.addSales(quantity, eventTime);
+            productMetricsRepository.save(newMetrics);
+        }
+
+        // 캐시 무효화 (판매량 변경 - 인기 상품 순위 영향)
+        productCacheService.onSalesCountChanged(productId);
+
+        log.debug("판매량 증가 완료: productId={}, quantity={}, eventTime={}", productId, quantity, eventTime);
+    }
+
+    @Override
+    public void handleStockDepleted(Long productId, Long brandId, Integer remainingStock, long occurredAtEpochMillis) {
+        // 재고 소진 이벤트 처리
+        // 메트릭 자체는 업데이트하지 않고 캐시만 처리
+
+        // 상품 상세 캐시의 재고 정보만 갱신 (빠른 응답을 위해)
+        int stockToUpdate = (remainingStock != null) ? remainingStock : 0;
+        productCacheService.updateProductStock(productId, stockToUpdate);
+
+        log.info("재고 소진 상세 캐시 갱신 완료: productId={}, brandId={}, remainingStock={}",
+                productId, brandId, stockToUpdate);
+    }
+
+    /**
+     * Epoch 밀리초를 ZonedDateTime으로 변환
+     */
+    private ZonedDateTime convertToZonedDateTime(long epochMillis) {
+        return ZonedDateTime.ofInstant(
+                Instant.ofEpochMilli(epochMillis),
+                ZoneId.systemDefault()
+        );
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
new file mode 100644
index 000000000..6615061fa
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
@@ -0,0 +1,13 @@
+package com.loopers.infrastructure.metrics;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.loopers.domain.metrics.ProductMetricsEntity;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+public interface ProductMetricsJpaRepository extends JpaRepository<ProductMetricsEntity, Long> {
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
new file mode 100644
index 000000000..4894e575e
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java
@@ -0,0 +1,36 @@
+package com.loopers.infrastructure.metrics;
+
+import java.util.Optional;
+
+import org.springframework.stereotype.Component;
+
+import com.loopers.domain.metrics.ProductMetricsEntity;
+import com.loopers.domain.metrics.ProductMetricsRepository;
+
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+@Component
+@RequiredArgsConstructor
+public class ProductMetricsRepositoryImpl implements ProductMetricsRepository {
+    private final ProductMetricsJpaRepository productMetricsJpaRepository;
+
+    @Override
+    public ProductMetricsEntity save(ProductMetricsEntity metrics) {
+        return productMetricsJpaRepository.save(metrics);
+    }
+
+    @Override
+    public Optional<ProductMetricsEntity> findById(Long productId) {
+        return productMetricsJpaRepository.findById(productId);
+    }
+
+    @Override
+    public void deleteAll() {
+        productMetricsJpaRepository.deleteAll();
+    }
+}
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/MetricsLockCleanupScheduler.java b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/MetricsLockCleanupScheduler.java
new file mode 100644
index 000000000..4fea5785b
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/infrastructure/scheduler/MetricsLockCleanupScheduler.java
@@ -0,0 +1,61 @@
+package com.loopers.infrastructure.scheduler;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import com.loopers.domain.metrics.MetricsService;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 메트릭 서비스의 메모리 락 정리 스케줄러
+ * <p>
+ * 주기적으로 사용하지 않는 락과 처리된 이벤트 캐시를 정리하여 메모리 누수를 방지합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class MetricsLockCleanupScheduler {
+
+    private final MetricsService metricsService;
+
+    /**
+     * 사용하지 않는 락 정리 (5분마다)
+     */
+    @Scheduled(fixedRate = 5 * 60 * 1000) // 5분
+    public void cleanupUnusedLocks() {
+        try {
+            metricsService.cleanupUnusedLocks();
+        } catch (Exception e) {
+            log.error("락 정리 중 오류 발생", e);
+        }
+    }
+
+    /**
+     * 처리된 이벤트 캐시 정리 (10분마다)
+     */
+    @Scheduled(fixedRate = 10 * 60 * 1000) // 10분
+    public void cleanupProcessedEvents() {
+        try {
+            metricsService.cleanupProcessedEvents();
+        } catch (Exception e) {
+            log.error("처리된 이벤트 캐시 정리 중 오류 발생", e);
+        }
+    }
+
+    /**
+     * 락 상태 모니터링 (1분마다)
+     */
+    @Scheduled(fixedRate = 60 * 1000) // 1분
+    public void monitorLockStatus() {
+        try {
+            metricsService.logLockStatus();
+        } catch (Exception e) {
+            log.error("락 상태 모니터링 중 오류 발생", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
index ba862cec6..e513f7f7b 100644
--- a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
+++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java
@@ -1,23 +1,24 @@
 package com.loopers.interfaces.consumer;
 
-import com.loopers.confg.kafka.KafkaConfig;
+import java.util.List;
+
 import org.apache.kafka.clients.consumer.ConsumerRecord;
 import org.springframework.kafka.annotation.KafkaListener;
 import org.springframework.kafka.support.Acknowledgment;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
+import com.loopers.confg.kafka.KafkaConfig;
 
 @Component
 public class DemoKafkaConsumer {
     @KafkaListener(
-        topics = {"${demo-kafka.test.topic-name}"},
-        containerFactory = KafkaConfig.BATCH_LISTENER
+            topics = {"${demo-kafka.test.topic-name}"},
+            containerFactory = KafkaConfig.BATCH_LISTENER
     )
     public void demoListener(
-        List<ConsumerRecord<Object,Object>> messages,
-        Acknowledgment acknowledgment
-    ){
+            List<ConsumerRecord<Object, Object>> messages,
+            Acknowledgment acknowledgment
+    ) {
         System.out.println(messages);
         acknowledgment.acknowledge();
     }
diff --git a/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsKafkaConsumer.java b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsKafkaConsumer.java
new file mode 100644
index 000000000..d5d62dbe7
--- /dev/null
+++ b/apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/MetricsKafkaConsumer.java
@@ -0,0 +1,215 @@
+package com.loopers.interfaces.consumer;
+
+import java.util.List;
+
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.support.Acknowledgment;
+import org.springframework.stereotype.Component;
+
+import com.loopers.confg.kafka.KafkaConfig;
+import com.loopers.domain.metrics.MetricsService;
+import com.loopers.infrastructure.event.DomainEventEnvelope;
+import com.loopers.infrastructure.event.EventDeserializer;
+import com.loopers.infrastructure.event.payloads.LikeActionPayloadV1;
+import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1;
+import com.loopers.infrastructure.event.payloads.ProductViewPayloadV1;
+import com.loopers.infrastructure.event.payloads.StockDepletedPayloadV1;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 메트릭스 Kafka 컨슈머 - 멱등성과 최신성을 보장하는 안전한 이벤트 처리
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 16.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class MetricsKafkaConsumer {
+
+    private final MetricsService metricsService;
+    private final EventDeserializer eventDeserializer;
+
+    @KafkaListener(
+            topics = {"catalog-events"},
+            containerFactory = KafkaConfig.BATCH_LISTENER
+    )
+    public void onCatalogEvents(
+            List<ConsumerRecord<Object, Object>> records,
+            Acknowledgment ack) {
+
+        log.debug("Processing {} catalog events", records.size());
+
+        for (ConsumerRecord<Object, Object> record : records) {
+            try {
+                processCatalogEvent(record);
+            } catch (Exception e) {
+                log.error("Failed to process catalog event: {}", record.value(), e);
+                // 개별 메시지 실패는 로그만 남기고 계속 진행
+                // 전체 배치를 실패시키지 않음
+            }
+        }
+
+        ack.acknowledge();
+        log.debug("Acknowledged {} catalog events", records.size());
+    }
+
+    @KafkaListener(
+            topics = {"order-events"},
+            containerFactory = KafkaConfig.BATCH_LISTENER
+    )
+    public void onOrderEvents(
+            final List<ConsumerRecord<Object, Object>> records,
+            final Acknowledgment ack
+    ) {
+
+        log.debug("Processing {} order events", records.size());
+
+        for (ConsumerRecord<Object, Object> record : records) {
+            try {
+                processOrderEvent(record);
+            } catch (Exception e) {
+                log.error("Failed to process order event: {}", record.value(), e);
+                // 개별 메시지 실패는 로그만 남기고 계속 진행
+            }
+        }
+
+        ack.acknowledge();
+        log.debug("Acknowledged {} order events", records.size());
+    }
+
+    private void processCatalogEvent(ConsumerRecord<Object, Object> record) {
+        final DomainEventEnvelope envelope = eventDeserializer.deserializeEnvelope(record.value());
+        if (envelope == null || envelope.eventId() == null) {
+            log.warn("Invalid event envelope: {}", record.value());
+            return;
+        }
+
+        // 과거 이벤트 필터링 (1시간 이상 된 이벤트는 무시)
+        if (isOldEvent(envelope.occurredAtEpochMillis())) {
+            log.debug("Ignoring old event: eventId={}, occurredAt={}",
+                    envelope.eventId(), envelope.occurredAtEpochMillis());
+            // 멱등성 테이블에는 기록하되 비즈니스 로직은 처리하지 않음
+            metricsService.tryMarkHandled(envelope.eventId());
+            return;
+        }
+
+        // 멱등성 체크 - 이미 처리된 이벤트는 무시
+        final boolean isFirstTime = metricsService.tryMarkHandled(envelope.eventId());
+        if (!isFirstTime) {
+            log.debug("Event already processed: {}", envelope.eventId());
+            return;
+        }
+
+        // 이벤트 타입별 처리
+        switch (envelope.eventType()) {
+            case "PRODUCT_VIEW" -> handleProductView(envelope);
+            case "LIKE_ACTION" -> handleLikeAction(envelope);
+            case "STOCK_DEPLETED" -> handleStockDepleted(envelope);
+            default -> log.debug("Unhandled catalog event type: {}", envelope.eventType());
+        }
+    }
+
+    private void processOrderEvent(ConsumerRecord<Object, Object> record) {
+        final DomainEventEnvelope envelope = eventDeserializer.deserializeEnvelope(record.value());
+        if (envelope == null || envelope.eventId() == null) {
+            log.warn("Invalid event envelope: {}", record.value());
+            return;
+        }
+
+        // 과거 이벤트 필터링
+        if (isOldEvent(envelope.occurredAtEpochMillis())) {
+            log.debug("Ignoring old event: eventId={}, occurredAt={}",
+                    envelope.eventId(), envelope.occurredAtEpochMillis());
+            metricsService.tryMarkHandled(envelope.eventId());
+            return;
+        }
+
+        // 멱등성 체크
+        final boolean isFirstTime = metricsService.tryMarkHandled(envelope.eventId());
+        if (!isFirstTime) {
+            log.debug("Event already processed: {}", envelope.eventId());
+            return;
+        }
+
+        // PAYMENT_SUCCESS 이벤트만 처리
+        if ("PAYMENT_SUCCESS".equals(envelope.eventType())) {
+            handlePaymentSuccess(envelope);
+        } else {
+            log.debug("Unhandled order event type: {}", envelope.eventType());
+        }
+    }
+
+    private void handleProductView(DomainEventEnvelope envelope) {
+        final ProductViewPayloadV1 payload = eventDeserializer.deserializeProductView(envelope.payloadJson());
+        if (payload == null || payload.productId() == null) {
+            log.warn("Invalid ProductView payload: {}", envelope.payloadJson());
+            return;
+        }
+
+        metricsService.incrementView(payload.productId(), envelope.occurredAtEpochMillis());
+        log.debug("Processed PRODUCT_VIEW for productId: {}", payload.productId());
+    }
+
+    private void handleLikeAction(DomainEventEnvelope envelope) {
+        final LikeActionPayloadV1 payload = eventDeserializer.deserializeLikeAction(envelope.payloadJson());
+        if (payload == null || payload.productId() == null || payload.action() == null) {
+            log.warn("Invalid LikeAction payload: {}", envelope.payloadJson());
+            return;
+        }
+
+        final int delta = "LIKE".equals(payload.action()) ? 1 : -1;
+        metricsService.applyLikeDelta(payload.productId(), delta, envelope.occurredAtEpochMillis());
+        log.debug("Processed LIKE_ACTION for productId: {}, action: {}", payload.productId(), payload.action());
+    }
+
+    private void handlePaymentSuccess(DomainEventEnvelope envelope) {
+        final PaymentSuccessPayloadV1 payload = eventDeserializer.deserializePaymentSuccess(envelope.payloadJson());
+        if (payload == null) {
+            log.warn("Invalid PaymentSuccess payload: {}", envelope.payloadJson());
+            return;
+        }
+
+        // 새로운 구조: 상품별 개별 이벤트 처리
+        if (payload.productId() != null && payload.quantity() != null && payload.quantity() > 0) {
+            metricsService.addSales(payload.productId(), payload.quantity(), envelope.occurredAtEpochMillis());
+
+            log.debug(
+                    "Processed PAYMENT_SUCCESS - orderId: {}, orderNumber: {}, userId: {}, productId: {}, quantity: {}, unitPrice: {}, totalPrice: {}",
+                    payload.orderId(), payload.orderNumber(), payload.userId(),
+                    payload.productId(), payload.quantity(), payload.unitPrice(), payload.totalPrice());
+        } else {
+            log.warn("Invalid PaymentSuccess payload - missing required fields: productId={}, quantity={}",
+                    payload.productId(), payload.quantity());
+        }
+    }
+
+    private void handleStockDepleted(DomainEventEnvelope envelope) {
+        final StockDepletedPayloadV1 payload = eventDeserializer.deserializeStockDepleted(envelope.payloadJson());
+        if (payload == null || payload.productId() == null) {
+            log.warn("Invalid StockDepleted payload: {}", envelope.payloadJson());
+            return;
+        }
+
+        // 재고 소진 이벤트 처리 - remainingStock 정보 전달
+        metricsService.handleStockDepleted(payload.productId(), payload.brandId(), payload.remainingStock(),
+                envelope.occurredAtEpochMillis());
+
+        log.info("Processed STOCK_DEPLETED - productId: {}, brandId: {}, productName: {}, remainingStock: {}",
+                payload.productId(), payload.brandId(), payload.productName(), payload.remainingStock());
+    }
+
+    /**
+     * 과거 이벤트인지 확인 (1시간 이상 된 이벤트는 과거 이벤트로 간주)
+     */
+    private boolean isOldEvent(long occurredAtEpochMillis) {
+        long currentTime = System.currentTimeMillis();
+        long eventAge = currentTime - occurredAtEpochMillis;
+        long oneHourInMillis = 60 * 60 * 1000; // 1시간
+
+        return eventAge > oneHourInMillis;
+    }
+}
diff --git a/apps/commerce-streamer/src/main/resources/application.yml b/apps/commerce-streamer/src/main/resources/application.yml
index 0651bc2bd..95629c631 100644
--- a/apps/commerce-streamer/src/main/resources/application.yml
+++ b/apps/commerce-streamer/src/main/resources/application.yml
@@ -1,3 +1,7 @@
+management:
+  server:
+    port: 8091
+
 server:
   shutdown: graceful
   tomcat:
@@ -9,12 +13,13 @@ server:
     accept-count: 100 # 대기 큐 크기 (default : 100)
     keep-alive-timeout: 60s # 60s
   max-http-request-header-size: 8KB
+  port: 8090
 
 spring:
   main:
     web-application-type: servlet
   application:
-    name: commerce-api
+    name: commerce-streamer
   profiles:
     active: local
   config:
diff --git a/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java
new file mode 100644
index 000000000..da767237b
--- /dev/null
+++ b/apps/commerce-streamer/src/test/java/com/loopers/integration/MetricsEventProcessingIntegrationTest.java
@@ -0,0 +1,250 @@
+package com.loopers.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import java.time.Duration;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.loopers.domain.event.EventRepository;
+import com.loopers.domain.metrics.ProductMetricsEntity;
+import com.loopers.domain.metrics.ProductMetricsRepository;
+import com.loopers.infrastructure.event.DomainEventEnvelope;
+import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1;
+import com.loopers.infrastructure.event.payloads.ProductViewPayloadV1;
+
+/**
+ * 메트릭스 이벤트 처리 통합 테스트
+ * 실제 Kafka를 통한 E2E 테스트
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+@SpringBootTest
+class MetricsEventProcessingIntegrationTest {
+
+    @Autowired
+    private KafkaTemplate<Object, Object> kafkaTemplate;
+
+    @Autowired
+    private ProductMetricsRepository productMetricsRepository;
+
+    @Autowired
+    private EventRepository eventRepository;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @BeforeEach
+    @Transactional
+    void setUp() {
+        // 테스트 데이터 정리
+        productMetricsRepository.deleteAll();
+    }
+
+    @Test
+    @DisplayName("PRODUCT_VIEW 이벤트가 정상적으로 처리되어 조회수가 증가해야 한다")
+    void shouldIncrementViewCountOnProductViewEvent() throws Exception {
+        // Given
+        Long productId = 1L;
+        String eventId = "product-view-test-" + System.currentTimeMillis();
+
+        ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 100L);
+        String payloadJson = objectMapper.writeValueAsString(payload);
+
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                payloadJson
+        );
+
+        // When
+        kafkaTemplate.send("catalog-events", envelope);
+
+        // Then
+        await().atMost(Duration.ofSeconds(10))
+                .untilAsserted(() -> {
+                    Optional<ProductMetricsEntity> metrics = productMetricsRepository.findById(productId);
+                    assertThat(metrics).isPresent();
+                    assertThat(metrics.get().getViewCount()).isEqualTo(1L);
+                    assertThat(metrics.get().getLastEventAt()).isNotNull();
+                });
+
+        // 멱등성 확인 - 이벤트가 처리되었음을 확인
+        assertThat(eventRepository.existsById(eventId)).isTrue();
+    }
+
+    @Test
+    @DisplayName("동일한 이벤트 ID로 중복 전송해도 한 번만 처리되어야 한다")
+    void shouldProcessDuplicateEventOnlyOnce() throws Exception {
+        // Given
+        Long productId = 2L;
+        String eventId = "duplicate-test-" + System.currentTimeMillis();
+
+        ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 200L);
+        String payloadJson = objectMapper.writeValueAsString(payload);
+
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                payloadJson
+        );
+
+        // When - 같은 이벤트를 두 번 전송
+        kafkaTemplate.send("catalog-events", envelope);
+        kafkaTemplate.send("catalog-events", envelope);
+
+        // Then - 조회수는 1만 증가해야 함
+        await().atMost(Duration.ofSeconds(10))
+                .untilAsserted(() -> {
+                    Optional<ProductMetricsEntity> metrics = productMetricsRepository.findById(productId);
+                    assertThat(metrics).isPresent();
+                    assertThat(metrics.get().getViewCount()).isEqualTo(1L);
+                });
+    }
+
+    @Test
+    @DisplayName("PAYMENT_SUCCESS 이벤트가 정상적으로 처리되어 판매수가 증가해야 한다")
+    void shouldIncrementSalesCountOnPaymentSuccessEvent() throws Exception {
+        // Given
+        Long productId = 3L;
+        String eventId = "payment-success-test-" + System.currentTimeMillis();
+
+        // 새로운 PaymentSuccessPayloadV1 구조 (상품별 개별 이벤트)
+        PaymentSuccessPayloadV1 payload = new PaymentSuccessPayloadV1(
+                12345L,    // orderId
+                67890L,    // orderNumber
+                100L,      // userId
+                productId, // productId
+                2,         // quantity
+                java.math.BigDecimal.valueOf(1000), // unitPrice
+                java.math.BigDecimal.valueOf(2000)  // totalPrice
+        );
+        String payloadJson = objectMapper.writeValueAsString(payload);
+
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PAYMENT_SUCCESS",
+                "v1",
+                System.currentTimeMillis(),
+                payloadJson
+        );
+
+        // When
+        kafkaTemplate.send("order-events", envelope);
+
+        // Then
+        await().atMost(Duration.ofSeconds(10))
+                .untilAsserted(() -> {
+                    Optional<ProductMetricsEntity> metrics = productMetricsRepository.findById(productId);
+
+                    assertThat(metrics).isPresent();
+                    assertThat(metrics.get().getSalesCount()).isEqualTo(2L);
+                    assertThat(metrics.get().getOrderCount()).isEqualTo(1L); // 주문 건수도 확인
+                });
+    }
+
+    @Test
+    @DisplayName("과거 이벤트는 무시되어야 한다")
+    void shouldIgnoreOldEvents() throws Exception {
+        // Given
+        Long productId = 5L;
+        long currentTime = System.currentTimeMillis();
+
+        // 먼저 최신 이벤트를 처리
+        String recentEventId = "recent-event-" + currentTime;
+        ProductViewPayloadV1 recentPayload = new ProductViewPayloadV1(productId, 100L);
+        String recentPayloadJson = objectMapper.writeValueAsString(recentPayload);
+
+        DomainEventEnvelope recentEnvelope = new DomainEventEnvelope(
+                recentEventId,
+                "PRODUCT_VIEW",
+                "v1",
+                currentTime,
+                recentPayloadJson
+        );
+
+        kafkaTemplate.send("catalog-events", recentEnvelope);
+
+        // 최신 이벤트가 처리될 때까지 대기
+        await().atMost(Duration.ofSeconds(10))
+                .untilAsserted(() -> {
+                    Optional<ProductMetricsEntity> metrics = productMetricsRepository.findById(productId);
+                    assertThat(metrics).isPresent();
+                    assertThat(metrics.get().getViewCount()).isEqualTo(1L);
+                });
+
+        // When - 과거 이벤트 전송 (1시간 전)
+        String oldEventId = "old-event-" + currentTime;
+        ProductViewPayloadV1 oldPayload = new ProductViewPayloadV1(productId, 200L);
+        String oldPayloadJson = objectMapper.writeValueAsString(oldPayload);
+
+        DomainEventEnvelope oldEnvelope = new DomainEventEnvelope(
+                oldEventId,
+                "PRODUCT_VIEW",
+                "v1",
+                currentTime - 3600000, // 1시간 전
+                oldPayloadJson
+        );
+
+        kafkaTemplate.send("catalog-events", oldEnvelope);
+
+// Then - 조회수는 여전히 1이어야 함 (과거 이벤트 무시)
+        await().atMost(Duration.ofSeconds(2))
+                .until(() -> {
+                    Optional<ProductMetricsEntity> finalMetrics = productMetricsRepository.findById(productId);
+                    return finalMetrics.isPresent() && finalMetrics.get().getViewCount()==1L;
+                });
+
+        // 과거 이벤트도 멱등성 테이블에는 기록되어야 함
+        assertThat(eventRepository.existsById(recentEventId)).isTrue();
+    }
+
+    @Test
+    @DisplayName("새로운 메트릭 필드들이 정상적으로 초기화되어야 한다")
+    void shouldInitializeNewMetricFields() throws Exception {
+        // Given
+        Long productId = 6L;
+        String eventId = "new-metrics-test-" + System.currentTimeMillis();
+
+        ProductViewPayloadV1 payload = new ProductViewPayloadV1(productId, 100L);
+        String payloadJson = objectMapper.writeValueAsString(payload);
+
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                payloadJson
+        );
+
+        // When
+        kafkaTemplate.send("catalog-events", envelope);
+
+        // Then - 새로운 메트릭 필드들이 0으로 초기화되어야 함
+        await().atMost(Duration.ofSeconds(10))
+                .untilAsserted(() -> {
+                    Optional<ProductMetricsEntity> metrics = productMetricsRepository.findById(productId);
+                    assertThat(metrics).isPresent();
+
+                    ProductMetricsEntity entity = metrics.get();
+                    assertThat(entity.getViewCount()).isEqualTo(1L);
+                    assertThat(entity.getLikeCount()).isEqualTo(0L);
+                    assertThat(entity.getSalesCount()).isEqualTo(0L);
+                    assertThat(entity.getOrderCount()).isEqualTo(0L);
+                    assertThat(entity.getLastEventAt()).isNotNull();
+                });
+    }
+}
diff --git a/apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsKafkaConsumerTest.java b/apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsKafkaConsumerTest.java
new file mode 100644
index 000000000..99dfd0b9e
--- /dev/null
+++ b/apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/MetricsKafkaConsumerTest.java
@@ -0,0 +1,190 @@
+package com.loopers.interfaces.consumer;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import java.util.List;
+
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.kafka.support.Acknowledgment;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.loopers.domain.metrics.MetricsService;
+import com.loopers.infrastructure.event.DomainEventEnvelope;
+import com.loopers.infrastructure.event.EventDeserializer;
+import com.loopers.infrastructure.event.payloads.PaymentSuccessPayloadV1;
+import com.loopers.infrastructure.event.payloads.ProductViewPayloadV1;
+
+/**
+ * MetricsKafkaConsumer 멱등성 및 신뢰성 테스트
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 18.
+ */
+@ExtendWith(MockitoExtension.class)
+class MetricsKafkaConsumerTest {
+
+    @Mock
+    private MetricsService metricsService;
+
+    @Mock
+    private EventDeserializer eventDeserializer;
+
+    @Mock
+    private Acknowledgment acknowledgment;
+
+    @InjectMocks
+    private MetricsKafkaConsumer consumer;
+
+    private ObjectMapper objectMapper;
+
+    @BeforeEach
+    void setUp() {
+        objectMapper = new ObjectMapper();
+    }
+
+    @Test
+    @DisplayName("중복된 이벤트 ID는 한 번만 처리되어야 한다")
+    void shouldProcessEventOnlyOnce() {
+        // Given
+        String eventId = "test-event-123";
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                "{\"productId\":1,\"userId\":100}"
+        );
+
+        ProductViewPayloadV1 payload = new ProductViewPayloadV1(1L, 100L);
+
+        ConsumerRecord<Object, Object> record = new ConsumerRecord<>("catalog-events", 0, 0, null, envelope);
+
+        // 첫 번째 호출에서는 true (처음 처리), 두 번째 호출에서는 false (이미 처리됨)
+        when(metricsService.tryMarkHandled(eventId))
+                .thenReturn(true)
+                .thenReturn(false);
+
+        when(eventDeserializer.deserializeEnvelope(envelope))
+                .thenReturn(envelope);
+
+        when(eventDeserializer.deserializeProductView(envelope.payloadJson()))
+                .thenReturn(payload);
+
+        // When - 같은 이벤트를 두 번 처리
+        consumer.onCatalogEvents(List.of(record, record), acknowledgment);
+
+        // Then - 비즈니스 로직은 한 번만 호출되어야 함
+        verify(metricsService, times(2)).tryMarkHandled(eventId);
+        verify(metricsService, times(1)).incrementView(eq(1L), anyLong());
+        verify(acknowledgment, times(1)).acknowledge();
+    }
+
+    @Test
+    @DisplayName("잘못된 이벤트 봉투는 무시되어야 한다")
+    void shouldIgnoreInvalidEventEnvelope() {
+        // Given
+        ConsumerRecord<Object, Object> record = new ConsumerRecord<>("catalog-events", 0, 0, null, "invalid-json");
+
+        when(eventDeserializer.deserializeEnvelope("invalid-json"))
+                .thenReturn(null);
+
+        // When
+        consumer.onCatalogEvents(List.of(record), acknowledgment);
+
+        // Then
+        verify(metricsService, never()).tryMarkHandled(anyString());
+        verify(metricsService, never()).incrementView(anyLong(), anyLong());
+        verify(acknowledgment, times(1)).acknowledge(); // 배치는 여전히 ack 되어야 함
+    }
+
+    @Test
+    @DisplayName("PAYMENT_SUCCESS 이벤트가 상품별로 개별 처리되어야 한다")
+    void shouldProcessPaymentSuccessEvent() {
+        // Given
+        String eventId = "payment-event-456";
+        DomainEventEnvelope envelope = new DomainEventEnvelope(
+                eventId,
+                "PAYMENT_SUCCESS",
+                "v1",
+                System.currentTimeMillis(),
+                "{\"orderId\":12345,\"orderNumber\":67890,\"userId\":100,\"productId\":1,\"quantity\":2,\"unitPrice\":1000,\"totalPrice\":2000}"
+        );
+
+        // 새로운 PaymentSuccessPayloadV1 구조 (상품별 개별 이벤트)
+        PaymentSuccessPayloadV1 payload = new PaymentSuccessPayloadV1(
+                12345L,    // orderId
+                67890L,    // orderNumber
+                100L,      // userId
+                1L,        // productId
+                2,         // quantity
+                java.math.BigDecimal.valueOf(1000), // unitPrice
+                java.math.BigDecimal.valueOf(2000)  // totalPrice
+        );
+
+        ConsumerRecord<Object, Object> record = new ConsumerRecord<>("order-events", 0, 0, null, envelope);
+
+        when(metricsService.tryMarkHandled(eventId)).thenReturn(true);
+        when(eventDeserializer.deserializeEnvelope(envelope)).thenReturn(envelope);
+        when(eventDeserializer.deserializePaymentSuccess(envelope.payloadJson())).thenReturn(payload);
+
+        // When
+        consumer.onOrderEvents(List.of(record), acknowledgment);
+
+        // Then
+        verify(metricsService, times(1)).tryMarkHandled(eventId);
+        verify(metricsService, times(1)).addSales(eq(1L), eq(2), anyLong());
+        verify(acknowledgment, times(1)).acknowledge();
+    }
+
+    @Test
+    @DisplayName("개별 메시지 처리 실패가 전체 배치를 실패시키지 않아야 한다")
+    void shouldContinueProcessingWhenIndividualMessageFails() {
+        // Given
+        String validEventId = "valid-event";
+        String invalidEventId = "invalid-event";
+
+        DomainEventEnvelope validEnvelope = new DomainEventEnvelope(
+                validEventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                "{\"productId\":1,\"userId\":100}"
+        );
+
+        DomainEventEnvelope invalidEnvelope = new DomainEventEnvelope(
+                invalidEventId,
+                "PRODUCT_VIEW",
+                "v1",
+                System.currentTimeMillis(),
+                "invalid-payload"
+        );
+
+        ProductViewPayloadV1 validPayload = new ProductViewPayloadV1(1L, 100L);
+
+        ConsumerRecord<Object, Object> validRecord = new ConsumerRecord<>("catalog-events", 0, 0, null, validEnvelope);
+        ConsumerRecord<Object, Object> invalidRecord = new ConsumerRecord<>("catalog-events", 0, 1, null, invalidEnvelope);
+
+        when(metricsService.tryMarkHandled(validEventId)).thenReturn(true);
+        when(metricsService.tryMarkHandled(invalidEventId)).thenReturn(true);
+
+        when(eventDeserializer.deserializeEnvelope(validEnvelope)).thenReturn(validEnvelope);
+        when(eventDeserializer.deserializeEnvelope(invalidEnvelope)).thenReturn(invalidEnvelope);
+
+        when(eventDeserializer.deserializeProductView(validEnvelope.payloadJson())).thenReturn(validPayload);
+        when(eventDeserializer.deserializeProductView(invalidEnvelope.payloadJson())).thenReturn(null); // 파싱 실패
+
+        // When
+        consumer.onCatalogEvents(List.of(validRecord, invalidRecord), acknowledgment);
+
+        // Then - 유효한 메시지는 처리되고, 전체 배치는 ack 되어야 함
+        verify(metricsService, times(1)).incrementView(eq(1L), anyLong());
+        verify(acknowledgment, times(1)).acknowledge();
+    }
+}
diff --git a/apps/pg-simulator/README.md b/apps/pg-simulator/README.md
index 118642638..e4691cd43 100644
--- a/apps/pg-simulator/README.md
+++ b/apps/pg-simulator/README.md
@@ -1,18 +1,23 @@
 ## PG-Simulator (PaymentGateway)
 
 ### Description
+
 Loopback BE 과정을 위해 PaymentGateway 를 시뮬레이션하는 App Module 입니다.
 `local` 프로필로 실행 권장하며, 커머스 서비스와의 동시 실행을 위해 서버 포트가 조정되어 있습니다.
+
 - server port : 8082
 - actuator port : 8083
 
 ### Getting Started
+
 부트 서버를 아래 명령어 혹은 `intelliJ` 통해 실행해주세요.
+
 ```shell
 ./gradlew :apps:pg-simulator:bootRun
 ```
 
 API 는 아래와 같이 주어지니, 커머스 서비스와 동시에 실행시킨 후 진행해주시면 됩니다.
+
 - 결제 요청 API
 - 결제 정보 확인 `by transactionKey`
 - 결제 정보 목록 조회 `by orderId`
diff --git a/apps/pg-simulator/build.gradle.kts b/apps/pg-simulator/build.gradle.kts
index 653d549da..71fb7844a 100644
--- a/apps/pg-simulator/build.gradle.kts
+++ b/apps/pg-simulator/build.gradle.kts
@@ -1,10 +1,10 @@
 plugins {
     val kotlinVersion = "2.0.20"
 
-    id("org.jetbrains.kotlin.jvm") version(kotlinVersion)
-    id("org.jetbrains.kotlin.kapt") version(kotlinVersion)
-    id("org.jetbrains.kotlin.plugin.spring") version(kotlinVersion)
-    id("org.jetbrains.kotlin.plugin.jpa") version(kotlinVersion)
+    id("org.jetbrains.kotlin.jvm") version (kotlinVersion)
+    id("org.jetbrains.kotlin.kapt") version (kotlinVersion)
+    id("org.jetbrains.kotlin.plugin.spring") version (kotlinVersion)
+    id("org.jetbrains.kotlin.plugin.jpa") version (kotlinVersion)
 }
 
 kotlin {
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
index 05595d135..e7017374b 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
@@ -5,7 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
 import org.springframework.boot.context.properties.ConfigurationPropertiesScan
 import org.springframework.boot.runApplication
 import org.springframework.scheduling.annotation.EnableAsync
-import java.util.TimeZone
+import java.util.*
 
 @ConfigurationPropertiesScan
 @EnableAsync
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
index 9a5ebdc5d..7fb361532 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
@@ -1,11 +1,6 @@
 package com.loopers.application.payment
 
-import com.loopers.domain.payment.Payment
-import com.loopers.domain.payment.PaymentEvent
-import com.loopers.domain.payment.PaymentEventPublisher
-import com.loopers.domain.payment.PaymentRelay
-import com.loopers.domain.payment.PaymentRepository
-import com.loopers.domain.payment.TransactionKeyGenerator
+import com.loopers.domain.payment.*
 import com.loopers.domain.user.UserInfo
 import com.loopers.support.error.CoreException
 import com.loopers.support.error.ErrorType
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
index cfc2386c1..6c35924dd 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
@@ -2,13 +2,7 @@ package com.loopers.domain.payment
 
 import com.loopers.support.error.CoreException
 import com.loopers.support.error.ErrorType
-import jakarta.persistence.Column
-import jakarta.persistence.Entity
-import jakarta.persistence.EnumType
-import jakarta.persistence.Enumerated
-import jakarta.persistence.Id
-import jakarta.persistence.Index
-import jakarta.persistence.Table
+import jakarta.persistence.*
 import java.time.LocalDateTime
 
 @Entity
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
index c8703a763..c0f1e2154 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
@@ -3,7 +3,7 @@ package com.loopers.domain.payment
 import org.springframework.stereotype.Component
 import java.time.LocalDateTime
 import java.time.format.DateTimeFormatter
-import java.util.UUID
+import java.util.*
 
 @Component
 class TransactionKeyGenerator {
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
index 434a229e2..1f8669a80 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
@@ -14,10 +14,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
 import org.springframework.web.server.ServerWebInputException
 import org.springframework.web.servlet.resource.NoResourceFoundException
-import kotlin.collections.joinToString
-import kotlin.jvm.java
-import kotlin.text.isNotEmpty
-import kotlin.text.toRegex
 
 @RestControllerAdvice
 class ApiControllerAdvice {
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
index 9ef6c25da..fe276e1a5 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
@@ -9,7 +9,7 @@ import org.springframework.web.context.request.NativeWebRequest
 import org.springframework.web.method.support.HandlerMethodArgumentResolver
 import org.springframework.web.method.support.ModelAndViewContainer
 
-class UserInfoArgumentResolver: HandlerMethodArgumentResolver {
+class UserInfoArgumentResolver : HandlerMethodArgumentResolver {
     companion object {
         private const val KEY_USER_ID = "X-USER-ID"
     }
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
index 22d5cbe38..fa60fe0fd 100644
--- a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
@@ -1,17 +1,11 @@
 package com.loopers.interfaces.api.payment
 
 import com.loopers.application.payment.PaymentApplicationService
-import com.loopers.interfaces.api.ApiResponse
 import com.loopers.domain.user.UserInfo
+import com.loopers.interfaces.api.ApiResponse
 import com.loopers.support.error.CoreException
 import com.loopers.support.error.ErrorType
-import org.springframework.web.bind.annotation.GetMapping
-import org.springframework.web.bind.annotation.PathVariable
-import org.springframework.web.bind.annotation.PostMapping
-import org.springframework.web.bind.annotation.RequestBody
-import org.springframework.web.bind.annotation.RequestMapping
-import org.springframework.web.bind.annotation.RequestParam
-import org.springframework.web.bind.annotation.RestController
+import org.springframework.web.bind.annotation.*
 
 @RestController
 @RequestMapping("/api/v1/payments")
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
index 723ffa6a4..595af2801 100644
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
+++ b/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
@@ -1,13 +1,14 @@
 package com.loopers.config.jpa;
 
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
 
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+
 @Configuration
 class DataSourceConfig {
     @Bean
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
index 2b95dd37a..753b07311 100644
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
+++ b/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
@@ -1,11 +1,13 @@
 package com.loopers.config.jpa;
 
-import com.querydsl.jpa.impl.JPAQueryFactory;
-import jakarta.persistence.EntityManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
 
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import jakarta.persistence.EntityManager;
+
 @Configuration
 class QueryDslConfig {
 
diff --git a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java b/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
index cf58548b2..848500688 100644
--- a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
+++ b/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
@@ -1,17 +1,13 @@
 package com.loopers.domain;
 
-import jakarta.persistence.Column;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.MappedSuperclass;
-import jakarta.persistence.PrePersist;
-import jakarta.persistence.PreUpdate;
-import lombok.Getter;
 import java.time.ZonedDateTime;
 
 import org.springframework.data.domain.AbstractAggregateRoot;
 
+import lombok.Getter;
+
+import jakarta.persistence.*;
+
 /**
  * 생성/수정/삭제 정보를 자동으로 관리해준다.
  * 재사용성을 위해 이 외의 컬럼이나 동작은 추가하지 않는다.
@@ -37,7 +33,8 @@ public abstract class BaseEntity<T extends BaseEntity<T>> extends AbstractAggreg
      * 엔티티의 유효성을 검증한다.
      * 이 메소드는 PrePersist 및 PreUpdate 시점에 호출된다.
      */
-    protected void guard() {}
+    protected void guard() {
+    }
 
     @PrePersist
     private void prePersist() {
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
index 9c41edacc..2883117b6 100644
--- a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
+++ b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
@@ -11,22 +11,22 @@ public class MySqlTestContainersConfig {
 
     static {
         mySqlContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
-            .withDatabaseName("loopers")
-            .withUsername("test")
-            .withPassword("test")
-            .withExposedPorts(3306)
-            .withCommand(
-                "--character-set-server=utf8mb4",
-                "--collation-server=utf8mb4_general_ci",
-                "--skip-character-set-client-handshake"
-            );
+                .withDatabaseName("loopers")
+                .withUsername("test")
+                .withPassword("test")
+                .withExposedPorts(3306)
+                .withCommand(
+                        "--character-set-server=utf8mb4",
+                        "--collation-server=utf8mb4_general_ci",
+                        "--skip-character-set-client-handshake"
+                );
         mySqlContainer.start();
 
         String mySqlJdbcUrl = String.format(
-            "jdbc:mysql://%s:%d/%s",
-            mySqlContainer.getHost(),
-            mySqlContainer.getFirstMappedPort(),
-            mySqlContainer.getDatabaseName()
+                "jdbc:mysql://%s:%d/%s",
+                mySqlContainer.getHost(),
+                mySqlContainer.getFirstMappedPort(),
+                mySqlContainer.getDatabaseName()
         );
 
         System.setProperty("datasource.mysql-jpa.main.jdbc-url", mySqlJdbcUrl);
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java b/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
index 85c43ace9..a2fd9a2f4 100644
--- a/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
+++ b/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
@@ -1,15 +1,16 @@
 package com.loopers.utils;
 
-import jakarta.persistence.Entity;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.Table;
+import java.util.ArrayList;
+import java.util.List;
+
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.ArrayList;
-import java.util.List;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Table;
 
 @Component
 public class DatabaseCleanUp implements InitializingBean {
@@ -22,9 +23,9 @@ public class DatabaseCleanUp implements InitializingBean {
     @Override
     public void afterPropertiesSet() {
         entityManager.getMetamodel().getEntities().stream()
-            .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
-            .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
-            .forEach(tableNames::add);
+                .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
+                .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
+                .forEach(tableNames::add);
     }
 
     @Transactional
diff --git a/modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java b/modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
index a73842775..5f0f81e3f 100644
--- a/modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
+++ b/modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
@@ -1,6 +1,8 @@
 package com.loopers.confg.kafka;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -13,8 +15,7 @@
 import org.springframework.kafka.support.converter.BatchMessagingMessageConverter;
 import org.springframework.kafka.support.converter.ByteArrayJsonMessageConverter;
 
-import java.util.HashMap;
-import java.util.Map;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
 @EnableKafka
 @Configuration
diff --git a/modules/kafka/src/main/resources/kafka.yml b/modules/kafka/src/main/resources/kafka.yml
index 9609dbf85..03a6bf4ce 100644
--- a/modules/kafka/src/main/resources/kafka.yml
+++ b/modules/kafka/src/main/resources/kafka.yml
@@ -3,18 +3,24 @@ spring:
     bootstrap-servers: ${BOOTSTRAP_SERVERS}
     client-id: ${spring.application.name}
     properties:
-        spring.json.add.type.headers: false # json serialize off
-        request.timeout.ms: 20000
-        retry.backoff.ms: 500
-        auto:
-          create.topics.enable: false
-          register.schemas: false
-          offset.reset: latest
-        use.latest.version: true
+      spring.json.add.type.headers: false # json serialize off
+      request.timeout.ms: 20000
+      retry.backoff.ms: 500
+      auto:
+        create.topics.enable: false
+        register.schemas: false
+        offset.reset: latest
+      use.latest.version: true
     producer:
       key-serializer: org.apache.kafka.common.serialization.StringSerializer
       value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
       retries: 3
+      acks: all
+      properties:
+        enable.idempotence: true
+        max.in.flight.requests.per.connection: 5
+        delivery.timeout.ms: 120000
+        request.timeout.ms: 30000
     consumer:
       group-id: loopers-default-consumer
       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
@@ -33,12 +39,59 @@ spring:
     admin:
       properties:
         bootstrap.servers: kafka:9092
+      # 토픽 자동 생성 설정
+      auto-create-topics-enable: true
+
+# 커스텀 토픽 설정
+kafka:
+  topics:
+    catalog-events:
+      name: catalog-events
+      partitions: 1
+      replicas: 1
+    order-events:
+      name: order-events
+      partitions: 1
+      replicas: 1
 
 ---
 spring.config.activate.on-profile: dev
 
+kafka:
+  topics:
+    catalog-events:
+      name: catalog-events
+      partitions: 6  # 개발환경에서는 더 많은 파티션
+      replicas: 2
+    order-events:
+      name: order-events
+      partitions: 6
+      replicas: 2
+
 ---
 spring.config.activate.on-profile: qa
 
+kafka:
+  topics:
+    catalog-events:
+      name: catalog-events
+      partitions: 9  # QA환경에서는 운영과 동일
+      replicas: 2
+    order-events:
+      name: order-events
+      partitions: 9
+      replicas: 2
+
 ---
 spring.config.activate.on-profile: prd
+
+kafka:
+  topics:
+    catalog-events:
+      name: catalog-events
+      partitions: 12  # 운영환경에서는 최대 성능
+      replicas: 3
+    order-events:
+      name: order-events
+      partitions: 12
+      replicas: 3
diff --git a/modules/redis/src/main/java/com/loopers/cache/BaseCacheService.java b/modules/redis/src/main/java/com/loopers/cache/BaseCacheService.java
new file mode 100644
index 000000000..21b7ba81a
--- /dev/null
+++ b/modules/redis/src/main/java/com/loopers/cache/BaseCacheService.java
@@ -0,0 +1,225 @@
+package com.loopers.cache;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 기본 캐시 서비스
+ * <p>
+ * Redis 캐시의 공통 연산을 제공합니다.
+ * 캐시 실패 시 로깅만 하고 서비스는 계속 동작합니다.
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class BaseCacheService {
+
+    private final RedisTemplate<String, String> redisTemplate;
+    private final ObjectMapper objectMapper;
+    private final CacheKeyGenerator cacheKeyGenerator;
+
+    // ========== 기본 캐시 연산 ==========
+
+    /**
+     * 캐시 저장
+     */
+    public void set(String key, Object value, long timeout, TimeUnit timeUnit) {
+        try {
+            String jsonValue = objectMapper.writeValueAsString(value);
+            redisTemplate.opsForValue().set(key, jsonValue, timeout, timeUnit);
+            log.debug("캐시 저장 - key: {}", key);
+        } catch (JsonProcessingException e) {
+            log.warn("캐시 저장 실패 (JSON 직렬화) - key: {}", key);
+        } catch (Exception e) {
+            log.warn("캐시 저장 실패 - key: {}, error: {}", key, e.getMessage());
+        }
+    }
+
+    /**
+     * 캐시 조회
+     */
+    public <T> Optional<T> get(String key, Class<T> clazz) {
+        try {
+            String value = redisTemplate.opsForValue().get(key);
+
+            if (value == null) {
+                log.debug("캐시 미스 - key: {}", key);
+                return Optional.empty();
+            }
+
+            T result = objectMapper.readValue(value, clazz);
+            log.debug("캐시 히트 - key: {}", key);
+
+            return Optional.of(result);
+        } catch (JsonProcessingException e) {
+            log.warn("캐시 조회 실패 (JSON 역직렬화) - key: {}", key);
+            return Optional.empty();
+        } catch (Exception e) {
+            log.warn("캐시 조회 실패 - key: {}, error: {}", key, e.getMessage());
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * 캐시 삭제
+     */
+    public void delete(String key) {
+        try {
+            Boolean deleted = redisTemplate.delete(key);
+            if (deleted) {
+                log.debug("캐시 삭제 - key: {}", key);
+            }
+        } catch (Exception e) {
+            log.warn("캐시 삭제 실패 - key: {}, error: {}", key, e.getMessage());
+        }
+    }
+
+    /**
+     * 패턴 기반 캐시 삭제
+     */
+    public void deleteByPattern(String pattern) {
+        try {
+            ScanOptions options = ScanOptions.scanOptions()
+                    .match(pattern)
+                    .count(100)
+                    .build();
+
+            Set<String> keys = new HashSet<>();
+            try (Cursor<String> cursor = redisTemplate.scan(options)) {
+                cursor.forEachRemaining(keys::add);
+            }
+
+            if (!keys.isEmpty()) {
+                redisTemplate.delete(keys);
+                log.debug("패턴 캐시 삭제 - pattern: {}, count: {}", pattern, keys.size());
+            }
+        } catch (Exception e) {
+            log.warn("패턴 캐시 삭제 실패 - pattern: {}, error: {}", pattern, e.getMessage());
+        }
+    }
+
+    // ========== 상품 캐시 무효화 ==========
+
+    /**
+     * 특정 상품의 캐시 무효화
+     */
+    public void evictProductCache(Long productId) {
+        try {
+            String productKey = cacheKeyGenerator.generateProductDetailKey(productId);
+            delete(productKey);
+            log.debug("상품 캐시 무효화 - productId: {}", productId);
+        } catch (Exception e) {
+            log.warn("상품 캐시 무효화 실패 - productId: {}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 목록 관련 캐시 무효화
+     */
+    public void evictProductListCaches() {
+        try {
+            // 인기 상품 목록 캐시 삭제
+            delete(cacheKeyGenerator.generatePopularProductsKey());
+
+            // 상품 목록 관련 캐시들 패턴 매칭으로 삭제
+            deleteByPattern(cacheKeyGenerator.generateProductListPattern());
+
+            log.debug("상품 목록 캐시 무효화 완료");
+        } catch (Exception e) {
+            log.warn("상품 목록 캐시 무효화 실패", e);
+        }
+    }
+
+    /**
+     * 판매량 변화 시 캐시 무효화
+     */
+    public void onSalesCountChanged(Long productId) {
+        evictProductCache(productId);
+        evictProductListCaches();
+        log.debug("판매량 변화로 캐시 무효화 - productId: {}", productId);
+    }
+
+    // ========== 브랜드 캐시 무효화 ==========
+
+    /**
+     * 특정 브랜드의 상품 목록 캐시 무효화
+     */
+    public void evictBrandProductListCache(Long brandId) {
+        try {
+            String pattern = cacheKeyGenerator.generateProductListPatternByBrand(brandId);
+            deleteByPattern(pattern);
+            log.debug("브랜드 상품 목록 캐시 무효화 - brandId: {}", brandId);
+        } catch (Exception e) {
+            log.warn("브랜드 상품 목록 캐시 무효화 실패 - brandId: {}", brandId, e);
+        }
+    }
+
+    // ========== 상품 재고 캐시 갱신 ==========
+
+    /**
+     * 상품 상세 캐시의 재고 정보만 업데이트
+     * 캐시가 존재하는 경우에만 업데이트하고, 없으면 스킵
+     */
+    public void updateProductStock(Long productId, Integer newStock) {
+        try {
+            String key = cacheKeyGenerator.generateProductDetailKey(productId);
+
+            // 기존 캐시 조회
+            String cachedValue = redisTemplate.opsForValue().get(key);
+            if (cachedValue == null) {
+                log.debug("상품 상세 캐시 없음 - 재고 갱신 스킵: productId={}", productId);
+                return;
+            }
+
+            // JSON 파싱하여 재고 정보 업데이트
+            try {
+                // 기존 캐시 데이터를 Map으로 파싱
+                @SuppressWarnings("unchecked")
+                Map<String, Object> productData = objectMapper.readValue(cachedValue, java.util.Map.class);
+
+                // 재고 정보 업데이트
+                productData.put("stock", newStock);
+                productData.put("isInStock", newStock > 0);
+
+                // 업데이트된 데이터를 다시 JSON으로 변환하여 저장
+                String updatedValue = objectMapper.writeValueAsString(productData);
+
+                // 기존 TTL 유지하면서 업데이트
+                long ttl = redisTemplate.getExpire(key);
+                if (ttl > 0) {
+                    redisTemplate.opsForValue().set(key, updatedValue, ttl, TimeUnit.SECONDS);
+                } else {
+                    // TTL이 없거나 만료된 경우 기본 30분으로 설정
+                    redisTemplate.opsForValue().set(key, updatedValue, 30, TimeUnit.MINUTES);
+                }
+
+                log.debug("상품 재고 캐시 갱신 완료: productId={}, newStock={}", productId, newStock);
+
+            } catch (JsonProcessingException e) {
+                log.warn("상품 재고 캐시 갱신 실패 (JSON 처리 오류) - 캐시 삭제: productId={}", productId, e);
+                // JSON 파싱 실패 시 캐시 삭제
+                delete(key);
+            }
+
+        } catch (Exception e) {
+            log.warn("상품 재고 캐시 갱신 실패: productId={}, error: {}", productId, e.getMessage());
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java b/modules/redis/src/main/java/com/loopers/cache/CacheKeyGenerator.java
similarity index 62%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java
rename to modules/redis/src/main/java/com/loopers/cache/CacheKeyGenerator.java
index dd0ceda07..5b5e9423f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheKeyGenerator.java
+++ b/modules/redis/src/main/java/com/loopers/cache/CacheKeyGenerator.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.cache;
+package com.loopers.cache;
 
 import java.util.StringJoiner;
 
@@ -9,10 +9,10 @@
 /**
  * 캐시 키 생성기
  * <p>
- * Hot/Warm/Cold 전략별 캐시 키 생성
- * - product:detail:{productId}
- * - product:ids:{strategy}:{brandId}:{page}:{size}:{sort}
- * - product:page:{brandId}:{productName}:{page}:{size}:{sort}
+ * 일관된 캐시 키 생성을 위한 공통 유틸리티
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
  */
 @Component
 public class CacheKeyGenerator {
@@ -21,10 +21,13 @@ public class CacheKeyGenerator {
     private static final String NULL_VALUE = "null";
 
     // 캐시 키 프리픽스
-    private static final String PRODUCT_PREFIX = "product";
-    private static final String DETAIL_PREFIX = "detail";
-    private static final String IDS_PREFIX = "ids";
-    private static final String PAGE_PREFIX = "page";
+    public static final String PRODUCT_PREFIX = "product";
+    public static final String DETAIL_PREFIX = "detail";
+    public static final String IDS_PREFIX = "ids";
+    public static final String PAGE_PREFIX = "page";
+    public static final String METRICS_PREFIX = "metrics";
+    public static final String POPULAR_PREFIX = "popular";
+    public static final String LIST_PREFIX = "list";
 
     /**
      * 상품 상세 캐시 키: product:detail:{productId}
@@ -39,7 +42,6 @@ public String generateProductDetailKey(Long productId) {
 
     /**
      * 상품 ID 리스트 캐시 키: product:ids:{strategy}:{brandId}:{page}:{size}:{sort}
-     * ID만 캐싱하여 개별 상품 변경 시 전체 캐시 무효화 방지
      */
     public String generateProductIdsKey(CacheStrategy strategy, Long brandId, Pageable pageable) {
         return new StringJoiner(DELIMITER)
@@ -53,7 +55,6 @@ public String generateProductIdsKey(CacheStrategy strategy, Long brandId, Pageab
                 .toString();
     }
 
-
     /**
      * 상품 ID 리스트 패턴: product:ids:{strategy}:*
      */
@@ -66,9 +67,8 @@ public String generateProductIdsPattern(CacheStrategy strategy) {
                 .toString();
     }
 
-
     /**
-     * 특정 브랜드의 모든 목록 패턴 (레거시): product:page:{brandId}:*
+     * 특정 브랜드의 모든 목록 패턴: product:page:{brandId}:*
      */
     public String generateProductListPatternByBrand(Long brandId) {
         return new StringJoiner(DELIMITER)
@@ -79,30 +79,40 @@ public String generateProductListPatternByBrand(Long brandId) {
                 .toString();
     }
 
-
     /**
-     * 상품명을 캐시 키에 안전한 형태로 변환 (공백→언더스코어, 특수문자 제거, 최대 50자)
+     * 인기 상품 캐시 키: product:popular
      */
-    private String sanitizeProductName(String productName) {
-        if (productName == null || productName.trim().isEmpty()) {
-            return NULL_VALUE;
-        }
-
-        // 공백을 언더스코어로 변환하고, 특수문자 제거
-        String sanitized = productName.trim()
-                .replaceAll("\\s+", "_")
-                .replaceAll("[^a-zA-Z0-9가-힣_]", "");
+    public String generatePopularProductsKey() {
+        return new StringJoiner(DELIMITER)
+                .add(PRODUCT_PREFIX)
+                .add(POPULAR_PREFIX)
+                .toString();
+    }
 
-        // 최대 50자로 제한
-        if (sanitized.length() > 50) {
-            sanitized = sanitized.substring(0, 50);
-        }
+    /**
+     * 상품 목록 캐시 패턴: product:list:*
+     */
+    public String generateProductListPattern() {
+        return new StringJoiner(DELIMITER)
+                .add(PRODUCT_PREFIX)
+                .add(LIST_PREFIX)
+                .add("*")
+                .toString();
+    }
 
-        return sanitized.isEmpty() ? NULL_VALUE : sanitized;
+    /**
+     * 상품 메트릭 캐시 키: product:metrics:{productId}
+     */
+    public String generateProductMetricsKey(Long productId) {
+        return new StringJoiner(DELIMITER)
+                .add(PRODUCT_PREFIX)
+                .add(METRICS_PREFIX)
+                .add(String.valueOf(productId))
+                .toString();
     }
 
     /**
-     * Sort 객체를 문자열로 변환 (예: "likeCount_desc,id_asc")
+     * Sort 객체를 문자열로 변환
      */
     private String generateSortString(Sort sort) {
         if (sort.isUnsorted()) {
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheStrategy.java b/modules/redis/src/main/java/com/loopers/cache/CacheStrategy.java
similarity index 63%
rename from apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheStrategy.java
rename to modules/redis/src/main/java/com/loopers/cache/CacheStrategy.java
index 200450d95..c873eafcb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/cache/CacheStrategy.java
+++ b/modules/redis/src/main/java/com/loopers/cache/CacheStrategy.java
@@ -1,4 +1,4 @@
-package com.loopers.infrastructure.cache;
+package com.loopers.cache;
 
 import java.util.concurrent.TimeUnit;
 
@@ -8,30 +8,19 @@
 /**
  * 캐시 전략: Hot/Warm/Cold 차별화된 TTL과 갱신 방식
  * <p>
- * - Hot: 배치 갱신, TTL 60분 (인기순, 1페이지)
+ * - Hot: 배치 갱신, TTL 30분 (인기순, 1페이지)
  * - Warm: Cache-Aside, TTL 10분 (2~3페이지)
  * - Cold: 캐시 미사용 (4페이지 이상)
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
  */
 @Getter
 @RequiredArgsConstructor
 public enum CacheStrategy {
 
-    /**
-     * Hot: 배치 갱신, TTL 60분
-     * 인기순 정렬, 1페이지 (가장 빈번 조회)
-     */
     HOT(30, TimeUnit.MINUTES, true, CacheUpdateStrategy.BATCH_REFRESH),
-
-    /**
-     * Warm: Cache-Aside, TTL 10분
-     * 2~3페이지 (꾸준한 조회)
-     */
     WARM(10, TimeUnit.MINUTES, false, CacheUpdateStrategy.CACHE_ASIDE),
-
-    /**
-     * Cold: 캐시 미사용
-     * 4페이지 이상 (거의 조회 안 됨)
-     */
     COLD(0, TimeUnit.MINUTES, false, CacheUpdateStrategy.NO_CACHE);
 
     private final long ttl;
@@ -39,16 +28,10 @@ public enum CacheStrategy {
     private final boolean useBatchUpdate;
     private final CacheUpdateStrategy updateStrategy;
 
-    /**
-     * 캐시 사용 여부
-     */
     public boolean shouldCache() {
         return this != COLD;
     }
 
-    /**
-     * 배치 갱신 사용 여부
-     */
     public boolean shouldUseBatchUpdate() {
         return useBatchUpdate;
     }
diff --git a/modules/redis/src/main/java/com/loopers/cache/CacheUpdateStrategy.java b/modules/redis/src/main/java/com/loopers/cache/CacheUpdateStrategy.java
new file mode 100644
index 000000000..c72a18bcf
--- /dev/null
+++ b/modules/redis/src/main/java/com/loopers/cache/CacheUpdateStrategy.java
@@ -0,0 +1,24 @@
+package com.loopers.cache;
+
+/**
+ * 캐시 갱신 전략
+ *
+ * @author hyunjikoh
+ * @since 2025. 12. 19.
+ */
+public enum CacheUpdateStrategy {
+    /**
+     * 배치 갱신: 주기적으로 캐시를 갱신
+     */
+    BATCH_REFRESH,
+
+    /**
+     * Cache-Aside: 캐시 미스 시 DB 조회 후 캐시 저장
+     */
+    CACHE_ASIDE,
+
+    /**
+     * 캐시 미사용
+     */
+    NO_CACHE
+}
diff --git a/modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java b/modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java
index 0a2b614ca..1625d2420 100644
--- a/modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java
+++ b/modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java
@@ -2,6 +2,9 @@
 
 
 import io.lettuce.core.ReadFrom;
+import java.util.List;
+import java.util.function.Consumer;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
@@ -13,18 +16,15 @@
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
-import java.util.List;
-import java.util.function.Consumer;
-
 @Configuration
 @EnableConfigurationProperties(RedisProperties.class)
-public class RedisConfig{
+public class RedisConfig {
     private static final String CONNECTION_MASTER = "redisConnectionMaster";
     public static final String REDIS_TEMPLATE_MASTER = "redisTemplateMaster";
 
     private final RedisProperties redisProperties;
 
-    public RedisConfig(RedisProperties redisProperties){
+    public RedisConfig(RedisProperties redisProperties) {
         this.redisProperties = redisProperties;
     }
 
@@ -74,22 +74,24 @@ private LettuceConnectionFactory lettuceConnectionFactory(
             RedisNodeInfo master,
             List<RedisNodeInfo> replicas,
             Consumer<LettuceClientConfiguration.LettuceClientConfigurationBuilder> customizer
-    ){
+    ) {
         LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = LettuceClientConfiguration.builder();
-        if(customizer != null) customizer.accept(builder);
+        if (customizer != null)
+            customizer.accept(builder);
         LettuceClientConfiguration clientConfig = builder.build();
-        RedisStaticMasterReplicaConfiguration masterReplicaConfig = new RedisStaticMasterReplicaConfiguration(master.host(), master.port());
+        RedisStaticMasterReplicaConfiguration masterReplicaConfig =
+                new RedisStaticMasterReplicaConfiguration(master.host(), master.port());
         masterReplicaConfig.setDatabase(database);
-        for(RedisNodeInfo r : replicas){
+        for (RedisNodeInfo r : replicas) {
             masterReplicaConfig.addNode(r.host(), r.port());
         }
         return new LettuceConnectionFactory(masterReplicaConfig, clientConfig);
     }
 
-    private <K,V> RedisTemplate<K,V> defaultRedisTemplate(
-            RedisTemplate<K,V> template,
+    private <K, V> RedisTemplate<K, V> defaultRedisTemplate(
+            RedisTemplate<K, V> template,
             LettuceConnectionFactory connectionFactory
-    ){
+    ) {
         StringRedisSerializer s = new StringRedisSerializer();
         template.setKeySerializer(s);
         template.setValueSerializer(s);
diff --git a/modules/redis/src/main/java/com/loopers/config/redis/RedisNodeInfo.java b/modules/redis/src/main/java/com/loopers/config/redis/RedisNodeInfo.java
index e03066c16..aeb9ce054 100644
--- a/modules/redis/src/main/java/com/loopers/config/redis/RedisNodeInfo.java
+++ b/modules/redis/src/main/java/com/loopers/config/redis/RedisNodeInfo.java
@@ -3,4 +3,5 @@
 public record RedisNodeInfo(
         String host,
         int port
-) { }
+) {
+}
diff --git a/modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java b/modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java
index a396ca456..ee238819c 100644
--- a/modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java
+++ b/modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java
@@ -1,12 +1,13 @@
 package com.loopers.config.redis;
 
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
 import java.util.List;
 
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
 @ConfigurationProperties(value = "datasource.redis")
 public record RedisProperties(
         int database,
         RedisNodeInfo master,
         List<RedisNodeInfo> replicas
-) { }
+) {
+}
diff --git a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
index 35bf94f06..fa42fa5ae 100644
--- a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
+++ b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
@@ -1,9 +1,10 @@
 package com.loopers.testcontainers;
 
-import com.redis.testcontainers.RedisContainer;
 import org.springframework.context.annotation.Configuration;
 import org.testcontainers.utility.DockerImageName;
 
+import com.redis.testcontainers.RedisContainer;
+
 @Configuration
 public class RedisTestContainersConfig {
     private static final RedisContainer redisContainer = new RedisContainer(DockerImageName.parse("redis:latest"));
diff --git a/modules/redis/src/testFixtures/java/com/loopers/utils/RedisCleanUp.java b/modules/redis/src/testFixtures/java/com/loopers/utils/RedisCleanUp.java
index 5f3cf4d13..a2eb780fb 100644
--- a/modules/redis/src/testFixtures/java/com/loopers/utils/RedisCleanUp.java
+++ b/modules/redis/src/testFixtures/java/com/loopers/utils/RedisCleanUp.java
@@ -12,7 +12,7 @@ public RedisCleanUp(RedisConnectionFactory redisConnectionFactory) {
         this.redisConnectionFactory = redisConnectionFactory;
     }
 
-    public void truncateAll(){
+    public void truncateAll() {
         try (RedisConnection connection = redisConnectionFactory.getConnection()) {
             connection.serverCommands().flushAll();
         }
diff --git a/stress/kafka_event-driven-e2e-test.js b/stress/kafka_event-driven-e2e-test.js
new file mode 100755
index 000000000..ea7cb80c1
--- /dev/null
+++ b/stress/kafka_event-driven-e2e-test.js
@@ -0,0 +1,284 @@
+// event-driven-e2e-test.js
+// 이벤트 기반 E2E 테스트: 아웃박스 패턴과 컨슈머 멱등성 검증
+import http from 'k6/http';
+import { check, sleep } from 'k6';
+import { Counter, Rate, Trend } from 'k6/metrics';
+
+export const options = {
+    scenarios: {
+        // 시나리오 1: 상품 조회 이벤트 대량 발생
+        product_view_load: {
+            executor: 'ramping-vus',
+            startVUs: 0,
+            stages: [
+                { duration: '10s', target: 50 },   // 50명이 동시에 상품 조회
+                { duration: '20s', target: 100 },  // 100명으로 증가
+                { duration: '10s', target: 0 },    // 종료
+            ],
+            exec: 'productViewScenario',
+        },
+        
+        // 시나리오 2: 좋아요 액션 이벤트
+        like_action_load: {
+            executor: 'ramping-vus',
+            startVUs: 0,
+            stages: [
+                { duration: '15s', target: 30 },   // 30명이 좋아요 액션
+                { duration: '15s', target: 60 },   // 60명으로 증가
+                { duration: '10s', target: 0 },    // 종료
+            ],
+            exec: 'likeActionScenario',
+            startTime: '5s', // 5초 후 시작
+        },
+        
+        // 시나리오 3: 결제 완료 이벤트
+        payment_success_load: {
+            executor: 'ramping-vus',
+            startVUs: 0,
+            stages: [
+                { duration: '20s', target: 20 },   // 20명이 결제
+                { duration: '10s', target: 0 },    // 종료
+            ],
+            exec: 'paymentSuccessScenario',
+            startTime: '10s', // 10초 후 시작
+        },
+        
+        // 시나리오 4: 멱등성 테스트 (동일한 결제 콜백 중복 전송)
+        idempotency_test: {
+            executor: 'constant-vus',
+            vus: 10,
+            duration: '30s',
+            exec: 'idempotencyScenario',
+            startTime: '20s', // 20초 후 시작
+        }
+    },
+    thresholds: {
+        'http_req_duration': ['p(95)<2000'],  // 95% 요청이 2초 이내
+        'http_req_failed': ['rate<0.05'],     // 에러율 5% 미만
+        'product_view_success': ['rate>0.95'], // 상품 조회 성공률 95% 이상
+        'like_action_success': ['rate>0.95'],  // 좋아요 성공률 95% 이상
+        'payment_success': ['rate>0.95'],      // 결제 성공률 95% 이상
+    },
+};
+
+// 메트릭 정의
+const productViewSuccess = new Rate('product_view_success');
+const likeActionSuccess = new Rate('like_action_success');
+const paymentSuccess = new Rate('payment_success');
+const idempotencyCheck = new Rate('idempotency_check');
+const eventCounter = new Counter('events_generated');
+
+const BASE_URL = 'http://localhost:8080/api/v1';
+const USERS = ['testuser1', 'testuser2', 'testuser3', 'testuser4', 'testuser5'];
+const PRODUCT_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+
+// 랜덤 사용자 선택
+function getRandomUser() {
+    return USERS[Math.floor(Math.random() * USERS.length)];
+}
+
+// 랜덤 상품 ID 선택
+function getRandomProductId() {
+    return PRODUCT_IDS[Math.floor(Math.random() * PRODUCT_IDS.length)];
+}
+
+// 시나리오 1: 상품 조회 (PRODUCT_VIEW 이벤트 발생)
+export function productViewScenario() {
+    const user = getRandomUser();
+    const productId = getRandomProductId();
+    
+    const response = http.get(`${BASE_URL}/products/${productId}`, {
+        headers: { 'X-USER-ID': user }
+    });
+    
+    const success = check(response, {
+        'product view status 200': (r) => r.status === 200,
+        'product view has data': (r) => r.json('data') !== null,
+    });
+    
+    productViewSuccess.add(success);
+    eventCounter.add(1, { event_type: 'PRODUCT_VIEW' });
+    
+    sleep(0.1); // 100ms 간격
+}
+
+// 시나리오 2: 좋아요 액션 (LIKE_ACTION 이벤트 발생)
+export function likeActionScenario() {
+    const user = getRandomUser();
+    const productId = getRandomProductId();
+    const action = Math.random() > 0.7 ? 'UNLIKE' : 'LIKE'; // 70% LIKE, 30% UNLIKE
+    
+    const response = http.post(`${BASE_URL}/likes/products/${productId}`, 
+        JSON.stringify({ action: action }), 
+        {
+            headers: { 
+                'Content-Type': 'application/json',
+                'X-USER-ID': user 
+            }
+        }
+    );
+    
+    const success = check(response, {
+        'like action status 200': (r) => r.status === 200,
+        'like action success': (r) => r.json('success') === true,
+    });
+    
+    likeActionSuccess.add(success);
+    eventCounter.add(1, { event_type: 'LIKE_ACTION' });
+    
+    sleep(0.2); // 200ms 간격
+}
+
+// 시나리오 3: 결제 완료 (PAYMENT_SUCCESS 이벤트 발생)
+export function paymentSuccessScenario() {
+    const user = getRandomUser();
+    const productId1 = getRandomProductId();
+    const productId2 = getRandomProductId();
+    
+    // 1단계: 카드 결제 주문 생성
+    const orderPayload = {
+        items: [
+            { productId: productId1, quantity: Math.floor(Math.random() * 3) + 1 },
+            { productId: productId2, quantity: Math.floor(Math.random() * 2) + 1 }
+        ],
+        cardInfo: {
+            cardType: 'SAMSUNG',
+            cardNo: '1234-5678-9012-3456',
+            callbackUrl: `${BASE_URL}/payments/callback`
+        }
+    };
+    
+    const orderResponse = http.post(`${BASE_URL}/orders/card`, 
+        JSON.stringify(orderPayload), 
+        {
+            headers: { 
+                'Content-Type': 'application/json',
+                'X-USER-ID': user 
+            }
+        }
+    );
+    
+    const orderSuccess = check(orderResponse, {
+        'order creation status 200': (r) => r.status === 200,
+        'order has payment info': (r) => r.json('data.paymentInfo') !== null,
+    });
+    
+    if (orderSuccess) {
+        const orderData = orderResponse.json('data');
+        const transactionKey = orderData.paymentInfo.transactionKey;
+        const orderId = orderData.orderId.toString();
+        
+        // 2단계: PG 결제 완료 콜백 (PAYMENT_SUCCESS 이벤트 발생)
+        sleep(0.5); // PG 처리 시간 시뮬레이션
+        
+        const callbackPayload = {
+            transactionKey: transactionKey,
+            orderId: orderId,
+            cardType: 'SAMSUNG',
+            cardNo: '1234-5678-9012-3456',
+            amount: Math.floor(orderData.finalTotalAmount),
+            status: 'SUCCESS',
+            reason: null
+        };
+        
+        const callbackResponse = http.post(`${BASE_URL}/payments/callback`, 
+            JSON.stringify(callbackPayload), 
+            {
+                headers: { 'Content-Type': 'application/json' }
+            }
+        );
+        
+        const callbackSuccess = check(callbackResponse, {
+            'payment callback status 200': (r) => r.status === 200,
+            'payment callback success': (r) => r.json('success') === true,
+        });
+        
+        paymentSuccess.add(callbackSuccess);
+        eventCounter.add(1, { event_type: 'PAYMENT_SUCCESS' });
+    }
+    
+    sleep(1); // 1초 간격
+}
+
+// 시나리오 4: 멱등성 테스트 (동일한 콜백 중복 전송)
+export function idempotencyScenario() {
+    const user = getRandomUser();
+    const productId = getRandomProductId();
+    
+    // 고정된 거래 키로 중복 콜백 테스트
+    const fixedTransactionKey = `IDEMPOTENCY_TEST_${__VU}_${Math.floor(Date.now() / 10000)}`;
+    const fixedOrderId = `${__VU}${Math.floor(Math.random() * 1000)}`;
+    
+    const callbackPayload = {
+        transactionKey: fixedTransactionKey,
+        orderId: fixedOrderId,
+        cardType: 'SAMSUNG',
+        cardNo: '1234-5678-9012-3456',
+        amount: 25000,
+        status: 'SUCCESS',
+        reason: null
+    };
+    
+    // 동일한 콜백을 3번 연속 전송 (멱등성 테스트)
+    let firstSuccess = false;
+    let subsequentResponses = [];
+    
+    for (let i = 0; i < 3; i++) {
+        const response = http.post(`${BASE_URL}/payments/callback`, 
+            JSON.stringify(callbackPayload), 
+            {
+                headers: { 'Content-Type': 'application/json' }
+            }
+        );
+        
+        if (i === 0) {
+            firstSuccess = response.status === 200;
+        } else {
+            subsequentResponses.push(response.status === 200);
+        }
+        
+        sleep(0.1); // 100ms 간격으로 중복 전송
+    }
+    
+    // 첫 번째는 성공, 나머지도 성공해야 함 (멱등성)
+    const idempotent = firstSuccess && subsequentResponses.every(success => success);
+    idempotencyCheck.add(idempotent);
+    
+    if (idempotent) {
+        eventCounter.add(1, { event_type: 'IDEMPOTENCY_TEST' });
+    }
+    
+    sleep(2); // 2초 간격
+}
+
+// 테스트 완료 후 요약 출력
+export function handleSummary(data) {
+    const summary = {
+        'test_duration': data.metrics.iteration_duration ? data.metrics.iteration_duration.avg : 0,
+        'total_requests': data.metrics.http_reqs ? data.metrics.http_reqs.count : 0,
+        'failed_requests': data.metrics.http_req_failed ? data.metrics.http_req_failed.count : 0,
+        'events_generated': data.metrics.events_generated ? data.metrics.events_generated.count : 0,
+        'product_view_success_rate': data.metrics.product_view_success ? data.metrics.product_view_success.rate : 0,
+        'like_action_success_rate': data.metrics.like_action_success ? data.metrics.like_action_success.rate : 0,
+        'payment_success_rate': data.metrics.payment_success ? data.metrics.payment_success.rate : 0,
+        'idempotency_success_rate': data.metrics.idempotency_check ? data.metrics.idempotency_check.rate : 0,
+    };
+    
+    console.log('\n=== 이벤트 기반 E2E 테스트 결과 ===');
+    console.log(`총 요청 수: ${summary.total_requests}`);
+    console.log(`실패 요청 수: ${summary.failed_requests}`);
+    console.log(`생성된 이벤트 수: ${summary.events_generated}`);
+    console.log(`상품 조회 성공률: ${(summary.product_view_success_rate * 100).toFixed(2)}%`);
+    console.log(`좋아요 액션 성공률: ${(summary.like_action_success_rate * 100).toFixed(2)}%`);
+    console.log(`결제 성공률: ${(summary.payment_success_rate * 100).toFixed(2)}%`);
+    console.log(`멱등성 테스트 성공률: ${(summary.idempotency_success_rate * 100).toFixed(2)}%`);
+    
+    // 디버깅을 위한 전체 메트릭 출력
+    console.log('\n=== 디버깅 정보 ===');
+    console.log('Available metrics:', Object.keys(data.metrics));
+    
+    return {
+        'stdout': JSON.stringify(summary, null, 2),
+        'summary.json': JSON.stringify(data, null, 2),
+    };
+}
\ No newline at end of file
diff --git a/stress/kafka_test_result.json b/stress/kafka_test_result.json
new file mode 100644
index 000000000..9207a5d49
--- /dev/null
+++ b/stress/kafka_test_result.json
@@ -0,0 +1,330 @@
+{
+  "root_group": {
+    "name": "",
+    "path": "",
+    "id": "d41d8cd98f00b204e9800998ecf8427e",
+    "groups": [],
+    "checks": [
+        {
+          "fails": 0,
+          "name": "product view status 200",
+          "path": "::product view status 200",
+          "id": "d7bcf489942c40269f55b32ca6ce4b46",
+          "passes": 20784
+        },
+        {
+          "name": "product view has data",
+          "path": "::product view has data",
+          "id": "87252c9e9c1b8992cfd921414e0b6022",
+          "passes": 20784,
+          "fails": 0
+        },
+        {
+          "path": "::like action status 200",
+          "id": "703d89a84372f3cd06c20ca212d785cd",
+          "passes": 5793,
+          "fails": 0,
+          "name": "like action status 200"
+        },
+        {
+          "passes": 0,
+          "fails": 5793,
+          "name": "like action success",
+          "path": "::like action success",
+          "id": "b761bcb03efbcded611b024c965a1987"
+        },
+        {
+          "passes": 118,
+          "fails": 22,
+          "name": "order creation status 200",
+          "path": "::order creation status 200",
+          "id": "618cd181f080fa12c8a21ca57866d4cd"
+        },
+        {
+          "passes": 140,
+          "fails": 0,
+          "name": "order has payment info",
+          "path": "::order has payment info",
+          "id": "6dc920b39e30a12a9783e986f5984d4c"
+        },
+        {
+          "id": "ce6654f5cd3e1fa102fd249859b7bd70",
+          "passes": 1,
+          "fails": 117,
+          "name": "payment callback status 200",
+          "path": "::payment callback status 200"
+        },
+        {
+          "path": "::payment callback success",
+          "id": "3e86da78f0eba1a92bab39b624bffb42",
+          "passes": 0,
+          "fails": 118,
+          "name": "payment callback success"
+        }
+      ]
+  },
+  "options": {
+    "summaryTimeUnit": "",
+    "noColor": false,
+    "summaryTrendStats": [
+      "avg",
+      "min",
+      "med",
+      "max",
+      "p(90)",
+      "p(95)"
+    ]
+  },
+  "state": {
+    "isStdOutTTY": true,
+    "isStdErrTTY": true,
+    "testRunDurationMs": 50116.796
+  },
+  "metrics": {
+    "like_action_success": {
+      "type": "rate",
+      "contains": "default",
+      "values": {
+        "rate": 0,
+        "passes": 0,
+        "fails": 5793
+      },
+      "thresholds": {
+        "rate>0.95": {
+          "ok": false
+        }
+      }
+    },
+    "data_received": {
+      "values": {
+        "count": 10863041,
+        "rate": 216754.4988310905
+      },
+      "type": "counter",
+      "contains": "data"
+    },
+    "iterations": {
+      "type": "counter",
+      "contains": "default",
+      "values": {
+        "count": 26847,
+        "rate": 535.6886741123674
+      }
+    },
+    "http_req_duration{expected_response:true}": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "p(95)": 16.176249999999996,
+        "avg": 11.192594583458256,
+        "min": 1.291,
+        "med": 5.9655000000000005,
+        "max": 2618.058,
+        "p(90)": 12.5615
+      }
+    },
+    "idempotency_check": {
+      "type": "rate",
+      "contains": "default",
+      "values": {
+        "fails": 130,
+        "rate": 0,
+        "passes": 0
+      }
+    },
+    "checks": {
+      "type": "rate",
+      "contains": "default",
+      "values": {
+        "passes": 47620,
+        "fails": 6050,
+        "rate": 0.8872740823551333
+      }
+    },
+    "iteration_duration": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "p(90)": 205.666992,
+        "p(95)": 207.953792,
+        "avg": 151.24222687860868,
+        "min": 101.416167,
+        "med": 107.799458,
+        "max": 4129.579875
+      }
+    },
+    "data_sent": {
+      "values": {
+        "count": 3551834,
+        "rate": 70871.13070835573
+      },
+      "type": "counter",
+      "contains": "data"
+    },
+    "vus_max": {
+      "type": "gauge",
+      "contains": "default",
+      "values": {
+        "value": 190,
+        "min": 190,
+        "max": 190
+      }
+    },
+    "http_req_duration": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "med": 5.951,
+        "max": 2618.058,
+        "p(90)": 12.5722,
+        "p(95)": 16.202,
+        "avg": 11.652998714416935,
+        "min": 1.132
+      },
+      "thresholds": {
+        "p(95)<2000": {
+          "ok": true
+        }
+      }
+    },
+    "payment_success": {
+      "values": {
+        "rate": 0,
+        "passes": 0,
+        "fails": 118
+      },
+      "thresholds": {
+        "rate>0.95": {
+          "ok": false
+        }
+      },
+      "type": "rate",
+      "contains": "default"
+    },
+    "http_reqs": {
+      "type": "counter",
+      "contains": "default",
+      "values": {
+        "count": 27225,
+        "rate": 543.2310557123404
+      }
+    },
+    "vus": {
+      "type": "gauge",
+      "contains": "default",
+      "values": {
+        "min": 4,
+        "max": 177,
+        "value": 10
+      }
+    },
+    "http_req_waiting": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "p(90)": 12.532200000000001,
+        "p(95)": 16.159399999999998,
+        "avg": 11.593664903581235,
+        "min": 1.089,
+        "med": 5.892,
+        "max": 2617.997
+      }
+    },
+    "http_req_receiving": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "min": 0.003,
+        "med": 0.038,
+        "max": 4.372,
+        "p(90)": 0.101,
+        "p(95)": 0.125,
+        "avg": 0.04995610651974213
+      }
+    },
+    "events_generated": {
+      "type": "counter",
+      "contains": "default",
+      "values": {
+        "count": 26695,
+        "rate": 532.6557587599973
+      }
+    },
+    "product_view_success": {
+      "type": "rate",
+      "contains": "default",
+      "values": {
+        "rate": 1,
+        "passes": 20784,
+        "fails": 0
+      },
+      "thresholds": {
+        "rate>0.95": {
+          "ok": true
+        }
+      }
+    },
+    "http_req_blocked": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "avg": 0.008632764003672995,
+        "min": 0,
+        "med": 0.002,
+        "max": 2.438,
+        "p(90)": 0.005,
+        "p(95)": 0.01
+      }
+    },
+    "http_req_connecting": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "med": 0,
+        "max": 2.208,
+        "p(90)": 0,
+        "p(95)": 0,
+        "avg": 0.004682644628099177,
+        "min": 0
+      }
+    },
+    "http_req_failed": {
+      "values": {
+        "rate": 0.019430670339761247,
+        "passes": 529,
+        "fails": 26696
+      },
+      "thresholds": {
+        "rate<0.05": {
+          "ok": true
+        }
+      },
+      "type": "rate",
+      "contains": "default"
+    },
+    "http_req_tls_handshaking": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "p(95)": 0,
+        "avg": 0,
+        "min": 0,
+        "med": 0,
+        "max": 0,
+        "p(90)": 0
+      }
+    },
+    "http_req_sending": {
+      "type": "trend",
+      "contains": "time",
+      "values": {
+        "p(90)": 0.016,
+        "p(95)": 0.023,
+        "avg": 0.009377704315884609,
+        "min": 0.001,
+        "med": 0.007,
+        "max": 1.162
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java b/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
index 4f8bda6f4..f3f11b298 100644
--- a/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
+++ b/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
@@ -1,15 +1,16 @@
 package com.loopers.config.jackson;
 
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.SerializationFeature;
 import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
 @Configuration
 class JacksonConfig {
 
@@ -23,25 +24,25 @@ public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
             // Serialization Features
             builder.serializationInclusion(JsonInclude.Include.NON_NULL);
             builder.featuresToEnable(
-                JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT,
-                JsonGenerator.Feature.IGNORE_UNKNOWN,
-                JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN
+                    JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT,
+                    JsonGenerator.Feature.IGNORE_UNKNOWN,
+                    JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN
             );
             builder.featuresToDisable(
-                SerializationFeature.FAIL_ON_EMPTY_BEANS
+                    SerializationFeature.FAIL_ON_EMPTY_BEANS
             );
 
             // Deserialization Features
             builder.featuresToEnable(
-                DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
-                DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY,
-                DeserializationFeature.READ_ENUMS_USING_TO_STRING,
-                DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES
+                    DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT,
+                    DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY,
+                    DeserializationFeature.READ_ENUMS_USING_TO_STRING,
+                    DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES
             );
             builder.featuresToDisable(
-                DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL,
-                DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,
-                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
+                    DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL,
+                    DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,
+                    DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
             );
         };
     }
diff --git a/supports/logging/src/main/resources/appenders/json-console-appender.xml b/supports/logging/src/main/resources/appenders/json-console-appender.xml
index d3b22c38b..0e3baf574 100644
--- a/supports/logging/src/main/resources/appenders/json-console-appender.xml
+++ b/supports/logging/src/main/resources/appenders/json-console-appender.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <included>
     <property name="CONSOLE_LOG_STRUCTURED_FORMAT" value="logstash"/>
-    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
-    <include resource="org/springframework/boot/logging/logback/structured-console-appender.xml" />
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+    <include resource="org/springframework/boot/logging/logback/structured-console-appender.xml"/>
 </included>
\ No newline at end of file
diff --git a/supports/logging/src/main/resources/appenders/plain-console-appender.xml b/supports/logging/src/main/resources/appenders/plain-console-appender.xml
index 162856618..a72fa102e 100644
--- a/supports/logging/src/main/resources/appenders/plain-console-appender.xml
+++ b/supports/logging/src/main/resources/appenders/plain-console-appender.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <included>
-    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
-    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
 </included>
\ No newline at end of file
diff --git a/supports/logging/src/main/resources/appenders/slack-appender.xml b/supports/logging/src/main/resources/appenders/slack-appender.xml
index a6175a185..bda543223 100644
--- a/supports/logging/src/main/resources/appenders/slack-appender.xml
+++ b/supports/logging/src/main/resources/appenders/slack-appender.xml
@@ -4,7 +4,8 @@
         <layout class="ch.qos.logback.classic.PatternLayout">
             <pattern>
                 *Application:* *`${appName:-}`*
-                *[%-5level]* *`%X{method:-}`* *`%X{requestUri:-}`* *Pod:* `${HOSTNAME:-}` *Trace ID:* `%X{traceId:-}` *Span ID:* `%X{spanId:-}` *Client IP:* `%X{clientIp:-}`%n%msg%n
+                *[%-5level]* *`%X{method:-}`* *`%X{requestUri:-}`* *Pod:* `${HOSTNAME:-}` *Trace ID:* `%X{traceId:-}`
+                *Span ID:* `%X{spanId:-}` *Client IP:* `%X{clientIp:-}`%n%msg%n
             </pattern>
         </layout>
         <webhookUri>${SLACK_WEBHOOK_URI}</webhookUri>
diff --git a/supports/monitoring/src/main/resources/monitoring.yml b/supports/monitoring/src/main/resources/monitoring.yml
index c6a87a9cf..be9eb4f52 100644
--- a/supports/monitoring/src/main/resources/monitoring.yml
+++ b/supports/monitoring/src/main/resources/monitoring.yml
@@ -30,8 +30,6 @@ management:
       enabled: true
     readinessState:
       enabled: true
-  server:
-    port: 8081
   observations:
     annotations:
       enabled: true
