diff --git a/README.md b/README.md
index 04950f29d..d8998daf6 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,8 @@ docker-compose -f ./docker/monitoring-compose.yml up
 Root
 ├── apps ( spring-applications )
 │   ├── 📦 commerce-api
-│   └── 📦 commerce-streamer
+│   ├── 📦 commerce-streamer
+│   └── 📦 pg-simulator
 ├── modules ( reusable-configurations )
 │   ├── 📦 jpa
 │   ├── 📦 redis
@@ -37,3 +38,25 @@ Root
     ├── 📦 monitoring
     └── 📦 logging
 ```
+
+## PG Payment System
+
+완전한 결제 시스템 구현 (Resilience 패턴 적용)
+
+### 빠른 시작
+```shell
+# PG Simulator 실행
+./gradlew :apps:pg-simulator:bootRun
+
+# Commerce API 실행
+./gradlew :apps:commerce-api:bootRun
+
+# 테스트
+curl -X POST http://localhost:8080/api/v1/payments/test/request \
+  -H "X-USER-ID: 12345" -H "Content-Type: application/json" \
+  -d '{"orderId":"ORDER-001","cardType":"SAMSUNG","cardNo":"1234-5678-9012-3456","amount":50000}'
+```
+
+### 문서
+- **PAYMENT-GUIDE.md**: 전체 가이드 (구현 사항, API, 테스트)
+- **test-payment-flow.http**: HTTP 테스트 시나리오
diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 7ba60d1b2..6a242d579 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -12,6 +12,11 @@ dependencies {
     implementation("org.springframework.boot:spring-boot-starter-actuator")
     implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
 
+    // resilience4j (Spring Boot 3.x 호환 버전)
+    implementation("io.github.resilience4j:resilience4j-spring-boot3:2.2.0")
+    implementation("io.github.resilience4j:resilience4j-micrometer:2.2.0")
+    implementation("org.springframework.boot:spring-boot-starter-aop")
+
     // querydsl
     annotationProcessor("com.querydsl:querydsl-apt::jakarta")
     annotationProcessor("jakarta.persistence:jakarta.persistence-api")
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
index 976c8f0db..e0df6e015 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
@@ -3,6 +3,7 @@
 import com.loopers.domain.common.vo.Money;
 import com.loopers.domain.order.Order;
 import com.loopers.domain.order.OrderItem;
+import com.loopers.domain.order.OrderStatus;
 import lombok.Builder;
 import lombok.Getter;
 
@@ -12,21 +13,25 @@
 @Getter
 @Builder
 public class OrderInfo {
-    
+
     private final Long id;
+    private final String orderNo;
     private final Long memberId;
+    private final OrderStatus status;
     private final Money totalPrice;
     private final List<OrderItemInfo> items;
     private final ZonedDateTime orderedAt;
-    
+
     public static OrderInfo from(Order order) {
         List<OrderItemInfo> itemInfos = order.getItems().stream()
                 .map(OrderItemInfo::from)
                 .toList();
-        
+
         return OrderInfo.builder()
                 .id(order.getId())
+                .orderNo(order.getOrderNo())
                 .memberId(order.getMemberId())
+                .status(order.getStatus())
                 .totalPrice(order.getTotalPrice())
                 .items(itemInfos)
                 .orderedAt(order.getCreatedAt())
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentCommand.java
new file mode 100644
index 000000000..77d5693be
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentCommand.java
@@ -0,0 +1,47 @@
+package com.loopers.application.payment;
+
+import com.loopers.domain.payment.CardType;
+
+public class PaymentCommand {
+
+    public record RequestPayment(
+            String orderId,
+            CardType cardType,
+            String cardNo,
+            Long amount,
+            String callbackUrl
+    ) {
+        public RequestPayment {
+            if (orderId == null || orderId.isBlank()) {
+                throw new IllegalArgumentException("주문 ID는 필수입니다.");
+            }
+            if (cardType == null) {
+                throw new IllegalArgumentException("카드 타입은 필수입니다.");
+            }
+            if (cardNo == null || cardNo.isBlank()) {
+                throw new IllegalArgumentException("카드 번호는 필수입니다.");
+            }
+            if (amount == null || amount <= 0) {
+                throw new IllegalArgumentException("결제 금액은 0보다 커야 합니다.");
+            }
+            if (callbackUrl == null || callbackUrl.isBlank()) {
+                throw new IllegalArgumentException("콜백 URL은 필수입니다.");
+            }
+        }
+    }
+
+    public record ProcessCallback(
+            String transactionKey,
+            String status,
+            String reason
+    ) {
+        public ProcessCallback {
+            if (transactionKey == null || transactionKey.isBlank()) {
+                throw new IllegalArgumentException("트랜잭션 키는 필수입니다.");
+            }
+            if (status == null || status.isBlank()) {
+                throw new IllegalArgumentException("상태는 필수입니다.");
+            }
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
new file mode 100644
index 000000000..3a4b55c78
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentFacade.java
@@ -0,0 +1,109 @@
+package com.loopers.application.payment;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.repository.OrderRepository;
+import com.loopers.domain.product.repository.ProductRepository;
+import com.loopers.domain.payment.Payment;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PaymentFacade {
+
+    private final PaymentService paymentService;
+    private final OrderRepository orderRepository;
+    private final ProductRepository productRepository;
+
+    @Transactional(propagation = Propagation.NOT_SUPPORTED)
+    public PaymentInfo requestPayment(String userId, PaymentCommand.RequestPayment command) {
+        return paymentService.requestPayment(userId, command);
+    }
+
+    @Transactional
+    public PaymentInfo processCallback(String userId, PaymentCommand.ProcessCallback command) {
+        PaymentInfo paymentInfo = paymentService.processCallback(userId, command);
+
+        if (paymentInfo.status() == com.loopers.domain.payment.PaymentStatus.SUCCESS) {
+            Order order = orderRepository.findByOrderNo(paymentInfo.orderNo())
+                    .orElseThrow(() -> new com.loopers.support.error.CoreException(
+                            com.loopers.support.error.ErrorType.NOT_FOUND,
+                            "주문을 찾을 수 없습니다: " + paymentInfo.orderNo()));
+            order.markAsPaid();
+            orderRepository.save(order);
+            log.info("[PaymentFacade] 결제 성공 및 주문 완료 - orderNo: {}", paymentInfo.orderNo());
+        } else if (paymentInfo.status() == com.loopers.domain.payment.PaymentStatus.FAILED) {
+            log.warn("[PaymentFacade] 결제 실패 - orderNo: {}", paymentInfo.orderNo());
+        }
+
+        return paymentInfo;
+    }
+
+    @Transactional
+    public PaymentInfo syncPaymentStatus(String userId, String transactionKey) {
+        PaymentInfo paymentInfo = paymentService.syncPaymentStatus(userId, transactionKey);
+
+        if (paymentInfo.status() == com.loopers.domain.payment.PaymentStatus.SUCCESS) {
+            Order order = orderRepository.findByOrderNo(paymentInfo.orderNo())
+                    .orElseThrow(() -> new com.loopers.support.error.CoreException(
+                            com.loopers.support.error.ErrorType.NOT_FOUND,
+                            "주문을 찾을 수 없습니다: " + paymentInfo.orderNo()));
+            if (!order.isPaid()) {
+                order.markAsPaid();
+                orderRepository.save(order);
+            }
+            log.info("[PaymentFacade] 동기화 완료 (성공) 및 주문 완료 - orderNo: {}", paymentInfo.orderNo());
+        } else if (paymentInfo.status() == com.loopers.domain.payment.PaymentStatus.FAILED) {
+            log.warn("[PaymentFacade] 동기화 완료 (실패) - orderNo: {}", paymentInfo.orderNo());
+        }
+
+        return paymentInfo;
+    }
+
+    @Transactional
+    public PaymentInfo cancelPayment(String userId, String orderNo, String reason) {
+        Payment payment = paymentService.findByOrderNo(orderNo);
+
+        if (payment.getStatus() == com.loopers.domain.payment.PaymentStatus.SUCCESS) {
+            throw new com.loopers.support.error.CoreException(
+                    com.loopers.support.error.ErrorType.BAD_REQUEST,
+                    "이미 성공한 결제는 취소할 수 없습니다.");
+        }
+
+        paymentService.cancelPayment(payment.getId(), reason);
+
+        Order order = payment.getOrder();
+        if (!order.isPaid()) {
+            order.cancel();
+            orderRepository.save(order);
+
+            order.getItems().forEach(item ->
+                    productRepository.increaseStock(item.getProductId(), item.getQuantity()));
+        }
+
+        log.info("[PaymentFacade] 결제 취소 완료 - orderNo: {}, reason: {}", orderNo, reason);
+        return PaymentInfo.from(paymentService.findById(payment.getId()));
+    }
+
+    @Transactional(readOnly = true)
+    public PaymentInfo getPaymentByOrderNo(String orderNo) {
+        return paymentService.getPaymentByOrderNo(orderNo);
+    }
+
+    @Transactional(readOnly = true)
+    public List<PaymentInfo> getPendingPaymentsOlderThan(LocalDateTime dateTime) {
+        return paymentService.getPendingPaymentsOlderThan(dateTime);
+    }
+
+    @Transactional(readOnly = true)
+    public List<PaymentInfo> getPaymentsRequiringRetry() {
+        return paymentService.getPaymentsRequiringRetry();
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java
new file mode 100644
index 000000000..94eb18f10
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentInfo.java
@@ -0,0 +1,35 @@
+package com.loopers.application.payment;
+
+import com.loopers.domain.payment.CardType;
+import com.loopers.domain.payment.Payment;
+import com.loopers.domain.payment.PaymentStatus;
+
+import java.time.ZonedDateTime;
+
+public record PaymentInfo(
+        Long id,
+        String orderNo,
+        String transactionKey,
+        PaymentStatus status,
+        CardType cardType,
+        String cardNo,
+        Long amount,
+        String reason,
+        boolean requiresRetry,
+        ZonedDateTime createdAt
+) {
+    public static PaymentInfo from(Payment payment) {
+        return new PaymentInfo(
+                payment.getId(),
+                payment.getOrder().getOrderNo(),
+                payment.getTransactionKey(),
+                payment.getStatus(),
+                payment.getCardType(),
+                payment.getCardNo(),
+                payment.getAmount(),
+                payment.getReason(),
+                payment.isRequiresRetry(),
+                payment.getCreatedAt()
+        );
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java
new file mode 100644
index 000000000..fec99eb48
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java
@@ -0,0 +1,209 @@
+package com.loopers.application.payment;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.repository.OrderRepository;
+import com.loopers.domain.payment.Payment;
+import com.loopers.domain.payment.PaymentRepository;
+import com.loopers.domain.payment.PaymentStatus;
+import com.loopers.domain.payment.gateway.PgGateway;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class PaymentService {
+
+    private final PaymentRepository paymentRepository;
+    private final OrderRepository orderRepository;
+    private final PgGateway pgGateway;
+
+    @Transactional
+    public Payment createPayment(String userId, PaymentCommand.RequestPayment command) {
+        log.info("[Payment] 결제 요청 시작 - orderNo: {}, amount: {}", command.orderId(), command.amount());
+
+        Order order = orderRepository.findByOrderNo(command.orderId())
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "주문을 찾을 수 없습니다: " + command.orderId()));
+
+        if (!order.isPendingPayment()) {
+            log.warn("[Payment] 결제 대기 상태가 아닌 주문 - orderNo: {}, status: {}",
+                    order.getOrderNo(), order.getStatus());
+            throw new CoreException(ErrorType.BAD_REQUEST,
+                    "결제 대기 상태가 아닌 주문입니다: " + order.getOrderNo());
+        }
+
+        if (paymentRepository.existsByOrder(order)) {
+            log.warn("[Payment] 중복된 결제 요청 - orderNo: {}", order.getOrderNo());
+            throw new CoreException(ErrorType.CONFLICT, "이미 처리 중인 결제입니다: " + order.getOrderNo());
+        }
+
+        Payment payment = Payment.create(
+                order,
+                command.cardType(),
+                command.cardNo(),
+                command.amount(),
+                command.callbackUrl()
+        );
+
+        try {
+            payment = paymentRepository.save(payment);
+        } catch (org.springframework.dao.DataIntegrityViolationException e) {
+            log.warn("[Payment] 동시 요청으로 인한 중복 - orderNo: {}", order.getOrderNo());
+            throw new CoreException(ErrorType.CONFLICT, "이미 처리 중인 결제입니다: " + order.getOrderNo());
+        }
+        log.info("[Payment] Payment 엔티티 생성 - id: {}, orderNo: {}", payment.getId(), order.getOrderNo());
+        return payment;
+    }
+
+    public PaymentInfo requestPayment(String userId, PaymentCommand.RequestPayment command) {
+        Payment payment = createPayment(userId, command);
+
+        PgGateway.PgPaymentCommand pgCommand = new PgGateway.PgPaymentCommand(
+                command.orderId(),
+                command.cardType(),
+                command.cardNo(),
+                command.amount(),
+                command.callbackUrl()
+        );
+
+        try {
+            CompletableFuture<PgGateway.PgPaymentResult> future = pgGateway.requestPayment(userId, pgCommand);
+            PgGateway.PgPaymentResult pgResult = future.get(2, TimeUnit.SECONDS);
+            applyPgResult(payment.getId(), pgResult);
+        } catch (Exception e) {
+            log.error("[Payment] PG 결제 요청 실패 - orderNo: {}", command.orderId(), e);
+            markRequiresRetry(payment.getId());
+        }
+
+        return PaymentInfo.from(findById(payment.getId()));
+    }
+
+    @Transactional
+    public PaymentInfo processCallback(String userId, PaymentCommand.ProcessCallback command) {
+        log.info("[Payment] 콜백 처리 시작 - transactionKey: {}, status: {}",
+                command.transactionKey(), command.status());
+
+        Payment payment = paymentRepository.findByTransactionKey(command.transactionKey())
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "결제 정보를 찾을 수 없습니다: " + command.transactionKey()));
+
+        PaymentStatus pgStatus = PaymentStatus.valueOf(command.status());
+        payment.updateFromPg(pgStatus, command.reason());
+
+        log.info("[Payment] 콜백 처리 완료 - orderNo: {}, status: {}",
+                payment.getOrder().getOrderNo(), payment.getStatus());
+
+        return PaymentInfo.from(payment);
+    }
+
+    public PaymentInfo syncPaymentStatus(String userId, String transactionKey) {
+        log.info("[Payment] 상태 동기화 시작 - transactionKey: {}", transactionKey);
+
+        Payment payment = paymentRepository.findByTransactionKey(transactionKey)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "결제 정보를 찾을 수 없습니다: " + transactionKey));
+
+        try {
+            PgGateway.PgPaymentDetail pgDetail = pgGateway.getPaymentStatus(userId, transactionKey);
+            PaymentStatus pgStatus = PaymentStatus.valueOf(pgDetail.status().name());
+            applyPgDetail(payment.getId(), pgStatus, pgDetail.reason());
+            log.info("[Payment] 상태 동기화 완료 - orderNo: {}, status: {}",
+                    payment.getOrder().getOrderNo(), pgStatus);
+        } catch (Exception e) {
+            log.error("[Payment] 상태 조회 실패 - transactionKey: {}", transactionKey, e);
+        }
+
+        return PaymentInfo.from(findById(payment.getId()));
+    }
+
+    public PaymentInfo getPaymentByOrderNo(String orderNo) {
+        Order order = orderRepository.findByOrderNo(orderNo)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "주문을 찾을 수 없습니다: " + orderNo));
+        Payment payment = paymentRepository.findByOrder(order)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "결제 정보를 찾을 수 없습니다: " + orderNo));
+        return PaymentInfo.from(payment);
+    }
+
+    public List<PaymentInfo> getPendingPaymentsOlderThan(LocalDateTime dateTime) {
+        return paymentRepository.findPendingPaymentsOlderThan(dateTime)
+                .stream()
+                .map(PaymentInfo::from)
+                .toList();
+    }
+
+    public List<PaymentInfo> getPaymentsRequiringRetry() {
+        return paymentRepository.findByRequiresRetryTrue()
+                .stream()
+                .map(PaymentInfo::from)
+                .toList();
+    }
+
+    @Transactional(readOnly = true)
+    public Payment findByOrderNo(String orderNo) {
+        Order order = orderRepository.findByOrderNo(orderNo)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "주문을 찾을 수 없습니다: " + orderNo));
+        return paymentRepository.findByOrder(order)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "결제 정보를 찾을 수 없습니다: " + orderNo));
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public Payment applyPgResult(Long paymentId, PgGateway.PgPaymentResult pgResult) {
+        Payment payment = findById(paymentId);
+
+        if (pgResult.transactionKey() != null) {
+            if (!payment.hasTransactionKey()) {
+                payment.assignTransactionKey(pgResult.transactionKey());
+                log.info("[Payment] PG 결제 요청 성공 - transactionKey: {}", pgResult.transactionKey());
+            } else {
+                log.info("[Payment] PG 결제 요청 성공(기존 key 유지) - transactionKey: {}", payment.getTransactionKey());
+            }
+        } else {
+            payment.markAsRequiresRetry();
+            log.warn("[Payment] PG 장애로 재시도 필요 - orderNo: {}", payment.getOrder().getOrderNo());
+        }
+        return payment;
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public Payment applyPgDetail(Long paymentId, PaymentStatus pgStatus, String reason) {
+        Payment payment = findById(paymentId);
+        payment.updateFromPg(pgStatus, reason);
+        return payment;
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public Payment markRequiresRetry(Long paymentId) {
+        Payment payment = findById(paymentId);
+        payment.markAsRequiresRetry();
+        return payment;
+    }
+
+    @Transactional(readOnly = true)
+    public Payment findById(Long paymentId) {
+        return paymentRepository.findById(paymentId)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                        "결제 정보를 찾을 수 없습니다: id=" + paymentId));
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public Payment cancelPayment(Long paymentId, String reason) {
+        Payment payment = findById(paymentId);
+        payment.markAsCancelled(reason);
+        return payment;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentStatusSyncScheduler.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentStatusSyncScheduler.java
new file mode 100644
index 000000000..0e378bbbf
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentStatusSyncScheduler.java
@@ -0,0 +1,110 @@
+package com.loopers.application.payment;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class PaymentStatusSyncScheduler {
+
+    private final PaymentFacade paymentFacade;
+
+    @Scheduled(fixedDelay = 60000, initialDelay = 60000)
+    public void syncPendingPayments() {
+        log.info("[PaymentSync] PENDING 결제 동기화 시작");
+
+        try {
+            LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(10);
+            List<PaymentInfo> pendingPayments = paymentFacade.getPendingPaymentsOlderThan(cutoffTime);
+
+            if (pendingPayments.isEmpty()) {
+                log.debug("[PaymentSync] 동기화할 PENDING 결제 없음");
+                return;
+            }
+
+            log.info("[PaymentSync] PENDING 결제 {}건 발견, 동기화 시작", pendingPayments.size());
+
+            int successCount = 0;
+            int failureCount = 0;
+
+            for (PaymentInfo paymentInfo : pendingPayments) {
+                try {
+                    if (paymentInfo.transactionKey() == null || paymentInfo.transactionKey().isEmpty()) {
+                        if (paymentInfo.requiresRetry()) {
+                            log.warn("[PaymentSync] transactionKey 없음 + 재시도 필요, 취소 처리 - orderNo: {}", paymentInfo.orderNo());
+                            paymentFacade.cancelPayment(
+                                    "SCHEDULER",
+                                    paymentInfo.orderNo(),
+                                    "PG 요청 실패로 자동 취소");
+                        } else {
+                            log.warn("[PaymentSync] transactionKey 없음, 스킵 - orderNo: {}", paymentInfo.orderNo());
+                        }
+                        continue;
+                    }
+
+                    PaymentInfo syncedPayment = paymentFacade.syncPaymentStatus(
+                            "SCHEDULER",
+                            paymentInfo.transactionKey()
+                    );
+
+                    log.info("[PaymentSync] 동기화 완료 - orderNo: {}, status: {} -> {}",
+                            paymentInfo.orderNo(),
+                            paymentInfo.status(),
+                            syncedPayment.status());
+
+                    successCount++;
+
+                } catch (Exception e) {
+                    log.error("[PaymentSync] 동기화 실패 - orderNo: {}", paymentInfo.orderNo(), e);
+                    failureCount++;
+                }
+            }
+
+            log.info("[PaymentSync] PENDING 결제 동기화 완료 - 성공: {}, 실패: {}", successCount, failureCount);
+
+        } catch (Exception e) {
+            log.error("[PaymentSync] PENDING 결제 동기화 오류", e);
+        }
+    }
+
+    @Scheduled(fixedDelay = 300000, initialDelay = 120000)
+    public void processRetryPayments() {
+        log.info("[PaymentRetry] 재시도 필요 결제 처리 시작");
+
+        try {
+            List<PaymentInfo> retryPayments = paymentFacade.getPaymentsRequiringRetry();
+
+            if (retryPayments.isEmpty()) {
+                log.debug("[PaymentRetry] 재시도 필요 결제 없음");
+                return;
+            }
+
+            log.info("[PaymentRetry] 재시도 필요 결제 {}건 발견", retryPayments.size());
+
+            for (PaymentInfo paymentInfo : retryPayments) {
+                try {
+                    if (paymentInfo.createdAt().isBefore(java.time.ZonedDateTime.now().minusMinutes(15))) {
+                        log.warn("[PaymentRetry] 재시도 한도 초과, 자동 취소 - orderNo: {}", paymentInfo.orderNo());
+                        paymentFacade.cancelPayment(
+                                "SCHEDULER",
+                                paymentInfo.orderNo(),
+                                "PG 재요청 실패 - 타임아웃 초과");
+                    } else {
+                        log.warn("[PaymentRetry] 수동 처리 필요 - orderNo: {}, 관리자 확인 필요", paymentInfo.orderNo());
+                    }
+                } catch (Exception e) {
+                    log.error("[PaymentRetry] 재시도 결제 처리 오류 - orderNo: {}", paymentInfo.orderNo(), e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("[PaymentRetry] 재시도 결제 처리 오류", e);
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/config/RestTemplateConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/RestTemplateConfig.java
new file mode 100644
index 000000000..6b3d55383
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/config/RestTemplateConfig.java
@@ -0,0 +1,23 @@
+package com.loopers.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate pgRestTemplate(RestTemplateBuilder builder) {
+        // PG 전용 RestTemplate - 더 짧은 타임아웃 설정
+        return builder
+                .setConnectTimeout(Duration.ofSeconds(1))   // PG 연결 타임아웃: 1초
+                .setReadTimeout(Duration.ofMillis(1800))    // PG 읽기 타임아웃: TimeLimiter(2s)보다 짧게
+                .requestFactory(SimpleClientHttpRequestFactory.class)
+                .build();
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
index e82817173..1f9a1aab5 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
@@ -14,13 +14,24 @@
 
 @NoArgsConstructor
 @Entity
-@Table(name = "orders")
+@Table(name = "orders", indexes = {
+        @Index(name = "idx_order_no", columnList = "orderNo")
+})
 public class Order extends BaseEntity {
 
+    @Getter
+    @Column(name = "order_no", nullable = false, unique = true, length = 100)
+    private String orderNo;
+
     @Getter
     @Column(name = "member_id", nullable = false)
     private Long memberId;
 
+    @Getter
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false, length = 20)
+    private OrderStatus status;
+
     @Getter
     @Embedded
     @AttributeOverride(name = "amount", column = @Column(name = "total_price"))
@@ -29,12 +40,14 @@ public class Order extends BaseEntity {
     @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
     private List<OrderItem> items = new ArrayList<>();
 
-    private Order(Long memberId, List<OrderItem> items, Money totalPrice) {
+    private Order(String orderNo, Long memberId, List<OrderItem> items, Money totalPrice, OrderStatus status) {
         validateMemberId(memberId);
         validateItems(items);
         validateTotalPrice(totalPrice);
 
+        this.orderNo = orderNo;
         this.memberId = memberId;
+        this.status = status;
         this.totalPrice = totalPrice;
         this.items = new ArrayList<>();
         items.forEach(this::addItem);
@@ -44,14 +57,20 @@ public static Order create(Long memberId, List<OrderItem> items) {
         validateMemberId(memberId);
         validateItems(items);
         Money totalPrice = calculateTotalPrice(items);
-        return new Order(memberId, items, totalPrice);
+        String orderNo = generateOrderNo();
+        return new Order(orderNo, memberId, items, totalPrice, OrderStatus.PENDING_PAYMENT);
     }
 
     public static Order create(Long memberId, List<OrderItem> items, Money finalPrice) {
         validateMemberId(memberId);
         validateItems(items);
         validateTotalPrice(finalPrice);
-        return new Order(memberId, items, finalPrice);
+        String orderNo = generateOrderNo();
+        return new Order(orderNo, memberId, items, finalPrice, OrderStatus.PENDING_PAYMENT);
+    }
+
+    private static String generateOrderNo() {
+        return "ORD-" + java.util.UUID.randomUUID().toString().substring(0, 8).toUpperCase();
     }
 
     private void addItem(OrderItem item) {
@@ -86,4 +105,29 @@ private static void validateTotalPrice(Money totalPrice) {
             throw new CoreException(ErrorType.BAD_REQUEST, "총 금액은 필수입니다.");
         }
     }
+
+    public void markAsPaid() {
+        if (this.status == OrderStatus.PAID) {
+            return;
+        }
+        if (this.status == OrderStatus.CANCELLED) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "취소된 주문은 결제 완료로 변경할 수 없습니다.");
+        }
+        this.status = OrderStatus.PAID;
+    }
+
+    public void cancel() {
+        if (this.status == OrderStatus.PAID) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "결제 완료된 주문은 취소할 수 없습니다.");
+        }
+        this.status = OrderStatus.CANCELLED;
+    }
+
+    public boolean isPendingPayment() {
+        return this.status == OrderStatus.PENDING_PAYMENT;
+    }
+
+    public boolean isPaid() {
+        return this.status == OrderStatus.PAID;
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
new file mode 100644
index 000000000..25eef070e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
@@ -0,0 +1,7 @@
+package com.loopers.domain.order;
+
+public enum OrderStatus {
+    PENDING_PAYMENT,
+    PAID,
+    CANCELLED
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/repository/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/repository/OrderRepository.java
index bb1912a8c..c00305e04 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/repository/OrderRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/repository/OrderRepository.java
@@ -5,8 +5,10 @@
 import java.util.Optional;
 
 public interface OrderRepository {
-    
+
     Optional<Order> findById(Long id);
-    
+
+    Optional<Order> findByOrderNo(String orderNo);
+
     Order save(Order order);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/service/OrderPlacementService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/service/OrderPlacementService.java
index 05bab573f..8f41463c1 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/service/OrderPlacementService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/service/OrderPlacementService.java
@@ -9,8 +9,6 @@
 import com.loopers.domain.order.command.OrderLineCommand;
 import com.loopers.domain.order.command.OrderPlacementCommand;
 import com.loopers.domain.order.repository.OrderRepository;
-import com.loopers.domain.points.Point;
-import com.loopers.domain.points.repository.PointRepository;
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.repository.ProductRepository;
 import com.loopers.support.error.CoreException;
@@ -28,7 +26,6 @@ public class OrderPlacementService {
     private final OrderRepository orderRepository;
     private final ProductRepository productRepository;
     private final MemberRepository memberRepository;
-    private final PointRepository pointRepository;
     private final MemberCouponRepository memberCouponRepository;
 
     public Order placeOrder(OrderPlacementCommand command) {
@@ -54,10 +51,6 @@ public Order placeOrder(OrderPlacementCommand command) {
             finalPrice = Money.zero();
         }
 
-        if (finalPrice.getAmount().compareTo(java.math.BigDecimal.ZERO) > 0) {
-            payWithPoints(command.getMemberId(), finalPrice);
-        }
-
         if (memberCoupon != null) {
             memberCoupon.use();
             memberCouponRepository.save(memberCoupon);
@@ -100,16 +93,4 @@ private void validateMemberExists(Long memberId) {
             throw new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다.");
         }
     }
-
-    private void payWithPoints(Long memberId, Money totalPrice) {
-        Point point = pointRepository.findByMemberIdForUpdate(memberId)
-                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "포인트 정보를 찾을 수 없습니다."));
-
-        if (!point.canAfford(totalPrice.getAmount())) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "포인트가 부족합니다.");
-        }
-
-        point.pay(totalPrice.getAmount());
-        pointRepository.save(point);
-    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/CardType.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/CardType.java
new file mode 100644
index 000000000..45d76b65d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/CardType.java
@@ -0,0 +1,7 @@
+package com.loopers.domain.payment;
+
+public enum CardType {
+    SAMSUNG,
+    KB,
+    HYUNDAI
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
new file mode 100644
index 000000000..87a47350b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
@@ -0,0 +1,151 @@
+package com.loopers.domain.payment;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.domain.order.Order;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "payments",
+    indexes = {
+        @Index(name = "idx_transaction_key", columnList = "transactionKey"),
+        @Index(name = "idx_status_created_at", columnList = "status,createdAt")
+    }
+)
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Payment extends BaseEntity {
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "order_id", nullable = false, unique = true)
+    private Order order;
+
+    @Column(length = 100)
+    private String transactionKey;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false, length = 20)
+    private PaymentStatus status;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false, length = 20)
+    private CardType cardType;
+
+    @Column(nullable = false, length = 50)
+    private String cardNo;
+
+    @Column(nullable = false)
+    private Long amount;
+
+    @Column(length = 500)
+    private String reason;
+
+    @Column(length = 500)
+    private String callbackUrl;
+
+    @Column(nullable = false)
+    private boolean requiresRetry = false;
+
+    private LocalDateTime lastCheckedAt;
+
+    public static Payment create(
+            Order order,
+            CardType cardType,
+            String cardNo,
+            Long amount,
+            String callbackUrl
+    ) {
+        Payment payment = new Payment();
+        payment.order = order;
+        payment.cardType = cardType;
+        payment.cardNo = maskCardNo(cardNo);
+        payment.amount = amount;
+        payment.callbackUrl = callbackUrl;
+        payment.status = PaymentStatus.PENDING;
+        payment.requiresRetry = false;
+        return payment;
+    }
+
+    public void assignTransactionKey(String transactionKey) {
+        if (this.transactionKey != null) {
+            throw new IllegalStateException("이미 transactionKey가 할당되었습니다.");
+        }
+        this.transactionKey = transactionKey;
+    }
+
+    public void markAsSuccess() {
+        if (this.status == PaymentStatus.SUCCESS) {
+            return;
+        }
+        this.status = PaymentStatus.SUCCESS;
+        this.requiresRetry = false;
+        this.lastCheckedAt = LocalDateTime.now();
+    }
+
+    public void markAsFailed(String reason) {
+        if (this.status == PaymentStatus.SUCCESS) {
+            throw new IllegalStateException("이미 성공한 결제는 실패로 변경할 수 없습니다.");
+        }
+        this.status = PaymentStatus.FAILED;
+        this.reason = reason;
+        this.requiresRetry = false;
+        this.lastCheckedAt = LocalDateTime.now();
+    }
+
+    public void markAsRequiresRetry() {
+        this.requiresRetry = true;
+        this.lastCheckedAt = LocalDateTime.now();
+    }
+
+    public void markAsCancelled(String reason) {
+        if (this.status == PaymentStatus.SUCCESS) {
+            throw new IllegalStateException("이미 성공한 결제는 취소할 수 없습니다.");
+        }
+        this.status = PaymentStatus.CANCELLED;
+        this.reason = reason;
+        this.requiresRetry = false;
+        this.lastCheckedAt = LocalDateTime.now();
+    }
+
+    public void updateFromPg(PaymentStatus pgStatus, String reason) {
+        this.lastCheckedAt = LocalDateTime.now();
+
+        switch (pgStatus) {
+            case SUCCESS -> markAsSuccess();
+            case FAILED -> markAsFailed(reason);
+            case CANCELLED -> markAsCancelled(reason);
+            case PENDING -> this.status = PaymentStatus.PENDING;
+        }
+    }
+
+    public boolean isPending() {
+        return status == PaymentStatus.PENDING;
+    }
+
+    public boolean isSuccess() {
+        return status == PaymentStatus.SUCCESS;
+    }
+
+    public boolean isFailed() {
+        return status == PaymentStatus.FAILED;
+    }
+
+    public boolean hasTransactionKey() {
+        return transactionKey != null && !transactionKey.isEmpty();
+    }
+
+    public boolean isOlderThan(LocalDateTime dateTime) {
+        return getCreatedAt().isBefore(dateTime.atZone(getCreatedAt().getZone()));
+    }
+
+    private static String maskCardNo(String cardNo) {
+        if (cardNo == null || cardNo.length() < 4) {
+            return "****-****-****-****";
+        }
+        return "****-****-****-" + cardNo.substring(cardNo.length() - 4);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java
new file mode 100644
index 000000000..2c18eea0d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentRepository.java
@@ -0,0 +1,24 @@
+package com.loopers.domain.payment;
+
+import com.loopers.domain.order.Order;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+public interface PaymentRepository {
+
+    Payment save(Payment payment);
+
+    Optional<Payment> findById(Long id);
+
+    Optional<Payment> findByOrder(Order order);
+
+    Optional<Payment> findByTransactionKey(String transactionKey);
+
+    boolean existsByOrder(Order order);
+
+    List<Payment> findPendingPaymentsOlderThan(LocalDateTime dateTime);
+
+    List<Payment> findByRequiresRetryTrue();
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
new file mode 100644
index 000000000..e77f2f789
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
@@ -0,0 +1,23 @@
+package com.loopers.domain.payment;
+
+public enum PaymentStatus {
+    /**
+     * 결제 진행 중 (PG 처리 대기)
+     */
+    PENDING,
+
+    /**
+     * 결제 성공
+     */
+    SUCCESS,
+
+    /**
+     * 결제 실패 (한도초과, 잘못된 카드 등)
+     */
+    FAILED,
+
+    /**
+     * 결제 취소 (사용자/시스템에 의해 중단)
+     */
+    CANCELLED
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/gateway/PgGateway.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/gateway/PgGateway.java
new file mode 100644
index 000000000..d8fc2c487
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/gateway/PgGateway.java
@@ -0,0 +1,48 @@
+package com.loopers.domain.payment.gateway;
+
+import com.loopers.domain.payment.CardType;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface PgGateway {
+
+    CompletableFuture<PgPaymentResult> requestPayment(
+            String userId,
+            PgPaymentCommand command
+    );
+
+    PgPaymentDetail getPaymentStatus(
+            String userId,
+            String transactionKey
+    );
+
+    record PgPaymentCommand(
+            String orderId,
+            CardType cardType,
+            String cardNo,
+            Long amount,
+            String callbackUrl
+    ) {}
+
+    record PgPaymentResult(
+            String transactionKey,
+            PgTransactionStatus status,
+            String message
+    ) {}
+
+    record PgPaymentDetail(
+            String transactionKey,
+            String orderId,
+            CardType cardType,
+            String cardNo,
+            Long amount,
+            PgTransactionStatus status,
+            String reason
+    ) {}
+
+    enum PgTransactionStatus {
+        PENDING,
+        SUCCESS,
+        FAILED
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/repository/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/repository/ProductRepository.java
index 0461b9dd4..864baeb68 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/repository/ProductRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/repository/ProductRepository.java
@@ -22,6 +22,8 @@ public interface ProductRepository {
 
     int decreaseStock(Long productId, int quantity);
 
+    int increaseStock(Long productId, int quantity);
+
     int incrementLikeCount(Long productId);
 
     int decrementLikeCount(Long productId);
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
index 512c7b640..76b479d8c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
@@ -4,6 +4,10 @@
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
+import java.util.Optional;
+
 @Repository
 public interface OrderJpaRepository extends JpaRepository<Order, Long> {
+
+    Optional<Order> findByOrderNo(String orderNo);
 }
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
index 1461c9d99..35cc3adfd 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
@@ -18,6 +18,11 @@ public Optional<Order> findById(Long id) {
         return orderJpaRepository.findById(id);
     }
 
+    @Override
+    public Optional<Order> findByOrderNo(String orderNo) {
+        return orderJpaRepository.findByOrderNo(orderNo);
+    }
+
     @Override
     public Order save(Order order) {
         return orderJpaRepository.save(order);
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java
new file mode 100644
index 000000000..c549a1d54
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentJpaRepository.java
@@ -0,0 +1,29 @@
+package com.loopers.infrastructure.payment;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.payment.Payment;
+import com.loopers.domain.payment.PaymentStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+
+public interface PaymentJpaRepository extends JpaRepository<Payment, Long> {
+
+    Optional<Payment> findByOrder(Order order);
+
+    Optional<Payment> findByTransactionKey(String transactionKey);
+
+    boolean existsByOrder(Order order);
+
+    @Query("SELECT p FROM Payment p WHERE p.status = :status AND p.createdAt < :dateTime")
+    List<Payment> findByStatusAndCreatedAtBefore(
+            @Param("status") PaymentStatus status,
+            @Param("dateTime") ZonedDateTime dateTime
+    );
+
+    List<Payment> findByRequiresRetryTrue();
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java
new file mode 100644
index 000000000..dcdf40f5f
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentRepositoryImpl.java
@@ -0,0 +1,57 @@
+package com.loopers.infrastructure.payment;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.payment.Payment;
+import com.loopers.domain.payment.PaymentRepository;
+import com.loopers.domain.payment.PaymentStatus;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class PaymentRepositoryImpl implements PaymentRepository {
+
+    private final PaymentJpaRepository jpaRepository;
+
+    @Override
+    public Payment save(Payment payment) {
+        return jpaRepository.save(payment);
+    }
+
+    @Override
+    public Optional<Payment> findById(Long id) {
+        return jpaRepository.findById(id);
+    }
+
+    @Override
+    public Optional<Payment> findByOrder(Order order) {
+        return jpaRepository.findByOrder(order);
+    }
+
+    @Override
+    public Optional<Payment> findByTransactionKey(String transactionKey) {
+        return jpaRepository.findByTransactionKey(transactionKey);
+    }
+
+    @Override
+    public boolean existsByOrder(Order order) {
+        return jpaRepository.existsByOrder(order);
+    }
+
+    @Override
+    public List<Payment> findPendingPaymentsOlderThan(LocalDateTime dateTime) {
+        ZonedDateTime zonedDateTime = dateTime.atZone(ZoneId.systemDefault());
+        return jpaRepository.findByStatusAndCreatedAtBefore(PaymentStatus.PENDING, zonedDateTime);
+    }
+
+    @Override
+    public List<Payment> findByRequiresRetryTrue() {
+        return jpaRepository.findByRequiresRetryTrue();
+    }
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentClient.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentClient.java
new file mode 100644
index 000000000..cffe29250
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentClient.java
@@ -0,0 +1,190 @@
+package com.loopers.infrastructure.payment.pg;
+
+import com.loopers.domain.payment.CardType;
+import com.loopers.domain.payment.gateway.PgGateway;
+import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
+import io.github.resilience4j.retry.annotation.Retry;
+import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+@Component
+public class PgPaymentClient implements PgGateway {
+
+    private final RestTemplate pgRestTemplate;
+
+    @Value("${payment.pg.base-url:http://localhost:8082}")
+    private String pgBaseUrl;
+
+    private static final String PG_INSTANCE = "pgSimulator";
+
+    public PgPaymentClient(RestTemplate pgRestTemplate) {
+        this.pgRestTemplate = pgRestTemplate;
+    }
+
+    @Override
+    @CircuitBreaker(name = PG_INSTANCE, fallbackMethod = "requestPaymentFallback")
+    @Retry(name = PG_INSTANCE)
+    @TimeLimiter(name = PG_INSTANCE)
+    public CompletableFuture<PgPaymentResult> requestPayment(String userId, PgPaymentCommand command) {
+        return CompletableFuture.supplyAsync(() -> {
+            log.info("[PG] 결제 요청 시작 - orderId: {}, amount: {}", command.orderId(), command.amount());
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId);
+            headers.set("Content-Type", "application/json");
+
+            PgPaymentRequest request = new PgPaymentRequest(
+                    command.orderId(),
+                    toPgCardType(command.cardType()),
+                    command.cardNo(),
+                    command.amount(),
+                    command.callbackUrl()
+            );
+
+            HttpEntity<PgPaymentRequest> entity = new HttpEntity<>(request, headers);
+
+            try {
+                String url = pgBaseUrl + "/api/v1/payments";
+                ResponseEntity<PgApiResponse<PgPaymentResponse>> response = pgRestTemplate.exchange(
+                        url,
+                        HttpMethod.POST,
+                        entity,
+                        new org.springframework.core.ParameterizedTypeReference<>() {}
+                );
+
+                if (response.getBody() != null && response.getBody().data() != null) {
+                    PgPaymentResponse paymentResponse = response.getBody().data();
+                    log.info("[PG] 결제 요청 성공 - transactionKey: {}, status: {}",
+                            paymentResponse.transactionKey(), paymentResponse.status());
+                    return new PgPaymentResult(
+                            paymentResponse.transactionKey(),
+                            toPgTransactionStatus(paymentResponse.status()),
+                            paymentResponse.reason()
+                    );
+                } else {
+                    throw new PgCommunicationException("PG 응답이 비어있습니다.");
+                }
+            } catch (RestClientException e) {
+                log.error("[PG] 결제 요청 실패 - orderId: {}", command.orderId(), e);
+                throw new PgCommunicationException("PG 통신 실패", e);
+            }
+        });
+    }
+
+    @Override
+    @CircuitBreaker(name = PG_INSTANCE, fallbackMethod = "getPaymentStatusFallback")
+    @Retry(name = PG_INSTANCE)
+    public PgPaymentDetail getPaymentStatus(String userId, String transactionKey) {
+        log.info("[PG] 결제 상태 조회 - transactionKey: {}", transactionKey);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-USER-ID", userId);
+
+        HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+        try {
+            String url = pgBaseUrl + "/api/v1/payments/" + transactionKey;
+            ResponseEntity<PgApiResponse<PgPaymentDetailResponse>> response = pgRestTemplate.exchange(
+                    url,
+                    HttpMethod.GET,
+                    entity,
+                    new org.springframework.core.ParameterizedTypeReference<>() {}
+            );
+
+            if (response.getBody() != null && response.getBody().data() != null) {
+                PgPaymentDetailResponse detailResponse = response.getBody().data();
+                log.info("[PG] 결제 상태 조회 성공 - transactionKey: {}, status: {}",
+                        transactionKey, detailResponse.status());
+                return new PgPaymentDetail(
+                        detailResponse.transactionKey(),
+                        detailResponse.orderId(),
+                        toCardType(detailResponse.cardType()),
+                        detailResponse.cardNo(),
+                        detailResponse.amount(),
+                        toPgTransactionStatus(detailResponse.status()),
+                        detailResponse.reason()
+                );
+            } else {
+                throw new PgCommunicationException("PG 응답이 비어있습니다.");
+            }
+        } catch (RestClientException e) {
+            log.error("[PG] 결제 상태 조회 실패 - transactionKey: {}", transactionKey, e);
+            throw new PgCommunicationException("PG 통신 실패", e);
+        }
+    }
+
+    private CompletableFuture<PgPaymentResult> requestPaymentFallback(
+            String userId,
+            PgPaymentCommand command,
+            Exception ex
+    ) {
+        log.error("[PG Fallback] 결제 요청 실패 - orderId: {}, error: {}",
+                command.orderId(), ex.getMessage());
+
+        PgPaymentResult fallbackResponse = new PgPaymentResult(
+                null,
+                PgTransactionStatus.PENDING,
+                "PG 시스템 일시 장애 - 나중에 재시도가 필요합니다."
+        );
+
+        return CompletableFuture.completedFuture(fallbackResponse);
+    }
+
+    private PgPaymentDetail getPaymentStatusFallback(
+            String userId,
+            String transactionKey,
+            Exception ex
+    ) {
+        log.warn("[PG Fallback] 결제 상태 조회 실패, PENDING 상태로 반환 - transactionKey: {}, error: {}",
+                transactionKey, ex.getMessage());
+
+        return new PgPaymentDetail(
+                transactionKey,
+                "UNKNOWN",
+                null,
+                "****-****-****-****",
+                0L,
+                PgTransactionStatus.PENDING,
+                "PG 시스템 일시 장애 - 결제 상태를 확인할 수 없습니다."
+        );
+    }
+
+    private PgPaymentRequest.PgCardType toPgCardType(CardType cardType) {
+        return PgPaymentRequest.PgCardType.valueOf(cardType.name());
+    }
+
+    private CardType toCardType(PgPaymentRequest.PgCardType cardType) {
+        return CardType.valueOf(cardType.name());
+    }
+
+    private PgTransactionStatus toPgTransactionStatus(PgPaymentResponse.PgTransactionStatus status) {
+        return PgTransactionStatus.valueOf(status.name());
+    }
+
+    public record PgApiResponse<T>(
+            String result,
+            String message,
+            T data
+    ) {}
+
+    public static class PgCommunicationException extends RuntimeException {
+        public PgCommunicationException(String message) {
+            super(message);
+        }
+
+        public PgCommunicationException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentDetailResponse.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentDetailResponse.java
new file mode 100644
index 000000000..fd16c1f95
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentDetailResponse.java
@@ -0,0 +1,23 @@
+package com.loopers.infrastructure.payment.pg;
+
+public record PgPaymentDetailResponse(
+        String transactionKey,
+        String orderId,
+        PgPaymentRequest.PgCardType cardType,
+        String cardNo,
+        Long amount,
+        PgPaymentResponse.PgTransactionStatus status,
+        String reason
+) {
+    public boolean isPending() {
+        return status == PgPaymentResponse.PgTransactionStatus.PENDING;
+    }
+
+    public boolean isSuccess() {
+        return status == PgPaymentResponse.PgTransactionStatus.SUCCESS;
+    }
+
+    public boolean isFailed() {
+        return status == PgPaymentResponse.PgTransactionStatus.FAILED;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentRequest.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentRequest.java
new file mode 100644
index 000000000..f74d80ccd
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentRequest.java
@@ -0,0 +1,13 @@
+package com.loopers.infrastructure.payment.pg;
+
+public record PgPaymentRequest(
+        String orderId,
+        PgCardType cardType,
+        String cardNo,
+        Long amount,
+        String callbackUrl
+) {
+    public enum PgCardType {
+        SAMSUNG, KB, HYUNDAI
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentResponse.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentResponse.java
new file mode 100644
index 000000000..dc9e0eb38
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/pg/PgPaymentResponse.java
@@ -0,0 +1,23 @@
+package com.loopers.infrastructure.payment.pg;
+
+public record PgPaymentResponse(
+        String transactionKey,
+        PgTransactionStatus status,
+        String reason
+) {
+    public enum PgTransactionStatus {
+        PENDING, SUCCESS, FAILED
+    }
+
+    public boolean isPending() {
+        return status == PgTransactionStatus.PENDING;
+    }
+
+    public boolean isSuccess() {
+        return status == PgTransactionStatus.SUCCESS;
+    }
+
+    public boolean isFailed() {
+        return status == PgTransactionStatus.FAILED;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
index 12965f337..f254374da 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
@@ -124,6 +124,15 @@ public int decreaseStock(Long productId, int quantity) {
                 .execute());
     }
 
+    @Override
+    public int increaseStock(Long productId, int quantity) {
+        return Math.toIntExact(queryFactory
+                .update(product)
+                .set(product.stock.quantity, product.stock.quantity.add(quantity))
+                .where(product.id.eq(productId))
+                .execute());
+    }
+
     @Override
     public int incrementLikeCount(Long productId) {
         return Math.toIntExact(queryFactory
@@ -249,4 +258,4 @@ private String[] decodeCursor(String cursor) {
             return null;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentController.java
new file mode 100644
index 000000000..a0b13ebdb
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentController.java
@@ -0,0 +1,114 @@
+package com.loopers.interfaces.api.payment;
+
+import com.loopers.application.payment.PaymentCommand;
+import com.loopers.application.payment.PaymentFacade;
+import com.loopers.application.payment.PaymentInfo;
+import com.loopers.domain.payment.CardType;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/payments")
+@RequiredArgsConstructor
+public class PaymentController {
+
+    private final PaymentFacade paymentFacade;
+
+    @PostMapping
+    public ResponseEntity<PaymentResponse> requestPayment(
+            @RequestHeader("X-USER-ID") String userId,
+            @RequestBody PaymentRequest request
+    ) {
+        log.info("[Payment API] 결제 요청 - userId: {}, orderId: {}", userId, request.orderId());
+
+        PaymentCommand.RequestPayment command = new PaymentCommand.RequestPayment(
+                request.orderId(),
+                request.cardType(),
+                request.cardNo(),
+                request.amount(),
+                request.callbackUrl()
+        );
+
+        PaymentInfo paymentInfo = paymentFacade.requestPayment(userId, command);
+
+        return ResponseEntity.ok(PaymentResponse.from(paymentInfo));
+    }
+
+    @GetMapping("/orders/{orderNo}")
+    public ResponseEntity<PaymentResponse> getPaymentByOrderNo(
+            @RequestHeader("X-USER-ID") String userId,
+            @PathVariable String orderNo
+    ) {
+        log.info("[Payment API] 결제 조회 - userId: {}, orderNo: {}", userId, orderNo);
+
+        PaymentInfo paymentInfo = paymentFacade.getPaymentByOrderNo(orderNo);
+
+        return ResponseEntity.ok(PaymentResponse.from(paymentInfo));
+    }
+
+    @PostMapping("/{transactionKey}/sync")
+    public ResponseEntity<PaymentResponse> syncPaymentStatus(
+            @RequestHeader("X-USER-ID") String userId,
+            @PathVariable String transactionKey
+    ) {
+        log.info("[Payment API] 상태 동기화 - userId: {}, transactionKey: {}", userId, transactionKey);
+
+        PaymentInfo paymentInfo = paymentFacade.syncPaymentStatus(userId, transactionKey);
+
+        return ResponseEntity.ok(PaymentResponse.from(paymentInfo));
+    }
+
+    @PostMapping("/orders/{orderNo}/cancel")
+    public ResponseEntity<PaymentResponse> cancelPayment(
+            @RequestHeader("X-USER-ID") String userId,
+            @PathVariable String orderNo,
+            @RequestBody CancelRequest request
+    ) {
+        log.info("[Payment API] 결제 취소 - userId: {}, orderNo: {}", userId, orderNo);
+
+        PaymentInfo paymentInfo = paymentFacade.cancelPayment(userId, orderNo, request.reason());
+
+        return ResponseEntity.ok(PaymentResponse.from(paymentInfo));
+    }
+
+    public record PaymentRequest(
+            String orderId,
+            CardType cardType,
+            String cardNo,
+            Long amount,
+            String callbackUrl
+    ) {}
+
+    public record CancelRequest(
+            String reason
+    ) {}
+
+    public record PaymentResponse(
+            Long id,
+            String orderNo,
+            String transactionKey,
+            String status,
+            String cardType,
+            String cardNo,
+            Long amount,
+            String reason,
+            boolean requiresRetry
+    ) {
+        public static PaymentResponse from(PaymentInfo info) {
+            return new PaymentResponse(
+                    info.id(),
+                    info.orderNo(),
+                    info.transactionKey(),
+                    info.status().name(),
+                    info.cardType().name(),
+                    info.cardNo(),
+                    info.amount(),
+                    info.reason(),
+                    info.requiresRetry()
+            );
+        }
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentTestController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentTestController.java
new file mode 100644
index 000000000..082c937fd
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentTestController.java
@@ -0,0 +1,55 @@
+package com.loopers.interfaces.api.payment;
+
+import com.loopers.domain.payment.CardType;
+import com.loopers.domain.payment.gateway.PgGateway;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/payments/test")
+@RequiredArgsConstructor
+public class PaymentTestController {
+
+    private final PgGateway pgGateway;
+
+    @PostMapping("/request")
+    public PgGateway.PgPaymentResult requestPaymentForTest(
+            @RequestHeader("X-USER-ID") String userId,
+            @RequestBody PaymentTestRequest request
+    ) throws Exception {
+        log.info("[TEST] PG 결제 요청 - userId: {}, orderId: {}", userId, request.orderId());
+
+        PgGateway.PgPaymentCommand command = new PgGateway.PgPaymentCommand(
+                request.orderId(),
+                CardType.valueOf(request.cardType()),
+                request.cardNo(),
+                request.amount(),
+                request.callbackUrl()
+        );
+
+        CompletableFuture<PgGateway.PgPaymentResult> future = pgGateway.requestPayment(userId, command);
+        return future.get(3, TimeUnit.SECONDS);
+    }
+
+    @GetMapping("/status/{transactionKey}")
+    public PgGateway.PgPaymentDetail getPaymentStatusForTest(
+            @RequestHeader("X-USER-ID") String userId,
+            @PathVariable String transactionKey
+    ) {
+        log.info("[TEST] PG 결제 상태 조회 - userId: {}, transactionKey: {}", userId, transactionKey);
+        return pgGateway.getPaymentStatus(userId, transactionKey);
+    }
+
+    public record PaymentTestRequest(
+            String orderId,
+            String cardType,
+            String cardNo,
+            Long amount,
+            String callbackUrl
+    ) {}
+}
diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml
index 484c070d0..30fab38db 100644
--- a/apps/commerce-api/src/main/resources/application.yml
+++ b/apps/commerce-api/src/main/resources/application.yml
@@ -23,12 +23,18 @@ spring:
       - redis.yml
       - logging.yml
       - monitoring.yml
+      - resilience4j.yml
 
 springdoc:
   use-fqn: true
   swagger-ui:
     path: /swagger-ui.html
 
+# PG Simulator 설정
+payment:
+  pg:
+    base-url: http://localhost:8082
+
 ---
 spring:
   config:
diff --git a/apps/commerce-api/src/main/resources/resilience4j.yml b/apps/commerce-api/src/main/resources/resilience4j.yml
new file mode 100644
index 000000000..1a52cf97c
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/resilience4j.yml
@@ -0,0 +1,56 @@
+resilience4j:
+  circuitbreaker:
+    configs:
+      default:
+        slidingWindowType: TIME_BASED
+        slidingWindowSize: 30
+        minimumNumberOfCalls: 5
+        failureRateThreshold: 50
+        slowCallRateThreshold: 50
+        slowCallDurationThreshold: 2s
+        waitDurationInOpenState: 10s
+        permittedNumberOfCallsInHalfOpenState: 3
+        automaticTransitionFromOpenToHalfOpenEnabled: true
+        registerHealthIndicator: true
+        recordExceptions:
+          - java.util.concurrent.TimeoutException
+          - java.io.IOException
+          - org.springframework.web.client.ResourceAccessException
+          - org.springframework.web.client.HttpServerErrorException
+
+    instances:
+      pgSimulator:
+        baseConfig: default
+        slidingWindowSize: 20
+        minimumNumberOfCalls: 3
+        failureRateThreshold: 30
+        slowCallDurationThreshold: 1s
+        waitDurationInOpenState: 15s
+
+  retry:
+    configs:
+      default:
+        maxAttempts: 3
+        waitDuration: 500ms
+        enableExponentialBackoff: true
+        exponentialBackoffMultiplier: 2
+        retryExceptions:
+          - java.util.concurrent.TimeoutException
+          - org.springframework.web.client.ResourceAccessException
+        ignoreExceptions:
+          - java.lang.IllegalArgumentException
+          - com.loopers.support.error.CoreException
+    instances:
+      pgSimulator:
+        baseConfig: default
+        maxAttempts: 3
+        waitDuration: 500ms
+
+  timelimiter:
+    configs:
+      default:
+        timeoutDuration: 3s
+        cancelRunningFuture: true
+    instances:
+      pgSimulator:
+        timeoutDuration: 2s
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponConcurrencyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponConcurrencyTest.java
index b9f3b898b..02c94a48a 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponConcurrencyTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponConcurrencyTest.java
@@ -68,8 +68,11 @@ void tearDown() {
     @DisplayName("동일한 쿠폰으로 여러 기기에서 동시에 주문해도, 쿠폰은 단 한번만 사용되어야 한다")
     void shouldUseCouponOnlyOnce_whenConcurrentOrdersWithSameCoupon() throws InterruptedException {
         // given
-        String memberId = "testUser";
-        memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+        String username = "testUser";
+        memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+
+        // Member ID is 1L (first member created after table truncation)
+        Long memberId = 1L;
 
         Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
         existingPoint.addAmount(BigDecimal.valueOf(100000));
@@ -144,9 +147,14 @@ void shouldUseEachCouponCorrectly_whenConcurrentOrdersWithDifferentCoupons() thr
         Long[] couponIds = new Long[threadCount];
 
         // 10명의 회원 생성 및 각각 쿠폰 발급
+        Long[] memberIds = new Long[threadCount];
         for (int i = 0; i < threadCount; i++) {
-            String memberId = "member" + i;
-            memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+            String username = "member" + i;
+            memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+
+            // Member IDs are assigned sequentially starting from 1L
+            Long memberId = (long) (i + 1);
+            memberIds[i] = memberId;
 
             Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
             existingPoint.addAmount(BigDecimal.valueOf(50000));
@@ -163,7 +171,7 @@ void shouldUseEachCouponCorrectly_whenConcurrentOrdersWithDifferentCoupons() thr
         // when: 10명이 각자의 쿠폰으로 동시에 주문
         try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) {
             for (int i = 0; i < threadCount; i++) {
-                final String memberId = "member" + i;
+                final Long memberId = memberIds[i];
                 final Long couponId = couponIds[i];
                 executorService.submit(() -> {
                     try {
@@ -200,8 +208,11 @@ void shouldUseEachCouponCorrectly_whenConcurrentOrdersWithDifferentCoupons() thr
     @DisplayName("동일한 유저가 여러 주문을 동시에 쿠폰과 함께 수행해도, 쿠폰별로 정상 처리되어야 한다")
     void shouldHandleConcurrentOrdersWithMultipleCoupons_forSameUser() throws InterruptedException {
         // given
-        String memberId = "testUser";
-        memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+        String username = "testUser";
+        memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+
+        // Member ID is 1L (first member created after table truncation)
+        Long memberId = 1L;
 
         Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
         existingPoint.addAmount(BigDecimal.valueOf(100000));
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/MemberCouponTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/MemberCouponTest.java
index cc22dcbd6..14a163d1e 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/MemberCouponTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/MemberCouponTest.java
@@ -21,7 +21,7 @@ class IssueMemberCoupon {
         @Test
         void shouldIssueMemberCoupon() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            String memberId = "member123";
+            Long memberId = 123L;
 
             MemberCoupon memberCoupon = MemberCoupon.issue(memberId, coupon);
 
@@ -30,12 +30,12 @@ void shouldIssueMemberCoupon() {
             assertThat(memberCoupon.isUsed()).isFalse();
         }
 
-        @DisplayName("회원 ID가 비어있으면 예외가 발생한다")
+        @DisplayName("회원 ID가 null이면 예외가 발생한다")
         @Test
         void shouldThrowException_whenMemberIdIsEmpty() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
 
-            assertThatThrownBy(() -> MemberCoupon.issue("", coupon))
+            assertThatThrownBy(() -> MemberCoupon.issue(null, coupon))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("회원 ID는 필수입니다");
         }
@@ -43,7 +43,7 @@ void shouldThrowException_whenMemberIdIsEmpty() {
         @DisplayName("쿠폰이 null이면 예외가 발생한다")
         @Test
         void shouldThrowException_whenCouponIsNull() {
-            assertThatThrownBy(() -> MemberCoupon.issue("member123", null))
+            assertThatThrownBy(() -> MemberCoupon.issue(123L, null))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("쿠폰 정보는 필수입니다");
         }
@@ -57,7 +57,7 @@ class UseMemberCoupon {
         @Test
         void shouldMarkAsUsed() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
 
             memberCoupon.use();
 
@@ -68,7 +68,7 @@ void shouldMarkAsUsed() {
         @Test
         void shouldThrowException_whenAlreadyUsed() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
             memberCoupon.use();
 
             assertThatThrownBy(memberCoupon::use)
@@ -85,7 +85,7 @@ class ValidateUsable {
         @Test
         void shouldPassValidation_whenUsable() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
 
             memberCoupon.validateUsable();
 
@@ -96,7 +96,7 @@ void shouldPassValidation_whenUsable() {
         @Test
         void shouldThrowException_whenNotUsable() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
             memberCoupon.use();
 
             assertThatThrownBy(memberCoupon::validateUsable)
@@ -113,18 +113,18 @@ class ValidateOwnership {
         @Test
         void shouldPassValidation_whenOwner() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
 
-            memberCoupon.validateOwnership("member123");
+            memberCoupon.validateOwnership(123L);
         }
 
         @DisplayName("소유자가 아닌 경우 예외가 발생한다")
         @Test
         void shouldThrowException_whenNotOwner() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
 
-            assertThatThrownBy(() -> memberCoupon.validateOwnership("otherMember"))
+            assertThatThrownBy(() -> memberCoupon.validateOwnership(999L))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("본인의 쿠폰만 사용할 수 있습니다");
         }
@@ -138,7 +138,7 @@ class CalculateDiscount {
         @Test
         void shouldDelegateToLinkedCoupon() {
             Coupon coupon = Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000));
-            MemberCoupon memberCoupon = MemberCoupon.issue("member123", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(123L, coupon);
             Money originalPrice = Money.of(10000);
 
             Money discount = memberCoupon.calculateDiscount(originalPrice);
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeReadServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeReadServiceTest.java
index fb5ee970a..618ce315d 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeReadServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeReadServiceTest.java
@@ -1,29 +1,33 @@
 package com.loopers.domain.like;
 
 import com.loopers.domain.like.service.LikeReadService;
+import com.loopers.infrastructure.cache.MemberLikesCache;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
 
 class LikeReadServiceTest {
 
     private InMemoryLikeRepository likeRepository;
+    private MemberLikesCache memberLikesCache;
     private LikeReadService likeReadService;
 
     @BeforeEach
     void setUp() {
         likeRepository = new InMemoryLikeRepository();
-        likeReadService = new LikeReadService(likeRepository);
+        memberLikesCache = mock(MemberLikesCache.class);
+        likeReadService = new LikeReadService(likeRepository, memberLikesCache);
     }
 
     @Test
     void should_count_likes_by_product_id() {
         // given
         Long productId = 1L;
-        likeRepository.save(new Like("member1", productId));
-        likeRepository.save(new Like("member2", productId));
-        likeRepository.save(new Like("member3", 2L)); // 다른 상품
+        likeRepository.save(new Like(1L, productId));
+        likeRepository.save(new Like(2L, productId));
+        likeRepository.save(new Like(3L, 2L)); // 다른 상품
 
         // when
         long count = likeReadService.countByProductId(productId);
@@ -47,7 +51,7 @@ void should_return_zero_when_no_likes_for_product() {
     @Test
     void should_check_if_member_liked_product() {
         // given
-        String memberId = "member1";
+        Long memberId = 1L;
         Long productId = 1L;
         likeRepository.save(new Like(memberId, productId));
 
@@ -61,7 +65,7 @@ void should_check_if_member_liked_product() {
     @Test
     void should_return_false_when_member_did_not_like_product() {
         // given
-        String memberId = "member1";
+        Long memberId = 1L;
         Long productId = 1L;
 
         // when
@@ -74,9 +78,9 @@ void should_return_false_when_member_did_not_like_product() {
     @Test
     void should_return_false_when_member_id_is_null() {
         // given
-        String memberId = null;
+        Long memberId = null;
         Long productId = 1L;
-        likeRepository.save(new Like("member1", productId));
+        likeRepository.save(new Like(1L, productId));
 
         // when
         boolean isLiked = likeReadService.isLikedBy(memberId, productId);
@@ -88,10 +92,10 @@ void should_return_false_when_member_id_is_null() {
     @Test
     void should_not_check_other_members_likes() {
         // given
-        String memberId1 = "member1";
-        String memberId2 = "member2";
+        Long memberId1 = 1L;
+        Long memberId2 = 2L;
         Long productId = 1L;
-        
+
         likeRepository.save(new Like(memberId1, productId));
 
         // when
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/InMemoryOrderRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/InMemoryOrderRepository.java
index e358d582d..8d36300fc 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/InMemoryOrderRepository.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/InMemoryOrderRepository.java
@@ -17,6 +17,13 @@ public Optional<Order> findById(Long id) {
         return Optional.ofNullable(store.get(id));
     }
 
+    @Override
+    public Optional<Order> findByOrderNo(String orderNo) {
+        return store.values().stream()
+                .filter(order -> orderNo.equals(order.getOrderNo()))
+                .findFirst();
+    }
+
     @Override
     public Order save(Order order) {
         if (order.getId() == null) {
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderConcurrencyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderConcurrencyTest.java
index 7f025a00b..0c8feb12f 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderConcurrencyTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderConcurrencyTest.java
@@ -76,10 +76,15 @@ void shouldDeductStockCorrectly_whenConcurrentOrders() throws InterruptedExcepti
         int threadCount = 10;
 
         // 10명의 회원 생성 및 포인트 부여
+        Long[] memberIds = new Long[threadCount];
         for (int i = 0; i < threadCount; i++) {
-            String memberId = "member" + i;
-            memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+            String username = "member" + i;
+            memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
             // MemberFacade가 이미 Point를 0원으로 생성했으므로 업데이트
+            // Member IDs are assigned sequentially starting from 1L
+            Long memberId = (long) (i + 1);
+            memberIds[i] = memberId;
+
             Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
             existingPoint.addAmount(BigDecimal.valueOf(10000));
             pointRepository.save(existingPoint);
@@ -92,7 +97,7 @@ void shouldDeductStockCorrectly_whenConcurrentOrders() throws InterruptedExcepti
         // when: 10명이 동시에 1개씩 주문
         try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) {
             for (int i = 0; i < threadCount; i++) {
-                final String memberId = "member" + i;
+                final Long memberId = memberIds[i];
                 executorService.submit(() -> {
                     try {
                         OrderCommand command = OrderCommand.of(
@@ -139,10 +144,14 @@ void shouldRejectOrders_whenStockInsufficient() throws InterruptedException {
         int threadCount = 10;
 
         // 10명의 회원 생성 및 포인트 부여
+        Long[] memberIds2 = new Long[threadCount];
         for (int i = 0; i < threadCount; i++) {
-            String memberId = "member" + i;
-            memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
-            // MemberFacade가 이미 Point를 0원으로 생성했으므로 업데이트
+            String username = "member" + i;
+            memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+            // Member IDs are assigned sequentially starting from 1L
+            Long memberId = (long) (i + 1);
+            memberIds2[i] = memberId;
+
             Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
             existingPoint.addAmount(BigDecimal.valueOf(10000));
             pointRepository.save(existingPoint);
@@ -155,7 +164,7 @@ void shouldRejectOrders_whenStockInsufficient() throws InterruptedException {
         // when: 10명이 동시에 1개씩 주문 (재고는 5개)
         try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) {
             for (int i = 0; i < threadCount; i++) {
-                final String memberId = "member" + i;
+                final Long memberId = memberIds2[i];
                 executorService.submit(() -> {
                     try {
                         OrderCommand command = OrderCommand.of(
@@ -209,9 +218,14 @@ void shouldDeductMultipleProductStocksCorrectly_whenConcurrentOrdersWithMultiple
         int threadCount = 5;
 
         // 5명의 회원 생성 및 포인트 부여
+        Long[] memberIds3 = new Long[threadCount];
         for (int i = 0; i < threadCount; i++) {
-            String memberId = "member" + i;
-            memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+            String username = "member" + i;
+            memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+            // Member IDs are assigned sequentially starting from 1L
+            Long memberId = (long) (i + 1);
+            memberIds3[i] = memberId;
+
             Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
             existingPoint.addAmount(BigDecimal.valueOf(50000));
             pointRepository.save(existingPoint);
@@ -222,7 +236,7 @@ void shouldDeductMultipleProductStocksCorrectly_whenConcurrentOrdersWithMultiple
         // when: 5명이 동시에 각 상품 2개씩 주문
         try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) {
             for (int i = 0; i < threadCount; i++) {
-                final String memberId = "member" + i;
+                final Long memberId = memberIds3[i];
                 executorService.submit(() -> {
                     try {
                         OrderCommand command = OrderCommand.of(
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPlacementServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPlacementServiceTest.java
index ad5d63071..ddb4be1e7 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPlacementServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderPlacementServiceTest.java
@@ -44,7 +44,6 @@ void setUp() {
                 orderRepository,
                 productRepository,
                 memberRepository,
-                pointRepository,
                 memberCouponRepository
         );
     }
@@ -57,8 +56,7 @@ class PlaceOrder {
         @Test
         void shouldProcessOrder_whenValidOrderPlaced() {
         // given
-        String memberId = "member1";
-        setupMemberWithPoints(memberId, BigDecimal.valueOf(50000));
+        Long memberId = setupMemberWithPoints("member1", BigDecimal.valueOf(50000));
         
         Product product1 = setupProduct(1L, Money.of(10000), Stock.of(100));
         Product product2 = setupProduct(2L, Money.of(15000), Stock.of(50));
@@ -93,8 +91,7 @@ void shouldProcessOrder_whenValidOrderPlaced() {
         @Test
         void shouldThrowException_whenInsufficientStock() {
         // given
-        String memberId = "member1";
-        setupMemberWithPoints(memberId, BigDecimal.valueOf(50000));
+        Long memberId = setupMemberWithPoints("member1", BigDecimal.valueOf(50000));
         
         Product product = setupProduct(1L, Money.of(10000), Stock.of(5));
 
@@ -116,8 +113,7 @@ void shouldThrowException_whenInsufficientStock() {
         @Test
         void shouldThrowException_whenInsufficientPoints() {
         // given
-        String memberId = "member1";
-        setupMemberWithPoints(memberId, BigDecimal.valueOf(5000)); // 부족한 포인트
+        Long memberId = setupMemberWithPoints("member1", BigDecimal.valueOf(5000)); // 부족한 포인트
         
         setupProduct(1L, Money.of(10000), Stock.of(100));
 
@@ -136,7 +132,7 @@ void shouldThrowException_whenInsufficientPoints() {
         @Test
         void shouldThrowException_whenMemberNotFound() {
         // given
-        String nonExistentMemberId = "none123";
+        Long nonExistentMemberId = 999L; // ID that doesn't exist
         setupProduct(1L, Money.of(10000), Stock.of(100));
 
         OrderPlacementCommand command = OrderPlacementCommand.of(
@@ -154,8 +150,7 @@ void shouldThrowException_whenMemberNotFound() {
         @Test
         void shouldThrowException_whenProductNotFound() {
         // given
-        String memberId = "member1";
-        setupMemberWithPoints(memberId, BigDecimal.valueOf(50000));
+        Long memberId = setupMemberWithPoints("member1", BigDecimal.valueOf(50000));
 
         OrderPlacementCommand command = OrderPlacementCommand.of(
                 memberId,
@@ -172,8 +167,7 @@ void shouldThrowException_whenProductNotFound() {
         @Test
         void shouldThrowException_whenMemberPointsNotFound() {
         // given
-        String memberId = "member1";
-        setupMember(memberId); // 포인트는 설정하지 않음
+        Long memberId = setupMember("member1"); // 포인트는 설정하지 않음
         setupProduct(1L, Money.of(10000), Stock.of(100));
 
         OrderPlacementCommand command = OrderPlacementCommand.of(
@@ -196,8 +190,7 @@ class CalculationAccuracy {
         @Test
         void shouldCalculateTotalCorrectly_whenOrderingMultipleProducts() {
         // given
-        String memberId = "member1";
-        setupMemberWithPoints(memberId, BigDecimal.valueOf(100000));
+        Long memberId = setupMemberWithPoints("member1", BigDecimal.valueOf(100000));
         
         setupProduct(1L, Money.of(10000), Stock.of(100));
         setupProduct(2L, Money.of(25000), Stock.of(50));
@@ -226,20 +219,30 @@ void shouldCalculateTotalCorrectly_whenOrderingMultipleProducts() {
         }
     }
 
-    private void setupMemberWithPoints(String memberId, BigDecimal points) {
-        setupMember(memberId);
+    private Long setupMemberWithPoints(String username, BigDecimal points) {
+        Member member = new Member(
+                username,
+                username + "@test.com",
+                "password123",
+                "1990-01-01",
+                Gender.MALE
+        );
+        Member saved = memberRepository.save(member);
+        Long memberId = saved.getId();
         pointRepository.save(Point.create(memberId, points));
+        return memberId;
     }
 
-    private void setupMember(String memberId) {
+    private Long setupMember(String username) {
         Member member = new Member(
-                memberId,
-                memberId + "@test.com",
+                username,
+                username + "@test.com",
                 "password123",
                 "1990-01-01",
                 Gender.MALE
         );
-        memberRepository.save(member);
+        Member saved = memberRepository.save(member);
+        return saved.getId();
     }
 
     private Product setupProduct(Long productId, Money price, Stock stock) {
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
index 6d30a5c85..909bf0743 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
@@ -85,14 +85,15 @@ class OrderCreateSuccess {
         void createOrder_success() {
 
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product p1 = productRepository.save(createProduct(1L, "아메리카노", 3000L, 100));
             Product p2 = productRepository.save(createProduct(1L, "라떼", 4000L, 200));
 
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(
                             OrderLineCommand.of(p1.getId(), 2),  // 6000원
                             OrderLineCommand.of(p2.getId(), 1)   // 4000원
@@ -116,7 +117,7 @@ void createOrder_success() {
             assertThat(updated2.getStock().getQuantity()).isEqualTo(199);
 
             // 포인트 감소 확인 (entityManager.clear() 이후이므로 새로운 조회 필요)
-            Point point = pointRepository.findByMemberId("user1").get();
+            Point point = pointRepository.findByMemberId(memberId).get();
             // 트랜잭션 내에서 포인트 차감이 일어났지만, @Transactional 테스트이므로
             // 실제 DB에는 커밋 전 상태. 대신 엔티티 상태로 확인
             assertThat(point.getAmount()).isNotNull();
@@ -132,12 +133,13 @@ class OrderCreateFail {
         @Transactional
         @DisplayName("재고 부족으로 실패")
         void insufficientStock_fail() {
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 1000L, 1));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(5000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(5000L)));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 5))
             );
 
@@ -149,12 +151,13 @@ void insufficientStock_fail() {
         @Transactional
         @DisplayName("포인트 부족으로 실패")
         void insufficientPoint_fail() {
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 1000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(2000L))); // 부족
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(2000L))); // 부족
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 5)) // 총 5000원
             );
 
@@ -166,11 +169,12 @@ void insufficientPoint_fail() {
         @Transactional
         @DisplayName("없는 상품 주문 시 실패")
         void noProduct_fail() {
-            memberRepository.save(createMember("user1"));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(10000L)));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(10000L)));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(999L, 1))
             );
 
@@ -182,11 +186,12 @@ void noProduct_fail() {
         @Transactional
         @DisplayName("유저 포인트 정보 없으면 실패")
         void noUserPoint_fail() {
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 1000L, 10));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1))
             );
 
@@ -203,15 +208,16 @@ class OrderWithCoupon {
         @DisplayName("정액 쿠폰 적용 시 할인된 금액으로 결제된다")
         void fixedCoupon_success() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000)));
-            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue("user1", coupon));
+            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue(memberId, coupon));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1)),
                     memberCoupon.getId()
             );
@@ -228,7 +234,7 @@ void fixedCoupon_success() {
             assertThat(usedCoupon.isUsed()).isTrue();
 
             // 포인트 차감 확인 (9000원만 차감됨)
-            Point point = pointRepository.findByMemberId("user1").orElseThrow();
+            Point point = pointRepository.findByMemberId(memberId).orElseThrow();
             assertThat(point.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(11000)); // 20000 - 9000
         }
 
@@ -236,15 +242,16 @@ void fixedCoupon_success() {
         @DisplayName("정률 쿠폰 적용 시 할인된 금액으로 결제된다")
         void percentageCoupon_success() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createPercentageCoupon("10% 할인", BigDecimal.valueOf(10)));
-            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue("user1", coupon));
+            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue(memberId, coupon));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1)),
                     memberCoupon.getId()
             );
@@ -257,7 +264,7 @@ void percentageCoupon_success() {
             assertThat(saved.getTotalPrice()).isEqualTo(Money.of(9000L)); // 10000 * 0.9 = 9000
 
             // 포인트 차감 확인
-            Point point = pointRepository.findByMemberId("user1").orElseThrow();
+            Point point = pointRepository.findByMemberId(memberId).orElseThrow();
             assertThat(point.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(11000)); // 20000 - 9000
         }
 
@@ -265,12 +272,13 @@ void percentageCoupon_success() {
         @DisplayName("존재하지 않는 쿠폰으로 주문 시 실패한다")
         void nonExistentCoupon_fail() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1)),
                     999L // 존재하지 않는 쿠폰 ID
             );
@@ -284,17 +292,18 @@ void nonExistentCoupon_fail() {
         @DisplayName("이미 사용된 쿠폰으로 주문 시 실패한다")
         void alreadyUsedCoupon_fail() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000)));
-            MemberCoupon memberCoupon = MemberCoupon.issue("user1", coupon);
+            MemberCoupon memberCoupon = MemberCoupon.issue(memberId, coupon);
             memberCoupon.use(); // 이미 사용 처리
             memberCouponRepository.save(memberCoupon);
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1)),
                     memberCoupon.getId()
             );
@@ -308,16 +317,18 @@ void alreadyUsedCoupon_fail() {
         @DisplayName("다른 회원의 쿠폰으로 주문 시 실패한다")
         void otherMemberCoupon_fail() {
             // given
-            memberRepository.save(createMember("user1"));
-            memberRepository.save(createMember("user2"));
+            Member member1 = memberRepository.save(createMember("user1"));
+            Long memberId1 = member1.getId();
+            Member member2 = memberRepository.save(createMember("user2"));
+            Long memberId2 = member2.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId1, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000)));
-            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue("user2", coupon)); // user2의 쿠폰
+            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue(memberId2, coupon)); // user2의 쿠폰
 
             OrderCommand command = OrderCommand.of(
-                    "user1", // user1이 user2의 쿠폰 사용 시도
+                    memberId1, // user1이 user2의 쿠폰 사용 시도
                     List.of(OrderLineCommand.of(item.getId(), 1)),
                     memberCoupon.getId()
             );
@@ -331,15 +342,16 @@ void otherMemberCoupon_fail() {
         @DisplayName("할인 금액이 주문 금액보다 클 경우 0원으로 결제된다")
         void discountExceedsPrice_zeroPayment() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 1000L, 10));
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createFixedCoupon("5000원 할인", BigDecimal.valueOf(5000)));
-            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue("user1", coupon));
+            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue(memberId, coupon));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 1)), // 1000원 상품
                     memberCoupon.getId()
             );
@@ -352,7 +364,7 @@ void discountExceedsPrice_zeroPayment() {
             assertThat(saved.getTotalPrice()).isEqualTo(Money.of(0L)); // 1000 - 1000(최대 할인) = 0
 
             // 포인트 차감 없음
-            Point point = pointRepository.findByMemberId("user1").orElseThrow();
+            Point point = pointRepository.findByMemberId(memberId).orElseThrow();
             assertThat(point.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(20000)); // 변동 없음
         }
 
@@ -360,15 +372,16 @@ void discountExceedsPrice_zeroPayment() {
         @DisplayName("쿠폰 적용 주문 실패 시 쿠폰 상태가 롤백된다")
         void couponRollback_onOrderFailure() {
             // given
-            memberRepository.save(createMember("user1"));
+            Member member = memberRepository.save(createMember("user1"));
+            Long memberId = member.getId();
             Product item = productRepository.save(createProduct(1L, "상품", 10000L, 1)); // 재고 1개
-            pointRepository.save(Point.create("user1", BigDecimal.valueOf(20000L)));
+            pointRepository.save(Point.create(memberId, BigDecimal.valueOf(20000L)));
 
             Coupon coupon = couponRepository.save(Coupon.createFixedCoupon("1000원 할인", BigDecimal.valueOf(1000)));
-            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue("user1", coupon));
+            MemberCoupon memberCoupon = memberCouponRepository.save(MemberCoupon.issue(memberId, coupon));
 
             OrderCommand command = OrderCommand.of(
-                    "user1",
+                    memberId,
                     List.of(OrderLineCommand.of(item.getId(), 5)), // 재고 초과
                     memberCoupon.getId()
             );
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
index d76c120c9..51f1f3d8d 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -23,10 +23,10 @@ void shouldCreateOrder_whenValidInput() {
                     new OrderItem(1L, 2, Money.of(10000)),
                     new OrderItem(2L, 1, Money.of(15000))
             );
-            
-            Order order = Order.create("member1", items);
-            
-            assertThat(order.getMemberId()).isEqualTo("member1");
+
+            Order order = Order.create(1L, items);
+
+            assertThat(order.getMemberId()).isEqualTo(1L);
             assertThat(order.getItems()).hasSize(2);
             assertThat(order.getTotalPrice()).isEqualTo(Money.of(35000)); // 20000 + 15000
         }
@@ -43,18 +43,18 @@ void shouldThrowException_whenMemberIdIsNull() {
                     .hasMessageContaining("회원 ID는 필수입니다");
         }
 
-        @DisplayName("회원 ID가 빈값이면 예외가 발생한다")
+        @DisplayName("회원 ID가 0 이하면 예외가 발생한다")
         @Test
-        void shouldThrowException_whenMemberIdIsEmpty() {
+        void shouldThrowException_whenMemberIdIsInvalid() {
             List<OrderItem> items = List.of(
                     new OrderItem(1L, 1, Money.of(10000))
             );
-            
-            assertThatThrownBy(() -> Order.create("", items))
+
+            assertThatThrownBy(() -> Order.create(0L, items))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("회원 ID는 필수입니다");
-            
-            assertThatThrownBy(() -> Order.create("   ", items))
+
+            assertThatThrownBy(() -> Order.create(-1L, items))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("회원 ID는 필수입니다");
         }
@@ -62,11 +62,11 @@ void shouldThrowException_whenMemberIdIsEmpty() {
         @DisplayName("주문 항목이 null이거나 비어있으면 예외가 발생한다")
         @Test
         void shouldThrowException_whenItemsAreNullOrEmpty() {
-            assertThatThrownBy(() -> Order.create("member1", null))
+            assertThatThrownBy(() -> Order.create(1L, null))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("주문 항목은 필수입니다");
-            
-            assertThatThrownBy(() -> Order.create("member1", List.of()))
+
+            assertThatThrownBy(() -> Order.create(1L, List.of()))
                     .isInstanceOf(CoreException.class)
                     .hasMessageContaining("주문 항목은 필수입니다");
         }
@@ -84,9 +84,9 @@ void shouldCalculateTotalPriceCorrectly_whenMultipleItems() {
                     new OrderItem(2L, 3, Money.of(8000)),   // 24000
                     new OrderItem(3L, 1, Money.of(12000))   // 12000
             );
-            
-            Order order = Order.create("member1", items);
-            
+
+            Order order = Order.create(1L, items);
+
             assertThat(order.getTotalPrice()).isEqualTo(Money.of(46000));
         }
 
@@ -95,9 +95,9 @@ void shouldCalculateTotalPriceCorrectly_whenMultipleItems() {
         void shouldEqualItemTotalPrice_whenSingleItem() {
             OrderItem item = new OrderItem(1L, 3, Money.of(10000));
             List<OrderItem> items = List.of(item);
-            
-            Order order = Order.create("member1", items);
-            
+
+            Order order = Order.create(1L, items);
+
             assertThat(order.getTotalPrice()).isEqualTo(item.getTotalPrice());
             assertThat(order.getTotalPrice()).isEqualTo(Money.of(30000));
         }
@@ -113,10 +113,10 @@ void shouldReturnImmutableItemList_whenGetItems() {
             List<OrderItem> items = List.of(
                     new OrderItem(1L, 1, Money.of(10000))
             );
-            
-            Order order = Order.create("member1", items);
+
+            Order order = Order.create(1L, items);
             List<OrderItem> returnedItems = order.getItems();
-            
+
             // 반환된 리스트 수정 시도 시 예외 발생
             assertThatThrownBy(() -> returnedItems.add(new OrderItem(2L, 1, Money.of(5000))))
                     .isInstanceOf(UnsupportedOperationException.class);
@@ -130,9 +130,9 @@ void shouldReturnCorrectItemCount_whenMultipleItems() {
                     new OrderItem(2L, 1, Money.of(10000)),
                     new OrderItem(3L, 3, Money.of(3000))
             );
-            
-            Order order = Order.create("member1", items);
-            
+
+            Order order = Order.create(1L, items);
+
             assertThat(order.getItems()).hasSize(3);
         }
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointConcurrencyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointConcurrencyTest.java
index cbc3303aa..1e9c41290 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointConcurrencyTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointConcurrencyTest.java
@@ -60,8 +60,11 @@ void tearDown() {
     @DisplayName("동일한 유저가 서로 다른 주문을 동시에 수행해도, 포인트가 정상적으로 차감되어야 한다")
     void shouldDeductPointsCorrectly_whenSameUserPlacesConcurrentOrders() throws InterruptedException {
         // given
-        String memberId = "member1";
-        memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+        String username = "member1";
+        memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+
+        // Member ID is 1L (first member created after table truncation)
+        Long memberId = 1L;
 
         BigDecimal initialPoints = BigDecimal.valueOf(10000);
         // MemberFacade가 이미 Point를 0원으로 생성했으므로 업데이트
@@ -141,8 +144,11 @@ void shouldDeductPointsCorrectly_whenSameUserPlacesConcurrentOrders() throws Int
     @DisplayName("포인트가 부족한 상황에서 동일 유저가 동시 주문 시, 일부만 성공하고 포인트는 음수가 되지 않아야 한다")
     void shouldRejectOrder_whenPointsInsufficientDuringConcurrentOrders() throws InterruptedException {
         // given
-        String memberId = "member1";
-        memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+        String username = "member1";
+        memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+
+        // Member ID is 1L (first member created after table truncation)
+        Long memberId = 1L;
 
         BigDecimal initialPoints = BigDecimal.valueOf(5000);
         // MemberFacade가 이미 Point를 0원으로 생성했으므로 업데이트
@@ -236,9 +242,14 @@ void shouldDeductPointsIndependently_whenMultipleUsersConcurrentlyPlaceOrders()
         int userCount = 5;
 
         // 5명의 회원 생성 및 각각 3000원 포인트 부여
+        Long[] memberIds = new Long[userCount];
         for (int i = 0; i < userCount; i++) {
-            String memberId = "member" + i;
-            memberFacade.registerMember(memberId, memberId + "@test.com", "password", "1990-01-01", Gender.MALE);
+            String username = "member" + i;
+            memberFacade.registerMember(username, username + "@test.com", "password", "1990-01-01", Gender.MALE);
+            // Member IDs are assigned sequentially starting from 1L
+            Long memberId = (long) (i + 1);
+            memberIds[i] = memberId;
+
             Point existingPoint = pointRepository.findByMemberId(memberId).orElseThrow();
             existingPoint.addAmount(BigDecimal.valueOf(3000));
             pointRepository.save(existingPoint);
@@ -249,7 +260,7 @@ void shouldDeductPointsIndependently_whenMultipleUsersConcurrentlyPlaceOrders()
         // when: 5명이 동시에 1000원 상품 주문
         try (ExecutorService executorService = Executors.newFixedThreadPool(userCount)) {
             for (int i = 0; i < userCount; i++) {
-                final String memberId = "member" + i;
+                final Long memberId = memberIds[i];
                 executorService.submit(() -> {
                     try {
                         OrderCommand command = OrderCommand.of(
@@ -268,7 +279,7 @@ void shouldDeductPointsIndependently_whenMultipleUsersConcurrentlyPlaceOrders()
 
         // then: 모든 유저가 2000원씩 남아있어야 함
         for (int i = 0; i < userCount; i++) {
-            String memberId = "member" + i;
+            Long memberId = memberIds[i];
             Point point = pointRepository.findByMemberId(memberId).orElseThrow();
             assertThat(point.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(2000)); // 3000 - 1000
         }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java
index 7d004cc50..1f748f46f 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/points/PointServiceIntegrationTest.java
@@ -33,7 +33,7 @@ class GetMemberPoints {
         @DisplayName("해당 ID의 회원이 존재할 경우, 보유 포인트가 반환된다")
         @Test
         void shouldReturnPoints_whenMemberExists() {
-            String memberId = "test123";
+            Long memberId = 123L;
             pointService.initializeMemberPoints(memberId);
 
             BigDecimal result = pointService.getMemberPoints(memberId);
@@ -45,7 +45,7 @@ void shouldReturnPoints_whenMemberExists() {
         @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
         @Test
         void shouldReturnNull_whenMemberDoesNotExist() {
-            BigDecimal result = pointService.getMemberPoints("nonexistent");
+            BigDecimal result = pointService.getMemberPoints(999L);
 
             assertThat(result).isNull();
         }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java
index 079a42ce5..c32edf324 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java
@@ -80,6 +80,16 @@ public int decreaseStock(Long productId, int quantity) {
         return 1;
     }
 
+    @Override
+    public int increaseStock(Long productId, int quantity) {
+        Product product = store.get(productId);
+        if (product == null) {
+            return 0;
+        }
+        product.increaseStock(quantity);
+        return 1;
+    }
+
     @Override
     public int incrementLikeCount(Long productId) {
         Product product = store.get(productId);
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductReadServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductReadServiceTest.java
index c7e86000e..ccd234234 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductReadServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductReadServiceTest.java
@@ -59,7 +59,7 @@ class GetProductDetail {
         void shouldGetProductDetail_whenValidInput() {
             // given
             Long productId = 1L;
-            String memberId = "member1";
+            Long memberId = 1L;
             Long brandId = 1L;
 
             Product product = createProduct(brandId);
@@ -93,7 +93,7 @@ void shouldGetProductDetail_whenValidInput() {
         void shouldGetProductDetail_whenMemberIdIsNull() {
             // given
             Long productId = 1L;
-            String memberId = null;
+            Long memberId = null;
             Long brandId = 1L;
 
             Product product = createProduct(brandId);
@@ -127,7 +127,7 @@ void shouldGetProductDetail_whenMemberIdIsNull() {
         void shouldThrowException_whenProductNotFound() {
             // given
             Long productId = 999L;
-            String memberId = "member1";
+            Long memberId = 1L;
 
             given(productRepository.findById(productId)).willReturn(Optional.empty());
 
@@ -146,7 +146,7 @@ void shouldThrowException_whenProductNotFound() {
         void shouldThrowException_whenBrandNotFound() {
             // given
             Long productId = 1L;
-            String memberId = "member1";
+            Long memberId = 1L;
             Long brandId = 999L;
 
             Product product = createProduct(brandId);
@@ -175,7 +175,7 @@ void shouldGetProducts_whenValidInput() {
             // given
             ProductSearchFilter filter = ProductSearchFilter.builder().build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product1 = createProduct(1L);
             Product product2 = createProduct(2L);
@@ -220,7 +220,7 @@ void shouldGetProducts_whenBrandIdProvided() {
                     .brandId(brandId)
                     .build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product1 = createProduct(brandId);
             Product product2 = createProduct(brandId);
@@ -254,7 +254,7 @@ void shouldGetProducts_whenBrandIdAndKeywordProvided() {
                     .keyword(keyword)
                     .build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product = createProduct(brandId);
             Brand brand = createBrand(brandId);
@@ -282,7 +282,7 @@ void shouldGetProducts_whenMemberIdIsNull() {
             // given
             ProductSearchFilter filter = ProductSearchFilter.builder().build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = null;
+            Long memberId = null;
 
             Product product = createProduct(1L);
             Brand brand = createBrand();
@@ -309,7 +309,7 @@ void shouldThrowException_whenBrandNotFound() {
             // given
             ProductSearchFilter filter = ProductSearchFilter.builder().build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product = createProduct(999L);
             Page<Product> productPage = new PageImpl<>(List.of(product));
@@ -332,7 +332,7 @@ void shouldReturnEmptyPage_whenNoProductsFound() {
             // given
             ProductSearchFilter filter = ProductSearchFilter.builder().build();
             Pageable pageable = PageRequest.of(0, 10);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Page<Product> emptyPage = new PageImpl<>(List.of());
 
@@ -359,7 +359,7 @@ class GetPopularProducts {
         @Test
         void shouldGetPopularProducts_whenValidInput() {
             // given
-            String memberId = "member1";
+            Long memberId = 1L;
 
             // 좋아요 수가 다른 여러 상품 생성
             Product product1 = createProductWithLikeCount(1L, 100);
@@ -402,7 +402,7 @@ void shouldGetPopularProducts_whenValidInput() {
         @Test
         void shouldGetPopularProducts_whenMemberIdIsNull() {
             // given
-            String memberId = null;
+            Long memberId = null;
 
             Product product1 = createProductWithLikeCount(1L, 100);
             Product product2 = createProductWithLikeCount(2L, 80);
@@ -432,7 +432,7 @@ void shouldGetPopularProducts_whenMemberIdIsNull() {
         @Test
         void shouldReturnAllProducts_whenLessThan100Products() {
             // given
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product1 = createProductWithLikeCount(1L, 50);
             Product product2 = createProductWithLikeCount(2L, 30);
@@ -459,7 +459,7 @@ void shouldReturnAllProducts_whenLessThan100Products() {
         @Test
         void shouldReturnEmptyList_whenNoProducts() {
             // given
-            String memberId = "member1";
+            Long memberId = 1L;
 
             given(productRepository.findTopByLikeCount(100)).willReturn(List.of());
 
@@ -484,7 +484,7 @@ class GetBrandPopularProducts {
         void shouldGetBrandPopularProducts_whenValidInput() {
             // given
             Long brandId = 1L;
-            String memberId = "member1";
+            Long memberId = 1L;
             int limit = 5;
 
             Product product1 = createProductWithLikeCount(1L, 100);
@@ -522,7 +522,7 @@ void shouldGetBrandPopularProducts_whenValidInput() {
         void shouldGetBrandPopularProducts_whenMemberIdIsNull() {
             // given
             Long brandId = 1L;
-            String memberId = null;
+            Long memberId = null;
             int limit = 10;
 
             Product product1 = createProductWithLikeCount(1L, 100);
@@ -553,7 +553,7 @@ void shouldGetBrandPopularProducts_whenMemberIdIsNull() {
         void shouldReturnEmptyList_whenNoProducts() {
             // given
             Long brandId = 1L;
-            String memberId = "member1";
+            Long memberId = 1L;
             int limit = 10;
 
             Brand brand = createBrand(brandId);
@@ -577,7 +577,7 @@ void shouldReturnEmptyList_whenNoProducts() {
         void shouldThrowException_whenBrandNotFound() {
             // given
             Long brandId = 999L;
-            String memberId = "member1";
+            Long memberId = 1L;
             int limit = 10;
 
             given(brandRepository.findById(brandId)).willReturn(Optional.empty());
@@ -602,7 +602,7 @@ class Collaboration {
         void shouldCallAllCollaborators_whenGetProductDetail() {
             // given
             Long productId = 1L;
-            String memberId = "member1";
+            Long memberId = 1L;
             Long brandId = 1L;
 
             Product product = createProduct(brandId);
@@ -627,7 +627,7 @@ void shouldCallRepositoryForAllProducts_whenGetProducts() {
             // given
             ProductSearchFilter filter = ProductSearchFilter.builder().build();
             Pageable pageable = PageRequest.of(0, 2);
-            String memberId = "member1";
+            Long memberId = 1L;
 
             Product product1 = createProduct(1L);
             Product product2 = createProduct(2L);
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderV1ApiE2ETest.java
index 92c8172e7..f682ebcf3 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderV1ApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/order/OrderV1ApiE2ETest.java
@@ -70,8 +70,11 @@ private Long setupProductAndMemberWithPoints() {
         // 회원 생성
         memberFacade.registerMember("test123", "test@example.com", "password", "1990-01-01", Gender.MALE);
 
+        // Member ID is 1L (first member created after table truncation)
+        Long memberId = 1L;
+
         // 충분한 포인트 지급 (기존 포인트를 조회하여 추가)
-        Point memberPoint = pointRepository.findByMemberId("test123")
+        Point memberPoint = pointRepository.findByMemberId(memberId)
                 .orElseThrow(() -> new IllegalStateException("회원의 포인트가 존재하지 않습니다"));
         memberPoint.addAmount(BigDecimal.valueOf(50000));
         pointRepository.save(memberPoint);
diff --git a/apps/pg-simulator/README.md b/apps/pg-simulator/README.md
new file mode 100644
index 000000000..118642638
--- /dev/null
+++ b/apps/pg-simulator/README.md
@@ -0,0 +1,42 @@
+## 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`
+
+```http request
+### 결제 요청
+POST {{pg-simulator}}/api/v1/payments
+X-USER-ID: 135135
+Content-Type: application/json
+
+{
+  "orderId": "1351039135",
+  "cardType": "SAMSUNG",
+  "cardNo": "1234-5678-9814-1451",
+  "amount" : "5000",
+  "callbackUrl": "http://localhost:8080/api/v1/examples/callback"
+}
+
+### 결제 정보 확인
+GET {{pg-simulator}}/api/v1/payments/20250816:TR:9577c5
+X-USER-ID: 135135
+
+### 주문에 엮인 결제 정보 조회
+GET {{pg-simulator}}/api/v1/payments?orderId=1351039135
+X-USER-ID: 135135
+
+```
\ No newline at end of file
diff --git a/apps/pg-simulator/build.gradle.kts b/apps/pg-simulator/build.gradle.kts
new file mode 100644
index 000000000..653d549da
--- /dev/null
+++ b/apps/pg-simulator/build.gradle.kts
@@ -0,0 +1,40 @@
+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)
+}
+
+kotlin {
+    compilerOptions {
+        jvmToolchain(21)
+        freeCompilerArgs.addAll("-Xjsr305=strict")
+    }
+}
+
+dependencies {
+    // add-ons
+    implementation(project(":modules:jpa"))
+    implementation(project(":modules:redis"))
+    implementation(project(":supports:jackson"))
+    implementation(project(":supports:logging"))
+    implementation(project(":supports:monitoring"))
+
+    // kotlin
+    implementation("org.jetbrains.kotlin:kotlin-reflect")
+    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+    // web
+    implementation("org.springframework.boot:spring-boot-starter-web")
+    implementation("org.springframework.boot:spring-boot-starter-actuator")
+    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
+
+    // querydsl
+    kapt("com.querydsl:querydsl-apt::jakarta")
+
+    // test-fixtures
+    testImplementation(testFixtures(project(":modules:jpa")))
+    testImplementation(testFixtures(project(":modules:redis")))
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
new file mode 100644
index 000000000..05595d135
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt
@@ -0,0 +1,24 @@
+package com.loopers
+
+import jakarta.annotation.PostConstruct
+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
+
+@ConfigurationPropertiesScan
+@EnableAsync
+@SpringBootApplication
+class PaymentGatewayApplication {
+
+    @PostConstruct
+    fun started() {
+        // set timezone
+        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
+    }
+}
+
+fun main(args: Array<String>) {
+    runApplication<PaymentGatewayApplication>(*args)
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt
new file mode 100644
index 000000000..7e04d1ce0
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/OrderInfo.kt
@@ -0,0 +1,14 @@
+package com.loopers.application.payment
+
+/**
+ * 결제 주문 정보
+ *
+ * 결제는 주문에 대한 다수 트랜잭션으로 구성됩니다.
+ *
+ * @property orderId 주문 정보
+ * @property transactions 주문에 엮인 트랜잭션 목록
+ */
+data class OrderInfo(
+    val orderId: String,
+    val transactions: List<TransactionInfo>,
+)
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
new file mode 100644
index 000000000..9a5ebdc5d
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
@@ -0,0 +1,88 @@
+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.user.UserInfo
+import com.loopers.support.error.CoreException
+import com.loopers.support.error.ErrorType
+import org.springframework.stereotype.Component
+import org.springframework.transaction.annotation.Transactional
+
+@Component
+class PaymentApplicationService(
+    private val paymentRepository: PaymentRepository,
+    private val paymentEventPublisher: PaymentEventPublisher,
+    private val paymentRelay: PaymentRelay,
+    private val transactionKeyGenerator: TransactionKeyGenerator,
+) {
+    companion object {
+        private val RATE_LIMIT_EXCEEDED = (1..20)
+        private val RATE_INVALID_CARD = (21..30)
+    }
+
+    @Transactional
+    fun createTransaction(command: PaymentCommand.CreateTransaction): TransactionInfo {
+        command.validate()
+
+        val transactionKey = transactionKeyGenerator.generate()
+        val payment = paymentRepository.save(
+            Payment(
+                transactionKey = transactionKey,
+                userId = command.userId,
+                orderId = command.orderId,
+                cardType = command.cardType,
+                cardNo = command.cardNo,
+                amount = command.amount,
+                callbackUrl = command.callbackUrl,
+            ),
+        )
+
+        paymentEventPublisher.publish(PaymentEvent.PaymentCreated.from(payment = payment))
+
+        return TransactionInfo.from(payment)
+    }
+
+    @Transactional(readOnly = true)
+    fun getTransactionDetailInfo(userInfo: UserInfo, transactionKey: String): TransactionInfo {
+        val payment = paymentRepository.findByTransactionKey(userId = userInfo.userId, transactionKey = transactionKey)
+            ?: throw CoreException(ErrorType.NOT_FOUND, "(transactionKey: $transactionKey) 결제건이 존재하지 않습니다.")
+        return TransactionInfo.from(payment)
+    }
+
+    @Transactional(readOnly = true)
+    fun findTransactionsByOrderId(userInfo: UserInfo, orderId: String): OrderInfo {
+        val payments = paymentRepository.findByOrderId(userId = userInfo.userId, orderId = orderId)
+        if (payments.isEmpty()) {
+            throw CoreException(ErrorType.NOT_FOUND, "(orderId: $orderId) 에 해당하는 결제건이 존재하지 않습니다.")
+        }
+
+        return OrderInfo(
+            orderId = orderId,
+            transactions = payments.map { TransactionInfo.from(it) },
+        )
+    }
+
+    @Transactional
+    fun handle(transactionKey: String) {
+        val payment = paymentRepository.findByTransactionKey(transactionKey)
+            ?: throw CoreException(ErrorType.NOT_FOUND, "(transactionKey: $transactionKey) 결제건이 존재하지 않습니다.")
+
+        val rate = (1..100).random()
+        when (rate) {
+            in RATE_LIMIT_EXCEEDED -> payment.limitExceeded()
+            in RATE_INVALID_CARD -> payment.invalidCard()
+            else -> payment.approve()
+        }
+        paymentEventPublisher.publish(event = PaymentEvent.PaymentHandled.from(payment))
+    }
+
+    fun notifyTransactionResult(transactionKey: String) {
+        val payment = paymentRepository.findByTransactionKey(transactionKey)
+            ?: throw CoreException(ErrorType.NOT_FOUND, "(transactionKey: $transactionKey) 결제건이 존재하지 않습니다.")
+        paymentRelay.notify(callbackUrl = payment.callbackUrl, transactionInfo = TransactionInfo.from(payment))
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt
new file mode 100644
index 000000000..01d8ae440
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentCommand.kt
@@ -0,0 +1,22 @@
+package com.loopers.application.payment
+
+import com.loopers.domain.payment.CardType
+import com.loopers.support.error.CoreException
+import com.loopers.support.error.ErrorType
+
+object PaymentCommand {
+    data class CreateTransaction(
+        val userId: String,
+        val orderId: String,
+        val cardType: CardType,
+        val cardNo: String,
+        val amount: Long,
+        val callbackUrl: String,
+    ) {
+        fun validate() {
+            if (amount <= 0L) {
+                throw CoreException(ErrorType.BAD_REQUEST, "요청 금액은 0 보다 큰 정수여야 합니다.")
+            }
+        }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt
new file mode 100644
index 000000000..5c21e51af
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/application/payment/TransactionInfo.kt
@@ -0,0 +1,39 @@
+package com.loopers.application.payment
+
+import com.loopers.domain.payment.CardType
+import com.loopers.domain.payment.Payment
+import com.loopers.domain.payment.TransactionStatus
+
+/**
+ * 트랜잭션 정보
+ *
+ * @property transactionKey 트랜잭션 KEY
+ * @property orderId 주문 ID
+ * @property cardType 카드 종류
+ * @property cardNo 카드 번호
+ * @property amount 금액
+ * @property status 처리 상태
+ * @property reason 처리 사유
+ */
+data class TransactionInfo(
+    val transactionKey: String,
+    val orderId: String,
+    val cardType: CardType,
+    val cardNo: String,
+    val amount: Long,
+    val status: TransactionStatus,
+    val reason: String?,
+) {
+    companion object {
+        fun from(payment: Payment): TransactionInfo =
+            TransactionInfo(
+                transactionKey = payment.transactionKey,
+                orderId = payment.orderId,
+                cardType = payment.cardType,
+                cardNo = payment.cardNo,
+                amount = payment.amount,
+                status = payment.status,
+                reason = payment.reason,
+            )
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt
new file mode 100644
index 000000000..8aec9dc82
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/config/web/WebMvcConfig.kt
@@ -0,0 +1,13 @@
+package com.loopers.config.web
+
+import com.loopers.interfaces.api.argumentresolver.UserInfoArgumentResolver
+import org.springframework.context.annotation.Configuration
+import org.springframework.web.method.support.HandlerMethodArgumentResolver
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
+
+@Configuration
+class WebMvcConfig : WebMvcConfigurer {
+    override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver?>) {
+        resolvers.add(UserInfoArgumentResolver())
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt
new file mode 100644
index 000000000..55008a95d
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/CardType.kt
@@ -0,0 +1,7 @@
+package com.loopers.domain.payment
+
+enum class CardType {
+    SAMSUNG,
+    KB,
+    HYUNDAI,
+}
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
new file mode 100644
index 000000000..cfc2386c1
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
@@ -0,0 +1,87 @@
+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 java.time.LocalDateTime
+
+@Entity
+@Table(
+    name = "payments",
+    indexes = [
+        Index(name = "idx_user_transaction", columnList = "user_id, transaction_key"),
+        Index(name = "idx_user_order", columnList = "user_id, order_id"),
+        Index(name = "idx_unique_user_order_transaction", columnList = "user_id, order_id, transaction_key", unique = true),
+    ]
+)
+class Payment(
+    @Id
+    @Column(name = "transaction_key", nullable = false, unique = true)
+    val transactionKey: String,
+
+    @Column(name = "user_id", nullable = false)
+    val userId: String,
+
+    @Column(name = "order_id", nullable = false)
+    val orderId: String,
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "card_type", nullable = false)
+    val cardType: CardType,
+
+    @Column(name = "card_no", nullable = false)
+    val cardNo: String,
+
+    @Column(name = "amount", nullable = false)
+    val amount: Long,
+
+    @Column(name = "callback_url", nullable = false)
+    val callbackUrl: String,
+) {
+    @Enumerated(EnumType.STRING)
+    @Column(name = "status", nullable = false)
+    var status: TransactionStatus = TransactionStatus.PENDING
+        private set
+
+    @Column(name = "reason", nullable = true)
+    var reason: String? = null
+        private set
+
+    @Column(name = "created_at", nullable = false)
+    var createdAt: LocalDateTime = LocalDateTime.now()
+        private set
+
+    @Column(name = "updated_at", nullable = false)
+    var updatedAt: LocalDateTime = LocalDateTime.now()
+        private set
+
+    fun approve() {
+        if (status != TransactionStatus.PENDING) {
+            throw CoreException(ErrorType.INTERNAL_ERROR, "결제승인은 대기상태에서만 가능합니다.")
+        }
+        status = TransactionStatus.SUCCESS
+        reason = "정상 승인되었습니다."
+    }
+
+    fun invalidCard() {
+        if (status != TransactionStatus.PENDING) {
+            throw CoreException(ErrorType.INTERNAL_ERROR, "결제처리는 대기상태에서만 가능합니다.")
+        }
+        status = TransactionStatus.FAILED
+        reason = "잘못된 카드입니다. 다른 카드를 선택해주세요."
+    }
+
+    fun limitExceeded() {
+        if (status != TransactionStatus.PENDING) {
+            throw CoreException(ErrorType.INTERNAL_ERROR, "한도초과 처리는 대기상태에서만 가능합니다.")
+        }
+        status = TransactionStatus.FAILED
+        reason = "한도초과입니다. 다른 카드를 선택해주세요."
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt
new file mode 100644
index 000000000..8e495b2e3
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEvent.kt
@@ -0,0 +1,28 @@
+package com.loopers.domain.payment
+
+object PaymentEvent {
+    data class PaymentCreated(
+        val transactionKey: String,
+    ) {
+        companion object {
+            fun from(payment: Payment): PaymentCreated = PaymentCreated(transactionKey = payment.transactionKey)
+        }
+    }
+
+    data class PaymentHandled(
+        val transactionKey: String,
+        val status: TransactionStatus,
+        val reason: String?,
+        val callbackUrl: String,
+    ) {
+        companion object {
+            fun from(payment: Payment): PaymentHandled =
+                PaymentHandled(
+                    transactionKey = payment.transactionKey,
+                    status = payment.status,
+                    reason = payment.reason,
+                    callbackUrl = payment.callbackUrl,
+                )
+        }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt
new file mode 100644
index 000000000..251c68319
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentEventPublisher.kt
@@ -0,0 +1,6 @@
+package com.loopers.domain.payment
+
+interface PaymentEventPublisher {
+    fun publish(event: PaymentEvent.PaymentCreated)
+    fun publish(event: PaymentEvent.PaymentHandled)
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt
new file mode 100644
index 000000000..e622899b2
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRelay.kt
@@ -0,0 +1,7 @@
+package com.loopers.domain.payment
+
+import com.loopers.application.payment.TransactionInfo
+
+interface PaymentRelay {
+    fun notify(callbackUrl: String, transactionInfo: TransactionInfo)
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt
new file mode 100644
index 000000000..c1173c0aa
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/PaymentRepository.kt
@@ -0,0 +1,8 @@
+package com.loopers.domain.payment
+
+interface PaymentRepository {
+    fun save(payment: Payment): Payment
+    fun findByTransactionKey(transactionKey: String): Payment?
+    fun findByTransactionKey(userId: String, transactionKey: String): Payment?
+    fun findByOrderId(userId: String, orderId: String): List<Payment>
+}
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
new file mode 100644
index 000000000..c8703a763
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt
@@ -0,0 +1,20 @@
+package com.loopers.domain.payment
+
+import org.springframework.stereotype.Component
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.UUID
+
+@Component
+class TransactionKeyGenerator {
+    companion object {
+        private const val KEY_TRANSACTION = "TR"
+        private val DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd")
+    }
+
+    fun generate(): String {
+        val now = LocalDateTime.now()
+        val uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 6)
+        return "${DATETIME_FORMATTER.format(now)}:$KEY_TRANSACTION:$uuid"
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt
new file mode 100644
index 000000000..0c94bcfb9
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionStatus.kt
@@ -0,0 +1,7 @@
+package com.loopers.domain.payment
+
+enum class TransactionStatus {
+    PENDING,
+    SUCCESS,
+    FAILED
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt
new file mode 100644
index 000000000..c51e660a9
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/domain/user/UserInfo.kt
@@ -0,0 +1,8 @@
+package com.loopers.domain.user
+
+/**
+ * user 정보
+ *
+ * @param userId 유저 식별자
+ */
+data class UserInfo(val userId: String)
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt
new file mode 100644
index 000000000..715516360
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreEventPublisher.kt
@@ -0,0 +1,19 @@
+package com.loopers.infrastructure.payment
+
+import com.loopers.domain.payment.PaymentEvent
+import com.loopers.domain.payment.PaymentEventPublisher
+import org.springframework.context.ApplicationEventPublisher
+import org.springframework.stereotype.Component
+
+@Component
+class PaymentCoreEventPublisher(
+    private val applicationEventPublisher: ApplicationEventPublisher,
+) : PaymentEventPublisher {
+    override fun publish(event: PaymentEvent.PaymentCreated) {
+        applicationEventPublisher.publishEvent(event)
+    }
+
+    override fun publish(event: PaymentEvent.PaymentHandled) {
+        applicationEventPublisher.publishEvent(event)
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt
new file mode 100644
index 000000000..ffd643c0f
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRelay.kt
@@ -0,0 +1,21 @@
+package com.loopers.infrastructure.payment
+
+import com.loopers.application.payment.TransactionInfo
+import com.loopers.domain.payment.PaymentRelay
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import org.springframework.web.client.RestTemplate
+
+@Component
+class PaymentCoreRelay : PaymentRelay {
+    companion object {
+        private val logger = LoggerFactory.getLogger(PaymentCoreRelay::class.java)
+        private val restTemplate = RestTemplate()
+    }
+
+    override fun notify(callbackUrl: String, transactionInfo: TransactionInfo) {
+        runCatching {
+            restTemplate.postForEntity(callbackUrl, transactionInfo, Any::class.java)
+        }.onFailure { e -> logger.error("콜백 호출을 실패했습니다. {}", e.message, e) }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt
new file mode 100644
index 000000000..cf521c47d
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentCoreRepository.kt
@@ -0,0 +1,32 @@
+package com.loopers.infrastructure.payment
+
+import com.loopers.domain.payment.Payment
+import com.loopers.domain.payment.PaymentRepository
+import org.springframework.stereotype.Component
+import org.springframework.transaction.annotation.Transactional
+import kotlin.jvm.optionals.getOrNull
+
+@Component
+class PaymentCoreRepository(
+    private val paymentJpaRepository: PaymentJpaRepository,
+) : PaymentRepository {
+    @Transactional
+    override fun save(payment: Payment): Payment {
+        return paymentJpaRepository.save(payment)
+    }
+
+    @Transactional(readOnly = true)
+    override fun findByTransactionKey(transactionKey: String): Payment? {
+        return paymentJpaRepository.findById(transactionKey).getOrNull()
+    }
+
+    @Transactional(readOnly = true)
+    override fun findByTransactionKey(userId: String, transactionKey: String): Payment? {
+        return paymentJpaRepository.findByUserIdAndTransactionKey(userId, transactionKey)
+    }
+
+    override fun findByOrderId(userId: String, orderId: String): List<Payment> {
+        return paymentJpaRepository.findByUserIdAndOrderId(userId, orderId)
+            .sortedByDescending { it.updatedAt }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt
new file mode 100644
index 000000000..a5ea32822
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/infrastructure/payment/PaymentJpaRepository.kt
@@ -0,0 +1,9 @@
+package com.loopers.infrastructure.payment
+
+import com.loopers.domain.payment.Payment
+import org.springframework.data.jpa.repository.JpaRepository
+
+interface PaymentJpaRepository : JpaRepository<Payment, String> {
+    fun findByUserIdAndTransactionKey(userId: String, transactionKey: String): Payment?
+    fun findByUserIdAndOrderId(userId: String, orderId: String): List<Payment>
+}
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
new file mode 100644
index 000000000..434a229e2
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
@@ -0,0 +1,119 @@
+package com.loopers.interfaces.api
+
+import com.fasterxml.jackson.databind.JsonMappingException
+import com.fasterxml.jackson.databind.exc.InvalidFormatException
+import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.loopers.support.error.CoreException
+import com.loopers.support.error.ErrorType
+import org.slf4j.LoggerFactory
+import org.springframework.http.ResponseEntity
+import org.springframework.http.converter.HttpMessageNotReadableException
+import org.springframework.web.bind.MissingServletRequestParameterException
+import org.springframework.web.bind.annotation.ExceptionHandler
+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 {
+    private val log = LoggerFactory.getLogger(ApiControllerAdvice::class.java)
+
+    @ExceptionHandler
+    fun handle(e: CoreException): ResponseEntity<ApiResponse<*>> {
+        log.warn("CoreException : {}", e.customMessage ?: e.message, e)
+        return failureResponse(errorType = e.errorType, errorMessage = e.customMessage)
+    }
+
+    @ExceptionHandler
+    fun handleBadRequest(e: MethodArgumentTypeMismatchException): ResponseEntity<ApiResponse<*>> {
+        val name = e.name
+        val type = e.requiredType?.simpleName ?: "unknown"
+        val value = e.value ?: "null"
+        val message = "요청 파라미터 '$name' (타입: $type)의 값 '$value'이(가) 잘못되었습니다."
+        return failureResponse(errorType = ErrorType.BAD_REQUEST, errorMessage = message)
+    }
+
+    @ExceptionHandler
+    fun handleBadRequest(e: MissingServletRequestParameterException): ResponseEntity<ApiResponse<*>> {
+        val name = e.parameterName
+        val type = e.parameterType
+        val message = "필수 요청 파라미터 '$name' (타입: $type)가 누락되었습니다."
+        return failureResponse(errorType = ErrorType.BAD_REQUEST, errorMessage = message)
+    }
+
+    @ExceptionHandler
+    fun handleBadRequest(e: HttpMessageNotReadableException): ResponseEntity<ApiResponse<*>> {
+        val errorMessage = when (val rootCause = e.rootCause) {
+            is InvalidFormatException -> {
+                val fieldName = rootCause.path.joinToString(".") { it.fieldName ?: "?" }
+
+                val valueIndicationMessage = when {
+                    rootCause.targetType.isEnum -> {
+                        val enumClass = rootCause.targetType
+                        val enumValues = enumClass.enumConstants.joinToString(", ") { it.toString() }
+                        "사용 가능한 값 : [$enumValues]"
+                    }
+
+                    else -> ""
+                }
+
+                val expectedType = rootCause.targetType.simpleName
+                val value = rootCause.value
+
+                "필드 '$fieldName'의 값 '$value'이(가) 예상 타입($expectedType)과 일치하지 않습니다. $valueIndicationMessage"
+            }
+
+            is MismatchedInputException -> {
+                val fieldPath = rootCause.path.joinToString(".") { it.fieldName ?: "?" }
+                "필수 필드 '$fieldPath'이(가) 누락되었습니다."
+            }
+
+            is JsonMappingException -> {
+                val fieldPath = rootCause.path.joinToString(".") { it.fieldName ?: "?" }
+                "필드 '$fieldPath'에서 JSON 매핑 오류가 발생했습니다: ${rootCause.originalMessage}"
+            }
+
+            else -> "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요."
+        }
+
+        return failureResponse(errorType = ErrorType.BAD_REQUEST, errorMessage = errorMessage)
+    }
+
+    @ExceptionHandler
+    fun handleBadRequest(e: ServerWebInputException): ResponseEntity<ApiResponse<*>> {
+        fun extractMissingParameter(message: String): String {
+            val regex = "'(.+?)'".toRegex()
+            return regex.find(message)?.groupValues?.get(1) ?: ""
+        }
+
+        val missingParams = extractMissingParameter(e.reason ?: "")
+        return if (missingParams.isNotEmpty()) {
+            failureResponse(errorType = ErrorType.BAD_REQUEST, errorMessage = "필수 요청 값 \'$missingParams\'가 누락되었습니다.")
+        } else {
+            failureResponse(errorType = ErrorType.BAD_REQUEST)
+        }
+    }
+
+    @ExceptionHandler
+    fun handleNotFound(e: NoResourceFoundException): ResponseEntity<ApiResponse<*>> {
+        return failureResponse(errorType = ErrorType.NOT_FOUND)
+    }
+
+    @ExceptionHandler
+    fun handle(e: Throwable): ResponseEntity<ApiResponse<*>> {
+        log.error("Exception : {}", e.message, e)
+        val errorType = ErrorType.INTERNAL_ERROR
+        return failureResponse(errorType = errorType)
+    }
+
+    private fun failureResponse(errorType: ErrorType, errorMessage: String? = null): ResponseEntity<ApiResponse<*>> =
+        ResponseEntity(
+            ApiResponse.fail(errorCode = errorType.code, errorMessage = errorMessage ?: errorType.message),
+            errorType.status,
+        )
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt
new file mode 100644
index 000000000..f5c38ab5e
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt
@@ -0,0 +1,32 @@
+package com.loopers.interfaces.api
+
+data class ApiResponse<T>(
+    val meta: Metadata,
+    val data: T?,
+) {
+    data class Metadata(
+        val result: Result,
+        val errorCode: String?,
+        val message: String?,
+    ) {
+        enum class Result { SUCCESS, FAIL }
+
+        companion object {
+            fun success() = Metadata(Result.SUCCESS, null, null)
+
+            fun fail(errorCode: String, errorMessage: String) = Metadata(Result.FAIL, errorCode, errorMessage)
+        }
+    }
+
+    companion object {
+        fun success(): ApiResponse<Any> = ApiResponse(Metadata.success(), null)
+
+        fun <T> success(data: T? = null) = ApiResponse(Metadata.success(), data)
+
+        fun fail(errorCode: String, errorMessage: String): ApiResponse<Any?> =
+            ApiResponse(
+                meta = Metadata.fail(errorCode = errorCode, errorMessage = errorMessage),
+                data = null,
+            )
+    }
+}
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
new file mode 100644
index 000000000..9ef6c25da
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/argumentresolver/UserInfoArgumentResolver.kt
@@ -0,0 +1,32 @@
+package com.loopers.interfaces.api.argumentresolver
+
+import com.loopers.domain.user.UserInfo
+import com.loopers.support.error.CoreException
+import com.loopers.support.error.ErrorType
+import org.springframework.core.MethodParameter
+import org.springframework.web.bind.support.WebDataBinderFactory
+import org.springframework.web.context.request.NativeWebRequest
+import org.springframework.web.method.support.HandlerMethodArgumentResolver
+import org.springframework.web.method.support.ModelAndViewContainer
+
+class UserInfoArgumentResolver: HandlerMethodArgumentResolver {
+    companion object {
+        private const val KEY_USER_ID = "X-USER-ID"
+    }
+
+    override fun supportsParameter(parameter: MethodParameter): Boolean {
+        return UserInfo::class.java.isAssignableFrom(parameter.parameterType)
+    }
+
+    override fun resolveArgument(
+        parameter: MethodParameter,
+        mavContainer: ModelAndViewContainer?,
+        webRequest: NativeWebRequest,
+        binderFactory: WebDataBinderFactory?,
+    ): UserInfo {
+        val userId = webRequest.getHeader(KEY_USER_ID)
+            ?: throw CoreException(ErrorType.BAD_REQUEST, "유저 ID 헤더는 필수입니다.")
+
+        return UserInfo(userId)
+    }
+}
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
new file mode 100644
index 000000000..22d5cbe38
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
@@ -0,0 +1,60 @@
+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.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
+
+@RestController
+@RequestMapping("/api/v1/payments")
+class PaymentApi(
+    private val paymentApplicationService: PaymentApplicationService,
+) {
+    @PostMapping
+    fun request(
+        userInfo: UserInfo,
+        @RequestBody request: PaymentDto.PaymentRequest,
+    ): ApiResponse<PaymentDto.TransactionResponse> {
+        request.validate()
+
+        // 100ms ~ 500ms 지연
+        Thread.sleep((100..500L).random())
+
+        // 40% 확률로 요청 실패
+        if ((1..100).random() <= 40) {
+            throw CoreException(ErrorType.INTERNAL_ERROR, "현재 서버가 불안정합니다. 잠시 후 다시 시도해주세요.")
+        }
+
+        return paymentApplicationService.createTransaction(request.toCommand(userInfo.userId))
+            .let { PaymentDto.TransactionResponse.from(it) }
+            .let { ApiResponse.success(it) }
+    }
+
+    @GetMapping("/{transactionKey}")
+    fun getTransaction(
+        userInfo: UserInfo,
+        @PathVariable("transactionKey") transactionKey: String,
+    ): ApiResponse<PaymentDto.TransactionDetailResponse> {
+        return paymentApplicationService.getTransactionDetailInfo(userInfo, transactionKey)
+            .let { PaymentDto.TransactionDetailResponse.from(it) }
+            .let { ApiResponse.success(it) }
+    }
+
+    @GetMapping
+    fun getTransactionsByOrder(
+        userInfo: UserInfo,
+        @RequestParam("orderId", required = false) orderId: String,
+    ): ApiResponse<PaymentDto.OrderResponse> {
+        return paymentApplicationService.findTransactionsByOrderId(userInfo, orderId)
+            .let { PaymentDto.OrderResponse.from(it) }
+            .let { ApiResponse.success(it) }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt
new file mode 100644
index 000000000..52a00b156
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentDto.kt
@@ -0,0 +1,136 @@
+package com.loopers.interfaces.api.payment
+
+import com.loopers.application.payment.OrderInfo
+import com.loopers.application.payment.PaymentCommand
+import com.loopers.application.payment.TransactionInfo
+import com.loopers.domain.payment.CardType
+import com.loopers.domain.payment.TransactionStatus
+import com.loopers.support.error.CoreException
+import com.loopers.support.error.ErrorType
+
+object PaymentDto {
+    data class PaymentRequest(
+        val orderId: String,
+        val cardType: CardTypeDto,
+        val cardNo: String,
+        val amount: Long,
+        val callbackUrl: String,
+    ) {
+        companion object {
+            private val REGEX_CARD_NO = Regex("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$")
+            private const val PREFIX_CALLBACK_URL = "http://localhost:8080"
+        }
+
+        fun validate() {
+            if (orderId.isBlank() || orderId.length < 6) {
+                throw CoreException(ErrorType.BAD_REQUEST, "주문 ID는 6자리 이상 문자열이어야 합니다.")
+            }
+            if (!REGEX_CARD_NO.matches(cardNo)) {
+                throw CoreException(ErrorType.BAD_REQUEST, "카드 번호는 xxxx-xxxx-xxxx-xxxx 형식이어야 합니다.")
+            }
+            if (amount <= 0) {
+                throw CoreException(ErrorType.BAD_REQUEST, "결제금액은 양의 정수여야 합니다.")
+            }
+            if (!callbackUrl.startsWith(PREFIX_CALLBACK_URL)) {
+                throw CoreException(ErrorType.BAD_REQUEST, "콜백 URL 은 $PREFIX_CALLBACK_URL 로 시작해야 합니다.")
+            }
+        }
+
+        fun toCommand(userId: String): PaymentCommand.CreateTransaction =
+            PaymentCommand.CreateTransaction(
+                userId = userId,
+                orderId = orderId,
+                cardType = cardType.toCardType(),
+                cardNo = cardNo,
+                amount = amount,
+                callbackUrl = callbackUrl,
+            )
+    }
+
+    data class TransactionDetailResponse(
+        val transactionKey: String,
+        val orderId: String,
+        val cardType: CardTypeDto,
+        val cardNo: String,
+        val amount: Long,
+        val status: TransactionStatusResponse,
+        val reason: String?,
+    ) {
+        companion object {
+            fun from(transactionInfo: TransactionInfo): TransactionDetailResponse =
+                TransactionDetailResponse(
+                    transactionKey = transactionInfo.transactionKey,
+                    orderId = transactionInfo.orderId,
+                    cardType = CardTypeDto.from(transactionInfo.cardType),
+                    cardNo = transactionInfo.cardNo,
+                    amount = transactionInfo.amount,
+                    status = TransactionStatusResponse.from(transactionInfo.status),
+                    reason = transactionInfo.reason,
+                )
+        }
+    }
+
+    data class TransactionResponse(
+        val transactionKey: String,
+        val status: TransactionStatusResponse,
+        val reason: String?,
+    ) {
+        companion object {
+            fun from(transactionInfo: TransactionInfo): TransactionResponse =
+                TransactionResponse(
+                    transactionKey = transactionInfo.transactionKey,
+                    status = TransactionStatusResponse.from(transactionInfo.status),
+                    reason = transactionInfo.reason,
+                )
+        }
+    }
+
+    data class OrderResponse(
+        val orderId: String,
+        val transactions: List<TransactionResponse>,
+    ) {
+        companion object {
+            fun from(orderInfo: OrderInfo): OrderResponse =
+                OrderResponse(
+                    orderId = orderInfo.orderId,
+                    transactions = orderInfo.transactions.map { TransactionResponse.from(it) },
+                )
+        }
+    }
+
+    enum class CardTypeDto {
+        SAMSUNG,
+        KB,
+        HYUNDAI,
+        ;
+
+        fun toCardType(): CardType = when (this) {
+            SAMSUNG -> CardType.SAMSUNG
+            KB -> CardType.KB
+            HYUNDAI -> CardType.HYUNDAI
+        }
+
+        companion object {
+            fun from(cardType: CardType) = when (cardType) {
+                CardType.SAMSUNG -> SAMSUNG
+                CardType.KB -> KB
+                CardType.HYUNDAI -> HYUNDAI
+            }
+        }
+    }
+
+    enum class TransactionStatusResponse {
+        PENDING,
+        SUCCESS,
+        FAILED,
+        ;
+
+        companion object {
+            fun from(transactionStatus: TransactionStatus) = when (transactionStatus) {
+                TransactionStatus.PENDING -> PENDING
+                TransactionStatus.SUCCESS -> SUCCESS
+                TransactionStatus.FAILED -> FAILED
+            }
+        }
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt
new file mode 100644
index 000000000..241322890
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/event/payment/PaymentEventListener.kt
@@ -0,0 +1,28 @@
+package com.loopers.interfaces.event.payment
+
+import com.loopers.application.payment.PaymentApplicationService
+import com.loopers.domain.payment.PaymentEvent
+import org.springframework.scheduling.annotation.Async
+import org.springframework.stereotype.Component
+import org.springframework.transaction.event.TransactionPhase
+import org.springframework.transaction.event.TransactionalEventListener
+
+@Component
+class PaymentEventListener(
+    private val paymentApplicationService: PaymentApplicationService,
+) {
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    fun handle(event: PaymentEvent.PaymentCreated) {
+        val thresholdMillis = (1000L..5000L).random()
+        Thread.sleep(thresholdMillis)
+
+        paymentApplicationService.handle(event.transactionKey)
+    }
+
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    fun handle(event: PaymentEvent.PaymentHandled) {
+        paymentApplicationService.notifyTransactionResult(transactionKey = event.transactionKey)
+    }
+}
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt
new file mode 100644
index 000000000..120f7fc5f
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt
@@ -0,0 +1,6 @@
+package com.loopers.support.error
+
+class CoreException(
+    val errorType: ErrorType,
+    val customMessage: String? = null,
+) : RuntimeException(customMessage ?: errorType.message)
diff --git a/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt b/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt
new file mode 100644
index 000000000..e0799a5ea
--- /dev/null
+++ b/apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt
@@ -0,0 +1,11 @@
+package com.loopers.support.error
+
+import org.springframework.http.HttpStatus
+
+enum class ErrorType(val status: HttpStatus, val code: String, val message: String) {
+    /** 범용 에러 */
+    INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.reasonPhrase, "일시적인 오류가 발생했습니다."),
+    BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.reasonPhrase, "잘못된 요청입니다."),
+    NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.reasonPhrase, "존재하지 않는 요청입니다."),
+    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.reasonPhrase, "이미 존재하는 리소스입니다."),
+}
diff --git a/apps/pg-simulator/src/main/resources/application.yml b/apps/pg-simulator/src/main/resources/application.yml
new file mode 100644
index 000000000..6482d9fdf
--- /dev/null
+++ b/apps/pg-simulator/src/main/resources/application.yml
@@ -0,0 +1,79 @@
+server:
+  shutdown: graceful
+  tomcat:
+    threads:
+      max: 200 # 최대 워커 스레드 수 (default : 200)
+      min-spare: 10 # 최소 유지 스레드 수 (default : 10)
+    connection-timeout: 1m # 연결 타임아웃 (ms) (default : 60000ms = 1m)
+    max-connections: 8192 # 최대 동시 연결 수 (default : 8192)
+    accept-count: 100 # 대기 큐 크기 (default : 100)
+    keep-alive-timeout: 60s # 60s
+  max-http-request-header-size: 8KB
+
+spring:
+  main:
+    web-application-type: servlet
+  application:
+    name: pg-simulator
+  profiles:
+    active: local
+  config:
+    import:
+      - jpa.yml
+      - redis.yml
+      - logging.yml
+      - monitoring.yml
+
+datasource:
+  mysql-jpa:
+    main:
+      jdbc-url: jdbc:mysql://localhost:3306/paymentgateway
+      username: application
+      password: application
+
+springdoc:
+  use-fqn: true
+  swagger-ui:
+    path: /swagger-ui.html
+
+---
+spring:
+  config:
+    activate:
+      on-profile: local, test
+
+server:
+  port: 8082
+
+management:
+  server:
+    port: 8083
+
+---
+spring:
+  config:
+    activate:
+      on-profile: dev
+
+server:
+  port: 8082
+
+management:
+  server:
+    port: 8083
+
+---
+spring:
+  config:
+    activate:
+      on-profile: qa
+
+---
+spring:
+  config:
+    activate:
+      on-profile: prd
+
+springdoc:
+  api-docs:
+    enabled: false
\ No newline at end of file
diff --git a/docker/grafana/provisioning/dashboards/dashboard.yml b/docker/grafana/provisioning/dashboards/dashboard.yml
new file mode 100644
index 000000000..cead8be26
--- /dev/null
+++ b/docker/grafana/provisioning/dashboards/dashboard.yml
@@ -0,0 +1,12 @@
+apiVersion: 1
+
+providers:
+  - name: 'default'
+    orgId: 1
+    folder: ''
+    type: file
+    disableDeletion: false
+    updateIntervalSeconds: 10
+    allowUiUpdates: true
+    options:
+      path: /etc/grafana/provisioning/dashboards/json
diff --git a/docker/grafana/provisioning/dashboards/json/circuit-breaker.json b/docker/grafana/provisioning/dashboards/json/circuit-breaker.json
new file mode 100644
index 000000000..0277005aa
--- /dev/null
+++ b/docker/grafana/provisioning/dashboards/json/circuit-breaker.json
@@ -0,0 +1,403 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "links": [],
+  "panels": [
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [
+            {
+              "options": {
+                "0": {
+                  "text": "CLOSED"
+                },
+                "1": {
+                  "text": "OPEN"
+                },
+                "2": {
+                  "text": "HALF_OPEN"
+                }
+              },
+              "type": "value"
+            }
+          ],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 1
+              },
+              {
+                "color": "yellow",
+                "value": 2
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 10,
+      "options": {
+        "orientation": "auto",
+        "reduceOptions": {
+          "values": false,
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": ""
+        },
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "text": {}
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_state{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "{{name}}",
+          "refId": "A"
+        }
+      ],
+      "title": "Circuit Breaker State",
+      "type": "gauge"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "hiddenSeries": false,
+      "id": 11,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{application=\"commerce-api\", kind=\"successful\"}[1m])",
+          "interval": "",
+          "legendFormat": "Successful - {{name}}",
+          "refId": "A"
+        },
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{application=\"commerce-api\", kind=\"failed\"}[1m])",
+          "interval": "",
+          "legendFormat": "Failed - {{name}}",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Circuit Breaker Call Rate",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "reqps",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 8
+      },
+      "hiddenSeries": false,
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_failure_rate{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Failure Rate - {{name}}",
+          "refId": "A"
+        },
+        {
+          "expr": "resilience4j_circuitbreaker_slow_call_rate{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Slow Call Rate - {{name}}",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Circuit Breaker Failure & Slow Call Rate",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percentunit",
+          "label": null,
+          "logBase": 1,
+          "max": "1",
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "hiddenSeries": false,
+      "id": 13,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{application=\"commerce-api\", kind=\"not_permitted\"}[1m])",
+          "interval": "",
+          "legendFormat": "Not Permitted - {{name}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Circuit Breaker Rejected Calls",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "reqps",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "schemaVersion": 27,
+  "style": "dark",
+  "tags": ["circuit-breaker", "resilience4j"],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-15m",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Circuit Breaker Monitoring",
+  "uid": "circuit-breaker-monitoring",
+  "version": 0
+}
diff --git a/docker/grafana/provisioning/dashboards/json/resilience4j-detailed.json b/docker/grafana/provisioning/dashboards/json/resilience4j-detailed.json
new file mode 100644
index 000000000..63c4796ad
--- /dev/null
+++ b/docker/grafana/provisioning/dashboards/json/resilience4j-detailed.json
@@ -0,0 +1,970 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 1,
+  "id": null,
+  "links": [],
+  "panels": [
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 100,
+      "panels": [],
+      "title": "Circuit Breaker 상태 (Overview)",
+      "type": "row"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "mappings": [
+            {
+              "options": {
+                "0": {
+                  "color": "green",
+                  "text": "CLOSED"
+                },
+                "1": {
+                  "color": "red",
+                  "text": "OPEN"
+                },
+                "2": {
+                  "color": "yellow",
+                  "text": "HALF_OPEN"
+                }
+              },
+              "type": "value"
+            }
+          ],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 1
+              },
+              {
+                "color": "yellow",
+                "value": 2
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 0,
+        "y": 1
+      },
+      "id": 1,
+      "options": {
+        "orientation": "auto",
+        "reduceOptions": {
+          "values": false,
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": ""
+        },
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "text": {}
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_state{name=\"pgSimulator\",application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "{{name}}",
+          "refId": "A"
+        }
+      ],
+      "title": "Circuit State",
+      "type": "gauge"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "max": 100,
+          "min": 0,
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "yellow",
+                "value": 40
+              },
+              {
+                "color": "red",
+                "value": 60
+              }
+            ]
+          },
+          "unit": "percent"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 6,
+        "y": 1
+      },
+      "id": 2,
+      "options": {
+        "orientation": "auto",
+        "reduceOptions": {
+          "values": false,
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": ""
+        },
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "text": {}
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_failure_rate{name=\"pgSimulator\",application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Failure Rate",
+          "refId": "A"
+        }
+      ],
+      "title": "Failure Rate",
+      "type": "gauge"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "thresholds"
+          },
+          "max": 100,
+          "min": 0,
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "yellow",
+                "value": 20
+              },
+              {
+                "color": "red",
+                "value": 50
+              }
+            ]
+          },
+          "unit": "percent"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 12,
+        "y": 1
+      },
+      "id": 3,
+      "options": {
+        "orientation": "auto",
+        "reduceOptions": {
+          "values": false,
+          "calcs": [
+            "lastNotNull"
+          ],
+          "fields": ""
+        },
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "text": {}
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_slow_call_rate{name=\"pgSimulator\",application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Slow Call Rate",
+          "refId": "A"
+        }
+      ],
+      "title": "Slow Call Rate",
+      "type": "gauge"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 1,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 6,
+        "x": 18,
+        "y": 1
+      },
+      "id": 4,
+      "options": {
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "resilience4j_circuitbreaker_buffered_calls{name=\"pgSimulator\",application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Buffered Calls",
+          "refId": "A"
+        }
+      ],
+      "title": "Buffered Calls (Window Size)",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 9
+      },
+      "id": 101,
+      "panels": [],
+      "title": "호출 통계 (Call Statistics)",
+      "type": "row"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "calls/sec",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "normal"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "reqps"
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Successful"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "green",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failed"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Not Permitted (Circuit OPEN)"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "orange",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 10
+      },
+      "id": 5,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "last"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "multi"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful\"}[1m])",
+          "interval": "",
+          "legendFormat": "Successful",
+          "refId": "A"
+        },
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"failed\"}[1m])",
+          "interval": "",
+          "legendFormat": "Failed",
+          "refId": "B"
+        },
+        {
+          "expr": "rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"not_permitted\"}[1m])",
+          "interval": "",
+          "legendFormat": "Not Permitted (Circuit OPEN)",
+          "refId": "C"
+        }
+      ],
+      "title": "Call Rate by Result",
+      "type": "timeseries"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "max": 100,
+          "min": 0,
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 60
+              }
+            ]
+          },
+          "unit": "percent"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 10
+      },
+      "id": 6,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "last"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "100 * rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful\"}[1m]) / (rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful\"}[1m]) + rate(resilience4j_circuitbreaker_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"failed\"}[1m]))",
+          "interval": "",
+          "legendFormat": "Success Rate %",
+          "refId": "A"
+        }
+      ],
+      "title": "Success Rate %",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 102,
+      "panels": [],
+      "title": "응답 시간 (Response Time)",
+      "type": "row"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 19
+      },
+      "id": 7,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "max"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "multi"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "histogram_quantile(0.50, rate(resilience4j_circuitbreaker_calls_seconds_bucket{name=\"pgSimulator\",application=\"commerce-api\"}[1m]))",
+          "interval": "",
+          "legendFormat": "p50",
+          "refId": "A"
+        },
+        {
+          "expr": "histogram_quantile(0.95, rate(resilience4j_circuitbreaker_calls_seconds_bucket{name=\"pgSimulator\",application=\"commerce-api\"}[1m]))",
+          "interval": "",
+          "legendFormat": "p95",
+          "refId": "B"
+        },
+        {
+          "expr": "histogram_quantile(0.99, rate(resilience4j_circuitbreaker_calls_seconds_bucket{name=\"pgSimulator\",application=\"commerce-api\"}[1m]))",
+          "interval": "",
+          "legendFormat": "p99",
+          "refId": "C"
+        }
+      ],
+      "title": "Response Time Percentiles",
+      "type": "timeseries"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 19
+      },
+      "id": 8,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "last"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "rate(resilience4j_circuitbreaker_slow_call_rate{name=\"pgSimulator\",application=\"commerce-api\"}[1m])",
+          "interval": "",
+          "legendFormat": "Slow Calls",
+          "refId": "A"
+        }
+      ],
+      "title": "Slow Calls Count",
+      "type": "timeseries"
+    },
+    {
+      "collapsed": false,
+      "datasource": null,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 27
+      },
+      "id": 103,
+      "panels": [],
+      "title": "Retry 분석 (Retry Analysis)",
+      "type": "row"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "normal"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "reqps"
+        },
+        "overrides": [
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Success Without Retry"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "green",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Success With Retry"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "yellow",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          },
+          {
+            "matcher": {
+              "id": "byName",
+              "options": "Failed With Retry"
+            },
+            "properties": [
+              {
+                "id": "color",
+                "value": {
+                  "fixedColor": "red",
+                  "mode": "fixed"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 28
+      },
+      "id": 9,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "last"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "multi"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful_without_retry\"}[1m])",
+          "interval": "",
+          "legendFormat": "Success Without Retry",
+          "refId": "A"
+        },
+        {
+          "expr": "rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful_with_retry\"}[1m])",
+          "interval": "",
+          "legendFormat": "Success With Retry",
+          "refId": "B"
+        },
+        {
+          "expr": "rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"failed_with_retry\"}[1m])",
+          "interval": "",
+          "legendFormat": "Failed With Retry",
+          "refId": "C"
+        }
+      ],
+      "title": "Retry Results",
+      "type": "timeseries"
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "color": {
+            "mode": "palette-classic"
+          },
+          "custom": {
+            "axisLabel": "",
+            "axisPlacement": "auto",
+            "barAlignment": 0,
+            "drawStyle": "line",
+            "fillOpacity": 10,
+            "gradientMode": "none",
+            "hideFrom": {
+              "tooltip": false,
+              "viz": false,
+              "legend": false
+            },
+            "lineInterpolation": "linear",
+            "lineWidth": 2,
+            "pointSize": 5,
+            "scaleDistribution": {
+              "type": "linear"
+            },
+            "showPoints": "never",
+            "spanNulls": true,
+            "stacking": {
+              "group": "A",
+              "mode": "none"
+            },
+            "thresholdsStyle": {
+              "mode": "off"
+            }
+          },
+          "max": 100,
+          "min": 0,
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          },
+          "unit": "percent"
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 28
+      },
+      "id": 10,
+      "options": {
+        "legend": {
+          "calcs": ["mean", "last"],
+          "displayMode": "table",
+          "placement": "bottom"
+        },
+        "tooltip": {
+          "mode": "single"
+        }
+      },
+      "pluginVersion": "8.0.0",
+      "targets": [
+        {
+          "expr": "100 * rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful_with_retry\"}[1m]) / (rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"successful_with_retry\"}[1m]) + rate(resilience4j_retry_calls_seconds_count{name=\"pgSimulator\",application=\"commerce-api\",kind=\"failed_with_retry\"}[1m]))",
+          "interval": "",
+          "legendFormat": "Retry Success Rate %",
+          "refId": "A"
+        }
+      ],
+      "title": "Retry Success Rate",
+      "type": "timeseries"
+    }
+  ],
+  "schemaVersion": 27,
+  "style": "dark",
+  "tags": ["resilience4j", "circuit-breaker", "retry", "detailed"],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-15m",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Resilience4j - Detailed Analysis",
+  "uid": "resilience4j-detailed",
+  "version": 0
+}
diff --git a/docker/grafana/provisioning/dashboards/json/spring-boot-load-test.json b/docker/grafana/provisioning/dashboards/json/spring-boot-load-test.json
new file mode 100644
index 000000000..88ba6dd6b
--- /dev/null
+++ b/docker/grafana/provisioning/dashboards/json/spring-boot-load-test.json
@@ -0,0 +1,829 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "id": null,
+  "links": [],
+  "panels": [
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "hiddenSeries": false,
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "rate(http_server_requests_seconds_count{application=\"commerce-api\"}[1m])",
+          "interval": "",
+          "legendFormat": "{{uri}} - {{method}} - {{status}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "HTTP Requests Rate (req/sec)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "reqps",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "hiddenSeries": false,
+      "id": 3,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{application=\"commerce-api\"}[1m])) by (uri, le))",
+          "interval": "",
+          "legendFormat": "p50 - {{uri}}",
+          "refId": "A"
+        },
+        {
+          "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{application=\"commerce-api\"}[1m])) by (uri, le))",
+          "interval": "",
+          "legendFormat": "p95 - {{uri}}",
+          "refId": "B"
+        },
+        {
+          "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application=\"commerce-api\"}[1m])) by (uri, le))",
+          "interval": "",
+          "legendFormat": "p99 - {{uri}}",
+          "refId": "C"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "HTTP Response Time (p50, p95, p99)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "s",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 8
+      },
+      "hiddenSeries": false,
+      "id": 4,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "jvm_memory_used_bytes{application=\"commerce-api\", area=\"heap\"}",
+          "interval": "",
+          "legendFormat": "Used - {{id}}",
+          "refId": "A"
+        },
+        {
+          "expr": "jvm_memory_max_bytes{application=\"commerce-api\", area=\"heap\"}",
+          "interval": "",
+          "legendFormat": "Max - {{id}}",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "JVM Heap Memory",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "bytes",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "hiddenSeries": false,
+      "id": 5,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "rate(jvm_gc_pause_seconds_sum{application=\"commerce-api\"}[1m])",
+          "interval": "",
+          "legendFormat": "GC Time - {{action}} {{cause}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "GC Time (sec/sec)",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percentunit",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 16
+      },
+      "hiddenSeries": false,
+      "id": 6,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "tomcat_threads_current_threads{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Current Threads",
+          "refId": "A"
+        },
+        {
+          "expr": "tomcat_threads_busy_threads{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Busy Threads",
+          "refId": "B"
+        },
+        {
+          "expr": "tomcat_threads_config_max_threads{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Max Threads",
+          "refId": "C"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Tomcat Threads",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 16
+      },
+      "hiddenSeries": false,
+      "id": 7,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "hikaricp_connections_active{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Active Connections",
+          "refId": "A"
+        },
+        {
+          "expr": "hikaricp_connections_idle{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Idle Connections",
+          "refId": "B"
+        },
+        {
+          "expr": "hikaricp_connections_max{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Max Connections",
+          "refId": "C"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "HikariCP Connection Pool",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 24
+      },
+      "hiddenSeries": false,
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "jvm_threads_states_threads{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "{{state}}",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "JVM Thread States",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {},
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 24
+      },
+      "hiddenSeries": false,
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "options": {
+        "alertThreshold": true
+      },
+      "percentage": false,
+      "pluginVersion": "8.0.0",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "process_cpu_usage{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "Process CPU Usage",
+          "refId": "A"
+        },
+        {
+          "expr": "system_cpu_usage{application=\"commerce-api\"}",
+          "interval": "",
+          "legendFormat": "System CPU Usage",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "CPU Usage",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "percentunit",
+          "label": null,
+          "logBase": 1,
+          "max": "1",
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "schemaVersion": 27,
+  "style": "dark",
+  "tags": ["spring-boot", "load-test"],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-15m",
+    "to": "now"
+  },
+  "timepicker": {},
+  "timezone": "",
+  "title": "Spring Boot - Load Test Dashboard",
+  "uid": "spring-boot-load-test",
+  "version": 0
+}
diff --git a/docker/monitoring-compose.yml b/docker/monitoring-compose.yml
index bc985b3a5..1984bd52e 100644
--- a/docker/monitoring-compose.yml
+++ b/docker/monitoring-compose.yml
@@ -1,11 +1,13 @@
 version: '3'
 services:
   prometheus:
-    image: prom/prometheus
+    image: prom/prometheus:v2.48.0
     ports:
       - "9090:9090"
     volumes:
       - ./grafana/prometheus.yml:/etc/prometheus/prometheus.yml
+      - prometheus-data:/prometheus
+    restart: unless-stopped
 
   grafana:
     image: grafana/grafana
@@ -17,6 +19,9 @@ services:
       - GF_SECURITY_ADMIN_USER=admin
       - GF_SECURITY_ADMIN_PASSWORD=admin
 
+volumes:
+  prometheus-data:
+
 networks:
   default:
     driver: bridge
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c99fb6360..906b49231 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,6 +3,7 @@ rootProject.name = "loopers-java-spring-template"
 include(
     ":apps:commerce-api",
     ":apps:commerce-streamer",
+    ":apps:pg-simulator",
     ":modules:jpa",
     ":modules:redis",
     ":modules:kafka",
diff --git a/supports/monitoring/src/main/resources/monitoring.yml b/supports/monitoring/src/main/resources/monitoring.yml
index c6a87a9cf..c431efbed 100644
--- a/supports/monitoring/src/main/resources/monitoring.yml
+++ b/supports/monitoring/src/main/resources/monitoring.yml
@@ -3,15 +3,30 @@ management:
     distribution:
       percentiles-histogram:
         http.server.requests: true
+      percentiles:
+        http.server.requests: 0.5, 0.95, 0.99
+      slo:
+        http.server.requests: 10ms, 50ms, 100ms, 200ms, 500ms, 1s, 2s, 5s, 10s
     tags:
       application:
         ${spring.application.name}
+    # 활성화할 메트릭 명시
+    enable:
+      jvm: true
+      system: true
+      tomcat: true
+      hikaricp: true
+      logback: true
+      process: true
+      resilience4j: true
   endpoints:
     web:
       exposure:
         include:
           - health
           - prometheus
+          - metrics
+          - info
   endpoint:
     health:
       probes:
@@ -23,8 +38,13 @@ management:
             - livenessState
         readiness:
           show-components: always
+          include:
+            - readinessState
+            - db
     prometheus:
       access: read_only
+    metrics:
+      enabled: true
   health:
     livenessState:
       enabled: true
