diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java
new file mode 100644
index 000000000..4186c20fa
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java
@@ -0,0 +1,23 @@
+package com.loopers.application.brand;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandService;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class BrandFacade {
+
+  private final BrandService brandService;
+
+  public BrandResult viewBrand(Long brandId) {
+    Brand brand = brandService.getById(brandId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다."));
+    return BrandResult.from(brand);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandResult.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandResult.java
new file mode 100644
index 000000000..1984fe5db
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandResult.java
@@ -0,0 +1,18 @@
+package com.loopers.application.brand;
+
+import com.loopers.domain.brand.Brand;
+
+public record BrandResult(
+    Long brandId,
+    String name,
+    String description
+) {
+
+  public static BrandResult from(Brand brand) {
+    return new BrandResult(
+        brand.getId(),
+        brand.getName(),
+        brand.getDescription()
+    );
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
new file mode 100644
index 000000000..9d2244263
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
@@ -0,0 +1,42 @@
+package com.loopers.application.like;
+
+import com.loopers.domain.product.ProductService;
+import com.loopers.domain.productlike.ProductLikeService;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class LikeFacade {
+
+  private final ProductLikeService productLikeService;
+  private final ProductService productService;
+
+  @Transactional
+  public void likeProduct(Long userId, Long productId) {
+    productService.getById(productId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
+    try {
+      productLikeService.createLike(userId, productId);
+      productService.increaseLikeCount(productId);
+    } catch (DataIntegrityViolationException e) {
+
+    }
+  }
+
+  @Transactional
+  public void unlikeProduct(Long userId, Long productId) {
+    productService.getById(productId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
+
+    int deleted = productLikeService.deleteLike(userId, productId);
+
+    if (deleted > 0) {
+      productService.decreaseLikeCount(productId);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
new file mode 100644
index 000000000..e8553ee12
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
@@ -0,0 +1,57 @@
+package com.loopers.application.order;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderCreateService;
+import com.loopers.domain.order.OrderPreparation;
+import com.loopers.domain.order.OrderService;
+import com.loopers.domain.order.orderitem.OrderItemCommand;
+import com.loopers.domain.point.PointService;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductService;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Component
+@RequiredArgsConstructor
+public class OrderFacade {
+
+  private final OrderService orderService;
+  private final OrderCreateService orderCreateService;
+  private final ProductService productService;
+  private final PointService pointService;
+
+  @Transactional
+  public Order createOrder(Long userId, List<OrderItemCommand> commands, LocalDateTime orderedAt) {
+    Objects.requireNonNull(userId, "유저아이디는 null 일수 없습니다.");
+    Objects.requireNonNull(commands, "상품 주문 정보는 null일 수 없습니다.");
+    Objects.requireNonNull(orderedAt, "주문 요청 시간은 null일 수 없습니다");
+
+    Map<Long, Product> productById = getProductMapWithLock(commands);
+
+    OrderPreparation result = orderCreateService.prepareOrder(commands, productById);
+
+    pointService.deduct(userId, result.totalAmount());
+
+    result.orderItems().decreaseStock(productById);
+
+    return orderService.create(userId, result.orderItems().getItems(), orderedAt);
+  }
+
+  private Map<Long, Product> getProductMapWithLock(List<OrderItemCommand> commands) {
+    List<Long> productIds = commands.stream()
+        .map(OrderItemCommand::productId)
+        .distinct()
+        .toList();
+
+    return productService.findByIdsWithLock(productIds).stream()
+        .collect(Collectors.toMap(Product::getId, Function.identity()));
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java
index 07f30f516..3f6bd6940 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java
@@ -1,9 +1,10 @@
 package com.loopers.application.point;
 
 import com.loopers.domain.point.PointService;
-import com.loopers.domain.point.Point;
+import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @Component
 @RequiredArgsConstructor
@@ -11,12 +12,13 @@ public class PointFacade {
 
     private final PointService pointService;
 
-    public PointResult findPointOrNull(String userId) {
-      Point point = pointService.findByUserId(userId);
-      return point == null ? null : PointResult.from(point);
+    public Optional<PointResult> getPoint(Long userId) {
+      return pointService.findByUserId(userId)
+          .map(PointResult::from);
     }
 
-    public PointResult charge(String userId, Long amount) {
+    @Transactional
+    public PointResult charge(Long userId, Long amount) {
       return PointResult.from(pointService.charge(userId, amount));
     }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointResult.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointResult.java
index 595f53a06..1cdb83fb1 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointResult.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointResult.java
@@ -4,7 +4,7 @@
 import java.util.Objects;
 
 public record PointResult(
-    String userId,
+    Long userId,
     Long amount
 ) {
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/BrandSummary.java b/apps/commerce-api/src/main/java/com/loopers/application/product/BrandSummary.java
new file mode 100644
index 000000000..5bb03e023
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/BrandSummary.java
@@ -0,0 +1,16 @@
+package com.loopers.application.product;
+
+import com.loopers.domain.brand.Brand;
+
+public record BrandSummary(
+    Long id,
+    String name
+) {
+
+  public static BrandSummary from(Brand brand) {
+    return new BrandSummary(
+        brand.getId(),
+        brand.getName()
+    );
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetail.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetail.java
new file mode 100644
index 000000000..1e19b89c7
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetail.java
@@ -0,0 +1,40 @@
+package com.loopers.application.product;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.product.Product;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
+public record ProductDetail(
+    Long productId,
+    String productName,
+    Long price,
+    String description,
+    Long stock,
+    Long brandId,
+    String brandName,
+    Long likeCount,
+    boolean liked
+) {
+
+  public static ProductDetail of(Product product, Brand brand, boolean liked) {
+    if (product == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_DETAIL_PRODUCT_EMPTY, "상품 정보는 필수입니다.");
+    }
+    if (brand == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_DETAIL_BRAND_EMPTY, "브랜드 정보는 필수입니다.");
+    }
+
+    return new ProductDetail(
+        product.getId(),
+        product.getName(),
+        product.getPriceValue(),
+        product.getDescription(),
+        product.getStockValue(),
+        brand.getId(),
+        brand.getName(),
+        product.getLikeCount(),
+        liked
+    );
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
new file mode 100644
index 000000000..eaf4c9846
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
@@ -0,0 +1,74 @@
+package com.loopers.application.product;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandService;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductService;
+import com.loopers.domain.productlike.ProductLikeService;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ProductFacade {
+
+  private final ProductService productService;
+  private final BrandService brandService;
+  private final ProductLikeService productLikeService;
+
+  public Page<ProductDetail> searchProductDetails(Long brandId, Long userId, Pageable pageable) {
+    Page<Product> productPage = productService.findProducts(brandId, pageable);
+    List<Product> products = productPage.getContent();
+
+    Map<Long, Brand> brandById = getBrandById(products);
+    Map<Long, Boolean> likeStatusByProductId = getLikeStatusByProductId(userId, products);
+
+    return getProductDetails(productPage, brandById, likeStatusByProductId);
+  }
+
+
+  private Map<Long, Brand> getBrandById(List<Product> products) {
+    List<Long> brandIds = products.stream()
+        .map(Product::getBrandId)
+        .distinct()
+        .toList();
+
+    return brandService.findByIdIn(brandIds).stream()
+        .collect(Collectors.toMap(Brand::getId, Function.identity()));
+  }
+
+  private Map<Long, Boolean> getLikeStatusByProductId(Long userId, List<Product> products) {
+    List<Long> productIds = products.stream()
+        .map(Product::getId)
+        .toList();
+
+    return productLikeService.findLikeStatusByProductId(userId, productIds);
+  }
+
+  private Page<ProductDetail> getProductDetails(Page<Product> productPage, Map<Long, Brand> brandById,
+      Map<Long, Boolean> likeStatusByProductId) {
+    return productPage.map(product -> ProductDetail.of(
+        product,
+        brandById.get(product.getBrandId()),
+        likeStatusByProductId.getOrDefault(product.getId(), false)
+    ));
+  }
+
+  public ProductDetail viewProductDetail(Long productId, Long userId) {
+    Product product = productService.getById(productId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
+    Brand brand = brandService.getById(product.getBrandId())
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다."));
+    boolean isLiked = productLikeService.isLiked(userId, productId);
+
+    return ProductDetail.of(product, brand, isLiked);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
index 7d0cc9ed0..49eb178f7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
@@ -15,13 +15,13 @@
 public class UserFacade {
     private final UserService userService;
 
-    public UserResult register(String userId, String email, LocalDate birth, Gender gender) {
-        User user = userService.registerUser(userId, email, birth, gender);
+    public UserResult register(String loginId, String email, LocalDate birth, Gender gender) {
+        User user = userService.registerUser(loginId, email, birth, gender);
         return UserResult.from(user);
     }
 
-    public UserResult getUser(String userId) {
-        User user = userService.findById(userId);
+    public UserResult lookupUserProfile(String loginId) {
+        User user = userService.findById(loginId);
         if (user == null) {
             throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다.");
         }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserResult.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserResult.java
index 566ca0272..1e9b68e48 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserResult.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserResult.java
@@ -5,10 +5,10 @@
 
 import java.time.LocalDate;
 
-public record UserResult(String userId, String email, LocalDate birth, Gender gender) {
+public record UserResult(String loginId, String email, LocalDate birth, Gender gender) {
     public static UserResult from(User user) {
         return new UserResult(
-            user.getUserId(),
+            user.getLoginId(),
             user.getEmail(),
             user.getBirth(),
             user.getGender()
diff --git a/apps/commerce-api/src/main/java/com/loopers/config/ClockConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/ClockConfig.java
new file mode 100644
index 000000000..ca76c1896
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/config/ClockConfig.java
@@ -0,0 +1,14 @@
+package com.loopers.config;
+
+import java.time.Clock;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ClockConfig {
+
+  @Bean
+  public Clock clock() {
+    return Clock.systemDefaultZone();
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java
new file mode 100644
index 000000000..9d1f0dbbc
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java
@@ -0,0 +1,61 @@
+package com.loopers.domain.brand;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.util.Objects;
+
+@Entity
+@Table(name = "brand")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Brand extends BaseEntity {
+
+  private static final int MAX_NAME_LENGTH = 50;
+
+  @Column(nullable = false, length = MAX_NAME_LENGTH)
+  private String name;
+
+  @Column(length = 200)
+  private String description;
+
+  private Brand(String name, String description) {
+    validateName(name);
+    this.name = name;
+    this.description = description;
+  }
+
+  public static Brand of(String name, String description) {
+    return new Brand(name, description);
+  }
+
+  public static Brand of(String name) {
+    return new Brand(name, null);
+  }
+
+  private void validateName(String name) {
+    if (name == null || name.isBlank()) {
+      throw new CoreException(ErrorType.INVALID_BRAND_NAME_EMPTY);
+    }
+    if (name.length() > MAX_NAME_LENGTH) {
+      throw new CoreException(ErrorType.INVALID_BRAND_NAME_LENGTH);
+    }
+  }
+
+  public boolean isSameId(Long otherId) {
+    return Objects.equals(this.getId(), otherId);
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java
new file mode 100644
index 000000000..675d24718
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java
@@ -0,0 +1,13 @@
+package com.loopers.domain.brand;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface BrandRepository {
+  
+  Optional<Brand> findById(Long id);
+
+  List<Brand> findByIdIn(List<Long> ids);
+
+  Brand save(Brand brand);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java
new file mode 100644
index 000000000..8f7385729
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java
@@ -0,0 +1,24 @@
+package com.loopers.domain.brand;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Optional;
+
+@Component
+@RequiredArgsConstructor
+public class BrandService {
+
+  private final BrandRepository brandRepository;
+
+
+  public Optional<Brand> getById(Long brandId) {
+    return brandRepository.findById(brandId);
+  }
+
+
+  public List<Brand> findByIdIn(List<Long> brandIds) {
+    return brandRepository.findByIdIn(brandIds);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brands.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brands.java
new file mode 100644
index 000000000..747b85a19
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brands.java
@@ -0,0 +1,32 @@
+package com.loopers.domain.brand;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class Brands {
+
+  private final List<Brand> values;
+
+  private Brands(List<Brand> values) {
+    this.values = values;
+  }
+
+  public static Brands from(List<Brand> brands) {
+    Objects.requireNonNull(brands, "brands는 null일 수 없습니다.");
+    return new Brands(List.copyOf(brands));
+  }
+
+  public Brand getBrandById(Long brandId) {
+    return values.stream()
+        .filter(brand -> brand.isSameId(brandId))
+        .findFirst()
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다."));
+  }
+
+  public List<Brand> toList() {
+    return new ArrayList<>(values);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/money/Money.java b/apps/commerce-api/src/main/java/com/loopers/domain/money/Money.java
new file mode 100644
index 000000000..8b1dc934d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/money/Money.java
@@ -0,0 +1,38 @@
+package com.loopers.domain.money;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EqualsAndHashCode
+public class Money {
+
+  private Long value;
+
+  private Money(Long value) {
+    if (value == null) {
+      throw new CoreException(ErrorType.INVALID_MONEY_VALUE);
+    }
+    if (value < 0) {
+      throw new CoreException(ErrorType.NEGATIVE_MONEY_VALUE);
+    }
+    this.value = value;
+  }
+
+  public static Money zero() {
+    return new Money(0L);
+  }
+
+  public static Money of(Long value) {
+    return new Money(value);
+  }
+
+  public Long getValue() {
+    return value;
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
new file mode 100644
index 000000000..14e3116cf
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
@@ -0,0 +1,118 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.*;
+import java.util.Objects;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Order Entity (Aggregate Root)
+ * 사용자의 주문 정보
+ * OrderItem의 생명주기를 관리
+ */
+@Entity
+@Table(name = "orders")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Order extends BaseEntity {
+
+  @Column(name = "ref_user_id", nullable = false)
+  private Long userId;
+
+  @Enumerated(EnumType.STRING)
+  @Column(nullable = false)
+  private OrderStatus status;
+
+  @Embedded
+  @AttributeOverride(name = "value", column = @Column(name = "total_amount"))
+  private Money totalAmount;
+
+  @Column(name = "ordered_at", nullable = false)
+  private LocalDateTime orderedAt;
+
+  @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
+  private List<OrderItem> items = new ArrayList<>();
+
+  private Order(Long userId, OrderStatus status, Money totalAmount, LocalDateTime orderedAt) {
+    validateUserId(userId);
+    validateStatus(status);
+    validateTotalAmount(totalAmount);
+    validateOrderedAt(orderedAt);
+
+    this.userId = userId;
+    this.status = status;
+    this.totalAmount = totalAmount;
+    this.orderedAt = orderedAt;
+  }
+
+  public static Order of(Long userId, OrderStatus status, Long totalAmount, LocalDateTime orderedAt) {
+    return new Order(userId, status, Money.of(totalAmount), orderedAt);
+  }
+
+  public Long getTotalAmountValue() {
+    return totalAmount.getValue();
+  }
+
+
+  public void addItem(OrderItem item) {
+    Objects.requireNonNull(item, "OrderItem은 null일 수 없습니다.");
+    this.items.add(item);
+    item.assignOrder(this);
+  }
+
+
+  public void complete() {
+    if (this.status != OrderStatus.PENDING) {
+      throw new CoreException(ErrorType.ORDER_CANNOT_COMPLETE);
+    }
+    this.status = OrderStatus.COMPLETED;
+  }
+
+  public void failPayment() {
+    if (this.status != OrderStatus.PENDING) {
+      throw new CoreException(ErrorType.ORDER_CANNOT_FAIL_PAYMENT);
+    }
+    this.status = OrderStatus.PAYMENT_FAILED;
+  }
+
+  public void retryComplete() {
+    if (this.status != OrderStatus.PAYMENT_FAILED) {
+      throw new CoreException(ErrorType.ORDER_CANNOT_RETRY_COMPLETE);
+    }
+    this.status = OrderStatus.COMPLETED;
+  }
+
+  private void validateUserId(Long userId) {
+    if (userId == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_USER_EMPTY);
+    }
+  }
+
+  private void validateStatus(OrderStatus status) {
+    if (status == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_STATUS_EMPTY);
+    }
+  }
+
+  private void validateTotalAmount(Money totalAmount) {
+    if (totalAmount == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_TOTAL_AMOUNT_EMPTY);
+    }
+  }
+
+  private void validateOrderedAt(LocalDateTime orderedAt) {
+    if (orderedAt == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_ORDERED_AT_EMPTY);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCreateService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCreateService.java
new file mode 100644
index 000000000..3d4b1dff9
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderCreateService.java
@@ -0,0 +1,50 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.domain.order.orderitem.OrderItemCommand;
+import com.loopers.domain.order.orderitem.OrderItems;
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.domain.product.Product;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Component
+public class OrderCreateService {
+
+  public OrderPreparation prepareOrder(List<OrderItemCommand> commands,
+                                          Map<Long, Product> productById) {
+    OrderItems orderItems = createOrderItems(commands, productById);
+
+    orderItems.validateStock(productById);
+
+    Long totalAmount = orderItems.calculateTotalAmount();
+
+    return new OrderPreparation(orderItems, totalAmount);
+  }
+
+  private OrderItems createOrderItems(List<OrderItemCommand> commands,
+                                      Map<Long, Product> productById) {
+    List<OrderItem> items = commands.stream()
+        .map(cmd -> createOrderItem(cmd, productById))
+        .toList();
+
+    return new OrderItems(items);
+  }
+
+  private OrderItem createOrderItem(OrderItemCommand command, Map<Long, Product> productById) {
+    Product product = Optional.ofNullable(productById.get(command.productId()))
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
+
+    return OrderItem.of(
+        product.getId(),
+        product.getName(),
+        command.quantity(),
+        OrderPrice.of(product.getPriceValue())
+    );
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderListDto.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderListDto.java
new file mode 100644
index 000000000..afe4d70dc
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderListDto.java
@@ -0,0 +1,17 @@
+package com.loopers.domain.order;
+
+import java.time.LocalDateTime;
+
+/**
+ * Order 목록 조회용 DTO (페이징)
+ * DTO 프로젝션 + SIZE() 사용으로 N+1 및 메모리 페이징 방지
+ */
+public record OrderListDto(
+    Long id,
+    Long userId,
+    OrderStatus status,
+    Long totalAmount,
+    LocalDateTime orderedAt,
+    Integer itemCount
+) {
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparation.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparation.java
new file mode 100644
index 000000000..847deb5f6
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparation.java
@@ -0,0 +1,9 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItems;
+
+public record OrderPreparation(
+    OrderItems orderItems,
+    Long totalAmount
+) {
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java
new file mode 100644
index 000000000..109b4e27a
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java
@@ -0,0 +1,24 @@
+package com.loopers.domain.order;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+
+public interface OrderRepository {
+
+
+  Optional<Order> findById(Long id);
+
+  /**
+   * 주문 ID로 OrderItem과 함께 조회 (@EntityGraph 사용)
+   */
+  Optional<Order> findWithItemsById(Long id);
+
+  /**
+   * 사용자별 주문 목록 조회 (DTO 프로젝션, 페이징)
+   */
+  Page<OrderListDto> findOrderList(Long userId, Pageable pageable);
+
+  Order save(Order order);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
new file mode 100644
index 000000000..089566399
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
@@ -0,0 +1,64 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@Component
+@RequiredArgsConstructor
+public class OrderService {
+
+  private final OrderRepository orderRepository;
+
+  public Optional<Order> getById(Long orderId) {
+    return orderRepository.findById(orderId);
+  }
+
+  public Optional<Order> getWithItemsById(Long orderId) {
+    return orderRepository.findWithItemsById(orderId);
+  }
+
+  public Order create(Long userId, List<OrderItem> orderItems, LocalDateTime orderedAt) {
+    Long totalAmount = calculateTotalAmount(orderItems);
+
+    Order order = Order.of(userId, OrderStatus.PENDING, totalAmount, orderedAt);
+
+    orderItems.forEach(order::addItem);
+
+    return orderRepository.save(order);
+  }
+
+  public Order completeOrder(Long orderId) {
+    Order order = getById(orderId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "주문을 찾을 수 없습니다."));
+    order.complete();
+    return orderRepository.save(order);
+  }
+
+  public Order failPaymentOrder(Long orderId) {
+    Order order = getById(orderId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "주문을 찾을 수 없습니다."));
+    order.failPayment();
+    return orderRepository.save(order);
+  }
+
+  public Order retryCompleteOrder(Long orderId) {
+    Order order = getById(orderId)
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "주문을 찾을 수 없습니다."));
+    order.retryComplete();
+    return orderRepository.save(order);
+  }
+
+  private Long calculateTotalAmount(List<OrderItem> orderItems) {
+    return orderItems.stream()
+        .mapToLong(item -> item.getOrderPriceValue() * item.getQuantityValue())
+        .sum();
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
new file mode 100644
index 000000000..220dc321b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
@@ -0,0 +1,18 @@
+package com.loopers.domain.order;
+
+
+public enum OrderStatus {
+  PENDING("주문 생성 완료"),
+  COMPLETED("결제 완료"),
+  PAYMENT_FAILED("결제 실패");
+
+  private final String description;
+
+  OrderStatus(String description) {
+    this.description = description;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItem.java
new file mode 100644
index 000000000..7b2aed119
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItem.java
@@ -0,0 +1,108 @@
+package com.loopers.domain.order.orderitem;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.*;
+import java.util.Objects;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * OrderItem Entity
+ * 주문에 포함된 개별 상품
+ * 주문 당시의 상품명, 가격, 수량을 스냅샷으로 저장 (Product 변동 영향 없음)
+ */
+@Entity
+@Table(name = "order_item")
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class OrderItem extends BaseEntity {
+
+  private static final int MAX_PRODUCT_NAME_LENGTH = 100;
+
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "ref_order_id", nullable = false)
+  private Order order;
+
+  @Column(name = "ref_product_id", nullable = false)
+  private Long productId;
+
+  @Column(name = "product_name", nullable = false, length = MAX_PRODUCT_NAME_LENGTH)
+  private String productName;
+
+  @Embedded
+  @AttributeOverride(name = "value", column = @Column(name = "quantity", nullable = false))
+  private Quantity quantity;
+
+  @Embedded
+  @AttributeOverride(name = "value", column = @Column(name = "order_price", nullable = false))
+  private OrderPrice orderPrice;
+
+  private OrderItem(Long productId, String productName, Quantity quantity, OrderPrice orderPrice) {
+    validateProductId(productId);
+    validateProductName(productName);
+    validateQuantity(quantity);
+    validateOrderPrice(orderPrice);
+
+    this.productId = productId;
+    this.productName = productName;
+    this.quantity = quantity;
+    this.orderPrice = orderPrice;
+  }
+
+  /**
+   * OrderItem 생성 팩토리 메서드
+   * Order는 addItem()을 통해 설정됨
+   */
+  public static OrderItem of(Long productId, String productName, Quantity quantity, OrderPrice orderPrice) {
+    return new OrderItem(productId, productName, quantity, orderPrice);
+  }
+
+  /**
+   * Order 할당 (양방향 관계 설정용)
+   * Order.addItem()에서만 호출되어야 함
+   */
+  public void assignOrder(Order order) {
+    Objects.requireNonNull(order, "Order는 null일 수 없습니다.");
+    this.order = order;
+  }
+
+  public Long getOrderPriceValue() {
+    return orderPrice.getValue();
+  }
+
+  public Long getQuantityValue() {
+    return quantity.getValue();
+  }
+
+  private void validateProductId(Long productId) {
+    if (productId == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_ITEM_PRODUCT_EMPTY);
+    }
+  }
+
+  private void validateProductName(String productName) {
+    if (productName == null || productName.isBlank()) {
+      throw new CoreException(ErrorType.INVALID_ORDER_ITEM_PRODUCT_NAME_EMPTY);
+    }
+    if (productName.length() > MAX_PRODUCT_NAME_LENGTH) {
+      throw new CoreException(ErrorType.INVALID_ORDER_ITEM_PRODUCT_NAME_LENGTH);
+    }
+  }
+
+  private void validateQuantity(Quantity quantity) {
+    if (quantity == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_ITEM_QUANTITY_EMPTY);
+    }
+  }
+
+  private void validateOrderPrice(OrderPrice orderPrice) {
+    if (orderPrice == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_PRICE_VALUE);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItemCommand.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItemCommand.java
new file mode 100644
index 000000000..9935a8d70
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItemCommand.java
@@ -0,0 +1,13 @@
+package com.loopers.domain.order.orderitem;
+
+import com.loopers.domain.quantity.Quantity;
+
+public record OrderItemCommand(
+    Long productId,
+    Quantity quantity
+) {
+
+  public static OrderItemCommand of(Long productId, Quantity quantity) {
+    return new OrderItemCommand(productId, quantity);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItems.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItems.java
new file mode 100644
index 000000000..e45c5ae5b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderItems.java
@@ -0,0 +1,47 @@
+package com.loopers.domain.order.orderitem;
+
+import com.loopers.domain.product.Product;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class OrderItems {
+
+  private final List<OrderItem> items;
+
+  public OrderItems(List<OrderItem> items) {
+    this.items = items;
+  }
+
+  public Long calculateTotalAmount() {
+    return items.stream()
+        .mapToLong(item -> item.getOrderPriceValue() * item.getQuantityValue())
+        .sum();
+  }
+
+  public void validateStock(Map<Long, Product> productById) {
+    for (OrderItem item : items) {
+      Product product = findById(productById, item);
+      product.validateStockForOrder(item.getQuantityValue());
+    }
+  }
+
+  public void decreaseStock(Map<Long, Product> productById) {
+    for (OrderItem item : items) {
+      Product product = findById(productById, item);
+      product.decreaseStock(item.getQuantityValue());
+    }
+  }
+
+  private Product findById(Map<Long, Product> productById, OrderItem item) {
+    return Optional.ofNullable(productById.get(item.getProductId()))
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
+  }
+
+  public List<OrderItem> getItems() {
+    return List.copyOf(items);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderPrice.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderPrice.java
new file mode 100644
index 000000000..71ed6ea4a
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/orderitem/OrderPrice.java
@@ -0,0 +1,34 @@
+package com.loopers.domain.order.orderitem;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EqualsAndHashCode
+public class OrderPrice {
+
+  private Long value;
+
+  private OrderPrice(Long value) {
+    if (value == null) {
+      throw new CoreException(ErrorType.INVALID_ORDER_PRICE_VALUE);
+    }
+    if (value < 0) {
+      throw new CoreException(ErrorType.NEGATIVE_ORDER_PRICE_VALUE);
+    }
+    this.value = value;
+  }
+
+  public static OrderPrice of(Long value) {
+    return new OrderPrice(value);
+  }
+
+  public Long getValue() {
+    return value;
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java
index af92be24e..d387d096d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java
@@ -1,16 +1,18 @@
 package com.loopers.domain.point;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.user.User;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
+import lombok.Getter;
 
 @Entity
 @Table(name = "point")
 public class Point extends BaseEntity {
 
-  @OneToOne(fetch = FetchType.LAZY, optional = false)
-  @JoinColumn(name = "user_id", nullable = false, unique = true)
-  private User user;
+  @Getter
+  @Column(name = "ref_user_id", nullable = false, unique = true)
+  private Long userId;
 
   @Embedded
   private PointAmount amount;
@@ -18,27 +20,34 @@ public class Point extends BaseEntity {
   protected Point() {
   }
 
-  private Point(User user, PointAmount amount) {
-    validateUser(user);
-    this.user = user;
+  private Point(Long userId, PointAmount amount) {
+    validateUserId(userId);
+    validateAmount(amount);
+    this.userId = userId;
     this.amount = amount;
   }
 
-  public static Point of(User user, Long amount) {
-    return new Point(user, PointAmount.of(amount));
+  public static Point of(Long userId, Long amount) {
+    return new Point(userId, PointAmount.of(amount));
   }
 
-  public static Point of(User user, PointAmount amount) {
-    return new Point(user, amount);
+  public static Point of(Long userId, PointAmount amount) {
+    return new Point(userId, amount);
   }
 
-  public static Point zero(User user) {
-    return new Point(user, PointAmount.zero());
+  public static Point zero(Long userId) {
+    return new Point(userId, PointAmount.zero());
   }
 
-  private void validateUser(User user) {
-    if (user == null) {
-      throw new IllegalArgumentException("사용자는 비어있을 수 없습니다.");
+  private void validateUserId(Long userId) {
+    if (userId == null) {
+      throw new CoreException(ErrorType.INVALID_POINT_USER_EMPTY);
+    }
+  }
+
+  private void validateAmount(PointAmount amount) {
+    if (amount == null) {
+      throw new CoreException(ErrorType.INVALID_POINT_AMOUNT_EMPTY);
     }
   }
 
@@ -46,12 +55,23 @@ public void charge(Long chargeAmount) {
     this.amount = this.amount.add(chargeAmount);
   }
 
-  public String getUserId() {
-    return user.getUserId();
+  public void deduct(Long deductAmount) {
+    this.amount = this.amount.subtract(deductAmount);
+  }
+
+  public boolean hasEnoughBalance(Long requiredAmount) {
+    if (requiredAmount == null || requiredAmount < 0) {
+      return false;
+    }
+    return this.amount.getValue() >= requiredAmount;
+  }
+
+  public boolean isNotEnough(Long requiredAmount) {
+    return !hasEnoughBalance(requiredAmount);
   }
 
   public Long getAmountValue() {
-    return amount.getAmount();
+    return amount.getValue();
   }
 
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointAmount.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointAmount.java
index 3c6ad1ea8..4e0bebe78 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointAmount.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointAmount.java
@@ -1,5 +1,7 @@
 package com.loopers.domain.point;
 
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Embeddable;
 import lombok.AccessLevel;
@@ -14,16 +16,16 @@
 public class PointAmount {
 
   @Column(name = "amount", nullable = false)
-  private Long amount;
+  private Long value;
 
-  private PointAmount(Long amount) {
-    if (amount == null) {
-      throw new IllegalArgumentException("포인트 금액은 비어있을 수 없습니다.");
+  private PointAmount(Long value) {
+    if (value == null) {
+      throw new CoreException(ErrorType.INVALID_POINT_AMOUNT_EMPTY);
     }
-    if (amount < 0) {
-      throw new IllegalArgumentException("포인트 금액은 음수일 수 없습니다.");
+    if (value < 0) {
+      throw new CoreException(ErrorType.NEGATIVE_POINT_AMOUNT);
     }
-    this.amount = amount;
+    this.value = value;
   }
 
   public static PointAmount zero() {
@@ -36,9 +38,19 @@ public static PointAmount of(Long amount) {
 
   public PointAmount add(Long chargeAmount) {
     if (chargeAmount == null || chargeAmount <= 0) {
-      throw new IllegalArgumentException("충전 금액은 0보다 커야 합니다.");
+      throw new CoreException(ErrorType.INVALID_CHARGE_AMOUNT);
     }
-    return PointAmount.of(this.amount + chargeAmount);
+    return PointAmount.of(this.value + chargeAmount);
+  }
+
+  public PointAmount subtract(Long deductAmount) {
+    if (deductAmount == null || deductAmount <= 0) {
+      throw new CoreException(ErrorType.INVALID_DEDUCT_AMOUNT);
+    }
+    if (this.value < deductAmount) {
+      throw new CoreException(ErrorType.INSUFFICIENT_POINT_BALANCE);
+    }
+    return PointAmount.of(this.value - deductAmount);
   }
 
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java
index 5f93cc325..c1c94e28b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java
@@ -3,7 +3,10 @@
 import java.util.Optional;
 
 public interface PointRepository {
-    Point save(Point point);
-    Point findByUserId(String userId);
-    Optional<Point> findByUserIdWithLock(String userId);
+
+  Point save(Point point);
+
+  Optional<Point> findByUserId(Long userId);
+
+  Optional<Point> findByUserIdWithLock(Long userId);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java
index d03efb8a2..84629c6a7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java
@@ -2,28 +2,36 @@
 
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
+import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
 @Component
 @RequiredArgsConstructor
-@Transactional(readOnly = true)
 public class PointService {
 
     private final PointRepository pointRepository;
 
-    public Point findByUserId(String userId) {
+    public Optional<Point> findByUserId(Long userId) {
       return pointRepository.findByUserId(userId);
     }
 
-
     @Transactional
-    public Point charge(String userId, Long chargeAmount) {
+    public Point charge(Long userId, Long chargeAmount) {
         Point point = pointRepository.findByUserIdWithLock(userId)
             .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
                 "포인트 정보를 찾을 수 없습니다."));
         point.charge(chargeAmount);
         return pointRepository.save(point);
     }
+
+    @Transactional
+    public Point deduct(Long userId, Long deductAmount) {
+        Point point = pointRepository.findByUserIdWithLock(userId)
+            .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
+                "포인트 정보를 찾을 수 없습니다."));
+        point.deduct(deductAmount);
+        return pointRepository.save(point);
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java
new file mode 100644
index 000000000..132dea3ee
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java
@@ -0,0 +1,140 @@
+package com.loopers.domain.product;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.stock.Stock;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.AttributeOverride;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "product")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class Product extends BaseEntity {
+
+  private static final int MAX_NAME_LENGTH = 100;
+
+  @Column(nullable = false, length = MAX_NAME_LENGTH)
+  private String name;
+
+  @Embedded
+  @AttributeOverride(name = "value", column = @Column(name = "price"))
+  private Money price;
+
+  @Column(length = 200)
+  private String description;
+
+  @Embedded
+  @AttributeOverride(name = "value", column = @Column(name = "stock"))
+  private Stock stock;
+
+  @Column(nullable = false)
+  private Long likeCount = 0L;
+
+  @Column(name = "ref_brand_id", nullable = false)
+  private Long brandId;
+
+  private Product(String name, Money price, String description, Stock stock, Long brandId) {
+    validateName(name);
+    validatePrice(price);
+    validateStock(stock);
+    validateBrandId(brandId);
+    this.name = name;
+    this.price = price;
+    this.description = description;
+    this.stock = stock;
+    this.brandId = brandId;
+  }
+
+  public static Product of(String name, Money price, String description, Stock stock, Long brandId) {
+    return new Product(name, price, description, stock, brandId);
+  }
+
+  private void validateName(String name) {
+    if (name == null || name.isBlank()) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_NAME_EMPTY);
+    }
+    if (name.length() > MAX_NAME_LENGTH) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_NAME_LENGTH);
+    }
+  }
+
+  private void validatePrice(Money price) {
+    if (price == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_PRICE_EMPTY);
+    }
+  }
+
+  private void validateStock(Stock stock) {
+    if (stock == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_STOCK_EMPTY);
+    }
+  }
+
+  private void validateBrandId(Long brandId) {
+    if (brandId == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_BRAND_EMPTY);
+    }
+  }
+
+  public void decreaseStock(Long amount) {
+    this.stock = this.stock.decrease(amount);
+  }
+
+  public boolean isAvailable() {
+    return stock.isAvailable();
+  }
+
+  public boolean isNotAvailable() {
+    return !isAvailable();
+  }
+
+  public boolean isSameId(Long id) {
+    return id != null && id.equals(getId());
+  }
+
+  public void validateStockForOrder(Long requestedQuantity) {
+    if (isNotAvailable()) {
+      throw new CoreException(ErrorType.INSUFFICIENT_STOCK,
+          String.format("상품 [%s]의 재고가 부족합니다.", name));
+    }
+
+    if (getStockValue() < requestedQuantity) {
+      throw new CoreException(ErrorType.INSUFFICIENT_STOCK,
+          String.format("상품 [%s]의 재고가 부족합니다. (요청: %d, 재고: %d)",
+              name, requestedQuantity, getStockValue()));
+    }
+  }
+
+  public void increaseLikeCount() {
+    this.likeCount++;
+  }
+
+  public void increaseLikeCount(int count) {
+    if (count > 0) {
+      this.likeCount += count;
+    }
+  }
+
+  public void decreaseLikeCount() {
+    if (this.likeCount > 0) {
+      this.likeCount--;
+    }
+  }
+
+  public Long getPriceValue() {
+    return price.getValue();
+  }
+
+  public Long getStockValue() {
+    return stock.getValue();
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
new file mode 100644
index 000000000..e9469c32d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
@@ -0,0 +1,26 @@
+package com.loopers.domain.product;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ProductRepository {
+
+  Page<Product> findAll(Pageable pageable);
+
+  Page<Product> findByBrandId(Long brandId, Pageable pageable);
+
+  Optional<Product> findById(Long id);
+
+  List<Product> findAllByIdWithLock(List<Long> ids);
+
+  Product save(Product product);
+
+  Product saveAndFlush(Product product);
+
+  void incrementLikeCount(Long productId);
+
+  void decrementLikeCount(Long productId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java
new file mode 100644
index 000000000..f2a73564b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java
@@ -0,0 +1,50 @@
+package com.loopers.domain.product;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class ProductService {
+
+  private final ProductRepository productRepository;
+
+  public Optional<Product> getById(Long productId) {
+    return productRepository.findById(productId);
+  }
+
+  @Transactional
+  public List<Product> findByIdsWithLock(List<Long> productIds) {
+    Objects.requireNonNull(productIds, "상품 ID 목록은 null일 수 없습니다");
+    List<Long> distinctIds = productIds.stream().distinct().toList();
+    return productRepository.findAllByIdWithLock(distinctIds);
+  }
+
+  public Page<Product> findProducts(Long brandId, Pageable pageable) {
+    return brandId != null
+        ? productRepository.findByBrandId(brandId, pageable)
+        : productRepository.findAll(pageable);
+  }
+
+  public Product create(Product product) {
+    return productRepository.saveAndFlush(product);
+  }
+
+  @Transactional
+  public void increaseLikeCount(Long productId) {
+    productRepository.incrementLikeCount(productId);
+  }
+
+  @Transactional
+  public void decreaseLikeCount(Long productId) {
+    productRepository.decrementLikeCount(productId);
+
+  }
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Products.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Products.java
new file mode 100644
index 000000000..9cda7addc
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Products.java
@@ -0,0 +1,41 @@
+package com.loopers.domain.product;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+public class Products {
+
+  private final List<Product> values;
+
+  private Products(List<Product> values) {
+    this.values = values;
+  }
+
+  public static Products from(List<Product> products) {
+    return new Products(new ArrayList<>(products));
+  }
+
+  public List<Long> getBrandIds() {
+    return values.stream()
+        .map(Product::getBrandId)
+        .distinct()
+        .toList();
+  }
+
+  public List<Long> getProductIds() {
+    return values.stream()
+        .map(Product::getId)
+        .toList();
+  }
+
+  public Optional<Product> findById(Long productId) {
+    return values.stream()
+        .filter(p -> p.isSameId(productId))
+        .findFirst();
+  }
+
+  public List<Product> toList() {
+    return List.copyOf(values);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLike.java b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLike.java
new file mode 100644
index 000000000..b872127c3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLike.java
@@ -0,0 +1,77 @@
+package com.loopers.domain.productlike;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.*;
+import java.util.Objects;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * ProductLike Entity
+ * User-Product 좋아요 관계를 추적
+ * UNIQUE 제약: userId + productId 조합 (중복 좋아요 방지)
+ */
+@Entity
+@Table(
+    name = "product_like",
+    uniqueConstraints = {
+        @UniqueConstraint(
+            name = "uk_product_like_user_product",
+            columnNames = {"ref_user_id", "ref_product_id"}
+        )
+    }
+)
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ProductLike extends BaseEntity {
+
+  @Column(name = "ref_user_id", nullable = false)
+  private Long userId;
+
+  @Column(name = "ref_product_id", nullable = false)
+  private Long productId;
+
+  @Column(name = "liked_at", nullable = false)
+  private LocalDateTime likedAt;
+
+  private ProductLike(Long userId, Long productId, LocalDateTime likedAt) {
+    validateUserId(userId);
+    validateProductId(productId);
+    validateLikedAt(likedAt);
+
+    this.userId = userId;
+    this.productId = productId;
+    this.likedAt = likedAt;
+  }
+
+  public static ProductLike of(Long userId, Long productId, LocalDateTime likedAt) {
+    return new ProductLike(userId, productId, likedAt);
+  }
+
+  public boolean isLikedBy(Long userId) {
+    return Objects.equals(this.userId, userId);
+  }
+
+  private void validateUserId(Long userId) {
+    if (userId == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_LIKE_USER_EMPTY);
+    }
+  }
+
+  private void validateProductId(Long productId) {
+    if (productId == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_LIKE_PRODUCT_EMPTY);
+    }
+  }
+
+  private void validateLikedAt(LocalDateTime likedAt) {
+    if (likedAt == null) {
+      throw new CoreException(ErrorType.INVALID_PRODUCT_LIKE_LIKED_AT_EMPTY);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeRepository.java
new file mode 100644
index 000000000..93751bae3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeRepository.java
@@ -0,0 +1,16 @@
+package com.loopers.domain.productlike;
+
+import java.util.List;
+
+public interface ProductLikeRepository {
+
+  boolean existsByUserIdAndProductId(Long userId, Long productId);
+
+  List<ProductLike> findByUserIdAndProductIdIn(Long userId, List<Long> productIds);
+
+  ProductLike save(ProductLike productLike);
+
+  ProductLike saveAndFlush(ProductLike productLike);
+
+  int deleteByUserIdAndProductId(Long userId, Long productId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeService.java
new file mode 100644
index 000000000..cfbc244d0
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikeService.java
@@ -0,0 +1,49 @@
+package com.loopers.domain.productlike;
+
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class ProductLikeService {
+
+  private final ProductLikeRepository productLikeRepository;
+  private final Clock clock;
+
+  public boolean isLiked(Long userId, Long productId) {
+    if (userId == null || productId == null) {
+      return false;
+    }
+    return productLikeRepository.existsByUserIdAndProductId(userId, productId);
+  }
+
+
+  public Map<Long, Boolean> findLikeStatusByProductId(Long userId, List<Long> productIds) {
+    if (userId == null || productIds == null || productIds.isEmpty()) {
+      return Map.of();
+    }
+
+    List<ProductLike> likes = productLikeRepository.findByUserIdAndProductIdIn(userId, productIds);
+    ProductLikes productLikes = ProductLikes.from(likes);
+
+    return productLikes.toLikeStatusByProductId(productIds);
+  }
+
+
+  @Transactional(propagation = Propagation.REQUIRES_NEW)
+  public void createLike(Long userId, Long productId) {
+      ProductLike like = ProductLike.of(userId, productId, LocalDateTime.now(clock));
+      productLikeRepository.saveAndFlush(like);
+  }
+
+  @Transactional
+  public int deleteLike(Long userId, Long productId) {
+    return productLikeRepository.deleteByUserIdAndProductId(userId, productId);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikes.java b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikes.java
new file mode 100644
index 000000000..a30a48e83
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/productlike/ProductLikes.java
@@ -0,0 +1,46 @@
+package com.loopers.domain.productlike;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class ProductLikes {
+
+  private final List<ProductLike> values;
+
+  private ProductLikes(List<ProductLike> productLikes) {
+    this.values = List.copyOf(productLikes);
+  }
+
+  public static ProductLikes from(List<ProductLike> productLikes) {
+    if (productLikes == null) {
+      throw new IllegalArgumentException("productLikes cannot be null");
+    }
+    return new ProductLikes(productLikes);
+  }
+
+  public static ProductLikes empty() {
+    return new ProductLikes(List.of());
+  }
+
+  // 중복 productId가 있으면 IllegalStateException 발생 (데이터 무결성 오류)
+  public Map<Long, Boolean> toLikedByProductId() {
+    return values.stream()
+        .collect(Collectors.toMap(ProductLike::getProductId, like -> true));
+  }
+
+  public Map<Long, Boolean> toLikeStatusByProductId(List<Long> allProductIds) {
+    if (allProductIds == null || allProductIds.isEmpty()) {
+      return Map.of();
+    }
+
+    Map<Long, Boolean> likedByProductId = toLikedByProductId();
+
+    // allProductIds에 중복이 있으면 IllegalStateException 발생
+    return allProductIds.stream()
+        .collect(Collectors.toMap(
+            productId -> productId,
+            productId -> likedByProductId.getOrDefault(productId, false)
+        ));
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/quantity/Quantity.java b/apps/commerce-api/src/main/java/com/loopers/domain/quantity/Quantity.java
new file mode 100644
index 000000000..abe73916d
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/quantity/Quantity.java
@@ -0,0 +1,33 @@
+package com.loopers.domain.quantity;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EqualsAndHashCode
+@Getter
+public class Quantity {
+
+  private Long value;
+
+  private Quantity(Long value) {
+    if (value == null) {
+      throw new CoreException(ErrorType.INVALID_QUANTITY_VALUE);
+    }
+    if (value < 0) {
+      throw new CoreException(ErrorType.INVALID_QUANTITY_RANGE);
+    }
+    this.value = value;
+  }
+
+  public static Quantity of(Long value) {
+    return new Quantity(value);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/stock/Stock.java b/apps/commerce-api/src/main/java/com/loopers/domain/stock/Stock.java
new file mode 100644
index 000000000..bc91b111b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/stock/Stock.java
@@ -0,0 +1,59 @@
+package com.loopers.domain.stock;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.Embeddable;
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@Embeddable
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@EqualsAndHashCode
+public class Stock {
+
+  private Long value;
+
+  private Stock(Long value) {
+    if (value == null) {
+      throw new CoreException(ErrorType.INVALID_STOCK_VALUE);
+    }
+    if (value < 0) {
+      throw new CoreException(ErrorType.NEGATIVE_STOCK_VALUE);
+    }
+    this.value = value;
+  }
+
+  public static Stock zero() {
+    return new Stock(0L);
+  }
+
+  public static Stock of(Long value) {
+    return new Stock(value);
+  }
+
+  public Stock increase(Long amount) {
+    if (amount == null || amount < 0) {
+      throw new CoreException(ErrorType.INVALID_STOCK_INCREASE_AMOUNT);
+    }
+    return Stock.of(this.value + amount);
+  }
+
+  public Stock decrease(Long amount) {
+    if (amount == null || amount < 0) {
+      throw new CoreException(ErrorType.INVALID_STOCK_DECREASE_AMOUNT);
+    }
+    if (this.value < amount) {
+      throw new CoreException(ErrorType.INSUFFICIENT_STOCK);
+    }
+    return Stock.of(this.value - amount);
+  }
+
+  public boolean isAvailable() {
+    return this.value > 0;
+  }
+
+  public Long getValue() {
+    return value;
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
index 035d205dc..defebdf63 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
@@ -1,30 +1,30 @@
 package com.loopers.domain.user;
 
 import com.loopers.domain.BaseEntity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
 import jakarta.persistence.Enumerated;
 import jakarta.persistence.EnumType;
 import jakarta.persistence.Table;
+import java.time.LocalDate;
 import java.util.regex.Pattern;
 import lombok.Getter;
 
-import java.time.Clock;
-import java.time.LocalDate;
-
 @Entity
 @Table(name = "users")
 @Getter
 public class User extends BaseEntity {
 
-  private static final int MAX_USER_ID_LENGTH = 10;
+  private static final int MAX_LOGIN_ID_LENGTH = 10;
   // 영문 대소문자 + 숫자만 허용 (특수문자, 공백, 한글 불가)
-  private static final Pattern USER_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
+  private static final Pattern LOGIN_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
   // 이메일 형식: xxx@yyy.zzz (공백 불가)
   private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
 
-  @Column(unique = true, nullable = false, length = MAX_USER_ID_LENGTH)
-  private String userId;
+  @Column(name = "login_id", unique = true, nullable = false, length = MAX_LOGIN_ID_LENGTH)
+  private String loginId;
 
   @Column(nullable = false)
   private String email;
@@ -39,62 +39,58 @@ public class User extends BaseEntity {
   protected User() {
   }
 
-  private User(String userId, String email, LocalDate birth, Gender gender, Clock clock) {
-    String normalizedUserId = userId != null ? userId.trim() : null;
+  private User(String loginId, String email, LocalDate birth, Gender gender, LocalDate currentDate) {
+    String normalizedLoginId = loginId != null ? loginId.trim() : null;
     String normalizedEmail = email != null ? email.toLowerCase().trim() : null;
 
-    validateUserId(normalizedUserId);
+    validateLoginId(normalizedLoginId);
     validateEmail(normalizedEmail);
-    validateBirth(birth, LocalDate.now(clock));
+    validateBirth(birth, currentDate);
     validateGender(gender);
 
-    this.userId = normalizedUserId;
+    this.loginId = normalizedLoginId;
     this.email = normalizedEmail;
     this.birth = birth;
     this.gender = gender;
   }
 
-  public static User of(String userId, String email, LocalDate birth, Gender gender) {
-    return of(userId, email, birth, gender, Clock.systemDefaultZone());
-  }
-
-  public static User of(String userId, String email, LocalDate birth, Gender gender, Clock clock) {
-    return new User(userId, email, birth, gender, clock);
+  public static User of(String loginId, String email, LocalDate birth, Gender gender, LocalDate currentDate) {
+    return new User(loginId, email, birth, gender, currentDate);
   }
 
-  private void validateUserId(String userId) {
-    if (userId == null || userId.isBlank()) {
-      throw new IllegalArgumentException("사용자 ID는 비어있을 수 없습니다.");
+  private void validateLoginId(String loginId) {
+    if (loginId == null || loginId.isBlank()) {
+      throw new CoreException(ErrorType.INVALID_LOGIN_ID_EMPTY);
     }
-    if (userId.length() > MAX_USER_ID_LENGTH) {
-      throw new IllegalArgumentException("사용자 ID는 10자 이내여야 합니다.");
+    if (loginId.length() > MAX_LOGIN_ID_LENGTH) {
+      throw new CoreException(ErrorType.INVALID_LOGIN_ID_LENGTH);
     }
-    if (!USER_ID_PATTERN.matcher(userId).matches()) {
-      throw new IllegalArgumentException("사용자 ID는 영문/숫자만 허용됩니다.");
+    if (!LOGIN_ID_PATTERN.matcher(loginId).matches()) {
+      throw new CoreException(ErrorType.INVALID_LOGIN_ID_FORMAT);
     }
   }
 
   private void validateEmail(String email) {
     if (email == null || email.isBlank()) {
-      throw new IllegalArgumentException("이메일은 비어있을 수 없습니다.");
+      throw new CoreException(ErrorType.INVALID_EMAIL_EMPTY);
     }
     if (!EMAIL_PATTERN.matcher(email).matches()) {
-      throw new IllegalArgumentException("이메일 형식이 올바르지 않습니다.");
+      throw new CoreException(ErrorType.INVALID_EMAIL_FORMAT);
     }
   }
 
   private void validateBirth(LocalDate birth, LocalDate currentDate) {
     if (birth == null) {
-      throw new IllegalArgumentException("생년월일은 비어있을 수 없습니다.");
+      throw new CoreException(ErrorType.INVALID_BIRTH_EMPTY);
     }
     if (birth.isAfter(currentDate)) {
-      throw new IllegalArgumentException("생년월일은 미래일 수 없습니다.");
+      throw new CoreException(ErrorType.INVALID_BIRTH_FUTURE);
     }
   }
 
   private void validateGender(Gender gender) {
     if (gender == null) {
-      throw new IllegalArgumentException("성별은 비어있을 수 없습니다.");
+      throw new CoreException(ErrorType.INVALID_GENDER_EMPTY);
     }
   }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
index b067aae6e..68d69bc3e 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java
@@ -6,5 +6,5 @@ public interface UserRepository {
 
   User save(User user);
 
-  Optional<User> findByUserId(String userId);
+  Optional<User> findByLoginId(String loginId);
 }
\ No newline at end of file
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
index b870b694b..4539aefcf 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java
@@ -2,6 +2,7 @@
 
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
+import java.time.Clock;
 import lombok.RequiredArgsConstructor;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Component;
@@ -15,17 +16,19 @@
 public class UserService {
 
   private final UserRepository userRepository;
+  private final Clock clock;
 
   @Transactional
-  public User registerUser(String userId, String email, LocalDate birth, Gender gender) {
+  public User registerUser(String loginId, String email, LocalDate birth, Gender gender) {
     try {
-      return userRepository.save(User.of(userId, email, birth, gender));
+      return userRepository.save(User.of(loginId, email, birth, gender, LocalDate.now(clock)));
     } catch (DataIntegrityViolationException e) {
-      throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 사용자 ID입니다.");
+      throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.");
     }
   }
 
-  public User findById(String userId) {
-    return userRepository.findByUserId(userId).orElse(null);
+  public User findById(String loginId) {
+    return userRepository.findByLoginId(loginId)
+        .orElse(null);
   }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java
new file mode 100644
index 000000000..c27f73b73
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java
@@ -0,0 +1,11 @@
+package com.loopers.infrastructure.brand;
+
+import com.loopers.domain.brand.Brand;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface BrandJpaRepository extends JpaRepository<Brand, Long> {
+
+  List<Brand> findByIdIn(List<Long> ids);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java
new file mode 100644
index 000000000..7584155f6
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java
@@ -0,0 +1,31 @@
+package com.loopers.infrastructure.brand;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class BrandRepositoryImpl implements BrandRepository {
+
+  private final BrandJpaRepository jpaRepository;
+
+  @Override
+  public Optional<Brand> findById(Long id) {
+    return jpaRepository.findById(id);
+  }
+
+  @Override
+  public List<Brand> findByIdIn(List<Long> ids) {
+    return jpaRepository.findByIdIn(ids);
+  }
+
+  @Override
+  public Brand save(Brand brand) {
+    return jpaRepository.save(brand);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
new file mode 100644
index 000000000..8a9d1fb6b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
@@ -0,0 +1,30 @@
+package com.loopers.infrastructure.order;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderListDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Optional;
+
+public interface OrderJpaRepository extends JpaRepository<Order, Long> {
+
+  /**
+   * OrderItem과 함께 조회 (@EntityGraph로 N+1 방지)
+   */
+  @EntityGraph(attributePaths = "items")
+  @Query("SELECT o FROM Order o WHERE o.id = :id")
+  Optional<Order> findWithItemsById(@Param("id") Long id);
+
+  /**
+   * 사용자별 주문 목록 조회 (DTO 프로젝션, SIZE()로 개수만 조회)
+   */
+  @Query("SELECT new com.loopers.domain.order.OrderListDto(" +
+      "o.id, o.userId, o.status, o.totalAmount.value, o.orderedAt, SIZE(o.items)) " +
+      "FROM Order o WHERE o.userId = :userId")
+  Page<OrderListDto> findOrderListByUserId(@Param("userId") Long userId, Pageable pageable);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
new file mode 100644
index 000000000..0984ab6a1
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
@@ -0,0 +1,38 @@
+package com.loopers.infrastructure.order;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderListDto;
+import com.loopers.domain.order.OrderRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class OrderRepositoryImpl implements OrderRepository {
+
+  private final OrderJpaRepository jpaRepository;
+
+  @Override
+  public Optional<Order> findById(Long id) {
+    return jpaRepository.findById(id);
+  }
+
+  @Override
+  public Optional<Order> findWithItemsById(Long id) {
+    return jpaRepository.findWithItemsById(id);
+  }
+
+  @Override
+  public Page<OrderListDto> findOrderList(Long userId, Pageable pageable) {
+    return jpaRepository.findOrderListByUserId(userId, pageable);
+  }
+
+  @Override
+  public Order save(Order order) {
+    return jpaRepository.save(order);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java
index 8e6b85146..d0120efaf 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java
@@ -11,11 +11,11 @@
 
 public interface PointJpaRepository extends JpaRepository<Point, Long> {
 
-  @Query("SELECT p FROM Point p JOIN FETCH p.user WHERE p.user.userId = :userId")
-  Point findByUserId(@Param("userId") String userId);
+  @Query("SELECT p FROM Point p WHERE p.userId = :userId")
+  Optional<Point> findByUserId(@Param("userId") Long userId);
 
   @Lock(LockModeType.PESSIMISTIC_WRITE)
-  @Query("SELECT p FROM Point p JOIN FETCH p.user WHERE p.user.userId = :userId")
-  Optional<Point> findByUserIdWithLock(@Param("userId") String userId);
+  @Query("SELECT p FROM Point p WHERE p.userId = :userId")
+  Optional<Point> findByUserIdWithLock(@Param("userId") Long userId);
 
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java
index a9b7dbc11..6f23b5065 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java
@@ -19,12 +19,12 @@ public Point save(Point point) {
   }
 
   @Override
-  public Point findByUserId(String userId) {
+  public Optional<Point> findByUserId(Long userId) {
     return pointJpaRepository.findByUserId(userId);
   }
 
   @Override
-  public Optional<Point> findByUserIdWithLock(String userId) {
+  public Optional<Point> findByUserIdWithLock(Long userId) {
     return pointJpaRepository.findByUserIdWithLock(userId);
   }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
new file mode 100644
index 000000000..72321701e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
@@ -0,0 +1,30 @@
+package com.loopers.infrastructure.product;
+
+import com.loopers.domain.product.Product;
+import jakarta.persistence.LockModeType;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface ProductJpaRepository extends JpaRepository<Product, Long> {
+
+  Page<Product> findByBrandId(Long brandId, Pageable pageable);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @Query("SELECT p FROM Product p WHERE p.id IN :ids ORDER BY p.id ASC")
+  List<Product> findAllByIdWithLock(@Param("ids") List<Long> ids);
+
+  @Modifying(clearAutomatically = true, flushAutomatically = true)
+  @Query("UPDATE Product p SET p.likeCount = p.likeCount + 1 WHERE p.id = :productId")
+  int incrementLikeCount(@Param("productId") Long productId);
+
+  @Modifying(clearAutomatically = true, flushAutomatically = true)
+  @Query("UPDATE Product p SET p.likeCount = p.likeCount - 1 WHERE p.id = :productId AND p.likeCount > 0")
+  int decrementLikeCount(@Param("productId") Long productId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
new file mode 100644
index 000000000..c188f163e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
@@ -0,0 +1,58 @@
+package com.loopers.infrastructure.product;
+
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class ProductRepositoryImpl implements ProductRepository {
+
+  private final ProductJpaRepository jpaRepository;
+
+  @Override
+  public Page<Product> findAll(Pageable pageable) {
+    return jpaRepository.findAll(pageable);
+  }
+
+  @Override
+  public Page<Product> findByBrandId(Long brandId, Pageable pageable) {
+    return jpaRepository.findByBrandId(brandId, pageable);
+  }
+
+  @Override
+  public Optional<Product> findById(Long id) {
+    return jpaRepository.findById(id);
+  }
+
+  @Override
+  public List<Product> findAllByIdWithLock(List<Long> ids) {
+    return jpaRepository.findAllByIdWithLock(ids);
+  }
+
+  @Override
+  public Product save(Product product) {
+    return jpaRepository.save(product);
+  }
+
+  @Override
+  public Product saveAndFlush(Product product) {
+    return jpaRepository.saveAndFlush(product);
+  }
+
+  @Override
+  public void incrementLikeCount(Long productId) {
+    jpaRepository.incrementLikeCount(productId);
+  }
+
+  @Override
+  public void decrementLikeCount(Long productId) {
+    jpaRepository.decrementLikeCount(productId);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeJpaRepository.java
new file mode 100644
index 000000000..3fa89d307
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeJpaRepository.java
@@ -0,0 +1,20 @@
+package com.loopers.infrastructure.productlike;
+
+import com.loopers.domain.productlike.ProductLike;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+
+public interface ProductLikeJpaRepository extends JpaRepository<ProductLike, Long> {
+
+  boolean existsByUserIdAndProductId(Long userId, Long productId);
+
+  List<ProductLike> findByUserIdAndProductIdIn(Long userId, List<Long> productIds);
+
+  @Modifying
+  @Query("DELETE FROM ProductLike pl WHERE pl.userId = :userId AND pl.productId = :productId")
+  int deleteByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId);
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeRepositoryImpl.java
new file mode 100644
index 000000000..d68fe84b8
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/productlike/ProductLikeRepositoryImpl.java
@@ -0,0 +1,46 @@
+package com.loopers.infrastructure.productlike;
+
+import com.loopers.domain.productlike.ProductLike;
+import com.loopers.domain.productlike.ProductLikeRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class ProductLikeRepositoryImpl implements ProductLikeRepository {
+
+  private final ProductLikeJpaRepository jpaRepository;
+
+  @Override
+  public boolean existsByUserIdAndProductId(Long userId, Long productId) {
+    if (userId == null) {
+      return false;
+    }
+    return jpaRepository.existsByUserIdAndProductId(userId, productId);
+  }
+
+  @Override
+  public List<ProductLike> findByUserIdAndProductIdIn(Long userId, List<Long> productIds) {
+    if (userId == null || productIds == null || productIds.isEmpty()) {
+      return List.of();
+    }
+    return jpaRepository.findByUserIdAndProductIdIn(userId, productIds);
+  }
+
+  @Override
+  public ProductLike save(ProductLike productLike) {
+    return jpaRepository.save(productLike);
+  }
+
+  @Override
+  public ProductLike saveAndFlush(ProductLike productLike) {
+    return jpaRepository.saveAndFlush(productLike);
+  }
+
+  @Override
+  public int deleteByUserIdAndProductId(Long userId, Long productId) {
+    return jpaRepository.deleteByUserIdAndProductId(userId, productId);
+  }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
index 1a78b9a69..d89ee854b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
@@ -6,5 +6,5 @@
 import java.util.Optional;
 
 public interface UserJpaRepository extends JpaRepository<User, Long> {
-    Optional<User> findByUserId(String userId);
+    Optional<User> findByLoginId(String loginId);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
index 591fdea66..3c0544a4a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
@@ -18,7 +18,7 @@ public User save(User user) {
     }
 
     @Override
-    public Optional<User> findByUserId(String userId) {
-        return userJpaRepository.findByUserId(userId);
+    public Optional<User> findByLoginId(String loginId) {
+        return userJpaRepository.findByLoginId(loginId);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
index e4296d092..28b7bb38d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
@@ -56,13 +56,6 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingRequestHeaderExcep
         return failureResponse(ErrorType.BAD_REQUEST, message);
     }
 
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleBadRequest(IllegalArgumentException e) {
-        log.warn("IllegalArgumentException : {}", e.getMessage(), e);
-        String message = e.getMessage() != null ? e.getMessage() : "잘못된 요청입니다.";
-        return failureResponse(ErrorType.BAD_REQUEST, message);
-    }
-
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
         String errorMessage = e.getBindingResult().getFieldErrors().stream()
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointApiSpec.java
index 7e7b52c5a..cdbf727b9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointApiSpec.java
@@ -20,7 +20,7 @@ public interface PointApiSpec {
     )
     ApiResponse<PointResponse> getPoint(
         @Parameter(description = "사용자 ID", required = true)
-        @RequestHeader(ApiHeaders.USER_ID) String userId
+        @RequestHeader(ApiHeaders.USER_ID) Long userId
     );
 
     @Operation(
@@ -29,7 +29,7 @@ ApiResponse<PointResponse> getPoint(
     )
     ApiResponse<ChargeResponse> chargePoint(
         @Parameter(description = "사용자 ID", required = true)
-        @RequestHeader(ApiHeaders.USER_ID) String userId,
+        @RequestHeader(ApiHeaders.USER_ID) Long userId,
         @RequestBody(description = "충전 요청", required = true)
         ChargeRequest request
     );
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointController.java
index bc795b61f..2cd097ea4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointController.java
@@ -1,7 +1,6 @@
 package com.loopers.interfaces.api.point;
 
 import com.loopers.application.point.PointFacade;
-import com.loopers.application.point.PointResult;
 import com.loopers.interfaces.api.ApiResponse;
 import com.loopers.interfaces.api.point.PointDto.ChargeRequest;
 import com.loopers.interfaces.api.point.PointDto.ChargeResponse;
@@ -25,16 +24,17 @@ public class PointController implements PointApiSpec {
 
   @Override
   @GetMapping
-public ApiResponse<PointResponse> getPoint(@RequestHeader(ApiHeaders.USER_ID) String userId) {
-    PointResult result = pointFacade.findPointOrNull(userId);
-    PointResponse response = result == null ? null : PointResponse.from(result);
+public ApiResponse<PointResponse> getPoint(@RequestHeader(ApiHeaders.USER_ID) Long userId) {
+    PointResponse response = pointFacade.getPoint(userId)
+        .map(PointResponse::from)
+        .orElse(null);
     return ApiResponse.success(response);
   }
 
   @Override
   @PatchMapping("/charge")
   public ApiResponse<ChargeResponse> chargePoint(
-      @RequestHeader(ApiHeaders.USER_ID) String userId,
+      @RequestHeader(ApiHeaders.USER_ID) Long userId,
       @Valid @RequestBody ChargeRequest request
   ) {
     ChargeResponse response = ChargeResponse.from(
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointDto.java
index 2447baead..15bac5a54 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointDto.java
@@ -6,7 +6,7 @@
 public class PointDto {
 
   public record PointResponse(
-      String userId,
+      Long userId,
       Long balance
   ) {
 
@@ -24,7 +24,7 @@ public record ChargeRequest(
   }
 
   public record ChargeResponse(
-      String userId,
+      Long userId,
       Long balance
   ) {
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java
index 2ac5d9eb5..34ab3524b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserApiSpec.java
@@ -19,10 +19,10 @@ ApiResponse<UserDto.UserResponse> register(
 
     @Operation(
         summary = "사용자 조회",
-        description = "사용자 ID로 사용자 정보를 조회합니다."
+        description = "로그인 ID로 사용자 정보를 조회합니다."
     )
     ApiResponse<UserDto.UserResponse> getUser(
-        @Schema(description = "사용자 ID")
-        String userId
+        @Schema(description = "로그인 ID")
+        String loginId
     );
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java
index b1277821b..bcfadf9b7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserController.java
@@ -25,7 +25,7 @@ public ApiResponse<UserDto.UserResponse> register(
         @Valid @RequestBody UserDto.RegisterRequest request
     ) {
         UserResult userResult = userFacade.register(
-            request.userId(),
+            request.loginId(),
             request.email(),
             request.birth(),
             request.gender()
@@ -35,10 +35,10 @@ public ApiResponse<UserDto.UserResponse> register(
         return ApiResponse.success(response);
     }
 
-    @GetMapping("/{userId}")
+    @GetMapping("/{loginId}")
     @Override
-    public ApiResponse<UserDto.UserResponse> getUser(@PathVariable String userId) {
-        UserResult userResult = userFacade.getUser(userId);
+    public ApiResponse<UserDto.UserResponse> getUser(@PathVariable String loginId) {
+        UserResult userResult = userFacade.lookupUserProfile(loginId);
         UserDto.UserResponse response = UserDto.UserResponse.from(userResult);
         return ApiResponse.success(response);
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java
index b3d828a7e..be1c2e3e6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserDto.java
@@ -11,14 +11,14 @@
 import java.time.LocalDate;
 
 public class UserDto {
-    private static final String VALID_USER_ID_PATTERN = "^[A-Za-z0-9]{1,10}$";
+    private static final String VALID_LOGIN_ID_PATTERN = "^[A-Za-z0-9]{1,10}$";
     private static final String VALID_EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
     private static final String YYYY_MM_DD = "yyyy-MM-dd";
 
     public record RegisterRequest(
-        @NotBlank(message = "사용자 ID는 비어있을 수 없습니다.")
-        @Pattern(regexp = VALID_USER_ID_PATTERN, message = "사용자 ID 형식이 올바르지 않습니다.")
-        String userId,
+        @NotBlank(message = "로그인 ID는 비어있을 수 없습니다.")
+        @Pattern(regexp = VALID_LOGIN_ID_PATTERN, message = "로그인 ID 형식이 올바르지 않습니다.")
+        String loginId,
 
         @NotBlank(message = "이메일은 비어있을 수 없습니다.")
         @Pattern(regexp = VALID_EMAIL_PATTERN, message = "이메일 형식이 올바르지 않습니다.")
@@ -34,18 +34,18 @@ public record RegisterRequest(
     ) {}
 
     public record UserResponse(
-        String userId,
+        String loginId,
         String email,
         @JsonFormat(pattern = YYYY_MM_DD)
         LocalDate birth,
         Gender gender
     ) {
-        public static UserResponse from(UserResult info) {
+        public static UserResponse from(UserResult result) {
             return new UserResponse(
-                info.userId(),
-                info.email(),
-                info.birth(),
-                info.gender()
+                result.loginId(),
+                result.email(),
+                result.birth(),
+                result.gender()
             );
         }
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
index 5d142efbf..2668d6a01 100644
--- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
+++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
@@ -11,7 +11,83 @@ public enum ErrorType {
     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(), "존재하지 않는 요청입니다."),
-    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다.");
+    CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."),
+
+    /** Money 도메인 에러 */
+    INVALID_MONEY_VALUE(HttpStatus.BAD_REQUEST, "INVALID_MONEY_VALUE", "금액은 비어있을 수 없습니다."),
+    NEGATIVE_MONEY_VALUE(HttpStatus.BAD_REQUEST, "NEGATIVE_MONEY_VALUE", "금액은 음수일 수 없습니다."),
+
+    /** User 도메인 에러 */
+    INVALID_LOGIN_ID_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_LOGIN_ID_EMPTY", "로그인 ID는 비어있을 수 없습니다."),
+    INVALID_LOGIN_ID_LENGTH(HttpStatus.BAD_REQUEST, "INVALID_LOGIN_ID_LENGTH", "로그인 ID는 10자 이내여야 합니다."),
+    INVALID_LOGIN_ID_FORMAT(HttpStatus.BAD_REQUEST, "INVALID_LOGIN_ID_FORMAT", "로그인 ID는 영문/숫자만 허용됩니다."),
+    INVALID_EMAIL_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_EMAIL_EMPTY", "이메일은 비어있을 수 없습니다."),
+    INVALID_EMAIL_FORMAT(HttpStatus.BAD_REQUEST, "INVALID_EMAIL_FORMAT", "이메일 형식이 올바르지 않습니다."),
+    INVALID_BIRTH_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_BIRTH_EMPTY", "생년월일은 비어있을 수 없습니다."),
+    INVALID_BIRTH_FUTURE(HttpStatus.BAD_REQUEST, "INVALID_BIRTH_FUTURE", "생년월일은 미래일 수 없습니다."),
+    INVALID_GENDER_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_GENDER_EMPTY", "성별은 비어있을 수 없습니다."),
+
+    /** Point 도메인 에러 */
+    INVALID_POINT_USER_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_POINT_USER_EMPTY", "사용자는 비어있을 수 없습니다."),
+
+    /** PointAmount 도메인 에러 */
+    INVALID_POINT_AMOUNT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_POINT_AMOUNT_EMPTY", "포인트 금액은 비어있을 수 없습니다."),
+    NEGATIVE_POINT_AMOUNT(HttpStatus.BAD_REQUEST, "NEGATIVE_POINT_AMOUNT", "포인트 금액은 음수일 수 없습니다."),
+    INVALID_CHARGE_AMOUNT(HttpStatus.BAD_REQUEST, "INVALID_CHARGE_AMOUNT", "충전 금액은 0보다 커야 합니다."),
+    INVALID_DEDUCT_AMOUNT(HttpStatus.BAD_REQUEST, "INVALID_DEDUCT_AMOUNT", "차감 금액은 0보다 커야 합니다."),
+    INSUFFICIENT_POINT_BALANCE(HttpStatus.BAD_REQUEST, "INSUFFICIENT_POINT_BALANCE", "포인트 잔액이 부족합니다."),
+
+    /** Stock 도메인 에러 */
+    INVALID_STOCK_VALUE(HttpStatus.BAD_REQUEST, "INVALID_STOCK_VALUE", "재고 수량은 비어있을 수 없습니다."),
+    NEGATIVE_STOCK_VALUE(HttpStatus.BAD_REQUEST, "NEGATIVE_STOCK_VALUE", "재고 수량은 음수일 수 없습니다."),
+    INVALID_STOCK_INCREASE_AMOUNT(HttpStatus.BAD_REQUEST, "INVALID_STOCK_INCREASE_AMOUNT", "증가 수량은 0 이상이어야 합니다."),
+    INVALID_STOCK_DECREASE_AMOUNT(HttpStatus.BAD_REQUEST, "INVALID_STOCK_DECREASE_AMOUNT", "감소 수량은 0 이상이어야 합니다."),
+    INSUFFICIENT_STOCK(HttpStatus.BAD_REQUEST, "INSUFFICIENT_STOCK", "재고가 부족합니다."),
+
+    /** Brand 도메인 에러 */
+    INVALID_BRAND_NAME_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_BRAND_NAME_EMPTY", "브랜드명은 비어있을 수 없습니다."),
+    INVALID_BRAND_NAME_LENGTH(HttpStatus.BAD_REQUEST, "INVALID_BRAND_NAME_LENGTH", "브랜드명은 50자 이내여야 합니다."),
+
+    /** Product 도메인 에러 */
+    INVALID_PRODUCT_NAME_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_NAME_EMPTY", "상품명은 비어있을 수 없습니다."),
+    INVALID_PRODUCT_NAME_LENGTH(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_NAME_LENGTH", "상품명은 100자 이내여야 합니다."),
+    INVALID_PRODUCT_PRICE_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_PRICE_EMPTY", "가격은 비어있을 수 없습니다."),
+    INVALID_PRODUCT_STOCK_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_STOCK_EMPTY", "재고는 비어있을 수 없습니다."),
+    INVALID_PRODUCT_BRAND_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_BRAND_EMPTY", "브랜드는 비어있을 수 없습니다."),
+
+    /** ProductLike 도메인 에러 */
+    INVALID_PRODUCT_LIKE_USER_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_LIKE_USER_EMPTY", "사용자는 비어있을 수 없습니다."),
+    INVALID_PRODUCT_LIKE_PRODUCT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_LIKE_PRODUCT_EMPTY", "상품은 비어있을 수 없습니다."),
+    INVALID_PRODUCT_LIKE_LIKED_AT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_LIKE_LIKED_AT_EMPTY", "좋아요 일시는 비어있을 수 없습니다."),
+
+    /** ProductDetail 도메인 에러 */
+    INVALID_PRODUCT_DETAIL_PRODUCT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_DETAIL_PRODUCT_EMPTY", "상품 정보는 필수입니다."),
+    INVALID_PRODUCT_DETAIL_BRAND_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_PRODUCT_DETAIL_BRAND_EMPTY", "브랜드 정보는 필수입니다."),
+
+    /** Quantity 도메인 에러 */
+    INVALID_QUANTITY_VALUE(HttpStatus.BAD_REQUEST, "INVALID_QUANTITY_VALUE", "수량은 비어있을 수 없습니다."),
+    INVALID_QUANTITY_RANGE(HttpStatus.BAD_REQUEST, "INVALID_QUANTITY_RANGE", "수량은 0 이상이어야 합니다."),
+
+    /** OrderItem 도메인 에러 */
+    INVALID_ORDER_ITEM_ORDER_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ITEM_ORDER_EMPTY", "주문은 비어있을 수 없습니다."),
+    INVALID_ORDER_ITEM_PRODUCT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ITEM_PRODUCT_EMPTY", "상품은 비어있을 수 없습니다."),
+    INVALID_ORDER_ITEM_PRODUCT_NAME_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ITEM_PRODUCT_NAME_EMPTY", "상품명은 비어있을 수 없습니다."),
+    INVALID_ORDER_ITEM_PRODUCT_NAME_LENGTH(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ITEM_PRODUCT_NAME_LENGTH", "상품명은 100자 이내여야 합니다."),
+    INVALID_ORDER_ITEM_QUANTITY_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ITEM_QUANTITY_EMPTY", "수량은 비어있을 수 없습니다."),
+
+    /** OrderPrice 도메인 에러 */
+    INVALID_ORDER_PRICE_VALUE(HttpStatus.BAD_REQUEST, "INVALID_ORDER_PRICE_VALUE", "주문 가격은 비어있을 수 없습니다."),
+    NEGATIVE_ORDER_PRICE_VALUE(HttpStatus.BAD_REQUEST, "NEGATIVE_ORDER_PRICE_VALUE", "주문 가격은 음수일 수 없습니다."),
+
+    /** Order 도메인 에러 */
+    INVALID_ORDER_USER_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_USER_EMPTY", "사용자는 비어있을 수 없습니다."),
+    INVALID_ORDER_STATUS_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_STATUS_EMPTY", "주문 상태는 비어있을 수 없습니다."),
+    INVALID_ORDER_TOTAL_AMOUNT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_TOTAL_AMOUNT_EMPTY", "주문 총액은 비어있을 수 없습니다."),
+    NEGATIVE_ORDER_TOTAL_AMOUNT(HttpStatus.BAD_REQUEST, "NEGATIVE_ORDER_TOTAL_AMOUNT", "주문 총액은 음수일 수 없습니다."),
+    INVALID_ORDER_ORDERED_AT_EMPTY(HttpStatus.BAD_REQUEST, "INVALID_ORDER_ORDERED_AT_EMPTY", "주문 시각은 비어있을 수 없습니다."),
+    ORDER_CANNOT_COMPLETE(HttpStatus.BAD_REQUEST, "ORDER_CANNOT_COMPLETE", "PENDING 상태의 주문만 완료할 수 있습니다."),
+    ORDER_CANNOT_FAIL_PAYMENT(HttpStatus.BAD_REQUEST, "ORDER_CANNOT_FAIL_PAYMENT", "PENDING 상태의 주문만 결제 실패 상태로 변경할 수 있습니다."),
+    ORDER_CANNOT_RETRY_COMPLETE(HttpStatus.BAD_REQUEST, "ORDER_CANNOT_RETRY_COMPLETE", "PAYMENT_FAILED 상태의 주문만 재시도 완료할 수 있습니다.");
 
     private final HttpStatus status;
     private final String code;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/brand/BrandFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/brand/BrandFacadeTest.java
new file mode 100644
index 000000000..2fc4d710f
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/brand/BrandFacadeTest.java
@@ -0,0 +1,60 @@
+package com.loopers.application.brand;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandService;
+import com.loopers.support.error.CoreException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("BrandFacade 테스트")
+class BrandFacadeTest {
+
+  @InjectMocks
+  private BrandFacade brandFacade;
+
+  @Mock
+  private BrandService brandService;
+
+  @Test
+  @DisplayName("브랜드 ID로 브랜드 조회 성공")
+  void viewBrand_success() {
+    // given
+    Long brandId = 1L;
+    Brand brand = Brand.of("나이키", "스포츠 브랜드");
+
+    given(brandService.getById(brandId)).willReturn(Optional.of(brand));
+
+    // when
+    BrandResult result = brandFacade.viewBrand(brandId);
+
+    // then
+    assertThat(result)
+        .extracting("brandId", "name", "description")
+        .containsExactly(brand.getId(), "나이키", "스포츠 브랜드");
+  }
+
+  @Test
+  @DisplayName("존재하지 않는 브랜드 ID 조회 시 CoreException 발생")
+  void viewBrand_notFound() {
+    // given
+    Long brandId = 999L;
+
+    given(brandService.getById(brandId)).willReturn(Optional.empty());
+
+    // when & then
+    assertThatThrownBy(() -> brandFacade.viewBrand(brandId))
+        .isInstanceOf(CoreException.class)
+        .hasMessageContaining("브랜드를 찾을 수 없습니다.");
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.java
new file mode 100644
index 000000000..124e81a84
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.java
@@ -0,0 +1,163 @@
+package com.loopers.application.like;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.productlike.ProductLike;
+import com.loopers.domain.productlike.ProductLikeRepository;
+import com.loopers.domain.stock.Stock;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import com.loopers.utils.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+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.boot.test.context.SpringBootTest;
+
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SpringBootTest
+@DisplayName("LikeFacade 통합 테스트")
+class LikeFacadeIntegrationTest {
+
+  private static final LocalDateTime LIKED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @Autowired
+  private LikeFacade likeFacade;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  @Autowired
+  private ProductLikeRepository productLikeRepository;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @Nested
+  @DisplayName("좋아요 등록 시")
+  class LikeProduct {
+
+    @Test
+    @DisplayName("좋아요 등록 성공하고 상품의 좋아요 수가 증가한다")
+    void likesProduct_andIncreasesLikeCount() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      Long userId = 1L;
+      Long productId = product.getId();
+
+      likeFacade.likeProduct(userId, productId);
+
+      assertThat(productLikeRepository.existsByUserIdAndProductId(userId, productId)).isTrue();
+      Product updatedProduct = productRepository.findById(productId).orElseThrow();
+      assertThat(updatedProduct.getLikeCount()).isEqualTo(1L);
+    }
+
+    @Test
+    @DisplayName("같은 사용자가 두 번 좋아요 시도해도 1개만 증가 (멱등성)")
+    void ensuresIdempotency_whenDuplicateLikes() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      Long userId = 1L;
+      Long productId = product.getId();
+
+      likeFacade.likeProduct(userId, productId);
+      likeFacade.likeProduct(userId, productId);
+
+      assertThat(productLikeRepository.existsByUserIdAndProductId(userId, productId)).isTrue();
+      Product updatedProduct = productRepository.findById(productId).orElseThrow();
+      assertThat(updatedProduct.getLikeCount()).isEqualTo(1L);
+    }
+
+    @Test
+    @DisplayName("존재하지 않는 상품 좋아요 시 예외 발생")
+    void throwsException_whenProductNotFound() {
+      Long userId = 1L;
+      Long productId = 999999L;
+
+      assertThatThrownBy(() -> likeFacade.likeProduct(userId, productId))
+          .isInstanceOf(CoreException.class)
+          .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND)
+          .hasMessageContaining("상품을 찾을 수 없습니다.");
+    }
+  }
+
+  @Nested
+  @DisplayName("좋아요 취소 시")
+  class UnlikeProduct {
+
+    @Test
+    @DisplayName("좋아요 취소 성공하고 상품의 좋아요 수가 감소한다")
+    void unlikesProduct_andDecreasesLikeCount() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      Long userId = 1L;
+      Long productId = product.getId();
+
+      productLikeRepository.save(ProductLike.of(userId, productId, LIKED_AT_2025_10_30));
+      product.increaseLikeCount();
+      productRepository.save(product);
+
+      likeFacade.unlikeProduct(userId, productId);
+
+      assertThat(productLikeRepository.existsByUserIdAndProductId(userId, productId)).isFalse();
+      Product updatedProduct = productRepository.findById(productId).orElseThrow();
+      assertThat(updatedProduct.getLikeCount()).isZero();
+    }
+
+    @Test
+    @DisplayName("같은 사용자가 두 번 좋아요 취소 시도해도 1개만 감소 (멱등성)")
+    void ensuresIdempotency_whenDuplicateUnlikes() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      Long userId = 1L;
+      Long productId = product.getId();
+
+      productLikeRepository.save(ProductLike.of(userId, productId, LIKED_AT_2025_10_30));
+      product.increaseLikeCount();
+      productRepository.save(product);
+
+      likeFacade.unlikeProduct(userId, productId);
+      likeFacade.unlikeProduct(userId, productId);
+
+      assertThat(productLikeRepository.existsByUserIdAndProductId(userId, productId)).isFalse();
+      Product updatedProduct = productRepository.findById(productId).orElseThrow();
+      assertThat(updatedProduct.getLikeCount()).isZero();
+    }
+
+    @Test
+    @DisplayName("존재하지 않는 상품 좋아요 취소 시 예외 발생")
+    void throwsException_whenProductNotFound() {
+      Long userId = 1L;
+      Long productId = 999999L;
+
+      assertThatThrownBy(() -> likeFacade.unlikeProduct(userId, productId))
+          .isInstanceOf(CoreException.class)
+          .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND)
+          .hasMessageContaining("상품을 찾을 수 없습니다.");
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
new file mode 100644
index 000000000..1717b87d4
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
@@ -0,0 +1,195 @@
+package com.loopers.application.order;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.orderitem.OrderItemCommand;
+import com.loopers.domain.order.OrderStatus;
+import com.loopers.domain.point.Point;
+import com.loopers.domain.point.PointRepository;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.domain.stock.Stock;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SpringBootTest
+@Transactional
+@DisplayName("OrderFacade 통합 테스트")
+class OrderFacadeIntegrationTest {
+
+  @Autowired
+  private OrderFacade orderFacade;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  @Autowired
+  private UserRepository userRepository;
+
+  @Autowired
+  private PointRepository pointRepository;
+
+  private User user;
+  private Brand brand;
+  private Product product1;
+  private Product product2;
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+  private static final LocalDate BIRTH_DATE_1990_01_01 = LocalDate.of(1990, 1, 1);
+
+  @BeforeEach
+  void setUp() {
+    user = userRepository.save(User.of("testuser", "test@example.com", BIRTH_DATE_1990_01_01, Gender.MALE, LocalDate.of(2025, 10, 30)));
+    brand = brandRepository.save(Brand.of("테스트브랜드"));
+    product1 = productRepository.save(
+        Product.of("상품1", Money.of(10000L), "설명1", Stock.of(10L), brand.getId())
+    );
+    product2 = productRepository.save(
+        Product.of("상품2", Money.of(30000L), "설명2", Stock.of(5L), brand.getId())
+    );
+    pointRepository.save(Point.of(user.getId(), 100000L));
+  }
+
+  @Nested
+  @DisplayName("주문 생성")
+  class CreateOrder {
+
+    @Test
+    @DisplayName("유효한 주문 요청이면 주문이 생성되고 재고와 포인트가 차감된다")
+    void createOrder_success() {
+      // given
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(product1.getId(), Quantity.of(2L)),
+          OrderItemCommand.of(product2.getId(), Quantity.of(1L))
+      );
+
+      // when
+      Order order = orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30);
+
+      // then
+      assertThat(order.getTotalAmountValue()).isEqualTo(50000L);
+      assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
+
+      // 재고 차감 확인 (실제 DB 조회)
+      Product updatedProduct1 = productRepository.findById(product1.getId()).orElseThrow();
+      assertThat(updatedProduct1.getStockValue()).isEqualTo(8L);
+
+      Product updatedProduct2 = productRepository.findById(product2.getId()).orElseThrow();
+      assertThat(updatedProduct2.getStockValue()).isEqualTo(4L);
+
+      // 포인트 차감 확인
+      Point updatedPoint = pointRepository.findByUserId(user.getId()).orElseThrow();
+      assertThat(updatedPoint.getAmountValue()).isEqualTo(50000L);
+    }
+
+    @Test
+    @DisplayName("재고가 부족하면 예외가 발생하고 트랜잭션이 롤백된다")
+    void createOrder_insufficientStock() {
+      // given
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(product1.getId(), Quantity.of(100L))
+      );
+
+      // when & then
+      assertThatThrownBy(() -> orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30))
+          .isInstanceOf(CoreException.class)
+          .hasFieldOrPropertyWithValue("errorType", ErrorType.INSUFFICIENT_STOCK)
+          .hasMessageContaining("재고가 부족합니다");
+
+      // 롤백 확인 - 재고 변경 안 됨
+      Product unchangedProduct = productRepository.findById(product1.getId()).orElseThrow();
+      assertThat(unchangedProduct.getStockValue()).isEqualTo(10L);
+
+      // 포인트도 변경 안 됨
+      Point unchangedPoint = pointRepository.findByUserId(user.getId()).orElseThrow();
+      assertThat(unchangedPoint.getAmountValue()).isEqualTo(100000L);
+    }
+
+    @Test
+    @DisplayName("재고가 0인 상품을 주문하면 예외가 발생한다")
+    void createOrder_zeroStock() {
+      // given
+      Product zeroStockProduct = productRepository.save(
+          Product.of("재고0상품", Money.of(10000L), "설명", Stock.of(0L), brand.getId())
+      );
+
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(zeroStockProduct.getId(), Quantity.of(1L))
+      );
+
+      // when & then
+      assertThatThrownBy(() -> orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30))
+          .isInstanceOf(CoreException.class)
+          .hasFieldOrPropertyWithValue("errorType", ErrorType.INSUFFICIENT_STOCK);
+    }
+
+    @Test
+    @DisplayName("포인트가 부족하면 예외가 발생하고 트랜잭션이 롤백된다")
+    void createOrder_insufficientPoint() {
+      // given
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(product1.getId(), Quantity.of(1L))
+      );
+
+      // 포인트를 5000으로 설정 (부족)
+      Point point = pointRepository.findByUserId(user.getId()).orElseThrow();
+      point.deduct(95000L);
+
+      // when & then
+      assertThatThrownBy(() -> orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30))
+          .isInstanceOf(CoreException.class)
+          .hasFieldOrPropertyWithValue("errorType", ErrorType.INSUFFICIENT_POINT_BALANCE);
+
+      // 재고 변경 안 됨 (포인트 차감 전 실패)
+      Product unchangedProduct = productRepository.findById(product1.getId()).orElseThrow();
+      assertThat(unchangedProduct.getStockValue()).isEqualTo(10L);
+
+      // 포인트는 차감 전 상태 유지
+      Point unchangedPoint = pointRepository.findByUserId(user.getId()).orElseThrow();
+      assertThat(unchangedPoint.getAmountValue()).isEqualTo(5000L);
+    }
+
+    @Test
+    @DisplayName("주문 생성 시 총액이 정확하게 계산되고 포인트가 차감된다")
+    void createOrder_totalAmountCalculation() {
+      // given
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(product1.getId(), Quantity.of(3L)),
+          OrderItemCommand.of(product2.getId(), Quantity.of(2L))
+      );
+
+      // when
+      Order order = orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30);
+
+      // then
+      assertThat(order.getTotalAmountValue()).isEqualTo(90000L);
+
+      // 포인트 차감 확인
+      Point updatedPoint = pointRepository.findByUserId(user.getId()).orElseThrow();
+      assertThat(updatedPoint.getAmountValue()).isEqualTo(10000L);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeRaceConditionTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeRaceConditionTest.java
new file mode 100644
index 000000000..4a00fe1b1
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeRaceConditionTest.java
@@ -0,0 +1,234 @@
+package com.loopers.application.order;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.order.orderitem.OrderItemCommand;
+import com.loopers.domain.point.Point;
+import com.loopers.domain.point.PointRepository;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.domain.stock.Stock;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.support.error.CoreException;
+import com.loopers.utils.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest
+@DisplayName("OrderFacade 동시성 테스트")
+class OrderFacadeRaceConditionTest {
+
+  @Autowired
+  private OrderFacade orderFacade;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  @Autowired
+  private UserRepository userRepository;
+
+  @Autowired
+  private PointRepository pointRepository;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+  private static final LocalDate BIRTH_DATE_1990_01_01 = LocalDate.of(1990, 1, 1);
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @Test
+  @DisplayName("동시에 2개 주문 시 하나만 성공하고 재고가 음수가 되지 않음")
+  void concurrentOrders_onlyOneSucceeds() {
+    Brand brand = brandRepository.save(Brand.of("테스트브랜드"));
+    Product product = productRepository.save(
+        Product.of("재고1개상품", Money.of(10000L), "재고 1개 상품", Stock.of(1L), brand.getId())
+    );
+
+    int random = (int) (System.nanoTime() % 10000);
+    User user1 = userRepository.save(User.of("u1" + random, "u1" + random + "@t.com", BIRTH_DATE_1990_01_01, Gender.MALE, LocalDate.of(2025, 10, 30)));
+    User user2 = userRepository.save(User.of("u2" + random, "u2" + random + "@t.com", BIRTH_DATE_1990_01_01, Gender.FEMALE, LocalDate.of(2025, 10, 30)));
+    pointRepository.save(Point.of(user1.getId(), 50000L));
+    pointRepository.save(Point.of(user2.getId(), 50000L));
+
+    List<OrderItemCommand> commands = List.of(
+        OrderItemCommand.of(product.getId(), Quantity.of(1L))
+    );
+
+    List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+    futures.add(CompletableFuture.runAsync(() -> {
+      try {
+        orderFacade.createOrder(user1.getId(), commands, ORDERED_AT_2025_10_30);
+      } catch (CoreException ignored) {
+        // 재고 부족 시 예외 발생 예상
+      }
+    }));
+
+    futures.add(CompletableFuture.runAsync(() -> {
+      try {
+        orderFacade.createOrder(user2.getId(), commands, ORDERED_AT_2025_10_30);
+      } catch (CoreException ignored) {
+        // 재고 부족 시 예외 발생 예상
+      }
+    }));
+
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    Product updatedProduct = productRepository.findById(product.getId()).orElseThrow();
+    assertThat(updatedProduct).extracting("stockValue").isEqualTo(0L);
+  }
+
+  @Test
+  @DisplayName("동시에 10개 주문 시 재고 5개 상품은 5개만 성공")
+  void concurrentOrders_multipleThreads() {
+    Brand brand = brandRepository.save(Brand.of("테스트브랜드"));
+    Product product = productRepository.save(
+        Product.of("재고5개상품", Money.of(5000L), "재고 5개 상품", Stock.of(5L), brand.getId())
+    );
+
+    int threadCount = 10;
+
+    List<OrderItemCommand> commands = List.of(
+        OrderItemCommand.of(product.getId(), Quantity.of(1L))
+    );
+
+    AtomicInteger counter = new AtomicInteger(0);
+    List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+    for (int i = 0; i < threadCount; i++) {
+      futures.add(CompletableFuture.runAsync(() -> {
+        try {
+          int id = counter.getAndIncrement();
+          String loginId = "u" + id;
+          User user = userRepository.save(User.of(loginId, loginId + "@t.com", BIRTH_DATE_1990_01_01, Gender.MALE, LocalDate.of(2025, 10, 30)));
+          pointRepository.save(Point.of(user.getId(), 50000L));
+          orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30);
+        } catch (CoreException ignored) {
+          // 재고 부족 시 예외 발생 예상
+        }
+      }));
+    }
+
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    Product updatedProduct = productRepository.findById(product.getId()).orElseThrow();
+    assertThat(updatedProduct).extracting("stockValue").isEqualTo(0L);
+  }
+
+  @Test
+  @DisplayName("동시 주문 시 재고가 음수가 되지 않음")
+  void concurrentOrders_stockNeverNegative() {
+    Brand brand = brandRepository.save(Brand.of("테스트브랜드"));
+    Product product = productRepository.save(
+        Product.of("재고3개상품", Money.of(3000L), "재고 3개 상품", Stock.of(3L), brand.getId())
+    );
+
+    int threadCount = 5;
+
+    List<OrderItemCommand> commands = List.of(
+        OrderItemCommand.of(product.getId(), Quantity.of(1L))
+    );
+
+    AtomicInteger counter = new AtomicInteger(100);
+    List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+    for (int i = 0; i < threadCount; i++) {
+      futures.add(CompletableFuture.runAsync(() -> {
+        try {
+          int id = counter.getAndIncrement();
+          String loginId = "u" + id;
+          User user = userRepository.save(User.of(loginId, loginId + "@t.com", BIRTH_DATE_1990_01_01, Gender.MALE, LocalDate.of(2025, 10, 30)));
+          pointRepository.save(Point.of(user.getId(), 50000L));
+          orderFacade.createOrder(user.getId(), commands, ORDERED_AT_2025_10_30);
+        } catch (CoreException ignored) {
+          // 재고 부족 시 예외 발생 예상
+        }
+      }));
+    }
+
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    Product finalProduct = productRepository.findById(product.getId()).orElseThrow();
+    assertThat(finalProduct).extracting("stockValue").matches(value -> (Long) value >= 0L);
+  }
+
+  @Test
+  @DisplayName("여러 상품 역순 동시 주문 시 데드락이 발생하지 않음 (ORDER BY 검증)")
+  void concurrentOrders_multipleProducts_noDeadlock() {
+    Brand brand = brandRepository.save(Brand.of("테스트브랜드"));
+    Product product1 = productRepository.save(
+        Product.of("상품1", Money.of(1000L), "상품 1", Stock.of(10L), brand.getId())
+    );
+    Product product2 = productRepository.save(
+        Product.of("상품2", Money.of(2000L), "상품 2", Stock.of(10L), brand.getId())
+    );
+    Product product3 = productRepository.save(
+        Product.of("상품3", Money.of(3000L), "상품 3", Stock.of(10L), brand.getId())
+    );
+
+    int random1 = (int) (System.nanoTime() % 10000);
+    int random2 = random1 + 1;
+    User user1 = userRepository.save(User.of("u1" + random1, "u1" + random1 + "@t.com", BIRTH_DATE_1990_01_01, Gender.MALE, LocalDate.of(2025, 10, 30)));
+    User user2 = userRepository.save(User.of("u2" + random2, "u2" + random2 + "@t.com", BIRTH_DATE_1990_01_01, Gender.FEMALE, LocalDate.of(2025, 10, 30)));
+    pointRepository.save(Point.of(user1.getId(), 100000L));
+    pointRepository.save(Point.of(user2.getId(), 100000L));
+
+    List<OrderItemCommand> commands1 = List.of(
+        OrderItemCommand.of(product1.getId(), Quantity.of(1L)),
+        OrderItemCommand.of(product2.getId(), Quantity.of(1L)),
+        OrderItemCommand.of(product3.getId(), Quantity.of(1L))
+    );
+
+    List<OrderItemCommand> commands2 = List.of(
+        OrderItemCommand.of(product3.getId(), Quantity.of(1L)),
+        OrderItemCommand.of(product2.getId(), Quantity.of(1L)),
+        OrderItemCommand.of(product1.getId(), Quantity.of(1L))
+    );
+
+    List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+    futures.add(CompletableFuture.runAsync(() -> {
+      orderFacade.createOrder(user1.getId(), commands1, ORDERED_AT_2025_10_30);
+    }));
+
+    futures.add(CompletableFuture.runAsync(() -> {
+      orderFacade.createOrder(user2.getId(), commands2, ORDERED_AT_2025_10_30);
+    }));
+
+    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    List<Product> finalProducts = List.of(
+        productRepository.findById(product1.getId()).orElseThrow(),
+        productRepository.findById(product2.getId()).orElseThrow(),
+        productRepository.findById(product3.getId()).orElseThrow()
+    );
+
+    assertThat(finalProducts)
+        .extracting("stockValue")
+        .containsExactly(8L, 8L, 8L);
+  }
+}
\ No newline at end of file
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java
new file mode 100644
index 000000000..8a0c744ec
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java
@@ -0,0 +1,495 @@
+package com.loopers.application.product;
+
+import com.loopers.application.like.LikeFacade;
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.productlike.ProductLike;
+import com.loopers.domain.productlike.ProductLikeRepository;
+import com.loopers.domain.stock.Stock;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import com.loopers.utils.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+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.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+@SpringBootTest
+class ProductFacadeIntegrationTest {
+
+  private static final LocalDateTime LIKED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @Autowired
+  private ProductFacade productFacade;
+
+  @Autowired
+  private LikeFacade likeFacade;
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  @Autowired
+  private ProductLikeRepository productLikeRepository;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @Nested
+  @DisplayName("상품 목록 조회 시")
+  class GetProducts {
+
+    @Test
+    @DisplayName("브랜드 필터 없이 조회하면 전체 상품이 반환된다")
+    void getsAllProducts_withoutBrandFilter() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품2", Money.of(20000L), "설명2", Stock.of(50L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .hasSize(2)
+          .extracting("productName", "price")
+          .containsExactlyInAnyOrder(
+              tuple("상품1", 10000L),
+              tuple("상품2", 20000L)
+          );
+    }
+
+    @Test
+    @DisplayName("특정 브랜드로 필터링하면 해당 브랜드의 상품만 반환된다")
+    void getsProducts_withBrandFilter() {
+      Brand brand1 = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Brand brand2 = brandRepository.save(Brand.of("브랜드B", "설명B"));
+
+      productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand1.getId())
+      );
+      productRepository.save(
+          Product.of("상품2", Money.of(20000L), "설명2", Stock.of(50L), brand1.getId())
+      );
+      productRepository.save(
+          Product.of("상품3", Money.of(30000L), "설명3", Stock.of(30L), brand2.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(brand1.getId(), null, pageable);
+
+      assertThat(result.getContent())
+          .hasSize(2)
+          .extracting("productName", "price")
+          .containsExactlyInAnyOrder(
+              tuple("상품1", 10000L),
+              tuple("상품2", 20000L)
+          );
+    }
+
+    @Test
+    @DisplayName("사용자가 좋아요한 상품은 isLiked가 true로 반환된다")
+    void includesLikeStatus_whenUserLikesProduct() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product1 = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품2", Money.of(20000L), "설명2", Stock.of(50L), brand.getId())
+      );
+
+      Long userId = 1L;
+      productLikeRepository.save(
+          ProductLike.of(userId, product1.getId(), LIKED_AT_2025_10_30)
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, userId, pageable);
+
+      assertThat(result.getContent())
+          .extracting("productName", "liked")
+          .containsExactlyInAnyOrder(
+              tuple("상품1", true),
+              tuple("상품2", false)
+          );
+    }
+
+    @Test
+    @DisplayName("userId 없이 조회 시 모든 상품의 isLiked가 false로 반환된다")
+    void returnsAllLikedFalse_whenUserIdIsNull() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .extracting("liked")
+          .containsOnly(false);
+    }
+
+    @Test
+    @DisplayName("페이지네이션을 적용하면 페이지 크기만큼만 상품이 반환된다")
+    void supportsPagination() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      for (int i = 1; i <= 5; i++) {
+        productRepository.save(
+            Product.of("상품" + i, Money.of(10000L), "설명" + i, Stock.of(100L), brand.getId())
+        );
+      }
+
+      Pageable pageable = PageRequest.of(0, 2);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertAll(
+          () -> assertThat(result.getContent()).hasSize(2),
+          () -> assertThat(result.getTotalElements()).isEqualTo(5L),
+          () -> assertThat(result.getTotalPages()).isEqualTo(3)
+      );
+    }
+
+    @Test
+    @DisplayName("상품이 하나도 없을 때 빈 페이지를 반환한다")
+    void returnsEmptyPage_whenNoProducts() {
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent()).isEmpty();
+      assertThat(result.getTotalElements()).isZero();
+    }
+
+    @Test
+    @DisplayName("존재하지 않는 브랜드로 필터링 시 빈 페이지를 반환한다")
+    void returnsEmptyPage_whenBrandHasNoProducts() {
+      Brand brand = brandRepository.save(Brand.of("빈브랜드", "상품없음"));
+
+      Pageable pageable = PageRequest.of(0, 20);
+      Page<ProductDetail> result = productFacade.searchProductDetails(brand.getId(), null, pageable);
+
+      assertThat(result.getContent()).isEmpty();
+    }
+
+    @Test
+    @DisplayName("마지막 페이지를 조회하면 남은 상품만 반환된다")
+    void getsLastPage() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      for (int i = 1; i <= 5; i++) {
+        productRepository.save(
+            Product.of("상품" + i, Money.of(10000L), "설명", Stock.of(100L), brand.getId())
+        );
+      }
+
+      Pageable pageable = PageRequest.of(2, 2);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent()).hasSize(1);
+      assertThat(result.isLast()).isTrue();
+    }
+
+    @Test
+    @DisplayName("범위를 벗어난 페이지 조회 시 빈 페이지를 반환한다")
+    void returnsEmptyPage_whenPageExceedsRange() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(10, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent()).isEmpty();
+      assertThat(result.getTotalElements()).isEqualTo(1);
+    }
+
+    @Test
+    @DisplayName("여러 사용자가 좋아요한 상품의 총 좋아요 수가 정확하다")
+    void showsCorrectLikeCount_whenMultipleUsersLike() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("인기상품", Money.of(10000L), "설명", Stock.of(100L), brand.getId())
+      );
+
+      for (long userId = 1L; userId <= 3L; userId++) {
+        likeFacade.likeProduct(userId, product.getId());
+      }
+
+      Pageable pageable = PageRequest.of(0, 20);
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, 1L, pageable);
+
+      assertThat(result.getContent())
+          .hasSize(1)
+          .first()
+          .extracting("likeCount", "liked")
+          .containsExactly(3L, true);
+    }
+
+    @Test
+    @DisplayName("좋아요가 0인 상품도 정상 조회된다")
+    void getsProduct_withZeroLikes() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("인기없는상품", Money.of(10000L), "설명", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .hasSize(1)
+          .first()
+          .extracting("likeCount", "liked")
+          .containsExactly(0L, false);
+    }
+
+    @Test
+    @DisplayName("응답에 브랜드 정보가 정확히 포함된다")
+    void includesBrandSummary_inResponse() {
+      Brand brand = brandRepository.save(Brand.of("나이키", "설명"));
+      productRepository.save(
+          Product.of("에어맥스", Money.of(150000L), "설명", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20);
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .hasSize(1)
+          .first()
+          .extracting("brandId", "brandName")
+          .containsExactly(brand.getId(), "나이키");
+    }
+
+    @Test
+    @DisplayName("존재하지 않는 브랜드 ID로 필터링 시 빈 결과를 반환한다")
+    void returnsEmpty_whenBrandIdDoesNotExist() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명", Stock.of(100L), brand.getId())
+      );
+
+      Long nonExistentBrandId = 999L;
+      Pageable pageable = PageRequest.of(0, 20);
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(nonExistentBrandId, null, pageable);
+
+      assertThat(result.getContent()).isEmpty();
+    }
+  }
+
+  @Nested
+  @DisplayName("상품 상세 조회 시")
+  class GetProduct {
+
+    @Test
+    @DisplayName("상품을 조회하면 상품 정보와 브랜드 정보가 반환된다")
+    void returnsProductWithBrand() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "상세설명", Stock.of(100L), brand.getId())
+      );
+
+      ProductDetail result = productFacade.viewProductDetail(product.getId(), null);
+
+      assertThat(result)
+          .extracting("productName", "price", "description", "stock", "brandName")
+          .containsExactly("상품1", 10000L, "상세설명", 100L, "브랜드A");
+    }
+
+    @Test
+    @DisplayName("좋아요한 상품 조회 시 isLiked가 true로 반환된다")
+    void returnsIsLikedTrue_whenUserLikesProduct() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+
+      Long userId = 1L;
+      productLikeRepository.save(
+          ProductLike.of(userId, product.getId(), LIKED_AT_2025_10_30)
+      );
+
+      ProductDetail result = productFacade.viewProductDetail(product.getId(), userId);
+
+      assertThat(result.liked()).isTrue();
+    }
+
+    @Test
+    @DisplayName("좋아요 안 한 상품 조회 시 isLiked가 false로 반환된다")
+    void returnsIsLikedFalse_whenUserDoesNotLikeProduct() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+
+      Long userId = 1L;
+
+      ProductDetail result = productFacade.viewProductDetail(product.getId(), userId);
+
+      assertThat(result.liked()).isFalse();
+    }
+
+    @Test
+    @DisplayName("존재하지 않는 상품 ID 조회 시 404 예외가 발생한다")
+    void throwsNotFoundException_whenProductDoesNotExist() {
+      Long nonExistentProductId = 999L;
+
+      assertThatThrownBy(() ->
+          productFacade.viewProductDetail(nonExistentProductId, null)
+      )
+          .isInstanceOf(CoreException.class)
+          .extracting("errorType", "message")
+          .containsExactly(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.");
+    }
+
+    @Test
+    @DisplayName("브랜드가 존재하지 않으면 404 예외가 발생한다")
+    void throwsNotFoundException_whenBrandDoesNotExist() {
+      Long nonExistentBrandId = 999L;
+      Product product = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), nonExistentBrandId)
+      );
+      Long productId = product.getId();
+
+      assertThatThrownBy(() ->
+          productFacade.viewProductDetail(productId, null)
+      )
+          .isInstanceOf(CoreException.class)
+          .extracting("errorType", "message")
+          .containsExactly(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다.");
+    }
+  }
+
+  @Nested
+  @DisplayName("상품 정렬 시")
+  class SortProducts {
+
+    @Test
+    @DisplayName("가격 오름차순으로 정렬하면 저렴한 순서로 상품이 반환된다")
+    void sortsByPriceAscending() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품C", Money.of(30000L), "설명C", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품A", Money.of(10000L), "설명A", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품B", Money.of(20000L), "설명B", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20, Sort.by("price").ascending());
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .extracting("productName", "price")
+          .containsExactly(
+              tuple("상품A", 10000L),
+              tuple("상품B", 20000L),
+              tuple("상품C", 30000L)
+          );
+    }
+
+    @Test
+    @DisplayName("좋아요순으로 정렬하면 좋아요가 많은 순서로 상품이 반환된다")
+    void sortsByLikesDescending() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      Product product1 = productRepository.save(
+          Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+      );
+      Product product2 = productRepository.save(
+          Product.of("상품2", Money.of(20000L), "설명2", Stock.of(100L), brand.getId())
+      );
+      Product product3 = productRepository.save(
+          Product.of("상품3", Money.of(30000L), "설명3", Stock.of(100L), brand.getId())
+      );
+
+      for (long userId = 1L; userId <= 5L; userId++) {
+        likeFacade.likeProduct(userId, product1.getId());
+      }
+
+      for (long userId = 1L; userId <= 10L; userId++) {
+        likeFacade.likeProduct(userId, product2.getId());
+      }
+
+      for (long userId = 1L; userId <= 2L; userId++) {
+        likeFacade.likeProduct(userId, product3.getId());
+      }
+
+      Pageable pageable = PageRequest.of(0, 20, Sort.by("likeCount").descending());
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .extracting("productName", "likeCount")
+          .containsExactly(
+              tuple("상품2", 10L),
+              tuple("상품1", 5L),
+              tuple("상품3", 2L)
+          );
+    }
+
+    @Test
+    @DisplayName("최신순으로 정렬하면 최근 생성된 순서로 상품이 반환된다")
+    void sortsByLatest() {
+      Brand brand = brandRepository.save(Brand.of("브랜드A", "설명A"));
+      productRepository.save(
+          Product.of("상품A", Money.of(10000L), "설명A", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품B", Money.of(20000L), "설명B", Stock.of(100L), brand.getId())
+      );
+      productRepository.save(
+          Product.of("상품C", Money.of(30000L), "설명C", Stock.of(100L), brand.getId())
+      );
+
+      Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
+
+      Page<ProductDetail> result = productFacade.searchProductDetails(null, null, pageable);
+
+      assertThat(result.getContent())
+          .extracting("productName", "price")
+          .containsExactly(
+              tuple("상품C", 30000L),
+              tuple("상품B", 20000L),
+              tuple("상품A", 10000L)
+          );
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java
new file mode 100644
index 000000000..762446305
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java
@@ -0,0 +1,139 @@
+package com.loopers.domain.brand;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.loopers.utils.DatabaseCleanUp;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.AfterEach;
+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.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class BrandServiceIntegrationTest {
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  private BrandService brandService;
+
+  @BeforeEach
+  void setUp() {
+    brandService = new BrandService(brandRepository);
+  }
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @Nested
+  @DisplayName("브랜드 단건 조회 시")
+  class GetById {
+
+    @Test
+    @DisplayName("해당 ID의 브랜드가 존재하면 브랜드가 반환된다")
+    void returnsBrand_whenBrandExists() {
+      // given
+      Brand brand = brandRepository.save(Brand.of("나이키", "스포츠 브랜드"));
+      Long brandId = brand.getId();
+
+      // when
+      Optional<Brand> result = brandService.getById(brandId);
+
+      // then
+      assertThat(result).isPresent();
+      assertThat(result.get())
+          .extracting("name", "description")
+          .containsExactly("나이키", "스포츠 브랜드");
+    }
+
+    @Test
+    @DisplayName("해당 ID의 브랜드가 존재하지 않으면 빈 Optional이 반환된다")
+    void returnsEmpty_whenBrandDoesNotExist() {
+      // given
+      Long nonExistentBrandId = 999L;
+
+      // when
+      Optional<Brand> result = brandService.getById(nonExistentBrandId);
+
+      // then
+      assertThat(result).isEmpty();
+    }
+  }
+
+  @Nested
+  @DisplayName("브랜드 다건 조회 시")
+  class FindByIdIn {
+
+    @Test
+    @DisplayName("모든 ID가 존재하면 해당 브랜드들이 모두 반환된다")
+    void returnsBrands_whenBrandsExist() {
+      // given
+      Brand brand1 = brandRepository.save(Brand.of("나이키", "스포츠 브랜드"));
+      Brand brand2 = brandRepository.save(Brand.of("아디다스", "스포츠 브랜드"));
+      Brand brand3 = brandRepository.save(Brand.of("퓨마", "스포츠 브랜드"));
+
+      List<Long> brandIds = List.of(brand1.getId(), brand2.getId(), brand3.getId());
+
+      // when
+      List<Brand> result = brandService.findByIdIn(brandIds);
+
+      // then
+      assertThat(result)
+          .hasSize(3)
+          .extracting("name")
+          .containsExactlyInAnyOrder("나이키", "아디다스", "퓨마");
+    }
+
+    @Test
+    @DisplayName("일부 ID만 존재하면 존재하는 브랜드만 반환된다")
+    void returnsExistingBrandsOnly_whenSomeBrandsDoNotExist() {
+      // given
+      Brand brand1 = brandRepository.save(Brand.of("나이키", "스포츠 브랜드"));
+      List<Long> brandIds = List.of(brand1.getId(), 999L, 998L);
+
+      // when
+      List<Brand> result = brandService.findByIdIn(brandIds);
+
+      // then
+      assertThat(result)
+          .hasSize(1)
+          .extracting("name")
+          .containsExactly("나이키");
+    }
+
+    @Test
+    @DisplayName("모든 ID가 존재하지 않으면 빈 리스트가 반환된다")
+    void returnsEmptyList_whenNoBrandsExist() {
+      // given
+      List<Long> nonExistentBrandIds = List.of(999L, 998L, 997L);
+
+      // when
+      List<Brand> result = brandService.findByIdIn(nonExistentBrandIds);
+
+      // then
+      assertThat(result).isEmpty();
+    }
+
+    @Test
+    @DisplayName("빈 ID 목록으로 조회하면 빈 리스트가 반환된다")
+    void returnsEmptyList_whenEmptyIdList() {
+      // given
+      List<Long> emptyBrandIds = List.of();
+
+      // when
+      List<Brand> result = brandService.findByIdIn(emptyBrandIds);
+
+      // then
+      assertThat(result).isEmpty();
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java
new file mode 100644
index 000000000..0c80c6453
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java
@@ -0,0 +1,80 @@
+package com.loopers.domain.brand;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.NullAndEmptySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Brand 도메인 테스트")
+class BrandTest {
+
+  @DisplayName("Brand를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("브랜드명과 설명으로 생성하면 성공한다")
+    @Test
+    void shouldCreate_whenNameAndDescription() {
+      String name = "Nike";
+      String description = "스포츠 브랜드";
+
+      Brand brand = Brand.of(name, description);
+
+      assertThat(brand).extracting("name", "description")
+          .containsExactly(name, description);
+    }
+
+    @DisplayName("브랜드명만으로 생성하면 성공한다")
+    @Test
+    void shouldCreate_whenNameOnly() {
+      String name = "Adidas";
+
+      Brand brand = Brand.of(name);
+
+      assertThat(brand).extracting("name", "description")
+          .containsExactly(name, null);
+    }
+  }
+
+  @DisplayName("name 검증")
+  @Nested
+  class ValidateName {
+
+    @DisplayName("50자 이내면 정상 생성된다")
+    @Test
+    void shouldCreate_whenValidLength() {
+      String maxLengthName = "a".repeat(50);
+
+      Brand brand = Brand.of(maxLengthName, "설명");
+
+      assertThat(brand).extracting("name").isEqualTo(maxLengthName);
+    }
+
+    @DisplayName("null 또는 빈 문자열이면 예외가 발생한다")
+    @ParameterizedTest
+    @NullAndEmptySource
+    void shouldThrowException_whenNullOrEmpty(String invalidName) {
+      assertThatThrownBy(() -> Brand.of(invalidName, "설명"))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("브랜드명은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_BRAND_NAME_EMPTY);
+    }
+
+    @DisplayName("50자를 초과하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenTooLong() {
+      String tooLongName = "a".repeat(51);
+
+      assertThatThrownBy(() -> Brand.of(tooLongName, "설명"))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("브랜드명은 50자 이내여야 합니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_BRAND_NAME_LENGTH);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandsTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandsTest.java
new file mode 100644
index 000000000..cbc6a529d
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandsTest.java
@@ -0,0 +1,38 @@
+package com.loopers.domain.brand;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Brands 일급컬렉션 테스트")
+class BrandsTest {
+
+  @Test
+  @DisplayName("Brand 리스트로 일급컬렉션을 생성한다")
+  void from() {
+    Brand brand1 = Brand.of("브랜드1", "설명1");
+    Brand brand2 = Brand.of("브랜드2", "설명2");
+    List<Brand> brandList = List.of(brand1, brand2);
+
+    Brands brands = Brands.from(brandList);
+
+    assertThat(brands.toList()).hasSize(2);
+  }
+
+  @Test
+  @DisplayName("내부 리스트를 불변 복사본으로 반환한다")
+  void toList() {
+    Brand brand = Brand.of("브랜드1", "설명1");
+    Brands brands = Brands.from(List.of(brand));
+
+    List<Brand> list1 = brands.toList();
+    List<Brand> list2 = brands.toList();
+
+    assertThat(list1)
+        .isNotSameAs(list2)
+        .isEqualTo(list2);
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/money/MoneyTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/money/MoneyTest.java
new file mode 100644
index 000000000..94c3cf8b1
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/money/MoneyTest.java
@@ -0,0 +1,71 @@
+package com.loopers.domain.money;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Money VO 테스트")
+class MoneyTest {
+
+  @DisplayName("Money를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      assertThatThrownBy(() -> Money.of(null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("금액은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_MONEY_VALUE);
+    }
+
+    @DisplayName("음수면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegative() {
+      assertThatThrownBy(() -> Money.of(-1000L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("금액은 음수일 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.NEGATIVE_MONEY_VALUE);
+    }
+
+    @DisplayName("0원 이상으로 금액을 생성할 수 있다")
+    @ParameterizedTest
+    @ValueSource(longs = {0L, 10000L, 100000L})
+    void shouldCreate_whenValidValue(Long value) {
+      Money money = Money.of(value);
+      assertThat(money).extracting("value").isEqualTo(value);
+    }
+
+  }
+
+  @DisplayName("두 객체의 동등성 비교할 때")
+  @Nested
+  class Equality {
+
+    @DisplayName("같은 금액의 Money는 동등하다")
+    @Test
+    void shouldBeEqual_whenSameValue() {
+      Money money1 = Money.of(1000L);
+      Money money2 = Money.of(1000L);
+
+      assertThat(money1).isEqualTo(money2);
+    }
+
+    @DisplayName("다른 금액의 Money는 동등하지 않다")
+    @Test
+    void shouldNotBeEqual_whenDifferentValue() {
+      Money money1 = Money.of(1000L);
+      Money money2 = Money.of(2000L);
+
+      assertThat(money1).isNotEqualTo(money2);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCreateServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCreateServiceTest.java
new file mode 100644
index 000000000..e3d9692ed
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderCreateServiceTest.java
@@ -0,0 +1,116 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.money.Money;
+import com.loopers.domain.order.orderitem.OrderItemCommand;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.domain.stock.Stock;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SpringBootTest
+@Transactional
+@DisplayName("OrderCreateService 통합 테스트")
+class OrderCreateServiceTest {
+
+  @Autowired
+  private OrderCreateService orderCreateService;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  @Autowired
+  private BrandRepository brandRepository;
+
+  private Brand brand;
+  private Product product1;
+  private Product product2;
+
+  @BeforeEach
+  void setUp() {
+    brand = brandRepository.save(Brand.of("테스트브랜드"));
+    product1 = productRepository.save(
+        Product.of("상품1", Money.of(10000L), "설명1", Stock.of(100L), brand.getId())
+    );
+    product2 = productRepository.save(
+        Product.of("상품2", Money.of(30000L), "설명2", Stock.of(50L), brand.getId())
+    );
+  }
+
+  @Nested
+  @DisplayName("주문 준비")
+  class PrepareOrder {
+
+    @Test
+    @DisplayName("정상적으로 주문을 준비한다")
+    void prepareOrder_success() {
+      Map<Long, Product> productMap = Map.of(
+          product1.getId(), product1,
+          product2.getId(), product2
+      );
+
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(product1.getId(), Quantity.of(10L)),
+          OrderItemCommand.of(product2.getId(), Quantity.of(5L))
+      );
+
+      OrderPreparation result = orderCreateService.prepareOrder(commands, productMap);
+
+      assertThat(result.orderItems()).isNotNull();
+      assertThat(result.totalAmount()).isEqualTo(250000L);
+      assertThat(product1.getStockValue()).isEqualTo(100L);
+      assertThat(product2.getStockValue()).isEqualTo(50L);
+    }
+
+    @Test
+    @DisplayName("상품을 찾을 수 없으면 예외 발생")
+    void prepareOrder_productNotFound() {
+      Map<Long, Product> productMap = Map.of();
+
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(999L, Quantity.of(10L))
+      );
+
+      assertThatThrownBy(() -> orderCreateService.prepareOrder(commands, productMap))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품을 찾을 수 없습니다.")
+          .extracting(ex -> ((CoreException) ex).getErrorType())
+          .isEqualTo(ErrorType.NOT_FOUND);
+    }
+
+    @Test
+    @DisplayName("재고가 부족하면 예외 발생")
+    void prepareOrder_insufficientStock() {
+      Product lowStockProduct = productRepository.save(
+          Product.of("재고부족상품", Money.of(10000L), "설명", Stock.of(5L), brand.getId())
+      );
+
+      Map<Long, Product> productMap = Map.of(lowStockProduct.getId(), lowStockProduct);
+
+      List<OrderItemCommand> commands = List.of(
+          OrderItemCommand.of(lowStockProduct.getId(), Quantity.of(10L))
+      );
+
+      assertThatThrownBy(() -> orderCreateService.prepareOrder(commands, productMap))
+          .isInstanceOf(CoreException.class)
+          .extracting(ex -> ((CoreException) ex).getErrorType())
+          .isEqualTo(ErrorType.INSUFFICIENT_STOCK);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderRepositoryIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderRepositoryIntegrationTest.java
new file mode 100644
index 000000000..ed9a31f84
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderRepositoryIntegrationTest.java
@@ -0,0 +1,172 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.utils.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+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.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+@SpringBootTest
+@Transactional
+@DisplayName("OrderRepository 통합 테스트")
+class OrderRepositoryIntegrationTest {
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @Autowired
+  private OrderRepository orderRepository;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @DisplayName("주문 목록 조회 (@Query + DTO)")
+  @Nested
+  class FindOrderList {
+
+    @DisplayName("사용자 ID로 주문을 페이지네이션 조회하면 DTO 목록이 반환된다")
+    @Test
+    void shouldReturnDtoPage() {
+      // given
+      Long userId = 1L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      Order order1 = Order.of(userId, OrderStatus.COMPLETED, 30000L, orderedAt);
+      order1.addItem(OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L)));
+      order1.addItem(OrderItem.of(200L, "상품2", Quantity.of(2L), OrderPrice.of(10000L)));
+      orderRepository.save(order1);
+
+      Order order2 = Order.of(userId, OrderStatus.PENDING, 50000L, orderedAt.plusDays(1));
+      order2.addItem(OrderItem.of(300L, "상품3", Quantity.of(5L), OrderPrice.of(10000L)));
+      orderRepository.save(order2);
+
+      PageRequest pageRequest = PageRequest.of(0, 10);
+
+      // when
+      Page<OrderListDto> result = orderRepository.findOrderList(userId, pageRequest);
+
+      // then
+      assertThat(result.getContent())
+          .hasSize(2)
+          .extracting("userId", "status", "itemCount", "totalAmount")
+          .containsExactly(
+              tuple(userId, OrderStatus.COMPLETED, 2, 30000L),
+              tuple(userId, OrderStatus.PENDING, 1, 50000L)
+          );
+    }
+
+    @DisplayName("다른 사용자의 주문은 조회되지 않는다")
+    @Test
+    void shouldFilterByUserId() {
+      // given
+      Long userId1 = 1L;
+      Long userId2 = 2L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      Order order1 = Order.of(userId1, OrderStatus.COMPLETED, 10000L, orderedAt);
+      order1.addItem(OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L)));
+      orderRepository.save(order1);
+
+      Order order2 = Order.of(userId2, OrderStatus.PENDING, 20000L, orderedAt);
+      order2.addItem(OrderItem.of(200L, "상품2", Quantity.of(2L), OrderPrice.of(10000L)));
+      orderRepository.save(order2);
+
+      PageRequest pageRequest = PageRequest.of(0, 10);
+
+      // when
+      Page<OrderListDto> result = orderRepository.findOrderList(userId1, pageRequest);
+
+      // then
+      assertThat(result.getContent())
+          .hasSize(1)
+          .element(0).extracting("userId")
+          .isEqualTo(userId1);
+    }
+
+    @DisplayName("15개 주문 중 두 번째 페이지(size=10)를 조회하면 5개가 반환된다")
+    @Test
+    void shouldPaginateCorrectly() {
+      // given
+      Long userId = 1L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      for (int i = 0; i < 15; i++) {
+        Order order = Order.of(userId, OrderStatus.COMPLETED, 10000L, orderedAt.plusDays(i));
+        order.addItem(OrderItem.of(100L + i, "상품" + i, Quantity.of(1L), OrderPrice.of(10000L)));
+        orderRepository.save(order);
+      }
+
+      PageRequest pageRequest = PageRequest.of(1, 10);
+
+      // when
+      Page<OrderListDto> result = orderRepository.findOrderList(userId, pageRequest);
+
+      // then
+      assertAll(
+          () -> assertThat(result.getContent()).hasSize(5),
+          () -> assertThat(result.getTotalElements()).isEqualTo(15L),
+          () -> assertThat(result.getTotalPages()).isEqualTo(2)
+      );
+    }
+  }
+
+  @DisplayName("주문 상세 조회 (@EntityGraph)")
+  @Nested
+  class FindWithItemsById {
+
+    @DisplayName("OrderItem을 포함하여 조회하면 함께 조회된다 (N+1 방지)")
+    @Test
+    void shouldFetchWithItems() {
+      // given
+      Long userId = 1L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      Order order = Order.of(userId, OrderStatus.COMPLETED, 30000L, orderedAt);
+      order.addItem(OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L)));
+      order.addItem(OrderItem.of(200L, "상품2", Quantity.of(2L), OrderPrice.of(10000L)));
+      Order savedOrder = orderRepository.save(order);
+
+      // when
+      Optional<Order> result = orderRepository.findWithItemsById(savedOrder.getId());
+
+      // then
+      assertThat(result).isPresent();
+      assertThat(result.get().getItems())
+          .hasSize(2)
+          .extracting("productName")
+          .containsExactly("상품1", "상품2");
+    }
+
+    @DisplayName("존재하지 않는 ID로 조회하면 빈 Optional이 반환된다")
+    @Test
+    void shouldReturnEmpty_whenNotFound() {
+      // given
+      Long nonExistentId = 999L;
+
+      // when
+      Optional<Order> result = orderRepository.findWithItemsById(nonExistentId);
+
+      // then
+      assertThat(result).isEmpty();
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
new file mode 100644
index 000000000..b087a7fbb
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java
@@ -0,0 +1,243 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.utils.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+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.boot.test.context.SpringBootTest;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@SpringBootTest
+@DisplayName("OrderService 통합 테스트")
+class OrderServiceIntegrationTest {
+
+  @Autowired
+  private OrderService orderService;
+
+  @Autowired
+  private DatabaseCleanUp databaseCleanUp;
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @AfterEach
+  void tearDown() {
+    databaseCleanUp.truncateAllTables();
+  }
+
+  @Nested
+  @DisplayName("주문 생성 및 조회")
+  class CreateAndGetOrder {
+
+    @Test
+    @DisplayName("주문을 생성한 뒤 ID로 조회하면 생성된 주문 정보가 정확히 반환된다")
+    void createOrder_thenGet_dataMatches() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(2L), OrderPrice.of(10000L)),
+          OrderItem.of(200L, "상품2", Quantity.of(1L), OrderPrice.of(30000L))
+      );
+
+      // when
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      Order foundOrder = orderService.getById(savedOrder.getId()).orElseThrow();
+
+      // then
+      assertThat(foundOrder)
+          .extracting("userId", "status", "orderedAt")
+          .containsExactly(userId, OrderStatus.PENDING, ORDERED_AT_2025_10_30);
+      assertThat(foundOrder.getTotalAmountValue()).isEqualTo(50000L);
+    }
+
+    @Test
+    @DisplayName("주문 생성 시 OrderItem이 함께 저장되고 조회된다")
+    void createOrder_itemsCascadePersisted() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L)),
+          OrderItem.of(200L, "상품2", Quantity.of(2L), OrderPrice.of(20000L))
+      );
+
+      // when
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      Order foundOrder = orderService.getWithItemsById(savedOrder.getId()).orElseThrow();
+
+      // then
+      assertThat(foundOrder.getItems())
+          .hasSize(2)
+          .extracting("productId", "productName")
+          .containsExactlyInAnyOrder(
+              org.assertj.core.groups.Tuple.tuple(100L, "상품1"),
+              org.assertj.core.groups.Tuple.tuple(200L, "상품2")
+          );
+    }
+  }
+
+  @Nested
+  @DisplayName("주문 완료 (PENDING → COMPLETED)")
+  class CompleteOrder {
+
+    @Test
+    @DisplayName("COMPLETED 주문을 완료 시도하면 예외가 발생한다")
+    void shouldThrowException_whenCompletedOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      orderService.completeOrder(savedOrder.getId());
+
+      // when & then
+      assertThatThrownBy(() -> orderService.completeOrder(savedOrder.getId()))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PENDING 상태의 주문만 완료할 수 있습니다.");
+    }
+
+    @Test
+    @DisplayName("PAYMENT_FAILED 주문을 완료 시도하면 예외가 발생한다")
+    void shouldThrowException_whenPaymentFailedOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      orderService.failPaymentOrder(savedOrder.getId());
+
+      // when & then
+      assertThatThrownBy(() -> orderService.completeOrder(savedOrder.getId()))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PENDING 상태의 주문만 완료할 수 있습니다.");
+    }
+
+    @Test
+    @DisplayName("PENDING 주문을 완료하면 COMPLETED 상태로 변경된다")
+    void shouldChangeToCompleted_whenPendingOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+
+      // when
+      orderService.completeOrder(savedOrder.getId());
+      Order foundOrder = orderService.getById(savedOrder.getId()).orElseThrow();
+
+      // then
+      assertThat(foundOrder).extracting("status").isEqualTo(OrderStatus.COMPLETED);
+    }
+  }
+
+  @Nested
+  @DisplayName("결제 실패 (PENDING → PAYMENT_FAILED)")
+  class FailPayment {
+
+    @Test
+    @DisplayName("COMPLETED 주문을 결제 실패 처리하면 예외가 발생한다")
+    void shouldThrowException_whenCompletedOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      orderService.completeOrder(savedOrder.getId());
+
+      // when & then
+      assertThatThrownBy(() -> orderService.failPaymentOrder(savedOrder.getId()))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PENDING 상태의 주문만 결제 실패 상태로 변경할 수 있습니다.");
+    }
+
+    @Test
+    @DisplayName("PENDING 주문의 결제가 실패하면 PAYMENT_FAILED 상태로 변경된다")
+    void shouldChangeToPaymentFailed_whenPendingOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+
+      // when
+      orderService.failPaymentOrder(savedOrder.getId());
+      Order foundOrder = orderService.getById(savedOrder.getId()).orElseThrow();
+
+      // then
+      assertThat(foundOrder).extracting("status").isEqualTo(OrderStatus.PAYMENT_FAILED);
+    }
+  }
+
+  @Nested
+  @DisplayName("재시도 완료 (PAYMENT_FAILED → COMPLETED)")
+  class RetryComplete {
+
+    @Test
+    @DisplayName("PENDING 주문을 재시도 완료하면 예외가 발생한다")
+    void shouldThrowException_whenPendingOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+
+      // when & then
+      assertThatThrownBy(() -> orderService.retryCompleteOrder(savedOrder.getId()))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PAYMENT_FAILED 상태의 주문만 재시도 완료할 수 있습니다.");
+    }
+
+    @Test
+    @DisplayName("COMPLETED 주문을 재시도 완료하면 예외가 발생한다")
+    void shouldThrowException_whenCompletedOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      orderService.completeOrder(savedOrder.getId());
+
+      // when & then
+      assertThatThrownBy(() -> orderService.retryCompleteOrder(savedOrder.getId()))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PAYMENT_FAILED 상태의 주문만 재시도 완료할 수 있습니다.");
+    }
+
+    @Test
+    @DisplayName("PAYMENT_FAILED 주문을 재시도 완료하면 COMPLETED 상태로 변경된다")
+    void shouldChangeToCompleted_whenPaymentFailedOrder() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+      Order savedOrder = orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+      orderService.failPaymentOrder(savedOrder.getId());
+
+      // when
+      orderService.retryCompleteOrder(savedOrder.getId());
+      Order foundOrder = orderService.getById(savedOrder.getId()).orElseThrow();
+
+      // then
+      assertThat(foundOrder).extracting("status").isEqualTo(OrderStatus.COMPLETED);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
new file mode 100644
index 000000000..5257226a8
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
@@ -0,0 +1,206 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.domain.quantity.Quantity;
+import org.junit.jupiter.api.DisplayName;
+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.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.times;
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("OrderService 단위 테스트")
+class OrderServiceTest {
+
+  @Mock
+  private OrderRepository orderRepository;
+
+  @InjectMocks
+  private OrderService orderService;
+
+  @Captor
+  private ArgumentCaptor<Order> orderCaptor;
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @Nested
+  @DisplayName("주문 생성")
+  class CreateOrder {
+
+    @Test
+    @DisplayName("사용자 ID와 주문 항목 목록으로 주문을 생성하면 총액이 계산되고 OrderItem이 추가된다")
+    void createOrder_success() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(2L), OrderPrice.of(10000L)),
+          OrderItem.of(200L, "상품2", Quantity.of(1L), OrderPrice.of(30000L))
+      );
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      given(orderRepository.save(any(Order.class)))
+          .willAnswer(invocation -> invocation.getArgument(0));
+
+      // when
+      orderService.create(userId, orderItems, orderedAt);
+
+      // then
+      then(orderRepository).should(times(1)).save(orderCaptor.capture());
+      Order capturedOrder = orderCaptor.getValue();
+
+      assertThat(capturedOrder)
+          .extracting("userId", "status", "orderedAt")
+          .containsExactly(userId, OrderStatus.PENDING, orderedAt);
+      assertThat(capturedOrder.getTotalAmountValue()).isEqualTo(50000L);
+
+      assertThat(capturedOrder.getItems()).hasSize(2);
+    }
+
+    @Test
+    @DisplayName("주문 생성 시 OrderItem이 Order에 추가되고 양방향 관계가 설정된다")
+    void createOrder_bidirectionalRelationship() {
+      // given
+      Long userId = 1L;
+      List<OrderItem> orderItems = List.of(
+          OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L))
+      );
+
+      given(orderRepository.save(any(Order.class)))
+          .willAnswer(invocation -> invocation.getArgument(0));
+
+      // when
+      orderService.create(userId, orderItems, ORDERED_AT_2025_10_30);
+
+      // then
+      then(orderRepository).should(times(1)).save(orderCaptor.capture());
+      Order capturedOrder = orderCaptor.getValue();
+
+      assertThat(capturedOrder.getItems()).hasSize(1);
+
+      OrderItem capturedItem = capturedOrder.getItems().get(0);
+      assertThat(capturedItem.getOrder()).isEqualTo(capturedOrder);
+
+      assertThat(capturedItem)
+          .extracting("productId", "productName")
+          .containsExactly(100L, "상품1");
+      assertThat(capturedItem.getQuantityValue()).isEqualTo(1L);
+      assertThat(capturedItem.getOrderPriceValue()).isEqualTo(10000L);
+    }
+  }
+
+  @Nested
+  @DisplayName("주문 조회")
+  class GetOrder {
+
+    @Test
+    @DisplayName("주문 ID로 조회 시 해당 주문이 존재하면 주문 정보가 반환된다")
+    void getById_success() {
+      // given
+      Long orderId = 1L;
+      Order order = Order.of(1L, OrderStatus.COMPLETED, 50000L, ORDERED_AT_2025_10_30);
+      given(orderRepository.findById(orderId)).willReturn(Optional.of(order));
+
+      // when
+      Optional<Order> result = orderService.getById(orderId);
+
+      // then
+      assertThat(result).isPresent();
+      assertThat(result.get())
+          .extracting("userId", "status")
+          .containsExactly(1L, OrderStatus.COMPLETED);
+      then(orderRepository).should(times(1)).findById(orderId);
+    }
+
+    @Test
+    @DisplayName("주문 ID로 조회 시 해당 주문이 존재하지 않으면 Optional.empty()가 반환된다")
+    void getById_notFound() {
+      // given
+      Long orderId = 999L;
+      given(orderRepository.findById(orderId)).willReturn(Optional.empty());
+
+      // when
+      Optional<Order> result = orderService.getById(orderId);
+
+      // then
+      assertThat(result).isEmpty();
+    }
+
+    @Test
+    @DisplayName("주문 ID로 조회 시 @EntityGraph를 사용하여 OrderItem과 함께 조회된다")
+    void getWithItemsById_success() {
+      // given
+      Long orderId = 1L;
+      Order order = Order.of(1L, OrderStatus.COMPLETED, 50000L, ORDERED_AT_2025_10_30);
+      given(orderRepository.findWithItemsById(orderId)).willReturn(Optional.of(order));
+
+      // when
+      Optional<Order> result = orderService.getWithItemsById(orderId);
+
+      // then
+      assertThat(result).isPresent();
+      then(orderRepository).should(times(1)).findWithItemsById(orderId);
+    }
+  }
+
+  @Nested
+  @DisplayName("주문 완료")
+  class CompleteOrder {
+
+    @Test
+    @DisplayName("PENDING 상태의 주문을 완료할 수 있다")
+    void shouldCompleteOrder_whenPending() {
+      // given
+      Long orderId = 1L;
+      Order order = Order.of(1L, OrderStatus.PENDING, 50000L, ORDERED_AT_2025_10_30);
+      given(orderRepository.findById(orderId)).willReturn(Optional.of(order));
+      given(orderRepository.save(order)).willReturn(order);
+
+      // when
+      Order result = orderService.completeOrder(orderId);
+
+      // then
+      assertThat(result.getStatus()).isEqualTo(OrderStatus.COMPLETED);
+      then(orderRepository).should(times(1)).findById(orderId);
+      then(orderRepository).should(times(1)).save(order);
+    }
+  }
+
+  @Nested
+  @DisplayName("재시도 후 주문 완료")
+  class RetryCompleteOrder {
+
+    @Test
+    @DisplayName("PAYMENT_FAILED 상태의 주문을 재시도 완료할 수 있다")
+    void shouldRetryCompleteOrder_whenPaymentFailed() {
+      // given
+      Long orderId = 1L;
+      Order order = Order.of(1L, OrderStatus.PAYMENT_FAILED, 50000L, ORDERED_AT_2025_10_30);
+      given(orderRepository.findById(orderId)).willReturn(Optional.of(order));
+      given(orderRepository.save(order)).willReturn(order);
+
+      // when
+      Order result = orderService.retryCompleteOrder(orderId);
+
+      // then
+      assertThat(result.getStatus()).isEqualTo(OrderStatus.COMPLETED);
+      then(orderRepository).should(times(1)).findById(orderId);
+      then(orderRepository).should(times(1)).save(order);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
new file mode 100644
index 000000000..c84b34193
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -0,0 +1,269 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.order.orderitem.OrderItem;
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Order 도메인 테스트")
+class OrderTest {
+
+  private static final LocalDateTime ORDERED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @DisplayName("Order를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("사용자 ID, 주문 상태, 총액, 주문 시각을 모두 제공하면 Order가 생성된다")
+    @Test
+    void shouldCreate_whenValid() {
+      Long userId = 1L;
+      OrderStatus status = OrderStatus.COMPLETED;
+      Long totalAmount = 50000L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      Order order = Order.of(userId, status, totalAmount, orderedAt);
+
+      assertThat(order).extracting("userId", "status", "orderedAt")
+          .containsExactly(userId, status, orderedAt);
+      assertThat(order.getTotalAmountValue()).isEqualTo(totalAmount);
+    }
+  }
+
+  @DisplayName("userId 검증")
+  @Nested
+  class ValidateUserId {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      OrderStatus status = OrderStatus.COMPLETED;
+      Long totalAmount = 50000L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      assertThatThrownBy(() -> Order.of(null, status, totalAmount, orderedAt))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("사용자는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_USER_EMPTY);
+    }
+  }
+
+  @DisplayName("status 검증")
+  @Nested
+  class ValidateStatus {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long userId = 1L;
+      Long totalAmount = 50000L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      assertThatThrownBy(() -> Order.of(userId, null, totalAmount, orderedAt))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("주문 상태는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_STATUS_EMPTY);
+    }
+  }
+
+  @DisplayName("totalAmount 검증")
+  @Nested
+  class ValidateTotalAmount {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long userId = 1L;
+      OrderStatus status = OrderStatus.COMPLETED;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      assertThatThrownBy(() -> Order.of(userId, status, null, orderedAt))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("금액은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_MONEY_VALUE);
+    }
+
+    @DisplayName("음수이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegative() {
+      Long userId = 1L;
+      OrderStatus status = OrderStatus.COMPLETED;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      assertThatThrownBy(() -> Order.of(userId, status, -1000L, orderedAt))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("금액은 음수일 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.NEGATIVE_MONEY_VALUE);
+    }
+
+    @DisplayName("0이면 정상 생성된다")
+    @Test
+    void shouldCreate_whenZero() {
+      Long userId = 1L;
+      OrderStatus status = OrderStatus.COMPLETED;
+      Long totalAmount = 0L;
+      LocalDateTime orderedAt = ORDERED_AT_2025_10_30;
+
+      Order order = Order.of(userId, status, totalAmount, orderedAt);
+
+      assertThat(order.getTotalAmountValue()).isZero();
+    }
+  }
+
+  @DisplayName("orderedAt 검증")
+  @Nested
+  class ValidateOrderedAt {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long userId = 1L;
+      OrderStatus status = OrderStatus.COMPLETED;
+      Long totalAmount = 50000L;
+
+      assertThatThrownBy(() -> Order.of(userId, status, totalAmount, null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("주문 시각은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ORDERED_AT_EMPTY);
+    }
+  }
+
+  @DisplayName("주문 완료")
+  @Nested
+  class Complete {
+
+    @DisplayName("PENDING 상태에서 완료할 수 있다")
+    @Test
+    void shouldComplete_whenPending() {
+      Order order = Order.of(1L, OrderStatus.PENDING, 50000L, ORDERED_AT_2025_10_30);
+
+      order.complete();
+
+      assertThat(order).extracting("status").isEqualTo(OrderStatus.COMPLETED);
+    }
+
+    @DisplayName("PENDING이 아닌 상태에서 완료하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNotPending() {
+      Order order = Order.of(1L, OrderStatus.COMPLETED, 50000L, ORDERED_AT_2025_10_30);
+
+      assertThatThrownBy(order::complete)
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PENDING 상태의 주문만 완료할 수 있습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.ORDER_CANNOT_COMPLETE);
+    }
+  }
+
+  @DisplayName("결제 실패 상태 변경")
+  @Nested
+  class FailPayment {
+
+    @DisplayName("PENDING 상태에서 PAYMENT_FAILED로 변경할 수 있다")
+    @Test
+    void shouldFailPayment_whenPending() {
+      Order order = Order.of(1L, OrderStatus.PENDING, 50000L, ORDERED_AT_2025_10_30);
+
+      order.failPayment();
+
+      assertThat(order).extracting("status").isEqualTo(OrderStatus.PAYMENT_FAILED);
+    }
+
+    @DisplayName("PENDING이 아닌 상태에서 변경하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNotPending() {
+      Order order = Order.of(1L, OrderStatus.COMPLETED, 50000L, ORDERED_AT_2025_10_30);
+
+      assertThatThrownBy(order::failPayment)
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PENDING 상태의 주문만 결제 실패 상태로 변경할 수 있습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.ORDER_CANNOT_FAIL_PAYMENT);
+    }
+  }
+
+  @DisplayName("재시도 후 주문 완료")
+  @Nested
+  class RetryComplete {
+
+    @DisplayName("PAYMENT_FAILED 상태에서 완료할 수 있다")
+    @Test
+    void shouldRetryComplete_whenPaymentFailed() {
+      Order order = Order.of(1L, OrderStatus.PAYMENT_FAILED, 50000L, ORDERED_AT_2025_10_30);
+
+      order.retryComplete();
+
+      assertThat(order).extracting("status").isEqualTo(OrderStatus.COMPLETED);
+    }
+
+    @DisplayName("PAYMENT_FAILED가 아닌 상태에서 완료하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNotPaymentFailed() {
+      Order order = Order.of(1L, OrderStatus.PENDING, 50000L, ORDERED_AT_2025_10_30);
+
+      assertThatThrownBy(order::retryComplete)
+          .isInstanceOf(CoreException.class)
+          .hasMessage("PAYMENT_FAILED 상태의 주문만 재시도 완료할 수 있습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.ORDER_CANNOT_RETRY_COMPLETE);
+    }
+  }
+
+  @DisplayName("Order-OrderItem 협업 테스트")
+  @Nested
+  class OrderWithOrderItem {
+
+    @DisplayName("OrderItem을 추가하면 items에 포함된다")
+    @Test
+    void shouldAddItem() {
+      // given
+      Order order = Order.of(1L, OrderStatus.PENDING, 0L, ORDERED_AT_2025_10_30);
+      OrderItem item = OrderItem.of(100L, "테스트 상품", Quantity.of(2L), OrderPrice.of(10000L));
+
+      // when
+      order.addItem(item);
+
+      // then
+      assertThat(order.getItems()).hasSize(1);
+      assertThat(order.getItems()).contains(item);
+    }
+
+    @DisplayName("여러 OrderItem을 추가할 수 있다")
+    @Test
+    void shouldAddMultipleItems() {
+      // given
+      Order order = Order.of(1L, OrderStatus.PENDING, 0L, ORDERED_AT_2025_10_30);
+      OrderItem item1 = OrderItem.of(100L, "상품1", Quantity.of(1L), OrderPrice.of(10000L));
+      OrderItem item2 = OrderItem.of(200L, "상품2", Quantity.of(2L), OrderPrice.of(20000L));
+
+      // when
+      order.addItem(item1);
+      order.addItem(item2);
+
+      // then
+      assertThat(order.getItems()).hasSize(2);
+      assertThat(order.getItems()).containsExactly(item1, item2);
+    }
+
+    @DisplayName("OrderItem 추가 시 양방향 관계가 설정된다")
+    @Test
+    void shouldSetBidirectionalRelationship() {
+      // given
+      Order order = Order.of(1L, OrderStatus.PENDING, 0L, ORDERED_AT_2025_10_30);
+      OrderItem item = OrderItem.of(100L, "테스트 상품", Quantity.of(2L), OrderPrice.of(10000L));
+
+      // when
+      order.addItem(item);
+
+      // then
+      // OrderItem을 블랙박스로 취급하여 최소한의 검증만 수행
+      assertThat(item.getOrder()).isEqualTo(order);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderItemTest.java
new file mode 100644
index 000000000..2b3e0a32e
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderItemTest.java
@@ -0,0 +1,172 @@
+package com.loopers.domain.order.orderitem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.loopers.domain.quantity.Quantity;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("OrderItem 도메인 테스트")
+class OrderItemTest {
+
+  @DisplayName("OrderItem을 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("올바른 정보로 생성하면 성공한다")
+    @Test
+    void shouldCreate_whenValid() {
+      // given
+      Long productId = 100L;
+      String productName = "테스트 상품";
+      Quantity quantity = Quantity.of(3L);
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when
+      OrderItem orderItem = OrderItem.of(productId, productName, quantity, orderPrice);
+
+      // then
+      assertThat(orderItem).extracting("productId", "productName")
+          .containsExactly(productId, productName);
+      assertThat(orderItem).extracting("quantity", "orderPrice")
+          .containsExactly(quantity, orderPrice);
+    }
+  }
+
+  @DisplayName("productId 검증")
+  @Nested
+  class ValidateProductId {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      // given
+      String productName = "테스트 상품";
+      Quantity quantity = Quantity.of(1L);
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(null, productName, quantity, orderPrice))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ITEM_PRODUCT_EMPTY);
+    }
+  }
+
+  @DisplayName("productName 검증")
+  @Nested
+  class ValidateProductName {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      // given
+      Long productId = 100L;
+      Quantity quantity = Quantity.of(1L);
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(productId, null, quantity, orderPrice))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품명은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ITEM_PRODUCT_NAME_EMPTY);
+    }
+
+    @DisplayName("빈 문자열이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenBlank() {
+      // given
+      Long productId = 100L;
+      Quantity quantity = Quantity.of(1L);
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(productId, "  ", quantity, orderPrice))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품명은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ITEM_PRODUCT_NAME_EMPTY);
+    }
+
+    @DisplayName("100자를 초과하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenTooLong() {
+      // given
+      Long productId = 100L;
+      String productName = "a".repeat(101);
+      Quantity quantity = Quantity.of(1L);
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(productId, productName, quantity, orderPrice))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품명은 100자 이내여야 합니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ITEM_PRODUCT_NAME_LENGTH);
+    }
+  }
+
+  @DisplayName("quantity 검증")
+  @Nested
+  class ValidateQuantity {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      // given
+      Long productId = 100L;
+      String productName = "테스트 상품";
+      OrderPrice orderPrice = OrderPrice.of(10000L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(productId, productName, null, orderPrice))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("수량은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_ITEM_QUANTITY_EMPTY);
+    }
+  }
+
+  @DisplayName("orderPrice 검증")
+  @Nested
+  class ValidateOrderPrice {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      // given
+      Long productId = 100L;
+      String productName = "테스트 상품";
+      Quantity quantity = Quantity.of(1L);
+
+      // when & then
+      assertThatThrownBy(() -> OrderItem.of(productId, productName, quantity, null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("주문 가격은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_PRICE_VALUE);
+    }
+  }
+
+  @DisplayName("비즈니스 로직")
+  @Nested
+  class BusinessLogic {
+
+    @DisplayName("주문 가격 값을 조회한다")
+    @Test
+    void shouldGetOrderPriceValue() {
+      // given
+      Long productId = 100L;
+      String productName = "테스트 상품";
+      Quantity quantity = Quantity.of(1L);
+      OrderPrice orderPrice = OrderPrice.of(20000L);
+      OrderItem orderItem = OrderItem.of(productId, productName, quantity, orderPrice);
+
+      // when
+      Long priceValue = orderItem.getOrderPriceValue();
+
+      // then
+      assertThat(priceValue).isEqualTo(20000L);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderPriceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderPriceTest.java
new file mode 100644
index 000000000..9628b13e9
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/orderitem/OrderPriceTest.java
@@ -0,0 +1,98 @@
+package com.loopers.domain.order.orderitem;
+
+import com.loopers.domain.order.orderitem.OrderPrice;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("OrderPrice Value Object 테스트")
+class OrderPriceTest {
+
+  @DisplayName("OrderPrice를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("올바른 값으로 생성하면 성공한다")
+    @Test
+    void shouldCreate_whenValid() {
+      // given
+      Long value = 10000L;
+
+      // when
+      OrderPrice orderPrice = OrderPrice.of(value);
+
+      // then
+      assertThat(orderPrice.getValue()).isEqualTo(value);
+    }
+
+    @DisplayName("0원으로 생성하면 성공한다")
+  @Test
+    void shouldCreate_whenZero() {
+      // given
+      Long value = 0L;
+
+      // when
+      OrderPrice orderPrice = OrderPrice.of(value);
+
+      // then
+      assertThat(orderPrice.getValue()).isZero();
+    }
+  }
+
+  @DisplayName("value 검증")
+  @Nested
+  class ValidateValue {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      // when & then
+      assertThatThrownBy(() -> OrderPrice.of(null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("주문 가격은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_ORDER_PRICE_VALUE);
+    }
+
+    @DisplayName("음수이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegative() {
+      // when & then
+      assertThatThrownBy(() -> OrderPrice.of(-1000L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("주문 가격은 음수일 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.NEGATIVE_ORDER_PRICE_VALUE);
+    }
+  }
+
+  @DisplayName("동등성 비교")
+  @Nested
+  class Equality {
+
+    @DisplayName("같은 값이면 동등하다")
+    @Test
+    void shouldBeEqual_whenSameValue() {
+      // given
+      OrderPrice orderPrice1 = OrderPrice.of(10000L);
+      OrderPrice orderPrice2 = OrderPrice.of(10000L);
+
+      // when & then
+      assertThat(orderPrice1).isEqualTo(orderPrice2);
+    }
+
+    @DisplayName("다른 값이면 동등하지 않다")
+    @Test
+    void shouldNotBeEqual_whenDifferentValue() {
+      // given
+      OrderPrice orderPrice1 = OrderPrice.of(10000L);
+      OrderPrice orderPrice2 = OrderPrice.of(20000L);
+
+      // when & then
+      assertThat(orderPrice1).isNotEqualTo(orderPrice2);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointAmountTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointAmountTest.java
index 5af5bc232..93204aa15 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointAmountTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointAmountTest.java
@@ -1,5 +1,6 @@
 package com.loopers.domain.point;
 
+import com.loopers.support.error.CoreException;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -26,7 +27,7 @@ void shouldCreate_whenValidAmount() {
       PointAmount result = PointAmount.of(validAmount);
 
       // then
-      assertThat(result.getAmount()).isEqualTo(validAmount);
+      assertThat(result.getValue()).isEqualTo(validAmount);
     }
 
     @Test
@@ -36,7 +37,7 @@ void shouldCreate_whenZeroAmount() {
       PointAmount result = PointAmount.zero();
 
       // then
-      assertThat(result.getAmount()).isZero();
+      assertThat(result.getValue()).isZero();
     }
 
     @Test
@@ -45,7 +46,7 @@ void shouldThrowException_whenNull() {
       assertThatThrownBy(() ->
           PointAmount.of(null)
       )
-          .isInstanceOf(IllegalArgumentException.class)
+          .isInstanceOf(CoreException.class)
           .hasMessage("포인트 금액은 비어있을 수 없습니다.");
     }
 
@@ -59,7 +60,7 @@ void shouldThrowException_whenNegative() {
       assertThatThrownBy(() ->
           PointAmount.of(negativeAmount)
       )
-          .isInstanceOf(IllegalArgumentException.class)
+          .isInstanceOf(CoreException.class)
           .hasMessage("포인트 금액은 음수일 수 없습니다.");
     }
   }
@@ -79,8 +80,8 @@ void shouldAdd_whenValidAmount() {
       PointAmount result = original.add(chargeAmount);
 
       // then
-      assertThat(result.getAmount()).isEqualTo(1500L);
-      assertThat(original.getAmount()).isEqualTo(1000L); // 불변성 확인
+      assertThat(result.getValue()).isEqualTo(1500L);
+      assertThat(original.getValue()).isEqualTo(1000L); // 불변성 확인
     }
 
     @ParameterizedTest
@@ -94,7 +95,7 @@ void shouldThrowException_whenZeroOrNegative(Long invalidAmount) {
       assertThatThrownBy(() ->
           original.add(invalidAmount)
       )
-          .isInstanceOf(IllegalArgumentException.class)
+          .isInstanceOf(CoreException.class)
           .hasMessage("충전 금액은 0보다 커야 합니다.");
     }
 
@@ -108,7 +109,7 @@ void shouldThrowException_whenNull() {
       assertThatThrownBy(() ->
           original.add(null)
       )
-          .isInstanceOf(IllegalArgumentException.class)
+          .isInstanceOf(CoreException.class)
           .hasMessage("충전 금액은 0보다 커야 합니다.");
     }
   }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java
index 23b9d99de..5e04cd434 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java
@@ -51,7 +51,7 @@ void tearDown() {
   class Get {
 
     @Test
-    @DisplayName("해당 ID의 회원이 존재할 경우, 보유 포인트가 반환된다")
+    @DisplayName("회원이 존재하면 해당 회원의 포인트가 반환된다")
     void returnsPoint_whenUserExists() {
       // given
       String userId = "testuser";
@@ -59,32 +59,29 @@ void returnsPoint_whenUserExists() {
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = Gender.MALE;
 
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
+      User user = User.of(userId, email, birth, gender, LocalDate.now(TEST_CLOCK));
       User savedUser = userRepository.save(user);
 
-      Point point = Point.zero(savedUser);
+      Point point = Point.zero(savedUser.getId());
       pointRepository.save(point);
 
       // when
-      Point result = pointService.findByUserId(userId);
+      Point result = pointService.findByUserId(savedUser.getId()).orElse(null);
 
       // then
       assertThat(result)
-          .extracting("user.userId", "amount.amount")
-          .containsExactly(userId, 0L);
+          .extracting("userId", "amount.value")
+          .containsExactly(savedUser.getId(), 0L);
     }
 
     @Test
-    @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
-    void returnsNull_whenUserDoesNotExist() {
+    @DisplayName("회원이 존재하지 않으면 빈 Optional이 반환된다")
+    void returnsEmpty_whenUserDoesNotExist() {
       // given
-      String nonExistentUserId = "nonexistent";
+      Long nonExistentUserId = 999999L;
 
-      // when
-      Point result = pointService.findByUserId(nonExistentUserId);
-
-      // then
-      assertThat(result).isNull();
+      // when & then
+      assertThat(pointService.findByUserId(nonExistentUserId)).isEmpty();
     }
   }
 
@@ -93,36 +90,36 @@ void returnsNull_whenUserDoesNotExist() {
   class Charge {
 
     @Test
-    @DisplayName("존재하는 유저 ID로 충전하면 성공한다")
+    @DisplayName("존재하는 회원 ID로 충전하면 포인트가 증가한다")
     void succeeds_whenUserExists() {
       // given
-      String userId = "testuser";
+      String loginId = "testuser";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = Gender.MALE;
 
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
+      User user = User.of(loginId, email, birth, gender, LocalDate.now(TEST_CLOCK));
       User savedUser = userRepository.save(user);
 
-      Point point = Point.of(savedUser, 1000L);
+      Point point = Point.of(savedUser.getId(), 1000L);
       pointRepository.save(point);
 
       Long chargeAmount = 500L;
 
       // when
-      Point result = pointService.charge(userId, chargeAmount);
+      Point result = pointService.charge(savedUser.getId(), chargeAmount);
 
       // then
       assertThat(result)
-          .extracting("amount.amount")
+          .extracting("amount.value")
           .isEqualTo(1500L);
     }
 
     @Test
-    @DisplayName("존재하지 않는 유저 ID로 충전하면 실패한다")
+    @DisplayName("존재하지 않는 회원 ID로 충전하면 예외가 발생한다")
     void fails_whenUserDoesNotExist() {
       // given
-      String nonExistentUserId = "nonexistent";
+      Long nonExistentUserId = 999999L;
       Long chargeAmount = 500L;
 
       // when & then
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java
index 54a5c99b0..aeb7bf0b1 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java
@@ -1,84 +1,70 @@
 package com.loopers.domain.point;
 
-import com.loopers.domain.user.Gender;
-import com.loopers.domain.user.User;
+import com.loopers.support.error.CoreException;
 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.ValueSource;
 
-import java.time.Clock;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.ZoneId;
-
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-@DisplayName("Point 엔티티")
+@DisplayName("Point 도메인 테스트")
 class PointTest {
 
-  private static final Clock TEST_CLOCK = Clock.fixed(
-      Instant.parse("2025-10-30T00:00:00Z"),
-      ZoneId.systemDefault()
-  );
-
   @Nested
-  @DisplayName("생성 시")
-  class Constructor {
+  @DisplayName("Point를 생성할 때")
+  class Create {
 
     @Test
-    @DisplayName("User가 null이면 예외가 발생한다")
-    void throwsException_whenUserIsNull() {
+    @DisplayName("userId가 null이면 예외가 발생한다")
+    void shouldThrowException_whenUserIdIsNull() {
       assertThatThrownBy(() -> Point.zero(null))
-          .isInstanceOf(IllegalArgumentException.class)
+          .isInstanceOf(CoreException.class)
           .hasMessage("사용자는 비어있을 수 없습니다.");
     }
 
     @Test
-    @DisplayName("Point.zero(user)로 생성하면 balance가 0으로 초기화된다")
-    void initializesBalanceToZero_whenValidUserIsProvided() {
-      // given
-      String userId = "testuser";
-      String email = "test@example.com";
-      LocalDate birth = LocalDate.of(1990, 1, 1);
-      Gender gender = Gender.MALE;
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
-
-      // when
-      Point point = Point.zero(user);
-
-      // then
+    @DisplayName("Point.zero()로 생성하면 amount가 0으로 초기화된다")
+    void shouldCreate_whenZero() {
+      Long userId = 1L;
+
+      Point point = Point.zero(userId);
+
       assertThat(point)
-          .extracting("user.userId", "amount.amount")
+          .extracting("userId", "amount.value")
           .containsExactly(userId, 0L);
     }
+
+    @Test
+    @DisplayName("올바른 userId와 amount로 생성하면 성공한다")
+    void shouldCreate_whenValid() {
+      Long userId = 1L;
+      Long amount = 1000L;
+
+      Point point = Point.of(userId, amount);
+
+      assertThat(point)
+          .extracting("userId", "amount.value")
+          .containsExactly(userId, amount);
+    }
   }
 
   @Nested
-  @DisplayName("포인트 충전 시")
+  @DisplayName("포인트 충전")
   class Charge {
 
     @Test
     @DisplayName("양수 금액으로 충전하면 성공한다")
     void shouldCharge_whenValidAmount() {
-      // given
-      String userId = "testuser";
-      String email = "test@example.com";
-      LocalDate birth = LocalDate.of(1990, 1, 1);
-      Gender gender = Gender.MALE;
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
-
-      Point point = Point.of(user, 1000L);
+      Point point = Point.of(1L, 1000L);
       Long chargeAmount = 500L;
 
-      // when
       point.charge(chargeAmount);
 
-      // then
       assertThat(point)
-          .extracting("amount.amount")
+          .extracting("amount.value")
           .isEqualTo(1500L);
     }
 
@@ -86,38 +72,20 @@ void shouldCharge_whenValidAmount() {
     @DisplayName("0 이하의 금액으로 충전하면 예외가 발생한다")
     @ValueSource(longs = {0L, -1L, -100L, -1000L})
     void shouldThrowException_whenZeroOrNegative(Long invalidAmount) {
-      // given
-      String userId = "testuser";
-      String email = "test@example.com";
-      LocalDate birth = LocalDate.of(1990, 1, 1);
-      Gender gender = Gender.MALE;
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
-      Point point = Point.of(user, 1000L);
-
-      // when & then
-      assertThatThrownBy(() ->
-          point.charge(invalidAmount)
-      )
-          .isInstanceOf(IllegalArgumentException.class)
+      Point point = Point.of(1L, 1000L);
+
+      assertThatThrownBy(() -> point.charge(invalidAmount))
+          .isInstanceOf(CoreException.class)
           .hasMessage("충전 금액은 0보다 커야 합니다.");
     }
 
     @Test
     @DisplayName("null로 충전하면 예외가 발생한다")
     void shouldThrowException_whenNull() {
-      // given
-      String userId = "testuser";
-      String email = "test@example.com";
-      LocalDate birth = LocalDate.of(1990, 1, 1);
-      Gender gender = Gender.MALE;
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
-      Point point = Point.of(user, 1000L);
-
-      // when & then
-      assertThatThrownBy(() ->
-          point.charge(null)
-      )
-          .isInstanceOf(IllegalArgumentException.class)
+      Point point = Point.of(1L, 1000L);
+
+      assertThatThrownBy(() -> point.charge(null))
+          .isInstanceOf(CoreException.class)
           .hasMessage("충전 금액은 0보다 커야 합니다.");
     }
   }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java
new file mode 100644
index 000000000..765f7a86a
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java
@@ -0,0 +1,71 @@
+package com.loopers.domain.product;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.times;
+
+@ExtendWith(MockitoExtension.class)
+@DisplayName("ProductService 단위 테스트")
+class ProductServiceTest {
+
+  @Mock
+  private ProductRepository productRepository;
+
+  @InjectMocks
+  private ProductService productService;
+
+  @Nested
+  @DisplayName("상품 목록 조회")
+  class FindProducts {
+
+    @Test
+    @DisplayName("brandId가 null이면 전체 상품 목록을 조회한다")
+    void findProducts_withNullBrandId() {
+      // given
+      Pageable pageable = PageRequest.of(0, 10);
+      Page<Product> expectedPage = new PageImpl<>(List.of());
+      given(productRepository.findAll(pageable)).willReturn(expectedPage);
+
+      // when
+      Page<Product> result = productService.findProducts(null, pageable);
+
+      // then
+      assertThat(result).isEqualTo(expectedPage);
+      then(productRepository).should(times(1)).findAll(pageable);
+      then(productRepository).should(times(0)).findByBrandId(null, pageable);
+    }
+
+    @Test
+    @DisplayName("brandId가 있으면 해당 브랜드의 상품 목록을 조회한다")
+    void findProducts_withBrandId() {
+      // given
+      Long brandId = 1L;
+      Pageable pageable = PageRequest.of(0, 10);
+      Page<Product> expectedPage = new PageImpl<>(List.of());
+      given(productRepository.findByBrandId(brandId, pageable)).willReturn(expectedPage);
+
+      // when
+      Page<Product> result = productService.findProducts(brandId, pageable);
+
+      // then
+      assertThat(result).isEqualTo(expectedPage);
+      then(productRepository).should(times(1)).findByBrandId(brandId, pageable);
+      then(productRepository).should(times(0)).findAll(pageable);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java
new file mode 100644
index 000000000..429de12ca
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java
@@ -0,0 +1,211 @@
+package com.loopers.domain.product;
+
+import com.loopers.domain.money.Money;
+import com.loopers.domain.stock.Stock;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.NullAndEmptySource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Product 도메인 테스트")
+class ProductTest {
+
+  @DisplayName("Product를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("올바른 정보로 생성하면 성공한다")
+    @Test
+    void shouldCreate_whenValid() {
+      String name = "나이키 에어맥스";
+      Money price = Money.of(150000L);
+      String description = "편안한 운동화";
+      Stock stock = Stock.of(100L);
+      Long brandId = 1L;
+
+      Product product = Product.of(name, price, description, stock, brandId);
+
+      assertThat(product).extracting("name", "description", "brandId")
+          .containsExactly(name, description, brandId);
+      assertThat(product).extracting("price").isEqualTo(price);
+      assertThat(product).extracting("stock").isEqualTo(stock);
+      assertThat(product).extracting("likeCount").isEqualTo(0L);
+    }
+  }
+
+  @DisplayName("name 검증")
+  @Nested
+  class ValidateName {
+
+    @DisplayName("100자 이내면 정상 생성된다")
+    @Test
+    void shouldCreate_whenValidLength() {
+      String maxLengthName = "a".repeat(100);
+      Money price = Money.of(10000L);
+      Stock stock = Stock.of(10L);
+
+      Product product = Product.of(maxLengthName, price, "설명", stock, 1L);
+
+      assertThat(product).extracting("name").isEqualTo(maxLengthName);
+    }
+
+    @DisplayName("null 또는 빈 문자열이면 예외가 발생한다")
+    @ParameterizedTest
+    @NullAndEmptySource
+    void shouldThrowException_whenNullOrEmpty(String invalidName) {
+      Money price = Money.of(10000L);
+      Stock stock = Stock.of(10L);
+
+      assertThatThrownBy(() -> Product.of(invalidName, price, "설명", stock, 1L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품명은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_NAME_EMPTY);
+    }
+
+    @DisplayName("100자를 초과하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenTooLong() {
+      String tooLongName = "a".repeat(101);
+      Money price = Money.of(10000L);
+      Stock stock = Stock.of(10L);
+
+      assertThatThrownBy(() -> Product.of(tooLongName, price, "설명", stock, 1L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품명은 100자 이내여야 합니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_NAME_LENGTH);
+    }
+  }
+
+  @DisplayName("price 검증")
+  @Nested
+  class ValidatePrice {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Stock stock = Stock.of(10L);
+
+      assertThatThrownBy(() -> Product.of("상품명", null, "설명", stock, 1L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("가격은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_PRICE_EMPTY);
+    }
+  }
+
+  @DisplayName("stock 검증")
+  @Nested
+  class ValidateStock {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Money price = Money.of(10000L);
+
+      assertThatThrownBy(() -> Product.of("상품명", price, "설명", null, 1L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("재고는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_STOCK_EMPTY);
+    }
+  }
+
+  @DisplayName("brandId 검증")
+  @Nested
+  class ValidateBrandId {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Money price = Money.of(10000L);
+      Stock stock = Stock.of(10L);
+
+      assertThatThrownBy(() -> Product.of("상품명", price, "설명", stock, null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("브랜드는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_BRAND_EMPTY);
+    }
+  }
+
+  @DisplayName("비즈니스 로직")
+  @Nested
+  class BusinessLogic {
+
+    @DisplayName("재고가 충분하면 차감에 성공한다")
+    @Test
+    void shouldDecreaseStock_whenSufficient() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(100L), 1L);
+
+      product.decreaseStock(30L);
+
+      assertThat(product).extracting("stock").isEqualTo(Stock.of(70L));
+    }
+
+    @DisplayName("재고가 부족하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenInsufficientStock() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(10L), 1L);
+
+      assertThatThrownBy(() -> product.decreaseStock(20L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("재고가 부족합니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INSUFFICIENT_STOCK);
+    }
+
+    @DisplayName("재고가 있으면 구매 가능하다")
+    @Test
+    void shouldReturnTrue_whenAvailable() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(1L), 1L);
+
+      boolean available = product.isAvailable();
+
+      assertThat(available).isTrue();
+    }
+
+    @DisplayName("재고가 0이면 구매 불가능하다")
+    @Test
+    void shouldReturnFalse_whenNotAvailable() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.zero(), 1L);
+
+      boolean available = product.isAvailable();
+
+      assertThat(available).isFalse();
+    }
+
+    @DisplayName("좋아요 수를 증가시킨다")
+    @Test
+    void shouldIncreaseLikeCount() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(10L), 1L);
+
+      product.increaseLikeCount();
+
+      assertThat(product).extracting("likeCount").isEqualTo(1L);
+    }
+
+    @DisplayName("좋아요 수를 감소시킨다")
+    @Test
+    void shouldDecreaseLikeCount() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(10L), 1L);
+      product.increaseLikeCount();
+      product.increaseLikeCount();
+
+      product.decreaseLikeCount();
+
+      assertThat(product).extracting("likeCount").isEqualTo(1L);
+    }
+
+    @DisplayName("좋아요 수가 0일 때 감소시켜도 음수가 되지 않는다")
+    @Test
+    void shouldNotDecreaseLikeCount_whenZero() {
+      Product product = Product.of("상품명", Money.of(10000L), "설명", Stock.of(10L), 1L);
+
+      product.decreaseLikeCount();
+
+      assertThat(product).extracting("likeCount").isEqualTo(0L);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductsTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductsTest.java
new file mode 100644
index 000000000..28568d400
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductsTest.java
@@ -0,0 +1,55 @@
+package com.loopers.domain.product;
+
+import com.loopers.domain.money.Money;
+import com.loopers.domain.stock.Stock;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("Products 일급컬렉션 테스트")
+class ProductsTest {
+
+  @Test
+  @DisplayName("Product 리스트로 일급컬렉션을 생성한다")
+  void from() {
+    Product product1 = Product.of("상품1", Money.of(1000L), "설명1", Stock.of(10L), 1L);
+    Product product2 = Product.of("상품2", Money.of(2000L), "설명2", Stock.of(20L), 2L);
+    List<Product> productList = List.of(product1, product2);
+
+    Products products = Products.from(productList);
+
+    assertThat(products.toList()).hasSize(2);
+  }
+
+  @Test
+  @DisplayName("브랜드 ID 목록을 중복 제거하여 추출한다")
+  void getBrandIds() {
+    Product product1 = Product.of("상품1", Money.of(1000L), "설명1", Stock.of(10L), 1L);
+    Product product2 = Product.of("상품2", Money.of(2000L), "설명2", Stock.of(20L), 1L);
+    Product product3 = Product.of("상품3", Money.of(3000L), "설명3", Stock.of(30L), 2L);
+    Products products = Products.from(List.of(product1, product2, product3));
+
+    List<Long> brandIds = products.getBrandIds();
+
+    assertThat(brandIds)
+        .hasSize(2)
+        .containsExactlyInAnyOrder(1L, 2L);
+  }
+
+  @Test
+  @DisplayName("내부 리스트를 불변 복사본으로 반환한다")
+  void toList() {
+    Product product = Product.of("상품1", Money.of(1000L), "설명1", Stock.of(10L), 1L);
+    Products products = Products.from(List.of(product));
+
+    List<Product> list1 = products.toList();
+    List<Product> list2 = products.toList();
+
+    assertThat(list1)
+        .isNotSameAs(list2)
+        .isEqualTo(list2);
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikeTest.java
new file mode 100644
index 000000000..c86d0a653
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikeTest.java
@@ -0,0 +1,114 @@
+package com.loopers.domain.productlike;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("ProductLike 도메인 테스트")
+class ProductLikeTest {
+
+  private static final LocalDateTime LIKED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @DisplayName("ProductLike를 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("사용자 ID, 상품 ID, 좋아요 시각을 모두 제공하면 ProductLike가 생성된다")
+    @Test
+    void shouldCreate_whenValid() {
+      Long userId = 1L;
+      Long productId = 100L;
+
+      ProductLike productLike = ProductLike.of(userId, productId, LIKED_AT_2025_10_30);
+
+      assertThat(productLike).extracting("userId", "productId", "likedAt")
+          .containsExactly(userId, productId, LIKED_AT_2025_10_30);
+    }
+  }
+
+  @DisplayName("userId 검증")
+  @Nested
+  class ValidateUserId {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long productId = 100L;
+
+      assertThatThrownBy(() -> ProductLike.of(null, productId, LIKED_AT_2025_10_30))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("사용자는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_LIKE_USER_EMPTY);
+    }
+  }
+
+  @DisplayName("productId 검증")
+  @Nested
+  class ValidateProductId {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long userId = 1L;
+
+      assertThatThrownBy(() -> ProductLike.of(userId, null, LIKED_AT_2025_10_30))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("상품은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_LIKE_PRODUCT_EMPTY);
+    }
+  }
+
+  @DisplayName("likedAt 검증")
+  @Nested
+  class ValidateLikedAt {
+
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      Long userId = 1L;
+      Long productId = 100L;
+
+      assertThatThrownBy(() -> ProductLike.of(userId, productId, null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("좋아요 일시는 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_PRODUCT_LIKE_LIKED_AT_EMPTY);
+    }
+  }
+
+  @DisplayName("비즈니스 로직")
+  @Nested
+  class BusinessLogic {
+
+    @DisplayName("해당 사용자의 좋아요이면 true를 반환한다")
+    @Test
+    void shouldReturnTrue_whenLikedByUser() {
+      Long userId = 1L;
+      Long productId = 100L;
+      ProductLike productLike = ProductLike.of(userId, productId, LIKED_AT_2025_10_30);
+
+      boolean result = productLike.isLikedBy(userId);
+
+      assertThat(result).isTrue();
+    }
+
+    @DisplayName("다른 사용자의 좋아요이면 false를 반환한다")
+    @Test
+    void shouldReturnFalse_whenNotLikedByUser() {
+      Long userId = 1L;
+      Long otherUserId = 2L;
+      Long productId = 100L;
+      ProductLike productLike = ProductLike.of(userId, productId, LIKED_AT_2025_10_30);
+
+      boolean result = productLike.isLikedBy(otherUserId);
+
+      assertThat(result).isFalse();
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikesTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikesTest.java
new file mode 100644
index 000000000..d26e59003
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/productlike/ProductLikesTest.java
@@ -0,0 +1,193 @@
+package com.loopers.domain.productlike;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("ProductLikes 일급 컬렉션 테스트")
+class ProductLikesTest {
+
+  private static final LocalDateTime LIKED_AT_2025_10_30 = LocalDateTime.of(2025, 10, 30, 0, 0, 0);
+
+  @Nested
+  @DisplayName("생성")
+  class Create {
+
+    @Test
+    @DisplayName("빈 리스트로 생성 성공")
+    void from_emptyList() {
+      ProductLikes productLikes = ProductLikes.from(List.of());
+
+      assertThat(productLikes).isNotNull();
+    }
+
+    @Test
+    @DisplayName("여러 개의 ProductLike로 생성 성공")
+    void from_multipleProductLikes() {
+      List<ProductLike> likes = List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 200L, LIKED_AT_2025_10_30)
+      );
+
+      ProductLikes productLikes = ProductLikes.from(likes);
+
+      assertThat(productLikes).isNotNull();
+    }
+
+    @Test
+    @DisplayName("null 리스트로 생성 시 IllegalArgumentException 발생")
+    void from_nullList() {
+      assertThatThrownBy(() -> ProductLikes.from(null))
+          .isInstanceOf(IllegalArgumentException.class)
+          .hasMessage("productLikes cannot be null");
+    }
+
+    @Test
+    @DisplayName("empty() 정적 팩토리 메서드로 빈 컬렉션 생성")
+    void empty() {
+      ProductLikes productLikes = ProductLikes.empty();
+
+      assertThat(productLikes).isNotNull();
+    }
+  }
+
+  @Nested
+  @DisplayName("toLikedByProductId 변환")
+  class ToLikedByProductId {
+
+    @Test
+    @DisplayName("빈 리스트를 빈 Map으로 변환")
+    void toLikedByProductId_empty() {
+      ProductLikes productLikes = ProductLikes.empty();
+
+      Map<Long, Boolean> result = productLikes.toLikedByProductId();
+
+      assertThat(result).isEmpty();
+    }
+
+    @Test
+    @DisplayName("ProductLike 리스트를 좋아요 Map으로 변환 (모두 true)")
+    void toLikedByProductId_success() {
+      List<ProductLike> likes = List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 200L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 300L, LIKED_AT_2025_10_30)
+      );
+      ProductLikes productLikes = ProductLikes.from(likes);
+
+      Map<Long, Boolean> result = productLikes.toLikedByProductId();
+
+      assertThat(result)
+          .hasSize(3)
+          .containsEntry(100L, true)
+          .containsEntry(200L, true)
+          .containsEntry(300L, true);
+    }
+  }
+
+  @Nested
+  @DisplayName("toLikeStatusByProductId 변환")
+  class ToLikeStatusByProductId {
+
+    @Test
+    @DisplayName("null productIds로 호출 시 빈 Map 반환")
+    void toLikeStatusByProductId_nullProductIds() {
+      ProductLikes productLikes = ProductLikes.empty();
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(null);
+
+      assertThat(result).isEmpty();
+    }
+
+    @Test
+    @DisplayName("빈 productIds로 호출 시 빈 Map 반환")
+    void toLikeStatusByProductId_emptyProductIds() {
+      ProductLikes productLikes = ProductLikes.from(List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30)
+      ));
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(List.of());
+
+      assertThat(result).isEmpty();
+    }
+
+    @Test
+    @DisplayName("좋아요한 상품과 안 한 상품 혼합 상태로 변환")
+    void toLikeStatusByProductId_mixed() {
+      List<ProductLike> likes = List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 300L, LIKED_AT_2025_10_30)
+      );
+      ProductLikes productLikes = ProductLikes.from(likes);
+      List<Long> allProductIds = List.of(100L, 200L, 300L, 400L);
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(allProductIds);
+
+      assertThat(result)
+          .hasSize(4)
+          .containsEntry(100L, true)
+          .containsEntry(200L, false)
+          .containsEntry(300L, true)
+          .containsEntry(400L, false);
+    }
+
+    @Test
+    @DisplayName("모든 상품을 좋아요하지 않은 경우")
+    void toLikeStatusByProductId_allNotLiked() {
+      ProductLikes productLikes = ProductLikes.empty();
+      List<Long> allProductIds = List.of(100L, 200L, 300L);
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(allProductIds);
+
+      assertThat(result)
+          .hasSize(3)
+          .containsEntry(100L, false)
+          .containsEntry(200L, false)
+          .containsEntry(300L, false);
+    }
+
+    @Test
+    @DisplayName("모든 상품을 좋아요한 경우")
+    void toLikeStatusByProductId_allLiked() {
+      List<ProductLike> likes = List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 200L, LIKED_AT_2025_10_30),
+          ProductLike.of(1L, 300L, LIKED_AT_2025_10_30)
+      );
+      ProductLikes productLikes = ProductLikes.from(likes);
+      List<Long> allProductIds = List.of(100L, 200L, 300L);
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(allProductIds);
+
+      assertThat(result)
+          .hasSize(3)
+          .containsEntry(100L, true)
+          .containsEntry(200L, true)
+          .containsEntry(300L, true);
+    }
+
+    @Test
+    @DisplayName("좋아요 목록에 없는 상품 ID 조회 시 false 반환")
+    void toLikeStatusByProductId_productNotInList() {
+      List<ProductLike> likes = List.of(
+          ProductLike.of(1L, 100L, LIKED_AT_2025_10_30)
+      );
+      ProductLikes productLikes = ProductLikes.from(likes);
+      List<Long> allProductIds = List.of(100L, 999L);
+
+      Map<Long, Boolean> result = productLikes.toLikeStatusByProductId(allProductIds);
+
+      assertThat(result)
+          .hasSize(2)
+          .containsEntry(100L, true)
+          .containsEntry(999L, false);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/quantity/QuantityTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/quantity/QuantityTest.java
new file mode 100644
index 000000000..342d12a3b
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/quantity/QuantityTest.java
@@ -0,0 +1,70 @@
+package com.loopers.domain.quantity;
+
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+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.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Quantity VO 테스트")
+class QuantityTest {
+
+  @DisplayName("Quantity를 생성할 때")
+  @Nested
+  class Create {
+    @DisplayName("null이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNull() {
+      assertThatThrownBy(() -> Quantity.of(null))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("수량은 비어있을 수 없습니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_QUANTITY_VALUE);
+    }
+
+    @DisplayName("음수이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegative() {
+      assertThatThrownBy(() -> Quantity.of(-1L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("수량은 0 이상이어야 합니다.")
+          .extracting("errorType").isEqualTo(ErrorType.INVALID_QUANTITY_RANGE);
+    }
+
+    @DisplayName("0 이상의 값으로 생성하면 성공한다")
+    @ParameterizedTest
+    @ValueSource(longs = {0, 1, 10, 100, 999})
+    void shouldCreate_whenValidValue(Long value) {
+      Quantity quantity = Quantity.of(value);
+
+      assertThat(quantity.getValue()).isEqualTo(value);
+    }
+  }
+
+  @DisplayName("두 객체의 동등성을 비교 할 때")
+  @Nested
+  class Equality {
+
+    @DisplayName("같은 값이면 동등하다")
+    @Test
+    void shouldBeEqual_whenSameValue() {
+      Quantity quantity1 = Quantity.of(5L);
+      Quantity quantity2 = Quantity.of(5L);
+
+      assertThat(quantity1).isEqualTo(quantity2);
+    }
+
+    @DisplayName("다른 값이면 동등하지 않다")
+    @Test
+    void shouldNotBeEqual_whenDifferentValue() {
+      Quantity quantity1 = Quantity.of(5L);
+      Quantity quantity2 = Quantity.of(10L);
+
+      assertThat(quantity1).isNotEqualTo(quantity2);
+    }
+  }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java
new file mode 100644
index 000000000..338b30aa1
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java
@@ -0,0 +1,140 @@
+package com.loopers.domain.stock;
+
+import com.loopers.support.error.CoreException;
+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.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("Stock VO 테스트")
+class StockTest {
+
+  @DisplayName("Stock을 생성할 때")
+  @Nested
+  class Create {
+
+    @DisplayName("음수 재고이면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegative() {
+      assertThatThrownBy(() -> Stock.of(-10L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("재고 수량은 음수일 수 없습니다.");
+    }
+
+    @DisplayName("0 이상의 재고로 생성할 수 있다")
+    @ParameterizedTest
+    @ValueSource(longs = {0L, 1000L, 10000L})
+    void shouldCreate_whenValidValue(Long value) {
+      Stock zero = Stock.of(value);
+      assertThat(zero).extracting("value").isEqualTo(value);
+    }
+
+
+  }
+
+  @DisplayName("재고를 증가할 때")
+  @Nested
+  class Increase {
+
+    @DisplayName("음수 수량으로 재고를 증가시키면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegativeAmount() {
+      Stock stock = Stock.of(100L);
+
+      assertThatThrownBy(() -> stock.increase(-10L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("증가 수량은 0 이상이어야 합니다.");
+    }
+
+    @DisplayName("0이상의 수량으로 재고를 증가할 수 있다")
+    @ParameterizedTest
+    @ValueSource(longs = {0L, 1000L, 10000L})
+    void shouldIncrease_whenPositiveAmount(Long value) {
+      Stock stock = Stock.of(0L);
+
+      Stock result = stock.increase(value);
+
+      assertThat(result).extracting("value").isEqualTo(value);
+    }
+
+  }
+
+  @DisplayName("재고를 감소할 때")
+  @Nested
+  class Decrease {
+
+    @DisplayName("음수 수량으로 재고를 감소시키면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenNegativeAmount() {
+      Stock stock = Stock.of(100L);
+
+      assertThatThrownBy(() -> stock.decrease(-10L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("감소 수량은 0 이상이어야 합니다.");
+    }
+
+    @DisplayName("재고가 부족하면 예외가 발생한다")
+    @Test
+    void shouldThrowException_whenInsufficientStock() {
+      Stock stock = Stock.of(50L);
+
+      assertThatThrownBy(() -> stock.decrease(51L))
+          .isInstanceOf(CoreException.class)
+          .hasMessage("재고가 부족합니다.");
+    }
+
+    @DisplayName("0개 이상의 재고를 감소할 수 있다")
+    @ParameterizedTest
+    @ValueSource(longs = {0L, 1L, 30L, 49L, 50L})
+    void shouldDecrease_whenSufficientStock(Long decreaseAmount) {
+      Stock stock = Stock.of(50L);
+
+      Stock result = stock.decrease(decreaseAmount);
+
+      assertThat(result).extracting("value").isEqualTo(50L - decreaseAmount);
+    }
+
+  }
+
+  @DisplayName("재고 가용성 확인할 때")
+  @Nested
+  class Availability {
+
+    @DisplayName("재고가 0보다 크면 available하다")
+    @Test
+    void shouldBeAvailable_whenPositiveStock() {
+      Stock stock = Stock.of(1L);
+
+      assertThat(stock.isAvailable()).isTrue();
+    }
+
+    @DisplayName("재고가 0이면 available하지 않다")
+    @Test
+    void shouldNotBeAvailable_whenZeroStock() {
+      Stock stock = Stock.zero();
+
+      assertThat(stock.isAvailable()).isFalse();
+    }
+
+  }
+
+  @DisplayName("두 객체의 동등성 비교할 때")
+  @Nested
+  class Equality {
+
+    @DisplayName("같은 수량의 Stock은 동등하다")
+    @Test
+    void shouldBeEqual_whenSameValue() {
+      Stock stock1 = Stock.of(100L);
+      Stock stock2 = Stock.of(100L);
+
+      assertThat(stock1).isEqualTo(stock2);
+    }
+
+  }
+
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java
index 6ff3b3568..404b4bc13 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java
@@ -12,6 +12,7 @@
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
+import java.time.Clock;
 import java.time.LocalDate;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -34,7 +35,7 @@ class UserServiceIntegrationTest {
 
   @BeforeEach
   void setUp() {
-    userService = new UserService(userRepository);
+    userService = new UserService(userRepository, Clock.systemDefaultZone());
   }
 
   @AfterEach
@@ -47,23 +48,23 @@ void tearDown() {
   class RegisterUser {
 
     @Test
-    @DisplayName("User 저장이 수행된다")
+    @DisplayName("회원가입을 요청하면 User가 저장된다")
     void saveUser_whenRegisterUser() {
       // given
       UserRepository spyRepository = spy(userRepository);
-      UserService spyService = new UserService(spyRepository);
+      UserService spyService = new UserService(spyRepository, Clock.systemDefaultZone());
 
-      String userId = "testuser";
+      String loginId = "testuser";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = MALE;
 
       // when
-      spyService.registerUser(userId, email, birth, gender);
+      spyService.registerUser(loginId, email, birth, gender);
 
       // then
       verify(spyRepository, times(1)).save(argThat(user ->
-          user.getUserId().equals(userId) &&
+          user.getLoginId().equals(loginId) &&
               user.getEmail().equals(email) &&
               user.getBirth().equals(birth) &&
               user.getGender().equals(gender)
@@ -71,20 +72,20 @@ void saveUser_whenRegisterUser() {
     }
 
     @Test
-    @DisplayName("이미 가입된 ID로 회원가입 시도 시 실패한다")
-    void throwsException_whenDuplicateUserId() {
+    @DisplayName("이미 가입된 ID로 회원가입하면 예외가 발생한다")
+    void throwsException_whenDuplicateLoginId() {
       // given
-      String userId = "existuser";
+      String loginId = "existuser";
       String firstEmail = "first@example.com";
       String secondEmail = "second@example.com";
       LocalDate firstBirth = LocalDate.of(1990, 1, 1);
       LocalDate secondBirth = LocalDate.of(1995, 5, 5);
       Gender gender = MALE;
 
-      userService.registerUser(userId, firstEmail, firstBirth, gender);
+      userService.registerUser(loginId, firstEmail, firstBirth, gender);
 
       // when & then
-      assertThatThrownBy(() -> userService.registerUser(userId, secondEmail, secondBirth, gender))
+      assertThatThrownBy(() -> userService.registerUser(loginId, secondEmail, secondBirth, gender))
           .isInstanceOf(CoreException.class)
           .extracting("errorType")
           .isEqualTo(ErrorType.CONFLICT);
@@ -94,18 +95,18 @@ void throwsException_whenDuplicateUserId() {
     @DisplayName("올바른 정보로 회원가입하면 User가 반환된다")
     void returnsUser_whenValidInfoIsProvided() {
       // given
-      String userId = "newuser";
+      String loginId = "newuser";
       String email = "new@example.com";
       LocalDate birth = LocalDate.of(1995, 3, 15);
       Gender gender = FEMALE;
 
       // when
-      User result = userService.registerUser(userId, email, birth, gender);
+      User result = userService.registerUser(loginId, email, birth, gender);
 
       // then
       assertThat(result)
-          .extracting("userId", "email", "birth", "gender")
-          .containsExactly(userId, email, birth, gender);
+          .extracting("loginId", "email", "birth", "gender")
+          .containsExactly(loginId, email, birth, gender);
 
     }
   }
@@ -115,32 +116,32 @@ void returnsUser_whenValidInfoIsProvided() {
   class Get {
 
     @Test
-    @DisplayName("해당 ID의 회원이 존재할 경우, 회원 정보가 반환된다")
+    @DisplayName("회원이 존재하면 회원 정보가 반환된다")
     void returnsUser_whenUserExists() {
       // given
-      String userId = "testuser";
+      String loginId = "testuser";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = MALE;
-      userService.registerUser(userId, email, birth, gender);
+      userService.registerUser(loginId, email, birth, gender);
 
       // when
-      User result = userService.findById(userId);
+      User result = userService.findById(loginId);
 
       // then
       assertThat(result)
-          .extracting("userId", "email", "birth", "gender")
-          .containsExactly(userId, email, birth, gender);
+          .extracting("loginId", "email", "birth", "gender")
+          .containsExactly(loginId, email, birth, gender);
     }
 
     @Test
-    @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null이 반환된다")
+    @DisplayName("회원이 존재하지 않으면 null이 반환된다")
     void returnsNull_whenUserDoesNotExist() {
       // given
-      String nonExistentUserId = "nonexistent";
+      String nonExistentLoginId = "nonexistent";
 
       // when
-      User result = userService.findById(nonExistentUserId);
+      User result = userService.findById(nonExistentLoginId);
 
       // then
       assertThat(result).isNull();
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java
index 2cd6d0227..01d6703b1 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java
@@ -1,5 +1,6 @@
 package com.loopers.domain.user;
 
+import com.loopers.support.error.CoreException;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -31,45 +32,45 @@ class Create {
         @DisplayName("올바른 정보로 생성하면 성공한다")
         @Test
         void shouldCreate_whenValid() {
-            String userId = "user123";
+            String loginId = "user123";
             String email = "user@example.com";
             LocalDate birth = LocalDate.of(1990, 1, 15);
             Gender gender = MALE;
 
-            User result = User.of(userId, email, birth, gender, TEST_CLOCK);
+            User result = User.of(loginId, email, birth, gender, LocalDate.now(TEST_CLOCK));
 
             assertThat(result)
-                .extracting("userId", "email", "birth", "gender")
-                .containsExactly(userId, email, birth, gender);
+                .extracting("loginId", "email", "birth", "gender")
+                .containsExactly(loginId, email, birth, gender);
         }
     }
 
-    @DisplayName("userId 검증")
+    @DisplayName("loginId 검증")
     @Nested
-    class ValidateUserId {
+    class ValidateLoginId {
 
         @DisplayName("영문+숫자 10자 이내면 정상 생성된다")
         @Test
-        void shouldCreate_whenValidUserId() {
-            String validUserId = "user123";
+        void shouldCreate_whenValidLoginId() {
+            String validLoginId = "user123";
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
-            User result = User.of(validUserId, "test@example.com", validBirth, MALE, TEST_CLOCK);
+            User result = User.of(validLoginId, "test@example.com", validBirth, MALE, LocalDate.now(TEST_CLOCK));
 
-            assertThat(result.getUserId()).isEqualTo(validUserId);
+            assertThat(result.getLoginId()).isEqualTo(validLoginId);
         }
 
         @DisplayName("null 또는 빈 문자열이면 예외가 발생한다")
         @ParameterizedTest
         @NullAndEmptySource
-        void shouldThrowException_whenNullOrEmpty(String invalidUserId) {
+        void shouldThrowException_whenNullOrEmpty(String invalidLoginId) {
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
             assertThatThrownBy(() ->
-                User.of(invalidUserId, "test@example.com", validBirth, MALE, TEST_CLOCK)
+                User.of(invalidLoginId, "test@example.com", validBirth, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class)
-                .hasMessage("사용자 ID는 비어있을 수 없습니다.");
+                .isInstanceOf(CoreException.class)
+                .hasMessage("로그인 ID는 비어있을 수 없습니다.");
         }
 
         @DisplayName("영문+숫자 10자 이내가 아니면 예외가 발생한다")
@@ -77,19 +78,19 @@ void shouldThrowException_whenNullOrEmpty(String invalidUserId) {
         @ValueSource(strings = {
             "abc12345678",    // 11자
             "user@123",       // 특수문자
-            "user-name",      // 하이픈
-            "user.name",      // 점
+            "user-productName",      // 하이픈
+            "user.productName",      // 점
             "user한글",        // 한글
             "홍길동",          // 한글만
-            "user name"       // 공백
+            "user productName"       // 공백
         })
-        void shouldThrowException_whenInvalidFormat(String invalidUserId) {
+        void shouldThrowException_whenInvalidFormat(String invalidLoginId) {
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
             assertThatThrownBy(() ->
-                User.of(invalidUserId, "test@example.com", validBirth, MALE, TEST_CLOCK)
+                User.of(invalidLoginId, "test@example.com", validBirth, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class);
+                .isInstanceOf(CoreException.class);
         }
     }
 
@@ -103,7 +104,7 @@ void shouldCreate_whenValidEmail() {
             String validEmail = "user@example.com";
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
-            User result = User.of("user123", validEmail, validBirth, MALE, TEST_CLOCK);
+            User result = User.of("user123", validEmail, validBirth, MALE, LocalDate.now(TEST_CLOCK));
 
             assertThat(result.getEmail()).isEqualTo(validEmail);
         }
@@ -115,9 +116,9 @@ void shouldThrowException_whenNullOrEmpty(String invalidEmail) {
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
             assertThatThrownBy(() ->
-                User.of("user123", invalidEmail, validBirth, MALE, TEST_CLOCK)
+                User.of("user123", invalidEmail, validBirth, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class)
+                .isInstanceOf(CoreException.class)
                 .hasMessage("이메일은 비어있을 수 없습니다.");
         }
 
@@ -136,9 +137,9 @@ void shouldThrowException_whenInvalidFormat(String invalidEmail) {
             LocalDate validBirth = LocalDate.of(1990, 1, 1);
 
             assertThatThrownBy(() ->
-                User.of("user123", invalidEmail, validBirth, MALE, TEST_CLOCK)
+                User.of("user123", invalidEmail, validBirth, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class);
+                .isInstanceOf(CoreException.class);
         }
     }
 
@@ -151,7 +152,7 @@ class ValidateBirth {
         void shouldCreate_whenValidBirth() {
             LocalDate validBirth = LocalDate.of(1990, 1, 15);
 
-            User result = User.of("user123", "test@example.com", validBirth, MALE, TEST_CLOCK);
+            User result = User.of("user123", "test@example.com", validBirth, MALE, LocalDate.now(TEST_CLOCK));
 
             assertThat(result.getBirth()).isEqualTo(validBirth);
         }
@@ -160,9 +161,9 @@ void shouldCreate_whenValidBirth() {
         @Test
         void shouldThrowException_whenNull() {
             assertThatThrownBy(() ->
-                User.of("user123", "test@example.com", null, MALE, TEST_CLOCK)
+                User.of("user123", "test@example.com", null, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class)
+                .isInstanceOf(CoreException.class)
                 .hasMessage("생년월일은 비어있을 수 없습니다.");
         }
 
@@ -172,9 +173,9 @@ void shouldThrowException_whenFutureDate() {
             LocalDate futureDate = LocalDate.now(TEST_CLOCK).plusDays(1);
 
             assertThatThrownBy(() ->
-                User.of("user123", "test@example.com", futureDate, MALE, TEST_CLOCK)
+                User.of("user123", "test@example.com", futureDate, MALE, LocalDate.now(TEST_CLOCK))
             )
-                .isInstanceOf(IllegalArgumentException.class)
+                .isInstanceOf(CoreException.class)
                 .hasMessage("생년월일은 미래일 수 없습니다.");
         }
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointApiE2ETest.java
index 0b287c245..054eae9b6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/point/PointApiE2ETest.java
@@ -93,20 +93,20 @@ void returnBadRequestException_whenUserIdHeaderIsMissing() {
     @DisplayName("포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.")
     void returnsPoint_whenPointExists() {
       //given
-      String userId = "testuser";
+      String loginId = "testuser";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = Gender.MALE;
 
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
+      User user = User.of(loginId, email, birth, gender, LocalDate.now(TEST_CLOCK));
       User savedUser = userJpaRepository.save(user);
 
-      Point point = Point.of(savedUser, 5L);
+      Point point = Point.of(savedUser.getId(), 5L);
       pointJpaRepository.save(point);
 
       // when
       HttpHeaders httpHeaders = new HttpHeaders();
-      httpHeaders.set("X-USER-ID", userId);
+      httpHeaders.set("X-USER-ID", String.valueOf(savedUser.getId()));
 
       ParameterizedTypeReference<ApiResponse<PointResponse>> responseType =
           new ParameterizedTypeReference<>() {
@@ -123,7 +123,7 @@ void returnsPoint_whenPointExists() {
       assertAll(
           () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
           () -> assertThat(response.getBody()).isNotNull(),
-          () -> assertThat(response.getBody().data().userId()).isEqualTo(userId),
+          () -> assertThat(response.getBody().data().userId()).isEqualTo(savedUser.getId()),
           () -> assertThat(response.getBody().data().balance()).isEqualTo(5L)
       );
     }
@@ -133,10 +133,10 @@ void returnsPoint_whenPointExists() {
     @DisplayName("해당 ID의 회원이 존재하지 않을 경우, null을 반환한다.")
     void returnsNull_whenUserDoesNotExists() {
       //given
-      String userId = "nonexistent";
+      Long userId = 999999L;
       // when
       HttpHeaders httpHeaders = new HttpHeaders();
-      httpHeaders.set("X-USER-ID", userId);
+      httpHeaders.set("X-USER-ID", String.valueOf(userId));
 
       ParameterizedTypeReference<ApiResponse<PointResponse>> responseType =
           new ParameterizedTypeReference<>() {
@@ -167,22 +167,22 @@ class ChargePoint {
     @DisplayName("존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다")
     void returnsTotalBalance_whenChargingThousand() {
       // given
-      String userId = "testuser";
+      String loginId = "testuser";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = Gender.MALE;
 
-      User user = User.of(userId, email, birth, gender, TEST_CLOCK);
+      User user = User.of(loginId, email, birth, gender, LocalDate.now(TEST_CLOCK));
       User savedUser = userJpaRepository.save(user);
 
-      Point point = Point.zero(savedUser);
+      Point point = Point.zero(savedUser.getId());
       pointJpaRepository.save(point);
 
       ChargeRequest chargeRequest = new ChargeRequest(1000L);
 
       // when
       HttpHeaders httpHeaders = new HttpHeaders();
-      httpHeaders.set("X-USER-ID", userId);
+      httpHeaders.set("X-USER-ID", String.valueOf(savedUser.getId()));
 
       ParameterizedTypeReference<ApiResponse<ChargeResponse>> responseType =
           new ParameterizedTypeReference<>() {
@@ -199,7 +199,7 @@ void returnsTotalBalance_whenChargingThousand() {
       assertAll(
           () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
           () -> assertThat(response.getBody()).isNotNull(),
-          () -> assertThat(response.getBody().data().userId()).isEqualTo(userId),
+          () -> assertThat(response.getBody().data().userId()).isEqualTo(savedUser.getId()),
           () -> assertThat(response.getBody().data().balance()).isEqualTo(1000L)
       );
     }
@@ -208,12 +208,12 @@ void returnsTotalBalance_whenChargingThousand() {
     @DisplayName("존재하지 않는 유저로 요청할 경우, 404 NOT FOUND 응답을 반환한다.")
     void returnsNotFoundException_whenUserDoesNotExists() {
       // given
-      String userId = "doesnotexist";
+      Long userId = 999999L;
       ChargeRequest chargeRequest = new ChargeRequest(1000L);
 
       // when
       HttpHeaders httpHeaders = new HttpHeaders();
-      httpHeaders.set("X-USER-ID", userId);
+      httpHeaders.set("X-USER-ID", String.valueOf(userId));
 
       ParameterizedTypeReference<ApiResponse<ChargeResponse>> responseType =
           new ParameterizedTypeReference<>() {
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java
index 439ad925f..a5daba7a8 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserApiE2ETest.java
@@ -56,11 +56,11 @@ class Register {
     @Test
     void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
       // given
-      String userId = "testuser1";
+      String loginId = "testuser1";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = MALE;
-      UserDto.RegisterRequest request = new UserDto.RegisterRequest(userId, email, birth, gender);
+      UserDto.RegisterRequest request = new UserDto.RegisterRequest(loginId, email, birth, gender);
 
       // when
       ResponseEntity<ApiResponse<UserDto.UserResponse>> response =
@@ -70,7 +70,7 @@ void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
       assertAll(
           () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
           () -> assertThat(response.getBody().data()).isNotNull(),
-          () -> assertThat(response.getBody().data().userId()).isEqualTo(userId),
+          () -> assertThat(response.getBody().data().loginId()).isEqualTo(loginId),
           () -> assertThat(response.getBody().data().email()).isEqualTo(email),
           () -> assertThat(response.getBody().data().birth()).isEqualTo(birth),
           () -> assertThat(response.getBody().data().gender()).isEqualTo(gender)
@@ -81,10 +81,10 @@ void returnsCreatedUserInfo_whenRegistrationIsSuccessful() {
     @Test
     void throwsBadRequest_whenGenderIsMissing() {
       // given
-      String userId = "testuser2";
+      String loginId = "testuser2";
       String email = "test2@example.com";
       LocalDate birth = LocalDate.of(1995, 5, 15);
-      UserDto.RegisterRequest request = new UserDto.RegisterRequest(userId, email, birth, null);
+      UserDto.RegisterRequest request = new UserDto.RegisterRequest(loginId, email, birth, null);
 
       // when
       ResponseEntity<ApiResponse<UserDto.UserResponse>> response =
@@ -95,7 +95,7 @@ void throwsBadRequest_whenGenderIsMissing() {
     }
   }
 
-  @DisplayName("GET /api/v1/users/{userId}")
+  @DisplayName("GET /api/v1/users/{loginId}")
   @Nested
   class GetUser {
 
@@ -103,23 +103,23 @@ class GetUser {
     @Test
     void returnsUserInfo_whenUserExists() {
       // given
-      String userId = "testuser1";
+      String loginId = "testuser1";
       String email = "test@example.com";
       LocalDate birth = LocalDate.of(1990, 1, 1);
       Gender gender = MALE;
-      UserDto.RegisterRequest registerRequest = new UserDto.RegisterRequest(userId, email, birth, gender);
+      UserDto.RegisterRequest registerRequest = new UserDto.RegisterRequest(loginId, email, birth, gender);
       testRestTemplate.exchange(ENDPOINT_REGISTER, HttpMethod.POST, new HttpEntity<>(registerRequest),
           USER_RESPONSE_TYPE);
 
       // when
       ResponseEntity<ApiResponse<UserDto.UserResponse>> response =
-          testRestTemplate.exchange("/api/v1/users/" + userId, HttpMethod.GET, null, USER_RESPONSE_TYPE);
+          testRestTemplate.exchange("/api/v1/users/" + loginId, HttpMethod.GET, null, USER_RESPONSE_TYPE);
 
       // then
       assertAll(
           () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
           () -> assertThat(response.getBody().data()).isNotNull(),
-          () -> assertThat(response.getBody().data().userId()).isEqualTo(userId),
+          () -> assertThat(response.getBody().data().loginId()).isEqualTo(loginId),
           () -> assertThat(response.getBody().data().email()).isEqualTo(email),
           () -> assertThat(response.getBody().data().birth()).isEqualTo(birth),
           () -> assertThat(response.getBody().data().gender()).isEqualTo(gender)
@@ -130,11 +130,11 @@ void returnsUserInfo_whenUserExists() {
     @Test
     void returnsNotFound_whenUserDoesNotExist() {
       // given
-      String nonExistentUserId = "nonexistent";
+      String nonExistentLoginId = "nonexistent";
 
       // when
       ResponseEntity<ApiResponse<UserDto.UserResponse>> response =
-          testRestTemplate.exchange("/api/v1/users/" + nonExistentUserId, HttpMethod.GET, null, USER_RESPONSE_TYPE);
+          testRestTemplate.exchange("/api/v1/users/" + nonExistentLoginId, HttpMethod.GET, null, USER_RESPONSE_TYPE);
 
       // then
       assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
diff --git a/docs/adr/ADR-001-order-orderitem-bidirectional-relationship.md b/docs/adr/ADR-001-order-orderitem-bidirectional-relationship.md
new file mode 100644
index 000000000..157029f7f
--- /dev/null
+++ b/docs/adr/ADR-001-order-orderitem-bidirectional-relationship.md
@@ -0,0 +1,91 @@
+# ADR-001: Order-OrderItem은 양방향 관계로 구현
+
+## 결정사항
+**Order-OrderItem 관계는 양방향 객체 참조(cascade + orphanRemoval)로 구현한다.**
+
+```java
+@Entity
+public class Order {
+  @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
+  private List<OrderItem> items = new ArrayList<>();
+
+  public void addItem(OrderItem item) {
+    this.items.add(item);
+    item.assignOrder(this);
+  }
+}
+
+@Entity
+public class OrderItem {
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "ref_order_id", nullable = false)
+  private Order order;
+}
+```
+
+## 상황
+Order-OrderItem은 다른 엔티티 관계와 다른 특성을 가짐:
+- OrderItem은 Order 없이 존재할 수 없음
+- 주문 생성 시 items와 totalAmount가 정합성을 유지해야 함
+- Order가 OrderItem의 생명주기를 완전히 제어해야 함
+
+## 선택 근거
+
+### 1. DDD Aggregate Root 패턴
+Order가 OrderItem의 생명주기를 완전히 제어:
+- Order 생성 → OrderItem 생성
+- Order 삭제 → OrderItem 자동 삭제
+- OrderItem 독립 생성 불가
+
+### 2. 트랜잭션 일관성 보장
+```java
+Order order = Order.create(userId, totalAmount);
+order.addItem(OrderItem.of(productId, quantity, price));
+orderRepository.save(order); // items도 함께 저장
+```
+
+### 3. 비즈니스 로직 캡슐화
+Order 도메인이 items 관리 로직 포함:
+- items 추가/제거 검증
+- totalAmount 자동 재계산
+- 양방향 관계 설정
+
+## 페이징 처리 전략
+
+양방향 관계에서 `JOIN FETCH` + 페이징은 메모리 페이징 문제가 발생한다.
+
+### ✅ 해결 방안
+
+**목록 조회**: DTO 프로젝션 (DB 레벨 페이징)
+
+```java
+@Query("SELECT new OrderListDto(o.id, o.orderedAt, o.totalAmount, o.status, SIZE(o.items)) " +
+       "FROM Order o WHERE o.userId = :userId")
+Page<OrderListDto> findOrderList(Long userId, Pageable pageable);
+```
+
+**상세 조회**: @EntityGraph (단건은 페이징 불필요)
+
+```java
+@EntityGraph(attributePaths = "items")
+Optional<Order> findWithItemsById(Long orderId);
+```
+
+**원칙**: 목록은 DTO, 상세는 Entity
+
+## 트레이드오프
+
+### ✅ 장점
+- 트랜잭션 일관성 보장 (cascade)
+- 비즈니스 로직 캡슐화
+- DDD Aggregate Root 패턴 구현
+- 명확한 생명주기 관리
+
+### ⚠️ 단점
+- 목록/상세 쿼리 패턴 분리 필요
+- DTO 프로젝션 필요
+- 양방향 관계 설정 필수
+
+## 참고
+- 요구사항: `docs/design/01-requirements.md`
+- ERD: `docs/design/04-erd.md`
\ No newline at end of file
diff --git a/docs/adr/ADR-002-productlike-hard-delete-strategy.md b/docs/adr/ADR-002-productlike-hard-delete-strategy.md
new file mode 100644
index 000000000..b3ae4adc8
--- /dev/null
+++ b/docs/adr/ADR-002-productlike-hard-delete-strategy.md
@@ -0,0 +1,75 @@
+# ADR-002: ProductLike는 Hard Delete 전략 사용
+
+## 결정사항
+**ProductLike 삭제 시 Hard Delete(물리 삭제)를 사용한다.**
+
+```java
+public void unlikeProduct(Long userId, Long productId) {
+  // Hard Delete 실행
+  productLikeRepository.deleteByUserIdAndProductId(userId, productId);
+
+  // Product likeCount 감소
+  product.decreaseLikeCount();
+}
+```
+
+## 상황
+ProductLike 삭제 전략 선택 시 고려사항:
+- 좋아요는 선호도 표시이며 법적 보관 의무가 없음
+- 대규모 트래픽 환경 가정 (요구사항 2.1.3)
+- 사용자가 언제든지 재등록 가능한 데이터
+- 조회 성능 최적화 필요
+
+## 선택 근거
+
+### 1. 법적 보관 의무 없음
+- 좋아요는 금전 거래 아님
+- 세법, 전자상거래법 적용 대상 아님
+- Order/Payment와 달리 이력 보관 불필요
+
+### 2. 성능 최적화
+**Soft Delete 시 문제점**:
+- 모든 조회 쿼리에 `deleted_at IS NULL` 필터 필수
+- 삭제된 데이터가 인덱스에 계속 남음
+- 테이블 크기 증가 → 조회 성능 저하
+
+**Hard Delete 장점**:
+- 테이블 크기 최소화 → 인덱스 효율 증가
+- 조회 쿼리가 단순함 (필터링 불필요)
+
+### 3. 비즈니스 의미 일치
+- 좋아요 취소 = 데이터 삭제 (직관적)
+- 재등록 가능하므로 복구 불필요
+
+### 4. 멱등성 보장
+요구사항: "이미 취소한 좋아요 재취소 → 200 OK"
+- DELETE 실행 (0건 영향) → 에러 없음
+- Product.decreaseLikeCount()에 음수 방어 로직 포함
+
+## 대안: 이력이 필요한 경우
+
+이벤트 기반 아키텍처로 이력 데이터를 분리:
+```
+ProductLike Delete → Event 발행 → 데이터 웨어하우스 저장
+```
+
+**장점**:
+- 메인 DB 성능 유지 (Hard Delete)
+- 이력은 분석 시스템에서 관리
+- 관심사 분리
+
+## 트레이드오프
+
+### ✅ 장점
+- 조회 성능 향상 (테이블 크기 최소화)
+- 코드 단순성 (필터링 로직 불필요)
+- 비즈니스 의미 일치
+- 멱등성 보장
+
+### ⚠️ 단점
+- 메인 DB에서 즉시 복구 불가
+- 이력 추적 제한 (이벤트로 해결 가능)
+
+## 참고
+- 요구사항: `docs/design/01-requirements.md` (2.1.3 좋아요 취소 멱등성)
+- ERD: `docs/design/04-erd.md`
diff --git a/docs/week2/01-requirements.md b/docs/design/01-requirements.md
similarity index 97%
rename from docs/week2/01-requirements.md
rename to docs/design/01-requirements.md
index 552feda1a..94fa67659 100644
--- a/docs/week2/01-requirements.md
+++ b/docs/design/01-requirements.md
@@ -265,7 +265,7 @@
 - **결제 처리 실패**:
   - 주문은 이미 DB에 저장 완료 (재고/포인트 차감 완료, 트랜잭션 커밋됨)
   - 자동 재시도 3회 (1초, 2초, 4초 간격, 지수 백오프)
-  - 재시도 실패 시 주문 상태를 '결제 대기'로 마킹
+  - 재시도 실패 시 주문 상태를 '결제 실패'로 마킹
   - 500 Internal Server Error 응답
 
 ##### 기능 요구사항
@@ -290,6 +290,7 @@
         7. 외부 시스템 전송 (Mock 가능)
     - **응답 데이터**:
         - 주문 ID, 주문 일시
+        - 주문 상태 (status): PENDING, COMPLETED, PAYMENT_FAILED
         - 주문 상품 목록: 상품 ID, 상품명, 수량, 가격
         - 총 결제 금액
 
@@ -299,12 +300,14 @@
         - `size`: 페이지당 주문 수 (기본값: 20)
     - **응답 데이터**:
         - 주문 ID, 주문 일시, 총 금액
+        - 주문 상태 (status): PENDING, COMPLETED, PAYMENT_FAILED
         - 주문 상품 수
         - 페이지네이션 정보: 총 개수, 현재 페이지, 전체 페이지 수, 페이지당 개수
 
 - **주문 상세 조회**: `GET /api/v1/orders/{orderId}`
     - **응답 데이터**:
         - 주문 ID, 주문 일시
+        - 주문 상태 (status): PENDING, COMPLETED, PAYMENT_FAILED
         - 주문 상품 목록: 상품 ID, 상품명, 수량, 가격
         - 총 결제 금액
 
diff --git a/docs/week2/02-sequence-diagrams.md b/docs/design/02-sequence-diagrams.md
similarity index 99%
rename from docs/week2/02-sequence-diagrams.md
rename to docs/design/02-sequence-diagrams.md
index 54c1570ce..a015df0b2 100644
--- a/docs/week2/02-sequence-diagrams.md
+++ b/docs/design/02-sequence-diagrams.md
@@ -592,7 +592,7 @@ sequenceDiagram
 **주요 처리 사항**:
 - 트랜잭션 커밋 후 결제 시스템 호출 (트랜잭션 밖)
 - 3회 자동 재시도 (1초, 2초, 4초 간격, 지수 백오프)
-- 재시도 실패 시 주문 상태를 '결제 대기'로 마킹 및 500 에러 응답
+- 재시도 실패 시 주문 상태를 '결제 실패'로 마킹 및 500 에러 응답
 
 ```mermaid
 sequenceDiagram
@@ -627,7 +627,7 @@ sequenceDiagram
     end
 
     Note over OrderFacade: 재시도 실패 (3회 모두 실패)
-    OrderFacade->>Order: updateStatus("결제 대기")
+    OrderFacade->>Order: failPayment()
     OrderFacade->>OrderRepository: save(Order)
 
     Note over OrderFacade: 모니터링 알림 발송
diff --git a/docs/design/03-class-diagram.md b/docs/design/03-class-diagram.md
new file mode 100644
index 000000000..0a2c192eb
--- /dev/null
+++ b/docs/design/03-class-diagram.md
@@ -0,0 +1,300 @@
+# 03-class-diagram.md - 클래스 다이어그램
+
+## 📑 목차
+
+- [1. 도메인 객체 설계](#1-도메인-객체-설계)
+- [2. 도메인 설명](#2-도메인-설명)
+  - [2.1 User (사용자)](#21-user-사용자)
+  - [2.2 Point (포인트)](#22-point-포인트)
+  - [2.3 Product (상품)](#23-product-상품)
+  - [2.4 Brand (브랜드)](#24-brand-브랜드)
+  - [2.5 ProductLike (좋아요)](#25-productlike-좋아요)
+  - [2.6 Order (주문)](#26-order-주문)
+  - [2.7 OrderItem (주문 항목)](#27-orderitem-주문-항목)
+  - [2.8 OrderPrice (주문 가격)](#28-orderprice-주문-가격)
+- [3. 관계 설명](#3-관계-설명)
+- [4. Order 상태 다이어그램](#4-order-상태-다이어그램)
+- [5. 참고사항](#5-참고사항)
+
+---
+
+## 1. 도메인 객체 설계
+
+```mermaid
+classDiagram
+    class User {
+        +Long id
+        +String loginId
+        +String email
+        +String gender
+        +LocalDate birthDate
+    }
+
+    class Point {
+        +Long id
+        +Long userId
+        +Long amount
+        +deduct(amount: Long) void
+        +charge(amount: Long) void
+        +hasEnough(amount: Long) boolean
+    }
+
+    class Product {
+        +Long id
+        +String name
+        +Long price
+        +String description
+        +Integer stock
+        +Long likeCount
+        +Long brandId
+        +LocalDateTime createdAt
+        +decreaseStock(quantity: Integer) void
+        +isAvailable() boolean
+        +increaseLikeCount() void
+        +decreaseLikeCount() void
+    }
+
+    class Brand {
+        +Long id
+        +String name
+        +String description
+    }
+
+    class ProductLike {
+        +Long id
+        +Long userId
+        +Long productId
+        +LocalDateTime likedAt
+        +isLikedBy(userId: Long) boolean
+    }
+
+    class Order {
+        +Long id
+        +Long userId
+        +OrderStatus status
+        +Long totalAmount
+        +LocalDateTime orderedAt
+        +updateStatus(status: OrderStatus) void
+    }
+
+    class OrderItem {
+        +Long id
+        +Long orderId
+        +Long productId
+        +String productName
+        +Integer quantity
+        +OrderPrice orderPrice
+        +calculateSubtotal() Long
+        +getOrderPriceValue() Long
+    }
+
+    class OrderPrice {
+        <<Value Object>>
+        +Long value
+        +of(value: Long) OrderPrice
+        +getValue() Long
+    }
+
+    Point "1" --> "1" User : references (userId)
+    Product "N" --> "1" Brand : references (brandId)
+    ProductLike "N" --> "1" User : references (userId)
+    ProductLike "N" --> "1" Product : references (productId)
+    Order "N" --> "1" User : references (userId)
+    OrderItem "N" --> "1" Order : references (orderId)
+    OrderItem "N" --> "1" Product : references (productId)
+```
+
+## 2. 도메인 설명
+
+### 2.1 User (사용자)
+- 회원 정보를 담는 엔티티
+- 좋아요와 주문의 주인
+
+### 2.2 Point (포인트)
+- 사용자의 포인트 잔액
+- User 1명당 Point 1개 (1:1 관계)
+- **비즈니스 로직**:
+  - `deduct()`: 포인트 차감 (부족 시 예외)
+  - `charge()`: 포인트 충전
+  - `hasEnough()`: 잔액 충분 여부 확인
+
+### 2.3 Product (상품)
+- 판매 상품 정보
+- 재고, 가격, 브랜드 정보를 가짐
+- `likeCount`: 좋아요 수 (비정규화된 집계 값, 성능 최적화용)
+- **비즈니스 로직**:
+  - `decreaseStock()`: 재고 차감 (부족 시 예외)
+  - `isAvailable()`: 판매 가능 여부 (재고 > 0)
+  - `increaseLikeCount()`, `decreaseLikeCount()`: 좋아요 수 관리
+
+### 2.4 Brand (브랜드)
+- 브랜드 정보
+- 여러 상품을 포함함
+
+### 2.5 ProductLike (좋아요)
+- 사용자가 상품에 누른 좋아요
+- 한 상품당 한 번만 좋아요 가능 (중복 방지)
+- `likedAt`: 좋아요 누른 시각 (최근 순 정렬용)
+- **비즈니스 로직**:
+  - `isLikedBy()`: 특정 사용자가 좋아요했는지 확인
+
+### 2.6 Order (주문)
+- 사용자의 주문 정보
+- `status`: 주문 상태 (OrderStatus Enum: PENDING, COMPLETED, PAYMENT_FAILED)
+- `totalAmount`: 주문 총액 (OrderItem 금액 합계)
+- `orderedAt`: 주문한 시각
+- **비즈니스 로직**:
+  - `updateStatus()`: 주문 상태 변경 (결제 실패 시 PAYMENT_FAILED으로 변경)
+- **주문 항목 조회**: OrderItemRepository를 통해 `findByOrderId()`로 조회
+- **상태 다이어그램**: [4. Order 상태 다이어그램](#4-order-상태-다이어그램) 참고
+
+### 2.7 OrderItem (주문 항목)
+- 주문에 포함된 개별 상품
+- **스냅샷 저장**: 주문 당시의 상품명, 가격, 수량을 저장하여 Product 변동에 영향받지 않음
+  - `productName`: 주문 시점 상품명 (Product 삭제 시에도 주문 내역 표시 가능)
+  - `orderPrice`: 주문 시점 가격 (OrderPrice Value Object, Product 가격 변동과 무관)
+  - `quantity`: 주문 수량
+- **Entity로 설계**: 독립 테이블로 관리되며 향후 확장 가능
+- **비즈니스 로직**:
+  - `calculateSubtotal()`: 항목 소계 (orderPrice × quantity)
+  - `getOrderPriceValue()`: 주문 가격 값 조회
+
+### 2.8 OrderPrice (주문 가격)
+- **Value Object**: 주문 시점의 가격 스냅샷
+- Product의 Money와 별도 타입으로 스냅샷 의도 명확화
+- **불변성**: 생성 후 변경 불가
+- **검증**: null 방지, 음수 방지
+
+## 3. 관계 설명
+
+| 관계 | 유형 | 설명 |
+|------|------|------|
+| **Point → User** | 1:1 (ID 참조) | 사용자 1명당 포인트 1개, `userId` 필드로 참조 |
+| **Product → Brand** | N:1 (ID 참조) | 여러 상품이 하나의 브랜드 소속, `brandId` 필드로 참조 |
+| **ProductLike → User** | N:1 (ID 참조) | 사용자가 여러 상품에 좋아요 가능, `userId` 필드로 참조 |
+| **ProductLike → Product** | N:1 (ID 참조) | 상품 하나에 여러 좋아요 가능, `productId` 필드로 참조 |
+| **Order → User** | N:1 (ID 참조) | 사용자가 여러 주문 가능, `userId` 필드로 참조 |
+| **OrderItem → Order** | N:1 (ID 참조) | 주문 항목이 주문 참조, `orderId` 필드로 참조 |
+| **OrderItem → Product** | N:1 (ID 참조) | 주문 항목이 상품 참조, `productId` 필드로 참조 |
+
+### 참조 방식
+- 참조 컬럼명: `ref_` 접두사 사용 (예: `ref_user_id`, `ref_order_id`)
+
+## 4. Order 상태 다이어그램
+
+### 4.1 상태 정의
+
+| 상태 | 설명 | 비고 |
+|------|------|------|
+| **PENDING** | 주문 생성 완료 | 외부 결제 API 호출 전 초기 상태 |
+| **COMPLETED** | 결제 완료 | 외부 결제 API 성공 시 최종 상태 |
+| **PAYMENT_FAILED** | 결제 재시도 실패 | 외부 결제 실패 후 재시도 실패 시 |
+
+**참고**: `FAILED` 상태는 존재하지 않음
+- 이유: 결제 실패 시 트랜잭션 롤백으로 주문 데이터가 DB에 저장되지 않음
+- 재시도 실패한 주문만 `PAYMENT_FAILED` 상태로 DB에 저장됨
+
+### 4.2 상태 전이 다이어그램
+
+```mermaid
+stateDiagram-v2
+    [*] --> PENDING: 주문 생성
+    PENDING --> COMPLETED: 외부 결제 성공
+    PENDING --> PAYMENT_FAILED: 외부 결제 재시도 실패
+    COMPLETED --> [*]
+    PAYMENT_FAILED --> [*]
+
+    note right of PENDING
+        초기 상태
+        외부 결제 API 호출 전
+    end note
+
+    note right of COMPLETED
+        최종 성공 상태
+        결제 완료
+    end note
+
+    note right of PAYMENT_FAILED
+        재시도 실패 상태
+        수동 처리 필요
+    end note
+```
+
+### 4.3 상태 전이 조건
+
+| 전이 | 조건 | 동작 |
+|------|------|------|
+| `[*] → PENDING` | 주문 생성 요청 | 1. 재고 확인 및 차감<br>2. 포인트 차감<br>3. Order, OrderItem 생성 (status: PENDING) |
+| `PENDING → COMPLETED` | 외부 결제 API 성공 | 1. Order.status를 COMPLETED로 업데이트<br>2. 트랜잭션 커밋 |
+| `PENDING → PAYMENT_FAILED` | 외부 결제 재시도 실패 | 1. Order.status를 PAYMENT_FAILED으로 업데이트<br>2. 트랜잭션 커밋 (수동 처리 위해 주문 기록 보존) |
+| `PENDING → [트랜잭션 롤백]` | 외부 결제 실패 (재시도 전) | 1. 전체 트랜잭션 롤백<br>2. DB에 주문 데이터 저장 안 됨 |
+
+### 4.4 주문 처리 흐름도
+
+```mermaid
+flowchart TD
+    Start([주문 요청]) --> ValidateStock{재고 확인}
+    ValidateStock -->|부족| Error1[재고 부족 예외]
+    ValidateStock -->|충분| DeductStock[재고 차감]
+
+    DeductStock --> ValidatePoint{포인트 확인}
+    ValidatePoint -->|부족| Error2[포인트 부족 예외]
+    ValidatePoint -->|충분| DeductPoint[포인트 차감]
+
+    DeductPoint --> CreateOrder[Order 생성<br/>status: PENDING]
+    CreateOrder --> CreateOrderItem[OrderItem 생성]
+
+    CreateOrderItem --> CallPayment[외부 결제 API 호출]
+    CallPayment --> PaymentSuccess{결제 성공?}
+
+    PaymentSuccess -->|성공| UpdateCompleted[status → COMPLETED]
+    UpdateCompleted --> Commit1[트랜잭션 커밋]
+    Commit1 --> End([주문 완료])
+
+    PaymentSuccess -->|실패| Retry{재시도 성공?}
+    Retry -->|성공| UpdateCompleted
+    Retry -->|실패| UpdatePending[status → PAYMENT_FAILED]
+    UpdatePending --> Commit2[트랜잭션 커밋<br/>수동 처리용 기록 보존]
+    Commit2 --> End2([재시도 실패])
+
+    Retry -->|재시도 전 실패| Rollback[트랜잭션 롤백]
+    Rollback --> End3([주문 실패<br/>DB 저장 안 됨])
+
+    Error1 --> End3
+    Error2 --> End3
+```
+
+### 4.5 상태별 비즈니스 규칙
+
+#### PENDING
+- **허용 동작**:
+  - 외부 결제 API 호출
+  - 상태 변경 → COMPLETED (결제 성공)
+  - 상태 변경 → PAYMENT_FAILED (재시도 실패)
+- **제약 사항**:
+  - 사용자에게 주문 상세 조회 시 "결제 처리 중" 표시
+  - 이 상태의 주문은 재고/포인트 이미 차감된 상태
+
+#### COMPLETED
+- **허용 동작**:
+  - 주문 조회 (주문 목록, 상세)
+- **제약 사항**:
+  - 더 이상 상태 변경 불가 (최종 상태)
+  - 취소/환불 기능은 현재 요구사항에 없음
+
+#### PAYMENT_FAILED
+- **허용 동작**:
+  - 관리자 수동 처리
+  - 주문 조회
+- **제약 사항**:
+  - 자동 재시도 불가 (이미 재시도 실패한 상태)
+  - 수동 처리 필요 (환불 또는 결제 재처리)
+
+## 5. 참고사항
+
+- User, Point는 1주차에서 이미 구현됨 (관계 표현 위해 포함)
+- **공통 필드** (BaseEntity 상속):
+  - `id`: 엔티티 식별자 (Long)
+  - `createdAt`: 생성 일시
+  - `updatedAt`: 수정 일시
+  - `deletedAt`: 삭제 일시 (Soft Delete용)
diff --git a/docs/week2/04-erd.md b/docs/design/04-erd.md
similarity index 95%
rename from docs/week2/04-erd.md
rename to docs/design/04-erd.md
index eb97c52db..14fab8059 100644
--- a/docs/week2/04-erd.md
+++ b/docs/design/04-erd.md
@@ -32,7 +32,7 @@ erDiagram
 
     USERS {
         bigint id PK "사용자 식별자"
-        varchar(50) user_id UK "로그인 ID"
+        varchar(50) login_id UK "로그인 ID"
         varchar(100) email UK "이메일 주소"
         varchar(10) gender "성별"
         date birth_date "생년월일"
@@ -57,7 +57,7 @@ erDiagram
     BRANDS {
         bigint id PK "브랜드 식별자"
         varchar(100) name "브랜드명"
-        text description "브랜드 설명"
+        varchar(200) description "브랜드 설명"
         timestamp created_at
         timestamp updated_at
         timestamp deleted_at
@@ -69,7 +69,7 @@ erDiagram
         bigint id PK "상품 식별자"
         varchar(100) name "상품명"
         bigint price "가격"
-        text description "상품 설명"
+        varchar(200) description "상품 설명"
         int stock "재고 수량"
         bigint like_count "좋아요 수 (비정규화)"
         bigint ref_brand_id "브랜드 참조"
@@ -123,7 +123,7 @@ erDiagram
 
 ### 2.1 USERS
 - 사용자 계정 정보
-- user_id, email 유니크 제약
+- login_id, email 유니크 제약
 
 ### 2.2 POINTS
 - 포인트 잔액
@@ -154,7 +154,7 @@ erDiagram
 - total_amount는 OrderItem 합계
 - **status**: 주문 상태
   - `COMPLETED`: 결제 완료
-  - `PAYMENT_PENDING`: 결제 대기 (결제 처리 실패 시)
+  - `PAYMENT_FAILED`: 결제 실패
 
 ### 2.7 ORDER_ITEMS
 - 주문 상품 상세
diff --git a/docs/loopers-1-week.md b/docs/week1/loopers-1-week.md
similarity index 100%
rename from docs/loopers-1-week.md
rename to docs/week1/loopers-1-week.md
diff --git a/docs/week2/03-class-diagram.md b/docs/week2/03-class-diagram.md
deleted file mode 100644
index aad7721f0..000000000
--- a/docs/week2/03-class-diagram.md
+++ /dev/null
@@ -1,170 +0,0 @@
-# 03-class-diagram.md - 클래스 다이어그램
-
-## 📑 목차
-
-- [1. 도메인 객체 설계](#1-도메인-객체-설계)
-- [2. 도메인 설명](#2-도메인-설명)
-  - [2.1 User (사용자)](#21-user-사용자)
-  - [2.2 Point (포인트)](#22-point-포인트)
-  - [2.3 Product (상품)](#23-product-상품)
-  - [2.4 Brand (브랜드)](#24-brand-브랜드)
-  - [2.5 ProductLike (좋아요)](#25-productlike-좋아요)
-  - [2.6 Order (주문)](#26-order-주문)
-  - [2.7 OrderItem (주문 항목)](#27-orderitem-주문-항목)
-- [3. 관계 설명](#3-관계-설명)
-- [4. 참고사항](#4-참고사항)
-
----
-
-## 1. 도메인 객체 설계
-
-```mermaid
-classDiagram
-    class User {
-        +Long id
-        +String userId
-        +String email
-        +String gender
-        +LocalDate birthDate
-    }
-
-    class Point {
-        +Long id
-        +Long amount
-        +deduct(amount: Long) void
-        +charge(amount: Long) void
-        +hasEnough(amount: Long) boolean
-    }
-
-    class Product {
-        +Long id
-        +LocalDateTime createdAt
-        +String name
-        +Long price
-        +String description
-        +Integer stock
-        +Long likeCount
-        +Long brandId
-        +decreaseStock(quantity: Integer) void
-        +isAvailable() boolean
-        +increaseLikeCount() void
-        +decreaseLikeCount() void
-    }
-
-    class Brand {
-        +Long id
-        +String name
-        +String description
-    }
-
-    class ProductLike {
-        +Long id
-        +Long userId
-        +Long productId
-        +LocalDateTime likedAt
-        +isLikedBy(userId: Long) boolean
-    }
-
-    class Order {
-        +Long id
-        +Long userId
-        +String status
-        +Long totalAmount
-        +LocalDateTime orderedAt
-        +List~OrderItem~ items
-        +addItem(product: Product, quantity: Integer) void
-        +calculateTotal() Long
-        +getItemCount() Integer
-        +updateStatus(status: String) void
-    }
-
-    class OrderItem {
-        +Long id
-        +Long orderId
-        +Long productId
-        +Integer quantity
-        +Long price
-        +calculateSubtotal() Long
-    }
-
-
-    Point "1" --> "1" User : references (userId)
-    Product "N" --> "1" Brand : references (brandId)
-    ProductLike "N" --> "1" User : references (userId)
-    ProductLike "N" --> "1" Product : references (productId)
-    Order "N" --> "1" User : references (userId)
-    Order "1" *-- "N" OrderItem : contains
-    OrderItem "N" --> "1" Product : references (productId)
-```
-
-## 2. 도메인 설명
-
-### 2.1 User (사용자)
-- 회원 정보를 담는 엔티티
-- 좋아요와 주문의 주인
-- ※ 1주차 구현 완료
-
-### 2.2 Point (포인트)
-- 사용자의 포인트 잔액
-- User 1명당 Point 1개 (1:1 관계)
-- **비즈니스 로직**:
-  - `deduct()`: 포인트 차감 (부족 시 예외)
-  - `charge()`: 포인트 충전
-  - `hasEnough()`: 잔액 충분 여부 확인
-- ※ 1주차 구현 완료
-
-### 2.3 Product (상품)
-- 판매 상품 정보
-- 재고, 가격, 브랜드 정보를 가짐
-- `likeCount`: 좋아요 수 (비정규화된 집계 값, 성능 최적화용)
-- **비즈니스 로직**:
-  - `decreaseStock()`: 재고 차감 (부족 시 예외)
-  - `isAvailable()`: 판매 가능 여부 (재고 > 0)
-  - `increaseLikeCount()`, `decreaseLikeCount()`: 좋아요 수 관리
-
-### 2.4 Brand (브랜드)
-- 브랜드 정보
-- 여러 상품을 포함함
-
-### 2.5 ProductLike (좋아요)
-- 사용자가 상품에 누른 좋아요
-- 한 상품당 한 번만 좋아요 가능 (중복 방지)
-- `likedAt`: 좋아요 누른 시각 (최근 순 정렬용)
-- **비즈니스 로직**:
-  - `isLikedBy()`: 특정 사용자가 좋아요했는지 확인
-
-### 2.6 Order (주문)
-- 사용자의 주문 정보
-- 여러 주문 항목(OrderItem)을 포함
-- `status`: 주문 상태 (COMPLETED, PAYMENT_PENDING 등)
-- `orderedAt`: 주문한 시각
-- **비즈니스 로직**:
-  - `addItem()`: 주문 항목 추가
-  - `calculateTotal()`: 총 주문 금액 계산
-  - `getItemCount()`: 주문 항목 수 조회
-  - `updateStatus()`: 주문 상태 변경 (결제 실패 시 PAYMENT_PENDING으로 변경)
-
-### 2.7 OrderItem (주문 항목)
-- 주문에 포함된 개별 상품
-- 주문 당시의 가격과 수량을 저장 (가격 변동 영향 없음)
-- **Entity로 설계**: 독립 테이블로 관리되며 향후 확장 가능
-- **비즈니스 로직**:
-  - `calculateSubtotal()`: 항목 소계 (가격 × 수량)
-
-## 3. 관계 설명
-
-- **Point ↔ User**: 1:1 관계
-- **Product → Brand**: 여러 상품이 하나의 브랜드 소속
-- **ProductLike → User/Product**: 사용자가 상품에 좋아요
-- **Order → User**: 사용자가 여러 주문 가능
-- **Order → OrderItem**: 주문에 여러 상품 포함
-- **OrderItem → Product**: 주문 항목이 상품 참조
-
-## 4. 참고사항
-
-- User, Point는 1주차에서 이미 구현됨 (관계 표현 위해 포함)
-- **공통 필드** (BaseEntity 상속):
-  - `id`: 엔티티 식별자 (Long)
-  - `createdAt`: 생성 일시
-  - `updatedAt`: 수정 일시
-  - `deletedAt`: 삭제 일시 (Soft Delete용)
