From 417d9d51d409b8c9498e7deba139c572949a11e4 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 16:33:27 +0900
Subject: [PATCH 01/29] =?UTF-8?q?feature=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?=
 =?UTF-8?q?=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=A9=EC=A0=84=20=EB=8F=99?=
 =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95,?=
 =?UTF-8?q?=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../user/repository/UserRepository.java       |  2 +
 .../mysql/user/UserJpaRepository.java         |  7 ++
 .../mysql/user/impl/UserRepositoryImpl.java   |  7 ++
 .../core/service/user/UserPointService.java   |  4 +-
 .../core/service/ConcurrencyTestUtil.java     | 65 +++++++++++++++++++
 .../user/UserPointServiceIntegrationTest.java | 58 ++++++++++++++++-
 6 files changed, 139 insertions(+), 4 deletions(-)
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
index 68a2942bf..c7b2197be 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -15,4 +15,6 @@ public interface UserRepository {
     Optional<User> findByIdentifier(UserIdentifier identifier);
 
     User getByIdentifier(UserIdentifier identifier);
+
+    User getByIdentifierWithLock(UserIdentifier identifier);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
index b84067f76..2e00ff2ff 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
@@ -1,11 +1,18 @@
 package com.loopers.core.infra.database.mysql.user;
 
 import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
+import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.Optional;
 
 public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
 
     Optional<UserEntity> findByIdentifier(String identifier);
+
+    @Lock(LockModeType.PESSIMISTIC_WRITE)
+    @Query("select u from UserEntity u where u.identifier = :identifier")
+    Optional<UserEntity> findByIdentifierWithLock(String identifier);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
index 8b0e6daba..15c872015 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -47,4 +47,11 @@ public User getByIdentifier(UserIdentifier identifier) {
                 .orElseThrow(() -> NotFoundException.withName("사용자"))
                 .to();
     }
+
+    @Override
+    public User getByIdentifierWithLock(UserIdentifier identifier) {
+        return repository.findByIdentifierWithLock(identifier.value())
+                .orElseThrow(() -> NotFoundException.withName("사용자"))
+                .to();
+    }
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
index 744a45f90..9208cd847 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
@@ -8,6 +8,7 @@
 import com.loopers.core.service.user.command.UserPointChargeCommand;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
 @RequiredArgsConstructor
@@ -16,8 +17,9 @@ public class UserPointService {
     private final UserRepository userRepository;
     private final UserPointRepository userPointRepository;
 
+    @Transactional
     public UserPoint charge(UserPointChargeCommand command) {
-        User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
+        User user = userRepository.getByIdentifierWithLock(new UserIdentifier(command.getUserIdentifier()));
         UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
 
         return userPointRepository.save(userPoint.charge(command.getPoint()));
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java b/core/service/user-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
new file mode 100644
index 000000000..260dcf8fa
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
@@ -0,0 +1,65 @@
+package com.loopers.core.service;
+
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class ConcurrencyTestUtil {
+
+    private ConcurrencyTestUtil() {
+        throw new UnsupportedOperationException("이 클래스의 인스턴스는 생성될 수 없습니다.");
+    }
+
+    public static <T> List<T> executeInParallel(
+            int threadCount,
+            Function<Integer, T> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+        CopyOnWriteArrayList<T> results = new CopyOnWriteArrayList<>();
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        T result = task.apply(index);
+                        results.add(result);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+
+        return results;
+    }
+
+    public static void executeInParallelWithoutResult(
+            int threadCount,
+            Consumer<Integer> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        task.accept(index);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
index 6a51cb3bb..d4d493fec 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -9,9 +9,9 @@
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.command.UserPointChargeCommand;
-import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -20,6 +20,9 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class UserPointServiceIntegrationTest extends IntegrationTest {
 
@@ -31,6 +34,8 @@ class UserPointServiceIntegrationTest extends IntegrationTest {
 
     @Autowired
     private UserPointRepository userPointRepository;
+    @Autowired
+    private UserPointService userPointService;
 
     @Nested
     @DisplayName("포인트 충전")
@@ -84,10 +89,57 @@ class 존재하지_않는_유저_ID로_충전 {
                 UserPointChargeCommand command = new UserPointChargeCommand("nonExist", new BigDecimal(1000));
 
                 // when & then
-                Assertions.assertThatThrownBy(() -> service.charge(command))
+                assertThatThrownBy(() -> service.charge(command))
                         .isInstanceOf(NotFoundException.class);
             }
         }
-    }
 
+        @Nested
+        @DisplayName("동시성 테스트")
+        class ConcurrencyTest {
+
+            @Nested
+            @DisplayName("100명이 동시에 10,000 포인트씩 적립하면")
+            class 동시_포인트_적립_정합성 {
+
+                User user;
+
+                @BeforeEach
+                void setUp() {
+                    user = userRepository.save(
+                            User.create(
+                                    UserIdentifier.create("kilian"),
+                                    UserEmail.create("kilian@gmail.com"),
+                                    UserBirthDay.create("1997-10-08"),
+                                    UserGender.create("MALE")
+                            )
+                    );
+                    userPointRepository.save(UserPoint.create(user.getUserId()));
+                }
+
+                @Test
+                @DisplayName("정확히 1,000,000 포인트가 된다")
+                void 포인트_충전_결과_확인() throws InterruptedException {
+                    // Given
+                    int requestCount = 100;
+                    BigDecimal chargeAmount = new BigDecimal(10_000);
+                    String userIdentifier = "kilian";
+
+                    // When - 100개의 동시 충전 요청 실행
+                    List<UserPoint> results = ConcurrencyTestUtil.executeInParallel(
+                            requestCount,
+                            index -> service.charge(new UserPointChargeCommand(userIdentifier, chargeAmount))
+                    );
+
+                    // Then
+                    UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+
+                    SoftAssertions.assertSoftly(softly -> {
+                        softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);
+                        softly.assertThat(userPoint.getBalance().value().intValue()).as("최종 포인트 잔액").isEqualTo(1_000_000);
+                    });
+                }
+            }
+        }
+    }
 }

From 6fc7241648df5281cc5226e29af1269a792bb01b Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 16:59:52 +0900
Subject: [PATCH 02/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=9E=AC=EA=B3=A0=20=EA=B0=90=EC=86=8C=20=EB=8F=99=EC=8B=9C?=
 =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95,=20?=
 =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../product/repository/ProductRepository.java |   2 +
 .../mysql/product/ProductJpaRepository.java   |   9 +-
 .../product/impl/ProductRepositoryImpl.java   |  10 +
 .../order/component/OrderLineAllocator.java   |   2 +-
 .../core/service/ConcurrencyTestUtil.java     |  65 +++++
 .../OrderLineAllocatorIntegrationTest.java    | 273 +++++++++++-------
 .../user/UserPointServiceIntegrationTest.java |   6 +-
 7 files changed, 259 insertions(+), 108 deletions(-)
 create mode 100644 core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/product/repository/ProductRepository.java b/core/domain/src/main/java/com/loopers/core/domain/product/repository/ProductRepository.java
index b6ec1acc7..20c270475 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/product/repository/ProductRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/product/repository/ProductRepository.java
@@ -10,6 +10,8 @@ public interface ProductRepository {
 
     Product getById(ProductId productId);
 
+    Product getByIdWithLock(ProductId productId);
+
     ProductListView findListWithCondition(
             BrandId brandId,
             OrderSort createdAtSort,
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductJpaRepository.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductJpaRepository.java
index 9fc240b39..c009493bb 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductJpaRepository.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductJpaRepository.java
@@ -1,9 +1,16 @@
 package com.loopers.core.infra.database.mysql.product;
 
 import com.loopers.core.infra.database.mysql.product.entity.ProductEntity;
+import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
 
-public interface ProductJpaRepository extends JpaRepository<ProductEntity, Long>, ProductQuerydslRepository {
+import java.util.Optional;
 
+public interface ProductJpaRepository extends JpaRepository<ProductEntity, Long>, ProductQuerydslRepository {
 
+    @Lock(LockModeType.PESSIMISTIC_WRITE)
+    @Query("select p from ProductEntity p where p.id = :id")
+    Optional<ProductEntity> findByIdWithLock(Long id);
 }
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/ProductRepositoryImpl.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/ProductRepositoryImpl.java
index fae2b37af..cddde3507 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/ProductRepositoryImpl.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/ProductRepositoryImpl.java
@@ -34,6 +34,16 @@ public Product getById(ProductId productId) {
                 .orElseThrow(() -> NotFoundException.withName("상품"));
     }
 
+    @Override
+    public Product getByIdWithLock(ProductId productId) {
+        return repository.findByIdWithLock(
+                        Objects.requireNonNull(Optional.ofNullable(productId.value())
+                                .map(Long::parseLong)
+                                .orElse(null))
+                ).map(ProductEntity::to)
+                .orElseThrow(() -> NotFoundException.withName("상품"));
+    }
+
     @Override
     public ProductListView findListWithCondition(
             BrandId brandId,
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderLineAllocator.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderLineAllocator.java
index cac50470b..560591d2e 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderLineAllocator.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderLineAllocator.java
@@ -19,7 +19,7 @@ public class OrderLineAllocator {
 
     @Transactional
     public BigDecimal allocate(OrderItem orderItem) {
-        Product product = productRepository.getById(orderItem.getProductId());
+        Product product = productRepository.getByIdWithLock(orderItem.getProductId());
         productRepository.save(product.decreaseStock(orderItem.getQuantity()));
         orderItemRepository.save(orderItem);
 
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java b/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
new file mode 100644
index 000000000..260dcf8fa
--- /dev/null
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
@@ -0,0 +1,65 @@
+package com.loopers.core.service;
+
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class ConcurrencyTestUtil {
+
+    private ConcurrencyTestUtil() {
+        throw new UnsupportedOperationException("이 클래스의 인스턴스는 생성될 수 없습니다.");
+    }
+
+    public static <T> List<T> executeInParallel(
+            int threadCount,
+            Function<Integer, T> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+        CopyOnWriteArrayList<T> results = new CopyOnWriteArrayList<>();
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        T result = task.apply(index);
+                        results.add(result);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+
+        return results;
+    }
+
+    public static void executeInParallelWithoutResult(
+            int threadCount,
+            Consumer<Integer> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        task.accept(index);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+    }
+}
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
index b8a6113f0..3b5fdec61 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
@@ -10,6 +10,7 @@
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.*;
+import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.component.OrderLineAllocator;
 import org.assertj.core.api.SoftAssertions;
@@ -20,8 +21,11 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
+import java.util.List;
 
-@DisplayName("OrderLineAllocator 통합 테스트")
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+@DisplayName("주문 라인 할당")
 class OrderLineAllocatorIntegrationTest extends IntegrationTest {
 
     @Autowired
@@ -30,111 +34,174 @@ class OrderLineAllocatorIntegrationTest extends IntegrationTest {
     @Autowired
     private ProductRepository productRepository;
 
-    private Product product;
-    private OrderItem orderItem;
-
-    @BeforeEach
-    void setUp() {
-        // 상품 생성 및 저장 (가격: 10,000, 재고: 100개)
-        product = Product.mappedBy(
-                ProductId.empty(),
-                new BrandId("1"),
-                new ProductName("테스트상품"),
-                new ProductPrice(new BigDecimal("10000")),
-                new ProductStock(100L),
-                ProductLikeCount.init(),
-                CreatedAt.now(),
-                UpdatedAt.now(),
-                DeletedAt.empty()
-        );
-        product = productRepository.save(product);
-
-        // 주문 상품 생성 (수량: 5개)
-        orderItem = OrderItem.create(
-            new OrderId("1"),
-            product.getProductId(),
-            new Quantity(5L)
-        );
-    }
-
     @Nested
-    @DisplayName("allocate 메서드")
-    class AllocateMethod {
-
-        @Test
-        @DisplayName("상품의 재고를 차감하고 총 가격을 반환한다")
-        void allocateSuccess() {
-            // when
-            BigDecimal totalPrice = orderLineAllocator.allocate(orderItem);
-
-            // then - 반환된 가격 검증 (10,000 * 5 = 50,000)
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(totalPrice).isEqualByComparingTo(new BigDecimal("50000"));
-            });
-
-            // 상품 재고 차감 확인
-            Product allocatedProduct = productRepository.getById(product.getProductId());
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(allocatedProduct.getStock().value()).isEqualTo(95L); // 100 - 5
-            });
-        }
-
-        @Test
-        @DisplayName("여러 번 할당하면 재고가 누적으로 차감된다")
-        void allocateMultipleTimes() {
-            // when - 첫 번째 할당 (5개)
-            orderLineAllocator.allocate(orderItem);
-
-            // 두 번째 할당 (5개)
-            orderLineAllocator.allocate(orderItem);
-
-            // then - 재고 누적 차감 확인
-            Product allocatedProduct = productRepository.getById(product.getProductId());
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(allocatedProduct.getStock().value()).isEqualTo(90L); // 100 - 5 - 5
-            });
-        }
-
-        @Test
-        @DisplayName("재고 부족 시 예외를 발생시킨다")
-        void throwExceptionWhenInsufficientStock() {
-            // given - 재고보다 많은 수량 주문
-            OrderItem largeOrderItem = OrderItem.create(
-                    new OrderId("1"),
-                    product.getProductId(),
-                    new Quantity(150L)
-            );
-
-            // when & then
-            org.assertj.core.api.Assertions.assertThatThrownBy(
-                            () -> orderLineAllocator.allocate(largeOrderItem)
-                    ).isInstanceOf(IllegalArgumentException.class)
-                    .hasMessage("상품의 재고가 부족합니다.");
+    @DisplayName("상품 할당 시")
+    class 상품_할당 {
+
+        @Nested
+        @DisplayName("충분한 재고가 있는 경우")
+        class 충분한_재고가_있는_경우 {
+
+            private Product product;
+            private OrderItem orderItem;
+
+            @BeforeEach
+            void setUp() {
+                // 상품 생성 및 저장 (가격: 10,000, 재고: 100개)
+                product = Product.mappedBy(
+                        ProductId.empty(),
+                        new BrandId("1"),
+                        new ProductName("테스트상품"),
+                        new ProductPrice(new BigDecimal("10000")),
+                        new ProductStock(100L),
+                        ProductLikeCount.init(),
+                        CreatedAt.now(),
+                        UpdatedAt.now(),
+                        DeletedAt.empty()
+                );
+                product = productRepository.save(product);
+
+                // 주문 상품 생성 (수량: 5개)
+                orderItem = OrderItem.create(
+                        new OrderId("1"),
+                        product.getProductId(),
+                        new Quantity(5L)
+                );
+            }
+
+            @Test
+            @DisplayName("상품의 재고를 차감하고 총 가격을 반환한다")
+            void 상품의_재고를_차감하고_총_가격을_반환한다() {
+                // when
+                BigDecimal totalPrice = orderLineAllocator.allocate(orderItem);
+
+                // then - 반환된 가격 검증 (10,000 * 5 = 50,000)
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(totalPrice)
+                            .as("할당된 총 가격")
+                            .isEqualByComparingTo(new BigDecimal("50000"));
+                });
+
+                // 상품 재고 차감 확인
+                Product allocatedProduct = productRepository.getById(product.getProductId());
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(allocatedProduct.getStock().value())
+                            .as("차감 후 남은 재고")
+                            .isEqualTo(95L); // 100 - 5
+                });
+            }
+
+            @Test
+            @DisplayName("여러 번 할당하면 재고가 누적으로 차감된다")
+            void 여러_번_할당하면_재고가_누적으로_차감된다() {
+                // when - 첫 번째 할당 (5개)
+                orderLineAllocator.allocate(orderItem);
+
+                // 두 번째 할당 (5개)
+                orderLineAllocator.allocate(orderItem);
+
+                // then - 재고 누적 차감 확인
+                Product allocatedProduct = productRepository.getById(product.getProductId());
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(allocatedProduct.getStock().value())
+                            .as("누적 차감 후 남은 재고")
+                            .isEqualTo(90L); // 100 - 5 - 5
+                });
+            }
+
+            @Test
+            @DisplayName("정확히 남은 재고만큼 할당하면 재고가 0이 된다")
+            void 정확히_남은_재고만큼_할당하면_재고가_0이_된다() {
+                // given - 정확히 현재 재고만큼 주문
+                OrderItem exactOrderItem = OrderItem.create(
+                        new OrderId("1"),
+                        product.getProductId(),
+                        new Quantity(100L)
+                );
+
+                // when
+                BigDecimal totalPrice = orderLineAllocator.allocate(exactOrderItem);
+
+                // then - 총 가격 검증 (10,000 * 100 = 1,000,000)
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(totalPrice)
+                            .as("할당된 총 가격")
+                            .isEqualByComparingTo(new BigDecimal("1000000"));
+                });
+
+                // 재고가 0이 되었는지 확인
+                Product allocatedProduct = productRepository.getById(product.getProductId());
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(allocatedProduct.getStock().value())
+                            .as("차감 후 남은 재고")
+                            .isZero();
+                });
+            }
+
+            @Test
+            @DisplayName("동시에 여러 사용자가 주문하면 주문한양만큼 감소한다.")
+            void 동시에_여러_사용자가_주문하면_주문한양만큼_감소한다() throws InterruptedException {
+                int requestCount = 100;
+                OrderItem orderItem = OrderItem.create(
+                        new OrderId("1"),
+                        product.getProductId(),
+                        new Quantity(1L)
+                );
+
+                List<BigDecimal> results = ConcurrencyTestUtil.executeInParallel(
+                        100,
+                        index -> orderLineAllocator.allocate(orderItem)
+                );
+
+                Product actualProduct = productRepository.getById(product.getProductId());
+
+
+                assertSoftly(softly -> {
+                    softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);
+                    softly.assertThat(actualProduct.getStock().value()).as("최종 재고 수").isEqualTo(0L);
+                });
+            }
         }
 
-        @Test
-        @DisplayName("정확히 남은 재고만큼 할당하면 재고가 0이 된다")
-        void allocateWithExactStock() {
-            // given - 정확히 현재 재고만큼 주문
-            OrderItem exactOrderItem = OrderItem.create(
-                    new OrderId("1"),
-                    product.getProductId(),
-                    new Quantity(100L)
-            );
-
-            // when
-            BigDecimal totalPrice = orderLineAllocator.allocate(exactOrderItem);
-
-            // then - 총 가격 검증 (10,000 * 100 = 1,000,000)
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(totalPrice).isEqualByComparingTo(new BigDecimal("1000000"));
-            });
-
-            // 재고가 0이 되었는지 확인
-            Product allocatedProduct = productRepository.getById(product.getProductId());
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(allocatedProduct.getStock().value()).isZero();
-            });
+        @Nested
+        @DisplayName("재고가 부족한 경우")
+        class 재고가_부족한_경우 {
+
+            private Product product;
+
+            @BeforeEach
+            void setUp() {
+                // 상품 생성 및 저장 (가격: 10,000, 재고: 100개)
+                product = Product.mappedBy(
+                        ProductId.empty(),
+                        new BrandId("1"),
+                        new ProductName("테스트상품"),
+                        new ProductPrice(new BigDecimal("10000")),
+                        new ProductStock(100L),
+                        ProductLikeCount.init(),
+                        CreatedAt.now(),
+                        UpdatedAt.now(),
+                        DeletedAt.empty()
+                );
+                product = productRepository.save(product);
+            }
+
+            @Test
+            @DisplayName("예외를 발생시킨다")
+            void 예외를_발생시킨다() {
+                // given - 재고보다 많은 수량 주문
+                OrderItem largeOrderItem = OrderItem.create(
+                        new OrderId("1"),
+                        product.getProductId(),
+                        new Quantity(150L)
+                );
+
+                // when & then
+                org.assertj.core.api.Assertions.assertThatThrownBy(
+                                () -> orderLineAllocator.allocate(largeOrderItem)
+                        ).isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("상품의 재고가 부족합니다.");
+            }
         }
     }
 }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
index d4d493fec..addb8e3eb 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -12,7 +12,6 @@
 import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.command.UserPointChargeCommand;
-import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,6 +22,7 @@
 import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
 class UserPointServiceIntegrationTest extends IntegrationTest {
 
@@ -71,7 +71,7 @@ void setUp() {
                 UserPoint chargedPoint = userPointRepository.getByUserId(user.getUserId());
 
                 // then
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(chargedPoint).isNotNull();
                     softly.assertThat(chargedPoint.getBalance().value().intValue()).isEqualTo(1000);
                 });
@@ -134,7 +134,7 @@ void setUp() {
                     // Then
                     UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
 
-                    SoftAssertions.assertSoftly(softly -> {
+                    assertSoftly(softly -> {
                         softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);
                         softly.assertThat(userPoint.getBalance().value().intValue()).as("최종 포인트 잔액").isEqualTo(1_000_000);
                     });

From 00212780344bdbe41dd6aa70a8f2a90c82bd7661 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 17:19:19 +0900
Subject: [PATCH 03/29] =?UTF-8?q?feature=20:=20=EC=A3=BC=EB=AC=B8=20?=
 =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B2=B0=EC=A0=9C=20=EB=8F=99?=
 =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95,?=
 =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../user/repository/UserPointRepository.java  |   4 +-
 .../mysql/user/UserPointJpaRepository.java    |   7 +
 .../user/impl/UserPointRepositoryImpl.java    |   7 +-
 .../service/order/component/OrderCashier.java |   2 +-
 .../order/OrderCashierIntegrationTest.java    | 241 +++++++++++-------
 5 files changed, 167 insertions(+), 94 deletions(-)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
index 5bedd7734..f474a9253 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
@@ -3,13 +3,11 @@
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.vo.UserId;
 
-import java.util.Optional;
-
 public interface UserPointRepository {
 
     UserPoint save(UserPoint userPoint);
 
     UserPoint getByUserId(UserId userId);
 
-    Optional<UserPoint> findByUserId(UserId userId);
+    UserPoint getByUserIdWithLock(UserId userId);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
index 95952b3c6..e69348120 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
@@ -1,11 +1,18 @@
 package com.loopers.core.infra.database.mysql.user;
 
 import com.loopers.core.infra.database.mysql.user.entity.UserPointEntity;
+import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.Optional;
 
 public interface UserPointJpaRepository extends JpaRepository<UserPointEntity, Long> {
 
     Optional<UserPointEntity> findByUserId(Long userId);
+
+    @Lock(LockModeType.PESSIMISTIC_WRITE)
+    @Query("select up from UserPointEntity up where up.userId = :userId")
+    Optional<UserPointEntity> findByUserIdWithLock(Long userId);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
index bcb90f1b1..dbdab92c8 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
@@ -36,13 +36,14 @@ public UserPoint getByUserId(UserId userId) {
     }
 
     @Override
-    public Optional<UserPoint> findByUserId(UserId userId) {
-        return repository.findByUserId(
+    public UserPoint getByUserIdWithLock(UserId userId) {
+        return repository.findByUserIdWithLock(
                         Objects.requireNonNull(Optional.ofNullable(userId.value())
                                 .map(Long::parseLong)
                                 .orElse(null)
                         )
                 )
-                .map(UserPointEntity::to);
+                .orElseThrow(() -> NotFoundException.withName("사용자 포인트"))
+                .to();
     }
 }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
index fa4d450d1..81132b895 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
@@ -20,7 +20,7 @@ public class OrderCashier {
 
     @Transactional
     public Payment checkout(User user, Order order, PayAmount payAmount) {
-        UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+        UserPoint userPoint = userPointRepository.getByUserIdWithLock(user.getUserId());
         userPointRepository.save(userPoint.pay(payAmount));
 
         Payment payment = Payment.create(order.getOrderId(), user.getUserId(), payAmount);
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
index d86844275..ca502b1b1 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
@@ -3,22 +3,19 @@
 import com.loopers.core.domain.common.vo.CreatedAt;
 import com.loopers.core.domain.common.vo.UpdatedAt;
 import com.loopers.core.domain.order.Order;
-import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.repository.OrderRepository;
-import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
-import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.*;
+import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.component.OrderCashier;
-import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -29,7 +26,9 @@
 import java.time.LocalDate;
 import java.util.List;
 
-@DisplayName("OrderCashier 통합 테스트")
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+@DisplayName("주문 결제 처리")
 class OrderCashierIntegrationTest extends IntegrationTest {
 
     @Autowired
@@ -47,91 +46,159 @@ class OrderCashierIntegrationTest extends IntegrationTest {
     @Autowired
     private PaymentRepository paymentRepository;
 
-    private User user;
-    private Order order;
-    private PayAmount payAmount;
-
-    @BeforeEach
-    void setUp() {
-        // 사용자 생성 및 저장
-        user = User.create(
-                new UserIdentifier("testuser"),
-                new UserEmail("test@example.com"),
-                new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                UserGender.MALE
-        );
-        user = userRepository.save(user);
-
-        // 사용자 포인트 생성 및 저장 (50,000 포인트)
-        UserPoint userPoint = UserPoint.mappedBy(
-                UserPointId.empty(),
-                user.getUserId(),
-                new UserPointBalance(new BigDecimal("50000")),
-                CreatedAt.now(),
-                UpdatedAt.now()
-        );
-        userPointRepository.save(userPoint);
-
-        // 주문 생성 및 저장
-        order = Order.create(user.getUserId());
-        order = orderRepository.save(order);
-
-        // 결제 금액
-        payAmount = new PayAmount(new BigDecimal("20000"));
-    }
-
     @Nested
-    @DisplayName("checkout 메서드")
-    class CheckoutMethod {
-
-        @Test
-        @DisplayName("주문을 생성하고 포인트를 차감하고 결제 정보를 저장한다")
-        void checkoutSuccess() {
-            // when
-            Payment result = orderCashier.checkout(user, order, payAmount);
-
-            // then - 결제 정보 확인
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(result).isNotNull();
-                softly.assertThat(result.getOrderId()).isEqualTo(order.getOrderId());
-                softly.assertThat(result.getUserId()).isEqualTo(user.getUserId());
-                softly.assertThat(result.getAmount().value()).isEqualByComparingTo(new BigDecimal("20000"));
-            });
-
-            // 포인트 차감 확인
-            UserPoint deductedUserPoint = userPointRepository.getByUserId(user.getUserId());
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(deductedUserPoint.getBalance().value())
-                        .isEqualByComparingTo(new BigDecimal("30000")); // 50000 - 20000
-            });
-        }
-
-        @Test
-        @DisplayName("포인트가 부족하면 예외를 발생시킨다")
-        void throwExceptionWhenInsufficientPoint() {
-            // given
-            PayAmount largePayAmount = new PayAmount(new BigDecimal("100000"));
-
-            // when & then
-            org.assertj.core.api.Assertions.assertThatThrownBy(() -> orderCashier.checkout(user, order, largePayAmount))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
+    @DisplayName("결제 시")
+    class 결제_시 {
+
+        @Nested
+        @DisplayName("포인트가 충분한 경우")
+        class 포인트가_충분한_경우 {
+
+            private User user;
+            private Order order;
+            private PayAmount payAmount;
+
+            @BeforeEach
+            void setUp() {
+                // 사용자 생성 및 저장
+                user = User.create(
+                        new UserIdentifier("testuser"),
+                        new UserEmail("test@example.com"),
+                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
+                        UserGender.MALE
+                );
+                user = userRepository.save(user);
+
+                // 사용자 포인트 생성 및 저장 (50,000 포인트)
+                UserPoint userPoint = UserPoint.mappedBy(
+                        UserPointId.empty(),
+                        user.getUserId(),
+                        new UserPointBalance(new BigDecimal("50000")),
+                        CreatedAt.now(),
+                        UpdatedAt.now()
+                );
+                userPointRepository.save(userPoint);
+
+                // 주문 생성 및 저장
+                order = Order.create(user.getUserId());
+                order = orderRepository.save(order);
+
+                // 결제 금액 (20,000 - 포인트는 50,000으로 충분함)
+                payAmount = new PayAmount(new BigDecimal("20000"));
+            }
+
+            @Test
+            @DisplayName("주문을 생성하고 포인트를 차감하고 결제 정보를 저장한다")
+            void 주문을_생성하고_포인트를_차감하고_결제_정보를_저장한다() {
+                // when
+                Payment result = orderCashier.checkout(user, order, payAmount);
+
+                // then - 결제 정보 확인
+                assertSoftly(softly -> {
+                    softly.assertThat(result)
+                            .as("결제 정보")
+                            .isNotNull();
+                    softly.assertThat(result.getOrderId())
+                            .as("주문 ID 일치")
+                            .isEqualTo(order.getOrderId());
+                    softly.assertThat(result.getUserId())
+                            .as("사용자 ID 일치")
+                            .isEqualTo(user.getUserId());
+                    softly.assertThat(result.getAmount().value())
+                            .as("결제 금액")
+                            .isEqualByComparingTo(new BigDecimal("20000"));
+                });
+
+                // 포인트 차감 확인
+                UserPoint deductedUserPoint = userPointRepository.getByUserId(user.getUserId());
+                assertSoftly(softly -> {
+                    softly.assertThat(deductedUserPoint.getBalance().value())
+                            .as("차감 후 포인트 잔액")
+                            .isEqualByComparingTo(new BigDecimal("30000")); // 50000 - 20000
+                });
+            }
+
+            @Test
+            @DisplayName("정확히 남은 포인트만큼 결제하면 포인트가 0이 된다")
+            void 정확히_남은_포인트만큼_결제하면_포인트가_0이_된다() {
+                // given
+                PayAmount exactPayAmount = new PayAmount(new BigDecimal("50000"));
+
+                // when
+                orderCashier.checkout(user, order, exactPayAmount);
+
+                // then
+                UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+                assertSoftly(softly -> {
+                    softly.assertThat(userPoint.getBalance().value())
+                            .as("차감 후 포인트 잔액")
+                            .isZero();
+                });
+            }
+
+            @Test
+            @DisplayName("동시에 여러 주문요청을 결제한다면 결제한 금액만큼 감소된다.")
+            void 동시에_여러_주문요청을_결제한다면_결제한_금액만큼_감소된다() throws InterruptedException {
+                int requestCount = 100;
+                PayAmount payAmountPerUser = new PayAmount(new BigDecimal("500"));
+                List<Payment> results = ConcurrencyTestUtil.executeInParallel(
+                        requestCount,
+                        index -> orderCashier.checkout(user, order, payAmountPerUser)
+                );
+
+                UserPoint actualUserPoint = userPointRepository.getByUserId(user.getUserId());
+                assertSoftly(softly -> {
+                    softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);
+                    softly.assertThat(actualUserPoint.getBalance().value()).isEqualByComparingTo(new BigDecimal(0));
+                });
+            }
         }
 
-        @Test
-        @DisplayName("정확히 남은 포인트만큼 결제하면 포인트가 0이 된다")
-        void checkoutWithExactPoint() {
-            // given
-            PayAmount exactPayAmount = new PayAmount(new BigDecimal("50000"));
-
-            // when
-            orderCashier.checkout(user, order, exactPayAmount);
-
-            // then
-            UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
-            SoftAssertions.assertSoftly(softly -> {
-                softly.assertThat(userPoint.getBalance().value()).isZero();
-            });
+        @Nested
+        @DisplayName("포인트가 부족한 경우")
+        class 포인트가_부족한_경우 {
+
+            private User user;
+            private Order order;
+
+            @BeforeEach
+            void setUp() {
+                // 사용자 생성 및 저장
+                user = User.create(
+                        new UserIdentifier("testuser"),
+                        new UserEmail("test@example.com"),
+                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
+                        UserGender.MALE
+                );
+                user = userRepository.save(user);
+
+                // 사용자 포인트 생성 및 저장 (50,000 포인트)
+                UserPoint userPoint = UserPoint.mappedBy(
+                        UserPointId.empty(),
+                        user.getUserId(),
+                        new UserPointBalance(new BigDecimal("50000")),
+                        CreatedAt.now(),
+                        UpdatedAt.now()
+                );
+                userPointRepository.save(userPoint);
+
+                // 주문 생성 및 저장
+                order = Order.create(user.getUserId());
+                order = orderRepository.save(order);
+            }
+
+            @Test
+            @DisplayName("예외를 발생시킨다")
+            void 예외를_발생시킨다() {
+                // given
+                PayAmount largePayAmount = new PayAmount(new BigDecimal("100000"));
+
+                // when & then
+                org.assertj.core.api.Assertions.assertThatThrownBy(
+                                () -> orderCashier.checkout(user, order, largePayAmount)
+                        ).isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
+            }
         }
     }
 }

From 21fdba36bef530c8830de3dd64abfb97682a9d92 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 17:52:21 +0900
Subject: [PATCH 04/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?=
 =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95,=20=ED=85=8C=EC=8A=A4?=
 =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/ProductLikeRepository.java     |  2 +
 .../productlike/ProductLikeJpaRepository.java |  7 ++
 .../impl/ProductLikeRepositoryImpl.java       |  8 +++
 .../productlike/ProductLikeService.java       |  4 +-
 .../core/service/ConcurrencyTestUtil.java     | 65 +++++++++++++++++++
 .../productlike/ProductLikeServiceTest.java   | 37 +++++++++++
 6 files changed, 121 insertions(+), 2 deletions(-)
 create mode 100644 core/service/product-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/productlike/repository/ProductLikeRepository.java b/core/domain/src/main/java/com/loopers/core/domain/productlike/repository/ProductLikeRepository.java
index c037c7a34..5330c2c63 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/productlike/repository/ProductLikeRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/productlike/repository/ProductLikeRepository.java
@@ -17,6 +17,8 @@ public interface ProductLikeRepository {
 
     Optional<ProductLike> findByUserIdAndProductId(UserId userId, ProductId productId);
 
+    Optional<ProductLike> findByUserIdAndProductIdWithLock(UserId userId, ProductId productId);
+
     LikeProductListView findLikeProductsListWithCondition(
             UserId userId,
             BrandId brandId,
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeJpaRepository.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeJpaRepository.java
index ddbf0bae2..5e00397a1 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeJpaRepository.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeJpaRepository.java
@@ -1,7 +1,10 @@
 package com.loopers.core.infra.database.mysql.productlike;
 
 import com.loopers.core.infra.database.mysql.productlike.entity.ProductLikeEntity;
+import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.Optional;
 
@@ -9,5 +12,9 @@ public interface ProductLikeJpaRepository extends JpaRepository<ProductLikeEntit
 
     Optional<ProductLikeEntity> findByUserIdAndProductId(Long userId, Long productId);
 
+    @Lock(LockModeType.PESSIMISTIC_WRITE)
+    @Query("select pl from ProductLikeEntity pl where pl.userId = :userId and pl.productId = :productId")
+    Optional<ProductLikeEntity> findByUserIdAndProductIdWithLock(Long userId, Long productId);
+
     void deleteByUserIdAndProductId(Long userId, Long productId);
 }
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/impl/ProductLikeRepositoryImpl.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/impl/ProductLikeRepositoryImpl.java
index e20972a6a..a087b9456 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/impl/ProductLikeRepositoryImpl.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/impl/ProductLikeRepositoryImpl.java
@@ -46,6 +46,14 @@ public Optional<ProductLike> findByUserIdAndProductId(UserId userId, ProductId p
         ).map(ProductLikeEntity::to);
     }
 
+    @Override
+    public Optional<ProductLike> findByUserIdAndProductIdWithLock(UserId userId, ProductId productId) {
+        return repository.findByUserIdAndProductIdWithLock(
+                Long.parseLong(userId.value()),
+                Long.parseLong(productId.value())
+        ).map(ProductLikeEntity::to);
+    }
+
     @Override
     public LikeProductListView findLikeProductsListWithCondition(
             UserId userId,
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
index fa8176c06..430008334 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
@@ -25,9 +25,9 @@ public class ProductLikeService {
     @Transactional
     public void like(ProductLikeCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
-        Product product = productRepository.getById(new ProductId(command.getProductId()));
+        Product product = productRepository.getByIdWithLock(new ProductId(command.getProductId()));
 
-        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductId(user.getUserId(), product.getProductId())
+        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getProductId())
                 .isPresent();
 
         if (!isAlreadyLiked) {
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java b/core/service/product-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
new file mode 100644
index 000000000..260dcf8fa
--- /dev/null
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
@@ -0,0 +1,65 @@
+package com.loopers.core.service;
+
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class ConcurrencyTestUtil {
+
+    private ConcurrencyTestUtil() {
+        throw new UnsupportedOperationException("이 클래스의 인스턴스는 생성될 수 없습니다.");
+    }
+
+    public static <T> List<T> executeInParallel(
+            int threadCount,
+            Function<Integer, T> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+        CopyOnWriteArrayList<T> results = new CopyOnWriteArrayList<>();
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        T result = task.apply(index);
+                        results.add(result);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+
+        return results;
+    }
+
+    public static void executeInParallelWithoutResult(
+            int threadCount,
+            Consumer<Integer> task) throws InterruptedException {
+
+        CountDownLatch latch = new CountDownLatch(threadCount);
+
+        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        task.accept(index);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+        }
+
+        boolean completed = latch.await(30, TimeUnit.SECONDS);
+        assertThat(completed).isTrue();
+    }
+}
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index f3aefa7bd..9ea79300e 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -18,6 +18,7 @@
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.productlike.command.ProductLikeCommand;
 import com.loopers.core.service.productlike.command.ProductUnlikeCommand;
@@ -30,6 +31,7 @@
 import java.math.BigDecimal;
 import java.time.LocalDate;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
@@ -102,6 +104,41 @@ class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
                             .isEqualTo(likeCountBefore + 1);
                 });
             }
+
+            @Test
+            @DisplayName("동시에 여러 사용자가 좋아요를 누르면 사용자 수 만큼 증가한다.")
+            void 동시에_여러_사용자가_좋아요를_누르면_사용자_수_만큼_증가한다() throws InterruptedException {
+                int requestCount = 100;
+
+                ConcurrencyTestUtil.executeInParallelWithoutResult(
+                        requestCount,
+                        index -> {
+                            User user = userRepository.save(User.create(
+                                    new UserIdentifier("user_" + index),
+                                    new UserEmail("user" + index + "@example.com"),
+                                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                                    UserGender.MALE
+                            ));
+                            productLikeService.like(new ProductLikeCommand(user.getIdentifier().value(), productId));
+                        }
+                );
+
+                Product productAfter = productRepository.getById(new ProductId(productId));
+                assertThat(productAfter.getLikeCount().value()).isEqualTo(requestCount);
+            }
+
+            @Test
+            @DisplayName("동시에 하나의 사용자가 좋아요를 누르면 1번만 증가한다.")
+            void 동시에_하나의_사용자가_좋아요를_누르면_1번만_증가한다() throws InterruptedException {
+                int requestCount = 100;
+                ConcurrencyTestUtil.executeInParallelWithoutResult(
+                        requestCount,
+                        index -> productLikeService.like(new ProductLikeCommand(userIdentifier, productId))
+                );
+
+                Product productAfter = productRepository.getById(new ProductId(productId));
+                assertThat(productAfter.getLikeCount().value()).isEqualTo(1L);
+            }
         }
 
         @Nested

From eaa7fec963e7cd3a2a22face78a6b1b38fda4481 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 21:19:14 +0900
Subject: [PATCH 05/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=B7=A8=EC=86=8C=20=EB=8F=99?=
 =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95,?=
 =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/core/domain/product/Product.java  |   6 +-
 .../mysql/product/entity/ProductEntity.java   |   2 +-
 .../OrderLineAllocatorIntegrationTest.java    |  16 +-
 .../OrderQueryServiceIntegrationTest.java     |   4 +-
 .../order/component/OrderCashierTest.java     | 200 +++++++++------
 .../component/OrderLineAllocatorTest.java     | 139 ++++++-----
 .../productlike/ProductLikeService.java       |  10 +-
 .../product/ProductQueryServiceTest.java      |   8 +-
 .../ProductLikeQueryServiceTest.java          |   6 +-
 .../productlike/ProductLikeServiceTest.java   | 235 ++++++++++++++----
 10 files changed, 419 insertions(+), 207 deletions(-)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/product/Product.java b/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
index 70eb60a1c..d64b2a5ee 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
@@ -15,7 +15,7 @@
 @Getter
 public class Product {
 
-    private final ProductId productId;
+    private final ProductId id;
 
     private final BrandId brandId;
 
@@ -35,7 +35,7 @@ public class Product {
 
     @Builder(access = AccessLevel.PRIVATE, toBuilder = true)
     private Product(
-            ProductId productId,
+            ProductId id,
             BrandId brandId,
             ProductName name,
             ProductPrice price,
@@ -45,7 +45,7 @@ private Product(
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        this.productId = productId;
+        this.id = id;
         this.brandId = brandId;
         this.name = name;
         this.price = price;
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/entity/ProductEntity.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/entity/ProductEntity.java
index 9f24f6a8f..cf818d15c 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/entity/ProductEntity.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/entity/ProductEntity.java
@@ -60,7 +60,7 @@ public class ProductEntity {
 
     public static ProductEntity from(Product product) {
         return new ProductEntity(
-                Optional.ofNullable(product.getProductId().value())
+                Optional.ofNullable(product.getId().value())
                         .map(Long::parseLong)
                         .orElse(null),
                 Long.parseLong(Objects.requireNonNull(product.getBrandId().value())),
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
index 3b5fdec61..9fc549460 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
@@ -64,7 +64,7 @@ void setUp() {
                 // 주문 상품 생성 (수량: 5개)
                 orderItem = OrderItem.create(
                         new OrderId("1"),
-                        product.getProductId(),
+                        product.getId(),
                         new Quantity(5L)
                 );
             }
@@ -83,7 +83,7 @@ void setUp() {
                 });
 
                 // 상품 재고 차감 확인
-                Product allocatedProduct = productRepository.getById(product.getProductId());
+                Product allocatedProduct = productRepository.getById(product.getId());
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("차감 후 남은 재고")
@@ -101,7 +101,7 @@ void setUp() {
                 orderLineAllocator.allocate(orderItem);
 
                 // then - 재고 누적 차감 확인
-                Product allocatedProduct = productRepository.getById(product.getProductId());
+                Product allocatedProduct = productRepository.getById(product.getId());
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("누적 차감 후 남은 재고")
@@ -115,7 +115,7 @@ void setUp() {
                 // given - 정확히 현재 재고만큼 주문
                 OrderItem exactOrderItem = OrderItem.create(
                         new OrderId("1"),
-                        product.getProductId(),
+                        product.getId(),
                         new Quantity(100L)
                 );
 
@@ -130,7 +130,7 @@ void setUp() {
                 });
 
                 // 재고가 0이 되었는지 확인
-                Product allocatedProduct = productRepository.getById(product.getProductId());
+                Product allocatedProduct = productRepository.getById(product.getId());
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("차감 후 남은 재고")
@@ -144,7 +144,7 @@ void setUp() {
                 int requestCount = 100;
                 OrderItem orderItem = OrderItem.create(
                         new OrderId("1"),
-                        product.getProductId(),
+                        product.getId(),
                         new Quantity(1L)
                 );
 
@@ -153,7 +153,7 @@ void setUp() {
                         index -> orderLineAllocator.allocate(orderItem)
                 );
 
-                Product actualProduct = productRepository.getById(product.getProductId());
+                Product actualProduct = productRepository.getById(product.getId());
 
 
                 assertSoftly(softly -> {
@@ -192,7 +192,7 @@ void setUp() {
                 // given - 재고보다 많은 수량 주문
                 OrderItem largeOrderItem = OrderItem.create(
                         new OrderId("1"),
-                        product.getProductId(),
+                        product.getId(),
                         new Quantity(150L)
                 );
 
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index b6380efa2..795de1127 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -289,13 +289,13 @@ void setUp() {
                 OrderId orderId = new OrderId(savedOrderId);
                 orderItemRepository.save(OrderItem.create(
                         orderId,
-                        product1.getProductId(),
+                        product1.getId(),
                         new Quantity(1L)
                 ));
 
                 orderItemRepository.save(OrderItem.create(
                         orderId,
-                        product2.getProductId(),
+                        product2.getId(),
                         new Quantity(2L)
                 ));
             }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
index 3481d30df..3f93b1a18 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
@@ -1,13 +1,9 @@
 package com.loopers.core.service.order.component;
 
 import com.loopers.core.domain.order.Order;
-import com.loopers.core.domain.order.OrderItem;
-import com.loopers.core.domain.order.vo.OrderId;
-import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
-import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
@@ -25,7 +21,6 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
-import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -46,92 +41,135 @@ class OrderCashierTest {
     @InjectMocks
     private OrderCashier orderCashier;
 
-    private User user;
-    private Order order;
-    private PayAmount payAmount;
-    private UserPoint userPoint;
-
-    @BeforeEach
-    void setUp() {
-        // given
-        user = User.create(
-                new UserIdentifier("testuser"),
-                new UserEmail("test@example.com"),
-                new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                UserGender.MALE
-        );
-
-        order = Order.create(user.getUserId());
-
-        payAmount = new PayAmount(new BigDecimal("20000"));
-
-        userPoint = UserPoint.mappedBy(
-                UserPointId.empty(),
-                user.getUserId(),
-                new UserPointBalance(new BigDecimal("50000")),
-                null,
-                null
-        );
-    }
-
     @Nested
     @DisplayName("checkout 메서드")
     class CheckoutMethod {
 
-        @Test
-        @DisplayName("포인트를 차감하고 Payment를 저장한다")
-        void checkoutSuccess() {
-            // given
-            when(userPointRepository.getByUserId(user.getUserId())).thenReturn(userPoint);
-
-            // when
-            Payment result = orderCashier.checkout(user, order, payAmount);
-
-            // then
-            assertThat(result).isNotNull();
-            assertThat(result.getOrderId()).isEqualTo(order.getOrderId());
-            assertThat(result.getUserId()).isEqualTo(user.getUserId());
-            assertThat(result.getAmount()).isEqualTo(payAmount);
-
-            // 포인트 차감 검증
-            ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
-            verify(userPointRepository).save(userPointCaptor.capture());
-            assertThat(userPointCaptor.getValue().getBalance().value())
-                    .isEqualByComparingTo(new BigDecimal("30000")); // 50000 - 20000
-
-            // Payment 저장 검증
-            verify(paymentRepository).save(any(Payment.class));
-        }
-
-        @Test
-        @DisplayName("포인트가 부족하면 예외를 발생시킨다")
-        void throwExceptionWhenInsufficientPoint() {
-            // given
-            PayAmount largePayAmount = new PayAmount(new BigDecimal("100000"));
+        private User user;
+        private Order order;
 
-            when(userPointRepository.getByUserId(user.getUserId())).thenReturn(userPoint);
+        @BeforeEach
+        void setUp() {
+            user = User.create(
+                    new UserIdentifier("testuser"),
+                    new UserEmail("test@example.com"),
+                    new UserBirthDay(LocalDate.of(2000, 1, 1)),
+                    UserGender.MALE
+            );
 
-            // when & then
-            assertThatThrownBy(() -> orderCashier.checkout(user, order, largePayAmount))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
+            order = Order.create(user.getUserId());
         }
 
-        @Test
-        @DisplayName("정확히 남은 포인트만큼 결제하면 포인트가 0이 된다")
-        void checkoutWithExactPoint() {
-            // given
-            PayAmount exactPayAmount = new PayAmount(new BigDecimal("50000"));
-
-            when(userPointRepository.getByUserId(user.getUserId())).thenReturn(userPoint);
+        @Nested
+        @DisplayName("포인트가 충분할 때")
+        class WhenSufficientPoint {
+
+            private UserPoint userPoint;
+            private PayAmount payAmount;
+
+            @BeforeEach
+            void setUp() {
+                userPoint = UserPoint.mappedBy(
+                        UserPointId.empty(),
+                        user.getUserId(),
+                        new UserPointBalance(new BigDecimal("50000")),
+                        null,
+                        null
+                );
+
+                payAmount = new PayAmount(new BigDecimal("20000"));
+
+                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+            }
+
+            @Test
+            @DisplayName("포인트를 차감하고 Payment를 저장한다")
+            void checkoutSuccessAndSavePayment() {
+                // when
+                Payment result = orderCashier.checkout(user, order, payAmount);
+
+                // then
+                assertThat(result).isNotNull();
+                assertThat(result.getOrderId()).isEqualTo(order.getOrderId());
+                assertThat(result.getUserId()).isEqualTo(user.getUserId());
+                assertThat(result.getAmount()).isEqualTo(payAmount);
+
+                // 포인트 차감 검증
+                ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
+                verify(userPointRepository).save(userPointCaptor.capture());
+                assertThat(userPointCaptor.getValue().getBalance().value())
+                        .isEqualByComparingTo(new BigDecimal("30000")); // 50000 - 20000
+
+                // Payment 저장 검증
+                verify(paymentRepository).save(any(Payment.class));
+            }
+        }
 
-            // when
-            orderCashier.checkout(user, order, exactPayAmount);
+        @Nested
+        @DisplayName("포인트가 부족할 때")
+        class WhenInsufficientPoint {
+
+            private UserPoint userPoint;
+            private PayAmount largePayAmount;
+
+            @BeforeEach
+            void setUp() {
+                userPoint = UserPoint.mappedBy(
+                        UserPointId.empty(),
+                        user.getUserId(),
+                        new UserPointBalance(new BigDecimal("50000")),
+                        null,
+                        null
+                );
+
+                largePayAmount = new PayAmount(new BigDecimal("100000"));
+
+                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+            }
+
+            @Test
+            @DisplayName("예외를 발생시킨다")
+            void throwExceptionWhenInsufficientPoint() {
+                // when & then
+                assertThatThrownBy(() -> orderCashier.checkout(user, order, largePayAmount))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
+            }
+        }
 
-            // then
-            ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
-            verify(userPointRepository).save(userPointCaptor.capture());
-            assertThat(userPointCaptor.getValue().getBalance().value()).isZero();
+        @Nested
+        @DisplayName("포인트가 정확히 남은 금액과 같을 때")
+        class WhenExactPoint {
+
+            private UserPoint userPoint;
+            private PayAmount exactPayAmount;
+
+            @BeforeEach
+            void setUp() {
+                userPoint = UserPoint.mappedBy(
+                        UserPointId.empty(),
+                        user.getUserId(),
+                        new UserPointBalance(new BigDecimal("50000")),
+                        null,
+                        null
+                );
+
+                exactPayAmount = new PayAmount(new BigDecimal("50000"));
+
+                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+            }
+
+            @Test
+            @DisplayName("포인트가 0이 된다")
+            void checkoutWithExactPointBecomesZero() {
+                // when
+                orderCashier.checkout(user, order, exactPayAmount);
+
+                // then
+                ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
+                verify(userPointRepository).save(userPointCaptor.capture());
+                assertThat(userPointCaptor.getValue().getBalance().value()).isZero();
+            }
         }
     }
 }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
index 9e5f03c3c..0a4692254 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
@@ -9,6 +9,7 @@
 import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductPrice;
 import com.loopers.core.domain.product.vo.ProductStock;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -42,65 +43,89 @@ class OrderLineAllocatorTest {
     @DisplayName("allocate 메서드")
     class AllocateMethod {
 
-        @Test
-        @DisplayName("충분한 재고가 있으면 재고를 차감하고 총 가격을 반환한다")
-        void allocateWithSufficientStock() {
-            // given
-            ProductId productId = new ProductId("1");
-            Quantity quantity = new Quantity(5L);
-            OrderItem orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
-
-            Product product = Product.mappedBy(
-                    productId,
-                    null,
-                    null,
-                    new ProductPrice(new BigDecimal("10000")),
-                    new ProductStock(100L),
-                    null,
-                    null,
-                    null,
-                    null
-            );
-
-            when(productRepository.getById(productId)).thenReturn(product);
-            when(productRepository.save(any(Product.class))).thenReturn(product.decreaseStock(quantity));
-            when(orderItemRepository.save(any(OrderItem.class))).thenReturn(orderItem);
-
-            // when
-            BigDecimal result = orderLineAllocator.allocate(orderItem);
-
-            // then
-            assertThat(result).isEqualByComparingTo(new BigDecimal("50000")); // 10000 * 5
-            verify(productRepository).save(any(Product.class));
-            verify(orderItemRepository).save(any(OrderItem.class));
+        @Nested
+        @DisplayName("충분한 재고가 있을 때")
+        class WhenSufficientStock {
+
+            private ProductId productId;
+            private Quantity quantity;
+            private OrderItem orderItem;
+            private Product product;
+
+            @BeforeEach
+            void setUp() {
+                productId = new ProductId("1");
+                quantity = new Quantity(5L);
+                orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
+
+                product = Product.mappedBy(
+                        productId,
+                        null,
+                        null,
+                        new ProductPrice(new BigDecimal("10000")),
+                        new ProductStock(100L),
+                        null,
+                        null,
+                        null,
+                        null
+                );
+
+                when(productRepository.getByIdWithLock(productId)).thenReturn(product);
+                when(productRepository.save(any(Product.class))).thenReturn(product.decreaseStock(quantity));
+                when(orderItemRepository.save(any(OrderItem.class))).thenReturn(orderItem);
+            }
+
+            @Test
+            @DisplayName("재고를 차감하고 총 가격을 반환한다")
+            void allocateAndReturnTotalPrice() {
+                // when
+                BigDecimal result = orderLineAllocator.allocate(orderItem);
+
+                // then
+                assertThat(result).isEqualByComparingTo(new BigDecimal("50000")); // 10000 * 5
+                verify(productRepository).save(any(Product.class));
+                verify(orderItemRepository).save(any(OrderItem.class));
+            }
         }
 
-        @Test
-        @DisplayName("재고가 부족하면 예외를 발생시킨다")
-        void throwExceptionWhenInsufficientStock() {
-            // given
-            ProductId productId = new ProductId("1");
-            Quantity quantity = new Quantity(101L); // 100개보다 많음
-            OrderItem orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
-
-            Product product = Product.mappedBy(
-                    productId,
-                    null,
-                    null,
-                    new ProductPrice(new BigDecimal("10000")),
-                    new ProductStock(100L),
-                    null,
-                    null,
-                    null,
-                    null
-            );
-
-            when(productRepository.getById(productId)).thenReturn(product);
-
-            // when & then
-            assertThatThrownBy(() -> orderLineAllocator.allocate(orderItem))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessage("상품의 재고가 부족합니다.");
+        @Nested
+        @DisplayName("재고가 부족할 때")
+        class WhenInsufficientStock {
+
+            private ProductId productId;
+            private Quantity quantity;
+            private OrderItem orderItem;
+            private Product product;
+
+            @BeforeEach
+            void setUp() {
+                productId = new ProductId("1");
+                quantity = new Quantity(101L); // 100개보다 많음
+                orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
+
+                product = Product.mappedBy(
+                        productId,
+                        null,
+                        null,
+                        new ProductPrice(new BigDecimal("10000")),
+                        new ProductStock(100L),
+                        null,
+                        null,
+                        null,
+                        null
+                );
+
+                when(productRepository.getByIdWithLock(productId)).thenReturn(product);
+            }
+
+            @Test
+            @DisplayName("예외를 발생시킨다")
+            void throwException() {
+                // when & then
+                assertThatThrownBy(() -> orderLineAllocator.allocate(orderItem))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("상품의 재고가 부족합니다.");
+            }
         }
     }
 }
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
index 430008334..e2e22fa29 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
@@ -27,11 +27,11 @@ public void like(ProductLikeCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
         Product product = productRepository.getByIdWithLock(new ProductId(command.getProductId()));
 
-        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getProductId())
+        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getId())
                 .isPresent();
 
         if (!isAlreadyLiked) {
-            productLikeRepository.save(ProductLike.create(user.getUserId(), product.getProductId()));
+            productLikeRepository.save(ProductLike.create(user.getUserId(), product.getId()));
             productRepository.save(product.increaseLikeCount());
         }
     }
@@ -39,13 +39,13 @@ public void like(ProductLikeCommand command) {
     @Transactional
     public void unlike(ProductUnlikeCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
-        Product product = productRepository.getById(new ProductId(command.getProductId()));
+        Product product = productRepository.getByIdWithLock(new ProductId(command.getProductId()));
 
-        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductId(user.getUserId(), product.getProductId())
+        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getId())
                 .isPresent();
 
         if (isAlreadyLiked) {
-            productLikeRepository.deleteByUserIdAndProductId(user.getUserId(), product.getProductId());
+            productLikeRepository.deleteByUserIdAndProductId(user.getUserId(), product.getId());
             productRepository.save(product.decreaseLikeCount());
         }
     }
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
index f7f60ff26..861ecdfc1 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
@@ -273,7 +273,7 @@ void setUp() {
                             new ProductPrice(new BigDecimal(1_300_000))
                     )
             );
-            savedProductId = product.getProductId().value();
+            savedProductId = product.getId().value();
         }
 
         @Nested
@@ -333,7 +333,7 @@ void setUp() {
                             new ProductPrice(new BigDecimal(1_300_000))
                     )
             );
-            savedProductId = product.getProductId().value();
+            savedProductId = product.getId().value();
         }
 
         @Nested
@@ -362,7 +362,7 @@ class 상품이_존재하는_경우 {
                 ProductDetail result = productQueryService.getProductDetail(query);
 
                 assertSoftly(softly -> {
-                    softly.assertThat(result.getProduct().getProductId().value())
+                    softly.assertThat(result.getProduct().getId().value())
                             .isEqualTo(savedProductId);
                     softly.assertThat(result.getProduct().getBrandId().value())
                             .isEqualTo(savedBrandId.value());
@@ -414,7 +414,7 @@ class 브랜드가_존재하지_않는_경우 {
                         )
                 );
 
-                GetProductDetailQuery query = new GetProductDetailQuery(orphanProduct.getProductId().value());
+                GetProductDetailQuery query = new GetProductDetailQuery(orphanProduct.getId().value());
 
                 assertThatThrownBy(() -> productQueryService.getProductDetail(query))
                         .isInstanceOf(NotFoundException.class)
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
index 0b453888c..4554a8cb5 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
@@ -111,15 +111,15 @@ void setUp() {
 
                 productLikeRepository.save(ProductLike.create(
                         new UserId(savedUserId),
-                        product1.getProductId()
+                        product1.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
                         new UserId(savedUserId),
-                        product2.getProductId()
+                        product2.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
                         new UserId(savedUserId),
-                        product3.getProductId()
+                        product3.getId()
                 ));
             }
 
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index 9ea79300e..5674fb393 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -9,8 +9,10 @@
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductId;
+import com.loopers.core.domain.product.vo.ProductLikeCount;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.productlike.ProductLike;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
@@ -22,6 +24,7 @@
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.productlike.command.ProductLikeCommand;
 import com.loopers.core.service.productlike.command.ProductUnlikeCommand;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -34,6 +37,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 class ProductLikeServiceTest extends IntegrationTest {
 
@@ -73,7 +77,7 @@ void setUp() {
                     new ProductName("MacBook Pro"),
                     new ProductPrice(new BigDecimal("1500000"))
             ));
-            productId = product.getProductId().value();
+            productId = product.getId().value();
 
             User user = userRepository.save(User.create(
                     new UserIdentifier("user123"),
@@ -104,10 +108,33 @@ class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
                             .isEqualTo(likeCountBefore + 1);
                 });
             }
+        }
+
+        @Nested
+        @DisplayName("하나의 사용자가 여러번 동시에 좋아요를 누른 경우")
+        class 하나의_사용자가_여러번_동시에_좋아요를_누른_경우 {
+
+            @Test
+            @DisplayName("1번만 증가한다.")
+            void 동시에_하나의_사용자가_좋아요를_누르면_1번만_증가한다() throws InterruptedException {
+                int requestCount = 100;
+                ConcurrencyTestUtil.executeInParallelWithoutResult(
+                        requestCount,
+                        index -> productLikeService.like(new ProductLikeCommand(userIdentifier, productId))
+                );
+
+                Product productAfter = productRepository.getById(new ProductId(productId));
+                assertThat(productAfter.getLikeCount().value()).isEqualTo(1L);
+            }
+        }
+
+        @Nested
+        @DisplayName("동시에 여러 사용자가 좋아요를 누른 경우")
+        class 동시에_여러_사용자가_좋아요를_누른_경우 {
 
             @Test
-            @DisplayName("동시에 여러 사용자가 좋아요를 누르면 사용자 수 만큼 증가한다.")
-            void 동시에_여러_사용자가_좋아요를_누르면_사용자_수_만큼_증가한다() throws InterruptedException {
+            @DisplayName("사용자 수 만큼 좋아요 수가 증가한다.")
+            void 사용자_수_만큼_좋아요_수가_증가한다() throws InterruptedException {
                 int requestCount = 100;
 
                 ConcurrencyTestUtil.executeInParallelWithoutResult(
@@ -126,21 +153,9 @@ class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
                 Product productAfter = productRepository.getById(new ProductId(productId));
                 assertThat(productAfter.getLikeCount().value()).isEqualTo(requestCount);
             }
-
-            @Test
-            @DisplayName("동시에 하나의 사용자가 좋아요를 누르면 1번만 증가한다.")
-            void 동시에_하나의_사용자가_좋아요를_누르면_1번만_증가한다() throws InterruptedException {
-                int requestCount = 100;
-                ConcurrencyTestUtil.executeInParallelWithoutResult(
-                        requestCount,
-                        index -> productLikeService.like(new ProductLikeCommand(userIdentifier, productId))
-                );
-
-                Product productAfter = productRepository.getById(new ProductId(productId));
-                assertThat(productAfter.getLikeCount().value()).isEqualTo(1L);
-            }
         }
 
+
         @Nested
         @DisplayName("상품이 존재하고 이미 좋아요를 누른 경우")
         class 상품이_존재하고_이미_좋아요를_누른_경우 {
@@ -209,36 +224,33 @@ class 상품_좋아요_취소_시 {
         private String productId;
         private String userIdentifier;
 
-        @BeforeEach
-        void setUp() {
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("Apple"),
-                    new BrandDescription("Apple products")
-            ));
-            brandId = brand.getBrandId();
-
-            Product product = productRepository.save(Product.create(
-                    brandId,
-                    new ProductName("MacBook Pro"),
-                    new ProductPrice(new BigDecimal("1500000"))
-            ));
-            productId = product.getProductId().value();
-
-            User user = userRepository.save(User.create(
-                    new UserIdentifier("user123"),
-                    new UserEmail("user@example.com"),
-                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                    UserGender.MALE
-            ));
-            userIdentifier = user.getIdentifier().value();
-        }
-
         @Nested
         @DisplayName("상품이 존재하고 이미 좋아요를 누른 경우")
         class 상품이_존재하고_이미_좋아요를_누른_경우 {
 
             @BeforeEach
             void setUp() {
+                Brand brand = brandRepository.save(Brand.create(
+                        new BrandName("Apple"),
+                        new BrandDescription("Apple products")
+                ));
+                brandId = brand.getBrandId();
+
+                Product product = productRepository.save(Product.create(
+                        brandId,
+                        new ProductName("MacBook Pro"),
+                        new ProductPrice(new BigDecimal("1500000"))
+                ));
+                productId = product.getId().value();
+
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("user123"),
+                        new UserEmail("user@example.com"),
+                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                        UserGender.MALE
+                ));
+                userIdentifier = user.getIdentifier().value();
+
                 ProductLikeCommand likeCommand = new ProductLikeCommand(userIdentifier, productId);
                 productLikeService.like(likeCommand);
             }
@@ -247,8 +259,6 @@ void setUp() {
             @DisplayName("좋아요가 삭제되고 상품의 좋아요 수가 1 감소한다.")
             void 좋아요가_삭제되고_상품의_좋아요_수가_1_감소한다() {
                 ProductUnlikeCommand command = new ProductUnlikeCommand(userIdentifier, productId);
-                Product productBefore = productRepository.getById(new ProductId(productId));
-                long likeCountBefore = productBefore.getLikeCount().value();
 
                 productLikeService.unlike(command);
 
@@ -256,15 +266,143 @@ void setUp() {
                 assertSoftly(softly -> {
                     softly.assertThat(productAfter.getLikeCount().value())
                             .as("상품의 좋아요 수가 1 감소해야 함")
-                            .isEqualTo(likeCountBefore - 1);
+                            .isEqualTo(0L);
                 });
             }
         }
 
+        @Nested
+        @DisplayName("동시에 하나의 사용자가 여러번 취소하면")
+        class 동시에_하나의_사용자가_여러번_취소하면 {
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(Brand.create(
+                        new BrandName("Apple"),
+                        new BrandDescription("Apple products")
+                ));
+                brandId = brand.getBrandId();
+
+                Product product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field("brandId"), brandId)
+                                .set(field("id"), ProductId.empty())
+                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .create()
+                );
+                productId = product.getId().value();
+
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("user123"),
+                        new UserEmail("user@example.com"),
+                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                        UserGender.MALE
+                ));
+                userIdentifier = user.getIdentifier().value();
+
+                productLikeRepository.save(
+                        ProductLike.create(
+                                user.getUserId(),
+                                product.getId()
+                        )
+                );
+            }
+
+            @Test
+            @DisplayName("동시에 하나의 사용자가 여러번 취소하면 1만 감소한다.")
+            void 동시에_하나의_사용자가_여러번_취소하면_1만_감소한다() throws InterruptedException {
+                ProductUnlikeCommand command = new ProductUnlikeCommand(userIdentifier, productId);
+
+                int requestCount = 100;
+                ConcurrencyTestUtil.executeInParallelWithoutResult(
+                        requestCount,
+                        index -> productLikeService.unlike(command)
+                );
+
+                Product productAfter = productRepository.getById(new ProductId(productId));
+                assertThat(productAfter.getLikeCount().value()).isEqualTo(99L);
+            }
+        }
+
+        @Nested
+        @DisplayName("동시에 여러 사용자가 취소하면")
+        class 동시에_여러_사용자가_취소하면 {
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(Brand.create(
+                        new BrandName("Apple"),
+                        new BrandDescription("Apple products")
+                ));
+                brandId = brand.getBrandId();
+
+                Product product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field("brandId"), brandId)
+                                .set(field("id"), ProductId.empty())
+                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .create()
+                );
+                productId = product.getId().value();
+            }
+
+            @Test
+            @DisplayName("사용자의 수만큼 좋아요 수가 감소한다.")
+            void 사용자의_수만큼_좋아요_수가_감소한다() throws InterruptedException {
+                int requestCount = 100;
+
+                ConcurrencyTestUtil.executeInParallelWithoutResult(
+                        requestCount,
+                        index -> {
+                            User user = userRepository.save(User.create(
+                                    new UserIdentifier("user_" + index),
+                                    new UserEmail("user" + index + "@example.com"),
+                                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                                    UserGender.MALE
+                            ));
+                            productLikeRepository.save(
+                                    ProductLike.create(
+                                            user.getUserId(),
+                                            new ProductId(productId)
+                                    )
+                            );
+                            productLikeService.unlike(new ProductUnlikeCommand(user.getIdentifier().value(), productId));
+                        }
+                );
+                Product product = productRepository.getById(new ProductId(productId));
+                assertThat(product.getLikeCount().value()).isEqualTo(0L);
+            }
+        }
+
         @Nested
         @DisplayName("상품이 존재하고 좋아요를 누른 적이 없는 경우")
         class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
 
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(Brand.create(
+                        new BrandName("Apple"),
+                        new BrandDescription("Apple products")
+                ));
+                brandId = brand.getBrandId();
+
+                Product product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field("brandId"), brandId)
+                                .set(field("id"), ProductId.empty())
+                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .create()
+                );
+                productId = product.getId().value();
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("user123"),
+                        new UserEmail("user@example.com"),
+                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                        UserGender.MALE
+                ));
+                userIdentifier = user.getIdentifier().value();
+            }
+
             @Test
             @DisplayName("아무것도 하지 않는다.")
             void 아무것도_하지_않는다() {
@@ -287,6 +425,17 @@ class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
         @DisplayName("상품이 존재하지 않는 경우")
         class 상품이_존재하지_않는_경우 {
 
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("user123"),
+                        new UserEmail("user@example.com"),
+                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
+                        UserGender.MALE
+                ));
+                userIdentifier = user.getIdentifier().value();
+            }
+
             @Test
             @DisplayName("NotFoundException이 던져진다.")
             void NotFoundException이_던져진다() {

From 9da4b2a5834f6f7178b983655fc612bda5475238 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 22:06:51 +0900
Subject: [PATCH 06/29] =?UTF-8?q?feature=20:=20=EB=B8=8C=EB=9E=9C=EB=93=9C?=
 =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts            |  1 +
 .../application/api/brand/BrandV1Api.java     | 29 +++++++++
 .../application/api/brand/BrandV1ApiSpec.java | 18 ++++++
 .../application/api/brand/BrandV1Dto.java     | 18 ++++++
 .../application/api/brand/BrandV1ApiTest.java | 62 +++++++++++++++++++
 .../com/loopers/core/domain/brand/Brand.java  |  6 +-
 .../mysql/brand/entity/BrandEntity.java       |  2 +-
 .../OrderQueryServiceIntegrationTest.java     |  2 +-
 .../service/brand/BrandQueryServiceTest.java  |  2 +-
 .../product/ProductQueryServiceTest.java      | 10 +--
 .../ProductLikeQueryServiceTest.java          |  4 +-
 .../productlike/ProductLikeServiceTest.java   | 10 +--
 12 files changed, 146 insertions(+), 18 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Api.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index a5f94f5ad..29cccb229 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -8,6 +8,7 @@ dependencies {
 
     //service
     implementation(project(":core:service:user-service"))
+    implementation(project(":core:service:product-service"))
 
     //domain
     implementation(project(":core:domain"))
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Api.java
new file mode 100644
index 000000000..5f7101c3c
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Api.java
@@ -0,0 +1,29 @@
+package com.loopers.application.api.brand;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.brand.Brand;
+import com.loopers.core.service.brand.BrandQueryService;
+import com.loopers.core.service.brand.query.GetBrandQuery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.loopers.application.api.brand.BrandV1Dto.GetBrandResponse;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/brands")
+public class BrandV1Api implements BrandV1ApiSpec {
+
+    private final BrandQueryService queryService;
+
+    @Override
+    @GetMapping("/{brandId}")
+    public ApiResponse<GetBrandResponse> getBrand(@PathVariable String brandId) {
+        Brand brand = queryService.getBrandBy(new GetBrandQuery(brandId));
+
+        return ApiResponse.success(GetBrandResponse.from(brand));
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
new file mode 100644
index 000000000..ab43f7801
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
@@ -0,0 +1,18 @@
+package com.loopers.application.api.brand;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import static com.loopers.application.api.brand.BrandV1Dto.GetBrandResponse;
+
+@Tag(name = "Brand V1 API", description = "Brand API 입니다.")
+public interface BrandV1ApiSpec {
+
+    @Operation(
+            summary = "사용자 조회",
+            description = "ID로 사용자를 조회합니다."
+    )
+    ApiResponse<GetBrandResponse> getBrand(String brandId);
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Dto.java
new file mode 100644
index 000000000..39b8508fb
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1Dto.java
@@ -0,0 +1,18 @@
+package com.loopers.application.api.brand;
+
+import com.loopers.core.domain.brand.Brand;
+
+public class BrandV1Dto {
+
+    public record GetBrandResponse(
+            String name,
+            String description
+    ) {
+        public static GetBrandResponse from(Brand brand) {
+            return new GetBrandResponse(
+                    brand.getName().value(),
+                    brand.getDescription().value()
+            );
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java
new file mode 100644
index 000000000..7165010af
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java
@@ -0,0 +1,62 @@
+package com.loopers.application.api.brand;
+
+import com.loopers.application.api.ApiIntegrationTest;
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.brand.Brand;
+import com.loopers.core.domain.brand.repository.BrandRepository;
+import com.loopers.core.domain.brand.vo.BrandDescription;
+import com.loopers.core.domain.brand.vo.BrandName;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class BrandV1ApiTest extends ApiIntegrationTest {
+
+    @Autowired
+    BrandRepository brandRepository;
+
+    @Nested
+    @DisplayName("브랜드 조회")
+    class 브랜드_조회 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            String brandId;
+
+            @BeforeEach
+            void setUp() {
+                brandId = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                ).getId().value();
+            }
+
+            @Test
+            @DisplayName("브랜드의 정보를 조회한다.")
+            void 브랜드의_정보를_조회한다() {
+                //given
+                String endPoint = "/api/v1/brands/" + brandId;
+                ParameterizedTypeReference<ApiResponse<BrandV1Dto.GetBrandResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                //when
+                ResponseEntity<ApiResponse<BrandV1Dto.GetBrandResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                //then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+            }
+        }
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/brand/Brand.java b/core/domain/src/main/java/com/loopers/core/domain/brand/Brand.java
index d4384825e..50811e030 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/brand/Brand.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/brand/Brand.java
@@ -11,7 +11,7 @@
 @Getter
 public class Brand {
 
-    private final BrandId brandId;
+    private final BrandId id;
 
     private final BrandName name;
 
@@ -24,14 +24,14 @@ public class Brand {
     private final DeletedAt deletedAt;
 
     private Brand(
-            BrandId brandId,
+            BrandId id,
             BrandName name,
             BrandDescription description,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        this.brandId = brandId;
+        this.id = id;
         this.name = name;
         this.description = description;
         this.createdAt = createdAt;
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/brand/entity/BrandEntity.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/brand/entity/BrandEntity.java
index 7c2657ce5..7371d49dc 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/brand/entity/BrandEntity.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/brand/entity/BrandEntity.java
@@ -42,7 +42,7 @@ public class BrandEntity {
 
     public static BrandEntity from(Brand brand) {
         return new BrandEntity(
-                Optional.ofNullable(brand.getBrandId().value())
+                Optional.ofNullable(brand.getId().value())
                         .map(Long::parseLong)
                         .orElse(null),
                 brand.getName().value(),
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index 795de1127..77a088e09 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -241,7 +241,7 @@ void setUp() {
                     new BrandName("loopers"),
                     new BrandDescription("education brand")
             ));
-            savedBrandId = brand.getBrandId();
+            savedBrandId = brand.getId();
 
             User user = userRepository.save(User.create(
                     UserIdentifier.create("loopers"),
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
index d211291ce..930205c37 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
@@ -40,7 +40,7 @@ void setUp() {
                 brandId = brandRepository.save(Brand.create(
                         new BrandName("loopers"),
                         new BrandDescription("education brand")
-                )).getBrandId();
+                )).getId();
             }
 
             @Test
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
index 861ecdfc1..2bbaf99f8 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
@@ -52,13 +52,13 @@ void setUp() {
                     new BrandName("loopers"),
                     new BrandDescription("education brand")
             ));
-            savedBrandId = brand.getBrandId();
+            savedBrandId = brand.getId();
 
             Brand otherBrand = brandRepository.save(Brand.create(
                     new BrandName("other"),
                     new BrandDescription("other brand")
             ));
-            otherBrandId = otherBrand.getBrandId();
+            otherBrandId = otherBrand.getId();
         }
 
         @Nested
@@ -264,7 +264,7 @@ void setUp() {
                     new BrandName("loopers"),
                     new BrandDescription("education brand")
             ));
-            savedBrandId = brand.getBrandId();
+            savedBrandId = brand.getId();
 
             Product product = productRepository.save(
                     Product.create(
@@ -324,7 +324,7 @@ void setUp() {
                     new BrandName("loopers"),
                     new BrandDescription("education brand")
             ));
-            savedBrandId = savedBrand.getBrandId();
+            savedBrandId = savedBrand.getId();
 
             Product product = productRepository.save(
                     Product.create(
@@ -366,7 +366,7 @@ class 상품이_존재하는_경우 {
                             .isEqualTo(savedProductId);
                     softly.assertThat(result.getProduct().getBrandId().value())
                             .isEqualTo(savedBrandId.value());
-                    softly.assertThat(result.getBrand().getBrandId().value())
+                    softly.assertThat(result.getBrand().getId().value())
                             .isEqualTo(savedBrandId.value());
                 });
             }
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
index 4554a8cb5..202034a83 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
@@ -72,13 +72,13 @@ void setUp() {
                     new BrandName("Apple"),
                     new BrandDescription("Apple products")
             ));
-            savedBrandId = brand.getBrandId();
+            savedBrandId = brand.getId();
 
             Brand otherBrand = brandRepository.save(Brand.create(
                     new BrandName("Samsung"),
                     new BrandDescription("Samsung products")
             ));
-            otherBrandId = otherBrand.getBrandId();
+            otherBrandId = otherBrand.getId();
         }
 
         @Nested
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index 5674fb393..b9dc3be5a 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -70,7 +70,7 @@ void setUp() {
                     new BrandName("Apple"),
                     new BrandDescription("Apple products")
             ));
-            brandId = brand.getBrandId();
+            brandId = brand.getId();
 
             Product product = productRepository.save(Product.create(
                     brandId,
@@ -234,7 +234,7 @@ void setUp() {
                         new BrandName("Apple"),
                         new BrandDescription("Apple products")
                 ));
-                brandId = brand.getBrandId();
+                brandId = brand.getId();
 
                 Product product = productRepository.save(Product.create(
                         brandId,
@@ -281,7 +281,7 @@ void setUp() {
                         new BrandName("Apple"),
                         new BrandDescription("Apple products")
                 ));
-                brandId = brand.getBrandId();
+                brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)
@@ -334,7 +334,7 @@ void setUp() {
                         new BrandName("Apple"),
                         new BrandDescription("Apple products")
                 ));
-                brandId = brand.getBrandId();
+                brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)
@@ -384,7 +384,7 @@ void setUp() {
                         new BrandName("Apple"),
                         new BrandDescription("Apple products")
                 ));
-                brandId = brand.getBrandId();
+                brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)

From 042bba09e2c575eaf1bb14fb3b01352a0f7da874 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 22:33:14 +0900
Subject: [PATCH 07/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 상품 목록 조회 시 orderBy NPE 오류 수정
---
 .../application/api/product/ProductV1Api.java | 35 +++++++
 .../api/product/ProductV1ApiSpec.java         | 23 +++++
 .../application/api/product/ProductV1Dto.java | 50 ++++++++++
 .../api/product/ProductV1ApiTest.java         | 97 +++++++++++++++++++
 .../ProductQuerydslRepositoryImpl.java        | 11 ++-
 5 files changed, 213 insertions(+), 3 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
new file mode 100644
index 000000000..226f9f1ff
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
@@ -0,0 +1,35 @@
+package com.loopers.application.api.product;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.product.ProductListView;
+import com.loopers.core.service.product.ProductQueryService;
+import com.loopers.core.service.product.query.GetProductListQuery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/products")
+public class ProductV1Api implements ProductV1ApiSpec {
+
+    private final ProductQueryService queryService;
+
+    @Override
+    @GetMapping("/")
+    public ApiResponse<ProductV1Dto.GetProductListResponse> getProductList(
+            @RequestParam String brandId,
+            @RequestParam String createdAtSort,
+            @RequestParam String priceSort,
+            @RequestParam String likeCountSort,
+            @RequestParam int pageNo,
+            @RequestParam int pageSize
+    ) {
+        ProductListView productList = queryService.getProductList(new GetProductListQuery(
+                brandId, createdAtSort, priceSort, likeCountSort, pageNo, pageSize
+        ));
+        return ApiResponse.success(ProductV1Dto.GetProductListResponse.from(productList));
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
new file mode 100644
index 000000000..ce2275b05
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
@@ -0,0 +1,23 @@
+package com.loopers.application.api.product;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "Product V1 API", description = "Product API 입니다.")
+public interface ProductV1ApiSpec {
+
+    @Operation(
+            summary = "상품 목록 조회",
+            description = "상품 목록을 조회합니다."
+    )
+    ApiResponse<ProductV1Dto.GetProductListResponse> getProductList(
+            String brandId,
+            String createdAtSort,
+            String priceSort,
+            String likeCountSort,
+            int pageNo,
+            int pageSize
+    );
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
new file mode 100644
index 000000000..7e580ea66
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
@@ -0,0 +1,50 @@
+package com.loopers.application.api.product;
+
+import com.loopers.core.domain.product.ProductListItem;
+import com.loopers.core.domain.product.ProductListView;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+public class ProductV1Dto {
+
+    public record GetProductListResponse(
+            List<GetProductListItemResponse> items,
+            long totalElements,
+            int totalPages,
+            boolean hasNext,
+            boolean hasPrevious
+    ) {
+        public static GetProductListResponse from(ProductListView view) {
+            return new GetProductListResponse(
+                    view.getItems().stream()
+                            .map(GetProductListItemResponse::from)
+                            .toList(),
+                    view.getTotalElements(),
+                    view.getTotalPages(),
+                    view.isHasNext(),
+                    view.isHasPrevious()
+            );
+        }
+
+        public record GetProductListItemResponse(
+                String productId,
+                String brandId,
+                String name,
+                BigDecimal price,
+                Long stock,
+                Long likeCount
+        ) {
+            public static GetProductListItemResponse from(ProductListItem item) {
+                return new GetProductListItemResponse(
+                        item.getProductId().value(),
+                        item.getBrandId().value(),
+                        item.getName().value(),
+                        item.getPrice().value(),
+                        item.getStock().value(),
+                        item.getLikeCount().value()
+                );
+            }
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java
new file mode 100644
index 000000000..662c3a7bc
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java
@@ -0,0 +1,97 @@
+package com.loopers.application.api.product;
+
+import com.loopers.application.api.ApiIntegrationTest;
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.brand.Brand;
+import com.loopers.core.domain.brand.repository.BrandRepository;
+import com.loopers.core.domain.brand.vo.BrandDescription;
+import com.loopers.core.domain.brand.vo.BrandId;
+import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.product.Product;
+import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductName;
+import com.loopers.core.domain.product.vo.ProductPrice;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+class ProductV1ApiTest extends ApiIntegrationTest {
+
+    @Autowired
+    private BrandRepository brandRepository;
+
+    @Autowired
+    private ProductRepository productRepository;
+
+    @Nested
+    @DisplayName("상품 목록 조회")
+    class 상품_목록_조회 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            String brandId;
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("guerlain"), new BrandDescription("명품 향수 브랜드"))
+                );
+                brandId = brand.getId().value();
+
+                Product product1 = productRepository.save(
+                        Product.create(
+                                new BrandId(brandId),
+                                new ProductName("라 푸파](향수)"),
+                                new ProductPrice(new BigDecimal("150.00"))
+                        )
+                );
+
+                Product product2 = productRepository.save(
+                        Product.create(
+                                new BrandId(brandId),
+                                new ProductName("라로스 에엑셀렌즈(향수)"),
+                                new ProductPrice(new BigDecimal("200.00"))
+                        )
+                );
+            }
+
+            @Test
+            @DisplayName("브랜드 ID로 상품 목록을 조회한다.")
+            void 브랜드_ID로_상품_목록을_조회한다() {
+                // When
+                String endPoint = "/api/v1/products/?brandId=" + brandId
+                        + "&createdAtSort=&priceSort=&likeCountSort=ASC&pageNo=0&pageSize=10";
+
+                ParameterizedTypeReference<ApiResponse<ProductV1Dto.GetProductListResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<ProductV1Dto.GetProductListResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertSoftly(softly -> {
+                    softly.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                    softly.assertThat(response.getBody()).isNotNull();
+                    softly.assertThat(response.getBody().data()).isNotNull();
+                    softly.assertThat(response.getBody().data().items()).hasSize(2);
+                    softly.assertThat(response.getBody().data().totalElements()).isEqualTo(2);
+                    softly.assertThat(response.getBody().data().totalPages()).isGreaterThan(0);
+                });
+            }
+        }
+    }
+}
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductQuerydslRepositoryImpl.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductQuerydslRepositoryImpl.java
index 0c6d73a5b..3d4680eaf 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductQuerydslRepositoryImpl.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/product/ProductQuerydslRepositoryImpl.java
@@ -14,7 +14,9 @@
 import org.springframework.stereotype.Component;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Stream;
 
 import static com.loopers.core.infra.database.mysql.product.entity.QProductEntity.productEntity;
 
@@ -51,9 +53,12 @@ public Page<ProductListProjection> findListWithCondition(
                 .offset(pageable.getOffset())
                 .limit(pageable.getPageSize())
                 .orderBy(
-                        orderByCreatedAt(createdAtSort),
-                        orderByPrice(priceSort),
-                        orderByLikeCount(likeCountSort)
+                        Stream.of(
+                                        orderByCreatedAt(createdAtSort),
+                                        orderByPrice(priceSort),
+                                        orderByLikeCount(likeCountSort)
+                                ).filter(Objects::nonNull)
+                                .toArray(OrderSpecifier[]::new)
                 ).fetch();
 
         JPAQuery<Long> countQuery = queryFactory

From 252c7560b89a1f09e69698c00dae9057883ad42b Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 22:47:40 +0900
Subject: [PATCH 08/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84=20=EB=A7=9F=20=ED=83=9C=EC=88=98=ED=88=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/product/ProductV1Api.java | 14 +++--
 .../api/product/ProductV1ApiSpec.java         |  6 ++
 .../application/api/product/ProductV1Dto.java | 21 +++++++
 ...st.java => BrandV1ApiIntegrationTest.java} |  2 +-
 ....java => ProductV1ApiIntegrationTest.java} | 63 +++++++++++++++++--
 5 files changed, 95 insertions(+), 11 deletions(-)
 rename apps/commerce-api/src/test/java/com/loopers/application/api/brand/{BrandV1ApiTest.java => BrandV1ApiIntegrationTest.java} (97%)
 rename apps/commerce-api/src/test/java/com/loopers/application/api/product/{ProductV1ApiTest.java => ProductV1ApiIntegrationTest.java} (59%)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
index 226f9f1ff..d2309ef28 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
@@ -1,14 +1,13 @@
 package com.loopers.application.api.product;
 
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.ProductListView;
 import com.loopers.core.service.product.ProductQueryService;
 import com.loopers.core.service.product.query.GetProductListQuery;
+import com.loopers.core.service.product.query.GetProductQuery;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequiredArgsConstructor
@@ -17,6 +16,13 @@ public class ProductV1Api implements ProductV1ApiSpec {
 
     private final ProductQueryService queryService;
 
+    @Override
+    @GetMapping("/{productId}")
+    public ApiResponse<ProductV1Dto.GetProductResponse> getProduct(@PathVariable String productId) {
+        Product product = queryService.getProductBy(new GetProductQuery(productId));
+        return ApiResponse.success(ProductV1Dto.GetProductResponse.from(product));
+    }
+
     @Override
     @GetMapping("/")
     public ApiResponse<ProductV1Dto.GetProductListResponse> getProductList(
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
index ce2275b05..df547bae7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
@@ -7,6 +7,12 @@
 @Tag(name = "Product V1 API", description = "Product API 입니다.")
 public interface ProductV1ApiSpec {
 
+    @Operation(
+            summary = "상품 정보 조회",
+            description = "상품 정보를 조회합니다."
+    )
+    ApiResponse<ProductV1Dto.GetProductResponse> getProduct(String productId);
+
     @Operation(
             summary = "상품 목록 조회",
             description = "상품 목록을 조회합니다."
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
index 7e580ea66..1b7aa2eb6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Dto.java
@@ -1,5 +1,6 @@
 package com.loopers.application.api.product;
 
+import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.ProductListItem;
 import com.loopers.core.domain.product.ProductListView;
 
@@ -47,4 +48,24 @@ public static GetProductListItemResponse from(ProductListItem item) {
             }
         }
     }
+
+    public record GetProductResponse(
+            String productId,
+            String brandId,
+            String name,
+            BigDecimal price,
+            Long stock,
+            Long likeCount
+    ) {
+        public static GetProductResponse from(Product product) {
+            return new GetProductResponse(
+                    product.getId().value(),
+                    product.getBrandId().value(),
+                    product.getName().value(),
+                    product.getPrice().value(),
+                    product.getStock().value(),
+                    product.getLikeCount().value()
+            );
+        }
+    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
similarity index 97%
rename from apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
index 7165010af..805cd3ed8 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
@@ -19,7 +19,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-class BrandV1ApiTest extends ApiIntegrationTest {
+class BrandV1ApiIntegrationTest extends ApiIntegrationTest {
 
     @Autowired
     BrandRepository brandRepository;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
similarity index 59%
rename from apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
index 662c3a7bc..74a89e40d 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
@@ -24,9 +24,11 @@
 
 import java.math.BigDecimal;
 
+import static com.loopers.application.api.product.ProductV1Dto.GetProductListResponse;
+import static com.loopers.application.api.product.ProductV1Dto.GetProductResponse;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
-class ProductV1ApiTest extends ApiIntegrationTest {
+class ProductV1ApiIntegrationTest extends ApiIntegrationTest {
 
     @Autowired
     private BrandRepository brandRepository;
@@ -47,14 +49,14 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("guerlain"), new BrandDescription("명품 향수 브랜드"))
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
                 );
                 brandId = brand.getId().value();
 
                 Product product1 = productRepository.save(
                         Product.create(
                                 new BrandId(brandId),
-                                new ProductName("라 푸파](향수)"),
+                                new ProductName("엔젤스 쉐어"),
                                 new ProductPrice(new BigDecimal("150.00"))
                         )
                 );
@@ -62,7 +64,7 @@ void setUp() {
                 Product product2 = productRepository.save(
                         Product.create(
                                 new BrandId(brandId),
-                                new ProductName("라로스 에엑셀렌즈(향수)"),
+                                new ProductName("라로스 에엑셀렌즈"),
                                 new ProductPrice(new BigDecimal("200.00"))
                         )
                 );
@@ -75,11 +77,11 @@ void setUp() {
                 String endPoint = "/api/v1/products/?brandId=" + brandId
                         + "&createdAtSort=&priceSort=&likeCountSort=ASC&pageNo=0&pageSize=10";
 
-                ParameterizedTypeReference<ApiResponse<ProductV1Dto.GetProductListResponse>> responseType =
+                ParameterizedTypeReference<ApiResponse<GetProductListResponse>> responseType =
                         new ParameterizedTypeReference<>() {
                         };
 
-                ResponseEntity<ApiResponse<ProductV1Dto.GetProductListResponse>> response =
+                ResponseEntity<ApiResponse<GetProductListResponse>> response =
                         testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
 
                 // Then
@@ -94,4 +96,53 @@ void setUp() {
             }
         }
     }
+
+    @Nested
+    @DisplayName("상품 정보 조회")
+    class 상품_정보_조회 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            String productId;
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                productId = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00"))
+                        )
+                ).getId().value();
+            }
+
+            @Test
+            @DisplayName("브랜드 ID로 상품 목록을 조회한다.")
+            void 브랜드_ID로_상품_목록을_조회한다() {
+                // When
+                String endPoint = "/api/v1/products/" + productId;
+
+
+                ParameterizedTypeReference<ApiResponse<GetProductResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<GetProductResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertSoftly(softly -> {
+                    softly.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                    softly.assertThat(response.getBody()).isNotNull();
+                    softly.assertThat(response.getBody().data()).isNotNull();
+                });
+            }
+        }
+    }
 }

From 725e19933eb30359d97b23903e1ea4c9d57f1730 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 23:10:15 +0900
Subject: [PATCH 09/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94,=20=EC=B7=A8=EC=86=8C=20API=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/productlike/ProductLikeV1Api.java     |  36 ++
 .../api/productlike/ProductLikeV1ApiSpec.java |  21 ++
 .../api/productlike/ProductLikeV1Dto.java     |   8 +
 .../ProductLikeV1ApiIntegrationTest.java      | 349 ++++++++++++++++++
 4 files changed, 414 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
new file mode 100644
index 000000000..d52d5739e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
@@ -0,0 +1,36 @@
+package com.loopers.application.api.productlike;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.service.productlike.ProductLikeService;
+import com.loopers.core.service.productlike.command.ProductLikeCommand;
+import com.loopers.core.service.productlike.command.ProductUnlikeCommand;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/like/products")
+public class ProductLikeV1Api implements ProductLikeV1ApiSpec {
+
+    private final ProductLikeService productLikeService;
+
+    @Override
+    @PostMapping("/{productId}")
+    public ApiResponse<Void> likeProduct(
+            @PathVariable String productId,
+            @RequestHeader(name = "X-USER-ID") String userIdentifier
+    ) {
+        productLikeService.like(new ProductLikeCommand(userIdentifier, productId));
+        return ApiResponse.success(null);
+    }
+
+    @Override
+    @DeleteMapping("/{productId}")
+    public ApiResponse<Void> unlikeProduct(
+            @PathVariable String productId,
+            @RequestHeader(name = "X-USER-ID") String userIdentifier
+    ) {
+        productLikeService.unlike(new ProductUnlikeCommand(userIdentifier, productId));
+        return ApiResponse.success(null);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
new file mode 100644
index 000000000..6ec8958fc
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
@@ -0,0 +1,21 @@
+package com.loopers.application.api.productlike;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "ProductLike V1 API", description = "상품 좋아요 API 입니다.")
+public interface ProductLikeV1ApiSpec {
+
+    @Operation(
+            summary = "상품 좋아요 등록",
+            description = "상품에 좋아요를 등록합니다."
+    )
+    ApiResponse<Void> likeProduct(String productId, String userIdentifier);
+
+    @Operation(
+            summary = "상품 좋아요 취소",
+            description = "상품의 좋아요를 취소합니다."
+    )
+    ApiResponse<Void> unlikeProduct(String productId, String userIdentifier);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
new file mode 100644
index 000000000..8a94dad3d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
@@ -0,0 +1,8 @@
+package com.loopers.application.api.productlike;
+
+public class ProductLikeV1Dto {
+
+    public record LikeProductRequest() {}
+
+    public record UnlikeProductRequest() {}
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
new file mode 100644
index 000000000..da28b42c2
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
@@ -0,0 +1,349 @@
+package com.loopers.application.api.productlike;
+
+import com.loopers.application.api.ApiIntegrationTest;
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.brand.Brand;
+import com.loopers.core.domain.brand.repository.BrandRepository;
+import com.loopers.core.domain.brand.vo.BrandDescription;
+import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.product.Product;
+import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductId;
+import com.loopers.core.domain.product.vo.ProductName;
+import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.*;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ProductLikeV1ApiIntegrationTest extends ApiIntegrationTest {
+
+    @Autowired
+    protected UserRepository userRepository;
+
+    @Autowired
+    protected BrandRepository brandRepository;
+
+    @Autowired
+    protected ProductRepository productRepository;
+
+    protected String userId;
+    protected String productId;
+    protected Product product;
+
+    @Nested
+    @DisplayName("상품 좋아요 등록")
+    class 상품_좋아요_등록 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userId = user.getIdentifier().value();
+
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                product = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00"))
+                        )
+                );
+                productId = product.getId().value();
+            }
+
+            @Test
+            @DisplayName("상품에 좋아요를 등록한다.")
+            void 상품에_좋아요를_등록한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+
+                // 좋아요 수 확인
+                Product likedProduct = productRepository.getById(new ProductId(productId));
+                assertThat(likedProduct.getLikeCount().value()).isEqualTo(1);
+            }
+        }
+
+        @Nested
+        @DisplayName("X-USER-ID 헤더가 없을 경우")
+        class X_USER_ID_헤더가_없을_경우 {
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 상품 ID로 요청할 경우")
+        class 존재하지_않는_상품_ID로_요청 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userId = user.getIdentifier().value();
+            }
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/99999";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 사용자로 요청할 경우")
+        class 존재하지_않는_사용자로_요청 {
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", "99999");
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+    }
+
+    @Nested
+    @DisplayName("상품 좋아요 취소")
+    class 상품_좋아요_취소 {
+
+        @Nested
+        @DisplayName("좋아요가 등록되어 있을 경우")
+        class 좋아요가_등록되어_있을_경우 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userId = user.getIdentifier().value();
+
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                productId = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00"))
+                        )
+                ).getId().value();
+
+                // 좋아요 등록
+                String endPoint = "/api/v1/like/products/" + productId;
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+            }
+
+            @Test
+            @DisplayName("상품의 좋아요를 취소한다.")
+            void 상품의_좋아요를_취소한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.DELETE, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+
+                // 좋아요 수 확인
+                Product unlikedProduct = productRepository.getById(new ProductId(productId));
+                assertThat(unlikedProduct.getLikeCount().value()).isEqualTo(0);
+            }
+        }
+
+        @Nested
+        @DisplayName("X-USER-ID 헤더가 없을 경우")
+        class X_USER_ID_헤더가_없을_경우 {
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.DELETE, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 상품 ID로 요청할 경우")
+        class 존재하지_않는_상품_ID로_요청 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userId = user.getIdentifier().value();
+            }
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/99999";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.DELETE, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 사용자로 요청할 경우")
+        class 존재하지_않는_사용자로_요청 {
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/" + productId;
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", "nonExistUser");
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.DELETE, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+    }
+}

From 8c5b76b23d8e4d63570f867832ea553eaec8b017 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 16 Nov 2025 23:52:40 +0900
Subject: [PATCH 10/29] =?UTF-8?q?feature=20:=20=EB=82=B4=EA=B0=80=20?=
 =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=ED=95=9C=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EB=B0=8F=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/brand/BrandV1ApiSpec.java |   2 +-
 .../api/product/ProductV1ApiSpec.java         |   2 +-
 .../api/productlike/ProductLikeV1Api.java     |  29 +++-
 .../api/productlike/ProductLikeV1ApiSpec.java |  15 ++
 .../api/productlike/ProductLikeV1Dto.java     |  47 +++++-
 .../application/api/user/UserV1ApiSpec.java   |   2 +-
 .../ProductLikeV1ApiIntegrationTest.java      | 148 ++++++++++++++++++
 .../ProductLikeQuerydslRepositoryImpl.java    |  11 +-
 .../productlike/ProductLikeQueryService.java  |   9 +-
 .../query/GetLikeProductsListQuery.java       |   6 +-
 10 files changed, 255 insertions(+), 16 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
index ab43f7801..ca04e59d7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
@@ -6,7 +6,7 @@
 
 import static com.loopers.application.api.brand.BrandV1Dto.GetBrandResponse;
 
-@Tag(name = "Brand V1 API", description = "Brand API 입니다.")
+@Tag(name = "Brand V1 API", description = "브랜드 API 입니다.")
 public interface BrandV1ApiSpec {
 
     @Operation(
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
index df547bae7..65a2e39a0 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java
@@ -4,7 +4,7 @@
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
-@Tag(name = "Product V1 API", description = "Product API 입니다.")
+@Tag(name = "Product V1 API", description = "상품 API 입니다.")
 public interface ProductV1ApiSpec {
 
     @Operation(
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
index d52d5739e..4b355df6d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
@@ -1,18 +1,24 @@
 package com.loopers.application.api.productlike;
 
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.productlike.LikeProductListView;
+import com.loopers.core.service.productlike.ProductLikeQueryService;
 import com.loopers.core.service.productlike.ProductLikeService;
 import com.loopers.core.service.productlike.command.ProductLikeCommand;
 import com.loopers.core.service.productlike.command.ProductUnlikeCommand;
+import com.loopers.core.service.productlike.query.GetLikeProductsListQuery;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
+import static com.loopers.application.api.productlike.ProductLikeV1Dto.LikeProductsResponse;
+
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/api/v1/like/products")
 public class ProductLikeV1Api implements ProductLikeV1ApiSpec {
 
-    private final ProductLikeService productLikeService;
+    private final ProductLikeService service;
+    private final ProductLikeQueryService queryService;
 
     @Override
     @PostMapping("/{productId}")
@@ -20,7 +26,7 @@ public ApiResponse<Void> likeProduct(
             @PathVariable String productId,
             @RequestHeader(name = "X-USER-ID") String userIdentifier
     ) {
-        productLikeService.like(new ProductLikeCommand(userIdentifier, productId));
+        service.like(new ProductLikeCommand(userIdentifier, productId));
         return ApiResponse.success(null);
     }
 
@@ -30,7 +36,24 @@ public ApiResponse<Void> unlikeProduct(
             @PathVariable String productId,
             @RequestHeader(name = "X-USER-ID") String userIdentifier
     ) {
-        productLikeService.unlike(new ProductUnlikeCommand(userIdentifier, productId));
+        service.unlike(new ProductUnlikeCommand(userIdentifier, productId));
         return ApiResponse.success(null);
     }
+
+    @Override
+    @GetMapping("/")
+    public ApiResponse<LikeProductsResponse> getLikeProducts(
+            @RequestHeader(name = "X-USER-ID") String userId,
+            @RequestParam String brandId,
+            @RequestParam String createdAtSort,
+            @RequestParam String priceSort,
+            @RequestParam String likeCountSort,
+            @RequestParam int pageNo,
+            @RequestParam int pageSize
+    ) {
+        LikeProductListView likeProductsListView = queryService.getLikeProductsListView(
+                new GetLikeProductsListQuery(userId, brandId, createdAtSort, priceSort, likeCountSort, pageNo, pageSize)
+        );
+        return ApiResponse.success(LikeProductsResponse.from(likeProductsListView));
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
index 6ec8958fc..19095b89f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1ApiSpec.java
@@ -18,4 +18,19 @@ public interface ProductLikeV1ApiSpec {
             description = "상품의 좋아요를 취소합니다."
     )
     ApiResponse<Void> unlikeProduct(String productId, String userIdentifier);
+
+    @Operation(
+            summary = "좋아요 한 상품 목록 조회",
+            description = "사용자가 좋아요한 상품 목록을 조회합니다."
+    )
+    ApiResponse<ProductLikeV1Dto.LikeProductsResponse> getLikeProducts(
+            String userId,
+            String brandId,
+            String createdAtSort,
+            String priceSort,
+            String likeCountSort,
+            int pageNo,
+            int pageSize
+    );
+
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
index 8a94dad3d..dd6c9cb82 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Dto.java
@@ -1,8 +1,51 @@
 package com.loopers.application.api.productlike;
 
+import com.loopers.core.domain.productlike.LikeProductListItem;
+import com.loopers.core.domain.productlike.LikeProductListView;
+
+import java.math.BigDecimal;
+import java.util.List;
+
 public class ProductLikeV1Dto {
 
-    public record LikeProductRequest() {}
+    public record LikeProductsResponse(
+            List<LikeProductResponse> items,
+            long totalElements,
+            int totalPages,
+            boolean hasNext,
+            boolean hasPrevious
+    ) {
+        public static LikeProductsResponse from(LikeProductListView view) {
+            return new LikeProductsResponse(
+                    view.getItems().stream()
+                            .map(LikeProductResponse::from)
+                            .toList(),
+                    view.getTotalElements(),
+                    view.getTotalPages(),
+                    view.isHasNext(),
+                    view.isHasPrevious()
+            );
+        }
+
 
-    public record UnlikeProductRequest() {}
+        public record LikeProductResponse(
+                String productId,
+                String brandId,
+                String name,
+                BigDecimal price,
+                Long stock,
+                Long likeCount
+        ) {
+            public static LikeProductResponse from(LikeProductListItem item) {
+                return new LikeProductResponse(
+                        item.getProductId().value(),
+                        item.getBrandId().value(),
+                        item.getName().value(),
+                        item.getPrice().value(),
+                        item.getStock().value(),
+                        item.getLikeCount().value()
+                );
+            }
+        }
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
index b1f6899fb..d2d784bd2 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -7,7 +7,7 @@
 
 import static com.loopers.application.api.user.UserV1Dto.*;
 
-@Tag(name = "User V1 API", description = "User API 입니다.")
+@Tag(name = "User V1 API", description = "사용자 API 입니다.")
 public interface UserV1ApiSpec {
 
     @Operation(
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
index da28b42c2..0581bf03c 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
@@ -346,4 +346,152 @@ class 존재하지_않는_사용자로_요청 {
             }
         }
     }
+
+    @Nested
+    @DisplayName("좋아요한 상품 목록 조회")
+    class 좋아요한_상품_목록_조회 {
+
+        @BeforeEach
+        void setUp() {
+            // 사용자 생성
+            User user = userRepository.save(
+                    User.create(
+                            UserIdentifier.create("kilian"),
+                            UserEmail.create("kilian@gmail.com"),
+                            UserBirthDay.create("1997-10-08"),
+                            UserGender.create("MALE")
+                    )
+            );
+            userId = user.getIdentifier().value();
+
+            // 브랜드 생성
+            Brand brand = brandRepository.save(
+                    Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+            );
+
+            // 상품 생성 (2개)
+            Product product1 = productRepository.save(
+                    Product.create(
+                            brand.getId(),
+                            new ProductName("엔젤스 쉐어"),
+                            new ProductPrice(new BigDecimal("150.00"))
+                    )
+            );
+            Product product2 = productRepository.save(
+                    Product.create(
+                            brand.getId(),
+                            new ProductName("라로스 에센스"),
+                            new ProductPrice(new BigDecimal("200.00"))
+                    )
+            );
+
+            // 좋아요 등록
+            String likeEndpoint1 = "/api/v1/like/products/" + product1.getId().value();
+            String likeEndpoint2 = "/api/v1/like/products/" + product2.getId().value();
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("X-USER-ID", userId);
+            HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+            ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                    new ParameterizedTypeReference<>() {
+                    };
+
+            testRestTemplate.exchange(likeEndpoint1, HttpMethod.POST, httpEntity, responseType);
+            testRestTemplate.exchange(likeEndpoint2, HttpMethod.POST, httpEntity, responseType);
+        }
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            @Test
+            @DisplayName("사용자가 좋아요한 상품 목록을 조회한다.")
+            void 사용자가_좋아요한_상품_목록을_조회한다() {
+                // When
+                String endPoint = "/api/v1/like/products/?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+                assertThat(response.getBody().data()).isNotNull();
+                assertThat(response.getBody().data().items()).hasSize(2);
+                assertThat(response.getBody().data().totalElements()).isEqualTo(2);
+            }
+        }
+
+        @Nested
+        @DisplayName("X-USER-ID 헤더가 없을 경우")
+        class X_USER_ID_헤더가_없을_경우 {
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/?brandId=null&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+
+        @Nested
+        @DisplayName("좋아요한 상품이 없을 경우")
+        class 좋아요한_상품이_없을_경우 {
+
+            @BeforeEach
+            void setUp() {
+                // 다른 사용자 생성 (좋아요한 상품이 없음)
+                User otherUser = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("otherUser"),
+                                UserEmail.create("other@gmail.com"),
+                                UserBirthDay.create("2000-01-01"),
+                                UserGender.create("FEMALE")
+                        )
+                );
+                userId = otherUser.getIdentifier().value();
+            }
+
+            @Test
+            @DisplayName("빈 목록을 반환한다.")
+            void 빈_목록을_반환한다() {
+                // When
+                String endPoint = "/api/v1/like/products/?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userId);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ParameterizedTypeReference<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+                assertThat(response.getBody().data()).isNotNull();
+                assertThat(response.getBody().data().items()).isEmpty();
+                assertThat(response.getBody().data().totalElements()).isEqualTo(0);
+            }
+        }
+    }
 }
diff --git a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeQuerydslRepositoryImpl.java b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeQuerydslRepositoryImpl.java
index 6c4cc6a70..a674b75ae 100644
--- a/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeQuerydslRepositoryImpl.java
+++ b/core/infra/database/mysql/product-mysql/src/main/java/com/loopers/core/infra/database/mysql/productlike/ProductLikeQuerydslRepositoryImpl.java
@@ -15,7 +15,9 @@
 import org.springframework.stereotype.Component;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Stream;
 
 import static com.loopers.core.infra.database.mysql.product.entity.QProductEntity.productEntity;
 import static com.loopers.core.infra.database.mysql.productlike.entity.QProductLikeEntity.productLikeEntity;
@@ -56,9 +58,12 @@ public Page<LikeProductListProjection> findLikeProductListWithCondition(
                 .offset(pageable.getOffset())
                 .limit(pageable.getPageSize())
                 .orderBy(
-                        orderByCreatedAt(createdAtSort),
-                        orderByPrice(priceSort),
-                        orderByLikeCount(likeCountSort)
+                        Stream.of(
+                                        orderByCreatedAt(createdAtSort),
+                                        orderByPrice(priceSort),
+                                        orderByLikeCount(likeCountSort)
+                                ).filter(Objects::nonNull)
+                                .toArray(OrderSpecifier[]::new)
                 ).fetch();
 
         JPAQuery<Long> countQuery = queryFactory
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
index 6ae49b36a..616b79200 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
@@ -4,7 +4,9 @@
 import com.loopers.core.domain.common.type.OrderSort;
 import com.loopers.core.domain.productlike.LikeProductListView;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
-import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.productlike.query.GetLikeProductsListQuery;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -14,10 +16,13 @@
 public class ProductLikeQueryService {
 
     private final ProductLikeRepository repository;
+    private final UserRepository userRepository;
 
     public LikeProductListView getLikeProductsListView(GetLikeProductsListQuery query) {
+        User user = userRepository.getByIdentifier(new UserIdentifier(query.getUserIdentifier()));
+
         return repository.findLikeProductsListWithCondition(
-                new UserId(query.getUserId()),
+                user.getUserId(),
                 new BrandId(query.getBrandId()),
                 OrderSort.from(query.getCreatedAtSort()),
                 OrderSort.from(query.getPriceSort()),
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/query/GetLikeProductsListQuery.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/query/GetLikeProductsListQuery.java
index d001f62b9..c4ac9218a 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/query/GetLikeProductsListQuery.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/query/GetLikeProductsListQuery.java
@@ -5,7 +5,7 @@
 @Getter
 public class GetLikeProductsListQuery {
 
-    private final String userId;
+    private final String userIdentifier;
 
     private final String brandId;
 
@@ -20,7 +20,7 @@ public class GetLikeProductsListQuery {
     private final int PageSize;
 
     public GetLikeProductsListQuery(
-            String userId,
+            String userIdentifier,
             String brandId,
             String createdAtSort,
             String priceSort,
@@ -28,7 +28,7 @@ public GetLikeProductsListQuery(
             int pageNo,
             int pageSize
     ) {
-        this.userId = userId;
+        this.userIdentifier = userIdentifier;
         this.brandId = brandId;
         this.createdAtSort = createdAtSort;
         this.priceSort = priceSort;

From f368a1b34da4c4978ed74b0852d15d7af13760dd Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 00:36:38 +0900
Subject: [PATCH 11/29] =?UTF-8?q?feature=20:=20=EC=A3=BC=EB=AC=B8=20API=20?=
 =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts            |   1 +
 .../application/api/order/OrderV1Api.java     |  26 ++
 .../application/api/order/OrderV1ApiSpec.java |  18 ++
 .../application/api/order/OrderV1Dto.java     |  37 +++
 .../api/order/OrderV1ApiIntegrationTest.java  | 274 ++++++++++++++++++
 .../product/ProductV1ApiIntegrationTest.java  |  10 +-
 .../ProductLikeV1ApiIntegrationTest.java      |  13 +-
 .../loopers/core/domain/product/Product.java  |   5 +-
 .../core/domain/product/vo/ProductStock.java  |   4 -
 .../OrderQueryServiceIntegrationTest.java     |   7 +-
 .../product/ProductQueryServiceTest.java      |  22 +-
 .../ProductLikeQueryServiceTest.java          |  99 ++++---
 .../productlike/ProductLikeServiceTest.java   |  11 +-
 13 files changed, 451 insertions(+), 76 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 29cccb229..6854f99a5 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -8,6 +8,7 @@ dependencies {
 
     //service
     implementation(project(":core:service:user-service"))
+    implementation(project(":core:service:order-service"))
     implementation(project(":core:service:product-service"))
 
     //domain
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
new file mode 100644
index 000000000..a4e833b40
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
@@ -0,0 +1,26 @@
+package com.loopers.application.api.order;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.order.Order;
+import com.loopers.core.service.order.OrderService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/orders")
+@RequiredArgsConstructor
+public class OrderV1Api implements OrderV1ApiSpec {
+
+    private final OrderService orderService;
+
+    @Override
+    @PostMapping
+    public ApiResponse<OrderV1Dto.OrderResponse> order(
+            @RequestHeader(name = "X-USER-ID") String userIdentifier,
+            @RequestBody OrderV1Dto.OrderRequest request
+    ) {
+        Order savedOrder = orderService.order(request.toCommand(userIdentifier));
+
+        return ApiResponse.success(new OrderV1Dto.OrderResponse(savedOrder.getOrderId().value()));
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
new file mode 100644
index 000000000..a760408e8
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
@@ -0,0 +1,18 @@
+package com.loopers.application.api.order;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "Order V1 API", description = "주문 API 입니다.")
+public interface OrderV1ApiSpec {
+
+    @Operation(
+            summary = "주문 요청",
+            description = "상품 목록으로 주문을 요청합니다."
+    )
+    ApiResponse<OrderV1Dto.OrderResponse> order(
+            String userIdentifier,
+            OrderV1Dto.OrderRequest request
+    );
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
new file mode 100644
index 000000000..636f57a28
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
@@ -0,0 +1,37 @@
+package com.loopers.application.api.order;
+
+import com.loopers.core.service.order.command.OrderProductsCommand;
+
+import java.util.List;
+
+public class OrderV1Dto {
+
+    public record OrderRequest(
+            List<OrderItemRequest> items
+    ) {
+
+        public OrderProductsCommand toCommand(String userIdentifier) {
+            return new OrderProductsCommand(
+                    userIdentifier,
+                    this.items.stream()
+                            .map(OrderItemRequest::toCommand)
+                            .toList()
+            );
+        }
+
+        public record OrderItemRequest(
+                String productId,
+                Long quantity
+        ) {
+
+            public OrderProductsCommand.OrderItem toCommand() {
+                return new OrderProductsCommand.OrderItem(productId, quantity);
+            }
+        }
+    }
+
+    public record OrderResponse(
+            String orderId
+    ) {
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
new file mode 100644
index 000000000..ca984d23a
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
@@ -0,0 +1,274 @@
+package com.loopers.application.api.order;
+
+import com.loopers.application.api.ApiIntegrationTest;
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.brand.Brand;
+import com.loopers.core.domain.brand.repository.BrandRepository;
+import com.loopers.core.domain.brand.vo.BrandDescription;
+import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.product.Product;
+import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductName;
+import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.*;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static com.loopers.application.api.order.OrderV1Dto.OrderRequest;
+import static com.loopers.application.api.order.OrderV1Dto.OrderResponse;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
+
+class OrderV1ApiIntegrationTest extends ApiIntegrationTest {
+
+    @Autowired
+    UserRepository userRepository;
+
+    @Autowired
+    BrandRepository brandRepository;
+
+    @Autowired
+    ProductRepository productRepository;
+
+    @Autowired
+    UserPointRepository userPointRepository;
+
+    String userIdentifier;
+    String productId;
+    Product product;
+
+    @Nested
+    @DisplayName("주문 요청")
+    class 주문_요청 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userIdentifier = user.getIdentifier().value();
+                userPointRepository.save(
+                        Instancio.of(UserPoint.class)
+                                .set(field("id"), UserPointId.empty())
+                                .set(field("userId"), user.getUserId())
+                                .set(field("balance"), new UserPointBalance(new BigDecimal(100_000)))
+                                .create()
+                );
+
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                product = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
+                        )
+                );
+                productId = product.getId().value();
+            }
+
+            @Test
+            @DisplayName("주문을 생성하고 주문 ID를 반환한다.")
+            void 주문을_생성하고_주문_ID를_반환한다() {
+                // When
+                String endPoint = "/api/v1/orders";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userIdentifier);
+                headers.setContentType(MediaType.APPLICATION_JSON);
+
+                OrderRequest request = new OrderRequest(
+                        List.of(
+                                new OrderRequest.OrderItemRequest(productId, 2L)
+                        )
+                );
+
+                HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
+
+                ParameterizedTypeReference<ApiResponse<OrderResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<OrderResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+                assertThat(response.getBody().data()).isNotNull();
+                assertThat(response.getBody().data().orderId()).isNotBlank();
+            }
+        }
+
+        @Nested
+        @DisplayName("X-USER-ID 헤더가 없을 경우")
+        class X_USER_ID_헤더가_없을_경우 {
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                product = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
+                        )
+                );
+                productId = product.getId().value();
+            }
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/orders";
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+
+                OrderRequest request = new OrderRequest(
+                        List.of(
+                                new OrderRequest.OrderItemRequest(productId, 2L)
+                        )
+                );
+
+                HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 사용자로 요청할 경우")
+        class 존재하지_않는_사용자로_요청 {
+
+            @BeforeEach
+            void setUp() {
+                Brand brand = brandRepository.save(
+                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                );
+
+                product = productRepository.save(
+                        Product.create(
+                                brand.getId(),
+                                new ProductName("엔젤스 쉐어"),
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
+                        )
+                );
+                productId = product.getId().value();
+            }
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/orders";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", "nonExistentUser");
+                headers.setContentType(MediaType.APPLICATION_JSON);
+
+                OrderRequest request = new OrderRequest(
+                        List.of(
+                                new OrderRequest.OrderItemRequest(productId, 2L)
+                        )
+                );
+
+                HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 상품 ID로 요청할 경우")
+        class 존재하지_않는_상품_ID로_요청 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userIdentifier = user.getIdentifier().value();
+            }
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // When
+                String endPoint = "/api/v1/orders";
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", userIdentifier);
+                headers.setContentType(MediaType.APPLICATION_JSON);
+
+                OrderRequest request = new OrderRequest(
+                        List.of(
+                                new OrderRequest.OrderItemRequest("99999", 2L)
+                        )
+                );
+
+                HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
+
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
index 74a89e40d..c2658166a 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
@@ -11,6 +11,7 @@
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -57,7 +58,8 @@ void setUp() {
                         Product.create(
                                 new BrandId(brandId),
                                 new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00"))
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
                         )
                 );
 
@@ -65,7 +67,8 @@ void setUp() {
                         Product.create(
                                 new BrandId(brandId),
                                 new ProductName("라로스 에엑셀렌즈"),
-                                new ProductPrice(new BigDecimal("200.00"))
+                                new ProductPrice(new BigDecimal("200.00")),
+                                new ProductStock(10L)
                         )
                 );
             }
@@ -117,7 +120,8 @@ void setUp() {
                         Product.create(
                                 brand.getId(),
                                 new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00"))
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
                         )
                 ).getId().value();
             }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
index 0581bf03c..fbc9a62e6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
@@ -11,6 +11,7 @@
 import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
@@ -72,7 +73,8 @@ void setUp() {
                         Product.create(
                                 brand.getId(),
                                 new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00"))
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
                         )
                 );
                 productId = product.getId().value();
@@ -218,7 +220,8 @@ void setUp() {
                         Product.create(
                                 brand.getId(),
                                 new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00"))
+                                new ProductPrice(new BigDecimal("150.00")),
+                                new ProductStock(10L)
                         )
                 ).getId().value();
 
@@ -374,14 +377,16 @@ void setUp() {
                     Product.create(
                             brand.getId(),
                             new ProductName("엔젤스 쉐어"),
-                            new ProductPrice(new BigDecimal("150.00"))
+                            new ProductPrice(new BigDecimal("150.00")),
+                            new ProductStock(10L)
                     )
             );
             Product product2 = productRepository.save(
                     Product.create(
                             brand.getId(),
                             new ProductName("라로스 에센스"),
-                            new ProductPrice(new BigDecimal("200.00"))
+                            new ProductPrice(new BigDecimal("200.00")),
+                            new ProductStock(10L)
                     )
             );
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/product/Product.java b/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
index d64b2a5ee..144157dc1 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/product/Product.java
@@ -59,14 +59,15 @@ private Product(
     public static Product create(
             BrandId brandId,
             ProductName name,
-            ProductPrice price
+            ProductPrice price,
+            ProductStock productStock
     ) {
         return new Product(
                 ProductId.empty(),
                 brandId,
                 name,
                 price,
-                ProductStock.init(),
+                productStock,
                 ProductLikeCount.init(),
                 CreatedAt.now(),
                 UpdatedAt.now(),
diff --git a/core/domain/src/main/java/com/loopers/core/domain/product/vo/ProductStock.java b/core/domain/src/main/java/com/loopers/core/domain/product/vo/ProductStock.java
index 8411a643b..0d1515ca6 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/product/vo/ProductStock.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/product/vo/ProductStock.java
@@ -16,10 +16,6 @@ public record ProductStock(Long value) {
         validateNotNegative(value);
     }
 
-    public static ProductStock init() {
-        return new ProductStock(0L);
-    }
-
     private static void validateNotNull(Long value) {
         Objects.requireNonNull(value, DomainErrorCode.notNullMessage(FIELD_NAME));
     }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index 77a088e09..922195f5b 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -18,6 +18,7 @@
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
@@ -274,7 +275,8 @@ void setUp() {
                         Product.create(
                                 savedBrandId,
                                 new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000))
+                                new ProductPrice(new BigDecimal(1_300_000)),
+                                new ProductStock(100L)
                         )
                 );
 
@@ -282,7 +284,8 @@ void setUp() {
                         Product.create(
                                 savedBrandId,
                                 new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000))
+                                new ProductPrice(new BigDecimal(800_000)),
+                                new ProductStock(100L)
                         )
                 );
 
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
index 2bbaf99f8..85f7f6963 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
@@ -12,6 +12,7 @@
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.product.query.GetProductDetailQuery;
 import com.loopers.core.service.product.query.GetProductListQuery;
@@ -71,21 +72,24 @@ void setUp() {
                         Product.create(
                                 savedBrandId,
                                 new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000))
+                                new ProductPrice(new BigDecimal(1_300_000)),
+                                new ProductStock(100_000L)
                         )
                 );
                 productRepository.save(
                         Product.create(
                                 savedBrandId,
                                 new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000))
+                                new ProductPrice(new BigDecimal(800_000)),
+                                new ProductStock(100_000L)
                         )
                 );
                 productRepository.save(
                         Product.create(
                                 savedBrandId,
                                 new ProductName("iPhone 15"),
-                                new ProductPrice(new BigDecimal(1_500_000))
+                                new ProductPrice(new BigDecimal(1_500_000)),
+                                new ProductStock(100_000L)
                         )
                 );
             }
@@ -198,7 +202,8 @@ void setUp() {
                         Product.create(
                                 otherBrandId,
                                 new ProductName("Samsung Galaxy"),
-                                new ProductPrice(new BigDecimal(1_000_000))
+                                new ProductPrice(new BigDecimal(1_000_000)),
+                                new ProductStock(100_000L)
                         )
                 );
 
@@ -270,7 +275,8 @@ void setUp() {
                     Product.create(
                             savedBrandId,
                             new ProductName("MacBook Pro"),
-                            new ProductPrice(new BigDecimal(1_300_000))
+                            new ProductPrice(new BigDecimal(1_300_000)),
+                            new ProductStock(100_000L)
                     )
             );
             savedProductId = product.getId().value();
@@ -330,7 +336,8 @@ void setUp() {
                     Product.create(
                             savedBrandId,
                             new ProductName("MacBook Pro"),
-                            new ProductPrice(new BigDecimal(1_300_000))
+                            new ProductPrice(new BigDecimal(1_300_000)),
+                            new ProductStock(100_000L)
                     )
             );
             savedProductId = product.getId().value();
@@ -410,7 +417,8 @@ class 브랜드가_존재하지_않는_경우 {
                         Product.create(
                                 new BrandId("99999"),
                                 new ProductName("Orphan Product"),
-                                new ProductPrice(new BigDecimal(100_000))
+                                new ProductPrice(new BigDecimal(100_000)),
+                                new ProductStock(100_000L)
                         )
                 );
 
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
index 202034a83..a485ceab3 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
@@ -2,13 +2,12 @@
 
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
-import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandId;
-import com.loopers.core.domain.brand.vo.BrandName;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.domain.productlike.LikeProductListView;
 import com.loopers.core.domain.productlike.ProductLike;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
@@ -17,10 +16,10 @@
 import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
-import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.productlike.query.GetLikeProductsListQuery;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -32,6 +31,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 public class ProductLikeQueryServiceTest extends IntegrationTest {
 
@@ -54,32 +54,8 @@ public class ProductLikeQueryServiceTest extends IntegrationTest {
     @DisplayName("좋아요 상품 리스트 조회 시")
     class 좋아요_상품_리스트_조회_시 {
 
-        private String savedUserId;
+        private String savedUserIdentifier;
         private BrandId savedBrandId;
-        private BrandId otherBrandId;
-
-        @BeforeEach
-        void setUp() {
-            User user = userRepository.save(User.create(
-                    new UserIdentifier("testUser"),
-                    new UserEmail("test@loopers.com"),
-                    new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                    UserGender.MALE
-            ));
-            savedUserId = user.getUserId().value();
-
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("Apple"),
-                    new BrandDescription("Apple products")
-            ));
-            savedBrandId = brand.getId();
-
-            Brand otherBrand = brandRepository.save(Brand.create(
-                    new BrandName("Samsung"),
-                    new BrandDescription("Samsung products")
-            ));
-            otherBrandId = otherBrand.getId();
-        }
 
         @Nested
         @DisplayName("사용자가 좋아요한 상품이 존재하는 경우")
@@ -87,38 +63,54 @@ class 사용자가_좋아요한_상품이_존재하는_경우 {
 
             @BeforeEach
             void setUp() {
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("testUser"),
+                        new UserEmail("test@loopers.com"),
+                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
+                        UserGender.MALE
+                ));
+                savedUserIdentifier = user.getIdentifier().value();
+                savedBrandId = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field("id"), BrandId.empty())
+                                .create()
+                ).getId();
+
                 Product product1 = productRepository.save(
                         Product.create(
                                 savedBrandId,
                                 new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000))
+                                new ProductPrice(new BigDecimal(1_300_000)),
+                                new ProductStock(10L)
                         )
                 );
                 Product product2 = productRepository.save(
                         Product.create(
                                 savedBrandId,
                                 new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000))
+                                new ProductPrice(new BigDecimal(800_000)),
+                                new ProductStock(10L)
                         )
                 );
                 Product product3 = productRepository.save(
                         Product.create(
-                                otherBrandId,
+                                savedBrandId,
                                 new ProductName("Galaxy S24"),
-                                new ProductPrice(new BigDecimal(1_100_000))
+                                new ProductPrice(new BigDecimal(1_100_000)),
+                                new ProductStock(10L)
                         )
                 );
 
                 productLikeRepository.save(ProductLike.create(
-                        new UserId(savedUserId),
+                        user.getUserId(),
                         product1.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
-                        new UserId(savedUserId),
+                        user.getUserId(),
                         product2.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
-                        new UserId(savedUserId),
+                        user.getUserId(),
                         product3.getId()
                 ));
             }
@@ -127,8 +119,8 @@ void setUp() {
             @DisplayName("사용자의 좋아요 상품 리스트가 조회된다.")
             void 사용자의_좋아요_상품_리스트가_조회된다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
-                        savedBrandId.value(),
+                        savedUserIdentifier,
+                        null,
                         "ASC",
                         "ASC",
                         "ASC",
@@ -141,7 +133,7 @@ void setUp() {
                 assertSoftly(softly -> {
                     softly.assertThat(result).isNotNull();
                     softly.assertThat(result.getItems()).isNotEmpty();
-                    softly.assertThat(result.getTotalElements()).isEqualTo(2);
+                    softly.assertThat(result.getTotalElements()).isEqualTo(3);
                 });
             }
 
@@ -149,7 +141,7 @@ void setUp() {
             @DisplayName("브랜드별로 필터링된 좋아요 상품만 조회된다.")
             void 브랜드별로_필터링된_좋아요_상품만_조회된다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
+                        savedUserIdentifier,
                         savedBrandId.value(),
                         "ASC",
                         "ASC",
@@ -161,7 +153,7 @@ void setUp() {
                 LikeProductListView result = productLikeQueryService.getLikeProductsListView(query);
 
                 assertSoftly(softly -> {
-                    softly.assertThat(result.getTotalElements()).isEqualTo(2);
+                    softly.assertThat(result.getTotalElements()).isEqualTo(3);
                     softly.assertThat(result.getItems())
                             .allMatch(item -> item.getBrandId().value().equals(savedBrandId.value()),
                                     "모든 상품이 특정 브랜드에 속해야 함");
@@ -172,8 +164,8 @@ void setUp() {
             @DisplayName("생성일시 오름차순으로 정렬된다.")
             void 생성일시_오름차순으로_정렬된다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
-                        savedBrandId.value(),
+                        savedUserIdentifier,
+                        null,
                         "ASC",
                         "ASC",
                         "ASC",
@@ -194,8 +186,8 @@ void setUp() {
             @DisplayName("생성일시 내림차순으로 정렬된다.")
             void 생성일시_내림차순으로_정렬된다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
-                        savedBrandId.value(),
+                        savedUserIdentifier,
+                        null,
                         "DESC",
                         "ASC",
                         "ASC",
@@ -216,8 +208,8 @@ void setUp() {
             @DisplayName("가격을 함께 정렬할 때 유효하다.")
             void 가격을_함께_정렬할_때_유효하다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
-                        savedBrandId.value(),
+                        savedUserIdentifier,
+                        null,
                         "ASC",
                         "ASC",
                         "ASC",
@@ -236,12 +228,23 @@ void setUp() {
         @DisplayName("사용자가 좋아요한 상품이 없는 경우")
         class 사용자가_좋아요한_상품이_없는_경우 {
 
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(User.create(
+                        new UserIdentifier("testUser"),
+                        new UserEmail("test@loopers.com"),
+                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
+                        UserGender.MALE
+                ));
+                savedUserIdentifier = user.getIdentifier().value();
+            }
+
             @Test
             @DisplayName("빈 리스트가 반환된다.")
             void 빈_리스트가_반환된다() {
                 GetLikeProductsListQuery query = new GetLikeProductsListQuery(
-                        savedUserId,
-                        savedBrandId.value(),
+                        savedUserIdentifier,
+                        null,
                         "ASC",
                         "ASC",
                         "ASC",
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index b9dc3be5a..76628157a 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -8,10 +8,7 @@
 import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
-import com.loopers.core.domain.product.vo.ProductId;
-import com.loopers.core.domain.product.vo.ProductLikeCount;
-import com.loopers.core.domain.product.vo.ProductName;
-import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.*;
 import com.loopers.core.domain.productlike.ProductLike;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
 import com.loopers.core.domain.user.User;
@@ -75,7 +72,8 @@ void setUp() {
             Product product = productRepository.save(Product.create(
                     brandId,
                     new ProductName("MacBook Pro"),
-                    new ProductPrice(new BigDecimal("1500000"))
+                    new ProductPrice(new BigDecimal("1500000")),
+                    new ProductStock(100_000L)
             ));
             productId = product.getId().value();
 
@@ -239,7 +237,8 @@ void setUp() {
                 Product product = productRepository.save(Product.create(
                         brandId,
                         new ProductName("MacBook Pro"),
-                        new ProductPrice(new BigDecimal("1500000"))
+                        new ProductPrice(new BigDecimal("1500000")),
+                        new ProductStock(100_000L)
                 ));
                 productId = product.getId().value();
 

From 939f40e46914b7dece6f2487d7a81bc127c9bbcd Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 21:22:48 +0900
Subject: [PATCH 12/29] =?UTF-8?q?feature=20:=20=EC=A3=BC=EB=AC=B8=20?=
 =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B0=9C?=
 =?UTF-8?q?=EB=B0=9C=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/order/OrderV1Api.java     | 25 +++++-
 .../application/api/order/OrderV1ApiSpec.java | 17 +++-
 .../application/api/order/OrderV1Dto.java     | 34 +++++++
 .../api/productlike/ProductLikeV1Api.java     |  2 +-
 .../application/api/user/UserV1Dto.java       |  4 +-
 .../api/order/OrderV1ApiIntegrationTest.java  | 88 ++++++++++++++++++-
 .../ProductLikeV1ApiIntegrationTest.java      |  2 +-
 .../com/loopers/core/domain/order/Order.java  | 10 +--
 .../com/loopers/core/domain/user/User.java    |  6 +-
 .../mysql/order/entity/OrderEntity.java       |  2 +-
 .../mysql/user/entity/UserEntity.java         |  2 +-
 .../core/service/order/OrderQueryService.java |  4 +-
 .../core/service/order/OrderService.java      |  4 +-
 .../service/order/component/OrderCashier.java |  4 +-
 .../order/OrderCashierIntegrationTest.java    | 18 ++--
 .../OrderQueryServiceIntegrationTest.java     | 10 +--
 .../order/component/OrderCashierTest.java     | 18 ++--
 .../productlike/ProductLikeQueryService.java  |  2 +-
 .../productlike/ProductLikeService.java       |  8 +-
 .../ProductLikeQueryServiceTest.java          |  6 +-
 .../productlike/ProductLikeServiceTest.java   |  4 +-
 .../core/service/user/JoinUserService.java    |  2 +-
 .../service/user/UserPointQueryService.java   |  4 +-
 .../core/service/user/UserPointService.java   |  2 +-
 .../user/JoinUserServiceIntegrationTest.java  |  2 +-
 .../UserPointQueryServiceIntegrationTest.java |  4 +-
 .../user/UserPointServiceIntegrationTest.java |  8 +-
 27 files changed, 220 insertions(+), 72 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
index a4e833b40..ae84185af 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
@@ -2,25 +2,44 @@
 
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.OrderListView;
+import com.loopers.core.service.order.OrderQueryService;
 import com.loopers.core.service.order.OrderService;
+import com.loopers.core.service.order.query.GetOrderListQuery;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
+import static com.loopers.application.api.order.OrderV1Dto.*;
+
 @RestController
 @RequestMapping("/api/v1/orders")
 @RequiredArgsConstructor
 public class OrderV1Api implements OrderV1ApiSpec {
 
     private final OrderService orderService;
+    private final OrderQueryService queryService;
 
     @Override
     @PostMapping
-    public ApiResponse<OrderV1Dto.OrderResponse> order(
+    public ApiResponse<OrderResponse> order(
             @RequestHeader(name = "X-USER-ID") String userIdentifier,
-            @RequestBody OrderV1Dto.OrderRequest request
+            @RequestBody OrderRequest request
     ) {
         Order savedOrder = orderService.order(request.toCommand(userIdentifier));
 
-        return ApiResponse.success(new OrderV1Dto.OrderResponse(savedOrder.getOrderId().value()));
+        return ApiResponse.success(new OrderResponse(savedOrder.getId().value()));
+    }
+
+    @Override
+    @GetMapping
+    public ApiResponse<OrderListResponse> getOrderList(
+            @RequestHeader(name = "X-USER-ID") String userIdentifier,
+            String createdAtSort,
+            int pageNo,
+            int pageSize
+    ) {
+        OrderListView view = queryService.getOrderListWithCondition(new GetOrderListQuery(userIdentifier, createdAtSort, pageNo, pageSize));
+
+        return ApiResponse.success(OrderListResponse.from(view));
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
index a760408e8..238d8cc74 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
@@ -4,6 +4,8 @@
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
+import static com.loopers.application.api.order.OrderV1Dto.*;
+
 @Tag(name = "Order V1 API", description = "주문 API 입니다.")
 public interface OrderV1ApiSpec {
 
@@ -11,8 +13,19 @@ public interface OrderV1ApiSpec {
             summary = "주문 요청",
             description = "상품 목록으로 주문을 요청합니다."
     )
-    ApiResponse<OrderV1Dto.OrderResponse> order(
+    ApiResponse<OrderResponse> order(
+            String userIdentifier,
+            OrderRequest request
+    );
+
+    @Operation(
+            summary = "주문 목록 조회",
+            description = "주문의 목록을 조회합니다."
+    )
+    ApiResponse<OrderListResponse> getOrderList(
             String userIdentifier,
-            OrderV1Dto.OrderRequest request
+            String createdAtSort,
+            int pageNo,
+            int pageSize
     );
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
index 636f57a28..46ed731db 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
@@ -1,5 +1,7 @@
 package com.loopers.application.api.order;
 
+import com.loopers.core.domain.order.OrderListItem;
+import com.loopers.core.domain.order.OrderListView;
 import com.loopers.core.service.order.command.OrderProductsCommand;
 
 import java.util.List;
@@ -34,4 +36,36 @@ public record OrderResponse(
             String orderId
     ) {
     }
+
+    public record OrderListResponse(
+            List<OrderListItemResponse> items,
+            long totalElements,
+            int totalPages,
+            boolean hasNext,
+            boolean hasPrevious
+    ) {
+        public static OrderListResponse from(OrderListView view) {
+            return new OrderListResponse(
+                    view.getItems().stream()
+                            .map(OrderListItemResponse::from)
+                            .toList(),
+                    view.getTotalElements(),
+                    view.getTotalPages(),
+                    view.isHasNext(),
+                    view.isHasPrevious()
+            );
+        }
+
+        public record OrderListItemResponse(
+                String orderId,
+                String userId
+        ) {
+            public static OrderListItemResponse from(OrderListItem item) {
+                return new OrderListItemResponse(
+                        item.getOrderId().value(),
+                        item.getUserId().value()
+                );
+            }
+        }
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
index 4b355df6d..8e55ae350 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/productlike/ProductLikeV1Api.java
@@ -41,7 +41,7 @@ public ApiResponse<Void> unlikeProduct(
     }
 
     @Override
-    @GetMapping("/")
+    @GetMapping
     public ApiResponse<LikeProductsResponse> getLikeProducts(
             @RequestHeader(name = "X-USER-ID") String userId,
             @RequestParam String brandId,
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index edbf2c9bf..50b22c5c4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -33,7 +33,7 @@ public record GetUserResponse(
 
         public static GetUserResponse from(User user) {
             return new GetUserResponse(
-                    user.getUserId().value(),
+                    user.getId().value(),
                     user.getIdentifier().value(),
                     user.getEmail().value(),
                     user.getBirthDay().value().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
@@ -52,7 +52,7 @@ public record JoinUserResponse(
 
         public static JoinUserResponse from(User user) {
             return new JoinUserResponse(
-                    user.getUserId().value(),
+                    user.getId().value(),
                     user.getIdentifier().value(),
                     user.getEmail().value(),
                     user.getBirthDay().value().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
index ca984d23a..53f69c769 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
@@ -6,6 +6,9 @@
 import com.loopers.core.domain.brand.repository.BrandRepository;
 import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.repository.OrderRepository;
+import com.loopers.core.domain.order.vo.OrderId;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductName;
@@ -29,8 +32,7 @@
 import java.math.BigDecimal;
 import java.util.List;
 
-import static com.loopers.application.api.order.OrderV1Dto.OrderRequest;
-import static com.loopers.application.api.order.OrderV1Dto.OrderResponse;
+import static com.loopers.application.api.order.OrderV1Dto.*;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.instancio.Select.field;
 
@@ -48,6 +50,9 @@ class OrderV1ApiIntegrationTest extends ApiIntegrationTest {
     @Autowired
     UserPointRepository userPointRepository;
 
+    @Autowired
+    OrderRepository orderRepository;
+
     String userIdentifier;
     String productId;
     Product product;
@@ -74,7 +79,7 @@ void setUp() {
                 userPointRepository.save(
                         Instancio.of(UserPoint.class)
                                 .set(field("id"), UserPointId.empty())
-                                .set(field("userId"), user.getUserId())
+                                .set(field("userId"), user.getId())
                                 .set(field("balance"), new UserPointBalance(new BigDecimal(100_000)))
                                 .create()
                 );
@@ -271,4 +276,81 @@ void setUp() {
             }
         }
     }
+
+    @Nested
+    @DisplayName("주문 목록 조회")
+    class 주문_목록_조회 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
+                );
+
+                userIdentifier = user.getIdentifier().value();
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), user.getId())
+                                .create()
+                );
+
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), user.getId())
+                                .create()
+                );
+            }
+
+            @Test
+            @DisplayName("Status 200")
+            void Status200() {
+                String endPoint = "/api/v1/orders?createdAtSort=DESC&pageNo=0&pageSize=10";
+                ParameterizedTypeReference<ApiResponse<OrderListResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", "kilian");
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ResponseEntity<ApiResponse<OrderListResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
+
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+            }
+        }
+
+        @Nested
+        @DisplayName("사용자가 존재하지 않는 경우")
+        class 사용자가_존재하지_않는_경우 {
+
+            @Test
+            @DisplayName("Status 404")
+            void Status404() {
+                String endPoint = "/api/v1/orders?createdAtSort=DESC&pageNo=0&pageSize=10";
+                ParameterizedTypeReference<ApiResponse<OrderListResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("X-USER-ID", "kilian");
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+
+                ResponseEntity<ApiResponse<OrderListResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
+
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+
+    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
index fbc9a62e6..03363bcba 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
@@ -413,7 +413,7 @@ class 정상_요청인_경우 {
             @DisplayName("사용자가 좋아요한 상품 목록을 조회한다.")
             void 사용자가_좋아요한_상품_목록을_조회한다() {
                 // When
-                String endPoint = "/api/v1/like/products/?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
+                String endPoint = "/api/v1/like/products?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
                 HttpHeaders headers = new HttpHeaders();
                 headers.set("X-USER-ID", userId);
                 HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/Order.java b/core/domain/src/main/java/com/loopers/core/domain/order/Order.java
index a6f8f4c12..4d7a9ace0 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/Order.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/Order.java
@@ -10,7 +10,7 @@
 @Getter
 public class Order {
 
-    private final OrderId orderId;
+    private final OrderId id;
 
     private final UserId userId;
 
@@ -21,13 +21,13 @@ public class Order {
     private final DeletedAt deletedAt;
 
     private Order(
-            OrderId orderId,
+            OrderId id,
             UserId userId,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        this.orderId = orderId;
+        this.id = id;
         this.userId = userId;
         this.createdAt = createdAt;
         this.updatedAt = updatedAt;
@@ -35,13 +35,13 @@ private Order(
     }
 
     public static Order mappedBy(
-            OrderId orderId,
+            OrderId id,
             UserId userId,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        return new Order(orderId, userId, createdAt, updatedAt, deletedAt);
+        return new Order(id, userId, createdAt, updatedAt, deletedAt);
     }
 
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index a4d68019d..b6236c8f1 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -13,7 +13,7 @@
 @Getter
 public class User {
 
-    private final UserId userId;
+    private final UserId id;
 
     private final UserIdentifier identifier;
 
@@ -30,7 +30,7 @@ public class User {
     private final DeletedAt deletedAt;
 
     private User(
-            UserId userId,
+            UserId id,
             UserIdentifier identifier,
             UserEmail email,
             UserBirthDay birthDay,
@@ -39,7 +39,7 @@ private User(
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        this.userId = userId;
+        this.id = id;
         this.identifier = identifier;
         this.email = email;
         this.birthDay = birthDay;
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/order/entity/OrderEntity.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/order/entity/OrderEntity.java
index 3c39f70c5..f5e5e2127 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/order/entity/OrderEntity.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/order/entity/OrderEntity.java
@@ -44,7 +44,7 @@ public class OrderEntity {
 
     public static OrderEntity from(Order order) {
         return new OrderEntity(
-                Optional.ofNullable(order.getOrderId().value())
+                Optional.ofNullable(order.getId().value())
                         .map(Long::parseLong)
                         .orElse(null),
                 Long.parseLong(Objects.requireNonNull(order.getUserId().value())),
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
index 12cc8a8bc..ab9c4702f 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
@@ -50,7 +50,7 @@ public class UserEntity {
 
     public static UserEntity from(User user) {
         return new UserEntity(
-                Optional.ofNullable(user.getUserId().value())
+                Optional.ofNullable(user.getId().value())
                         .map(Long::parseLong)
                         .orElse(null),
                 user.getIdentifier().value(),
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderQueryService.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderQueryService.java
index 7c281fff5..5788edfb3 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderQueryService.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderQueryService.java
@@ -30,7 +30,7 @@ public OrderListView getOrderListWithCondition(GetOrderListQuery query) {
         User user = userRepository.getByIdentifier(new UserIdentifier(query.getUserIdentifier()));
 
         return orderRepository.findListWithCondition(
-                user.getUserId(),
+                user.getId(),
                 OrderSort.from(query.getCreatedAtSort()),
                 query.getPageNo(),
                 query.getPageSize()
@@ -39,7 +39,7 @@ public OrderListView getOrderListWithCondition(GetOrderListQuery query) {
 
     public OrderDetail getOrderDetail(GetOrderDetailQuery query) {
         Order order = orderRepository.getById(new OrderId(query.getOrderId()));
-        List<OrderItem> orderItems = orderItemRepository.findAllByOrderId(order.getOrderId());
+        List<OrderItem> orderItems = orderItemRepository.findAllByOrderId(order.getId());
 
         return new OrderDetail(order, orderItems);
     }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
index 6b4e8d77e..0e2de20ac 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
@@ -30,10 +30,10 @@ public class OrderService {
     @Transactional
     public Order order(OrderProductsCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
-        Order savedOrder = orderRepository.save(Order.create(user.getUserId()));
+        Order savedOrder = orderRepository.save(Order.create(user.getId()));
         List<OrderItem> orderItems = command.getOrderItems().stream()
                 .map(productCommand -> OrderItem.create(
-                                savedOrder.getOrderId(),
+                                savedOrder.getId(),
                                 new ProductId(productCommand.getProductId()),
                                 new Quantity(productCommand.getQuantity())
                         )
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
index 81132b895..354a97259 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
@@ -20,10 +20,10 @@ public class OrderCashier {
 
     @Transactional
     public Payment checkout(User user, Order order, PayAmount payAmount) {
-        UserPoint userPoint = userPointRepository.getByUserIdWithLock(user.getUserId());
+        UserPoint userPoint = userPointRepository.getByUserIdWithLock(user.getId());
         userPointRepository.save(userPoint.pay(payAmount));
 
-        Payment payment = Payment.create(order.getOrderId(), user.getUserId(), payAmount);
+        Payment payment = Payment.create(order.getId(), user.getId(), payAmount);
         paymentRepository.save(payment);
 
         return payment;
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
index ca502b1b1..44fc32d90 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
@@ -72,7 +72,7 @@ void setUp() {
                 // 사용자 포인트 생성 및 저장 (50,000 포인트)
                 UserPoint userPoint = UserPoint.mappedBy(
                         UserPointId.empty(),
-                        user.getUserId(),
+                        user.getId(),
                         new UserPointBalance(new BigDecimal("50000")),
                         CreatedAt.now(),
                         UpdatedAt.now()
@@ -80,7 +80,7 @@ void setUp() {
                 userPointRepository.save(userPoint);
 
                 // 주문 생성 및 저장
-                order = Order.create(user.getUserId());
+                order = Order.create(user.getId());
                 order = orderRepository.save(order);
 
                 // 결제 금액 (20,000 - 포인트는 50,000으로 충분함)
@@ -100,17 +100,17 @@ void setUp() {
                             .isNotNull();
                     softly.assertThat(result.getOrderId())
                             .as("주문 ID 일치")
-                            .isEqualTo(order.getOrderId());
+                            .isEqualTo(order.getId());
                     softly.assertThat(result.getUserId())
                             .as("사용자 ID 일치")
-                            .isEqualTo(user.getUserId());
+                            .isEqualTo(user.getId());
                     softly.assertThat(result.getAmount().value())
                             .as("결제 금액")
                             .isEqualByComparingTo(new BigDecimal("20000"));
                 });
 
                 // 포인트 차감 확인
-                UserPoint deductedUserPoint = userPointRepository.getByUserId(user.getUserId());
+                UserPoint deductedUserPoint = userPointRepository.getByUserId(user.getId());
                 assertSoftly(softly -> {
                     softly.assertThat(deductedUserPoint.getBalance().value())
                             .as("차감 후 포인트 잔액")
@@ -128,7 +128,7 @@ void setUp() {
                 orderCashier.checkout(user, order, exactPayAmount);
 
                 // then
-                UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+                UserPoint userPoint = userPointRepository.getByUserId(user.getId());
                 assertSoftly(softly -> {
                     softly.assertThat(userPoint.getBalance().value())
                             .as("차감 후 포인트 잔액")
@@ -146,7 +146,7 @@ void setUp() {
                         index -> orderCashier.checkout(user, order, payAmountPerUser)
                 );
 
-                UserPoint actualUserPoint = userPointRepository.getByUserId(user.getUserId());
+                UserPoint actualUserPoint = userPointRepository.getByUserId(user.getId());
                 assertSoftly(softly -> {
                     softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);
                     softly.assertThat(actualUserPoint.getBalance().value()).isEqualByComparingTo(new BigDecimal(0));
@@ -175,7 +175,7 @@ void setUp() {
                 // 사용자 포인트 생성 및 저장 (50,000 포인트)
                 UserPoint userPoint = UserPoint.mappedBy(
                         UserPointId.empty(),
-                        user.getUserId(),
+                        user.getId(),
                         new UserPointBalance(new BigDecimal("50000")),
                         CreatedAt.now(),
                         UpdatedAt.now()
@@ -183,7 +183,7 @@ void setUp() {
                 userPointRepository.save(userPoint);
 
                 // 주문 생성 및 저장
-                order = Order.create(user.getUserId());
+                order = Order.create(user.getId());
                 order = orderRepository.save(order);
             }
 
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index 922195f5b..98cc4d07f 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -78,7 +78,7 @@ void setUp() {
                     UserGender.create("MALE")
             ));
             savedUserIdentifier = user.getIdentifier().value();
-            savedUserId = user.getUserId();
+            savedUserId = user.getId();
 
             User otherUser = userRepository.save(User.create(
                     UserIdentifier.create("other"),
@@ -86,7 +86,7 @@ void setUp() {
                     UserBirthDay.create("1991-02-02"),
                     UserGender.create("FEMALE")
             ));
-            otherUserId = otherUser.getUserId();
+            otherUserId = otherUser.getId();
         }
 
         @Nested
@@ -250,7 +250,7 @@ void setUp() {
                     UserBirthDay.create("1990-01-01"),
                     UserGender.create("MALE")
             ));
-            savedUserId = user.getUserId();
+            savedUserId = user.getId();
 
             User otherUser = userRepository.save(User.create(
                     UserIdentifier.create("other"),
@@ -260,9 +260,9 @@ void setUp() {
             ));
 
             Order order = orderRepository.save(Order.create(savedUserId));
-            savedOrderId = order.getOrderId().value();
+            savedOrderId = order.getId().value();
 
-            Order otherOrder = orderRepository.save(Order.create(otherUser.getUserId()));
+            Order otherOrder = orderRepository.save(Order.create(otherUser.getId()));
         }
 
         @Nested
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
index 3f93b1a18..e3cb10bbb 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
@@ -57,7 +57,7 @@ void setUp() {
                     UserGender.MALE
             );
 
-            order = Order.create(user.getUserId());
+            order = Order.create(user.getId());
         }
 
         @Nested
@@ -71,7 +71,7 @@ class WhenSufficientPoint {
             void setUp() {
                 userPoint = UserPoint.mappedBy(
                         UserPointId.empty(),
-                        user.getUserId(),
+                        user.getId(),
                         new UserPointBalance(new BigDecimal("50000")),
                         null,
                         null
@@ -79,7 +79,7 @@ void setUp() {
 
                 payAmount = new PayAmount(new BigDecimal("20000"));
 
-                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+                when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
             }
 
             @Test
@@ -90,8 +90,8 @@ void checkoutSuccessAndSavePayment() {
 
                 // then
                 assertThat(result).isNotNull();
-                assertThat(result.getOrderId()).isEqualTo(order.getOrderId());
-                assertThat(result.getUserId()).isEqualTo(user.getUserId());
+                assertThat(result.getOrderId()).isEqualTo(order.getId());
+                assertThat(result.getUserId()).isEqualTo(user.getId());
                 assertThat(result.getAmount()).isEqualTo(payAmount);
 
                 // 포인트 차감 검증
@@ -116,7 +116,7 @@ class WhenInsufficientPoint {
             void setUp() {
                 userPoint = UserPoint.mappedBy(
                         UserPointId.empty(),
-                        user.getUserId(),
+                        user.getId(),
                         new UserPointBalance(new BigDecimal("50000")),
                         null,
                         null
@@ -124,7 +124,7 @@ void setUp() {
 
                 largePayAmount = new PayAmount(new BigDecimal("100000"));
 
-                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+                when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
             }
 
             @Test
@@ -148,7 +148,7 @@ class WhenExactPoint {
             void setUp() {
                 userPoint = UserPoint.mappedBy(
                         UserPointId.empty(),
-                        user.getUserId(),
+                        user.getId(),
                         new UserPointBalance(new BigDecimal("50000")),
                         null,
                         null
@@ -156,7 +156,7 @@ void setUp() {
 
                 exactPayAmount = new PayAmount(new BigDecimal("50000"));
 
-                when(userPointRepository.getByUserIdWithLock(user.getUserId())).thenReturn(userPoint);
+                when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
             }
 
             @Test
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
index 616b79200..2c7a32bfb 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeQueryService.java
@@ -22,7 +22,7 @@ public LikeProductListView getLikeProductsListView(GetLikeProductsListQuery quer
         User user = userRepository.getByIdentifier(new UserIdentifier(query.getUserIdentifier()));
 
         return repository.findLikeProductsListWithCondition(
-                user.getUserId(),
+                user.getId(),
                 new BrandId(query.getBrandId()),
                 OrderSort.from(query.getCreatedAtSort()),
                 OrderSort.from(query.getPriceSort()),
diff --git a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
index e2e22fa29..81cecb85d 100644
--- a/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
+++ b/core/service/product-service/src/main/java/com/loopers/core/service/productlike/ProductLikeService.java
@@ -27,11 +27,11 @@ public void like(ProductLikeCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
         Product product = productRepository.getByIdWithLock(new ProductId(command.getProductId()));
 
-        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getId())
+        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getId(), product.getId())
                 .isPresent();
 
         if (!isAlreadyLiked) {
-            productLikeRepository.save(ProductLike.create(user.getUserId(), product.getId()));
+            productLikeRepository.save(ProductLike.create(user.getId(), product.getId()));
             productRepository.save(product.increaseLikeCount());
         }
     }
@@ -41,11 +41,11 @@ public void unlike(ProductUnlikeCommand command) {
         User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
         Product product = productRepository.getByIdWithLock(new ProductId(command.getProductId()));
 
-        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getUserId(), product.getId())
+        boolean isAlreadyLiked = productLikeRepository.findByUserIdAndProductIdWithLock(user.getId(), product.getId())
                 .isPresent();
 
         if (isAlreadyLiked) {
-            productLikeRepository.deleteByUserIdAndProductId(user.getUserId(), product.getId());
+            productLikeRepository.deleteByUserIdAndProductId(user.getId(), product.getId());
             productRepository.save(product.decreaseLikeCount());
         }
     }
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
index a485ceab3..f0d721aa1 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
@@ -102,15 +102,15 @@ void setUp() {
                 );
 
                 productLikeRepository.save(ProductLike.create(
-                        user.getUserId(),
+                        user.getId(),
                         product1.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
-                        user.getUserId(),
+                        user.getId(),
                         product2.getId()
                 ));
                 productLikeRepository.save(ProductLike.create(
-                        user.getUserId(),
+                        user.getId(),
                         product3.getId()
                 ));
             }
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index 76628157a..9f44b92e4 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -301,7 +301,7 @@ void setUp() {
 
                 productLikeRepository.save(
                         ProductLike.create(
-                                user.getUserId(),
+                                user.getId(),
                                 product.getId()
                         )
                 );
@@ -361,7 +361,7 @@ void setUp() {
                             ));
                             productLikeRepository.save(
                                     ProductLike.create(
-                                            user.getUserId(),
+                                            user.getId(),
                                             new ProductId(productId)
                                     )
                             );
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index 5bbb9052c..af58facd1 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -36,7 +36,7 @@ public User joinUser(JoinUserCommand command) {
         UserGender userGender = UserGender.create(command.getGender());
 
         User savedUser = userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
-        userPointRepository.save(UserPoint.create(savedUser.getUserId()));
+        userPointRepository.save(UserPoint.create(savedUser.getId()));
 
         return savedUser;
     }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
index da9b419b8..b79976fa9 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
@@ -20,8 +20,8 @@ public class UserPointQueryService {
     public UserPoint getByUserIdentifier(GetUserPointQuery query) {
         try {
             User user = userRepository.getByIdentifier(new UserIdentifier(query.getUserIdentifier()));
-            
-            return userPointRepository.getByUserId(user.getUserId());
+
+            return userPointRepository.getByUserId(user.getId());
         } catch (NotFoundException exception) {
             return null;
         }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
index 9208cd847..af60af880 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
@@ -20,7 +20,7 @@ public class UserPointService {
     @Transactional
     public UserPoint charge(UserPointChargeCommand command) {
         User user = userRepository.getByIdentifierWithLock(new UserIdentifier(command.getUserIdentifier()));
-        UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+        UserPoint userPoint = userPointRepository.getByUserId(user.getId());
 
         return userPointRepository.save(userPoint.charge(command.getPoint()));
     }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
index 248e95e94..db6d8e2ea 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -45,7 +45,7 @@ class 유효한_경우 {
                 );
 
                 User user = joinUserService.joinUser(joinUserCommand);
-                Optional<User> findUser = userRepository.findById(user.getUserId());
+                Optional<User> findUser = userRepository.findById(user.getId());
 
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(findUser.isPresent()).isTrue();
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
index d19a64945..bbb01bdca 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
@@ -64,7 +64,7 @@ void setUp() {
                         )
                 );
 
-                savedUserPoint = userPointRepository.save(UserPoint.create(savedUser.getUserId()));
+                savedUserPoint = userPointRepository.save(UserPoint.create(savedUser.getId()));
             }
 
             @Test
@@ -74,7 +74,7 @@ void setUp() {
 
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(userPoint).isNotNull();
-                    softly.assertThat(userPoint.getUserId().value()).isEqualTo(savedUser.getUserId().value());
+                    softly.assertThat(userPoint.getUserId().value()).isEqualTo(savedUser.getId().value());
                     softly.assertThat(userPoint.getBalance().value().intValue()).isEqualTo(0);
                 });
             }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
index addb8e3eb..3a3d3f973 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -57,7 +57,7 @@ void setUp() {
                                 UserGender.create("MALE")
                         )
                 );
-                userPointRepository.save(UserPoint.create(user.getUserId()));
+                userPointRepository.save(UserPoint.create(user.getId()));
             }
 
             @Test
@@ -68,7 +68,7 @@ void setUp() {
 
                 // when
                 service.charge(command);
-                UserPoint chargedPoint = userPointRepository.getByUserId(user.getUserId());
+                UserPoint chargedPoint = userPointRepository.getByUserId(user.getId());
 
                 // then
                 assertSoftly(softly -> {
@@ -114,7 +114,7 @@ void setUp() {
                                     UserGender.create("MALE")
                             )
                     );
-                    userPointRepository.save(UserPoint.create(user.getUserId()));
+                    userPointRepository.save(UserPoint.create(user.getId()));
                 }
 
                 @Test
@@ -132,7 +132,7 @@ void setUp() {
                     );
 
                     // Then
-                    UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+                    UserPoint userPoint = userPointRepository.getByUserId(user.getId());
 
                     assertSoftly(softly -> {
                         softly.assertThat(results).as("동시 요청 결과 수").hasSize(requestCount);

From 36c92832cb1473ab60f9589ad7a51b7764534a07 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 21:42:30 +0900
Subject: [PATCH 13/29] =?UTF-8?q?feature=20:=20=EC=A3=BC=EB=AC=B8=20?=
 =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/order/OrderV1Api.java     | 12 +++
 .../application/api/order/OrderV1ApiSpec.java | 11 ++-
 .../application/api/order/OrderV1Dto.java     | 31 ++++++++
 .../api/order/OrderV1ApiIntegrationTest.java  | 74 +++++++++++++++++++
 4 files changed, 124 insertions(+), 4 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
index ae84185af..214ad23ca 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Api.java
@@ -2,9 +2,11 @@
 
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.OrderDetail;
 import com.loopers.core.domain.order.OrderListView;
 import com.loopers.core.service.order.OrderQueryService;
 import com.loopers.core.service.order.OrderService;
+import com.loopers.core.service.order.query.GetOrderDetailQuery;
 import com.loopers.core.service.order.query.GetOrderListQuery;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
@@ -42,4 +44,14 @@ public ApiResponse<OrderListResponse> getOrderList(
 
         return ApiResponse.success(OrderListResponse.from(view));
     }
+
+    @Override
+    @GetMapping("/{orderId}")
+    public ApiResponse<OrderDetailResponse> getOrderDetail(
+            @PathVariable String orderId
+    ) {
+        OrderDetail orderDetail = queryService.getOrderDetail(new GetOrderDetailQuery(orderId));
+        
+        return ApiResponse.success(OrderDetailResponse.from(orderDetail));
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
index 238d8cc74..526c94a5a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1ApiSpec.java
@@ -13,10 +13,7 @@ public interface OrderV1ApiSpec {
             summary = "주문 요청",
             description = "상품 목록으로 주문을 요청합니다."
     )
-    ApiResponse<OrderResponse> order(
-            String userIdentifier,
-            OrderRequest request
-    );
+    ApiResponse<OrderResponse> order(String userIdentifier, OrderRequest request);
 
     @Operation(
             summary = "주문 목록 조회",
@@ -28,4 +25,10 @@ ApiResponse<OrderListResponse> getOrderList(
             int pageNo,
             int pageSize
     );
+
+    @Operation(
+            summary = "주문 상세 조회",
+            description = "주문의 상세 정보를 조회합니다."
+    )
+    ApiResponse<OrderDetailResponse> getOrderDetail(String orderId);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
index 46ed731db..6576c8557 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
@@ -1,5 +1,7 @@
 package com.loopers.application.api.order;
 
+import com.loopers.core.domain.order.OrderDetail;
+import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.OrderListItem;
 import com.loopers.core.domain.order.OrderListView;
 import com.loopers.core.service.order.command.OrderProductsCommand;
@@ -68,4 +70,33 @@ public static OrderListItemResponse from(OrderListItem item) {
             }
         }
     }
+
+    public record OrderDetailResponse(
+            String orderId,
+            String userId,
+            List<OrderDetailItemResponse> items
+    ) {
+
+        public static OrderDetailResponse from(OrderDetail detail) {
+            return new OrderDetailResponse(
+                    detail.getOrder().getId().value(),
+                    detail.getOrder().getUserId().value(),
+                    detail.getOrderItems().stream()
+                            .map(OrderDetailItemResponse::from)
+                            .toList()
+            );
+        }
+
+        public record OrderDetailItemResponse(
+                String productId,
+                Long quantity
+        ) {
+            public static OrderDetailItemResponse from(OrderItem item) {
+                return new OrderDetailItemResponse(
+                        item.getProductId().value(),
+                        item.getQuantity().value()
+                );
+            }
+        }
+    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
index 53f69c769..4aa4df049 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
@@ -7,10 +7,14 @@
 import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandName;
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.OrderItem;
+import com.loopers.core.domain.order.repository.OrderItemRepository;
 import com.loopers.core.domain.order.repository.OrderRepository;
 import com.loopers.core.domain.order.vo.OrderId;
+import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
 import com.loopers.core.domain.product.vo.ProductStock;
@@ -53,6 +57,9 @@ class OrderV1ApiIntegrationTest extends ApiIntegrationTest {
     @Autowired
     OrderRepository orderRepository;
 
+    @Autowired
+    OrderItemRepository orderItemRepository;
+
     String userIdentifier;
     String productId;
     Product product;
@@ -351,6 +358,73 @@ void Status404() {
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
             }
         }
+    }
+
+    @Nested
+    @DisplayName("상품 상세 조회")
+    class 상품_상세_조회 {
+
+        @Nested
+        @DisplayName("정상 조회 되는 경우")
+        class 정상_조회_되는_경우 {
+
+            Order savedOrder;
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
+                );
+
+                userIdentifier = user.getIdentifier().value();
+                savedOrder = orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), user.getId())
+                                .create()
+                );
+                orderItemRepository.save(
+                        Instancio.of(OrderItem.class)
+                                .set(field(OrderItem::getId), OrderItemId.empty())
+                                .set(field(OrderItem::getProductId), new ProductId("1"))
+                                .set(field(OrderItem::getOrderId), savedOrder.getId())
+                                .create()
+                );
+            }
+
+            @Test
+            @DisplayName("Status 200")
+            void Status200() {
+                String endPoint = "/api/v1/orders/" + savedOrder.getId().value();
+                ParameterizedTypeReference<ApiResponse<OrderDetailResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+                ResponseEntity<ApiResponse<OrderDetailResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+            }
+        }
+
+        @Nested
+        @DisplayName("주문이 존재하지 않는 경우")
+        class 주문이_존재하지_않는_경우 {
 
+            @Test
+            @DisplayName("Status 404")
+            void Status404() {
+                String endPoint = "/api/v1/orders/99999";
+                ParameterizedTypeReference<ApiResponse<OrderDetailResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+                ResponseEntity<ApiResponse<OrderDetailResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
     }
 }

From f98c724c8cc2bdb4c0d3013d4a9d625e20462a4a Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 22:17:28 +0900
Subject: [PATCH 14/29] =?UTF-8?q?test=20:=20API=20=ED=86=B5=ED=95=A9?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95,=20testFixture?=
 =?UTF-8?q?=20Instancio=EB=A1=9C=20=EC=88=98=EC=A0=95=20API=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=EB=8A=94=20=EC=83=81?=
 =?UTF-8?q?=ED=83=9C=EB=A7=8C=20=ED=99=95=EC=9D=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/product/ProductV1Api.java |   2 +-
 .../api/brand/BrandV1ApiIntegrationTest.java  |  37 +++-
 .../api/order/OrderV1ApiIntegrationTest.java  |  92 +++------
 .../product/ProductV1ApiIntegrationTest.java  |  98 +++++-----
 .../ProductLikeV1ApiIntegrationTest.java      | 174 ++++++------------
 .../api/user/UserV1ApiApiIntegrationTest.java |  73 +++-----
 6 files changed, 192 insertions(+), 284 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
index d2309ef28..9e17bc72f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1Api.java
@@ -24,7 +24,7 @@ public ApiResponse<ProductV1Dto.GetProductResponse> getProduct(@PathVariable Str
     }
 
     @Override
-    @GetMapping("/")
+    @GetMapping
     public ApiResponse<ProductV1Dto.GetProductListResponse> getProductList(
             @RequestParam String brandId,
             @RequestParam String createdAtSort,
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
index 805cd3ed8..2d5bb823b 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/brand/BrandV1ApiIntegrationTest.java
@@ -4,8 +4,8 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
-import com.loopers.core.domain.brand.vo.BrandDescription;
-import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.brand.vo.BrandId;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -17,7 +17,9 @@
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 
+import static com.loopers.application.api.brand.BrandV1Dto.GetBrandResponse;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
 
 class BrandV1ApiIntegrationTest extends ApiIntegrationTest {
 
@@ -37,26 +39,47 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 brandId = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .create()
                 ).getId().value();
             }
 
             @Test
-            @DisplayName("브랜드의 정보를 조회한다.")
-            void 브랜드의_정보를_조회한다() {
+            @DisplayName("Status 200")
+            void Status200() {
                 //given
                 String endPoint = "/api/v1/brands/" + brandId;
-                ParameterizedTypeReference<ApiResponse<BrandV1Dto.GetBrandResponse>> responseType =
+                ParameterizedTypeReference<ApiResponse<GetBrandResponse>> responseType =
                         new ParameterizedTypeReference<>() {
                         };
 
                 //when
-                ResponseEntity<ApiResponse<BrandV1Dto.GetBrandResponse>> response =
+                ResponseEntity<ApiResponse<GetBrandResponse>> response =
                         testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
 
                 //then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
             }
         }
+
+        @Nested
+        @DisplayName("브랜드가 존재하지 않는 경우")
+        class 브랜드가_존재하지_않는_경우 {
+
+            @Test
+            @DisplayName("Status 404")
+            void Status404() {
+                String endPoint = "/api/v1/brands/99999";
+                ParameterizedTypeReference<ApiResponse<GetBrandResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+                ResponseEntity<ApiResponse<GetBrandResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                //then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
     }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
index 4aa4df049..e03843df6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
@@ -4,8 +4,7 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
-import com.loopers.core.domain.brand.vo.BrandDescription;
-import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.domain.order.Order;
 import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.repository.OrderItemRepository;
@@ -15,14 +14,10 @@
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductId;
-import com.loopers.core.domain.product.vo.ProductName;
-import com.loopers.core.domain.product.vo.ProductPrice;
-import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.*;
 import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
@@ -75,12 +70,11 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 User user = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
                 userIdentifier = user.getIdentifier().value();
                 userPointRepository.save(
@@ -92,23 +86,23 @@ void setUp() {
                 );
 
                 Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .create()
                 );
 
                 product = productRepository.save(
-                        Product.create(
-                                brand.getId(),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brand.getId())
+                                .create()
                 );
                 productId = product.getId().value();
             }
 
             @Test
-            @DisplayName("주문을 생성하고 주문 ID를 반환한다.")
-            void 주문을_생성하고_주문_ID를_반환한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
                 String endPoint = "/api/v1/orders";
                 HttpHeaders headers = new HttpHeaders();
@@ -132,9 +126,6 @@ void setUp() {
 
                 // Then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertThat(response.getBody()).isNotNull();
-                assertThat(response.getBody().data()).isNotNull();
-                assertThat(response.getBody().data().orderId()).isNotBlank();
             }
         }
 
@@ -142,26 +133,9 @@ void setUp() {
         @DisplayName("X-USER-ID 헤더가 없을 경우")
         class X_USER_ID_헤더가_없을_경우 {
 
-            @BeforeEach
-            void setUp() {
-                Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
-                );
-
-                product = productRepository.save(
-                        Product.create(
-                                brand.getId(),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
-                );
-                productId = product.getId().value();
-            }
-
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // When
                 String endPoint = "/api/v1/orders";
                 HttpHeaders headers = new HttpHeaders();
@@ -191,26 +165,9 @@ void setUp() {
         @DisplayName("존재하지 않는 사용자로 요청할 경우")
         class 존재하지_않는_사용자로_요청 {
 
-            @BeforeEach
-            void setUp() {
-                Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
-                );
-
-                product = productRepository.save(
-                        Product.create(
-                                brand.getId(),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
-                );
-                productId = product.getId().value();
-            }
-
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // When
                 String endPoint = "/api/v1/orders";
                 HttpHeaders headers = new HttpHeaders();
@@ -244,12 +201,11 @@ class 존재하지_않는_상품_ID로_요청 {
             @BeforeEach
             void setUp() {
                 User user = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
                 userIdentifier = user.getIdentifier().value();
             }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
index c2658166a..27b05d5f7 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/product/ProductV1ApiIntegrationTest.java
@@ -4,14 +4,12 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
-import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandId;
-import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
-import com.loopers.core.domain.product.vo.ProductName;
-import com.loopers.core.domain.product.vo.ProductPrice;
-import com.loopers.core.domain.product.vo.ProductStock;
+import com.loopers.core.domain.product.vo.ProductId;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,11 +21,10 @@
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 
-import java.math.BigDecimal;
-
 import static com.loopers.application.api.product.ProductV1Dto.GetProductListResponse;
 import static com.loopers.application.api.product.ProductV1Dto.GetProductResponse;
-import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
 
 class ProductV1ApiIntegrationTest extends ApiIntegrationTest {
 
@@ -50,34 +47,34 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .create()
                 );
                 brandId = brand.getId().value();
 
                 Product product1 = productRepository.save(
-                        Product.create(
-                                new BrandId(brandId),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brand.getId())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 );
 
                 Product product2 = productRepository.save(
-                        Product.create(
-                                new BrandId(brandId),
-                                new ProductName("라로스 에엑셀렌즈"),
-                                new ProductPrice(new BigDecimal("200.00")),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brand.getId())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 );
             }
 
             @Test
-            @DisplayName("브랜드 ID로 상품 목록을 조회한다.")
-            void 브랜드_ID로_상품_목록을_조회한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
-                String endPoint = "/api/v1/products/?brandId=" + brandId
+                String endPoint = "/api/v1/products?brandId=" + brandId
                         + "&createdAtSort=&priceSort=&likeCountSort=ASC&pageNo=0&pageSize=10";
 
                 ParameterizedTypeReference<ApiResponse<GetProductListResponse>> responseType =
@@ -88,14 +85,7 @@ void setUp() {
                         testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
 
                 // Then
-                assertSoftly(softly -> {
-                    softly.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                    softly.assertThat(response.getBody()).isNotNull();
-                    softly.assertThat(response.getBody().data()).isNotNull();
-                    softly.assertThat(response.getBody().data().items()).hasSize(2);
-                    softly.assertThat(response.getBody().data().totalElements()).isEqualTo(2);
-                    softly.assertThat(response.getBody().data().totalPages()).isGreaterThan(0);
-                });
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
             }
         }
     }
@@ -113,26 +103,46 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .create()
                 );
 
                 productId = productRepository.save(
-                        Product.create(
-                                brand.getId(),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brand.getId())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 ).getId().value();
             }
 
             @Test
-            @DisplayName("브랜드 ID로 상품 목록을 조회한다.")
-            void 브랜드_ID로_상품_목록을_조회한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
                 String endPoint = "/api/v1/products/" + productId;
 
+                ParameterizedTypeReference<ApiResponse<GetProductResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<GetProductResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // Then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+            }
+        }
 
+        @Nested
+        @DisplayName("상품이 존재하지 않는 경우")
+        class 상품이_존재하지_않는_경우 {
+
+            @Test
+            @DisplayName("Status 404")
+            void status404() {
+                String endPoint = "/api/v1/products/99999";
                 ParameterizedTypeReference<ApiResponse<GetProductResponse>> responseType =
                         new ParameterizedTypeReference<>() {
                         };
@@ -141,11 +151,7 @@ void setUp() {
                         testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
 
                 // Then
-                assertSoftly(softly -> {
-                    softly.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                    softly.assertThat(response.getBody()).isNotNull();
-                    softly.assertThat(response.getBody().data()).isNotNull();
-                });
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
             }
         }
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
index 03363bcba..e1ea08274 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/productlike/ProductLikeV1ApiIntegrationTest.java
@@ -5,19 +5,19 @@
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
 import com.loopers.core.domain.brand.vo.BrandDescription;
+import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.domain.brand.vo.BrandName;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
-import com.loopers.core.domain.product.vo.ProductId;
-import com.loopers.core.domain.product.vo.ProductName;
-import com.loopers.core.domain.product.vo.ProductPrice;
-import com.loopers.core.domain.product.vo.ProductStock;
+import com.loopers.core.domain.product.vo.*;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -29,6 +29,7 @@
 import java.math.BigDecimal;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
 
 class ProductLikeV1ApiIntegrationTest extends ApiIntegrationTest {
 
@@ -56,33 +57,33 @@ class 정상_요청인_경우 {
             @BeforeEach
             void setUp() {
                 User user = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
                 userId = user.getIdentifier().value();
 
                 Brand brand = brandRepository.save(
-                        Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .create()
                 );
 
                 product = productRepository.save(
-                        Product.create(
-                                brand.getId(),
-                                new ProductName("엔젤스 쉐어"),
-                                new ProductPrice(new BigDecimal("150.00")),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brand.getId())
+                                .set(field(Product::getLikeCount), ProductLikeCount.init())
+                                .create()
                 );
                 productId = product.getId().value();
             }
 
             @Test
-            @DisplayName("상품에 좋아요를 등록한다.")
-            void 상품에_좋아요를_등록한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
                 String endPoint = "/api/v1/like/products/" + productId;
                 HttpHeaders headers = new HttpHeaders();
@@ -103,6 +104,7 @@ void setUp() {
                 // 좋아요 수 확인
                 Product likedProduct = productRepository.getById(new ProductId(productId));
                 assertThat(likedProduct.getLikeCount().value()).isEqualTo(1);
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
             }
         }
 
@@ -111,8 +113,8 @@ void setUp() {
         class X_USER_ID_헤더가_없을_경우 {
 
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // When
                 String endPoint = "/api/v1/like/products/" + productId;
 
@@ -146,8 +148,8 @@ void setUp() {
             }
 
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // When
                 String endPoint = "/api/v1/like/products/99999";
                 HttpHeaders headers = new HttpHeaders();
@@ -171,8 +173,8 @@ void setUp() {
         class 존재하지_않는_사용자로_요청 {
 
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // When
                 String endPoint = "/api/v1/like/products/" + productId;
                 HttpHeaders headers = new HttpHeaders();
@@ -239,8 +241,8 @@ void setUp() {
             }
 
             @Test
-            @DisplayName("상품의 좋아요를 취소한다.")
-            void 상품의_좋아요를_취소한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
                 String endPoint = "/api/v1/like/products/" + productId;
                 HttpHeaders headers = new HttpHeaders();
@@ -256,11 +258,6 @@ void setUp() {
 
                 // Then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertThat(response.getBody()).isNotNull();
-
-                // 좋아요 수 확인
-                Product unlikedProduct = productRepository.getById(new ProductId(productId));
-                assertThat(unlikedProduct.getLikeCount().value()).isEqualTo(0);
             }
         }
 
@@ -269,8 +266,8 @@ void setUp() {
         class X_USER_ID_헤더가_없을_경우 {
 
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // When
                 String endPoint = "/api/v1/like/products/" + productId;
 
@@ -293,19 +290,18 @@ class 존재하지_않는_상품_ID로_요청 {
             @BeforeEach
             void setUp() {
                 User user = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
                 userId = user.getIdentifier().value();
             }
 
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // When
                 String endPoint = "/api/v1/like/products/99999";
                 HttpHeaders headers = new HttpHeaders();
@@ -358,36 +354,35 @@ class 좋아요한_상품_목록_조회 {
         void setUp() {
             // 사용자 생성
             User user = userRepository.save(
-                    User.create(
-                            UserIdentifier.create("kilian"),
-                            UserEmail.create("kilian@gmail.com"),
-                            UserBirthDay.create("1997-10-08"),
-                            UserGender.create("MALE")
-                    )
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                            .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                            .create()
             );
             userId = user.getIdentifier().value();
 
             // 브랜드 생성
             Brand brand = brandRepository.save(
-                    Brand.create(new BrandName("kilian"), new BrandDescription("향수 브랜드"))
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .create()
             );
 
             // 상품 생성 (2개)
             Product product1 = productRepository.save(
-                    Product.create(
-                            brand.getId(),
-                            new ProductName("엔젤스 쉐어"),
-                            new ProductPrice(new BigDecimal("150.00")),
-                            new ProductStock(10L)
-                    )
+                    Instancio.of(Product.class)
+                            .set(field(Product::getId), ProductId.empty())
+                            .set(field(Product::getBrandId), brand.getId())
+                            .set(field(Product::getLikeCount), ProductLikeCount.init())
+                            .create()
             );
             Product product2 = productRepository.save(
-                    Product.create(
-                            brand.getId(),
-                            new ProductName("라로스 에센스"),
-                            new ProductPrice(new BigDecimal("200.00")),
-                            new ProductStock(10L)
-                    )
+                    Instancio.of(Product.class)
+                            .set(field(Product::getId), ProductId.empty())
+                            .set(field(Product::getBrandId), brand.getId())
+                            .set(field(Product::getLikeCount), ProductLikeCount.init())
+                            .create()
             );
 
             // 좋아요 등록
@@ -410,8 +405,8 @@ void setUp() {
         class 정상_요청인_경우 {
 
             @Test
-            @DisplayName("사용자가 좋아요한 상품 목록을 조회한다.")
-            void 사용자가_좋아요한_상품_목록을_조회한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // When
                 String endPoint = "/api/v1/like/products?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
                 HttpHeaders headers = new HttpHeaders();
@@ -427,10 +422,6 @@ class 정상_요청인_경우 {
 
                 // Then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertThat(response.getBody()).isNotNull();
-                assertThat(response.getBody().data()).isNotNull();
-                assertThat(response.getBody().data().items()).hasSize(2);
-                assertThat(response.getBody().data().totalElements()).isEqualTo(2);
             }
         }
 
@@ -439,10 +430,10 @@ class 정상_요청인_경우 {
         class X_USER_ID_헤더가_없을_경우 {
 
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // When
-                String endPoint = "/api/v1/like/products/?brandId=null&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
+                String endPoint = "/api/v1/like/products?brandId=null&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
 
                 ParameterizedTypeReference<ApiResponse<Void>> responseType =
                         new ParameterizedTypeReference<>() {
@@ -455,48 +446,5 @@ class X_USER_ID_헤더가_없을_경우 {
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
             }
         }
-
-        @Nested
-        @DisplayName("좋아요한 상품이 없을 경우")
-        class 좋아요한_상품이_없을_경우 {
-
-            @BeforeEach
-            void setUp() {
-                // 다른 사용자 생성 (좋아요한 상품이 없음)
-                User otherUser = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("otherUser"),
-                                UserEmail.create("other@gmail.com"),
-                                UserBirthDay.create("2000-01-01"),
-                                UserGender.create("FEMALE")
-                        )
-                );
-                userId = otherUser.getIdentifier().value();
-            }
-
-            @Test
-            @DisplayName("빈 목록을 반환한다.")
-            void 빈_목록을_반환한다() {
-                // When
-                String endPoint = "/api/v1/like/products/?brandId=1&createdAtSort=&priceSort=&likeCountSort=&pageNo=0&pageSize=10";
-                HttpHeaders headers = new HttpHeaders();
-                headers.set("X-USER-ID", userId);
-                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
-
-                ParameterizedTypeReference<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> responseType =
-                        new ParameterizedTypeReference<>() {
-                        };
-
-                ResponseEntity<ApiResponse<ProductLikeV1Dto.LikeProductsResponse>> response =
-                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
-
-                // Then
-                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertThat(response.getBody()).isNotNull();
-                assertThat(response.getBody().data()).isNotNull();
-                assertThat(response.getBody().data().items()).isEmpty();
-                assertThat(response.getBody().data().totalElements()).isEqualTo(0);
-            }
-        }
     }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 77155076e..4573ed5f8 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -4,12 +4,12 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.user.JoinUserService;
 import com.loopers.core.service.user.command.JoinUserCommand;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -24,7 +24,7 @@
 import java.math.BigDecimal;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
 
@@ -43,8 +43,8 @@ class 회원가입 {
         class 정상_요청인_경우 {
 
             @Test
-            @DisplayName("생성된 유저의 정보를 반환한다.")
-            void 생성된_유저의_정보를_반환한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // given
                 UserV1Dto.JoinUserRequest request = new UserV1Dto.JoinUserRequest(
                         "user123",
@@ -64,12 +64,6 @@ class 정상_요청인_경우 {
 
                 // then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertThat(response.getBody()).isNotNull();
-                assertThat(response.getBody().data()).isNotNull();
-                assertThat(response.getBody().data().identifier()).isEqualTo("user123");
-                assertThat(response.getBody().data().email()).isEqualTo("user@example.com");
-                assertThat(response.getBody().data().birthday()).isEqualTo("2000-01-15");
-                assertThat(response.getBody().data().gender()).isEqualTo("MALE");
             }
         }
 
@@ -78,8 +72,8 @@ class 정상_요청인_경우 {
         class 성별이_없는_경우 {
 
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // given
                 UserV1Dto.JoinUserRequest request = new UserV1Dto.JoinUserRequest(
                         "user123",
@@ -112,18 +106,17 @@ class 성공할_경우 {
             @BeforeEach
             void setUp() {
                 userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
             }
 
             @Test
-            @DisplayName("해당하는 유저 정보를 응답으로 반환한다.")
-            void 해당하는_유저_정보를_응답으로_반환한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // given
                 String identifier = "kilian";
                 String endPoint = "/api/v1/users/" + identifier;
@@ -137,14 +130,6 @@ void setUp() {
 
                 // then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertSoftly(softly -> {
-                    softly.assertThat(response.getBody()).isNotNull();
-                    softly.assertThat(response.getBody().data()).isNotNull();
-                    softly.assertThat(response.getBody().data().identifier()).isEqualTo("kilian");
-                    softly.assertThat(response.getBody().data().email()).isEqualTo("kilian@gmail.com");
-                    softly.assertThat(response.getBody().data().birthday()).isEqualTo("1997-10-08");
-                    softly.assertThat(response.getBody().data().gender()).isEqualTo("MALE");
-                });
             }
         }
 
@@ -153,8 +138,8 @@ void setUp() {
         class 존재하지_않는_ID로_조회할_경우 {
 
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // given
                 String identifier = "nonExist";
                 String endPoint = "/api/v1/users/" + identifier;
@@ -191,8 +176,8 @@ void setUp() {
             }
 
             @Test
-            @DisplayName("보유 포인트를 응답으로 반환한다.")
-            void 보유_포인트를_응답으로_반환한다() {
+            @DisplayName("Status 200")
+            void status200() {
                 // given
                 String userIdentifier = "kilian";
                 String endPoint = "/api/v1/users/points";
@@ -209,11 +194,6 @@ void setUp() {
 
                 // then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertSoftly(softly -> {
-                    softly.assertThat(response.getBody()).isNotNull();
-                    softly.assertThat(response.getBody().data()).isNotNull();
-                    softly.assertThat(response.getBody().data().balance().intValue()).isEqualTo(0);
-                });
             }
         }
 
@@ -222,8 +202,8 @@ void setUp() {
         class X_USER_ID_헤더가_없을_경우 {
 
             @Test
-            @DisplayName("400 Bad Request 응답을 반환한다.")
-            void badRequest응답을_반환한다() {
+            @DisplayName("Status 400")
+            void status400() {
                 // given
                 String endPoint = "/api/v1/users/points";
                 ParameterizedTypeReference<ApiResponse<Void>> responseType =
@@ -259,8 +239,8 @@ void setUp() {
             }
 
             @Test
-            @DisplayName("충전된 보유 총량을 응답으로 반환한다.")
-            void 포인트_충전_성공() {
+            @DisplayName("Status 200")
+            void status200() {
                 // given
                 String userIdentifier = "kilian";
                 String endPoint = "/api/v1/users/points/charge";
@@ -278,11 +258,6 @@ void setUp() {
 
                 // then
                 assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
-                assertSoftly(softly -> {
-                    softly.assertThat(response.getBody()).isNotNull();
-                    softly.assertThat(response.getBody().data()).isNotNull();
-                    softly.assertThat(response.getBody().data().balance().intValue()).isEqualTo(1000);
-                });
             }
         }
 
@@ -291,8 +266,8 @@ void setUp() {
         class 존재하지_않는_유저로_요청 {
 
             @Test
-            @DisplayName("404 Not Found 응답을 반환한다.")
-            void notFound응답을_반환한다() {
+            @DisplayName("Status 404")
+            void status404() {
                 // given
                 String userIdentifier = "nonExist";
                 String endPoint = "/api/v1/users/points/charge";

From c607e66ea732a6e6f9de06b54562c812d31b44fd Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 22:23:20 +0900
Subject: [PATCH 15/29] =?UTF-8?q?feature=20:=20=EB=A1=9C=EC=BB=AC=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20http=20=ED=8C=8C=EC=9D=BC=20?=
 =?UTF-8?q?=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/resources/http/brand/get-brand.http  |  3 ++
 .../http/like/create-product-like.http        |  4 +++
 .../http/like/delete-product-like.http        |  4 +++
 .../http/like/get-liked-product-list.http     | 29 +++++++++++++++++++
 .../resources/http/order/create-order.http    | 16 ++++++++++
 .../http/order/get-order-detail.http          |  3 ++
 .../resources/http/order/get-order-list.http  | 19 ++++++++++++
 .../http/product/get-product-detail.http      |  3 ++
 .../http/product/get-product-list.http        | 27 +++++++++++++++++
 .../http/{ => user}/charge-user-point.http    |  0
 .../{ => user}/get-user-by-identifier.http    |  0
 .../get-user-point-by-user-identifier.http    |  0
 .../resources/http/{ => user}/join-user.http  |  0
 13 files changed, 108 insertions(+)
 create mode 100644 apps/commerce-api/src/main/resources/http/brand/get-brand.http
 create mode 100644 apps/commerce-api/src/main/resources/http/like/create-product-like.http
 create mode 100644 apps/commerce-api/src/main/resources/http/like/delete-product-like.http
 create mode 100644 apps/commerce-api/src/main/resources/http/like/get-liked-product-list.http
 create mode 100644 apps/commerce-api/src/main/resources/http/order/create-order.http
 create mode 100644 apps/commerce-api/src/main/resources/http/order/get-order-detail.http
 create mode 100644 apps/commerce-api/src/main/resources/http/order/get-order-list.http
 create mode 100644 apps/commerce-api/src/main/resources/http/product/get-product-detail.http
 create mode 100644 apps/commerce-api/src/main/resources/http/product/get-product-list.http
 rename apps/commerce-api/src/main/resources/http/{ => user}/charge-user-point.http (100%)
 rename apps/commerce-api/src/main/resources/http/{ => user}/get-user-by-identifier.http (100%)
 rename apps/commerce-api/src/main/resources/http/{ => user}/get-user-point-by-user-identifier.http (100%)
 rename apps/commerce-api/src/main/resources/http/{ => user}/join-user.http (100%)

diff --git a/apps/commerce-api/src/main/resources/http/brand/get-brand.http b/apps/commerce-api/src/main/resources/http/brand/get-brand.http
new file mode 100644
index 000000000..df8f94a84
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/brand/get-brand.http
@@ -0,0 +1,3 @@
+### 브랜드 정보 조회
+GET {{commerce-api}}/api/v1/brands/1
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/like/create-product-like.http b/apps/commerce-api/src/main/resources/http/like/create-product-like.http
new file mode 100644
index 000000000..2a2bfe62d
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/like/create-product-like.http
@@ -0,0 +1,4 @@
+### 상품 좋아요 추가
+POST {{commerce-api}}/api/v1/like/products/1
+X-USER-ID: {{identifier}}
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/like/delete-product-like.http b/apps/commerce-api/src/main/resources/http/like/delete-product-like.http
new file mode 100644
index 000000000..be0b6a181
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/like/delete-product-like.http
@@ -0,0 +1,4 @@
+### 상품 좋아요 취소
+DELETE {{commerce-api}}/api/v1/like/products/1
+X-USER-ID: {{identifier}}
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/like/get-liked-product-list.http b/apps/commerce-api/src/main/resources/http/like/get-liked-product-list.http
new file mode 100644
index 000000000..78daebc99
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/like/get-liked-product-list.http
@@ -0,0 +1,29 @@
+### 좋아요한 상품 목록 조회 (기본)
+GET {{commerce-api}}/api/v1/like/products
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 좋아요한 상품 목록 조회 (브랜드별 필터)
+GET {{commerce-api}}/api/v1/like/products?brandId=1
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 좋아요한 상품 목록 조회 (가격 정렬)
+GET {{commerce-api}}/api/v1/like/products?priceSort=ASC
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 좋아요한 상품 목록 조회 (좋아요 수 정렬)
+GET {{commerce-api}}/api/v1/like/products?likeCountSort=DESC
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 좋아요한 상품 목록 조회 (페이지네이션)
+GET {{commerce-api}}/api/v1/like/products?pageNo=0&pageSize=20
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 좋아요한 상품 목록 조회 (조합된 필터)
+GET {{commerce-api}}/api/v1/like/products?brandId=1&priceSort=ASC&pageNo=0&pageSize=20
+X-USER-ID: {{identifier}}
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/order/create-order.http b/apps/commerce-api/src/main/resources/http/order/create-order.http
new file mode 100644
index 000000000..fcdeacdc7
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/order/create-order.http
@@ -0,0 +1,16 @@
+POST {{commerce-api}}/api/v1/orders
+Content-Type: application/json
+X-USER-ID: {{identifier}}
+
+{
+  "orderItems": [
+    {
+      "productId": 1,
+      "quantity": 2
+    },
+    {
+      "productId": 2,
+      "quantity": 1
+    }
+  ]
+}
diff --git a/apps/commerce-api/src/main/resources/http/order/get-order-detail.http b/apps/commerce-api/src/main/resources/http/order/get-order-detail.http
new file mode 100644
index 000000000..a64b5c56b
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/order/get-order-detail.http
@@ -0,0 +1,3 @@
+### 주문 상세 조회
+GET {{commerce-api}}/api/v1/orders/1
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/order/get-order-list.http b/apps/commerce-api/src/main/resources/http/order/get-order-list.http
new file mode 100644
index 000000000..79829d8c6
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/order/get-order-list.http
@@ -0,0 +1,19 @@
+### 주문 목록 조회 (기본)
+GET {{commerce-api}}/api/v1/orders
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 주문 목록 조회 (생성일자 정렬)
+GET {{commerce-api}}/api/v1/orders?createdAtSort=DESC
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 주문 목록 조회 (페이지네이션)
+GET {{commerce-api}}/api/v1/orders?pageNo=0&pageSize=20
+X-USER-ID: {{identifier}}
+Content-Type: application/json
+
+### 주문 목록 조회 (정렬 + 페이지네이션)
+GET {{commerce-api}}/api/v1/orders?createdAtSort=DESC&pageNo=0&pageSize=20
+X-USER-ID: {{identifier}}
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/product/get-product-detail.http b/apps/commerce-api/src/main/resources/http/product/get-product-detail.http
new file mode 100644
index 000000000..10e71506a
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/product/get-product-detail.http
@@ -0,0 +1,3 @@
+### 상품 상세 조회
+GET {{commerce-api}}/api/v1/products/1
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/product/get-product-list.http b/apps/commerce-api/src/main/resources/http/product/get-product-list.http
new file mode 100644
index 000000000..2aa7abcf1
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/product/get-product-list.http
@@ -0,0 +1,27 @@
+### 상품 목록 조회 (기본)
+GET {{commerce-api}}/api/v1/products
+Content-Type: application/json
+
+### 상품 목록 조회 (브랜드별 필터)
+GET {{commerce-api}}/api/v1/products?brandId=1
+Content-Type: application/json
+
+### 상품 목록 조회 (생성일자 정렬)
+GET {{commerce-api}}/api/v1/products?createdAtSort=DESC
+Content-Type: application/json
+
+### 상품 목록 조회 (가격 정렬)
+GET {{commerce-api}}/api/v1/products?priceSort=ASC
+Content-Type: application/json
+
+### 상품 목록 조회 (좋아요 수 정렬)
+GET {{commerce-api}}/api/v1/products?likeCountSort=DESC
+Content-Type: application/json
+
+### 상품 목록 조회 (페이지네이션)
+GET {{commerce-api}}/api/v1/products?pageNo=0&pageSize=20
+Content-Type: application/json
+
+### 상품 목록 조회 (조합된 필터)
+GET {{commerce-api}}/api/v1/products?brandId=1&priceSort=ASC&pageNo=0&pageSize=20
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/charge-user-point.http b/apps/commerce-api/src/main/resources/http/user/charge-user-point.http
similarity index 100%
rename from apps/commerce-api/src/main/resources/http/charge-user-point.http
rename to apps/commerce-api/src/main/resources/http/user/charge-user-point.http
diff --git a/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http b/apps/commerce-api/src/main/resources/http/user/get-user-by-identifier.http
similarity index 100%
rename from apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
rename to apps/commerce-api/src/main/resources/http/user/get-user-by-identifier.http
diff --git a/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http b/apps/commerce-api/src/main/resources/http/user/get-user-point-by-user-identifier.http
similarity index 100%
rename from apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
rename to apps/commerce-api/src/main/resources/http/user/get-user-point-by-user-identifier.http
diff --git a/apps/commerce-api/src/main/resources/http/join-user.http b/apps/commerce-api/src/main/resources/http/user/join-user.http
similarity index 100%
rename from apps/commerce-api/src/main/resources/http/join-user.http
rename to apps/commerce-api/src/main/resources/http/user/join-user.http

From 1b7efd106e6c78df7dfc962238ce8a21e263a436 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 22:45:40 +0900
Subject: [PATCH 16/29] =?UTF-8?q?feature=20:=20=EC=A3=BC=EB=AC=B8=20?=
 =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20Fix?=
 =?UTF-8?q?ture=20Instancio=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../order/OrderCashierIntegrationTest.java    |  80 ++++----
 .../OrderLineAllocatorIntegrationTest.java    | 111 +++++------
 .../OrderQueryServiceIntegrationTest.java     | 185 ++++++++++++------
 .../order/component/OrderCashierTest.java     |  59 +++---
 .../component/OrderLineAggregatorTest.java    |  38 +++-
 .../component/OrderLineAllocatorTest.java     |  53 ++---
 6 files changed, 313 insertions(+), 213 deletions(-)

diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
index 44fc32d90..b8e960491 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
@@ -1,9 +1,8 @@
 package com.loopers.core.service.order;
 
-import com.loopers.core.domain.common.vo.CreatedAt;
-import com.loopers.core.domain.common.vo.UpdatedAt;
 import com.loopers.core.domain.order.Order;
 import com.loopers.core.domain.order.repository.OrderRepository;
+import com.loopers.core.domain.order.vo.OrderId;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
@@ -11,11 +10,11 @@
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.*;
 import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.component.OrderCashier;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,10 +22,11 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
-import java.time.LocalDate;
 import java.util.List;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 @DisplayName("주문 결제 처리")
 class OrderCashierIntegrationTest extends IntegrationTest {
@@ -61,27 +61,30 @@ class 포인트가_충분한_경우 {
             @BeforeEach
             void setUp() {
                 // 사용자 생성 및 저장
-                user = User.create(
-                        new UserIdentifier("testuser"),
-                        new UserEmail("test@example.com"),
-                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                        UserGender.MALE
+                user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("testuser"))
+                                .set(field(User::getEmail), new UserEmail("test@example.com"))
+                                .create()
                 );
-                user = userRepository.save(user);
 
                 // 사용자 포인트 생성 및 저장 (50,000 포인트)
-                UserPoint userPoint = UserPoint.mappedBy(
-                        UserPointId.empty(),
-                        user.getId(),
-                        new UserPointBalance(new BigDecimal("50000")),
-                        CreatedAt.now(),
-                        UpdatedAt.now()
+                userPointRepository.save(
+                        Instancio.of(UserPoint.class)
+                                .set(field("id"), UserPointId.empty())
+                                .set(field("userId"), user.getId())
+                                .set(field("balance"), new UserPointBalance(new BigDecimal("50000")))
+                                .create()
                 );
-                userPointRepository.save(userPoint);
 
                 // 주문 생성 및 저장
-                order = Order.create(user.getId());
-                order = orderRepository.save(order);
+                order = orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), user.getId())
+                                .create()
+                );
 
                 // 결제 금액 (20,000 - 포인트는 50,000으로 충분함)
                 payAmount = new PayAmount(new BigDecimal("20000"));
@@ -164,27 +167,30 @@ class 포인트가_부족한_경우 {
             @BeforeEach
             void setUp() {
                 // 사용자 생성 및 저장
-                user = User.create(
-                        new UserIdentifier("testuser"),
-                        new UserEmail("test@example.com"),
-                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                        UserGender.MALE
+                user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("testuser"))
+                                .set(field(User::getEmail), new UserEmail("test@example.com"))
+                                .create()
                 );
-                user = userRepository.save(user);
 
                 // 사용자 포인트 생성 및 저장 (50,000 포인트)
-                UserPoint userPoint = UserPoint.mappedBy(
-                        UserPointId.empty(),
-                        user.getId(),
-                        new UserPointBalance(new BigDecimal("50000")),
-                        CreatedAt.now(),
-                        UpdatedAt.now()
+                userPointRepository.save(
+                        Instancio.of(UserPoint.class)
+                                .set(field("id"), UserPointId.empty())
+                                .set(field("userId"), user.getId())
+                                .set(field("balance"), new UserPointBalance(new BigDecimal("50000")))
+                                .create()
                 );
-                userPointRepository.save(userPoint);
 
                 // 주문 생성 및 저장
-                order = Order.create(user.getId());
-                order = orderRepository.save(order);
+                order = orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), user.getId())
+                                .create()
+                );
             }
 
             @Test
@@ -194,9 +200,9 @@ void setUp() {
                 PayAmount largePayAmount = new PayAmount(new BigDecimal("100000"));
 
                 // when & then
-                org.assertj.core.api.Assertions.assertThatThrownBy(
-                                () -> orderCashier.checkout(user, order, largePayAmount)
-                        ).isInstanceOf(IllegalArgumentException.class)
+                assertThatThrownBy(
+                        () -> orderCashier.checkout(user, order, largePayAmount)
+                ).isInstanceOf(IllegalArgumentException.class)
                         .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
             }
         }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
index 9fc549460..96b735449 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderLineAllocatorIntegrationTest.java
@@ -1,19 +1,20 @@
 package com.loopers.core.service.order;
 
 import com.loopers.core.domain.brand.vo.BrandId;
-import com.loopers.core.domain.common.vo.CreatedAt;
-import com.loopers.core.domain.common.vo.DeletedAt;
-import com.loopers.core.domain.common.vo.UpdatedAt;
 import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.vo.OrderId;
+import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
-import com.loopers.core.domain.product.vo.*;
+import com.loopers.core.domain.product.vo.ProductId;
+import com.loopers.core.domain.product.vo.ProductName;
+import com.loopers.core.domain.product.vo.ProductPrice;
+import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.component.OrderLineAllocator;
-import org.assertj.core.api.SoftAssertions;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,7 +24,9 @@
 import java.math.BigDecimal;
 import java.util.List;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 @DisplayName("주문 라인 할당")
 class OrderLineAllocatorIntegrationTest extends IntegrationTest {
@@ -48,25 +51,23 @@ class 충분한_재고가_있는_경우 {
             @BeforeEach
             void setUp() {
                 // 상품 생성 및 저장 (가격: 10,000, 재고: 100개)
-                product = Product.mappedBy(
-                        ProductId.empty(),
-                        new BrandId("1"),
-                        new ProductName("테스트상품"),
-                        new ProductPrice(new BigDecimal("10000")),
-                        new ProductStock(100L),
-                        ProductLikeCount.init(),
-                        CreatedAt.now(),
-                        UpdatedAt.now(),
-                        DeletedAt.empty()
+                product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), new BrandId("1"))
+                                .set(field(Product::getName), new ProductName("테스트상품"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal("10000")))
+                                .set(field(Product::getStock), new ProductStock(100L))
+                                .create()
                 );
-                product = productRepository.save(product);
 
                 // 주문 상품 생성 (수량: 5개)
-                orderItem = OrderItem.create(
-                        new OrderId("1"),
-                        product.getId(),
-                        new Quantity(5L)
-                );
+                orderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getOrderId), new OrderId("1"))
+                        .set(field(OrderItem::getProductId), product.getId())
+                        .set(field(OrderItem::getQuantity), new Quantity(5L))
+                        .create();
             }
 
             @Test
@@ -76,7 +77,7 @@ void setUp() {
                 BigDecimal totalPrice = orderLineAllocator.allocate(orderItem);
 
                 // then - 반환된 가격 검증 (10,000 * 5 = 50,000)
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(totalPrice)
                             .as("할당된 총 가격")
                             .isEqualByComparingTo(new BigDecimal("50000"));
@@ -84,7 +85,7 @@ void setUp() {
 
                 // 상품 재고 차감 확인
                 Product allocatedProduct = productRepository.getById(product.getId());
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("차감 후 남은 재고")
                             .isEqualTo(95L); // 100 - 5
@@ -102,7 +103,7 @@ void setUp() {
 
                 // then - 재고 누적 차감 확인
                 Product allocatedProduct = productRepository.getById(product.getId());
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("누적 차감 후 남은 재고")
                             .isEqualTo(90L); // 100 - 5 - 5
@@ -113,17 +114,18 @@ void setUp() {
             @DisplayName("정확히 남은 재고만큼 할당하면 재고가 0이 된다")
             void 정확히_남은_재고만큼_할당하면_재고가_0이_된다() {
                 // given - 정확히 현재 재고만큼 주문
-                OrderItem exactOrderItem = OrderItem.create(
-                        new OrderId("1"),
-                        product.getId(),
-                        new Quantity(100L)
-                );
+                OrderItem exactOrderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getOrderId), new OrderId("1"))
+                        .set(field(OrderItem::getProductId), product.getId())
+                        .set(field(OrderItem::getQuantity), new Quantity(100L))
+                        .create();
 
                 // when
                 BigDecimal totalPrice = orderLineAllocator.allocate(exactOrderItem);
 
                 // then - 총 가격 검증 (10,000 * 100 = 1,000,000)
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(totalPrice)
                             .as("할당된 총 가격")
                             .isEqualByComparingTo(new BigDecimal("1000000"));
@@ -131,7 +133,7 @@ void setUp() {
 
                 // 재고가 0이 되었는지 확인
                 Product allocatedProduct = productRepository.getById(product.getId());
-                SoftAssertions.assertSoftly(softly -> {
+                assertSoftly(softly -> {
                     softly.assertThat(allocatedProduct.getStock().value())
                             .as("차감 후 남은 재고")
                             .isZero();
@@ -142,11 +144,12 @@ void setUp() {
             @DisplayName("동시에 여러 사용자가 주문하면 주문한양만큼 감소한다.")
             void 동시에_여러_사용자가_주문하면_주문한양만큼_감소한다() throws InterruptedException {
                 int requestCount = 100;
-                OrderItem orderItem = OrderItem.create(
-                        new OrderId("1"),
-                        product.getId(),
-                        new Quantity(1L)
-                );
+                OrderItem orderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getOrderId), new OrderId("1"))
+                        .set(field(OrderItem::getProductId), product.getId())
+                        .set(field(OrderItem::getQuantity), new Quantity(1L))
+                        .create();
 
                 List<BigDecimal> results = ConcurrencyTestUtil.executeInParallel(
                         100,
@@ -172,34 +175,32 @@ class 재고가_부족한_경우 {
             @BeforeEach
             void setUp() {
                 // 상품 생성 및 저장 (가격: 10,000, 재고: 100개)
-                product = Product.mappedBy(
-                        ProductId.empty(),
-                        new BrandId("1"),
-                        new ProductName("테스트상품"),
-                        new ProductPrice(new BigDecimal("10000")),
-                        new ProductStock(100L),
-                        ProductLikeCount.init(),
-                        CreatedAt.now(),
-                        UpdatedAt.now(),
-                        DeletedAt.empty()
+                product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), new BrandId("1"))
+                                .set(field(Product::getName), new ProductName("테스트상품"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal("10000")))
+                                .set(field(Product::getStock), new ProductStock(100L))
+                                .create()
                 );
-                product = productRepository.save(product);
             }
 
             @Test
             @DisplayName("예외를 발생시킨다")
             void 예외를_발생시킨다() {
                 // given - 재고보다 많은 수량 주문
-                OrderItem largeOrderItem = OrderItem.create(
-                        new OrderId("1"),
-                        product.getId(),
-                        new Quantity(150L)
-                );
+                OrderItem largeOrderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getOrderId), new OrderId("1"))
+                        .set(field(OrderItem::getProductId), product.getId())
+                        .set(field(OrderItem::getQuantity), new Quantity(150L))
+                        .create();
 
                 // when & then
-                org.assertj.core.api.Assertions.assertThatThrownBy(
-                                () -> orderLineAllocator.allocate(largeOrderItem)
-                        ).isInstanceOf(IllegalArgumentException.class)
+                assertThatThrownBy(
+                        () -> orderLineAllocator.allocate(largeOrderItem)
+                ).isInstanceOf(IllegalArgumentException.class)
                         .hasMessage("상품의 재고가 부족합니다.");
             }
         }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index 98cc4d07f..348c9caa1 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -13,9 +13,14 @@
 import com.loopers.core.domain.order.repository.OrderItemRepository;
 import com.loopers.core.domain.order.repository.OrderRepository;
 import com.loopers.core.domain.order.vo.OrderId;
+import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.order.vo.Quantity;
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
 import com.loopers.core.domain.product.vo.ProductStock;
@@ -26,9 +31,11 @@
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.query.GetOrderDetailQuery;
 import com.loopers.core.service.order.query.GetOrderListQuery;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -40,6 +47,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 class OrderQueryServiceIntegrationTest extends IntegrationTest {
 
@@ -71,21 +79,23 @@ class 주문_리스트_조회_시 {
 
         @BeforeEach
         void setUp() {
-            User user = userRepository.save(User.create(
-                    UserIdentifier.create("loopers"),
-                    new UserEmail("loopers@test.com"),
-                    UserBirthDay.create("1990-01-01"),
-                    UserGender.create("MALE")
-            ));
+            User user = userRepository.save(
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), UserIdentifier.create("loopers"))
+                            .set(field(User::getEmail), new UserEmail("loopers@test.com"))
+                            .create()
+            );
             savedUserIdentifier = user.getIdentifier().value();
             savedUserId = user.getId();
 
-            User otherUser = userRepository.save(User.create(
-                    UserIdentifier.create("other"),
-                    new UserEmail("other@test.com"),
-                    UserBirthDay.create("1991-02-02"),
-                    UserGender.create("FEMALE")
-            ));
+            User otherUser = userRepository.save(
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), UserIdentifier.create("other"))
+                            .set(field(User::getEmail), new UserEmail("other@test.com"))
+                            .create()
+            );
             otherUserId = otherUser.getId();
         }
 
@@ -95,9 +105,33 @@ class 조건에_맞는_주문이_존재하는_경우 {
 
             @BeforeEach
             void setUp() {
-                orderRepository.save(Order.create(savedUserId));
-                orderRepository.save(Order.create(savedUserId));
-                orderRepository.save(Order.create(savedUserId));
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), savedUserId)
+                                .set(field(Order::getCreatedAt), CreatedAt.now())
+                                .set(field(Order::getUpdatedAt), UpdatedAt.now())
+                                .set(field(Order::getDeletedAt), DeletedAt.empty())
+                                .create()
+                );
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), savedUserId)
+                                .set(field(Order::getCreatedAt), CreatedAt.now())
+                                .set(field(Order::getUpdatedAt), UpdatedAt.now())
+                                .set(field(Order::getDeletedAt), DeletedAt.empty())
+                                .create()
+                );
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), savedUserId)
+                                .set(field(Order::getCreatedAt), CreatedAt.now())
+                                .set(field(Order::getUpdatedAt), UpdatedAt.now())
+                                .set(field(Order::getDeletedAt), DeletedAt.empty())
+                                .create()
+                );
             }
 
             @Test
@@ -160,8 +194,18 @@ void setUp() {
             @Test
             @DisplayName("특정 사용자의 주문만 조회된다.")
             void 특정_사용자의_주문만_조회된다() {
-                orderRepository.save(Order.create(otherUserId));
-                orderRepository.save(Order.create(otherUserId));
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), otherUserId)
+                                .create()
+                );
+                orderRepository.save(
+                        Instancio.of(Order.class)
+                                .set(field(Order::getId), OrderId.empty())
+                                .set(field(Order::getUserId), otherUserId)
+                                .create()
+                );
 
                 GetOrderListQuery query = new GetOrderListQuery(
                         savedUserIdentifier,
@@ -238,31 +282,46 @@ class 주문_상세_조회_시 {
 
         @BeforeEach
         void setUp() {
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("loopers"),
-                    new BrandDescription("education brand")
-            ));
+            Brand brand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("loopers"))
+                            .set(field(Brand::getDescription), new BrandDescription("education brand"))
+                            .create()
+            );
             savedBrandId = brand.getId();
 
-            User user = userRepository.save(User.create(
-                    UserIdentifier.create("loopers"),
-                    new UserEmail("loopers@test.com"),
-                    UserBirthDay.create("1990-01-01"),
-                    UserGender.create("MALE")
-            ));
+            User user = userRepository.save(
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), UserIdentifier.create("loopers"))
+                            .set(field(User::getEmail), new UserEmail("loopers@test.com"))
+                            .create()
+            );
             savedUserId = user.getId();
 
-            User otherUser = userRepository.save(User.create(
-                    UserIdentifier.create("other"),
-                    new UserEmail("other@test.com"),
-                    UserBirthDay.create("1991-02-02"),
-                    UserGender.create("FEMALE")
-            ));
-
-            Order order = orderRepository.save(Order.create(savedUserId));
+            User otherUser = userRepository.save(
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), UserIdentifier.create("other"))
+                            .set(field(User::getEmail), new UserEmail("other@test.com"))
+                            .create()
+            );
+
+            Order order = orderRepository.save(
+                    Instancio.of(Order.class)
+                            .set(field(Order::getId), OrderId.empty())
+                            .set(field(Order::getUserId), savedUserId)
+                            .create()
+            );
             savedOrderId = order.getId().value();
 
-            Order otherOrder = orderRepository.save(Order.create(otherUser.getId()));
+            Order otherOrder = orderRepository.save(
+                    Instancio.of(Order.class)
+                            .set(field(Order::getId), OrderId.empty())
+                            .set(field(Order::getUserId), otherUser.getId())
+                            .create()
+            );
         }
 
         @Nested
@@ -272,35 +331,43 @@ class 주문이_존재하는_경우 {
             @BeforeEach
             void setUp() {
                 Product product1 = productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000)),
-                                new ProductStock(100L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("MacBook Pro"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
+                                .set(field(Product::getStock), new ProductStock(100L))
+                                .create()
                 );
 
                 Product product2 = productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000)),
-                                new ProductStock(100L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("iPad Air"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(800_000)))
+                                .set(field(Product::getStock), new ProductStock(100L))
+                                .create()
                 );
 
                 OrderId orderId = new OrderId(savedOrderId);
-                orderItemRepository.save(OrderItem.create(
-                        orderId,
-                        product1.getId(),
-                        new Quantity(1L)
-                ));
-
-                orderItemRepository.save(OrderItem.create(
-                        orderId,
-                        product2.getId(),
-                        new Quantity(2L)
-                ));
+                orderItemRepository.save(
+                        Instancio.of(OrderItem.class)
+                                .set(field(OrderItem::getId), OrderItemId.empty())
+                                .set(field(OrderItem::getOrderId), orderId)
+                                .set(field(OrderItem::getProductId), product1.getId())
+                                .set(field(OrderItem::getQuantity), new Quantity(1L))
+                                .create()
+                );
+
+                orderItemRepository.save(
+                        Instancio.of(OrderItem.class)
+                                .set(field(OrderItem::getId), OrderItemId.empty())
+                                .set(field(OrderItem::getOrderId), orderId)
+                                .set(field(OrderItem::getProductId), product2.getId())
+                                .set(field(OrderItem::getQuantity), new Quantity(2L))
+                                .create()
+                );
             }
 
             @Test
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
index e3cb10bbb..87eb8114a 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
@@ -1,14 +1,15 @@
 package com.loopers.core.service.order.component;
 
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.vo.OrderId;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
-import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.*;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -20,10 +21,10 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.math.BigDecimal;
-import java.time.LocalDate;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.instancio.Select.field;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -50,14 +51,16 @@ class CheckoutMethod {
 
         @BeforeEach
         void setUp() {
-            user = User.create(
-                    new UserIdentifier("testuser"),
-                    new UserEmail("test@example.com"),
-                    new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                    UserGender.MALE
-            );
-
-            order = Order.create(user.getId());
+            user = Instancio.of(User.class)
+                    .set(field(User::getId), UserId.empty())
+                    .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                    .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                    .create();
+
+            order = Instancio.of(Order.class)
+                    .set(field(Order::getId), OrderId.empty())
+                    .set(field(Order::getUserId), user.getId())
+                    .create();
         }
 
         @Nested
@@ -69,13 +72,11 @@ class WhenSufficientPoint {
 
             @BeforeEach
             void setUp() {
-                userPoint = UserPoint.mappedBy(
-                        UserPointId.empty(),
-                        user.getId(),
-                        new UserPointBalance(new BigDecimal("50000")),
-                        null,
-                        null
-                );
+                userPoint = Instancio.of(UserPoint.class)
+                        .set(field("id"), UserPointId.empty())
+                        .set(field("userId"), user.getId())
+                        .set(field("balance"), new UserPointBalance(new BigDecimal(50_000)))
+                        .create();
 
                 payAmount = new PayAmount(new BigDecimal("20000"));
 
@@ -114,13 +115,11 @@ class WhenInsufficientPoint {
 
             @BeforeEach
             void setUp() {
-                userPoint = UserPoint.mappedBy(
-                        UserPointId.empty(),
-                        user.getId(),
-                        new UserPointBalance(new BigDecimal("50000")),
-                        null,
-                        null
-                );
+                userPoint = Instancio.of(UserPoint.class)
+                        .set(field("id"), UserPointId.empty())
+                        .set(field("userId"), user.getId())
+                        .set(field("balance"), new UserPointBalance(new BigDecimal(50_000)))
+                        .create();
 
                 largePayAmount = new PayAmount(new BigDecimal("100000"));
 
@@ -146,13 +145,11 @@ class WhenExactPoint {
 
             @BeforeEach
             void setUp() {
-                userPoint = UserPoint.mappedBy(
-                        UserPointId.empty(),
-                        user.getId(),
-                        new UserPointBalance(new BigDecimal("50000")),
-                        null,
-                        null
-                );
+                userPoint = Instancio.of(UserPoint.class)
+                        .set(field("id"), UserPointId.empty())
+                        .set(field("userId"), user.getId())
+                        .set(field("balance"), new UserPointBalance(new BigDecimal(50_000)))
+                        .create();
 
                 exactPayAmount = new PayAmount(new BigDecimal("50000"));
 
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAggregatorTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAggregatorTest.java
index 674fa3cfc..cc5ce1440 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAggregatorTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAggregatorTest.java
@@ -2,9 +2,11 @@
 
 import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.vo.OrderId;
+import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.product.vo.ProductId;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -17,6 +19,7 @@
 import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
 import static org.mockito.Mockito.when;
 
 @DisplayName("OrderLineAggregator 단위 테스트")
@@ -37,8 +40,18 @@ class AggregateMethod {
         @DisplayName("여러 상품의 가격을 합산한다")
         void aggregateMultipleProducts() {
             // given
-            OrderItem product1 = OrderItem.create(OrderId.empty(), new ProductId("1"), new Quantity(2L));
-            OrderItem product2 = OrderItem.create(OrderId.empty(), new ProductId("2"), new Quantity(3L));
+            OrderItem product1 = Instancio.of(OrderItem.class)
+                    .set(field(OrderItem::getId), OrderItemId.empty())
+                    .set(field(OrderItem::getProductId), new ProductId("1"))
+                    .set(field(OrderItem::getQuantity), new Quantity(2L))
+                    .set(field(OrderItem::getOrderId), OrderId.empty())
+                    .create();
+            OrderItem product2 = Instancio.of(OrderItem.class)
+                    .set(field(OrderItem::getId), OrderItemId.empty())
+                    .set(field(OrderItem::getProductId), new ProductId("2"))
+                    .set(field(OrderItem::getQuantity), new Quantity(3L))
+                    .set(field(OrderItem::getOrderId), OrderId.empty())
+                    .create();
             List<OrderItem> orderItems = List.of(product1, product2);
 
             when(orderLineAllocator.allocate(product1)).thenReturn(new BigDecimal("20000"));
@@ -55,7 +68,12 @@ void aggregateMultipleProducts() {
         @DisplayName("단일 상품의 가격을 반환한다")
         void aggregateSingleProduct() {
             // given
-            OrderItem orderItem = OrderItem.create(OrderId.empty(), new ProductId("1"), new Quantity(5L));
+            OrderItem orderItem = Instancio.of(OrderItem.class)
+                    .set(field(OrderItem::getId), OrderItemId.empty())
+                    .set(field(OrderItem::getProductId), new ProductId("1"))
+                    .set(field(OrderItem::getQuantity), new Quantity(5L))
+                    .set(field(OrderItem::getOrderId), OrderId.empty())
+                    .create();
             List<OrderItem> orderItems = List.of(orderItem);
 
             when(orderLineAllocator.allocate(orderItem)).thenReturn(new BigDecimal("50000"));
@@ -71,8 +89,18 @@ void aggregateSingleProduct() {
         @DisplayName("소수점이 포함된 가격을 정확하게 합산한다")
         void aggregateWithDecimalPrices() {
             // given
-            OrderItem product1 = OrderItem.create(OrderId.empty(), new ProductId("1"), new Quantity(1L));
-            OrderItem product2 = OrderItem.create(OrderId.empty(), new ProductId("2"), new Quantity(1L));
+            OrderItem product1 = Instancio.of(OrderItem.class)
+                    .set(field(OrderItem::getId), OrderItemId.empty())
+                    .set(field(OrderItem::getProductId), new ProductId("1"))
+                    .set(field(OrderItem::getQuantity), new Quantity(1L))
+                    .set(field(OrderItem::getOrderId), OrderId.empty())
+                    .create();
+            OrderItem product2 = Instancio.of(OrderItem.class)
+                    .set(field(OrderItem::getId), OrderItemId.empty())
+                    .set(field(OrderItem::getProductId), new ProductId("1"))
+                    .set(field(OrderItem::getQuantity), new Quantity(2L))
+                    .set(field(OrderItem::getOrderId), OrderId.empty())
+                    .create();
             List<OrderItem> orderItems = List.of(product1, product2);
 
             when(orderLineAllocator.allocate(product1)).thenReturn(new BigDecimal("10000.50"));
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
index 0a4692254..c2ccfc946 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderLineAllocatorTest.java
@@ -3,12 +3,14 @@
 import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.repository.OrderItemRepository;
 import com.loopers.core.domain.order.vo.OrderId;
+import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductPrice;
 import com.loopers.core.domain.product.vo.ProductStock;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -22,6 +24,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.instancio.Select.field;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -56,19 +59,18 @@ class WhenSufficientStock {
             void setUp() {
                 productId = new ProductId("1");
                 quantity = new Quantity(5L);
-                orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
-
-                product = Product.mappedBy(
-                        productId,
-                        null,
-                        null,
-                        new ProductPrice(new BigDecimal("10000")),
-                        new ProductStock(100L),
-                        null,
-                        null,
-                        null,
-                        null
-                );
+                orderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getProductId), productId)
+                        .set(field(OrderItem::getQuantity), quantity)
+                        .set(field(OrderItem::getOrderId), OrderId.empty())
+                        .create();
+
+                product = Instancio.of(Product.class)
+                        .set(field(Product::getId), productId)
+                        .set(field(Product::getPrice), new ProductPrice(new BigDecimal("10000")))
+                        .set(field(Product::getStock), new ProductStock(100L))
+                        .create();
 
                 when(productRepository.getByIdWithLock(productId)).thenReturn(product);
                 when(productRepository.save(any(Product.class))).thenReturn(product.decreaseStock(quantity));
@@ -101,19 +103,18 @@ class WhenInsufficientStock {
             void setUp() {
                 productId = new ProductId("1");
                 quantity = new Quantity(101L); // 100개보다 많음
-                orderItem = OrderItem.create(OrderId.empty(), productId, quantity);
-
-                product = Product.mappedBy(
-                        productId,
-                        null,
-                        null,
-                        new ProductPrice(new BigDecimal("10000")),
-                        new ProductStock(100L),
-                        null,
-                        null,
-                        null,
-                        null
-                );
+                orderItem = Instancio.of(OrderItem.class)
+                        .set(field(OrderItem::getId), OrderItemId.empty())
+                        .set(field(OrderItem::getProductId), productId)
+                        .set(field(OrderItem::getQuantity), quantity)
+                        .set(field(OrderItem::getOrderId), OrderId.empty())
+                        .create();
+
+                product = Instancio.of(Product.class)
+                        .set(field(Product::getId), productId)
+                        .set(field(Product::getPrice), new ProductPrice(new BigDecimal("10000")))
+                        .set(field(Product::getStock), new ProductStock(100L))
+                        .create();
 
                 when(productRepository.getByIdWithLock(productId)).thenReturn(product);
             }

From a4dcb22464973c701e82ec0ee72dcc28e98d8704 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 23:17:55 +0900
Subject: [PATCH 17/29] =?UTF-8?q?feature=20:=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20Fix?=
 =?UTF-8?q?ture=20Instancio=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/brand/BrandQueryServiceTest.java  |  13 +-
 .../product/ProductQueryServiceTest.java      | 142 +++++++-----
 .../ProductLikeQueryServiceTest.java          | 120 ++++++----
 .../productlike/ProductLikeServiceTest.java   | 211 ++++++++++--------
 4 files changed, 285 insertions(+), 201 deletions(-)

diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
index 930205c37..f496fb60c 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/brand/BrandQueryServiceTest.java
@@ -8,6 +8,7 @@
 import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.brand.query.GetBrandQuery;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -16,6 +17,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.instancio.Select.field;
 
 class BrandQueryServiceTest extends IntegrationTest {
 
@@ -37,10 +39,13 @@ class 브랜드가_존재하는_경우 {
 
             @BeforeEach
             void setUp() {
-                brandId = brandRepository.save(Brand.create(
-                        new BrandName("loopers"),
-                        new BrandDescription("education brand")
-                )).getId();
+                brandId = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("loopers"))
+                                .set(field(Brand::getDescription), new BrandDescription("education brand"))
+                                .create()
+                ).getId();
             }
 
             @Test
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
index 85f7f6963..c4e0a3edb 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
@@ -17,6 +17,12 @@
 import com.loopers.core.service.product.query.GetProductDetailQuery;
 import com.loopers.core.service.product.query.GetProductListQuery;
 import com.loopers.core.service.product.query.GetProductQuery;
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.product.vo.ProductId;
+import com.loopers.core.domain.product.vo.ProductLikeCount;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -28,6 +34,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 class ProductQueryServiceTest extends IntegrationTest {
 
@@ -49,16 +56,22 @@ class 상품_리스트_조회_시 {
 
         @BeforeEach
         void setUp() {
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("loopers"),
-                    new BrandDescription("education brand")
-            ));
+            Brand brand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("loopers"))
+                            .set(field(Brand::getDescription), new BrandDescription("education brand"))
+                            .create()
+            );
             savedBrandId = brand.getId();
 
-            Brand otherBrand = brandRepository.save(Brand.create(
-                    new BrandName("other"),
-                    new BrandDescription("other brand")
-            ));
+            Brand otherBrand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("other"))
+                            .set(field(Brand::getDescription), new BrandDescription("other brand"))
+                            .create()
+            );
             otherBrandId = otherBrand.getId();
         }
 
@@ -69,28 +82,31 @@ class 조건에_맞는_상품이_존재하는_경우 {
             @BeforeEach
             void setUp() {
                 productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000)),
-                                new ProductStock(100_000L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("MacBook Pro"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .create()
                 );
                 productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000)),
-                                new ProductStock(100_000L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("iPad Air"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(800_000)))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .create()
                 );
                 productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("iPhone 15"),
-                                new ProductPrice(new BigDecimal(1_500_000)),
-                                new ProductStock(100_000L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("iPhone 15"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_500_000)))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .create()
                 );
             }
 
@@ -199,12 +215,13 @@ void setUp() {
             @DisplayName("특정 브랜드의 상품만 조회된다.")
             void 특정_브랜드의_상품만_조회된다() {
                 productRepository.save(
-                        Product.create(
-                                otherBrandId,
-                                new ProductName("Samsung Galaxy"),
-                                new ProductPrice(new BigDecimal(1_000_000)),
-                                new ProductStock(100_000L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), otherBrandId)
+                                .set(field(Product::getName), new ProductName("Samsung Galaxy"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_000_000)))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .create()
                 );
 
                 GetProductListQuery query = new GetProductListQuery(
@@ -265,19 +282,23 @@ class ID로_상품_조회_시 {
 
         @BeforeEach
         void setUp() {
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("loopers"),
-                    new BrandDescription("education brand")
-            ));
+            Brand brand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("loopers"))
+                            .set(field(Brand::getDescription), new BrandDescription("education brand"))
+                            .create()
+            );
             savedBrandId = brand.getId();
 
             Product product = productRepository.save(
-                    Product.create(
-                            savedBrandId,
-                            new ProductName("MacBook Pro"),
-                            new ProductPrice(new BigDecimal(1_300_000)),
-                            new ProductStock(100_000L)
-                    )
+                    Instancio.of(Product.class)
+                            .set(field(Product::getId), ProductId.empty())
+                            .set(field(Product::getBrandId), savedBrandId)
+                            .set(field(Product::getName), new ProductName("MacBook Pro"))
+                            .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
+                            .set(field(Product::getStock), new ProductStock(100_000L))
+                            .create()
             );
             savedProductId = product.getId().value();
         }
@@ -326,19 +347,23 @@ class 상품_상세_조회_시 {
 
         @BeforeEach
         void setUp() {
-            savedBrand = brandRepository.save(Brand.create(
-                    new BrandName("loopers"),
-                    new BrandDescription("education brand")
-            ));
+            savedBrand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("loopers"))
+                            .set(field(Brand::getDescription), new BrandDescription("education brand"))
+                            .create()
+            );
             savedBrandId = savedBrand.getId();
 
             Product product = productRepository.save(
-                    Product.create(
-                            savedBrandId,
-                            new ProductName("MacBook Pro"),
-                            new ProductPrice(new BigDecimal(1_300_000)),
-                            new ProductStock(100_000L)
-                    )
+                    Instancio.of(Product.class)
+                            .set(field(Product::getId), ProductId.empty())
+                            .set(field(Product::getBrandId), savedBrandId)
+                            .set(field(Product::getName), new ProductName("MacBook Pro"))
+                            .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
+                            .set(field(Product::getStock), new ProductStock(100_000L))
+                            .create()
             );
             savedProductId = product.getId().value();
         }
@@ -414,12 +439,13 @@ class 브랜드가_존재하지_않는_경우 {
             void NotFoundException이_던져진다() {
                 // 먼저 상품을 생성하고, 브랜드를 삭제하는 시나리오
                 Product orphanProduct = productRepository.save(
-                        Product.create(
-                                new BrandId("99999"),
-                                new ProductName("Orphan Product"),
-                                new ProductPrice(new BigDecimal(100_000)),
-                                new ProductStock(100_000L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), new BrandId("99999"))
+                                .set(field(Product::getName), new ProductName("Orphan Product"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(100_000)))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .create()
                 );
 
                 GetProductDetailQuery query = new GetProductDetailQuery(orphanProduct.getId().value());
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
index f0d721aa1..0f8db8bdf 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeQueryServiceTest.java
@@ -2,20 +2,21 @@
 
 import com.loopers.core.domain.brand.Brand;
 import com.loopers.core.domain.brand.repository.BrandRepository;
+import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandId;
+import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
-import com.loopers.core.domain.product.vo.ProductName;
-import com.loopers.core.domain.product.vo.ProductPrice;
-import com.loopers.core.domain.product.vo.ProductStock;
+import com.loopers.core.domain.product.vo.*;
 import com.loopers.core.domain.productlike.LikeProductListView;
 import com.loopers.core.domain.productlike.ProductLike;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
+import com.loopers.core.domain.productlike.vo.ProductLikeId;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.productlike.query.GetLikeProductsListQuery;
@@ -27,7 +28,6 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
-import java.time.LocalDate;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
@@ -63,56 +63,77 @@ class 사용자가_좋아요한_상품이_존재하는_경우 {
 
             @BeforeEach
             void setUp() {
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("testUser"),
-                        new UserEmail("test@loopers.com"),
-                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("testUser"))
+                                .set(field(User::getEmail), new UserEmail("test@loopers.com"))
+                                .create()
+                );
                 savedUserIdentifier = user.getIdentifier().value();
                 savedBrandId = brandRepository.save(
                         Instancio.of(Brand.class)
-                                .set(field("id"), BrandId.empty())
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("Test Brand"))
+                                .set(field(Brand::getDescription), new BrandDescription("Test Description"))
                                 .create()
                 ).getId();
 
                 Product product1 = productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("MacBook Pro"),
-                                new ProductPrice(new BigDecimal(1_300_000)),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("MacBook Pro"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
+                                .set(field(Product::getStock), new ProductStock(10L))
+                                .set(field(Product::getLikeCount), ProductLikeCount.init())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 );
                 Product product2 = productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("iPad Air"),
-                                new ProductPrice(new BigDecimal(800_000)),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("iPad Air"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(800_000)))
+                                .set(field(Product::getStock), new ProductStock(10L))
+                                .set(field(Product::getLikeCount), ProductLikeCount.init())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 );
                 Product product3 = productRepository.save(
-                        Product.create(
-                                savedBrandId,
-                                new ProductName("Galaxy S24"),
-                                new ProductPrice(new BigDecimal(1_100_000)),
-                                new ProductStock(10L)
-                        )
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), savedBrandId)
+                                .set(field(Product::getName), new ProductName("Galaxy S24"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_100_000)))
+                                .set(field(Product::getStock), new ProductStock(10L))
+                                .set(field(Product::getLikeCount), ProductLikeCount.init())
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
+                                .create()
                 );
 
-                productLikeRepository.save(ProductLike.create(
-                        user.getId(),
-                        product1.getId()
-                ));
-                productLikeRepository.save(ProductLike.create(
-                        user.getId(),
-                        product2.getId()
-                ));
-                productLikeRepository.save(ProductLike.create(
-                        user.getId(),
-                        product3.getId()
-                ));
+                productLikeRepository.save(
+                        Instancio.of(ProductLike.class)
+                                .set(field(ProductLike::getId), ProductLikeId.empty())
+                                .set(field(ProductLike::getUserId), user.getId())
+                                .set(field(ProductLike::getProductId), product1.getId())
+                                .create()
+                );
+                productLikeRepository.save(
+                        Instancio.of(ProductLike.class)
+                                .set(field(ProductLike::getId), ProductLikeId.empty())
+                                .set(field(ProductLike::getUserId), user.getId())
+                                .set(field(ProductLike::getProductId), product2.getId())
+                                .create()
+                );
+                productLikeRepository.save(
+                        Instancio.of(ProductLike.class)
+                                .set(field(ProductLike::getId), ProductLikeId.empty())
+                                .set(field(ProductLike::getUserId), user.getId())
+                                .set(field(ProductLike::getProductId), product3.getId())
+                                .create()
+                );
             }
 
             @Test
@@ -230,12 +251,13 @@ class 사용자가_좋아요한_상품이_없는_경우 {
 
             @BeforeEach
             void setUp() {
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("testUser"),
-                        new UserEmail("test@loopers.com"),
-                        new UserBirthDay(LocalDate.of(2000, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("testUser"))
+                                .set(field(User::getEmail), new UserEmail("test@loopers.com"))
+                                .create()
+                );
                 savedUserIdentifier = user.getIdentifier().value();
             }
 
diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
index 9f44b92e4..5ae8cbece 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/productlike/ProductLikeServiceTest.java
@@ -11,11 +11,11 @@
 import com.loopers.core.domain.product.vo.*;
 import com.loopers.core.domain.productlike.ProductLike;
 import com.loopers.core.domain.productlike.repository.ProductLikeRepository;
+import com.loopers.core.domain.productlike.vo.ProductLikeId;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
@@ -29,7 +29,6 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.math.BigDecimal;
-import java.time.LocalDate;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -63,26 +62,34 @@ class 상품_좋아요_등록_시 {
 
         @BeforeEach
         void setUp() {
-            Brand brand = brandRepository.save(Brand.create(
-                    new BrandName("Apple"),
-                    new BrandDescription("Apple products")
-            ));
+            Brand brand = brandRepository.save(
+                    Instancio.of(Brand.class)
+                            .set(field(Brand::getId), BrandId.empty())
+                            .set(field(Brand::getName), new BrandName("Apple"))
+                            .set(field(Brand::getDescription), new BrandDescription("Apple products"))
+                            .create()
+            );
             brandId = brand.getId();
 
-            Product product = productRepository.save(Product.create(
-                    brandId,
-                    new ProductName("MacBook Pro"),
-                    new ProductPrice(new BigDecimal("1500000")),
-                    new ProductStock(100_000L)
-            ));
+            Product product = productRepository.save(
+                    Instancio.of(Product.class)
+                            .set(field(Product::getId), ProductId.empty())
+                            .set(field(Product::getBrandId), brandId)
+                            .set(field(Product::getName), new ProductName("MacBook Pro"))
+                            .set(field(Product::getPrice), new ProductPrice(new BigDecimal("1500000")))
+                            .set(field(Product::getStock), new ProductStock(100_000L))
+                            .set(field(Product::getLikeCount), ProductLikeCount.init())
+                            .create()
+            );
             productId = product.getId().value();
 
-            User user = userRepository.save(User.create(
-                    new UserIdentifier("user123"),
-                    new UserEmail("user@example.com"),
-                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                    UserGender.MALE
-            ));
+            User user = userRepository.save(
+                    Instancio.of(User.class)
+                            .set(field(User::getId), UserId.empty())
+                            .set(field(User::getIdentifier), UserIdentifier.create("user123"))
+                            .set(field(User::getEmail), new UserEmail("user@example.com"))
+                            .create()
+            );
             userIdentifier = user.getIdentifier().value();
         }
 
@@ -138,12 +145,14 @@ class 동시에_여러_사용자가_좋아요를_누른_경우 {
                 ConcurrencyTestUtil.executeInParallelWithoutResult(
                         requestCount,
                         index -> {
-                            User user = userRepository.save(User.create(
-                                    new UserIdentifier("user_" + index),
-                                    new UserEmail("user" + index + "@example.com"),
-                                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                                    UserGender.MALE
-                            ));
+                            // 동시성 테스트는 domain factory method 사용
+                            User user = userRepository.save(
+                                    Instancio.of(User.class)
+                                            .set(field(User::getId), UserId.empty())
+                                            .set(field(User::getIdentifier), new UserIdentifier("user_" + index))
+                                            .set(field(User::getEmail), new UserEmail("user_" + index + "@example.com"))
+                                            .create()
+                            );
                             productLikeService.like(new ProductLikeCommand(user.getIdentifier().value(), productId));
                         }
                 );
@@ -228,26 +237,34 @@ class 상품이_존재하고_이미_좋아요를_누른_경우 {
 
             @BeforeEach
             void setUp() {
-                Brand brand = brandRepository.save(Brand.create(
-                        new BrandName("Apple"),
-                        new BrandDescription("Apple products")
-                ));
+                Brand brand = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("Apple"))
+                                .set(field(Brand::getDescription), new BrandDescription("Apple products"))
+                                .create()
+                );
                 brandId = brand.getId();
 
-                Product product = productRepository.save(Product.create(
-                        brandId,
-                        new ProductName("MacBook Pro"),
-                        new ProductPrice(new BigDecimal("1500000")),
-                        new ProductStock(100_000L)
-                ));
+                Product product = productRepository.save(
+                        Instancio.of(Product.class)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getBrandId), brandId)
+                                .set(field(Product::getName), new ProductName("MacBook Pro"))
+                                .set(field(Product::getPrice), new ProductPrice(new BigDecimal("1500000")))
+                                .set(field(Product::getStock), new ProductStock(100_000L))
+                                .set(field(Product::getLikeCount), ProductLikeCount.init())
+                                .create()
+                );
                 productId = product.getId().value();
 
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("user123"),
-                        new UserEmail("user@example.com"),
-                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("user123"))
+                                .set(field(User::getEmail), new UserEmail("user@example.com"))
+                                .create()
+                );
                 userIdentifier = user.getIdentifier().value();
 
                 ProductLikeCommand likeCommand = new ProductLikeCommand(userIdentifier, productId);
@@ -276,27 +293,31 @@ class 동시에_하나의_사용자가_여러번_취소하면 {
 
             @BeforeEach
             void setUp() {
-                Brand brand = brandRepository.save(Brand.create(
-                        new BrandName("Apple"),
-                        new BrandDescription("Apple products")
-                ));
+                Brand brand = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("Apple"))
+                                .set(field(Brand::getDescription), new BrandDescription("Apple products"))
+                                .create()
+                );
                 brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)
-                                .set(field("brandId"), brandId)
-                                .set(field("id"), ProductId.empty())
-                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .set(field(Product::getBrandId), brandId)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getLikeCount), new ProductLikeCount(100L))
                                 .create()
                 );
                 productId = product.getId().value();
 
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("user123"),
-                        new UserEmail("user@example.com"),
-                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("user123"))
+                                .set(field(User::getEmail), new UserEmail("user@example.com"))
+                                .create()
+                );
                 userIdentifier = user.getIdentifier().value();
 
                 productLikeRepository.save(
@@ -329,17 +350,20 @@ class 동시에_여러_사용자가_취소하면 {
 
             @BeforeEach
             void setUp() {
-                Brand brand = brandRepository.save(Brand.create(
-                        new BrandName("Apple"),
-                        new BrandDescription("Apple products")
-                ));
+                Brand brand = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("Apple"))
+                                .set(field(Brand::getDescription), new BrandDescription("Apple products"))
+                                .create()
+                );
                 brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)
-                                .set(field("brandId"), brandId)
-                                .set(field("id"), ProductId.empty())
-                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .set(field(Product::getBrandId), brandId)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getLikeCount), new ProductLikeCount(100L))
                                 .create()
                 );
                 productId = product.getId().value();
@@ -353,17 +377,19 @@ void setUp() {
                 ConcurrencyTestUtil.executeInParallelWithoutResult(
                         requestCount,
                         index -> {
-                            User user = userRepository.save(User.create(
-                                    new UserIdentifier("user_" + index),
-                                    new UserEmail("user" + index + "@example.com"),
-                                    new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                                    UserGender.MALE
-                            ));
+                            User user = userRepository.save(
+                                    Instancio.of(User.class)
+                                            .set(field(User::getId), UserId.empty())
+                                            .set(field(User::getIdentifier), new UserIdentifier("user_" + index))
+                                            .set(field(User::getEmail), new UserEmail("user_" + index + "@example.com"))
+                                            .create()
+                            );
                             productLikeRepository.save(
-                                    ProductLike.create(
-                                            user.getId(),
-                                            new ProductId(productId)
-                                    )
+                                    Instancio.of(ProductLike.class)
+                                            .set(field(ProductLike::getId), ProductLikeId.empty())
+                                            .set(field(ProductLike::getUserId), user.getId())
+                                            .set(field(ProductLike::getProductId), new ProductId(productId))
+                                            .create()
                             );
                             productLikeService.unlike(new ProductUnlikeCommand(user.getIdentifier().value(), productId));
                         }
@@ -379,26 +405,30 @@ class 상품이_존재하고_좋아요를_누른_적이_없는_경우 {
 
             @BeforeEach
             void setUp() {
-                Brand brand = brandRepository.save(Brand.create(
-                        new BrandName("Apple"),
-                        new BrandDescription("Apple products")
-                ));
+                Brand brand = brandRepository.save(
+                        Instancio.of(Brand.class)
+                                .set(field(Brand::getId), BrandId.empty())
+                                .set(field(Brand::getName), new BrandName("Apple"))
+                                .set(field(Brand::getDescription), new BrandDescription("Apple products"))
+                                .create()
+                );
                 brandId = brand.getId();
 
                 Product product = productRepository.save(
                         Instancio.of(Product.class)
-                                .set(field("brandId"), brandId)
-                                .set(field("id"), ProductId.empty())
-                                .set(field("likeCount"), new ProductLikeCount(100L))
+                                .set(field(Product::getBrandId), brandId)
+                                .set(field(Product::getId), ProductId.empty())
+                                .set(field(Product::getLikeCount), new ProductLikeCount(100L))
                                 .create()
                 );
                 productId = product.getId().value();
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("user123"),
-                        new UserEmail("user@example.com"),
-                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("user123"))
+                                .set(field(User::getEmail), new UserEmail("user@example.com"))
+                                .create()
+                );
                 userIdentifier = user.getIdentifier().value();
             }
 
@@ -426,12 +456,13 @@ class 상품이_존재하지_않는_경우 {
 
             @BeforeEach
             void setUp() {
-                User user = userRepository.save(User.create(
-                        new UserIdentifier("user123"),
-                        new UserEmail("user@example.com"),
-                        new UserBirthDay(LocalDate.of(1990, 1, 1)),
-                        UserGender.MALE
-                ));
+                User user = userRepository.save(
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), UserIdentifier.create("user123"))
+                                .set(field(User::getEmail), new UserEmail("user@example.com"))
+                                .create()
+                );
                 userIdentifier = user.getIdentifier().value();
             }
 

From 121c1160ac2ac93198b25e95226c6e2134ee17e8 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 17 Nov 2025 23:26:20 +0900
Subject: [PATCH 18/29] =?UTF-8?q?feature=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?=
 =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?Fixture=20Instancio=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../product/ProductQueryServiceTest.java      | 10 ++---
 .../user/JoinUserServiceIntegrationTest.java  | 17 +++----
 .../UserPointQueryServiceIntegrationTest.java | 27 ++++++-----
 .../user/UserPointServiceIntegrationTest.java | 45 +++++++++++--------
 .../service/user/UserQueryServiceTest.java    | 18 ++++----
 5 files changed, 67 insertions(+), 50 deletions(-)

diff --git a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
index c4e0a3edb..a38cf3339 100644
--- a/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
+++ b/core/service/product-service/src/test/java/com/loopers/core/service/product/ProductQueryServiceTest.java
@@ -5,11 +5,13 @@
 import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.ProductDetail;
 import com.loopers.core.domain.product.ProductListView;
 import com.loopers.core.domain.product.repository.ProductRepository;
+import com.loopers.core.domain.product.vo.ProductId;
 import com.loopers.core.domain.product.vo.ProductName;
 import com.loopers.core.domain.product.vo.ProductPrice;
 import com.loopers.core.domain.product.vo.ProductStock;
@@ -17,11 +19,6 @@
 import com.loopers.core.service.product.query.GetProductDetailQuery;
 import com.loopers.core.service.product.query.GetProductListQuery;
 import com.loopers.core.service.product.query.GetProductQuery;
-import com.loopers.core.domain.common.vo.CreatedAt;
-import com.loopers.core.domain.common.vo.UpdatedAt;
-import com.loopers.core.domain.common.vo.DeletedAt;
-import com.loopers.core.domain.product.vo.ProductId;
-import com.loopers.core.domain.product.vo.ProductLikeCount;
 import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -88,6 +85,7 @@ void setUp() {
                                 .set(field(Product::getName), new ProductName("MacBook Pro"))
                                 .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_300_000)))
                                 .set(field(Product::getStock), new ProductStock(100_000L))
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
                                 .create()
                 );
                 productRepository.save(
@@ -97,6 +95,7 @@ void setUp() {
                                 .set(field(Product::getName), new ProductName("iPad Air"))
                                 .set(field(Product::getPrice), new ProductPrice(new BigDecimal(800_000)))
                                 .set(field(Product::getStock), new ProductStock(100_000L))
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
                                 .create()
                 );
                 productRepository.save(
@@ -106,6 +105,7 @@ void setUp() {
                                 .set(field(Product::getName), new ProductName("iPhone 15"))
                                 .set(field(Product::getPrice), new ProductPrice(new BigDecimal(1_500_000)))
                                 .set(field(Product::getStock), new ProductStock(100_000L))
+                                .set(field(Product::getDeletedAt), DeletedAt.empty())
                                 .create()
                 );
             }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
index db6d8e2ea..979db3d74 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -2,20 +2,22 @@
 
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.command.JoinUserCommand;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import static org.instancio.Select.field;
+
 import java.util.Optional;
 
 class JoinUserServiceIntegrationTest extends IntegrationTest {
@@ -61,12 +63,11 @@ class 이미_존재하는_회원의_ID인_경우 {
             @BeforeEach
             void setUp() {
                 userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
             }
 
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
index bbb01bdca..b3f196b36 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
@@ -4,20 +4,20 @@
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
-import com.loopers.core.domain.user.vo.UserEmail;
-import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.domain.user.vo.*;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.query.GetUserPointQuery;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import static org.instancio.Select.field;
+
 class UserPointQueryServiceIntegrationTest extends IntegrationTest {
 
     @Autowired
@@ -56,15 +56,20 @@ class 해당_ID의_회원이_존재할_경우 {
             @BeforeEach
             void setUp() {
                 savedUser = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
 
-                savedUserPoint = userPointRepository.save(UserPoint.create(savedUser.getId()));
+                savedUserPoint = userPointRepository.save(
+                        Instancio.of(UserPoint.class)
+                                .set(field(UserPoint::getId), UserPointId.empty())
+                                .set(field(UserPoint::getUserId), savedUser.getId())
+                                .set(field(UserPoint::getBalance), UserPointBalance.init())
+                                .create()
+                );
             }
 
             @Test
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
index 3a3d3f973..352a61bd4 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -5,13 +5,11 @@
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
-import com.loopers.core.domain.user.vo.UserEmail;
-import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.domain.user.vo.*;
 import com.loopers.core.service.ConcurrencyTestUtil;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.command.UserPointChargeCommand;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,6 +21,7 @@
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
 
 class UserPointServiceIntegrationTest extends IntegrationTest {
 
@@ -50,14 +49,19 @@ class 정상_충전 {
             @BeforeEach
             void setUp() {
                 user = userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
+                );
+                userPointRepository.save(
+                        Instancio.of(UserPoint.class)
+                                .set(field(UserPoint::getId), UserPointId.empty())
+                                .set(field(UserPoint::getUserId), user.getId())
+                                .set(field(UserPoint::getBalance), UserPointBalance.init())
+                                .create()
                 );
-                userPointRepository.save(UserPoint.create(user.getId()));
             }
 
             @Test
@@ -107,14 +111,19 @@ class 동시_포인트_적립_정합성 {
                 @BeforeEach
                 void setUp() {
                     user = userRepository.save(
-                            User.create(
-                                    UserIdentifier.create("kilian"),
-                                    UserEmail.create("kilian@gmail.com"),
-                                    UserBirthDay.create("1997-10-08"),
-                                    UserGender.create("MALE")
-                            )
+                            Instancio.of(User.class)
+                                    .set(field(User::getId), UserId.empty())
+                                    .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                    .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                    .create()
+                    );
+                    userPointRepository.save(
+                            Instancio.of(UserPoint.class)
+                                    .set(field(UserPoint::getId), UserPointId.empty())
+                                    .set(field(UserPoint::getUserId), user.getId())
+                                    .set(field(UserPoint::getBalance), UserPointBalance.init())
+                                    .create()
                     );
-                    userPointRepository.save(UserPoint.create(user.getId()));
                 }
 
                 @Test
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
index 099d6427f..a8564cbc3 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
@@ -3,19 +3,22 @@
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.query.GetUserQuery;
 import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
+import org.instancio.Instancio;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import static org.instancio.Select.field;
+
 class UserQueryServiceTest extends IntegrationTest {
 
     @Autowired
@@ -47,12 +50,11 @@ class ID에_해당하는_사용자가_존재할_경우 {
             @BeforeEach
             void setUp() {
                 userRepository.save(
-                        User.create(
-                                UserIdentifier.create("kilian"),
-                                UserEmail.create("kilian@gmail.com"),
-                                UserBirthDay.create("1997-10-08"),
-                                UserGender.create("MALE")
-                        )
+                        Instancio.of(User.class)
+                                .set(field(User::getId), UserId.empty())
+                                .set(field(User::getIdentifier), new UserIdentifier("kilian"))
+                                .set(field(User::getEmail), new UserEmail("kilian@gmail.com"))
+                                .create()
                 );
             }
 
@@ -65,7 +67,7 @@ void setUp() {
                     softly.assertThat(user).isNotNull();
                     softly.assertThat(user.getIdentifier().value()).isEqualTo("kilian");
                     softly.assertThat(user.getEmail().value()).isEqualTo("kilian@gmail.com");
-                    softly.assertThat(user.getGender()).isEqualTo(UserGender.MALE);
+                    softly.assertThat(user.getGender()).isNotNull();
                 });
             }
         }

From 8ad0ccd9ba8d42e5f337a7ff89829344fd7732ca Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 18 Nov 2025 00:44:59 +0900
Subject: [PATCH 19/29] =?UTF-8?q?feature=20:=20=EC=BF=A0=ED=8F=B0=20?=
 =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=B0=8F=20=EC=98=81=EC=86=8D?=
 =?UTF-8?q?=EC=84=B1=20=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/domain/error/DomainErrorCode.java    |  3 +-
 .../core/domain/order/AbstractCoupon.java     | 52 ++++++++++
 .../domain/order/AmountDiscountCoupon.java    | 27 ++++++
 .../com/loopers/core/domain/order/Coupon.java |  5 +
 .../core/domain/order/DefaultCoupon.java      | 44 +++++++++
 .../core/domain/order/RateDiscountCoupon.java | 30 ++++++
 .../order/repository/CouponRepository.java    | 11 +++
 .../core/domain/order/type/CouponStatus.java  |  5 +
 .../order/vo/AmountDiscountCouponId.java      |  8 ++
 .../domain/order/vo/CouponDiscountAmount.java | 26 +++++
 .../domain/order/vo/CouponDiscountRate.java   | 27 ++++++
 .../core/domain/order/vo/CouponId.java        |  8 ++
 .../domain/order/vo/RateDiscountCouponId.java |  8 ++
 .../core/domain/order/DefaultCouponTest.java  | 33 +++++++
 .../order/vo/CouponDiscountAmountTest.java    | 85 +++++++++++++++++
 .../order/vo/CouponDiscountRateTest.java      | 94 +++++++++++++++++++
 .../AmountDiscountCouponJpaRepository.java    | 11 +++
 .../mysql/coupon/CouponJpaRepository.java     |  7 ++
 .../RateDiscountCouponJpaRepository.java      | 11 +++
 .../entity/AmountDiscountCouponEntity.java    | 52 ++++++++++
 .../mysql/coupon/entity/CouponEntity.java     | 75 +++++++++++++++
 .../entity/RateDiscountCouponEntity.java      | 52 ++++++++++
 .../coupon/impl/CouponRepositoryImpl.java     | 65 +++++++++++++
 23 files changed, 738 insertions(+), 1 deletion(-)
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/type/CouponStatus.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/vo/AmountDiscountCouponId.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/order/vo/RateDiscountCouponId.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/DefaultCouponTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountAmountTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountRateTest.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/AmountDiscountCouponJpaRepository.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/RateDiscountCouponJpaRepository.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/AmountDiscountCouponEntity.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/RateDiscountCouponEntity.java
 create mode 100644 core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 0d9be76cc..d88e0f904 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -17,7 +17,8 @@ public enum DomainErrorCode {
     CANNOT_CHARGE_POINTS_NEGATIVE(1009, "음수 포인트를 충전할 수 없습니다."),
     PRODUCT_OUT_OF_STOCK(1010, "상품의 재고가 부족합니다."),
     COULD_NOT_BE_NEGATIVE(1011, "%s는(은) 음수가 될 수 없습니다."),
-    NOT_ENOUGH_USER_POINT_BALANCE(1012, "사용자의 포인트 잔액이 충분하지 않습니다.");
+    NOT_ENOUGH_USER_POINT_BALANCE(1012, "사용자의 포인트 잔액이 충분하지 않습니다."),
+    NOT_SUPPORTED_COUPON_TYPE(1013, "지원하지 않는 쿠폰 유형입니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
new file mode 100644
index 000000000..70c41b07e
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
@@ -0,0 +1,52 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.order.type.CouponStatus;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.user.vo.UserId;
+import lombok.Getter;
+
+@Getter
+public abstract class AbstractCoupon implements Coupon {
+
+    protected final CouponId couponId;
+
+    protected final UserId userId;
+
+    protected final CouponStatus status;
+
+    protected final CreatedAt createdAt;
+
+    protected final UpdatedAt updatedAt;
+
+    protected final DeletedAt deletedAt;
+
+    protected AbstractCoupon(
+            CouponId couponId,
+            UserId userId,
+            CouponStatus status,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt,
+            DeletedAt deletedAt
+    ) {
+        this.couponId = couponId;
+        this.userId = userId;
+        this.status = status;
+        this.createdAt = createdAt;
+        this.updatedAt = updatedAt;
+        this.deletedAt = deletedAt;
+    }
+
+    protected AbstractCoupon(
+            AbstractCoupon abstractCoupon
+    ) {
+        this.couponId = abstractCoupon.couponId;
+        this.userId = abstractCoupon.userId;
+        this.status = abstractCoupon.status;
+        this.createdAt = abstractCoupon.createdAt;
+        this.updatedAt = abstractCoupon.updatedAt;
+        this.deletedAt = abstractCoupon.deletedAt;
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
new file mode 100644
index 000000000..68d5db904
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
@@ -0,0 +1,27 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.order.vo.AmountDiscountCouponId;
+import com.loopers.core.domain.order.vo.CouponDiscountAmount;
+import lombok.Getter;
+
+@Getter
+public class AmountDiscountCoupon extends AbstractCoupon {
+
+    private final AmountDiscountCouponId id;
+
+    private final CouponDiscountAmount amount;
+
+    private AmountDiscountCoupon(AbstractCoupon abstractCoupon, AmountDiscountCouponId id, CouponDiscountAmount amount) {
+        super(abstractCoupon);
+        this.id = id;
+        this.amount = amount;
+    }
+
+    public static AmountDiscountCoupon create(AbstractCoupon abstractCoupon, CouponDiscountAmount amount) {
+        return new AmountDiscountCoupon(abstractCoupon, AmountDiscountCouponId.empty(), amount);
+    }
+
+    public static AmountDiscountCoupon mappedBy(AmountDiscountCouponId id, AbstractCoupon abstractCoupon, CouponDiscountAmount amount) {
+        return new AmountDiscountCoupon(abstractCoupon, id, amount);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
new file mode 100644
index 000000000..bf2db7696
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
@@ -0,0 +1,5 @@
+package com.loopers.core.domain.order;
+
+public interface Coupon {
+
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
new file mode 100644
index 000000000..3acfa7b32
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
@@ -0,0 +1,44 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.order.type.CouponStatus;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.user.vo.UserId;
+
+public class DefaultCoupon extends AbstractCoupon {
+
+    private DefaultCoupon(
+            CouponId couponId,
+            UserId userId,
+            CouponStatus status,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt,
+            DeletedAt deletedAt
+    ) {
+        super(couponId, userId, status, createdAt, updatedAt, deletedAt);
+    }
+
+    public static DefaultCoupon create(UserId userId) {
+        return new DefaultCoupon(
+                CouponId.empty(),
+                userId,
+                CouponStatus.AVAILABLE,
+                CreatedAt.now(),
+                UpdatedAt.now(),
+                DeletedAt.empty()
+        );
+    }
+
+    public static DefaultCoupon mappedBy(
+            CouponId couponId,
+            UserId userId,
+            CouponStatus status,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt,
+            DeletedAt deletedAt
+    ) {
+        return new DefaultCoupon(couponId, userId, status, createdAt, updatedAt, deletedAt);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
new file mode 100644
index 000000000..8bd367b0a
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
@@ -0,0 +1,30 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.order.vo.CouponDiscountRate;
+import com.loopers.core.domain.order.vo.RateDiscountCouponId;
+import lombok.Getter;
+
+@Getter
+public class RateDiscountCoupon extends AbstractCoupon {
+
+    private final RateDiscountCouponId id;
+
+    private final CouponDiscountRate rate;
+
+    private RateDiscountCoupon(
+            AbstractCoupon abstractCoupon,
+            RateDiscountCouponId id,
+            CouponDiscountRate rate) {
+        super(abstractCoupon);
+        this.id = id;
+        this.rate = rate;
+    }
+
+    public static RateDiscountCoupon create(AbstractCoupon abstractCoupon, CouponDiscountRate rate) {
+        return new RateDiscountCoupon(abstractCoupon, RateDiscountCouponId.empty(), rate);
+    }
+
+    public static RateDiscountCoupon mappedBy(RateDiscountCouponId id, AbstractCoupon abstractCoupon, CouponDiscountRate rate) {
+        return new RateDiscountCoupon(abstractCoupon, id, rate);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
new file mode 100644
index 000000000..481c84875
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.core.domain.order.repository;
+
+import com.loopers.core.domain.order.Coupon;
+import com.loopers.core.domain.order.vo.CouponId;
+
+public interface CouponRepository {
+
+    Coupon save(Coupon coupon);
+
+    Coupon getById(CouponId couponId);
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/type/CouponStatus.java b/core/domain/src/main/java/com/loopers/core/domain/order/type/CouponStatus.java
new file mode 100644
index 000000000..6e2196b36
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/type/CouponStatus.java
@@ -0,0 +1,5 @@
+package com.loopers.core.domain.order.type;
+
+public enum CouponStatus {
+    USED, EXPIRED, AVAILABLE
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/AmountDiscountCouponId.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/AmountDiscountCouponId.java
new file mode 100644
index 000000000..0519f1988
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/AmountDiscountCouponId.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.order.vo;
+
+public record AmountDiscountCouponId(String value) {
+
+    public static AmountDiscountCouponId empty() {
+        return new AmountDiscountCouponId(null);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
new file mode 100644
index 000000000..1f855a77c
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
@@ -0,0 +1,26 @@
+package com.loopers.core.domain.order.vo;
+
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+public record CouponDiscountAmount(BigDecimal value) {
+
+    private static final String FILED_NAME = "쿠폰 할인금액";
+
+    public CouponDiscountAmount {
+        validateNotNull(value);
+        validateNotNegative(value);
+    }
+
+    private static void validateNotNull(BigDecimal value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage(FILED_NAME));
+    }
+
+    private static void validateNotNegative(BigDecimal value) {
+        if (value.signum() < 0) {
+            throw new IllegalArgumentException(DomainErrorCode.negativeMessage(FILED_NAME));
+        }
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
new file mode 100644
index 000000000..0cdc8fb0c
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
@@ -0,0 +1,27 @@
+package com.loopers.core.domain.order.vo;
+
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+public record CouponDiscountRate(BigDecimal value) {
+
+    private static final BigDecimal MIN_RATE = BigDecimal.ZERO;
+    private static final BigDecimal MAX_RATE = new BigDecimal("100.00");
+
+    public CouponDiscountRate {
+        validateNotNull(value);
+        validateRange(value);
+    }
+
+    private static void validateNotNull(BigDecimal value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("쿠폰 할인율"));
+    }
+
+    private static void validateRange(BigDecimal value) {
+        if (value.compareTo(MIN_RATE) < 0 || value.compareTo(MAX_RATE) > 0) {
+            throw new IllegalArgumentException("할인율은 0~100 사이여야 합니다");
+        }
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
new file mode 100644
index 000000000..bd0cd8ba7
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.order.vo;
+
+public record CouponId(String value) {
+
+    public static CouponId empty() {
+        return new CouponId(null);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/RateDiscountCouponId.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/RateDiscountCouponId.java
new file mode 100644
index 000000000..2466d13ba
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/RateDiscountCouponId.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.order.vo;
+
+public record RateDiscountCouponId(String value) {
+
+    public static RateDiscountCouponId empty() {
+        return new RateDiscountCouponId(null);
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/DefaultCouponTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/DefaultCouponTest.java
new file mode 100644
index 000000000..7fa86cd20
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/DefaultCouponTest.java
@@ -0,0 +1,33 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.user.vo.UserId;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static com.loopers.core.domain.order.type.CouponStatus.AVAILABLE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DefaultCouponTest {
+
+    @Nested
+    @DisplayName("create()")
+    class CreateMethod {
+
+        @Nested
+        @DisplayName("정상 생성되었을때")
+        class DefaultValue {
+
+            @Test
+            @DisplayName("기본값을 가진다.")
+            void hasDefaultValue() {
+                DefaultCoupon coupon = DefaultCoupon.create(
+                        new UserId("1")
+                );
+
+                assertThat(coupon.status).isEqualTo(AVAILABLE);
+            }
+        }
+    }
+
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountAmountTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountAmountTest.java
new file mode 100644
index 000000000..b00869939
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountAmountTest.java
@@ -0,0 +1,85 @@
+package com.loopers.core.domain.order.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigDecimal;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("쿠폰 할인금액")
+class CouponDiscountAmountTest {
+
+    @Nested
+    @DisplayName("생성자")
+    class Constructor {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            static Stream<BigDecimal> validDiscountAmounts() {
+                return Stream.of(
+                        BigDecimal.ZERO,
+                        new BigDecimal(1),
+                        new BigDecimal(1000),
+                        new BigDecimal(10000),
+                        new BigDecimal("50000.50")
+                );
+            }
+
+            @ParameterizedTest
+            @MethodSource("validDiscountAmounts")
+            @DisplayName("CouponDisCountAmount 객체를 생성한다")
+            void 객체_생성(BigDecimal validAmount) {
+                // when
+                CouponDiscountAmount discountAmount = new CouponDiscountAmount(validAmount);
+
+                // then
+                assertThat(discountAmount.value()).isEqualByComparingTo(validAmount);
+            }
+        }
+
+        @Nested
+        @DisplayName("값이 음수인 경우")
+        class 값이_음수인_경우 {
+
+            static Stream<BigDecimal> invalidDiscountAmounts() {
+                return Stream.of(
+                        new BigDecimal(-1),
+                        new BigDecimal(-100),
+                        new BigDecimal("-10000.50")
+                );
+            }
+
+            @ParameterizedTest
+            @MethodSource("invalidDiscountAmounts")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(BigDecimal invalidAmount) {
+                // when & then
+                assertThatThrownBy(() -> new CouponDiscountAmount(invalidAmount))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("쿠폰 할인금액는(은) 음수가 될 수 없습니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("값이 null인 경우")
+        class 값이_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException이 발생한다")
+            void NullPointerException이_발생한다() {
+                // when & then
+                assertThatThrownBy(() -> new CouponDiscountAmount(null))
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("쿠폰 할인금액는(은) Null이 될 수 없습니다.");
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountRateTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountRateTest.java
new file mode 100644
index 000000000..b457d4276
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/vo/CouponDiscountRateTest.java
@@ -0,0 +1,94 @@
+package com.loopers.core.domain.order.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("쿠폰 할인율")
+class CouponDiscountRateTest {
+
+    @Nested
+    @DisplayName("쿠폰 할인율 생성 시")
+    class 쿠폰_할인율_생성 {
+
+        @Nested
+        @DisplayName("값이 null인 경우")
+        class 값이_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException이 발생한다.")
+            void NullPointerException이_발생한다() {
+                assertThatThrownBy(() -> new CouponDiscountRate(null))
+                        .isInstanceOf(NullPointerException.class);
+            }
+        }
+
+        @Nested
+        @DisplayName("값이 0 미만인 경우")
+        class 값이_0_미만인_경우 {
+
+            @Test
+            @DisplayName("IllegalArgumentException이 발생한다.")
+            void IllegalArgumentException이_발생한다() {
+                assertThatThrownBy(() -> new CouponDiscountRate(new BigDecimal("-0.01")))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("할인율은 0~100 사이여야 합니다");
+            }
+        }
+
+        @Nested
+        @DisplayName("값이 100을 초과하는 경우")
+        class 값이_100을_초과하는_경우 {
+
+            @Test
+            @DisplayName("IllegalArgumentException이 발생한다.")
+            void IllegalArgumentException이_발생한다() {
+                assertThatThrownBy(() -> new CouponDiscountRate(new BigDecimal("100.01")))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("할인율은 0~100 사이여야 합니다");
+            }
+        }
+
+        @Nested
+        @DisplayName("유효한 할인율이 주어진 경우")
+        class 유효한_할인율이_주어진_경우 {
+
+            @Test
+            @DisplayName("쿠폰 할인율이 생성된다.")
+            void 쿠폰_할인율이_생성된다() {
+                CouponDiscountRate rate = new CouponDiscountRate(new BigDecimal("15.50"));
+
+                assertThat(rate.value()).isEqualByComparingTo(new BigDecimal("15.50"));
+            }
+
+            @Test
+            @DisplayName("최소값(0)으로 생성된다.")
+            void 최소값으로_생성된다() {
+                CouponDiscountRate rate = new CouponDiscountRate(BigDecimal.ZERO);
+
+                assertThat(rate.value()).isEqualByComparingTo(BigDecimal.ZERO);
+            }
+
+            @Test
+            @DisplayName("최대값(100)으로 생성된다.")
+            void 최대값으로_생성된다() {
+                CouponDiscountRate rate = new CouponDiscountRate(new BigDecimal("100.00"));
+
+                assertThat(rate.value()).isEqualByComparingTo(new BigDecimal("100.00"));
+            }
+
+            @Test
+            @DisplayName("정수값으로 생성된다.")
+            void 정수값으로_생성된다() {
+                CouponDiscountRate rate = new CouponDiscountRate(new BigDecimal("10"));
+
+                assertThat(rate.value()).isEqualByComparingTo(new BigDecimal("10"));
+            }
+        }
+    }
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/AmountDiscountCouponJpaRepository.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/AmountDiscountCouponJpaRepository.java
new file mode 100644
index 000000000..3a1b0c6aa
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/AmountDiscountCouponJpaRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.core.infra.database.mysql.coupon;
+
+import com.loopers.core.infra.database.mysql.coupon.entity.AmountDiscountCouponEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface AmountDiscountCouponJpaRepository extends JpaRepository<AmountDiscountCouponEntity, Long> {
+
+    Optional<AmountDiscountCouponEntity> findByCouponId(Long couponId);
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
new file mode 100644
index 000000000..d6548a914
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
@@ -0,0 +1,7 @@
+package com.loopers.core.infra.database.mysql.coupon;
+
+import com.loopers.core.infra.database.mysql.coupon.entity.CouponEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CouponJpaRepository extends JpaRepository<CouponEntity, Long> {
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/RateDiscountCouponJpaRepository.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/RateDiscountCouponJpaRepository.java
new file mode 100644
index 000000000..0766204f2
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/RateDiscountCouponJpaRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.core.infra.database.mysql.coupon;
+
+import com.loopers.core.infra.database.mysql.coupon.entity.RateDiscountCouponEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface RateDiscountCouponJpaRepository extends JpaRepository<RateDiscountCouponEntity, Long> {
+
+    Optional<RateDiscountCouponEntity> findByCouponId(Long couponId);
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/AmountDiscountCouponEntity.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/AmountDiscountCouponEntity.java
new file mode 100644
index 000000000..fa4f26f33
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/AmountDiscountCouponEntity.java
@@ -0,0 +1,52 @@
+package com.loopers.core.infra.database.mysql.coupon.entity;
+
+import com.loopers.core.domain.order.AmountDiscountCoupon;
+import com.loopers.core.domain.order.vo.AmountDiscountCouponId;
+import com.loopers.core.domain.order.vo.CouponDiscountAmount;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+@Entity
+@Table(
+        name = "amount_discount_coupon",
+        indexes = {
+                @Index(name = "idx_amount_discount_coupon_coupon_id", columnList = "coupon_id"),
+        }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class AmountDiscountCouponEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(nullable = false)
+    private Long couponId;
+
+    @Column(nullable = false, precision = 19, scale = 2)
+    private BigDecimal amount;
+
+    public static AmountDiscountCouponEntity of(CouponEntity couponEntity, AmountDiscountCoupon amountDiscountCoupon) {
+        return new AmountDiscountCouponEntity(
+                Optional.ofNullable(amountDiscountCoupon.getId().value())
+                        .map(Long::parseLong)
+                        .orElse(null),
+                couponEntity.getId(),
+                amountDiscountCoupon.getAmount().value()
+        );
+    }
+
+    public AmountDiscountCoupon to(CouponEntity couponEntity) {
+        return AmountDiscountCoupon.mappedBy(
+                new AmountDiscountCouponId(this.id.toString()),
+                couponEntity.to(),
+                new CouponDiscountAmount(this.amount)
+        );
+    }
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
new file mode 100644
index 000000000..671b63108
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
@@ -0,0 +1,75 @@
+package com.loopers.core.infra.database.mysql.coupon.entity;
+
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.order.AbstractCoupon;
+import com.loopers.core.domain.order.DefaultCoupon;
+import com.loopers.core.domain.order.type.CouponStatus;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.user.vo.UserId;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+import java.util.Optional;
+
+@Getter
+@Entity
+@Table(
+        name = "coupon",
+        indexes = {
+                @Index(name = "idx_coupon_user_id", columnList = "user_id"),
+                @Index(name = "idx_coupon_created_at", columnList = "created_at")
+        }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class CouponEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(nullable = false)
+    private Long userId;
+
+    @Column(nullable = false)
+    private String status;
+
+    @Column(nullable = false)
+    private LocalDateTime createdAt;
+
+    @Column(nullable = false)
+    private LocalDateTime updatedAt;
+
+    private LocalDateTime deletedAt;
+
+    public static CouponEntity from(AbstractCoupon coupon) {
+        return new CouponEntity(
+                Optional.ofNullable(coupon.getCouponId().value())
+                        .map(Long::parseLong)
+                        .orElse(null),
+                Long.parseLong(Objects.requireNonNull(coupon.getUserId().value())),
+                coupon.getStatus().name(),
+                coupon.getCreatedAt().value(),
+                coupon.getUpdatedAt().value(),
+                coupon.getDeletedAt().value()
+        );
+    }
+
+    public AbstractCoupon to() {
+        return DefaultCoupon.mappedBy(
+                new CouponId(this.id.toString()),
+                new UserId(this.userId.toString()),
+                CouponStatus.valueOf(this.status),
+                new CreatedAt(this.createdAt),
+                new UpdatedAt(this.updatedAt),
+                new DeletedAt(this.deletedAt)
+        );
+    }
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/RateDiscountCouponEntity.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/RateDiscountCouponEntity.java
new file mode 100644
index 000000000..9cc7fcb0a
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/RateDiscountCouponEntity.java
@@ -0,0 +1,52 @@
+package com.loopers.core.infra.database.mysql.coupon.entity;
+
+import com.loopers.core.domain.order.RateDiscountCoupon;
+import com.loopers.core.domain.order.vo.CouponDiscountRate;
+import com.loopers.core.domain.order.vo.RateDiscountCouponId;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+@Entity
+@Table(
+        name = "rate_discount_coupon",
+        indexes = {
+                @Index(name = "idx_rate_discount_coupon_coupon_id", columnList = "coupon_id"),
+        }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class RateDiscountCouponEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(nullable = false)
+    private Long couponId;
+
+    @Column(nullable = false, precision = 19, scale = 2)
+    private BigDecimal rate;
+
+    public static RateDiscountCouponEntity of(CouponEntity couponEntity, RateDiscountCoupon rateDiscountCoupon) {
+        return new RateDiscountCouponEntity(
+                Optional.ofNullable(rateDiscountCoupon.getId().value())
+                        .map(Long::parseLong)
+                        .orElse(null),
+                couponEntity.getId(),
+                rateDiscountCoupon.getRate().value()
+        );
+    }
+
+    public RateDiscountCoupon to(CouponEntity couponEntity) {
+        return RateDiscountCoupon.mappedBy(
+                new RateDiscountCouponId(this.id.toString()),
+                couponEntity.to(),
+                new CouponDiscountRate(this.rate)
+        );
+    }
+}
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
new file mode 100644
index 000000000..8d018290c
--- /dev/null
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
@@ -0,0 +1,65 @@
+package com.loopers.core.infra.database.mysql.coupon.impl;
+
+import com.loopers.core.domain.error.DomainErrorCode;
+import com.loopers.core.domain.error.NotFoundException;
+import com.loopers.core.domain.order.AmountDiscountCoupon;
+import com.loopers.core.domain.order.Coupon;
+import com.loopers.core.domain.order.RateDiscountCoupon;
+import com.loopers.core.domain.order.repository.CouponRepository;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.infra.database.mysql.coupon.AmountDiscountCouponJpaRepository;
+import com.loopers.core.infra.database.mysql.coupon.CouponJpaRepository;
+import com.loopers.core.infra.database.mysql.coupon.RateDiscountCouponJpaRepository;
+import com.loopers.core.infra.database.mysql.coupon.entity.AmountDiscountCouponEntity;
+import com.loopers.core.infra.database.mysql.coupon.entity.CouponEntity;
+import com.loopers.core.infra.database.mysql.coupon.entity.RateDiscountCouponEntity;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class CouponRepositoryImpl implements CouponRepository {
+
+    private final CouponJpaRepository couponRepository;
+    private final RateDiscountCouponJpaRepository rateDiscountCouponRepository;
+    private final AmountDiscountCouponJpaRepository amountDiscountCouponRepository;
+
+    @Override
+    public Coupon save(Coupon coupon) {
+        if (coupon instanceof RateDiscountCoupon rateDiscountCoupon) {
+            CouponEntity savedCouponEntity = couponRepository.save(CouponEntity.from(rateDiscountCoupon));
+
+            return rateDiscountCouponRepository.save(RateDiscountCouponEntity.of(savedCouponEntity, rateDiscountCoupon))
+                    .to(savedCouponEntity);
+        }
+
+        if (coupon instanceof AmountDiscountCoupon amountDiscountCoupon) {
+            CouponEntity savedCouponEntity = couponRepository.save(CouponEntity.from(amountDiscountCoupon));
+
+            return amountDiscountCouponRepository.save(AmountDiscountCouponEntity.of(savedCouponEntity, amountDiscountCoupon))
+                    .to(savedCouponEntity);
+        }
+
+        throw new IllegalArgumentException(DomainErrorCode.NOT_SUPPORTED_COUPON_TYPE.getMessage());
+    }
+
+    @Override
+    public Coupon getById(CouponId couponId) {
+        CouponEntity coupon = couponRepository.findById(Long.parseLong(couponId.value()))
+                .orElseThrow(() -> NotFoundException.withName("쿠폰"));
+
+        Optional<RateDiscountCouponEntity> rateDiscountCoupon = rateDiscountCouponRepository.findByCouponId(coupon.getId());
+        if (rateDiscountCoupon.isPresent()) {
+            return rateDiscountCoupon.get().to(coupon);
+        }
+
+        Optional<AmountDiscountCouponEntity> amountDiscountCoupon = amountDiscountCouponRepository.findByCouponId(coupon.getId());
+        if (amountDiscountCoupon.isPresent()) {
+            return amountDiscountCoupon.get().to(coupon);
+        }
+
+        throw NotFoundException.withName("쿠폰");
+    }
+}

From effc99ccafaabfb652d98469541d39ac16ae4a11 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 18 Nov 2025 01:37:34 +0900
Subject: [PATCH 20/29] =?UTF-8?q?feature=20:=20=EC=BF=A0=ED=8F=B0=EC=97=90?=
 =?UTF-8?q?=20=ED=95=A0=EC=9D=B8=20=EC=A0=81=EC=9A=A9=EA=B0=80=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/domain/error/DomainErrorCode.java    |  3 +-
 .../core/domain/order/AbstractCoupon.java     | 29 +++++++++++++++----
 .../domain/order/AmountDiscountCoupon.java    |  9 ++++++
 .../com/loopers/core/domain/order/Coupon.java |  5 ++++
 .../core/domain/order/DefaultCoupon.java      | 15 ++++++++++
 .../core/domain/order/RateDiscountCoupon.java | 11 ++++++-
 .../domain/order/vo/CouponDiscountRate.java   |  5 ++++
 .../core/domain/payment/vo/PayAmount.java     | 14 +++++++++
 8 files changed, 83 insertions(+), 8 deletions(-)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index d88e0f904..1bdb8b6fd 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -18,7 +18,8 @@ public enum DomainErrorCode {
     PRODUCT_OUT_OF_STOCK(1010, "상품의 재고가 부족합니다."),
     COULD_NOT_BE_NEGATIVE(1011, "%s는(은) 음수가 될 수 없습니다."),
     NOT_ENOUGH_USER_POINT_BALANCE(1012, "사용자의 포인트 잔액이 충분하지 않습니다."),
-    NOT_SUPPORTED_COUPON_TYPE(1013, "지원하지 않는 쿠폰 유형입니다.");
+    NOT_SUPPORTED_COUPON_TYPE(1013, "지원하지 않는 쿠폰 유형입니다."),
+    NOT_AVAILABLE_COUPON_STATUS(1014, "사용할 수 없는 쿠폰의 상태입니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
index 70c41b07e..6a998691f 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
@@ -3,25 +3,29 @@
 import com.loopers.core.domain.common.vo.CreatedAt;
 import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.error.DomainErrorCode;
 import com.loopers.core.domain.order.type.CouponStatus;
 import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.user.vo.UserId;
 import lombok.Getter;
 
+import java.math.BigDecimal;
+
 @Getter
 public abstract class AbstractCoupon implements Coupon {
 
-    protected final CouponId couponId;
+    protected CouponId couponId;
 
-    protected final UserId userId;
+    protected UserId userId;
 
-    protected final CouponStatus status;
+    protected CouponStatus status;
 
-    protected final CreatedAt createdAt;
+    protected CreatedAt createdAt;
 
-    protected final UpdatedAt updatedAt;
+    protected UpdatedAt updatedAt;
 
-    protected final DeletedAt deletedAt;
+    protected DeletedAt deletedAt;
 
     protected AbstractCoupon(
             CouponId couponId,
@@ -49,4 +53,17 @@ protected AbstractCoupon(
         this.updatedAt = abstractCoupon.updatedAt;
         this.deletedAt = abstractCoupon.deletedAt;
     }
+
+    public void use() {
+        this.status = CouponStatus.USED;
+    }
+
+    public PayAmount apply(PayAmount payAmount) {
+        if (this.status != CouponStatus.AVAILABLE) {
+            throw new IllegalArgumentException(DomainErrorCode.NOT_AVAILABLE_COUPON_STATUS.getMessage());
+        }
+
+        BigDecimal discountAmount = calculateDiscountAmount(payAmount);
+        return payAmount.minus(discountAmount);
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
index 68d5db904..6905e0be1 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AmountDiscountCoupon.java
@@ -2,8 +2,11 @@
 
 import com.loopers.core.domain.order.vo.AmountDiscountCouponId;
 import com.loopers.core.domain.order.vo.CouponDiscountAmount;
+import com.loopers.core.domain.payment.vo.PayAmount;
 import lombok.Getter;
 
+import java.math.BigDecimal;
+
 @Getter
 public class AmountDiscountCoupon extends AbstractCoupon {
 
@@ -24,4 +27,10 @@ public static AmountDiscountCoupon create(AbstractCoupon abstractCoupon, CouponD
     public static AmountDiscountCoupon mappedBy(AmountDiscountCouponId id, AbstractCoupon abstractCoupon, CouponDiscountAmount amount) {
         return new AmountDiscountCoupon(abstractCoupon, id, amount);
     }
+
+
+    @Override
+    public BigDecimal calculateDiscountAmount(PayAmount payAmount) {
+        return this.amount.value();
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
index bf2db7696..d36e68129 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
@@ -1,5 +1,10 @@
 package com.loopers.core.domain.order;
 
+import com.loopers.core.domain.payment.vo.PayAmount;
+
+import java.math.BigDecimal;
+
 public interface Coupon {
 
+    BigDecimal calculateDiscountAmount(PayAmount payAmount);
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
index 3acfa7b32..92db00342 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
@@ -5,10 +5,16 @@
 import com.loopers.core.domain.common.vo.UpdatedAt;
 import com.loopers.core.domain.order.type.CouponStatus;
 import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.user.vo.UserId;
+import lombok.AccessLevel;
+import lombok.Builder;
+
+import java.math.BigDecimal;
 
 public class DefaultCoupon extends AbstractCoupon {
 
+    @Builder(access = AccessLevel.PRIVATE, toBuilder = true)
     private DefaultCoupon(
             CouponId couponId,
             UserId userId,
@@ -20,6 +26,10 @@ private DefaultCoupon(
         super(couponId, userId, status, createdAt, updatedAt, deletedAt);
     }
 
+    protected DefaultCoupon(AbstractCoupon coupon) {
+        super(coupon);
+    }
+
     public static DefaultCoupon create(UserId userId) {
         return new DefaultCoupon(
                 CouponId.empty(),
@@ -41,4 +51,9 @@ public static DefaultCoupon mappedBy(
     ) {
         return new DefaultCoupon(couponId, userId, status, createdAt, updatedAt, deletedAt);
     }
+
+    @Override
+    public BigDecimal calculateDiscountAmount(PayAmount payAmount) {
+        return BigDecimal.ZERO;
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
index 8bd367b0a..b203235a7 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/RateDiscountCoupon.java
@@ -2,8 +2,11 @@
 
 import com.loopers.core.domain.order.vo.CouponDiscountRate;
 import com.loopers.core.domain.order.vo.RateDiscountCouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
 import lombok.Getter;
 
+import java.math.BigDecimal;
+
 @Getter
 public class RateDiscountCoupon extends AbstractCoupon {
 
@@ -14,7 +17,8 @@ public class RateDiscountCoupon extends AbstractCoupon {
     private RateDiscountCoupon(
             AbstractCoupon abstractCoupon,
             RateDiscountCouponId id,
-            CouponDiscountRate rate) {
+            CouponDiscountRate rate
+    ) {
         super(abstractCoupon);
         this.id = id;
         this.rate = rate;
@@ -27,4 +31,9 @@ public static RateDiscountCoupon create(AbstractCoupon abstractCoupon, CouponDis
     public static RateDiscountCoupon mappedBy(RateDiscountCouponId id, AbstractCoupon abstractCoupon, CouponDiscountRate rate) {
         return new RateDiscountCoupon(abstractCoupon, id, rate);
     }
+
+    @Override
+    public BigDecimal calculateDiscountAmount(PayAmount payAmount) {
+        return this.rate.calculate(payAmount);
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
index 0cdc8fb0c..37fb7b277 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountRate.java
@@ -1,6 +1,7 @@
 package com.loopers.core.domain.order.vo;
 
 import com.loopers.core.domain.error.DomainErrorCode;
+import com.loopers.core.domain.payment.vo.PayAmount;
 
 import java.math.BigDecimal;
 import java.util.Objects;
@@ -24,4 +25,8 @@ private static void validateRange(BigDecimal value) {
             throw new IllegalArgumentException("할인율은 0~100 사이여야 합니다");
         }
     }
+
+    public BigDecimal calculate(PayAmount payAmount) {
+        return payAmount.applyRateDiscount(this.value);
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/payment/vo/PayAmount.java b/core/domain/src/main/java/com/loopers/core/domain/payment/vo/PayAmount.java
index 7969e6be7..9d1693f04 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/payment/vo/PayAmount.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/payment/vo/PayAmount.java
@@ -27,4 +27,18 @@ private static void validateNull(BigDecimal value) {
             throw new NullPointerException(notNullMessage(FIELD_NAME));
         }
     }
+
+    public PayAmount minus(BigDecimal amount) {
+        BigDecimal minusValue = this.value.subtract(amount);
+        if (minusValue.signum() < 0) {
+            return new PayAmount(BigDecimal.ZERO);
+        }
+
+        return new PayAmount(minusValue);
+    }
+
+    public BigDecimal applyRateDiscount(BigDecimal discountRate) {
+        return this.value.multiply(discountRate).divide(new BigDecimal("100"), 2, java.math.RoundingMode.HALF_UP);
+    }
+
 }

From 2c32bc7cca337ad31c94c031b85a23893dccc020 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 18 Nov 2025 22:00:16 +0900
Subject: [PATCH 21/29] =?UTF-8?q?feature=20:=20=EA=B8=88=EC=95=A1=ED=95=A0?=
 =?UTF-8?q?=EC=9D=B8=20=EC=A0=84=EB=9E=B5=ED=8C=A8=ED=84=B4=20=EC=A0=81?=
 =?UTF-8?q?=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/order/OrderV1Dto.java     |   6 +-
 .../api/order/OrderV1ApiIntegrationTest.java  |  12 +-
 .../core/domain/order/AbstractCoupon.java     |   5 +-
 .../com/loopers/core/domain/order/Coupon.java |   6 +-
 .../order/repository/CouponRepository.java    |   2 +
 .../core/domain/order/vo/CouponId.java        |   6 +
 .../core/domain/order/AbstractCouponTest.java |  38 ++++++
 .../order/AmountDiscountCouponTest.java       |  38 ++++++
 .../domain/order/RateDiscountCouponTest.java  |  37 ++++++
 .../core/domain/payment/vo/PayAmountTest.java |  57 +++++++--
 .../mysql/coupon/CouponJpaRepository.java     |   9 ++
 .../coupon/impl/CouponRepositoryImpl.java     |  18 +++
 .../core/service/order/OrderService.java      |  12 +-
 .../order/command/OrderProductsCommand.java   |  25 +---
 .../component/CouponDiscountStrategy.java     |  27 ++++
 .../order/component/NoneDiscountStrategy.java |  14 +++
 .../service/order/component/OrderCashier.java |  12 +-
 .../component/PayAmountDiscountStrategy.java  |   9 ++
 .../PayAmountDiscountStrategySelector.java    |  21 ++++
 .../order/OrderCashierIntegrationTest.java    |   9 +-
 .../component/CouponDiscountStrategyTest.java | 115 ++++++++++++++++++
 .../order/component/OrderCashierTest.java     |  88 +-------------
 ...PayAmountDiscountStrategySelectorTest.java |  55 +++++++++
 23 files changed, 476 insertions(+), 145 deletions(-)
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/AbstractCouponTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/AmountDiscountCouponTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/order/RateDiscountCouponTest.java
 create mode 100644 core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
 create mode 100644 core/service/order-service/src/main/java/com/loopers/core/service/order/component/NoneDiscountStrategy.java
 create mode 100644 core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategy.java
 create mode 100644 core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelector.java
 create mode 100644 core/service/order-service/src/test/java/com/loopers/core/service/order/component/CouponDiscountStrategyTest.java
 create mode 100644 core/service/order-service/src/test/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelectorTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
index 6576c8557..4f83b0cbb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/order/OrderV1Dto.java
@@ -11,7 +11,8 @@
 public class OrderV1Dto {
 
     public record OrderRequest(
-            List<OrderItemRequest> items
+            List<OrderItemRequest> items,
+            String couponId
     ) {
 
         public OrderProductsCommand toCommand(String userIdentifier) {
@@ -19,7 +20,8 @@ public OrderProductsCommand toCommand(String userIdentifier) {
                     userIdentifier,
                     this.items.stream()
                             .map(OrderItemRequest::toCommand)
-                            .toList()
+                            .toList(),
+                    this.couponId
             );
         }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
index e03843df6..f5e959223 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/order/OrderV1ApiIntegrationTest.java
@@ -112,7 +112,8 @@ void status200() {
                 OrderRequest request = new OrderRequest(
                         List.of(
                                 new OrderRequest.OrderItemRequest(productId, 2L)
-                        )
+                        ),
+                        null
                 );
 
                 HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
@@ -144,7 +145,8 @@ void status400() {
                 OrderRequest request = new OrderRequest(
                         List.of(
                                 new OrderRequest.OrderItemRequest(productId, 2L)
-                        )
+                        ),
+                        "1"
                 );
 
                 HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
@@ -177,7 +179,8 @@ void status404() {
                 OrderRequest request = new OrderRequest(
                         List.of(
                                 new OrderRequest.OrderItemRequest(productId, 2L)
-                        )
+                        ),
+                        null
                 );
 
                 HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
@@ -222,7 +225,8 @@ void setUp() {
                 OrderRequest request = new OrderRequest(
                         List.of(
                                 new OrderRequest.OrderItemRequest("99999", 2L)
-                        )
+                        ),
+                        null
                 );
 
                 HttpEntity<OrderRequest> httpEntity = new HttpEntity<>(request, headers);
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
index 6a998691f..2f2b4b41a 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
@@ -54,11 +54,12 @@ protected AbstractCoupon(
         this.deletedAt = abstractCoupon.deletedAt;
     }
 
+    @Override
     public void use() {
         this.status = CouponStatus.USED;
     }
 
-    public PayAmount apply(PayAmount payAmount) {
+    public PayAmount discount(PayAmount payAmount) {
         if (this.status != CouponStatus.AVAILABLE) {
             throw new IllegalArgumentException(DomainErrorCode.NOT_AVAILABLE_COUPON_STATUS.getMessage());
         }
@@ -66,4 +67,6 @@ public PayAmount apply(PayAmount payAmount) {
         BigDecimal discountAmount = calculateDiscountAmount(payAmount);
         return payAmount.minus(discountAmount);
     }
+
+    public abstract BigDecimal calculateDiscountAmount(PayAmount payAmount);
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
index d36e68129..50935980a 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/Coupon.java
@@ -2,9 +2,9 @@
 
 import com.loopers.core.domain.payment.vo.PayAmount;
 
-import java.math.BigDecimal;
-
 public interface Coupon {
 
-    BigDecimal calculateDiscountAmount(PayAmount payAmount);
+    PayAmount discount(PayAmount payAmount);
+
+    void use();
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
index 481c84875..b86d5d956 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
@@ -8,4 +8,6 @@ public interface CouponRepository {
     Coupon save(Coupon coupon);
 
     Coupon getById(CouponId couponId);
+
+    Coupon getByIdWithLock(CouponId couponId);
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
index bd0cd8ba7..64f10c5e3 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponId.java
@@ -1,8 +1,14 @@
 package com.loopers.core.domain.order.vo;
 
+import java.util.Objects;
+
 public record CouponId(String value) {
 
     public static CouponId empty() {
         return new CouponId(null);
     }
+
+    public boolean nonEmpty() {
+        return Objects.nonNull(value);
+    }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/AbstractCouponTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/AbstractCouponTest.java
new file mode 100644
index 000000000..8b7831e64
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/AbstractCouponTest.java
@@ -0,0 +1,38 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.order.type.CouponStatus;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.instancio.Select.field;
+
+class AbstractCouponTest {
+
+    @Nested
+    @DisplayName("discount()")
+    class DiscountMethod {
+
+        @Nested
+        @DisplayName("이미 사용된 쿠폰이면")
+        class couponUnavailable {
+
+            @Test
+            @DisplayName("예외가 발생한다.")
+            void throwException() {
+                DefaultCoupon defaultCoupon = Instancio.of(DefaultCoupon.class)
+                        .set(field(DefaultCoupon::getStatus), CouponStatus.USED)
+                        .create();
+                PayAmount payAmount = new PayAmount(new BigDecimal(1000));
+                assertThatThrownBy(() -> defaultCoupon.discount(payAmount))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessageContaining("사용할 수 없는");
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/AmountDiscountCouponTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/AmountDiscountCouponTest.java
new file mode 100644
index 000000000..0a9bf3169
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/AmountDiscountCouponTest.java
@@ -0,0 +1,38 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.order.vo.CouponDiscountAmount;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
+
+@DisplayName("정액 할인 쿠폰")
+class AmountDiscountCouponTest {
+
+    @Nested
+    @DisplayName("calculateDiscountAmount()")
+    class CalculateDiscountAmount {
+
+        @Nested
+        @DisplayName("할인 금액은")
+        class DiscountAmount {
+
+            @Test
+            @DisplayName("일정 양을 가진다.")
+            void discountAmount() {
+                PayAmount payAmount = new PayAmount(new BigDecimal(10000));
+                AmountDiscountCoupon coupon = Instancio.of(AmountDiscountCoupon.class)
+                        .set(field(AmountDiscountCoupon::getAmount), new CouponDiscountAmount(new BigDecimal(1000)))
+                        .create();
+
+                assertThat(coupon.calculateDiscountAmount(payAmount)).isEqualByComparingTo(new BigDecimal(1000));
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/order/RateDiscountCouponTest.java b/core/domain/src/test/java/com/loopers/core/domain/order/RateDiscountCouponTest.java
new file mode 100644
index 000000000..4a3f4b4da
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/order/RateDiscountCouponTest.java
@@ -0,0 +1,37 @@
+package com.loopers.core.domain.order;
+
+import com.loopers.core.domain.order.vo.CouponDiscountRate;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.instancio.Select.field;
+
+class RateDiscountCouponTest {
+
+    @Nested
+    @DisplayName("calculateDiscountAmount()")
+    class CalculateDiscountAmount {
+
+        @Nested
+        @DisplayName("할인 금액은")
+        class DiscountAmount {
+
+            @Test
+            @DisplayName("일정 비율을 가진다.")
+            void discountAmount() {
+                PayAmount payAmount = new PayAmount(new BigDecimal(10000));
+                RateDiscountCoupon coupon = Instancio.of(RateDiscountCoupon.class)
+                        .set(field(RateDiscountCoupon::getRate), new CouponDiscountRate(new BigDecimal(20)))
+                        .create();
+
+                assertThat(coupon.calculateDiscountAmount(payAmount)).isEqualByComparingTo(new BigDecimal(2000));
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/payment/vo/PayAmountTest.java b/core/domain/src/test/java/com/loopers/core/domain/payment/vo/PayAmountTest.java
index 94d74204b..e2ed46941 100644
--- a/core/domain/src/test/java/com/loopers/core/domain/payment/vo/PayAmountTest.java
+++ b/core/domain/src/test/java/com/loopers/core/domain/payment/vo/PayAmountTest.java
@@ -25,11 +25,11 @@ class 유효한_값인_경우 {
 
             static Stream<BigDecimal> validPayAmounts() {
                 return Stream.of(
-                    BigDecimal.ZERO,
-                    new BigDecimal(1),
-                    new BigDecimal(10000),
-                    new BigDecimal(100000),
-                    new BigDecimal("999999.99")
+                        BigDecimal.ZERO,
+                        new BigDecimal(1),
+                        new BigDecimal(10000),
+                        new BigDecimal(100000),
+                        new BigDecimal("999999.99")
                 );
             }
 
@@ -51,9 +51,9 @@ class 값이_음수인_경우 {
 
             static Stream<BigDecimal> invalidPayAmounts() {
                 return Stream.of(
-                    new BigDecimal(-1),
-                    new BigDecimal(-100),
-                    new BigDecimal("-10000.50")
+                        new BigDecimal(-1),
+                        new BigDecimal(-100),
+                        new BigDecimal("-10000.50")
                 );
             }
 
@@ -63,8 +63,8 @@ static Stream<BigDecimal> invalidPayAmounts() {
             void 예외_발생(BigDecimal invalidAmount) {
                 // when & then
                 assertThatThrownBy(() -> new PayAmount(invalidAmount))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessage("결제 총 금액는(은) 음수가 될 수 없습니다.");
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("결제 총 금액는(은) 음수가 될 수 없습니다.");
             }
         }
 
@@ -77,8 +77,41 @@ class 값이_null인_경우 {
             void NullPointerException이_발생한다() {
                 // when & then
                 assertThatThrownBy(() -> new PayAmount(null))
-                    .isInstanceOf(NullPointerException.class)
-                    .hasMessage("결제 총 금액는(은) Null이 될 수 없습니다.");
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("결제 총 금액는(은) Null이 될 수 없습니다.");
+            }
+        }
+    }
+
+    @Nested
+    @DisplayName("minus()")
+    class Minus {
+
+        @Nested
+        @DisplayName("가진금액보다 감소 시킬 금액이 크다면")
+        class SmallAmount {
+
+            @Test
+            @DisplayName("0원을 가진다.")
+            void hasZero() {
+                PayAmount payAmount = new PayAmount(new BigDecimal(1000));
+                PayAmount minus = payAmount.minus(new BigDecimal(10000));
+
+                assertThat(minus.value()).isEqualByComparingTo(new BigDecimal(0));
+            }
+        }
+
+        @Nested
+        @DisplayName("가진금액이 감소시킬 금액보다 크다면")
+        class LargeAmount {
+
+            @Test
+            @DisplayName("금액이 감소한다.")
+            void minus() {
+                PayAmount payAmount = new PayAmount(new BigDecimal(10000));
+                PayAmount minus = payAmount.minus(new BigDecimal(1000));
+
+                assertThat(minus.value()).isEqualByComparingTo(new BigDecimal(9000));
             }
         }
     }
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
index d6548a914..e26733f74 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
@@ -1,7 +1,16 @@
 package com.loopers.core.infra.database.mysql.coupon;
 
 import com.loopers.core.infra.database.mysql.coupon.entity.CouponEntity;
+import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.Optional;
 
 public interface CouponJpaRepository extends JpaRepository<CouponEntity, Long> {
+
+    @Lock(LockModeType.PESSIMISTIC_WRITE)
+    @Query("select c from CouponEntity c where c.id = :id")
+    Optional<CouponEntity> findByIdWithLock(Long id);
 }
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
index 8d018290c..b789f37c1 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
@@ -62,4 +62,22 @@ public Coupon getById(CouponId couponId) {
 
         throw NotFoundException.withName("쿠폰");
     }
+
+    @Override
+    public Coupon getByIdWithLock(CouponId couponId) {
+        CouponEntity coupon = couponRepository.findByIdWithLock(Long.parseLong(couponId.value()))
+                .orElseThrow(() -> NotFoundException.withName("쿠폰"));
+
+        Optional<RateDiscountCouponEntity> rateDiscountCoupon = rateDiscountCouponRepository.findByCouponId(coupon.getId());
+        if (rateDiscountCoupon.isPresent()) {
+            return rateDiscountCoupon.get().to(coupon);
+        }
+
+        Optional<AmountDiscountCouponEntity> amountDiscountCoupon = amountDiscountCouponRepository.findByCouponId(coupon.getId());
+        if (amountDiscountCoupon.isPresent()) {
+            return amountDiscountCoupon.get().to(coupon);
+        }
+
+        throw NotFoundException.withName("쿠폰");
+    }
 }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
index 0e2de20ac..5767d05d1 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/OrderService.java
@@ -3,6 +3,7 @@
 import com.loopers.core.domain.order.Order;
 import com.loopers.core.domain.order.OrderItem;
 import com.loopers.core.domain.order.repository.OrderRepository;
+import com.loopers.core.domain.order.vo.CouponId;
 import com.loopers.core.domain.order.vo.Quantity;
 import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.product.vo.ProductId;
@@ -29,19 +30,20 @@ public class OrderService {
 
     @Transactional
     public Order order(OrderProductsCommand command) {
-        User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
+        User user = userRepository.getByIdentifier(new UserIdentifier(command.userIdentifier()));
         Order savedOrder = orderRepository.save(Order.create(user.getId()));
-        List<OrderItem> orderItems = command.getOrderItems().stream()
+        CouponId couponId = new CouponId(command.couponId());
+        List<OrderItem> orderItems = command.orderItems().stream()
                 .map(productCommand -> OrderItem.create(
                                 savedOrder.getId(),
-                                new ProductId(productCommand.getProductId()),
-                                new Quantity(productCommand.getQuantity())
+                                new ProductId(productCommand.productId()),
+                                new Quantity(productCommand.quantity())
                         )
                 )
                 .toList();
 
         PayAmount payAmount = orderLineAggregator.aggregate(orderItems);
-        orderCashier.checkout(user, savedOrder, payAmount);
+        orderCashier.checkout(user, savedOrder, payAmount, couponId);
 
         return savedOrder;
     }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/command/OrderProductsCommand.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/command/OrderProductsCommand.java
index efd641265..e02fade2d 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/command/OrderProductsCommand.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/command/OrderProductsCommand.java
@@ -1,31 +1,10 @@
 package com.loopers.core.service.order.command;
 
-import lombok.Getter;
-
 import java.util.List;
 
-@Getter
-public class OrderProductsCommand {
-
-    private final String userIdentifier;
-
-    private final List<OrderItem> orderItems;
-
-    public OrderProductsCommand(String userIdentifier, List<OrderItem> orderItems) {
-        this.userIdentifier = userIdentifier;
-        this.orderItems = orderItems;
-    }
-
-    @Getter
-    public static class OrderItem {
-
-        private final String productId;
+public record OrderProductsCommand(String userIdentifier, List<OrderItem> orderItems, String couponId) {
 
-        private final Long quantity;
+    public record OrderItem(String productId, Long quantity) {
 
-        public OrderItem(String productId, Long quantity) {
-            this.productId = productId;
-            this.quantity = quantity;
-        }
     }
 }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
new file mode 100644
index 000000000..1d7ce5e44
--- /dev/null
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
@@ -0,0 +1,27 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.Coupon;
+import com.loopers.core.domain.order.repository.CouponRepository;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class CouponDiscountStrategy implements PayAmountDiscountStrategy {
+
+    private final CouponRepository couponRepository;
+
+    @Override
+    @Transactional
+    public PayAmount discount(PayAmount payAmount, CouponId couponId) {
+        Coupon coupon = couponRepository.getByIdWithLock(couponId);
+        PayAmount discountedAmount = coupon.discount(payAmount);
+        coupon.use();
+        couponRepository.save(coupon);
+
+        return discountedAmount;
+    }
+}
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/NoneDiscountStrategy.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/NoneDiscountStrategy.java
new file mode 100644
index 000000000..d6bfbf4b9
--- /dev/null
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/NoneDiscountStrategy.java
@@ -0,0 +1,14 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NoneDiscountStrategy implements PayAmountDiscountStrategy {
+
+    @Override
+    public PayAmount discount(PayAmount payAmount, CouponId couponId) {
+        return payAmount;
+    }
+}
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
index 354a97259..4abb63397 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
@@ -1,6 +1,7 @@
 package com.loopers.core.service.order.component;
 
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.vo.CouponId;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
@@ -17,15 +18,16 @@ public class OrderCashier {
 
     private final UserPointRepository userPointRepository;
     private final PaymentRepository paymentRepository;
+    private final PayAmountDiscountStrategySelector discountStrategySelector;
 
     @Transactional
-    public Payment checkout(User user, Order order, PayAmount payAmount) {
+    public Payment checkout(User user, Order order, PayAmount payAmount, CouponId couponId) {
         UserPoint userPoint = userPointRepository.getByUserIdWithLock(user.getId());
         userPointRepository.save(userPoint.pay(payAmount));
+        PayAmountDiscountStrategy discountStrategy = discountStrategySelector.select(couponId);
+        PayAmount discountedPayAmount = discountStrategy.discount(payAmount, couponId);
+        Payment payment = Payment.create(order.getId(), user.getId(), discountedPayAmount);
 
-        Payment payment = Payment.create(order.getId(), user.getId(), payAmount);
-        paymentRepository.save(payment);
-
-        return payment;
+        return paymentRepository.save(payment);
     }
 }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategy.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategy.java
new file mode 100644
index 000000000..e0b850531
--- /dev/null
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategy.java
@@ -0,0 +1,9 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
+
+public interface PayAmountDiscountStrategy {
+
+    PayAmount discount(PayAmount payAmount, CouponId couponId);
+}
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelector.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelector.java
new file mode 100644
index 000000000..ac7949db6
--- /dev/null
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelector.java
@@ -0,0 +1,21 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.vo.CouponId;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class PayAmountDiscountStrategySelector {
+
+    private final NoneDiscountStrategy noneDiscountStrategy;
+    private final CouponDiscountStrategy couponDiscountStrategy;
+
+    public PayAmountDiscountStrategy select(CouponId couponId) {
+        if (couponId.nonEmpty()) {
+            return couponDiscountStrategy;
+        }
+
+        return noneDiscountStrategy;
+    }
+}
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
index b8e960491..e538a0810 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderCashierIntegrationTest.java
@@ -2,6 +2,7 @@
 
 import com.loopers.core.domain.order.Order;
 import com.loopers.core.domain.order.repository.OrderRepository;
+import com.loopers.core.domain.order.vo.CouponId;
 import com.loopers.core.domain.order.vo.OrderId;
 import com.loopers.core.domain.payment.Payment;
 import com.loopers.core.domain.payment.repository.PaymentRepository;
@@ -94,7 +95,7 @@ void setUp() {
             @DisplayName("주문을 생성하고 포인트를 차감하고 결제 정보를 저장한다")
             void 주문을_생성하고_포인트를_차감하고_결제_정보를_저장한다() {
                 // when
-                Payment result = orderCashier.checkout(user, order, payAmount);
+                Payment result = orderCashier.checkout(user, order, payAmount, CouponId.empty());
 
                 // then - 결제 정보 확인
                 assertSoftly(softly -> {
@@ -128,7 +129,7 @@ void setUp() {
                 PayAmount exactPayAmount = new PayAmount(new BigDecimal("50000"));
 
                 // when
-                orderCashier.checkout(user, order, exactPayAmount);
+                orderCashier.checkout(user, order, exactPayAmount, CouponId.empty());
 
                 // then
                 UserPoint userPoint = userPointRepository.getByUserId(user.getId());
@@ -146,7 +147,7 @@ void setUp() {
                 PayAmount payAmountPerUser = new PayAmount(new BigDecimal("500"));
                 List<Payment> results = ConcurrencyTestUtil.executeInParallel(
                         requestCount,
-                        index -> orderCashier.checkout(user, order, payAmountPerUser)
+                        index -> orderCashier.checkout(user, order, payAmountPerUser, CouponId.empty())
                 );
 
                 UserPoint actualUserPoint = userPointRepository.getByUserId(user.getId());
@@ -201,7 +202,7 @@ void setUp() {
 
                 // when & then
                 assertThatThrownBy(
-                        () -> orderCashier.checkout(user, order, largePayAmount)
+                        () -> orderCashier.checkout(user, order, largePayAmount, CouponId.empty())
                 ).isInstanceOf(IllegalArgumentException.class)
                         .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
             }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/CouponDiscountStrategyTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/CouponDiscountStrategyTest.java
new file mode 100644
index 000000000..eec577333
--- /dev/null
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/CouponDiscountStrategyTest.java
@@ -0,0 +1,115 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.AmountDiscountCoupon;
+import com.loopers.core.domain.order.repository.CouponRepository;
+import com.loopers.core.domain.order.type.CouponStatus;
+import com.loopers.core.domain.order.vo.AmountDiscountCouponId;
+import com.loopers.core.domain.order.vo.CouponDiscountAmount;
+import com.loopers.core.domain.order.vo.CouponId;
+import com.loopers.core.domain.payment.vo.PayAmount;
+import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.service.ConcurrencyTestUtil;
+import com.loopers.core.service.IntegrationTest;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+import static org.instancio.Select.field;
+
+@DisplayName("쿠폰 할인전략")
+class CouponDiscountStrategyTest extends IntegrationTest {
+
+    @Autowired
+    private CouponRepository couponRepository;
+
+    @Autowired
+    private CouponDiscountStrategy discountStrategy;
+
+    @Nested
+    @DisplayName("discount()")
+    class DiscountMethod {
+
+        @Nested
+        @DisplayName("쿠폰이 존재하면")
+        class couponExist {
+
+            CouponId couponId;
+
+            @BeforeEach
+            void setUp() {
+                AmountDiscountCoupon coupon = (AmountDiscountCoupon) couponRepository.save(
+                        Instancio.of(AmountDiscountCoupon.class)
+                                .set(field(AmountDiscountCoupon::getCouponId), CouponId.empty())
+                                .set(field(AmountDiscountCoupon::getId), AmountDiscountCouponId.empty())
+                                .set(field(AmountDiscountCoupon::getUserId), new UserId("1"))
+                                .set(field(AmountDiscountCoupon::getAmount), new CouponDiscountAmount(new BigDecimal(1000)))
+                                .set(field(AmountDiscountCoupon::getStatus), CouponStatus.AVAILABLE)
+                                .create()
+                );
+                couponId = coupon.getCouponId();
+            }
+
+            @Test
+            @DisplayName("쿠폰을 사용하고 할인된다.")
+            void usedCouponAndDiscount() {
+                PayAmount payAmount = new PayAmount(new BigDecimal(10000));
+                PayAmount discounted = discountStrategy.discount(payAmount, couponId);
+
+                AmountDiscountCoupon usedCoupon = (AmountDiscountCoupon) couponRepository.getById(couponId);
+
+                assertSoftly(softly -> {
+                    softly.assertThat(discounted.value()).isEqualByComparingTo(new BigDecimal(9000));
+                    softly.assertThat(usedCoupon.getStatus()).isEqualTo(CouponStatus.USED);
+                });
+            }
+        }
+
+        @Nested
+        @DisplayName("동시에 하나의 쿠폰을 여러번 사용하려고 하면")
+        class concurrentlyUseCoupon {
+
+            CouponId couponId;
+
+            @BeforeEach
+            void setUp() {
+                AmountDiscountCoupon coupon = (AmountDiscountCoupon) couponRepository.save(
+                        Instancio.of(AmountDiscountCoupon.class)
+                                .set(field(AmountDiscountCoupon::getCouponId), CouponId.empty())
+                                .set(field(AmountDiscountCoupon::getId), AmountDiscountCouponId.empty())
+                                .set(field(AmountDiscountCoupon::getUserId), new UserId("1"))
+                                .set(field(AmountDiscountCoupon::getAmount), new CouponDiscountAmount(new BigDecimal(1000)))
+                                .set(field(AmountDiscountCoupon::getStatus), CouponStatus.AVAILABLE)
+                                .create()
+                );
+                couponId = coupon.getCouponId();
+            }
+
+            @Test
+            @DisplayName("한번만 사용된다.")
+            void usedOnce() throws InterruptedException {
+                int requestCount = 100;
+                List<PayAmount> payAmounts = ConcurrencyTestUtil.executeInParallel(
+                        requestCount,
+                        index -> {
+                            PayAmount payAmount = new PayAmount(new BigDecimal(10000));
+                            return discountStrategy.discount(payAmount, couponId);
+                        }
+                );
+
+                List<PayAmount> discountedPayAmounts = payAmounts.stream()
+                        .filter(payAmount -> payAmount.value().compareTo(new BigDecimal(9000)) == 0)
+                        .toList();
+                assertThat(discountedPayAmounts).hasSize(1);
+            }
+        }
+    }
+
+}
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
index 87eb8114a..418ec965f 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
@@ -1,9 +1,8 @@
 package com.loopers.core.service.order.component;
 
 import com.loopers.core.domain.order.Order;
+import com.loopers.core.domain.order.vo.CouponId;
 import com.loopers.core.domain.order.vo.OrderId;
-import com.loopers.core.domain.payment.Payment;
-import com.loopers.core.domain.payment.repository.PaymentRepository;
 import com.loopers.core.domain.payment.vo.PayAmount;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
@@ -15,18 +14,14 @@
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.math.BigDecimal;
 
-import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.instancio.Select.field;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @DisplayName("OrderCashier 단위 테스트")
@@ -36,9 +31,6 @@ class OrderCashierTest {
     @Mock
     private UserPointRepository userPointRepository;
 
-    @Mock
-    private PaymentRepository paymentRepository;
-
     @InjectMocks
     private OrderCashier orderCashier;
 
@@ -63,49 +55,6 @@ void setUp() {
                     .create();
         }
 
-        @Nested
-        @DisplayName("포인트가 충분할 때")
-        class WhenSufficientPoint {
-
-            private UserPoint userPoint;
-            private PayAmount payAmount;
-
-            @BeforeEach
-            void setUp() {
-                userPoint = Instancio.of(UserPoint.class)
-                        .set(field("id"), UserPointId.empty())
-                        .set(field("userId"), user.getId())
-                        .set(field("balance"), new UserPointBalance(new BigDecimal(50_000)))
-                        .create();
-
-                payAmount = new PayAmount(new BigDecimal("20000"));
-
-                when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
-            }
-
-            @Test
-            @DisplayName("포인트를 차감하고 Payment를 저장한다")
-            void checkoutSuccessAndSavePayment() {
-                // when
-                Payment result = orderCashier.checkout(user, order, payAmount);
-
-                // then
-                assertThat(result).isNotNull();
-                assertThat(result.getOrderId()).isEqualTo(order.getId());
-                assertThat(result.getUserId()).isEqualTo(user.getId());
-                assertThat(result.getAmount()).isEqualTo(payAmount);
-
-                // 포인트 차감 검증
-                ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
-                verify(userPointRepository).save(userPointCaptor.capture());
-                assertThat(userPointCaptor.getValue().getBalance().value())
-                        .isEqualByComparingTo(new BigDecimal("30000")); // 50000 - 20000
-
-                // Payment 저장 검증
-                verify(paymentRepository).save(any(Payment.class));
-            }
-        }
-
         @Nested
         @DisplayName("포인트가 부족할 때")
         class WhenInsufficientPoint {
@@ -130,43 +79,10 @@ void setUp() {
             @DisplayName("예외를 발생시킨다")
             void throwExceptionWhenInsufficientPoint() {
                 // when & then
-                assertThatThrownBy(() -> orderCashier.checkout(user, order, largePayAmount))
+                assertThatThrownBy(() -> orderCashier.checkout(user, order, largePayAmount, CouponId.empty()))
                         .isInstanceOf(IllegalArgumentException.class)
                         .hasMessage("사용자의 포인트 잔액이 충분하지 않습니다.");
             }
         }
-
-        @Nested
-        @DisplayName("포인트가 정확히 남은 금액과 같을 때")
-        class WhenExactPoint {
-
-            private UserPoint userPoint;
-            private PayAmount exactPayAmount;
-
-            @BeforeEach
-            void setUp() {
-                userPoint = Instancio.of(UserPoint.class)
-                        .set(field("id"), UserPointId.empty())
-                        .set(field("userId"), user.getId())
-                        .set(field("balance"), new UserPointBalance(new BigDecimal(50_000)))
-                        .create();
-
-                exactPayAmount = new PayAmount(new BigDecimal("50000"));
-
-                when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
-            }
-
-            @Test
-            @DisplayName("포인트가 0이 된다")
-            void checkoutWithExactPointBecomesZero() {
-                // when
-                orderCashier.checkout(user, order, exactPayAmount);
-
-                // then
-                ArgumentCaptor<UserPoint> userPointCaptor = ArgumentCaptor.forClass(UserPoint.class);
-                verify(userPointRepository).save(userPointCaptor.capture());
-                assertThat(userPointCaptor.getValue().getBalance().value()).isZero();
-            }
-        }
     }
 }
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelectorTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelectorTest.java
new file mode 100644
index 000000000..f81114ea4
--- /dev/null
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/PayAmountDiscountStrategySelectorTest.java
@@ -0,0 +1,55 @@
+package com.loopers.core.service.order.component;
+
+import com.loopers.core.domain.order.vo.CouponId;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+@DisplayName("할인 전략 셀렉터")
+class PayAmountDiscountStrategySelectorTest {
+
+    @Nested
+    @DisplayName("select()")
+    class Select {
+
+        @Nested
+        @DisplayName("Coupon이 Null인 경우")
+        class CouponIsNull {
+
+            @Test
+            @DisplayName("NoneDiscountStrategy를 반환한다.")
+            void noneDiscountStrategy() {
+                NoneDiscountStrategy noneDiscountStrategy = mock(NoneDiscountStrategy.class);
+                CouponDiscountStrategy couponDiscountStrategy = mock(CouponDiscountStrategy.class);
+                PayAmountDiscountStrategySelector selector = new PayAmountDiscountStrategySelector(
+                        noneDiscountStrategy,
+                        couponDiscountStrategy
+                );
+
+                PayAmountDiscountStrategy discountStrategy = selector.select(CouponId.empty());
+                assertThat(discountStrategy).isExactlyInstanceOf(NoneDiscountStrategy.class);
+            }
+        }
+
+        @Nested
+        @DisplayName("쿠폰이 Null이 아닌 경우")
+        class couponIsNotNull {
+
+            @Test
+            @DisplayName("CouponDiscountStrategy를 반환한다.")
+            void couponDiscountStrategy() {
+                NoneDiscountStrategy noneDiscountStrategy = mock(NoneDiscountStrategy.class);
+                CouponDiscountStrategy couponDiscountStrategy = mock(CouponDiscountStrategy.class);
+                PayAmountDiscountStrategySelector selector = new PayAmountDiscountStrategySelector(
+                        noneDiscountStrategy,
+                        couponDiscountStrategy
+                );
+                PayAmountDiscountStrategy discountStrategy = selector.select(new CouponId("1"));
+                assertThat(discountStrategy).isExactlyInstanceOf(CouponDiscountStrategy.class);
+            }
+        }
+    }
+}

From 25c0077758217964dcd5971b82ce955bea70b1eb Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 19 Nov 2025 02:46:29 +0900
Subject: [PATCH 22/29] =?UTF-8?q?chore=20:=20=EC=95=88=EC=93=B0=EB=8A=94?=
 =?UTF-8?q?=20=EB=B9=8C=EB=8D=94=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
 =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/infra/database/mysql/user/entity/UserPointEntity.java  | 2 --
 .../core/service/user/UserPointServiceIntegrationTest.java      | 2 --
 2 files changed, 4 deletions(-)

diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
index 16845b390..a5202623e 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
@@ -9,7 +9,6 @@
 import jakarta.persistence.*;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
@@ -26,7 +25,6 @@
 )
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
-@Builder(access = AccessLevel.PRIVATE)
 public class UserPointEntity {
 
     @Id
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
index 352a61bd4..dcffa0561 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -33,8 +33,6 @@ class UserPointServiceIntegrationTest extends IntegrationTest {
 
     @Autowired
     private UserPointRepository userPointRepository;
-    @Autowired
-    private UserPointService userPointService;
 
     @Nested
     @DisplayName("포인트 충전")

From 02dad317a252e70de24d36f93100eb2a15aaf5f0 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 19 Nov 2025 11:55:53 +0900
Subject: [PATCH 23/29] =?UTF-8?q?feature=20:=20=EC=BF=A0=ED=8F=B0=20?=
 =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=82=99=EA=B4=80=EC=A0=81=EB=9D=BD?=
 =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/error/ApiControllerAdvice.java         |  6 ++++++
 .../application/api/error/ApiErrorType.java    |  4 +++-
 .../core/domain/error/DomainErrorCode.java     |  3 ++-
 .../core/domain/order/AbstractCoupon.java      |  7 ++++++-
 .../core/domain/order/DefaultCoupon.java       | 13 ++++++++-----
 .../order/repository/CouponRepository.java     |  2 --
 .../mysql/coupon/CouponJpaRepository.java      |  8 --------
 .../mysql/coupon/entity/CouponEntity.java      |  9 +++++++--
 .../coupon/impl/CouponRepositoryImpl.java      | 18 ------------------
 .../component/CouponDiscountStrategy.java      |  2 +-
 .../core/service/ConcurrencyTestUtil.java      |  5 +++++
 11 files changed, 38 insertions(+), 39 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index 9b6fe4638..bb6e6f70f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -6,6 +6,7 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.error.NotFoundException;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.OptimisticLockingFailureException;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -143,6 +144,11 @@ public ResponseEntity<ApiResponse<?>> handleNotFound(NotFoundException e) {
         return failureResponse(ApiErrorType.NOT_FOUND, e.getMessage());
     }
 
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleOptimisticLock(OptimisticLockingFailureException exception) {
+        return failureResponse(ApiErrorType.CONFLICT, exception.getMessage());
+    }
+
     private String extractMissingParameter(String message) {
         Pattern pattern = Pattern.compile("'(.+?)'");
         Matcher matcher = pattern.matcher(message);
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
index fa50edbd5..599a5b32d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
@@ -7,7 +7,9 @@
 @Getter
 @RequiredArgsConstructor
 public enum ApiErrorType {
-    /** 범용 에러 */
+    /**
+     * 범용 에러
+     */
     INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."),
     BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."),
     NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."),
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 1bdb8b6fd..d15e283ef 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -19,7 +19,8 @@ public enum DomainErrorCode {
     COULD_NOT_BE_NEGATIVE(1011, "%s는(은) 음수가 될 수 없습니다."),
     NOT_ENOUGH_USER_POINT_BALANCE(1012, "사용자의 포인트 잔액이 충분하지 않습니다."),
     NOT_SUPPORTED_COUPON_TYPE(1013, "지원하지 않는 쿠폰 유형입니다."),
-    NOT_AVAILABLE_COUPON_STATUS(1014, "사용할 수 없는 쿠폰의 상태입니다.");
+    NOT_AVAILABLE_COUPON_STATUS(1014, "사용할 수 없는 쿠폰의 상태입니다."),
+    OTHER_USING_THIS_COUPON(1015, "누군가 이미 사용중인 쿠폰입니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
index 2f2b4b41a..0a65c7b42 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
@@ -27,13 +27,16 @@ public abstract class AbstractCoupon implements Coupon {
 
     protected DeletedAt deletedAt;
 
+    protected Long version;
+
     protected AbstractCoupon(
             CouponId couponId,
             UserId userId,
             CouponStatus status,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
-            DeletedAt deletedAt
+            DeletedAt deletedAt,
+            Long version
     ) {
         this.couponId = couponId;
         this.userId = userId;
@@ -41,6 +44,7 @@ protected AbstractCoupon(
         this.createdAt = createdAt;
         this.updatedAt = updatedAt;
         this.deletedAt = deletedAt;
+        this.version = version;
     }
 
     protected AbstractCoupon(
@@ -52,6 +56,7 @@ protected AbstractCoupon(
         this.createdAt = abstractCoupon.createdAt;
         this.updatedAt = abstractCoupon.updatedAt;
         this.deletedAt = abstractCoupon.deletedAt;
+        this.version = abstractCoupon.version;
     }
 
     @Override
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
index 92db00342..ee70ed942 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java
@@ -21,9 +21,10 @@ private DefaultCoupon(
             CouponStatus status,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
-            DeletedAt deletedAt
+            DeletedAt deletedAt,
+            Long version
     ) {
-        super(couponId, userId, status, createdAt, updatedAt, deletedAt);
+        super(couponId, userId, status, createdAt, updatedAt, deletedAt, version);
     }
 
     protected DefaultCoupon(AbstractCoupon coupon) {
@@ -37,7 +38,8 @@ public static DefaultCoupon create(UserId userId) {
                 CouponStatus.AVAILABLE,
                 CreatedAt.now(),
                 UpdatedAt.now(),
-                DeletedAt.empty()
+                DeletedAt.empty(),
+                0L
         );
     }
 
@@ -47,9 +49,10 @@ public static DefaultCoupon mappedBy(
             CouponStatus status,
             CreatedAt createdAt,
             UpdatedAt updatedAt,
-            DeletedAt deletedAt
+            DeletedAt deletedAt,
+            Long version
     ) {
-        return new DefaultCoupon(couponId, userId, status, createdAt, updatedAt, deletedAt);
+        return new DefaultCoupon(couponId, userId, status, createdAt, updatedAt, deletedAt, version);
     }
 
     @Override
diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
index b86d5d956..481c84875 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/repository/CouponRepository.java
@@ -8,6 +8,4 @@ public interface CouponRepository {
     Coupon save(Coupon coupon);
 
     Coupon getById(CouponId couponId);
-
-    Coupon getByIdWithLock(CouponId couponId);
 }
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
index e26733f74..9e378bc2b 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/CouponJpaRepository.java
@@ -1,16 +1,8 @@
 package com.loopers.core.infra.database.mysql.coupon;
 
 import com.loopers.core.infra.database.mysql.coupon.entity.CouponEntity;
-import jakarta.persistence.LockModeType;
 import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Lock;
-import org.springframework.data.jpa.repository.Query;
-
-import java.util.Optional;
 
 public interface CouponJpaRepository extends JpaRepository<CouponEntity, Long> {
 
-    @Lock(LockModeType.PESSIMISTIC_WRITE)
-    @Query("select c from CouponEntity c where c.id = :id")
-    Optional<CouponEntity> findByIdWithLock(Long id);
 }
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
index 671b63108..27882c1c5 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/entity/CouponEntity.java
@@ -49,6 +49,9 @@ public class CouponEntity {
 
     private LocalDateTime deletedAt;
 
+    @Version
+    private Long version;
+
     public static CouponEntity from(AbstractCoupon coupon) {
         return new CouponEntity(
                 Optional.ofNullable(coupon.getCouponId().value())
@@ -58,7 +61,8 @@ public static CouponEntity from(AbstractCoupon coupon) {
                 coupon.getStatus().name(),
                 coupon.getCreatedAt().value(),
                 coupon.getUpdatedAt().value(),
-                coupon.getDeletedAt().value()
+                coupon.getDeletedAt().value(),
+                coupon.getVersion()
         );
     }
 
@@ -69,7 +73,8 @@ public AbstractCoupon to() {
                 CouponStatus.valueOf(this.status),
                 new CreatedAt(this.createdAt),
                 new UpdatedAt(this.updatedAt),
-                new DeletedAt(this.deletedAt)
+                new DeletedAt(this.deletedAt),
+                this.version
         );
     }
 }
diff --git a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
index b789f37c1..8d018290c 100644
--- a/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
+++ b/core/infra/database/mysql/order-mysql/src/main/java/com/loopers/core/infra/database/mysql/coupon/impl/CouponRepositoryImpl.java
@@ -62,22 +62,4 @@ public Coupon getById(CouponId couponId) {
 
         throw NotFoundException.withName("쿠폰");
     }
-
-    @Override
-    public Coupon getByIdWithLock(CouponId couponId) {
-        CouponEntity coupon = couponRepository.findByIdWithLock(Long.parseLong(couponId.value()))
-                .orElseThrow(() -> NotFoundException.withName("쿠폰"));
-
-        Optional<RateDiscountCouponEntity> rateDiscountCoupon = rateDiscountCouponRepository.findByCouponId(coupon.getId());
-        if (rateDiscountCoupon.isPresent()) {
-            return rateDiscountCoupon.get().to(coupon);
-        }
-
-        Optional<AmountDiscountCouponEntity> amountDiscountCoupon = amountDiscountCouponRepository.findByCouponId(coupon.getId());
-        if (amountDiscountCoupon.isPresent()) {
-            return amountDiscountCoupon.get().to(coupon);
-        }
-
-        throw NotFoundException.withName("쿠폰");
-    }
 }
diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
index 1d7ce5e44..25ecd0365 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/CouponDiscountStrategy.java
@@ -17,7 +17,7 @@ public class CouponDiscountStrategy implements PayAmountDiscountStrategy {
     @Override
     @Transactional
     public PayAmount discount(PayAmount payAmount, CouponId couponId) {
-        Coupon coupon = couponRepository.getByIdWithLock(couponId);
+        Coupon coupon = couponRepository.getById(couponId);
         PayAmount discountedAmount = coupon.discount(payAmount);
         coupon.use();
         couponRepository.save(coupon);
diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java b/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
index 260dcf8fa..716207078 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/ConcurrencyTestUtil.java
@@ -1,5 +1,7 @@
 package com.loopers.core.service;
 
+import lombok.extern.slf4j.Slf4j;
+
 import java.util.List;
 import java.util.concurrent.*;
 import java.util.function.Consumer;
@@ -7,6 +9,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+@Slf4j
 public final class ConcurrencyTestUtil {
 
     private ConcurrencyTestUtil() {
@@ -27,6 +30,8 @@ public static <T> List<T> executeInParallel(
                     try {
                         T result = task.apply(index);
                         results.add(result);
+                    } catch (Exception e) {
+                        System.out.println(e.getMessage() + e.getClass().getName());
                     } finally {
                         latch.countDown();
                     }

From c015b7c9edcf72d1c617b92c83deb3d10d6f3054 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 21:27:52 +0900
Subject: [PATCH 24/29] =?UTF-8?q?feature=20:=20use()=EC=97=90=20updateAt?=
 =?UTF-8?q?=20=EC=B5=9C=EC=8B=A0=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/loopers/core/domain/order/AbstractCoupon.java  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
index 0a65c7b42..da11f4952 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/AbstractCoupon.java
@@ -62,6 +62,7 @@ protected AbstractCoupon(
     @Override
     public void use() {
         this.status = CouponStatus.USED;
+        this.updatedAt = UpdatedAt.now();
     }
 
     public PayAmount discount(PayAmount payAmount) {

From b6238df1984d2d2bc5291df0a0566f15774bd715 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 22:22:23 +0900
Subject: [PATCH 25/29] =?UTF-8?q?feature=20:=20=EB=B8=8C=EB=9E=9C=EB=93=9C?=
 =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/application/api/brand/BrandV1ApiSpec.java    | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
index ca04e59d7..ca8a2566b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/brand/BrandV1ApiSpec.java
@@ -10,9 +10,8 @@
 public interface BrandV1ApiSpec {
 
     @Operation(
-            summary = "사용자 조회",
-            description = "ID로 사용자를 조회합니다."
+            summary = "브랜드 조회",
+            description = "ID로 브랜드를 조회합니다."
     )
     ApiResponse<GetBrandResponse> getBrand(String brandId);
-
 }

From 145aaac6f478fa32a3adaf0fd52b267abe781d77 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 22:23:10 +0900
Subject: [PATCH 26/29] =?UTF-8?q?feature=20:=20=EC=83=81=EC=88=98=EB=AA=85?=
 =?UTF-8?q?=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/core/domain/order/vo/CouponDiscountAmount.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
index 1f855a77c..281417d43 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/order/vo/CouponDiscountAmount.java
@@ -7,7 +7,7 @@
 
 public record CouponDiscountAmount(BigDecimal value) {
 
-    private static final String FILED_NAME = "쿠폰 할인금액";
+    private static final String FIELD_NAME = "쿠폰 할인금액";
 
     public CouponDiscountAmount {
         validateNotNull(value);
@@ -15,12 +15,12 @@ public record CouponDiscountAmount(BigDecimal value) {
     }
 
     private static void validateNotNull(BigDecimal value) {
-        Objects.requireNonNull(value, DomainErrorCode.notNullMessage(FILED_NAME));
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage(FIELD_NAME));
     }
 
     private static void validateNotNegative(BigDecimal value) {
         if (value.signum() < 0) {
-            throw new IllegalArgumentException(DomainErrorCode.negativeMessage(FILED_NAME));
+            throw new IllegalArgumentException(DomainErrorCode.negativeMessage(FIELD_NAME));
         }
     }
 }

From 9bb0fe25be2eaa91f6ac9fd1e07b0846c3cd3f10 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 22:26:24 +0900
Subject: [PATCH 27/29] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?=
 =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B2=B0=EC=A0=9C=20=EB=B2=84?=
 =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/core/service/order/component/OrderCashier.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
index 4abb63397..4b1f6d768 100644
--- a/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
+++ b/core/service/order-service/src/main/java/com/loopers/core/service/order/component/OrderCashier.java
@@ -23,9 +23,9 @@ public class OrderCashier {
     @Transactional
     public Payment checkout(User user, Order order, PayAmount payAmount, CouponId couponId) {
         UserPoint userPoint = userPointRepository.getByUserIdWithLock(user.getId());
-        userPointRepository.save(userPoint.pay(payAmount));
         PayAmountDiscountStrategy discountStrategy = discountStrategySelector.select(couponId);
         PayAmount discountedPayAmount = discountStrategy.discount(payAmount, couponId);
+        userPointRepository.save(userPoint.pay(discountedPayAmount));
         Payment payment = Payment.create(order.getId(), user.getId(), discountedPayAmount);
 
         return paymentRepository.save(payment);

From 357f43c018c7d4a6287b9c18e049f4a9ac7bb8d3 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 22:31:44 +0900
Subject: [PATCH 28/29] =?UTF-8?q?fix=20:=20=EC=95=88=EC=93=B0=EB=8A=94=20I?=
 =?UTF-8?q?mport=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../service/order/OrderQueryServiceIntegrationTest.java  | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
index 348c9caa1..1805e8bc1 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/OrderQueryServiceIntegrationTest.java
@@ -5,6 +5,9 @@
 import com.loopers.core.domain.brand.vo.BrandDescription;
 import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.domain.brand.vo.BrandName;
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
 import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.order.Order;
 import com.loopers.core.domain.order.OrderDetail;
@@ -15,9 +18,6 @@
 import com.loopers.core.domain.order.vo.OrderId;
 import com.loopers.core.domain.order.vo.OrderItemId;
 import com.loopers.core.domain.order.vo.Quantity;
-import com.loopers.core.domain.common.vo.CreatedAt;
-import com.loopers.core.domain.common.vo.UpdatedAt;
-import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.product.Product;
 import com.loopers.core.domain.product.repository.ProductRepository;
 import com.loopers.core.domain.product.vo.ProductId;
@@ -26,12 +26,9 @@
 import com.loopers.core.domain.product.vo.ProductStock;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
-import com.loopers.core.domain.user.type.UserGender;
-import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserIdentifier;
-import com.loopers.core.domain.brand.vo.BrandId;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.order.query.GetOrderDetailQuery;
 import com.loopers.core.service.order.query.GetOrderListQuery;

From 5497c3f726fe734dcd675928f88c8bae18f6c75b Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 20 Nov 2025 22:37:00 +0900
Subject: [PATCH 29/29] =?UTF-8?q?test=20:=20OrderCashierTest=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/service/order/component/OrderCashierTest.java   | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
index 418ec965f..befd0ef06 100644
--- a/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
+++ b/core/service/order-service/src/test/java/com/loopers/core/service/order/component/OrderCashierTest.java
@@ -22,6 +22,7 @@
 
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.instancio.Select.field;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
 
 @DisplayName("OrderCashier 단위 테스트")
@@ -31,6 +32,12 @@ class OrderCashierTest {
     @Mock
     private UserPointRepository userPointRepository;
 
+    @Mock
+    private PayAmountDiscountStrategySelector strategySelector;
+
+    @Mock
+    private PayAmountDiscountStrategy discountStrategy;
+
     @InjectMocks
     private OrderCashier orderCashier;
 
@@ -73,6 +80,8 @@ void setUp() {
                 largePayAmount = new PayAmount(new BigDecimal("100000"));
 
                 when(userPointRepository.getByUserIdWithLock(user.getId())).thenReturn(userPoint);
+                when(strategySelector.select(any())).thenReturn(discountStrategy);
+                when(discountStrategy.discount(any(), any())).thenReturn(largePayAmount);
             }
 
             @Test
