From d2d8a627c1ef017e5e6133a1f1534a85312882db Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 02:19:16 +0900
Subject: [PATCH 01/17] =?UTF-8?q?[Round=204]=20=ED=9A=8C=EC=9B=90=20?=
 =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/point/PointFacade.java        |  12 +-
 .../loopers/application/user/UserFacade.java  |  24 --
 .../java/com/loopers/domain/point/Point.java  |  70 ++--
 .../loopers/domain/point/PointRepository.java |   5 +-
 .../loopers/domain/point/PointService.java    |  67 ++++
 .../java/com/loopers/domain/user/User.java    |  33 +-
 .../com/loopers/domain/user/UserService.java  |   3 +
 .../point/PointQueryRepository.java           |  56 +++
 .../point/PointRepositoryImpl.java            |  13 +-
 .../application/point/PointFacadeTest.java    | 151 +++++++
 .../user/UserFacadeTest.java}                 |   8 +-
 .../domain/point/PointIntegrationTest.java    |  63 ---
 .../domain/point/PointServiceTest.java        | 377 ++++++++++++++++++
 .../com/loopers/domain/point/PointTest.java   | 214 +++++-----
 .../com/loopers/domain/user/UserTest.java     |  14 +-
 15 files changed, 823 insertions(+), 287 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
 rename apps/commerce-api/src/test/java/com/loopers/{domain/user/UserIntegrationTest.java => application/user/UserFacadeTest.java} (96%)
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java

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 7c2131ede..6a529631c 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
@@ -6,6 +6,7 @@
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 
@@ -16,6 +17,7 @@ public class PointFacade {
     private final PointService pointService;
 
     // 포인트 조회
+    @Transactional(readOnly = true)
     public PointInfo getPoint(String loginId){
         Point point = pointService.findByUserLoginId(loginId)
                 .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] Point를 찾을 수 없습니다."));
@@ -23,12 +25,14 @@ public PointInfo getPoint(String loginId){
     }
 
     // 포인트 충전
+    @Transactional
     public PointInfo charge(String loginId, BigDecimal chargeAmount) {
+        // 동시성 안전하게 포인트 충전
+        pointService.charge(loginId, chargeAmount);
+        
+        // 충전 후 최신 포인트 정보 조회
         Point point = pointService.findByUserLoginId(loginId)
-                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] 포인트를 충전할 Point 객체를 찾을 수 없습니다."));
-        point.charge(chargeAmount);
-        Point savedPoint = pointService.savePoint(point)
                 .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] 포인트를 충전 후 Point 객체를 찾을 수 없습니다."));
-        return PointInfo.from(savedPoint);
+        return PointInfo.from(point);
     }
 }
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 1b9c0cd1b..774290d56 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
@@ -1,6 +1,5 @@
 package com.loopers.application.user;
 
-import com.loopers.domain.point.Point;
 import com.loopers.domain.point.PointService;
 import com.loopers.domain.user.User;
 import com.loopers.domain.user.UserService;
@@ -10,8 +9,6 @@
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.math.BigDecimal;
-import java.util.ArrayList;
 import java.util.Optional;
 
 @RequiredArgsConstructor
@@ -35,39 +32,18 @@ public UserInfo saveUser(UserInfo userInfo) {
             throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 회원을 다시 저장 못합니다.");
         }
 
-        // 이미 존재하는 회원 포인트 객체가 있는지 확인
-        Optional<Point> foundPoint = pointService.findByUserLoginId(userInfo.loginId());
-        if(foundPoint.isPresent()){
-            throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 회원의 포인트를 다시 저장 못합니다.");
-        }
-
         // 회원 객체 생성 (point는 null, 빈 likes 리스트 초기화)
         User user = User.builder()
                 .loginId(userInfo.loginId())
                 .email(userInfo.email())
                 .birthday(userInfo.birthday())
                 .gender(userInfo.gender())
-                .point(null)  // Point는 나중에 설정
-                .likes(new ArrayList<>())
                 .build();
 
         // 회원 저장 (Point 없이 먼저 저장)
         User savedUser = userService.saveUser(user)
                 .orElseThrow(() -> new CoreException(ErrorType.INTERNAL_ERROR, "User 를 저장하지 못했습니다."));
 
-        // 포인트 객체 생성 (저장된 User 참조)
-        Point point = Point.builder()
-                .user(savedUser)  // 저장된 User 참조
-                .amount(BigDecimal.valueOf(0))
-                .build();
-
-        // Point 저장
-        Point savedPoint = pointService.savePoint(point)
-                .orElseThrow(() -> new CoreException(ErrorType.INTERNAL_ERROR, "Point 를 저장하지 못했습니다."));
-
-        // User에 Point 설정 (양방향 관계 완성)
-        savedUser.setPoint(savedPoint);
-
         return UserInfo.from(savedUser);
     }
 
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 4b0fd055a..f9418ddaf 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
@@ -18,29 +18,23 @@
 @Getter
 public class Point extends BaseEntity {
 
-    private BigDecimal amount;
+    private BigDecimal amount = BigDecimal.ZERO;    // 포인트 잔액 
 
     @OneToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "user_id")
-    private User user;
+    private User user;                              // 회원 엔티티
 
     @Builder
     private Point(
-            BigDecimal amount
-            , User user
+        User user
     ) {
-        this.amount = amount;
         this.user = user;
         guard();
     }
 
-    // User 필드를 세팅
-    public void setUser(User user) {
-        if(this.user != null) return;
-        this.user = user;
-    }
-
-    // 유효성 검사
+    /**
+     * Point 엔티티의 유효성 검사
+     */
     @Override
     protected void guard() {
         // amount : Null 혹은 음수인지 검사
@@ -51,35 +45,35 @@ protected void guard() {
         }
 
         // user : Null 인지 검사
-//        if(user == null) {
-//            throw new CoreException(ErrorType.BAD_REQUEST, "Point : user가 Null 이 되면 안 됩니다.");
-//        }
+       if(user == null) {
+           throw new CoreException(ErrorType.BAD_REQUEST, "Point : user가 Null 이 되면 안 됩니다.");
+       }
     }
 
-    public BigDecimal charge(BigDecimal amount) {
-        // 충전하는 포인트가 음수이면 BAD REQUEST CoreException 을 발생
-        if(amount.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "충전할 point는 0 이하가 될 수 없습니다.");
-        }
-        this.amount = this.amount.add(amount);
-        return this.amount;
-    }
+    // public BigDecimal charge(BigDecimal amount) {
+    //     // 충전하는 포인트가 음수이면 BAD REQUEST CoreException 을 발생
+    //     if(amount.compareTo(BigDecimal.ZERO) <= 0) {
+    //         throw new CoreException(ErrorType.BAD_REQUEST, "충전할 point는 0 이하가 될 수 없습니다.");
+    //     }
+    //     this.amount = this.amount.add(amount);
+    //     return this.amount;
+    // }
 
-    public BigDecimal deduct(BigDecimal amount) {
-        // 차감하는 포인트가 음수이면 BAD REQUEST CoreException 을 발생
-        if(amount.compareTo(BigDecimal.ZERO) <= 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "차감할 point는 0 이하가 될 수 없습니다.");
-        }
+    // public BigDecimal deduct(BigDecimal amount) {
+    //     // 차감하는 포인트가 음수이면 BAD REQUEST CoreException 을 발생
+    //     if(amount.compareTo(BigDecimal.ZERO) <= 0) {
+    //         throw new CoreException(ErrorType.BAD_REQUEST, "차감할 point는 0 이하가 될 수 없습니다.");
+    //     }
         
-        // 포인트가 부족하면 BAD REQUEST CoreException 을 발생
-        if(this.amount.compareTo(amount) < 0) {
-            throw new CoreException(
-                ErrorType.BAD_REQUEST, 
-                "포인트가 부족합니다. (현재 포인트: " + this.amount + ", 요청 금액: " + amount + ")"
-            );
-        }
+    //     // 포인트가 부족하면 BAD REQUEST CoreException 을 발생
+    //     if(this.amount.compareTo(amount) < 0) {
+    //         throw new CoreException(
+    //             ErrorType.BAD_REQUEST, 
+    //             "포인트가 부족합니다. (현재 포인트: " + this.amount + ", 요청 금액: " + amount + ")"
+    //         );
+    //     }
         
-        this.amount = this.amount.subtract(amount);
-        return this.amount;
-    }
+    //     this.amount = this.amount.subtract(amount);
+    //     return this.amount;
+    // }
 }
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 9f420aa22..13bb6128c 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
@@ -1,10 +1,11 @@
 package com.loopers.domain.point;
 
-import com.loopers.domain.user.User;
-
+import java.math.BigDecimal;
 import java.util.Optional;
 
 public interface PointRepository {
     Optional<Point> findByUser_loginId(String loginId);
     Optional<Point> save(Point point);
+    long deduct(String loginId, BigDecimal deductAmount);
+    long chargeAmount(String loginId, BigDecimal chargeAmount);
 }
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 f431f7d6b..3179c443e 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
@@ -1,8 +1,12 @@
 package com.loopers.domain.point;
 
+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;
 
+import java.math.BigDecimal;
 import java.util.Optional;
 
 @RequiredArgsConstructor
@@ -11,11 +15,74 @@ public class PointService {
 
     private final PointRepository pointRepository;
 
+    @Transactional(readOnly = true)
     public Optional<Point> findByUserLoginId(String loginId) {
         return pointRepository.findByUser_loginId(loginId);
     }
 
+    @Transactional
     public Optional<Point> savePoint(Point point) {
         return pointRepository.save(point);
     }
+
+    /**
+     * 포인트 차감 (동시성 안전)
+     * @param loginId 사용자 로그인 ID
+     * @param deductAmount 차감할 금액
+     * @throws CoreException 차감량이 0 이하이거나, 포인트가 부족한 경우
+     */
+    @Transactional
+    public void deduct(String loginId, BigDecimal deductAmount) {
+        // 차감량 유효성 검사
+        if (deductAmount == null || deductAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "포인트 차감량은 0보다 커야 합니다."
+            );
+        }
+
+        // QueryDSL로 동시성 안전하게 업데이트
+        long updatedRows = pointRepository.deduct(loginId, deductAmount);
+        
+        if (updatedRows == 0L) {
+            // 포인트 조회하여 상세 메시지 제공
+            Point point = pointRepository.findByUser_loginId(loginId)
+                    .orElseThrow(() -> new CoreException(
+                            ErrorType.NOT_FOUND,
+                            "[loginId = " + loginId + "] Point를 찾을 수 없습니다."
+                    ));
+            
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "포인트가 부족합니다. (현재 포인트: " + point.getAmount() + ", 요청 금액: " + deductAmount + ")"
+            );
+        }
+    }
+
+    /**
+     * 포인트 충전 (동시성 안전)
+     * @param loginId 사용자 로그인 ID
+     * @param chargeAmount 충전할 금액
+     * @throws CoreException 충전량이 0 이하이거나, Point를 찾을 수 없는 경우
+     */
+    @Transactional
+    public void charge(String loginId, BigDecimal chargeAmount) {
+        // 충전량 유효성 검사
+        if (chargeAmount == null || chargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "포인트 충전량은 0보다 커야 합니다."
+            );
+        }
+
+        // QueryDSL로 동시성 안전하게 업데이트
+        long updatedRows = pointRepository.chargeAmount(loginId, chargeAmount);
+        
+        if (updatedRows == 0L) {
+            throw new CoreException(
+                    ErrorType.NOT_FOUND,
+                    "[loginId = " + loginId + "] 포인트를 충전할 Point 객체를 찾을 수 없습니다."
+            );
+        }
+    }
 }
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 d9bfe461c..df93f7093 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,7 +1,8 @@
 package com.loopers.domain.user;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.like.Like;
+import com.loopers.domain.coupon.Coupon;
+import com.loopers.domain.like.entity.Like;
 import com.loopers.domain.point.Point;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
@@ -11,6 +12,7 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @Entity
@@ -22,14 +24,18 @@ public class User extends BaseEntity {
     private String loginId;
     private String email;
     private String birthday;
+
     @Enumerated(EnumType.STRING)
     private Gender gender;
 
-    @OneToOne(mappedBy = "user")
-    private Point point;
+    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
+    private Point point = Point.builder().user(this).build();
+
+    @OneToMany(mappedBy = "user")
+    private List<Like> likes = new ArrayList<>();
 
     @OneToMany(mappedBy = "user")
-    private List<Like> likes;
+    private List<Coupon> coupons = new ArrayList<>();
 
     @Builder
     private User(
@@ -37,26 +43,14 @@ private User(
         , String email
         , String birthday
         , Gender gender
-        , Point point
-        , List<Like> likes
     ) {
         this.loginId = loginId;
         this.email = email;
         this.birthday = birthday;
         this.gender = gender;
-        this.point = point;
-        this.likes = likes;
         this.guard();
     }
 
-    // 포인트 필드를 세팅 (초기화 위해 있음)
-    public void setPoint(Point point) {
-        if(this.point != null) {
-            throw new CoreException(ErrorType.CONFLICT, "User : Point 가 이미 존재합니다.");
-        }
-        this.point = point;
-    }
-
     // 유효성 검사
     @Override
     protected void guard() {
@@ -85,13 +79,6 @@ protected void guard() {
         if(gender == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "User : gender가 NULL일 수 없습니다.");
         }
-        //throw new CoreException(ErrorType.BAD_REQUEST, "gender가 `MALE`, `FEMALE`, `OTHER` 형식에 맞아야 합니다.");
-        //this.gender = Gender.valueOf(gender.toUpperCase());
-
-        // point : NULL 이면 안 됨
-//        if(point == null) {
-//            throw new CoreException(ErrorType.BAD_REQUEST, "User : point가 비어있을 수 없습니다.");
-//        }
     }
 
 }
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 ad132b437..5df001b08 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 lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Optional;
 
@@ -11,10 +12,12 @@ public class UserService {
 
     private final UserRepository userRepository;
 
+    @Transactional
     public Optional<User> saveUser(User user) {
         return userRepository.save(user);
     }
 
+    @Transactional(readOnly = true)
     public Optional<User> findUserByLoginId(String loginId) {
         return userRepository.findByLoginId(loginId);
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
new file mode 100644
index 000000000..50731d1b1
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
@@ -0,0 +1,56 @@
+package com.loopers.infrastructure.point;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+
+import static com.loopers.domain.point.QPoint.point;
+
+@RequiredArgsConstructor
+@Component
+public class PointQueryRepository {
+
+    private final JPAQueryFactory queryFactory;
+
+    /**
+     * 포인트 차감 (동시성 안전)
+     * @param loginId 사용자 로그인 ID
+     * @param deductAmount 차감할 금액
+     * @return 업데이트된 행 수 (1이면 성공, 0이면 포인트 부족 또는 사용자 없음)
+     */
+    public long deduct(String loginId, BigDecimal deductAmount) {
+        if (deductAmount == null || deductAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return 0L;
+        }
+
+        return queryFactory
+                .update(point)
+                .set(point.amount, point.amount.subtract(deductAmount))
+                .where(
+                        point.user.loginId.eq(loginId)
+                        .and(point.amount.goe(deductAmount))  // 포인트가 충분한 경우만
+                )
+                .execute();
+    }
+
+    /**
+     * 포인트 충전 (동시성 안전)
+     * @param loginId 사용자 로그인 ID
+     * @param chargeAmount 충전할 금액
+     * @return 업데이트된 행 수 (1이면 성공, 0이면 사용자 없음)
+     */
+    public long chargeAmount(String loginId, BigDecimal chargeAmount) {
+        if (chargeAmount == null || chargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return 0L;
+        }
+
+        return queryFactory
+                .update(point)
+                .set(point.amount, point.amount.add(chargeAmount))
+                .where(point.user.loginId.eq(loginId))
+                .execute();
+    }
+}
+
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 81b0c56e2..e0012ed9f 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
@@ -5,6 +5,7 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
 import java.util.Optional;
 
 @RequiredArgsConstructor
@@ -12,7 +13,7 @@
 public class PointRepositoryImpl implements PointRepository {
 
     private final PointJpaRepository pointJpaRepository;
-
+    private final PointQueryRepository pointQueryRepository;
 
     @Override
     public Optional<Point> findByUser_loginId(String loginId) {
@@ -24,4 +25,14 @@ public Optional<Point> save(Point point) {
         Point savedPoint = pointJpaRepository.save(point);
         return Optional.of(savedPoint);
     }
+
+    @Override
+    public long deduct(String loginId, BigDecimal deductAmount) {
+        return pointQueryRepository.deduct(loginId, deductAmount);
+    }
+
+    @Override
+    public long chargeAmount(String loginId, BigDecimal chargeAmount) {
+        return pointQueryRepository.chargeAmount(loginId, chargeAmount);
+    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
new file mode 100644
index 000000000..ef02a7c0f
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
@@ -0,0 +1,151 @@
+package com.loopers.application.point;
+
+import com.loopers.application.user.UserFacade;
+import com.loopers.application.user.UserInfo;
+import com.loopers.domain.user.Gender;
+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.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("PointFacade 통합 테스트")
+@SpringBootTest
+public class PointFacadeTest {
+
+    @Autowired
+    private PointFacade pointFacade;
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    final String validLoginId = "bobby34";
+    final String validEmail = "bobby34@naver.com";
+    final String validBirthday = "1994-04-08";
+    final Gender validGender = Gender.MALE;
+
+    @DisplayName("getPoint 테스트")
+    @Nested
+    class GetPointTest {
+
+        @DisplayName("성공 케이스: 존재하는 User의 포인트 조회 성공")
+        @Test
+        void getPoint_withValidUser_Success() {
+            // arrange
+            UserInfo userInfo = UserInfo.builder()
+                    .loginId(validLoginId)
+                    .email(validEmail)
+                    .birthday(validBirthday)
+                    .gender(validGender)
+                    .build();
+            userFacade.saveUser(userInfo);
+
+            // act
+            PointInfo result = pointFacade.getPoint(validLoginId);
+
+            // assert
+            assertNotNull(result);
+            assertEquals(0, BigDecimal.ZERO.compareTo(result.amount()), "초기 포인트는 0이어야 함");
+        }
+
+        @DisplayName("실패 케이스: 존재하지 않는 User의 포인트 조회 시 NOT_FOUND 예외 발생")
+        @Test
+        void getPoint_withNonExistentUser_NotFound() {
+            // arrange
+            String nonExistentLoginId = "nonexistent";
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    pointFacade.getPoint(nonExistentLoginId)
+            );
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] Point를 찾을 수 없습니다."));
+        }
+    }
+
+    @DisplayName("charge 테스트")
+    @Nested
+    class ChargeTest {
+
+        @DisplayName("성공 케이스: 존재하는 User의 포인트 충전 성공")
+        @Test
+        void charge_withValidUser_Success() {
+            // arrange
+            UserInfo userInfo = UserInfo.builder()
+                    .loginId(validLoginId)
+                    .email(validEmail)
+                    .birthday(validBirthday)
+                    .gender(validGender)
+                    .build();
+            userFacade.saveUser(userInfo);
+
+            BigDecimal chargeAmount = BigDecimal.valueOf(1000);
+            BigDecimal expectedAmount = BigDecimal.valueOf(1000);
+
+            // act
+            PointInfo result = pointFacade.charge(validLoginId, chargeAmount);
+
+            // assert
+            assertNotNull(result);
+            assertEquals(0, expectedAmount.compareTo(result.amount()), "충전된 포인트가 일치해야 함");
+        }
+
+        @DisplayName("성공 케이스: 여러 번 충전 시 누적되어 저장됨")
+        @Test
+        void charge_multipleCharges_accumulates() {
+            // arrange
+            UserInfo userInfo = UserInfo.builder()
+                    .loginId(validLoginId)
+                    .email(validEmail)
+                    .birthday(validBirthday)
+                    .gender(validGender)
+                    .build();
+            userFacade.saveUser(userInfo);
+
+            BigDecimal firstCharge = BigDecimal.valueOf(1000);
+            BigDecimal secondCharge = BigDecimal.valueOf(2000);
+            BigDecimal expectedAmount = BigDecimal.valueOf(3000);
+
+            // act
+            pointFacade.charge(validLoginId, firstCharge);
+            PointInfo result = pointFacade.charge(validLoginId, secondCharge);
+
+            // assert
+            assertNotNull(result);
+            assertEquals(0, expectedAmount.compareTo(result.amount()), "충전된 포인트가 누적되어야 함");
+        }
+
+        @DisplayName("실패 케이스: 존재하지 않는 User의 포인트 충전 시 NOT_FOUND 예외 발생")
+        @Test
+        void charge_withNonExistentUser_NotFound() {
+            // arrange
+            String nonExistentLoginId = "nonexistent";
+            BigDecimal chargeAmount = BigDecimal.valueOf(1000);
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    pointFacade.charge(nonExistentLoginId, chargeAmount)
+            );
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("포인트를 충전할 Point 객체를 찾을 수 없습니다."));
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
similarity index 96%
rename from apps/commerce-api/src/test/java/com/loopers/domain/user/UserIntegrationTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
index 9130b38f6..172ff949a 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
@@ -1,7 +1,7 @@
-package com.loopers.domain.user;
+package com.loopers.application.user;
 
-import com.loopers.application.user.UserFacade;
-import com.loopers.application.user.UserInfo;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.UserService;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
@@ -18,7 +18,7 @@
 
 @DisplayName("User 통합 테스트")
 @SpringBootTest
-public class UserIntegrationTest {
+public class UserFacadeTest {
 
     @Autowired
     private UserFacade userFacade;
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointIntegrationTest.java
deleted file mode 100644
index afc3d11e7..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointIntegrationTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.loopers.domain.point;
-
-import com.loopers.application.point.PointFacade;
-import com.loopers.domain.user.Gender;
-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.math.BigDecimal;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-@DisplayName("Point 통합 테스트")
-@SpringBootTest
-public class PointIntegrationTest {
-
-    @Autowired
-    private PointFacade pointFacade;
-
-    @Autowired
-    private DatabaseCleanUp databaseCleanUp;
-
-    @AfterEach
-    void tearDown() {databaseCleanUp.truncateAllTables();}
-
-    final String validLoginId = "bobby34";
-    final String validEmail = "bobby34@naver.com";
-    final String validBirthday = "1994-04-08";
-    final Gender validGender = Gender.MALE;
-    final BigDecimal validPoint = BigDecimal.valueOf(0);
-
-    @DisplayName("Point 충전 테스트")
-    @Nested
-    class PointTest {
-
-        @DisplayName("실패 케이스 : 존재하지 않는 User ID 로 충전을 시도한 경우, 실패")
-        @Test
-        void addPoint_withNoUser_NotFound() {
-            // arrange
-            BigDecimal requestPoint = BigDecimal.valueOf(1000);
-
-            // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> pointFacade.charge(validLoginId,  requestPoint)
-            );
-
-            System.out.println("--- CoreException Details ---");
-            System.out.println("ErrorType: " + result.getErrorType());
-            System.out.println("CustomMessage: " + result.getCustomMessage());
-            System.out.println("------------------------");
-
-            // assert
-            assertEquals(ErrorType.NOT_FOUND, result.getErrorType());
-            assertTrue(result.getCustomMessage().endsWith("포인트를 충전할 Point 객체를 찾을 수 없습니다."));
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
new file mode 100644
index 000000000..3ee67e305
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
@@ -0,0 +1,377 @@
+package com.loopers.domain.point;
+
+import com.loopers.application.user.UserFacade;
+import com.loopers.application.user.UserInfo;
+import com.loopers.domain.user.Gender;
+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.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 java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Point Service 단위 테스트")
+@SpringBootTest
+class PointServiceTest {
+
+    @Autowired
+    private PointService pointService;
+
+    @Autowired
+    private PointRepository pointRepository;
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    private final String testLoginId = "bobby34";
+    private final BigDecimal initialPointAmount = BigDecimal.valueOf(100000);
+
+    @BeforeEach
+    void setUp() {
+        // 테스트용 User 생성
+        UserInfo userInfo = UserInfo.builder()
+                .loginId(testLoginId)
+                .email(testLoginId + "@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+        userFacade.saveUser(userInfo);
+
+        // 포인트 충전
+        pointService.charge(testLoginId, initialPointAmount);
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("charge 테스트")
+    @Nested
+    class ChargeTest {
+
+        @DisplayName("실패 케이스: 0 이하의 정수로 포인트를 충전 시 실패")
+        @Test
+        void charge_inputZero_BadRequest() {
+            // arrange
+            BigDecimal requestPoint = BigDecimal.valueOf(0);
+
+            // act
+            CoreException result = assertThrows(CoreException.class,
+                    () -> pointService.charge(testLoginId, requestPoint)
+            );
+
+            // assert
+            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
+            assertEquals("포인트 충전량은 0보다 커야 합니다.", result.getCustomMessage());
+        }
+
+        @DisplayName("실패 케이스: 음수로 포인트를 충전 시 실패")
+        @Test
+        void charge_inputBelowZero_BadRequest() {
+            // arrange
+            BigDecimal requestPoint = BigDecimal.valueOf(-10);
+
+            // act
+            CoreException result = assertThrows(CoreException.class,
+                    () -> pointService.charge(testLoginId, requestPoint)
+            );
+
+            // assert
+            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
+            assertEquals("포인트 충전량은 0보다 커야 합니다.", result.getCustomMessage());
+        }
+    }
+
+    @DisplayName("deduct 테스트")
+    @Nested
+    class DeductTest {
+
+        @DisplayName("성공 케이스: 정상적인 포인트 차감")
+        @Test
+        void deduct_validAmount_Success() {
+            // arrange
+            BigDecimal deductAmount = BigDecimal.valueOf(5000);
+
+            // act
+            pointService.deduct(testLoginId, deductAmount);
+
+            // assert
+            BigDecimal finalAmount = pointService.findByUserLoginId(testLoginId)
+                    .map(Point::getAmount)
+                    .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
+            BigDecimal expectedAmount = initialPointAmount.subtract(deductAmount);
+            assertEquals(0, finalAmount.compareTo(expectedAmount));
+        }
+
+        @DisplayName("실패 케이스: 0 이하의 정수로 포인트를 차감 시 실패")
+        @Test
+        void deduct_inputZero_BadRequest() {
+            // arrange
+            BigDecimal requestPoint = BigDecimal.valueOf(0);
+
+            // act
+            CoreException result = assertThrows(CoreException.class,
+                    () -> pointService.deduct(testLoginId, requestPoint)
+            );
+
+            // assert
+            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
+            assertEquals("포인트 차감량은 0보다 커야 합니다.", result.getCustomMessage());
+        }
+
+        @DisplayName("실패 케이스: 음수로 포인트를 차감 시 실패")
+        @Test
+        void deduct_inputBelowZero_BadRequest() {
+            // arrange
+            BigDecimal requestPoint = BigDecimal.valueOf(-10);
+
+            // act
+            CoreException result = assertThrows(CoreException.class,
+                    () -> pointService.deduct(testLoginId, requestPoint)
+            );
+
+            // assert
+            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
+            assertEquals("포인트 차감량은 0보다 커야 합니다.", result.getCustomMessage());
+        }
+
+        @DisplayName("실패 케이스: 포인트가 부족한 경우")
+        @Test
+        void deduct_insufficientPoint_BadRequest() {
+            // arrange
+            BigDecimal requestPoint = initialPointAmount.add(BigDecimal.valueOf(10000)); // 현재 포인트보다 많은 금액
+
+            // act
+            CoreException result = assertThrows(CoreException.class,
+                    () -> pointService.deduct(testLoginId, requestPoint)
+            );
+
+            // assert
+            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
+            assertTrue(result.getCustomMessage().contains("포인트가 부족합니다"));
+        }
+    }
+
+    @DisplayName("deduct 동시성 테스트")
+    @Nested
+    class DeductConcurrencyTest {
+
+        @DisplayName("성공 케이스: 여러 스레드가 동시에 같은 사용자의 포인트를 차감해도 정확히 차감됨")
+        @Test
+        void deduct_concurrentRequests_success() throws InterruptedException {
+            // arrange
+            int threadCount = 10;
+            BigDecimal deductAmountPerThread = BigDecimal.valueOf(5000);
+            BigDecimal expectedFinalPoint = initialPointAmount.subtract(
+                    deductAmountPerThread.multiply(BigDecimal.valueOf(threadCount))
+            );
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+            List<Exception> exceptions = new ArrayList<>();
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                        exceptions.add(e);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+                    .map(Point::getAmount)
+                    .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
+
+            assertEquals(0, finalPoint.compareTo(expectedFinalPoint),
+                    String.format("최종 포인트는 예상값과 일치해야 함: 예상=%s, 실제=%s", expectedFinalPoint, finalPoint));
+            assertEquals(threadCount, successCount.get(),
+                    String.format("성공한 스레드 수는 전체 스레드 수와 일치해야 함: 예상=%d, 실제=%d", threadCount, successCount.get()));
+            assertEquals(0, failureCount.get(),
+                    String.format("실패한 스레드 수는 0이어야 함: 실제=%d", failureCount.get()));
+            assertTrue(exceptions.isEmpty(),
+                    String.format("예외가 발생하지 않아야 함: 예외 개수=%d", exceptions.size()));
+        }
+
+        @DisplayName("성공 케이스: 포인트가 부족한 경우 일부만 성공하고 나머지는 실패함")
+        @Test
+        void deduct_insufficientPoint_partialSuccess() throws InterruptedException {
+            // arrange
+            int threadCount = 25; // 포인트 100000원에서 각 5000원씩 차감하면 최대 20개 가능
+            BigDecimal deductAmountPerThread = BigDecimal.valueOf(5000);
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+            List<Exception> exceptions = new ArrayList<>();
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                        exceptions.add(e);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+                    .map(Point::getAmount)
+                    .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
+
+            // 최대 20개 차감 가능 (100000 / 5000 = 20)
+            int maxPossibleDeductions = initialPointAmount.divide(deductAmountPerThread, RoundingMode.DOWN).intValue();
+            assertTrue(successCount.get() <= maxPossibleDeductions,
+                    String.format("성공한 차감 수는 최대 가능 차감 수 이하여야 함: 최대=%d, 실제=%d", maxPossibleDeductions, successCount.get()));
+            assertEquals(threadCount, successCount.get() + failureCount.get(),
+                    String.format("성공 수 + 실패 수는 전체 스레드 수와 일치해야 함: 예상=%d, 실제=%d", threadCount, successCount.get() + failureCount.get()));
+            assertTrue(finalPoint.compareTo(BigDecimal.ZERO) >= 0,
+                    String.format("최종 포인트는 0 이상이어야 함: 실제=%s", finalPoint));
+            assertTrue(finalPoint.compareTo(deductAmountPerThread) < 0,
+                    String.format("남은 포인트는 차감 단위보다 작아야 함: 차감 단위=%s, 남은 포인트=%s", deductAmountPerThread, finalPoint));
+
+            // 실패한 경우는 포인트 부족 예외여야 함
+            long pointInsufficientExceptions = exceptions.stream()
+                    .filter(e -> e instanceof CoreException)
+                    .filter(e -> ((CoreException) e).getErrorType() == ErrorType.BAD_REQUEST)
+                    .filter(e -> e.getMessage().contains("포인트가 부족"))
+                    .count();
+            assertEquals(failureCount.get(), pointInsufficientExceptions,
+                    String.format("포인트 부족 예외 수는 실패 수와 일치해야 함: 예상=%d, 실제=%d", failureCount.get(), pointInsufficientExceptions));
+        }
+
+        @DisplayName("성공 케이스: 포인트 차감이 원자적으로 처리되어 Lost Update가 발생하지 않음")
+        @Test
+        void deduct_atomicOperation_noLostUpdate() throws InterruptedException {
+            // arrange
+            int threadCount = 20;
+            BigDecimal deductAmountPerThread = BigDecimal.valueOf(1000);
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        // 실패는 무시 (포인트 부족 등)
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+                    .map(Point::getAmount)
+                    .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
+
+            // Lost Update가 발생하지 않았는지 확인
+            // 성공한 차감 수만큼 정확히 차감되어야 함
+            BigDecimal expectedPoint = initialPointAmount.subtract(
+                    deductAmountPerThread.multiply(BigDecimal.valueOf(successCount.get()))
+            );
+            assertEquals(0, finalPoint.compareTo(expectedPoint),
+                    String.format("Lost Update가 발생하지 않아야 함: 예상 포인트=%s, 실제 포인트=%s, 성공한 차감 수=%d", expectedPoint, finalPoint, successCount.get()));
+        }
+
+        @DisplayName("성공 케이스: 동시에 여러 스레드가 포인트를 차감해도 음수가 되지 않음")
+        @Test
+        void deduct_concurrentRequests_noNegativePoint() throws InterruptedException {
+            // arrange
+            int threadCount = 25; // 포인트 100000원에서 각 5000원씩 차감하면 최대 20개 가능
+            BigDecimal deductAmountPerThread = BigDecimal.valueOf(5000);
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+                    .map(Point::getAmount)
+                    .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
+
+            // 포인트가 음수가 되지 않아야 함
+            assertTrue(finalPoint.compareTo(BigDecimal.ZERO) >= 0,
+                    String.format("포인트가 음수가 되지 않아야 함: 실제 포인트=%s", finalPoint));
+            // 성공한 차감 수만큼 정확히 차감되어야 함
+            BigDecimal expectedPoint = initialPointAmount.subtract(
+                    deductAmountPerThread.multiply(BigDecimal.valueOf(successCount.get()))
+            );
+            assertEquals(0, finalPoint.compareTo(expectedPoint),
+                    String.format("성공한 차감 수만큼 정확히 차감되어야 함: 예상 포인트=%s, 실제 포인트=%s, 성공한 차감 수=%d", expectedPoint, finalPoint, successCount.get()));
+        }
+    }
+}
+
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 42ee4ebcb..4dac0d163 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,6 +1,7 @@
 package com.loopers.domain.point;
 
 import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import org.junit.jupiter.api.DisplayName;
@@ -14,149 +15,126 @@
 @DisplayName("Point 테스트")
 public class PointTest {
 
-    @DisplayName("point 충전 테스트")
-    @Nested
-    class AddPointTest {
-
-        final String validLoginId = "bobby34";
-        final String validEmail = "bobby34@naver.com";
-        final String validBirthday = "1994-04-08";
-        final Gender validGender = Gender.MALE;
-        final BigDecimal validPoint = BigDecimal.valueOf(0);
-
-        @DisplayName("실패 케이스 : 0 이하의 정수로 포인트를 충전 시 실패")
-        @Test
-        void charge_inputZero_BadRequest() {
-            // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            BigDecimal requestPoint = BigDecimal.valueOf(0);
-
-            // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> point.charge(requestPoint)
-            );
-
-            // assert
-            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertEquals("충전할 point는 0 이하가 될 수 없습니다.", result.getCustomMessage());
-        }
-
-        @DisplayName("실패 케이스 : 음수로 포인트를 충전 시 실패")
-        @Test
-        void charge_inputBelowZero_BadRequest() {
-            // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            BigDecimal requestPoint = BigDecimal.valueOf(-10);
-
-            // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> point.charge(requestPoint)
-            );
-
-            // assert
-            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertEquals("충전할 point는 0 이하가 될 수 없습니다.", result.getCustomMessage());
-        }
+    private User createValidUser() {
+        return User.builder()
+                .loginId("testuser1")
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
     }
 
-    @DisplayName("point 차감 테스트")
+    @DisplayName("Point 엔티티 생성")
     @Nested
-    class DeductPointTest {
-
-        final String validLoginId = "bobby34";
-        final String validEmail = "bobby34@naver.com";
-        final String validBirthday = "1994-04-08";
-        final Gender validGender = Gender.MALE;
-        final BigDecimal validPoint = BigDecimal.valueOf(10000);
+    class CreatePointTest {
 
-        @DisplayName("성공 케이스 : 정상적인 포인트 차감")
+        @DisplayName("성공 케이스: 필드가 모두 유효하면 Point 객체 생성 성공")
         @Test
-        void deduct_validAmount_Success() {
+        void createPoint_withValidFields_Success() {
             // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            BigDecimal deductAmount = BigDecimal.valueOf(5000);
+            User user = createValidUser();
 
             // act
-            BigDecimal result = point.deduct(deductAmount);
-
-            // assert
-            assertEquals(BigDecimal.valueOf(5000), result);
-            assertEquals(BigDecimal.valueOf(5000), point.getAmount());
-        }
-
-        @DisplayName("실패 케이스 : 0 이하의 정수로 포인트를 차감 시 실패")
-        @Test
-        void deduct_inputZero_BadRequest() {
-            // arrange
             Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
+                    .user(user)
                     .build();
 
-            BigDecimal requestPoint = BigDecimal.valueOf(0);
-
-            // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> point.deduct(requestPoint)
-            );
-
             // assert
-            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertEquals("차감할 point는 0 이하가 될 수 없습니다.", result.getCustomMessage());
+            assertNotNull(point);
+            assertAll(
+                    () -> assertEquals(BigDecimal.ZERO, point.getAmount(), "amount는 필드 초기화로 BigDecimal.ZERO가 기본값"),
+                    () -> assertEquals(user, point.getUser())
+            );
         }
 
-        @DisplayName("실패 케이스 : 음수로 포인트를 차감 시 실패")
+        @DisplayName("실패 케이스: user가 null이면 예외 발생")
         @Test
-        void deduct_inputBelowZero_BadRequest() {
-            // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            BigDecimal requestPoint = BigDecimal.valueOf(-10);
-
-            // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> point.deduct(requestPoint)
+        void createPoint_withNullUser_ThrowsException() {
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Point.builder()
+                            .user(null)
+                            .build()
             );
 
-            // assert
-            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertEquals("차감할 point는 0 이하가 될 수 없습니다.", result.getCustomMessage());
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("user가 Null 이 되면 안 됩니다"));
         }
 
-        @DisplayName("실패 케이스 : 포인트가 부족한 경우")
+        // @DisplayName("실패 케이스: amount가 null이면 예외 발생")
+        // @Test
+        // void createPoint_withNullAmount_ThrowsException() {
+        //     // arrange
+        //     User user = createValidUser();
+        //     Point point = Point.builder()
+        //             .user(user)
+        //             .build();
+
+        //     // 리플렉션을 사용하여 amount를 null로 설정
+        //     try {
+        //         java.lang.reflect.Field amountField = Point.class.getDeclaredField("amount");
+        //         amountField.setAccessible(true);
+        //         amountField.set(point, null);
+        //     } catch (Exception e) {
+        //         throw new RuntimeException("amount 필드 설정 실패", e);
+        //     }
+
+        //     // act & assert
+        //     CoreException exception = assertThrows(CoreException.class, () -> {
+        //         // guard()를 리플렉션으로 호출하여 검증
+        //         java.lang.reflect.Method guardMethod = point.getClass().getSuperclass().getDeclaredMethod("guard");
+        //         guardMethod.setAccessible(true);
+        //         guardMethod.invoke(point);
+        //     });
+
+        //     assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+        //     assertEquals(exception.getCustomMessage(), "Point : amount가 Null 이 되면 안 됩니다.");
+        // }
+
+        // @DisplayName("실패 케이스: amount가 음수이면 예외 발생")
+        // @Test
+        // void createPoint_withNegativeAmount_ThrowsException() {
+        //     // arrange
+        //     User user = createValidUser();
+        //     Point point = Point.builder()
+        //             .user(user)
+        //             .build();
+
+        //     // 리플렉션을 사용하여 amount를 음수로 설정
+        //     try {
+        //         java.lang.reflect.Field amountField = Point.class.getDeclaredField("amount");
+        //         amountField.setAccessible(true);
+        //         amountField.set(point, BigDecimal.valueOf(-1000));
+        //     } catch (Exception e) {
+        //         throw new RuntimeException("amount 필드 설정 실패", e);
+        //     }
+
+        //     // act & assert
+        //     CoreException exception = assertThrows(CoreException.class, () -> {
+        //         // guard()를 리플렉션으로 호출하여 검증
+        //         java.lang.reflect.Method guardMethod = point.getClass().getSuperclass().getDeclaredMethod("guard");
+        //         guardMethod.setAccessible(true);
+        //         guardMethod.invoke(point);
+        //     });
+
+        //     assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+        //     assertTrue(exception.getCustomMessage().contains("amount는 음수가 될 수 없습니다"));
+        // }
+
+        @DisplayName("성공 케이스: amount가 0이면 Point 객체 생성 성공")
         @Test
-        void deduct_insufficientPoint_BadRequest() {
+        void createPoint_withZeroAmount_Success() {
             // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            BigDecimal requestPoint = BigDecimal.valueOf(20000); // 현재 포인트보다 많은 금액
+            User user = createValidUser();
 
             // act
-            CoreException result = assertThrows(CoreException.class,
-                    () -> point.deduct(requestPoint)
-            );
+            Point point = Point.builder()
+                    .user(user)
+                    .build();
 
             // assert
-            assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertTrue(result.getCustomMessage().contains("포인트가 부족합니다"));
+            assertNotNull(point);
+            assertEquals(BigDecimal.ZERO, point.getAmount());
         }
     }
 }
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 09ec0c4a6..bbc5f8157 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,6 +1,5 @@
 package com.loopers.domain.user;
 
-import com.loopers.domain.point.Point;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import org.junit.jupiter.api.DisplayName;
@@ -27,19 +26,13 @@ class CreateUserTest {
         @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 User 객체 생성 성공")
         @Test
         void createUser_withValidFields_Success() {
-            // arrange
-            Point point = Point.builder()
-                    .user(null)
-                    .amount(validPoint)
-                    .build();
-
-            // act
+            // arrange & act
+            // User 생성 시 Point가 자동으로 생성됨 (필드 초기화)
             User user = User.builder()
                     .loginId(validLoginId)
                     .email(validEmail)
                     .birthday(validBirthday)
                     .gender(validGender)
-                    .point(point)
                     .build();
 
             // assert
@@ -49,7 +42,8 @@ void createUser_withValidFields_Success() {
                     , () -> assertEquals(user.getEmail(), validEmail)
                     , () -> assertEquals(user.getBirthday(), validBirthday)
                     , () -> assertEquals(user.getGender(), validGender)
-                    , () -> assertEquals(user.getPoint().getAmount(), validPoint)
+                    , () -> assertNotNull(user.getPoint(), "Point가 자동으로 생성되어야 함")
+                    , () -> assertEquals(user.getPoint().getAmount(), validPoint, "Point의 초기 amount는 0이어야 함")
             );
         }
 

From 5d3a18d53a419fbd0f68919bd746b10f87feaf90 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 02:23:20 +0900
Subject: [PATCH 02/17] =?UTF-8?q?[Round=204]=20=EC=83=81=ED=92=88=20?=
 =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/application/like/LikeInfo.java    |  15 +-
 .../application/product/ProductFacade.java    |  15 +-
 .../loopers/domain/brand/BrandService.java    |   2 +
 .../loopers/domain/like/LikeRepository.java   |   3 +
 .../com/loopers/domain/like/LikeService.java  |   7 +-
 .../domain/like/{ => entity}/Like.java        |  56 ++--
 .../loopers/domain/like/entity/LikeId.java    |  34 +++
 .../like/{ => entity}/LikeTargetType.java     |   2 +-
 .../com/loopers/domain/product/Product.java   |  30 +-
 .../domain/product/ProductService.java        |   5 +
 .../java/com/loopers/domain/stock/Stock.java  |  18 +-
 .../loopers/domain/stock/StockRepository.java |   3 +-
 .../loopers/domain/stock/StockService.java    |  35 ++-
 .../like/LikeJpaRepository.java               |  12 +-
 .../like/LikeQueryRepository.java             |  28 +-
 .../like/LikeRepositoryImpl.java              |   7 +-
 .../product/ProductQueryRepository.java       |  32 +-
 .../stock/StockQueryRepository.java           |  20 +-
 .../stock/StockRepositoryImpl.java            |  10 +-
 ...tegrationTest.java => LikeFacadeTest.java} | 111 +++++--
 ...rationTest.java => ProductFacadeTest.java} |  26 +-
 .../com/loopers/domain/like/LikeTest.java     |  39 +--
 .../loopers/domain/product/ProductTest.java   |  79 +----
 .../domain/stock/StockServiceTest.java        | 286 ++++++++++++++++++
 .../com/loopers/domain/stock/StockTest.java   | 123 ++++++++
 .../java/com/loopers/domain/AuditEntity.java  |  66 ++++
 .../java/com/loopers/domain/BaseEntity.java   |   2 +-
 27 files changed, 815 insertions(+), 251 deletions(-)
 rename apps/commerce-api/src/main/java/com/loopers/domain/like/{ => entity}/Like.java (51%)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
 rename apps/commerce-api/src/main/java/com/loopers/domain/like/{ => entity}/LikeTargetType.java (52%)
 rename apps/commerce-api/src/test/java/com/loopers/application/like/{LikeFacadeIntegrationTest.java => LikeFacadeTest.java} (74%)
 rename apps/commerce-api/src/test/java/com/loopers/application/product/{ProductFacadeIntegrationTest.java => ProductFacadeTest.java} (88%)
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java
 create mode 100644 modules/jpa/src/main/java/com/loopers/domain/AuditEntity.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java
index 5cab07650..399c11ab6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java
@@ -1,22 +1,21 @@
 package com.loopers.application.like;
 
-import com.loopers.domain.like.Like;
-import com.loopers.domain.like.LikeTargetType;
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeTargetType;
+
 import lombok.Builder;
 
 @Builder
 public record LikeInfo(
-        Long id
-        , Long userId
+        Long userId
         , Long likeTargetId
         , LikeTargetType likeTargetType
 ) {
     public static LikeInfo from(Like like) {
         return LikeInfo.builder()
-                .id(like.getId())
-                .userId(like.getUser().getId())
-                .likeTargetId(like.getLikeTargetId())
-                .likeTargetType(like.getLikeTargetType())
+                .userId(like.getLikeId().getUserId())
+                .likeTargetId(like.getLikeId().getLikeTargetId())
+                .likeTargetType(like.getLikeId().getLikeTargetType())
                 .build();
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
index d53ee5faa..0af649e42 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
@@ -3,7 +3,13 @@
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductCondition;
 import com.loopers.domain.product.ProductService;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
 import lombok.RequiredArgsConstructor;
+
+import java.util.Optional;
+
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
@@ -30,8 +36,12 @@ public Page<ProductInfo> getProducts(ProductCondition condition, Pageable pageab
      */
     @Transactional(readOnly = true)
     public ProductInfo getProduct(Long productId) {
-        ProductService.ProductWithBrand productWithBrand = productService.getProductDetailWithBrand(productId);
-        return ProductInfo.from(productWithBrand);
+        Product product = productService.findById(productId)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.NOT_FOUND,
+                        "[productId = " + productId + "] Product를 찾을 수 없습니다."
+                ));
+        return ProductInfo.from(product);
     }
 
     /**
@@ -44,7 +54,6 @@ public ProductInfo saveProduct(ProductInfo productInfo) {
                 .name(productInfo.name())
                 .description(productInfo.description())
                 .price(productInfo.price())
-                .likeCount(productInfo.likeCount())
                 .status(productInfo.status())
                 .isVisible(productInfo.isVisible())
                 .isSellable(productInfo.isSellable())
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
index d9222ac16..3fbf3bcf6 100644
--- 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
@@ -4,6 +4,7 @@
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @Component
 @RequiredArgsConstructor
@@ -14,6 +15,7 @@ public class BrandService {
     /**
      * 브랜드 조회
      */
+    @Transactional(readOnly = true)
     public Brand findById(Long brandId) {
         return brandRepository.findById(brandId)
                 .orElseThrow(() -> new CoreException(
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
index d3870a120..1ecb246ec 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
@@ -2,6 +2,9 @@
 
 import java.util.Optional;
 
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeTargetType;
+
 public interface LikeRepository {
 
     Optional<Like> save(Like like);
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
index abd5baaaf..91104a733 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
@@ -1,9 +1,11 @@
 package com.loopers.domain.like;
 
+import com.loopers.domain.like.entity.LikeTargetType;
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.user.User;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @Component
 @RequiredArgsConstructor
@@ -17,13 +19,11 @@ public class LikeService {
      * 멱등성: 중복 저장하면 이전 것을 덮어씀
      * 동시성 안전: INSERT ... ON DUPLICATE KEY UPDATE를 사용하여 원자적으로 처리
      */
+    @Transactional
     public void saveProductLike(User user, Long productId) {
         // INSERT ... ON DUPLICATE KEY UPDATE 실행
         // 반환값: 1 = 새로 생성됨, 2 = 이미 존재하여 UPDATE만 됨
         long affectedRows = likeRepository.insertOrIgnore(user.getId(), productId, LikeTargetType.PRODUCT);
-
-        // 디버깅: 실제 반환값 확인
-        System.out.println("insertOrIgnore 반환값: " + affectedRows);
         
         // 실제로 새로 생성된 경우에만 likeCount 증가
         if (affectedRows == 1) {
@@ -35,6 +35,7 @@ public void saveProductLike(User user, Long productId) {
      * 상품 좋아요 취소
      * 멱등성: Like가 없어도 예외 없이 처리
      */
+    @Transactional
     public void deleteProductLike(User user, Long productId) {
         // 삭제 실행 (영향받은 행 수 반환)
         long deletedRows = likeRepository.deleteByUserIdAndLikeTargetId(
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
similarity index 51%
rename from apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java
rename to apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
index 31513f34f..fa0fc3c89 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
@@ -1,6 +1,6 @@
-package com.loopers.domain.like;
+package com.loopers.domain.like.entity;
 
-import com.loopers.domain.BaseEntity;
+import com.loopers.domain.AuditEntity;
 import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
@@ -11,41 +11,45 @@
 import lombok.NoArgsConstructor;
 
 @Entity
-@Table(
-    name = "user_like",
-    // 💡 UNIQUE 인덱스를 @Index로 정의하는 것이 더 명확하고 유연할 수 있습니다.
-    indexes = {
-        @Index(
-            name = "uk_user_like_target", 
-            columnList = "user_id, like_target_id, like_target_type", 
-            unique = true // 명시적으로 UNIQUE 지정
-        )
-    }
-)
+@Table(name = "user_like")
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Getter
-public class Like extends BaseEntity {
+public class Like extends AuditEntity {
+
+    @EmbeddedId
+    private LikeId likeId;
 
+    @MapsId("userId")
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "user_id")
     private User user;
 
-    @Column(name = "like_target_id")
-    private Long likeTargetId;
-
-    @Enumerated(EnumType.STRING)
-    @Column(name = "like_target_type")
-    private LikeTargetType likeTargetType;
-
     @Builder
     private Like(
         User user
         , Long likeTargetId
         , LikeTargetType likeTargetType
     ) {
+        // 입력값 선검증으로 NPE 방지 및 메시지 일관화
+        if (user == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : user가 비어있을 수 없습니다.");
+        }
+        if (likeTargetId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId가 비어있을 수 없습니다.");
+        }
+        if (likeTargetId <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId는 양수여야 합니다.");
+        }
+        if (likeTargetType == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetType이 비어있을 수 없습니다.");
+        }
+
         this.user = user;
-        this.likeTargetId = likeTargetId;
-        this.likeTargetType = likeTargetType;
+        this.likeId = LikeId.builder()
+            .userId(user.getId())
+            .likeTargetId(likeTargetId)
+            .likeTargetType(likeTargetType)
+            .build();
         guard();
     }
 
@@ -58,14 +62,14 @@ protected void guard() {
         }
 
         // likeTargetId 유효성 검사
-        if(likeTargetId == null) {
+        if(likeId.getLikeTargetId() == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId가 비어있을 수 없습니다.");
-        } else if(likeTargetId <= 0) {
+        } else if(likeId.getLikeTargetId() <= 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId는 양수여야 합니다.");
         }
 
         // likeTargetType 유효성 검사
-        if(likeTargetType == null) {
+        if(likeId.getLikeTargetType() == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetType이 비어있을 수 없습니다.");
         }
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
new file mode 100644
index 000000000..f95f5759e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
@@ -0,0 +1,34 @@
+package com.loopers.domain.like.entity;
+
+import java.io.Serializable;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Embeddable
+@NoArgsConstructor
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Getter
+@EqualsAndHashCode
+@Builder
+public class LikeId implements Serializable {
+    
+    @Column(name = "user_id")
+    private Long userId;
+
+    @Column(name = "like_target_id")
+    private Long likeTargetId;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "like_target_type")
+    private LikeTargetType likeTargetType;
+
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeTargetType.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeTargetType.java
similarity index 52%
rename from apps/commerce-api/src/main/java/com/loopers/domain/like/LikeTargetType.java
rename to apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeTargetType.java
index 6a4ff61eb..774b346b5 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeTargetType.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeTargetType.java
@@ -1,4 +1,4 @@
-package com.loopers.domain.like;
+package com.loopers.domain.like.entity;
 
 public enum LikeTargetType {
     PRODUCT
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
index bbcfb7c2e..64ba6b83d 100644
--- 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
@@ -22,7 +22,7 @@ public class Product extends BaseEntity {
     private String name;
     private String description;
     private BigDecimal price;
-    private Long likeCount;
+    private Long likeCount = 0L;
     @Enumerated(EnumType.STRING)
     private ProductStatus status;
     private Boolean isVisible;
@@ -32,49 +32,34 @@ public class Product extends BaseEntity {
     @JoinColumn(name = "brand_id")
     private Brand brand;
 
-    @OneToOne(mappedBy = "product")
-    private Stock stock;
+    @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
+    private Stock stock = Stock.builder().product(this).quantity(0L).build();
 
     @Builder
     private Product(
         String name
         , String description
         , BigDecimal price
-        , Long likeCount
         , ProductStatus status
         , Boolean isVisible
         , Boolean isSellable
         , Brand brand
-        , Stock stock
     ) {
         this.name = name;
         this.description = description;
         this.price = price;
-        this.likeCount = likeCount;
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
         this.brand = brand;
-        this.stock = stock;
         guard();
     }
 
-    // 브랜드 필드를 세팅 (초기화 위해 있음)
+    // 브랜드 필드를 세팅
     public void setBrand(Brand brand) {
-        if(this.brand != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Product : Brand 가 이미 존재합니다.");
-        }
         this.brand = brand;
     }
 
-    // 재고 필드를 세팅 (초기화 위해 있음)
-    public void setStock(Stock stock) {
-        if(this.stock != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Product : Stock 가 이미 존재합니다.");
-        }
-        this.stock = stock;
-    }
-
     // 유효성 검사
     @Override
     protected void guard() {
@@ -113,5 +98,12 @@ protected void guard() {
         if(isSellable == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Product : isSellable 가 비어있을 수 없습니다.");
         }
+
+        // stock 유효성 검사
+        if(stock == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Product : stock 가 비어있을 수 없습니다.");
+        }
+
+        // brand nullable
     }
 }
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
index f4c3f2213..3e6a5968e 100644
--- 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
@@ -7,6 +7,7 @@
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Optional;
 
@@ -16,14 +17,17 @@ public class ProductService {
 
     private final ProductRepository productRepository;
 
+    @Transactional
     public Optional<Product> saveProduct(Product product) {
         return productRepository.save(product);
     }
 
+    @Transactional(readOnly = true)
     public Optional<Product> findById(Long productId) {
         return productRepository.findById(productId);
     }
 
+    @Transactional(readOnly = true)
     public Page<Product> findProducts(ProductCondition condition, Pageable pageable) {
         return productRepository.findProducts(condition, pageable);
     }
@@ -33,6 +37,7 @@ public Page<Product> findProducts(ProductCondition condition, Pageable pageable)
      * 도메인 서비스에서 Product + Brand 조합 로직 처리
      * Repository에서 fetch join을 통해 Brand를 함께 조회
      */
+    @Transactional(readOnly = true)
     public ProductWithBrand getProductDetailWithBrand(Long productId) {
         Product product = productRepository.findByIdWithBrand(productId)
                 .orElseThrow(() -> new CoreException(
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
index f7c4e4e0f..33fb25091 100644
--- 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
@@ -29,16 +29,11 @@ private Stock(
     ) {
         this.quantity = quantity;
         this.product = product;
+        
+        // 유효성 검사
+        guard();
     }
-
-    // 상품 필드를 세팅 (초기화 위해 있음)
-    public void setProduct(Product product) {
-        if(this.product != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Stock : Product 가 이미 존재합니다.");
-        }
-        this.product = product;
-    }
-
+    
     // 유효성 검사
     @Override
     protected void guard() {
@@ -48,6 +43,11 @@ protected void guard() {
         } else if(quantity < 0L) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Stock : quantity 는 음수가 될 수 없습니다.");
         }
+
+        // product 유효성 검사
+        if(product == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Stock : product 가 비어있을 수 없습니다.");
+        }
     }
 
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockRepository.java
index c43b0a1da..81f8ade11 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockRepository.java
@@ -5,6 +5,7 @@
 public interface StockRepository {
     Optional<Stock> findByProductId(Long productId);
     Optional<Stock> save(Stock stock);
-    boolean decreaseQuantity(Long productId, Long decreaseQuantity);
+    long decreaseQuantity(Long productId, Long decreaseQuantity);
+    long increaseQuantity(Long productId, Long increaseQuantity);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockService.java b/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockService.java
index 76196feb5..ec549ce0b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/stock/StockService.java
@@ -4,6 +4,7 @@
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Optional;
 
@@ -13,10 +14,12 @@ public class StockService {
 
     private final StockRepository stockRepository;
 
+    @Transactional(readOnly = true)
     public Optional<Stock> findByProductId(Long productId) {
         return stockRepository.findByProductId(productId);
     }
 
+    @Transactional
     public Optional<Stock> saveStock(Stock stock) {
         return stockRepository.save(stock);
     }
@@ -27,6 +30,7 @@ public Optional<Stock> saveStock(Stock stock) {
      * @param decreaseQuantity 감소할 수량
      * @throws CoreException 감소량이 0 이하이거나, 재고가 부족한 경우
      */
+    @Transactional
     public void decreaseQuantity(Long productId, Long decreaseQuantity) {
         // 감소량 유효성 검사
         if (decreaseQuantity == null || decreaseQuantity <= 0L) {
@@ -37,9 +41,9 @@ public void decreaseQuantity(Long productId, Long decreaseQuantity) {
         }
 
         // QueryDSL로 동시성 안전하게 업데이트
-        boolean success = stockRepository.decreaseQuantity(productId, decreaseQuantity);
+        long updatedRows = stockRepository.decreaseQuantity(productId, decreaseQuantity);
         
-        if (!success) {
+        if (updatedRows == 0L) {
             // 재고 조회하여 상세 메시지 제공
             Stock stock = stockRepository.findByProductId(productId)
                     .orElseThrow(() -> new CoreException(
@@ -53,5 +57,32 @@ public void decreaseQuantity(Long productId, Long decreaseQuantity) {
             );
         }
     }
+
+    /**
+     * 재고 증가 (동시성 안전)
+     * @param productId 상품 ID
+     * @param increaseQuantity 증가할 수량
+     * @throws CoreException 증가량이 0 이하이거나, Stock을 찾을 수 없는 경우
+     */
+    @Transactional
+    public void increaseQuantity(Long productId, Long increaseQuantity) {
+        // 증가량 유효성 검사
+        if (increaseQuantity == null || increaseQuantity <= 0L) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "재고 증가량은 0보다 커야 합니다."
+            );
+        }
+
+        // QueryDSL로 동시성 안전하게 업데이트
+        long updatedRows = stockRepository.increaseQuantity(productId, increaseQuantity);
+        
+        if (updatedRows == 0L) {
+            throw new CoreException(
+                    ErrorType.NOT_FOUND,
+                    "[productId = " + productId + "] 재고를 증가할 Stock 객체를 찾을 수 없습니다."
+            );
+        }
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
index 76bae4567..290a3c8d9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
@@ -1,13 +1,15 @@
 package com.loopers.infrastructure.like;
 
-import com.loopers.domain.like.Like;
-import com.loopers.domain.like.LikeTargetType;
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeId;
+import com.loopers.domain.like.entity.LikeTargetType;
+
 import org.springframework.data.jpa.repository.JpaRepository;
 
 import java.util.Optional;
 
-public interface LikeJpaRepository extends JpaRepository<Like, Long> {
-    Optional<Like> findByUser_IdAndLikeTargetIdAndLikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
-    void deleteByUser_IdAndLikeTargetIdAndLikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
+public interface LikeJpaRepository extends JpaRepository<Like, LikeId> {
+    Optional<Like> findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
+    void deleteByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
index 7040f8751..dc7aadcac 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
@@ -1,13 +1,18 @@
 package com.loopers.infrastructure.like;
 
+import com.loopers.domain.like.entity.LikeTargetType;
+import com.querydsl.jpa.impl.JPAQueryFactory;
 import jakarta.persistence.EntityManager;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
+import static com.loopers.domain.like.entity.QLike.like;
+
 @RequiredArgsConstructor
 @Component
 public class LikeQueryRepository {
 
+    private final JPAQueryFactory queryFactory;
     private final EntityManager entityManager;
 
     /**
@@ -22,9 +27,8 @@ public class LikeQueryRepository {
     public long insertOrIgnore(Long userId, Long likeTargetId, String likeTargetType) {
 
         String sql = """
-            INSERT INTO user_like (user_id, like_target_id, like_target_type, created_at, updated_at)
+            INSERT IGNORE INTO user_like (user_id, like_target_id, like_target_type, created_at, updated_at)
             VALUES (:userId, :likeTargetId, :likeTargetType, NOW(), NOW())
-            ON DUPLICATE KEY UPDATE updated_at = NOW()
             """;
 
         return entityManager.createNativeQuery(sql)
@@ -43,18 +47,14 @@ INSERT INTO user_like (user_id, like_target_id, like_target_type, created_at, up
      * @return 영향받은 행 수 (0: 삭제할 데이터 없음, 1: 삭제 성공)
      */
     public long deleteByUserIdAndLikeTargetId(Long userId, Long likeTargetId, String likeTargetType) {
-        String sql = """
-            DELETE FROM user_like 
-            WHERE user_id = :userId 
-            AND like_target_id = :likeTargetId 
-            AND like_target_type = :likeTargetType
-            """;
-
-        return entityManager.createNativeQuery(sql)
-                .setParameter("userId", userId)
-                .setParameter("likeTargetId", likeTargetId)
-                .setParameter("likeTargetType", likeTargetType)
-                .executeUpdate();
+        LikeTargetType targetType = LikeTargetType.valueOf(likeTargetType);
+        
+        return queryFactory
+                .delete(like)
+                .where(like.likeId.userId.eq(userId)
+                        .and(like.likeId.likeTargetId.eq(likeTargetId))
+                        .and(like.likeId.likeTargetType.eq(targetType)))
+                .execute();
     }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
index a386f9025..ab9fca2ed 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
@@ -1,8 +1,9 @@
 package com.loopers.infrastructure.like;
 
-import com.loopers.domain.like.Like;
 import com.loopers.domain.like.LikeRepository;
-import com.loopers.domain.like.LikeTargetType;
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeTargetType;
+
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
@@ -28,7 +29,7 @@ public long deleteByUserIdAndLikeTargetId(Long userId, Long likeTargetId, LikeTa
 
     @Override
     public Optional<Like> findByUserIdAndLikeTargetId(Long userId, Long likeTargetId, LikeTargetType likeTargetType) {
-        return likeJpaRepository.findByUser_IdAndLikeTargetIdAndLikeTargetType(userId, likeTargetId, likeTargetType);
+        return likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(userId, likeTargetId, likeTargetType);
     }
 
     @Override
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
index a4c04a9a3..2c967c4b9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
@@ -6,7 +6,6 @@
 import com.querydsl.core.BooleanBuilder;
 import com.querydsl.core.types.OrderSpecifier;
 import com.querydsl.jpa.impl.JPAQueryFactory;
-import jakarta.persistence.EntityManager;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -22,7 +21,6 @@
 public class ProductQueryRepository {
 
     private final JPAQueryFactory queryFactory;
-    private final EntityManager entityManager;
     private final QProduct product = QProduct.product;
 
     public Page<Product> findProducts(ProductCondition condition, Pageable pageable) {
@@ -78,15 +76,11 @@ public Page<Product> findProducts(ProductCondition condition, Pageable pageable)
      * @param productId 상품 ID
      */
     public void incrementLikeCount(Long productId) {
-        String sql = """
-            UPDATE product 
-            SET like_count = like_count + 1 
-            WHERE id = :productId
-            """;
-
-        entityManager.createNativeQuery(sql)
-                .setParameter("productId", productId)
-                .executeUpdate();
+        queryFactory
+                .update(product)
+                .set(product.likeCount, product.likeCount.add(1L))
+                .where(product.id.eq(productId))
+                .execute();
     }
 
     /**
@@ -96,16 +90,12 @@ public void incrementLikeCount(Long productId) {
      * @param productId 상품 ID
      */
     public void decrementLikeCount(Long productId) {
-        String sql = """
-            UPDATE product 
-            SET like_count = like_count - 1 
-            WHERE id = :productId 
-            AND like_count > 0
-            """;
-
-        entityManager.createNativeQuery(sql)
-                .setParameter("productId", productId)
-                .executeUpdate();
+        queryFactory
+                .update(product)
+                .set(product.likeCount, product.likeCount.subtract(1L))
+                .where(product.id.eq(productId)
+                        .and(product.likeCount.gt(0L)))
+                .execute();
     }
 
     /**
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
index e1bcde8d2..b1c03c8fe 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
@@ -28,9 +28,27 @@ public long decreaseQuantity(Long productId, Long decreaseQuantity) {
                 .set(stock.quantity, stock.quantity.subtract(decreaseQuantity))
                 .where(
                         stock.product.id.eq(productId)
-                                .and(stock.quantity.goe(decreaseQuantity))  // 재고가 충분한 경우만
+                        .and(stock.quantity.goe(decreaseQuantity))  // 재고가 충분한 경우만
                 )
                 .execute();
     }
+
+    /**
+     * 재고 증가 (동시성 안전)
+     * @param productId 상품 ID
+     * @param increaseQuantity 증가할 수량
+     * @return 업데이트된 행 수 (1이면 성공, 0이면 Stock을 찾을 수 없음)
+     */
+    public long increaseQuantity(Long productId, Long increaseQuantity) {
+        if (increaseQuantity == null || increaseQuantity <= 0L) {
+            return 0L;
+        }
+
+        return queryFactory
+                .update(stock)
+                .set(stock.quantity, stock.quantity.add(increaseQuantity))
+                .where(stock.product.id.eq(productId))
+                .execute();
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockRepositoryImpl.java
index 154242a66..1949204ad 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockRepositoryImpl.java
@@ -26,9 +26,13 @@ public Optional<Stock> save(Stock stock) {
     }
 
     @Override
-    public boolean decreaseQuantity(Long productId, Long decreaseQuantity) {
-        long updatedRows = stockQueryRepository.decreaseQuantity(productId, decreaseQuantity);
-        return updatedRows > 0L;
+    public long decreaseQuantity(Long productId, Long decreaseQuantity) {
+        return stockQueryRepository.decreaseQuantity(productId, decreaseQuantity);
+    }
+
+    @Override
+    public long increaseQuantity(Long productId, Long increaseQuantity) {
+        return stockQueryRepository.increaseQuantity(productId, increaseQuantity);
     }
 }
 
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/LikeFacadeTest.java
similarity index 74%
rename from apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
index 78ddb4cef..5187ab345 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
@@ -4,8 +4,8 @@
 import com.loopers.application.product.ProductInfo;
 import com.loopers.application.user.UserFacade;
 import com.loopers.application.user.UserInfo;
-import com.loopers.domain.like.Like;
-import com.loopers.domain.like.LikeTargetType;
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeTargetType;
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.product.ProductStatus;
@@ -25,13 +25,16 @@
 
 import java.math.BigDecimal;
 import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
-@DisplayName("LikeFacade 통합 테스트")
+@DisplayName("LikeFacade 테스트")
 @SpringBootTest
-class LikeFacadeIntegrationTest {
+class LikeFacadeTest {
 
     @Autowired
     private LikeFacade likeFacade;
@@ -82,20 +85,41 @@ void saveProductLike_withValidUserAndProduct_Success() {
             likeFacade.saveProductLike(validLoginId, productId);
 
             // assert
-            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeTargetIdAndLikeTargetType(
+            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertTrue(savedLike.isPresent());
             assertEquals(userId, savedLike.get().getUser().getId());
-            assertEquals(productId, savedLike.get().getLikeTargetId());
-            assertEquals(LikeTargetType.PRODUCT, savedLike.get().getLikeTargetType());
+            assertEquals(productId, savedLike.get().getLikeId().getLikeTargetId());
+            assertEquals(LikeTargetType.PRODUCT, savedLike.get().getLikeId().getLikeTargetType());
             
             // Product.likeCount가 1 증가했는지 확인
             Product product = productRepository.findById(productId).orElseThrow();
             assertEquals(1L, product.getLikeCount(), "좋아요 등록 시 likeCount가 1 증가해야 합니다.");
         }
 
-        @DisplayName("멱등성 테스트: 동일한 좋아요를 중복 등록해도 예외 없이 처리")
+        @DisplayName("실패 케이스: 존재하지 않는 사용자로 좋아요 등록 시 NOT_FOUND 예외 발생")
+        @Test
+        void saveProductLike_withNonExistentUser_NotFound() {
+            // arrange
+            String nonExistentLoginId = "nonexistent";
+            Long productId = 1L;
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () -> {
+                likeFacade.saveProductLike(nonExistentLoginId, productId);
+            });
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] User를 찾을 수 없습니다."));
+        }
+    }
+
+    @DisplayName("saveProductLike 동시성 테스트")
+    @Nested
+    class SaveProductLikeConcurrencyTest {
+
+        @DisplayName("멱등성 테스트: 동일한 좋아요를 중복 등록해도 Like 가 1개이고 Product의 likeCount는 1 증가해야 한다")
         @Test
         void saveProductLike_duplicateLike_Idempotent() {
             // arrange
@@ -113,15 +137,15 @@ void saveProductLike_duplicateLike_Idempotent() {
             likeFacade.saveProductLike(validLoginId, productId); // 중복 등록
 
             // assert
-            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeTargetIdAndLikeTargetType(
+            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertTrue(savedLike.isPresent());
             // 멱등성: 중복 등록해도 하나만 존재
             long count = likeJpaRepository.findAll().stream()
                     .filter(like -> like.getUser().getId().equals(userId)
-                            && like.getLikeTargetId().equals(productId)
-                            && like.getLikeTargetType() == LikeTargetType.PRODUCT)
+                            && like.getLikeId().getLikeTargetId().equals(productId)
+                            && like.getLikeId().getLikeTargetType() == LikeTargetType.PRODUCT)
                     .count();
             assertThat(count).isEqualTo(1);
             
@@ -131,21 +155,52 @@ void saveProductLike_duplicateLike_Idempotent() {
                     "중복 등록 시 likeCount가 증가하지 않아야 합니다.");
         }
 
-        @DisplayName("실패 케이스: 존재하지 않는 사용자로 좋아요 등록 시 NOT_FOUND 예외 발생")
+        @DisplayName("동시성 테스트: 동일한 좋아요 요청이 동시에 와도 likeCount는 한 번만 증가해야 한다")
         @Test
-        void saveProductLike_withNonExistentUser_NotFound() {
+        void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException {
             // arrange
-            String nonExistentLoginId = "nonexistent";
-            Long productId = 1L;
+            createAndSaveUser(validLoginId);
+            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
+            Long userId = user.getId();
+            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
+            Long productId = productInfo.id();
 
-            // act & assert
-            CoreException exception = assertThrows(CoreException.class, () -> {
-                likeFacade.saveProductLike(nonExistentLoginId, productId);
-            });
+            // act
+            int threads = 10;
+            ExecutorService executorService = Executors.newFixedThreadPool(threads);
+            CountDownLatch latch = new CountDownLatch(threads);
+
+            // 스레드 실행행
+            for(int i = 1; i <= threads; i++) {
+                executorService.execute(() -> {
+                    try {
+                        likeFacade.saveProductLike(validLoginId, productId);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+            
+            // 모든 스레드 마칠 때까지 대기
+            latch.await();
+            executorService.shutdown();
 
-            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] User를 찾을 수 없습니다."));
+            // assert
+            
+            // Like 한 개만 저장되어 있어야 함
+            long count = likeJpaRepository.findAll().stream()
+                    .filter(like -> like.getUser().getId().equals(userId)
+                            && like.getLikeId().getLikeTargetId().equals(productId)
+                            && like.getLikeId().getLikeTargetType() == LikeTargetType.PRODUCT)
+                    .count();
+            assertThat(count).isEqualTo(1);
+            
+            // Product.likeCount가 1만 증가했는지 확인
+            Product productAfterConcurrent = productRepository.findById(productId).orElseThrow();
+            assertEquals(1L, productAfterConcurrent.getLikeCount(), 
+                    "동시성 요청 시 likeCount가 1 증가해야 합니다.");
         }
+
     }
 
     @DisplayName("deleteProductLike 메서드")
@@ -170,7 +225,7 @@ void deleteProductLike_withExistingLike_Success() {
             likeFacade.deleteProductLike(validLoginId, productId);
 
             // assert
-            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeTargetIdAndLikeTargetType(
+            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertFalse(deletedLike.isPresent());
@@ -200,7 +255,7 @@ void deleteProductLike_withNonExistentLike_Idempotent() {
             });
 
             // assert
-            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeTargetIdAndLikeTargetType(
+            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertFalse(deletedLike.isPresent());
@@ -244,12 +299,20 @@ private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long lik
                 .name(name)
                 .description("테스트 설명")
                 .price(price)
-                .likeCount(likeCount)
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
                 .build();
 
+        // 리플렉션을 사용하여 likeCount 설정 (Builder에 파라미터가 없으므로)
+        try {
+            java.lang.reflect.Field likeCountField = Product.class.getDeclaredField("likeCount");
+            likeCountField.setAccessible(true);
+            likeCountField.set(product, likeCount);
+        } catch (Exception e) {
+            throw new RuntimeException("likeCount 설정 실패", e);
+        }
+
         return productFacade.saveProduct(ProductInfo.from(product));
     }
 }
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/ProductFacadeTest.java
similarity index 88%
rename from apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
index 78908339c..2cecbf1c5 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
@@ -16,6 +16,7 @@
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 
+import java.lang.reflect.Field;
 import java.math.BigDecimal;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -23,11 +24,14 @@
 
 @DisplayName("ProductFacade 통합 테스트")
 @SpringBootTest
-class ProductFacadeIntegrationTest {
+class ProductFacadeTest {
 
     @Autowired
     private ProductFacade productFacade;
 
+    @Autowired
+    private com.loopers.domain.product.ProductRepository productRepository;
+
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
@@ -196,13 +200,29 @@ private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long lik
                 .name(name)
                 .description("테스트 설명")
                 .price(price)
-                .likeCount(likeCount)
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
                 .build();
 
-        return productFacade.saveProduct(ProductInfo.from(product));
+        // 먼저 Product 저장
+        ProductInfo savedProductInfo = productFacade.saveProduct(ProductInfo.from(product));
+        
+        // 저장 후 likeCount를 reflection으로 설정
+        Product savedProduct = productRepository.findById(savedProductInfo.id())
+                .orElseThrow(() -> new RuntimeException("Product를 찾을 수 없습니다"));
+        try {
+            Field likeCountField = Product.class.getDeclaredField("likeCount");
+            likeCountField.setAccessible(true);
+            likeCountField.set(savedProduct, likeCount);
+            productRepository.save(savedProduct); // 다시 저장하여 likeCount 반영
+        } catch (Exception e) {
+            throw new RuntimeException("likeCount 설정 실패", e);
+        }
+        
+        // 최종 ProductInfo 반환
+        return ProductInfo.from(productRepository.findById(savedProductInfo.id())
+                .orElseThrow(() -> new RuntimeException("Product를 찾을 수 없습니다")));
     }
 }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
index 96d7b244b..2d6303288 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
@@ -1,11 +1,14 @@
 package com.loopers.domain.like;
 
+import com.loopers.domain.like.entity.Like;
+import com.loopers.domain.like.entity.LikeTargetType;
 import com.loopers.domain.user.User;
 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.mockito.Mockito;
 
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -23,12 +26,8 @@ class CreateLikeTest {
         @Test
         void createLike_withValidFields_Success() {
             // arrange
-            User user = User.builder()
-                    .loginId("test123")
-                    .email("test@test.com")
-                    .birthday("1990-01-01")
-                    .gender(com.loopers.domain.user.Gender.MALE)
-                    .build();
+            User user = Mockito.mock(User.class);
+            Mockito.when(user.getId()).thenReturn(1L);
 
             // act
             Like like = Like.builder()
@@ -41,8 +40,8 @@ void createLike_withValidFields_Success() {
             assertNotNull(like);
             assertAll(
                     () -> assertNotNull(like.getUser()),
-                    () -> assertEquals(validLikeTargetId, like.getLikeTargetId()),
-                    () -> assertEquals(validLikeTargetType, like.getLikeTargetType())
+                    () -> assertEquals(validLikeTargetId, like.getLikeId().getLikeTargetId()),
+                    () -> assertEquals(validLikeTargetType, like.getLikeId().getLikeTargetType())
             );
         }
 
@@ -66,12 +65,8 @@ void createLike_withNullUser_BadRequest() {
         @Test
         void createLike_withNullLikeTargetId_BadRequest() {
             // arrange
-            User user = User.builder()
-                    .loginId("test123")
-                    .email("test@test.com")
-                    .birthday("1990-01-01")
-                    .gender(com.loopers.domain.user.Gender.MALE)
-                    .build();
+            User user = Mockito.mock(User.class);
+            Mockito.when(user.getId()).thenReturn(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
@@ -90,12 +85,8 @@ void createLike_withNullLikeTargetId_BadRequest() {
         @Test
         void createLike_withInvalidLikeTargetId_BadRequest() {
             // arrange
-            User user = User.builder()
-                    .loginId("test123")
-                    .email("test@test.com")
-                    .birthday("1990-01-01")
-                    .gender(com.loopers.domain.user.Gender.MALE)
-                    .build();
+            User user = Mockito.mock(User.class);
+            Mockito.when(user.getId()).thenReturn(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
@@ -114,12 +105,8 @@ void createLike_withInvalidLikeTargetId_BadRequest() {
         @Test
         void createLike_withNullLikeTargetType_BadRequest() {
             // arrange
-            User user = User.builder()
-                    .loginId("test123")
-                    .email("test@test.com")
-                    .birthday("1990-01-01")
-                    .gender(com.loopers.domain.user.Gender.MALE)
-                    .build();
+            User user = Mockito.mock(User.class);
+            Mockito.when(user.getId()).thenReturn(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
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
index 08bcce113..6598c861d 100644
--- 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
@@ -20,7 +20,6 @@ class CreateProductTest {
         final String validName = "테스트 상품";
         final String validDescription = "테스트 설명";
         final BigDecimal validPrice = BigDecimal.valueOf(10000L);
-        final Long validLikeCount = 0L;
         final ProductStatus validStatus = ProductStatus.ON_SALE;
         final Boolean validIsVisible = true;
         final Boolean validIsSellable = true;
@@ -33,7 +32,6 @@ void createProduct_withValidFields_Success() {
                     .name(validName)
                     .description(validDescription)
                     .price(validPrice)
-                    .likeCount(validLikeCount)
                     .status(validStatus)
                     .isVisible(validIsVisible)
                     .isSellable(validIsSellable)
@@ -45,7 +43,7 @@ void createProduct_withValidFields_Success() {
                     () -> assertEquals(product.getName(), validName),
                     () -> assertEquals(product.getDescription(), validDescription),
                     () -> assertEquals(product.getPrice(), validPrice),
-                    () -> assertEquals(product.getLikeCount(), validLikeCount),
+                    () -> assertEquals(0L, product.getLikeCount(), "likeCount는 필드 초기화로 0L이 기본값"),
                     () -> assertEquals(product.getStatus(), validStatus),
                     () -> assertEquals(product.getIsVisible(), validIsVisible),
                     () -> assertEquals(product.getIsSellable(), validIsSellable)
@@ -65,7 +63,6 @@ void createProduct_withNullName_BadRequest() {
                                 .name(null)
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -86,7 +83,6 @@ void createProduct_withBlankName_BadRequest() {
                                 .name("")
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -107,7 +103,6 @@ void createProduct_withWhitespaceName_BadRequest() {
                                 .name("   ")
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -133,7 +128,6 @@ void createProduct_withNullPrice_BadRequest() {
                                 .name(validName)
                                 .description(validDescription)
                                 .price(null)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -154,7 +148,6 @@ void createProduct_withNegativePrice_BadRequest() {
                                 .name(validName)
                                 .description(validDescription)
                                 .price(BigDecimal.valueOf(-1000L))
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -174,7 +167,6 @@ void createProduct_withZeroPrice_Success() {
                         .name(validName)
                         .description(validDescription)
                         .price(BigDecimal.ZERO)
-                        .likeCount(validLikeCount)
                         .status(validStatus)
                         .isVisible(validIsVisible)
                         .isSellable(validIsSellable)
@@ -186,71 +178,6 @@ void createProduct_withZeroPrice_Success() {
             }
         }
 
-        @DisplayName("Product likeCount 유효성 검사")
-        @Nested
-        class LikeCountTest {
-
-            @DisplayName("실패 케이스: likeCount가 null이면 Product 객체 생성 실패")
-            @Test
-            void createProduct_withNullLikeCount_BadRequest() {
-                // arrange & act
-                CoreException result = assertThrows(CoreException.class,
-                        () -> Product.builder()
-                                .name(validName)
-                                .description(validDescription)
-                                .price(validPrice)
-                                .likeCount(null)
-                                .status(validStatus)
-                                .isVisible(validIsVisible)
-                                .isSellable(validIsSellable)
-                                .build()
-                );
-
-                // assert
-                assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-                assertEquals("Product : likeCount 가 비어있을 수 없습니다.", result.getCustomMessage());
-            }
-
-            @DisplayName("실패 케이스: likeCount가 음수이면 Product 객체 생성 실패")
-            @Test
-            void createProduct_withNegativeLikeCount_BadRequest() {
-                // arrange & act
-                CoreException result = assertThrows(CoreException.class,
-                        () -> Product.builder()
-                                .name(validName)
-                                .description(validDescription)
-                                .price(validPrice)
-                                .likeCount(-1L)
-                                .status(validStatus)
-                                .isVisible(validIsVisible)
-                                .isSellable(validIsSellable)
-                                .build()
-                );
-
-                // assert
-                assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-                assertEquals("Product : likeCount 는 음수가 될 수 없습니다.", result.getCustomMessage());
-            }
-
-            @DisplayName("성공 케이스: likeCount가 0이면 Product 객체 생성 성공")
-            @Test
-            void createProduct_withZeroLikeCount_Success() {
-                // arrange & act
-                Product product = Product.builder()
-                        .name(validName)
-                        .description(validDescription)
-                        .price(validPrice)
-                        .likeCount(0L)
-                        .status(validStatus)
-                        .isVisible(validIsVisible)
-                        .isSellable(validIsSellable)
-                        .build();
-
-                // assert
-                assertNotNull(product);
-                assertEquals(0L, product.getLikeCount());
-            }
-        }
 
         @DisplayName("Product status 유효성 검사")
         @Nested
@@ -265,7 +192,6 @@ void createProduct_withNullStatus_BadRequest() {
                                 .name(validName)
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(null)
                                 .isVisible(validIsVisible)
                                 .isSellable(validIsSellable)
@@ -291,7 +217,6 @@ void createProduct_withNullIsVisible_BadRequest() {
                                 .name(validName)
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(null)
                                 .isSellable(validIsSellable)
@@ -317,7 +242,6 @@ void createProduct_withNullIsSellable_BadRequest() {
                                 .name(validName)
                                 .description(validDescription)
                                 .price(validPrice)
-                                .likeCount(validLikeCount)
                                 .status(validStatus)
                                 .isVisible(validIsVisible)
                                 .isSellable(null)
@@ -342,7 +266,6 @@ void createProduct_withNullDescription_Success() {
                         .name(validName)
                         .description(null)
                         .price(validPrice)
-                        .likeCount(validLikeCount)
                         .status(validStatus)
                         .isVisible(validIsVisible)
                         .isSellable(validIsSellable)
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
new file mode 100644
index 000000000..c350fe625
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
@@ -0,0 +1,286 @@
+package com.loopers.domain.stock;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.infrastructure.brand.BrandJpaRepository;
+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.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 java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Stock Service 테스트")
+@SpringBootTest
+class StockServiceTest {
+
+    @Autowired
+    private StockService stockService;
+
+    @Autowired
+    private StockRepository stockRepository;
+
+    @Autowired
+    private ProductRepository productRepository;
+
+    @Autowired
+    private BrandJpaRepository brandJpaRepository;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    private Long productId;
+    private final Long initialStockQuantity = 100L;
+
+    @BeforeEach
+    void setUp() {
+        // 테스트용 Brand 생성
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+        Brand savedBrand = brandJpaRepository.save(brand);
+
+        // 테스트용 Product 생성
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(10000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(savedBrand)
+                .build();
+        Product savedProduct = productRepository.save(product)
+                .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
+        productId = savedProduct.getId();
+
+        // Stock quantity 세팅
+        stockService.increaseQuantity(productId, initialStockQuantity);
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("decreaseQuantity 동시성 테스트")
+    @Nested
+    class DecreaseQuantityConcurrencyTest {
+
+        @DisplayName("성공 케이스: 여러 스레드가 동시에 같은 상품의 재고를 차감해도 정확히 차감됨")
+        @Test
+        void decreaseQuantity_concurrentRequests_success() throws InterruptedException {
+            // arrange
+            int threadCount = 10;
+            long decreaseQuantityPerThread = 5L;
+            Long expectedFinalStock = initialStockQuantity - (threadCount * decreaseQuantityPerThread);
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+            List<Exception> exceptions = new ArrayList<>();
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        stockService.decreaseQuantity(productId, decreaseQuantityPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                        exceptions.add(e);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Long finalStock = stockRepository.findByProductId(productId)
+                    .map(Stock::getQuantity)
+                    .orElseThrow(() -> new RuntimeException("재고를 찾을 수 없습니다"));
+
+            assertEquals(expectedFinalStock, finalStock,
+                    String.format("최종 재고는 예상값과 일치해야 함: 예상=%d, 실제=%d", expectedFinalStock, finalStock));
+            assertEquals(threadCount, successCount.get(),
+                    String.format("성공한 스레드 수는 전체 스레드 수와 일치해야 함: 예상=%d, 실제=%d", threadCount, successCount.get()));
+            assertEquals(0, failureCount.get(),
+                    String.format("실패한 스레드 수는 0이어야 함: 실제=%d", failureCount.get()));
+            assertTrue(exceptions.isEmpty(),
+                    String.format("예외가 발생하지 않아야 함: 예외 개수=%d", exceptions.size()));
+        }
+
+        @DisplayName("성공 케이스: 재고가 부족한 경우 일부만 성공하고 나머지는 실패함")
+        @Test
+        void decreaseQuantity_insufficientStock_partialSuccess() throws InterruptedException {
+            // arrange
+            int threadCount = 25; // 재고 100개에서 각 5개씩 차감하면 최대 20개 가능
+            Long decreaseQuantityPerThread = 5L;
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+            List<Exception> exceptions = new ArrayList<>();
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        stockService.decreaseQuantity(productId, decreaseQuantityPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                        exceptions.add(e);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Long finalStock = stockRepository.findByProductId(productId)
+                    .map(Stock::getQuantity)
+                    .orElseThrow(() -> new RuntimeException("재고를 찾을 수 없습니다"));
+
+            // 최대 20개 차감 가능 (100 / 5 = 20)
+            Long initialStock = 100L;
+            int maxPossibleDecreases = (int) (initialStock / decreaseQuantityPerThread);
+            assertTrue(successCount.get() <= maxPossibleDecreases,
+                    String.format("성공한 차감 수는 최대 가능 차감 수 이하여야 함: 최대=%d, 실제=%d", maxPossibleDecreases, successCount.get()));
+            assertEquals(threadCount, successCount.get() + failureCount.get(),
+                    String.format("성공 수 + 실패 수는 전체 스레드 수와 일치해야 함: 예상=%d, 실제=%d", threadCount, successCount.get() + failureCount.get()));
+            assertTrue(finalStock >= 0L,
+                    String.format("최종 재고는 0 이상이어야 함: 실제=%d", finalStock));
+            assertTrue(finalStock < decreaseQuantityPerThread,
+                    String.format("남은 재고는 차감 단위보다 작아야 함: 차감 단위=%d, 남은 재고=%d", decreaseQuantityPerThread, finalStock));
+
+            // 실패한 경우는 재고 부족 예외여야 함
+            long stockInsufficientExceptions = exceptions.stream()
+                    .filter(e -> e instanceof CoreException)
+                    .filter(e -> ((CoreException) e).getErrorType() == ErrorType.BAD_REQUEST)
+                    .filter(e -> e.getMessage().contains("재고가 부족"))
+                    .count();
+            assertEquals(failureCount.get(), stockInsufficientExceptions,
+                    String.format("재고 부족 예외 수는 실패 수와 일치해야 함: 예상=%d, 실제=%d", failureCount.get(), stockInsufficientExceptions));
+        }
+
+        @DisplayName("성공 케이스: 재고 차감이 원자적으로 처리되어 Lost Update가 발생하지 않음")
+        @Test
+        void decreaseQuantity_atomicOperation_noLostUpdate() throws InterruptedException {
+            // arrange
+            int threadCount = 20;
+            long decreaseQuantityPerThread = 1L;
+            long initialStock = 100L;
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        stockService.decreaseQuantity(productId, decreaseQuantityPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        // 실패는 무시 (재고 부족 등)
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Long finalStock = stockRepository.findByProductId(productId)
+                    .map(Stock::getQuantity)
+                    .orElseThrow(() -> new RuntimeException("재고를 찾을 수 없습니다"));
+
+            // Lost Update가 발생하지 않았는지 확인
+            // 성공한 차감 수만큼 정확히 차감되어야 함
+            Long expectedStock = initialStock - (successCount.get() * decreaseQuantityPerThread);
+            assertEquals(expectedStock, finalStock,
+                    String.format("Lost Update가 발생하지 않아야 함: 예상 재고=%d, 실제 재고=%d, 성공한 차감 수=%d", expectedStock, finalStock, successCount.get()));
+        }
+
+        @DisplayName("성공 케이스: 동시에 여러 스레드가 재고를 차감해도 음수가 되지 않음")
+        @Test
+        void decreaseQuantity_concurrentRequests_noNegativeStock() throws InterruptedException {
+            // arrange
+            int threadCount = 25; // 재고 100개에서 각 5개씩 차감하면 최대 20개 가능
+            long decreaseQuantityPerThread = 5L;
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        stockService.decreaseQuantity(productId, decreaseQuantityPerThread);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Long finalStock = stockRepository.findByProductId(productId)
+                    .map(Stock::getQuantity)
+                    .orElseThrow(() -> new RuntimeException("재고를 찾을 수 없습니다"));
+
+            // 재고가 음수가 되지 않아야 함
+            assertTrue(finalStock >= 0L,
+                    String.format("재고가 음수가 되지 않아야 함: 실제 재고=%d", finalStock));
+            // 성공한 차감 수만큼 정확히 차감되어야 함
+            Long expectedStock = initialStockQuantity - (successCount.get() * decreaseQuantityPerThread);
+            assertEquals(expectedStock, finalStock,
+                    String.format("성공한 차감 수만큼 정확히 차감되어야 함: 예상 재고=%d, 실제 재고=%d, 성공한 차감 수=%d", expectedStock, finalStock, successCount.get()));
+        }
+    }
+}
+
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..38d4ffb09
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockTest.java
@@ -0,0 +1,123 @@
+package com.loopers.domain.stock;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductStatus;
+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.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Stock 테스트")
+public class StockTest {
+
+    // 고정 fixture
+    private final Long validQuantity = 100L;
+
+    private Product createValidProduct() {
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(5000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(brand)
+                .build();
+
+        return product;
+    }
+
+    @DisplayName("Stock 엔티티 생성")
+    @Nested
+    class CreateStockTest {
+
+        @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 Stock 객체 생성 성공")
+        @Test
+        void createStock_withValidFields_Success() {
+            // arrange
+            Product product = createValidProduct();
+
+            // act
+            Stock stock = Stock.builder()
+                    .quantity(validQuantity)
+                    .product(product)
+                    .build();
+
+            // assert
+            assertNotNull(stock);
+            assertAll(
+                    () -> assertEquals(validQuantity, stock.getQuantity()),
+                    () -> assertEquals(product, stock.getProduct())
+            );
+        }
+
+        @DisplayName("실패 케이스 : quantity가 null이면 예외 발생")
+        @Test
+        void createStock_withNullQuantity_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Stock.builder()
+                            .quantity(null)
+                            .product(product)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("Stock : quantity 가 비어있을 수 없습니다."));
+        }
+
+        @DisplayName("실패 케이스 : quantity가 음수이면 예외 발생")
+        @Test
+        void createStock_withNegativeQuantity_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Stock.builder()
+                            .quantity(-1L)
+                            .product(product)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("Stock : quantity 는 음수가 될 수 없습니다."));
+        }
+
+        @DisplayName("성공 케이스 : quantity가 0이면 Stock 객체 생성 성공")
+        @Test
+        void createStock_withZeroQuantity_Success() {
+            // arrange
+            Product product = createValidProduct();
+
+            // act
+            Stock stock = Stock.builder()
+                    .quantity(0L)
+                    .product(product)
+                    .build();
+
+            // assert
+            assertNotNull(stock);
+            assertEquals(0L, stock.getQuantity());
+        }
+    }
+}
+
diff --git a/modules/jpa/src/main/java/com/loopers/domain/AuditEntity.java b/modules/jpa/src/main/java/com/loopers/domain/AuditEntity.java
new file mode 100644
index 000000000..f4418b8c1
--- /dev/null
+++ b/modules/jpa/src/main/java/com/loopers/domain/AuditEntity.java
@@ -0,0 +1,66 @@
+package com.loopers.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.PreUpdate;
+import lombok.Getter;
+import java.time.ZonedDateTime;
+
+/**
+ * 생성/수정/삭제 정보를 자동으로 관리해준다.
+ * 재사용성을 위해 이 외의 컬럼이나 동작은 추가하지 않는다.
+ */
+@MappedSuperclass
+@Getter
+public abstract class AuditEntity {
+
+    @Column(name = "created_at", nullable = false, updatable = false)
+    private ZonedDateTime createdAt;
+
+    @Column(name = "updated_at", nullable = false)
+    private ZonedDateTime updatedAt;
+
+    @Column(name = "deleted_at")
+    private ZonedDateTime deletedAt;
+
+    /**
+     * 엔티티의 유효성을 검증한다.
+     * 이 메소드는 PrePersist 및 PreUpdate 시점에 호출된다.
+     */
+    protected void guard() {}
+
+    @PrePersist
+    private void prePersist() {
+        guard();
+
+        ZonedDateTime now = ZonedDateTime.now();
+        this.createdAt = now;
+        this.updatedAt = now;
+    }
+
+    @PreUpdate
+    private void preUpdate() {
+        guard();
+
+        this.updatedAt = ZonedDateTime.now();
+    }
+
+    /**
+     * delete 연산은 멱등하게 동작할 수 있도록 한다. (삭제된 엔티티를 다시 삭제해도 동일한 결과가 나오도록)
+     */
+    public void delete() {
+        if (this.deletedAt == null) {
+            this.deletedAt = ZonedDateTime.now();
+        }
+    }
+
+    /**
+     * restore 연산은 멱등하게 동작할 수 있도록 한다. (삭제되지 않은 엔티티를 복원해도 동일한 결과가 나오도록)
+     */
+    public void restore() {
+        if (this.deletedAt != null) {
+            this.deletedAt = null;
+        }
+    }
+}
diff --git a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java b/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
index d15a9c764..72fdc2ece 100644
--- a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
+++ b/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
@@ -20,7 +20,7 @@ public abstract class BaseEntity {
 
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private final Long id = 0L;
+    private Long id;
 
     @Column(name = "created_at", nullable = false, updatable = false)
     private ZonedDateTime createdAt;

From 347b06f52583788582784ce488f25a16fc9f78a1 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 02:48:02 +0900
Subject: [PATCH 03/17] =?UTF-8?q?[Round=204]=20=EB=AC=B8=EC=84=9C=20?=
 =?UTF-8?q?=EC=9E=90=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-2-week.md                  |  41 ++++
 .codeguide/loopers-3-week.md                  |  60 ++++++
 .codeguide/loopers-4-week.md                  |  86 ++++++++
 .../src/doc/week4/03-class-diagram.md         | 185 ++++++++++++++++++
 apps/commerce-api/src/doc/week4/04-erd.md     | 118 +++++++++++
 5 files changed, 490 insertions(+)
 create mode 100644 .codeguide/loopers-2-week.md
 create mode 100644 .codeguide/loopers-3-week.md
 create mode 100644 .codeguide/loopers-4-week.md
 create mode 100644 apps/commerce-api/src/doc/week4/03-class-diagram.md
 create mode 100644 apps/commerce-api/src/doc/week4/04-erd.md

diff --git a/.codeguide/loopers-2-week.md b/.codeguide/loopers-2-week.md
new file mode 100644
index 000000000..cf790bda5
--- /dev/null
+++ b/.codeguide/loopers-2-week.md
@@ -0,0 +1,41 @@
+# 📝 Round 2 Quests
+
+---
+
+## ✍️ Design Quest
+
+> **이커머스 도메인(상품, 브랜드, 좋아요, 주문 등)에 대한 설계**를 완료하고, 다음 주부터 개발 가능한 수준의 설계 문서를 정리하여 PR로 제출합니다.
+>
+
+### 📋 과제 정보
+
+- **설계 범위**
+    - 상품 목록 / 상품 상세 / 브랜드 조회
+    - 상품 좋아요 등록/취소 (멱등 동작)
+    - 주문 생성 및 결제 흐름 (재고 차감, 포인트 차감, 외부 시스템 연동)
+- **제외 도메인**
+    - 회원가입, 포인트 충전 (1주차 구현 완료 기준)
+- **요구사항 기반**
+    - 루프팩 이커머스 시나리오 문서를 기준으로 기능/제약사항을 설계에 반영합니다.
+- **제출 방식**
+    1. 아래 파일들을 프로젝트 내 `docs/week2/` 폴더에 `.md`로 저장
+    2. Github PR로 제출
+        - PR 제목: `[2주차] 설계 문서 제출 - 홍길동`
+        - PR 본문에 리뷰 포인트 포함 (예: 고민한 지점 등)
+
+### ✅ 제출 파일 목록 (.docs/design 디렉토리 내)
+
+| 파일명 | 내용 |
+| --- | --- |
+| `01-requirements.md` | 유저 시나리오 기반 기능 정의, 요구사항 명세 |
+| `02-sequence-diagrams.md` | 시퀀스 다이어그램 최소 2개 이상 (Mermaid 기반 작성 권장) |
+| `03-class-diagram.md` | 도메인 객체 설계 (클래스 다이어그램 or 설명 중심) |
+| `04-erd.md` | 전체 테이블 구조 및 관계 정리 (ERD Mermaid 작성 가능) |
+
+## ✅ Checklist
+
+- [ ]  상품/브랜드/좋아요/주문 도메인이 모두 포함되어 있는가?
+- [ ]  기능 요구사항이 유저 중심으로 정리되어 있는가?
+- [ ]  시퀀스 다이어그램에서 책임 객체가 드러나는가?
+- [ ]  클래스 구조가 도메인 설계를 잘 표현하고 있는가?
+- [ ]  ERD 설계 시 데이터 정합성을 고려하여 구성하였는가?
\ No newline at end of file
diff --git a/.codeguide/loopers-3-week.md b/.codeguide/loopers-3-week.md
new file mode 100644
index 000000000..d55db563f
--- /dev/null
+++ b/.codeguide/loopers-3-week.md
@@ -0,0 +1,60 @@
+# 📝 Round 3 Quests
+
+---
+
+## 💻 Implementation Quest
+
+> *** 도메인 모델링**을 통해 Product, Brand, Like, Order 기능의 핵심 개념을 **Entity, Value Object, Domain Service 등 적합한** **코드**로 작성합니다.
+* 레이어드 아키텍처 + DIP 를 적용해 유연하고 테스트 가능한 구조로 구성합니다.
+* **Application Layer를 경량 수준**으로 구성하여, 도메인 객체를 조합하는 흐름을 실제 구현해봅니다.
+* **단위 테스트를 작성**하여 도메인 로직의 정합성과 규칙을 검증합니다.
+>
+
+### 📋 과제 정보
+
+- 상품, 브랜드, 좋아요, 주문 기능의 **도메인 모델 및 도메인 서비스**를 구현합니다.
+- 도메인 간 협력 흐름을 설계하고, 필요한 로직을 **도메인 서비스**로 분리합니다.
+- Application Layer에서 도메인 객체를 조합하는 흐름을 구현합니다.
+  (예: `ProductFacade.getProductDetail(productId)` → `Product + Brand + Like 조합`)
+- Repository Interface 와 구현체는 분리하고, 테스트 가능성을 고려한 구조를 설계합니다.
+- 모든 핵심 도메인 로직에 대해 단위 테스트를 작성하고, 예외/경계 케이스도 포함합니다.
+
+## ✅ Checklist
+
+### 🏷 Product / Brand 도메인
+
+- [ ]  상품 정보 객체는 브랜드 정보, 좋아요 수를 포함한다.
+- [ ]  상품의 정렬 조건(`latest`, `price_asc`, `likes_desc`) 을 고려한 조회 기능을 설계했다
+- [ ]  상품은 재고를 가지고 있고, 주문 시 차감할 수 있어야 한다
+- [ ]  재고는 감소만 가능하며 음수 방지는 도메인 레벨에서 처리된다
+
+### 👍 Like 도메인
+
+- [ ]  좋아요는 유저와 상품 간의 관계로 별도 도메인으로 분리했다
+- [ ]  중복 좋아요 방지를 위한 멱등성 처리가 구현되었다
+- [ ]  상품의 좋아요 수는 상품 상세/목록 조회에서 함께 제공된다
+- [ ]  단위 테스트에서 좋아요 등록/취소/중복 방지 흐름을 검증했다
+
+### 🛒 Order 도메인
+
+- [ ]  주문은 여러 상품을 포함할 수 있으며, 각 상품의 수량을 명시한다
+- [ ]  주문 시 상품의 재고 차감, 유저 포인트 차감 등을 수행한다
+- [ ]  재고 부족, 포인트 부족 등 예외 흐름을 고려해 설계되었다
+- [ ]  단위 테스트에서 정상 주문 / 예외 주문 흐름을 모두 검증했다
+
+### 🧩 도메인 서비스
+
+- [ ]  도메인 간 협력 로직은 Domain Service에 위치시켰다
+- [ ]  상품 상세 조회 시 Product + Brand 정보 조합은 도메인 서비스에서 처리했다
+- [ ]  복합 유스케이스는 Application Layer에 존재하고, 도메인 로직은 위임되었다
+- [ ]  도메인 서비스는 상태 없이, 도메인 객체의 협력 중심으로 설계되었다
+
+### **🧱 소프트웨어 아키텍처 & 설계**
+
+- [ ]  전체 프로젝트의 구성은 아래 아키텍처를 기반으로 구성되었다
+    - Application → **Domain** ← Infrastructure
+- [ ]  Application Layer는 도메인 객체를 조합해 흐름을 orchestration 했다
+- [ ]  핵심 비즈니스 로직은 Entity, VO, Domain Service 에 위치한다
+- [ ]  Repository Interface는 Domain Layer 에 정의되고, 구현체는 Infra에 위치한다
+- [ ]  패키지는 계층 + 도메인 기준으로 구성되었다 (`/domain/order`, `/application/like` 등)
+- [ ]  테스트는 외부 의존성을 분리하고, Fake/Stub 등을 사용해 단위 테스트가 가능하게 구성되었다
\ No newline at end of file
diff --git a/.codeguide/loopers-4-week.md b/.codeguide/loopers-4-week.md
new file mode 100644
index 000000000..68187b65b
--- /dev/null
+++ b/.codeguide/loopers-4-week.md
@@ -0,0 +1,86 @@
+# 📝 Round 4 Quests
+
+---
+
+## 💻 Implementation Quest
+
+> 주문 시, 재고/포인트/쿠폰의 정합성을 트랜잭션으로 보장하고, 동시성 이슈를 제어합니다.
+>
+
+<aside>
+🎯
+
+**Must-Have (이번 주에 무조건 가져가야 좋을 것-**무조건 ****하세요**)**
+
+- DB 트랜잭션
+- Lock
+- 동시성 테스트
+
+**Nice-To-Have (부가적으로 가져가면 좋을 것-**시간이 ****허락하면 ****꼭 ****해보세요**)**
+
+- 쿠폰 개념
+- 미비한 기능 완성을 통한 API 마무리
+</aside>
+
+### **📦 추가 요구사항**
+
+```json
+{
+  "items": [
+    { "productId": 1, "quantity": 2 },
+    { "productId": 3, "quantity": 1 }
+  ],
+  "couponId": 42 // 추가 스펙
+}
+```
+
+- 주문 시에 쿠폰을 이용해 사용자가 소유한 쿠폰을 적용해 할인받을 수 있도록 합니다.
+- 쿠폰은 **정액, 정률 쿠폰이 존재**하며 **재사용이 불가능**합니다.
+- 존재하지 않거나 사용 불가능한 쿠폰으로 요청 시, 주문은 실패해야 합니다.
+
+### 📋 과제 정보
+
+- 주문 API에 트랜잭션을 적용하고, 재고/포인트/주문 도메인의 정합성을 보장합니다.
+- 동시성 이슈(Lost Update)가 발생하지 않도록 낙관적 락 또는 비관적 락을 적용합니다.
+- 주요 구현 대상은 Application Layer (혹은 OrderFacade 등)에서의 트랜잭션 처리입니다.
+- 동시성 이슈가 발생할 수 있는 기능에 대한 테스트가 모두 성공해야 합니다.
+
+**예시 (주문 처리 흐름)**
+
+```kotlin
+1. 주문 요청
+2. "주문을 위한 처리" ( 순서 무관 )
+	- 쿠폰 사용 및 차감 // 동시성 이슈 위험 구간
+	- 상품 재고 확인 및 차감 // 동시성 이슈 위험 구간
+	- 유저 포인트 확인 및 차감 // 동시성 이슈 위험 구간
+5. 주문 엔티티 생성 및 저장
+```
+
+### 🚀 구현 보강
+
+- 모든 API 가 요구사항 기반으로 동작해야 합니다.
+- 미비한 구현에 대해 모두 완성해주세요.
+
+## ✅ Checklist
+
+### 🗞️ Coupon 도메인
+
+- [ ]  쿠폰은 사용자가 소유하고 있으며, 이미 사용된 쿠폰은 사용할 수 없어야 한다.
+- [ ]  쿠폰 종류는 정액 / 정률로 구분되며, 각 적용 로직을 구현하였다.
+- [ ]  각 발급된 쿠폰은 최대 한번만 사용될 수 있다.
+
+### 🧾 **주문**
+
+- [ ]  주문 전체 흐름에 대해 원자성이 보장되어야 한다.
+- [ ]  사용 불가능하거나 존재하지 않는 쿠폰일 경우 주문은 실패해야 한다.
+- [ ]  재고가 존재하지 않거나 부족할 경우 주문은 실패해야 한다.
+- [ ]  주문 시 유저의 포인트 잔액이 부족할 경우 주문은 실패해야 한다
+- [ ]  쿠폰, 재고, 포인트 처리 등 하나라도 작업이 실패하면 모두 롤백처리되어야 한다.
+- [ ]  주문 성공 시, 모든 처리는 정상 반영되어야 한다.
+
+### 🧪 동시성 테스트
+
+- [ ]  동일한 상품에 대해 여러명이 좋아요/싫어요를 요청해도, 상품의 좋아요 개수가 정상 반영되어야 한다.
+- [ ]  동일한 쿠폰으로 여러 기기에서 동시에 주문해도, 쿠폰은 단 한번만 사용되어야 한다.
+- [ ]  동일한 유저가 서로 다른 주문을 동시에 수행해도, 포인트가 정상적으로 차감되어야 한다.
+- [ ]  동일한 상품에 대해 여러 주문이 동시에 요청되어도, 재고가 정상적으로 차감되어야 한다.
\ No newline at end of file
diff --git a/apps/commerce-api/src/doc/week4/03-class-diagram.md b/apps/commerce-api/src/doc/week4/03-class-diagram.md
new file mode 100644
index 000000000..e29ac6c42
--- /dev/null
+++ b/apps/commerce-api/src/doc/week4/03-class-diagram.md
@@ -0,0 +1,185 @@
+---
+title: E-Commerce Class Diagram
+---
+classDiagram
+    class User {
+        -Long id
+        -String loginId
+        -String email
+        -String birthday
+        -Enum gender
+        -Point point
+        -List~Like~ likes
+        -List~Coupon~ coupons
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+    }
+    class Point {
+        -Long id
+        -BigDecimal amount
+        -User user
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+    }
+    class Product {
+        -Long id
+        -String name
+        -String description
+        -BigDecimal price
+        -Long likeCount
+        -Enum status
+        -Boolean isVisible
+        -Boolean isSellable
+        -Brand brand
+        -Stock stock
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+        +setBrand(Brand brand)
+    }
+    class Stock {
+        -Long id
+        -Long quantity
+        -Product product
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+    }
+    class Brand {
+        -Long id
+        -String name
+        -String description
+        -Enum status
+        -Boolean isVisible
+        -Boolean isSellable
+        -List~Product~ products
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+        +setPoint(List~Product~ products)
+    }
+    class Like {
+        -LikeId likeId
+        -User user
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+    }
+    class LikeId {
+        -Long userId
+        -Long likeTargetId
+        -Enum likeTargetType
+    }
+    class Order {
+        -Long id
+        -BigDecimal finalAmount
+        -BigDecimal totalPrice
+        -BigDecimal discountAmount
+        -BigDecimal shippingFee
+        -Enum orderStatus
+        -User user
+        -List~OrderItem~ orderItems
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+        +addOrderItem(Product product, Integer quantity)
+        +applyDiscount(BigDecimal discountAmount)
+        +confirm()
+    }
+    class Coupon {
+        -Long id
+        -Enum couponType
+        -BigDecimal discountValue
+        -Boolean isUsed
+        -User user
+        -Order order
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+        +calculateDiscount(BigDecimal totalPrice) BigDecimal
+    }
+    class OrderItem {
+        -Long id
+        -Integer quantity
+        -BigDecimal unitPrice
+        -BigDecimal totalAmount
+        -Order order
+        -Product product
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+    }
+    class Payment {
+        -Long id
+        -Enum method
+        -BigDecimal amount
+        -Enum paymentStatus
+        -Order order
+        -ZonedDateTime createdAt
+        -ZonedDateTime updatedAt
+        -ZonedDateTime deletedAt
+    }
+    class UserService {
+        +saveUser(User user) Optional~User~
+        +findUserByLoginId(String loginId) Optional~User~
+    }
+    class PointService {
+        +findByUserLoginId(String loginId) Optional~Point~
+        +savePoint(Point point) Optional~Point~
+        +deduct(String loginId, BigDecimal deductAmount)
+        +charge(String loginId, BigDecimal chargeAmount)
+    }
+    class ProductService {
+        +saveProduct(Product product) Optional~Product~
+        +findById(Long productId) Optional~Product~
+        +findProducts(ProductCondition condition, Pageable pageable) Page~Product~
+        +getProductDetailWithBrand(Long productId) ProductWithBrand
+    }
+    class StockService {
+        +findByProductId(Long productId) Optional~Stock~
+        +saveStock(Stock stock) Optional~Stock~
+        +decreaseQuantity(Long productId, Long decreaseQuantity)
+        +increaseQuantity(Long productId, Long increaseQuantity)
+    }
+    class BrandService {
+        +findById(Long brandId) Brand
+    }
+    class LikeService {
+        +saveProductLike(User user, Long productId)
+        +deleteProductLike(User user, Long productId)
+    }
+    class OrderService {
+        +saveOrder(Order order) Optional~Order~
+        +findOrderById(Long orderId) Order
+        +findOrdersByUser(User user) List~Order~
+    }
+    class CouponService {
+        +saveCoupon(Coupon coupon) Optional~Coupon~
+        +findById(Long couponId) Coupon
+        +findByUser(User user) List~Coupon~
+        +findAvailableCouponsByUser(User user) List~Coupon~
+        +useCoupon(Order order, Long couponId)
+    }
+
+    User "1" --> "0..1" Point
+    User "1" --> "0..*" Like
+    User "1" --> "0..*" Order
+    User "1" --> "0..*" Coupon
+    Product "1" --> "0..*" Like
+    Product "1" --> "0..1" Stock
+    Product "1" --> "0..*" OrderItem
+    Brand "1" --> "0..*" Product
+    Order "1" --> "1..*" OrderItem
+    Order "1" --> "0..1" Payment
+    Coupon "0..1" --> "0..1" Order
+    Like "*--" "1" LikeId
+    
+    UserService ..> User : uses
+    PointService ..> Point : uses
+    ProductService ..> Product : uses
+    StockService ..> Stock : uses
+    BrandService ..> Brand : uses
+    LikeService ..> Like : uses
+    OrderService ..> Order : uses
+    CouponService ..> Coupon : uses
diff --git a/apps/commerce-api/src/doc/week4/04-erd.md b/apps/commerce-api/src/doc/week4/04-erd.md
new file mode 100644
index 000000000..269d58eef
--- /dev/null
+++ b/apps/commerce-api/src/doc/week4/04-erd.md
@@ -0,0 +1,118 @@
+---
+title: E-Commerce ERD
+---
+erDiagram
+    user {
+        bigint id PK
+        varchar login_id UK
+        varchar email UK
+        varchar birthday
+        enum gender
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    point {
+        bigint id PK
+        bigint user_id FK
+        decimal amount
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    product {
+        bigint id PK
+        bigint brand_id FK
+        varchar name
+        varchar description
+        enum status
+        decimal price
+        bigint like_count
+        boolean is_visible
+        boolean is_sellable
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    stock {
+        bigint id PK
+        bigint product_id FK
+        bigint quantity
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    brand {
+        bigint id PK
+        varchar name
+        varchar description
+        enum status
+        boolean is_visible
+        boolean is_sellable
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    user_like {
+        bigint user_id PK,FK
+        bigint like_target_id PK
+        enum like_target_type PK
+        datetime created_at
+        datetime updated_at
+    }
+    orders {
+        bigint id PK
+        bigint user_id FK
+        enum order_status
+        decimal final_amount
+        decimal total_price
+        decimal discount_amount
+        decimal shipping_fee
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    order_item {
+        bigint id PK
+        bigint order_id FK
+        bigint product_id FK
+        int quantity
+        decimal unit_price
+        decimal total_amount
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    payment {
+        bigint id PK
+        bigint order_id FK
+        enum method
+        decimal amount
+        enum payment_status
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+    coupon {
+        bigint id PK
+        bigint user_id FK
+        bigint order_id FK
+        enum coupon_type
+        decimal discount_value
+        boolean is_used
+        datetime created_at
+        datetime updated_at
+        datetime deleted_at
+    }
+
+    user ||--|| point : "has"
+    user ||--o{ user_like : "creates"
+    user ||--o{ orders : "places"
+    user ||--o{ coupon : "owns"
+    product ||--o{ user_like : "receives"
+    product ||--|| stock : "has"
+    product ||--o{ order_item : "included_in"
+    brand ||--o{ product : "has"
+    orders ||--|{ order_item : "contains"
+    orders ||--|| payment : "has"
+    coupon ||--o| orders : "applied_to"
\ No newline at end of file

From 296b2e10863e10be7bab418ef9389c0ab451df33 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 02:50:56 +0900
Subject: [PATCH 04/17] =?UTF-8?q?[Round=204]=20=EC=A3=BC=EB=AC=B8=20?=
 =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20API=20=EA=B0=9C=EB=B0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/order/OrderFacade.java        |  76 +++-
 .../java/com/loopers/domain/order/Order.java  | 135 +++---
 .../com/loopers/domain/order/OrderItem.java   |  62 ++-
 .../loopers/domain/order/OrderService.java    | 138 +-----
 .../com/loopers/domain/order/OrderStatus.java |   7 +-
 .../com/loopers/domain/payment/Payment.java   | 104 +----
 .../loopers/domain/payment/PaymentStatus.java |   6 +-
 .../interfaces/api/order/OrderController.java |   8 +-
 .../interfaces/api/order/OrderDto.java        |  15 +-
 .../order/OrderFacadeIntegrationTest.java     | 407 ++++++++++++++++++
 .../loopers/domain/order/OrderItemTest.java   | 200 +++++++++
 .../domain/order/OrderServiceTest.java        | 273 ------------
 .../com/loopers/domain/order/OrderTest.java   | 308 +++++++++++++
 .../loopers/domain/payment/PaymentTest.java   | 164 +++++++
 14 files changed, 1311 insertions(+), 592 deletions(-)
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
index 74a6588c4..914228184 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
@@ -1,16 +1,26 @@
 package com.loopers.application.order;
 
+import com.loopers.domain.coupon.CouponService;
 import com.loopers.domain.order.Order;
 import com.loopers.domain.order.OrderService;
+import com.loopers.domain.point.PointService;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductService;
+import com.loopers.domain.stock.StockService;
 import com.loopers.domain.user.User;
 import com.loopers.domain.user.UserService;
+import com.loopers.interfaces.api.order.OrderDto;
 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;
 
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RequiredArgsConstructor
 @Component
@@ -18,25 +28,82 @@ public class OrderFacade {
 
     private final OrderService orderService;
     private final UserService userService;
+    private final ProductService productService;
+    private final StockService stockService;
+    private final PointService pointService;
+    private final CouponService couponService;
 
     /**
      * 주문 생성
      */
     @Transactional
-    public OrderInfo createOrder(String loginId, List<OrderService.OrderItemRequest> orderItemRequests) {
+    public OrderInfo createOrder(String loginId, OrderDto.CreateOrderRequest request) {
+        List<OrderDto.OrderItemRequest> orderItemRequests = request.items();
+        // 1. User 객체 조회
         User user = userService.findUserByLoginId(loginId)
                 .orElseThrow(() -> new CoreException(
-                        ErrorType.NOT_FOUND,
+                        ErrorType.NOT_FOUND, 
                         "[loginId = " + loginId + "] User를 찾을 수 없습니다."
                 ));
 
-        Order order = orderService.createOrder(user, orderItemRequests);
-        return OrderInfo.from(order);
+        // 2. Product 조회 및 재고 차감
+        Map<Long, Product> productMap = new HashMap<>();
+        orderItemRequests.stream()
+                .sorted(Comparator.comparing(OrderDto.OrderItemRequest::productId))  // productId로 정렬 (데드락 방지)
+                .forEach(itemRequest -> {
+                    // 2-1. Product 객체 조회
+                    Product product = productService.findById(itemRequest.productId())
+                            .orElseThrow(() -> new CoreException(
+                                ErrorType.NOT_FOUND, 
+                                "[productId = " + itemRequest.productId() + "] Product를 찾을 수 없습니다."
+                    ));
+                    productMap.put(itemRequest.productId(), product);
+
+                    // 2-2. Stock(재고) 차감, 만약 재고가 부족하면 예외 발생
+                    stockService.decreaseQuantity(itemRequest.productId(), Long.valueOf(itemRequest.quantity()));
+                });
+
+        // 3. 주문 생성
+        Order order = Order.builder()
+                .discountAmount(BigDecimal.ZERO)  // 초기 할인 금액은 0 (쿠폰 적용 전)
+                .shippingFee(BigDecimal.ZERO)     // 배송비는 기본값 0 (필요시 비즈니스 로직으로 계산)
+                .user(user)
+                .build();
+        
+        // 3-1. OrderItem 리스트 생성 및 적용 (Order를 통해서만 생성)
+        orderItemRequests.forEach(itemRequest -> {
+            Product product = productMap.get(itemRequest.productId());
+            order.addOrderItem(product, itemRequest.quantity());
+        });
+
+        // 3-2. 주문 먼저 저장 (쿠폰 적용을 위해 order.getId()가 필요)
+        Order savedOrder = orderService.saveOrder(order)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
+                        "Order 저장에 실패했습니다."
+                ));
+
+        // 5. 포인트 차감 (쿠폰 할인 적용 후 최종 금액으로)
+        pointService.deduct(loginId, savedOrder.getFinalAmount());
+        
+        // 6. 주문 상태 변경
+        savedOrder.confirm();
+
+        // 7. 주문 상태 변경 후 다시 저장
+        savedOrder = orderService.saveOrder(savedOrder)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
+                        "Order 저장에 실패했습니다."
+                ));
+
+        // 주문 정보 반환
+        return OrderInfo.from(savedOrder);
     }
 
     /**
      * 단일 주문 상세 조회
      */
+    @Transactional(readOnly = true)
     public OrderInfo getOrder(Long orderId) {
         Order order = orderService.findOrderById(orderId);
         return OrderInfo.from(order);
@@ -45,6 +112,7 @@ public OrderInfo getOrder(Long orderId) {
     /**
      * 유저의 주문 목록 조회
      */
+    @Transactional(readOnly = true)
     public List<OrderInfo> getOrders(String loginId) {
         User user = userService.findUserByLoginId(loginId)
                 .orElseThrow(() -> new CoreException(
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
index 227fc8485..8e84f1906 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
@@ -1,6 +1,7 @@
 package com.loopers.domain.order;
 
 import com.loopers.domain.BaseEntity;
+import com.loopers.domain.product.Product;
 import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
@@ -20,13 +21,13 @@
 @Getter
 public class Order extends BaseEntity {
 
-    private BigDecimal finalAmount;
-    private BigDecimal totalPrice;
+    private BigDecimal finalAmount = BigDecimal.ZERO;
+    private BigDecimal totalPrice = BigDecimal.ZERO;
     private BigDecimal discountAmount;
     private BigDecimal shippingFee;
 
     @Enumerated(EnumType.STRING)
-    private OrderStatus orderStatus;
+    private OrderStatus orderStatus = OrderStatus.PENDING;
 
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "user_id")
@@ -37,92 +38,110 @@ public class Order extends BaseEntity {
 
     @Builder
     private Order(
-            BigDecimal finalAmount,
-            BigDecimal totalPrice,
             BigDecimal discountAmount,
             BigDecimal shippingFee,
-            OrderStatus orderStatus,
-            User user,
-            List<OrderItem> orderItems
+            User user
     ) {
-        this.finalAmount = finalAmount;
-        this.totalPrice = totalPrice;
         this.discountAmount = discountAmount;
         this.shippingFee = shippingFee;
-        this.orderStatus = orderStatus;
         this.user = user;
-        if (orderItems != null) {
-            this.orderItems = orderItems;
-            orderItems.forEach(item -> item.setOrder(this));
-        }
-        guard();
-    }
-
-    public void setUser(User user) {
-        if (this.user != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Order : User가 이미 존재합니다.");
-        }
-        this.user = user;
-    }
 
-    public void addOrderItem(OrderItem orderItem) {
-        this.orderItems.add(orderItem);
-        orderItem.setOrder(this);
-    }
-
-    public void confirm() {
-        if (this.orderStatus != OrderStatus.PENDING) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : PENDING 상태의 주문만 확인할 수 있습니다.");
-        }
-        this.orderStatus = OrderStatus.CONFIRMED;
+        // 유효성 검사
+        guard();
     }
 
     /**
-     * Payment에서 계산된 최종 금액을 설정
+     * Order 엔티티의 유효성 검사
      */
-    public void setFinalAmount(BigDecimal finalAmount) {
-        if (finalAmount == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : finalAmount가 비어있을 수 없습니다.");
-        }
-        if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : finalAmount는 음수가 될 수 없습니다.");
-        }
-        this.finalAmount = finalAmount;
-    }
-
     @Override
     protected void guard() {
-        if (finalAmount == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : finalAmount가 비어있을 수 없습니다.");
-        } else if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : finalAmount는 음수가 될 수 없습니다.");
-        }
-
-        if (totalPrice == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : totalPrice가 비어있을 수 없습니다.");
-        } else if (totalPrice.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : totalPrice는 음수가 될 수 없습니다.");
-        }
 
+        // discountAmount 검증: null이 아니어야 하며, 0 이상이어야 함
         if (discountAmount == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : discountAmount가 비어있을 수 없습니다.");
         } else if (discountAmount.compareTo(BigDecimal.ZERO) < 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : discountAmount는 음수가 될 수 없습니다.");
         }
 
+        // shippingFee 검증: null이 아니어야 하며, 0 이상이어야 함
         if (shippingFee == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : shippingFee가 비어있을 수 없습니다.");
         } else if (shippingFee.compareTo(BigDecimal.ZERO) < 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : shippingFee는 음수가 될 수 없습니다.");
         }
 
+        // orderStatus 검증: null이 아니어야 함
         if (orderStatus == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : orderStatus가 비어있을 수 없습니다.");
         }
 
-        if (orderItems == null || orderItems.isEmpty()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : orderItems가 비어있을 수 없습니다.");
+        // user 검증: null이 아니어야 함 (주문한 사용자 정보 필수)
+        if (user == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : user가 비어있을 수 없습니다.");
+        }
+    }
+
+    /**
+     * 주문 상품 추가
+     * 주문 상품을 리스트에 추가하고, 양방향 관계를 설정함
+     */
+    public void addOrderItem(Product product, Integer quantity) {
+        
+        // 주문 상태 검증
+        if(this.orderStatus != OrderStatus.PENDING) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : PENDING 상태의 주문만 주문 상품을 추가할 수 있습니다.");
+        }
+
+        // OrderItem 생성
+        OrderItem orderItem = OrderItem.builder()
+            .product(product)
+            .quantity(quantity)
+            .order(this)
+            .build();
+
+        // 주문 상품 추가
+        this.orderItems.add(orderItem);
+
+        // 총가격과 결제 금액 계산하기
+        this.totalPrice = this.totalPrice.add(orderItem.getTotalAmount());
+        this.finalAmount = this.totalPrice.subtract(this.discountAmount)
+                                          .add(this.shippingFee);
+    }
+
+    /**
+     * 할인 금액 적용 (쿠폰)
+     * @param discountAmount 할인 금액
+     */
+    public void applyDiscount(BigDecimal discountAmount) {
+
+        // 주문 상태 검증
+        if (this.orderStatus != OrderStatus.PENDING) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : PENDING 상태의 주문만 할인을 적용할 수 있습니다.");
+        }
+
+        // 할인 금액 검증
+        if (discountAmount == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : discountAmount가 비어있을 수 없습니다.");
+        } else if (discountAmount.compareTo(BigDecimal.ZERO) < 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : discountAmount는 음수가 될 수 없습니다.");
+        }
+
+        // 할인 금액 적용
+        this.discountAmount = this.discountAmount.add(discountAmount);
+        this.finalAmount = this.totalPrice.subtract(this.discountAmount)
+                                          .add(this.shippingFee);
+    }
+
+    /**
+     * 주문 확인 처리
+     * PENDING 상태의 주문만 CONFIRMED 상태로 변경 가능
+     */
+    public void confirm() {
+        if (this.orderStatus != OrderStatus.PENDING) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : PENDING 상태의 주문만 확인할 수 있습니다.");
         }
+        this.orderStatus = OrderStatus.CONFIRMED;
     }
+
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
index ffeca6734..ee0724622 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
@@ -19,8 +19,8 @@
 public class OrderItem extends BaseEntity {
 
     private Integer quantity;
-    private BigDecimal unitPrice;
-    private BigDecimal totalAmount;
+    private BigDecimal unitPrice = BigDecimal.ZERO;
+    private BigDecimal totalAmount = BigDecimal.ZERO;
 
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "order_id")
@@ -33,49 +33,69 @@ public class OrderItem extends BaseEntity {
     @Builder
     private OrderItem(
             Integer quantity,
-            BigDecimal unitPrice,
-            BigDecimal totalAmount,
-            Order order,
-            Product product
+            Product product,
+            Order order
     ) {
         this.quantity = quantity;
-        this.unitPrice = unitPrice;
-        this.totalAmount = totalAmount;
-        this.order = order;
         this.product = product;
+        this.order = order;
+        // 유효성 검사
         guard();
-    }
 
-    public void setOrder(Order order) {
-        if (this.order != null && !this.order.equals(order)) {
-            throw new CoreException(ErrorType.CONFLICT, "OrderItem : Order가 이미 존재합니다.");
-        }
-        this.order = order;
+        // 계산하기
+        this.unitPrice = product.getPrice();
+        this.totalAmount = this.unitPrice.multiply(BigDecimal.valueOf(quantity));
     }
 
     @Override
     protected void guard() {
+        // quantity 검증: null이 아니어야 하며, 0보다 커야 함
         if (quantity == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : quantity가 비어있을 수 없습니다.");
         } else if (quantity <= 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : quantity는 0보다 커야 합니다.");
         }
+        
+        // product 검증: null이 아니어야 함 (주문 상품 정보 필수)
+        if (product == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : product가 비어있을 수 없습니다.");
+        }
 
+        // order 검증: null이 아니어야 함 (주문 정보 필수)
+        if (order == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : order가 비어있을 수 없습니다.");
+        }
+        
+        // unitPrice 검증: null이고 0보다 커야 함
         if (unitPrice == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : unitPrice가 비어있을 수 없습니다.");
         } else if (unitPrice.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : unitPrice는 음수가 될 수 없습니다.");
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : unitPrice는 0보다 커야 합니다.");
         }
 
+        // totalAmount 검증: null이고 0보다 커야 함
         if (totalAmount == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : totalAmount가 비어있을 수 없습니다.");
         } else if (totalAmount.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : totalAmount는 음수가 될 수 없습니다.");
-        }
-
-        if (product == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : product가 비어있을 수 없습니다.");
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : totalAmount는 0보다 커야 합니다.");
         }
     }
+
+    // Order를 통해서만 OrderItem을 생성할 수 있도록 하는 메서드
+    // order는 null일 수 있음 (나중에 setOrder로 설정 가능)
+    // public static OrderItem create(Order order, Integer quantity, Product product) {
+    //     OrderItem orderItem = new OrderItem(quantity, product);
+    //     orderItem.order = order;
+    //     return orderItem;
+    // }
+
+    // public void setOrder(Order order) {
+    //     if (this.order != null && !this.order.equals(order)) {
+    //         throw new CoreException(ErrorType.CONFLICT, "OrderItem : Order가 이미 존재합니다.");
+    //     }
+    //     this.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
index 5481d0938..fd17b0802 100644
--- 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
@@ -1,135 +1,33 @@
 package com.loopers.domain.order;
 
-import com.loopers.domain.payment.Payment;
-import com.loopers.domain.payment.PaymentMethod;
-import com.loopers.domain.point.Point;
-import com.loopers.domain.point.PointService;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.stock.StockService;
 import com.loopers.domain.user.User;
 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;
 
-import java.math.BigDecimal;
 import java.util.List;
+import java.util.Optional;
 
 @Component
 @RequiredArgsConstructor
 public class OrderService {
 
     private final OrderRepository orderRepository;
-    private final ProductRepository productRepository;
-    private final StockService stockService;
-    private final PointService pointService;
 
     /**
-     * 주문 생성 및 처리
-     * 1. 주문 아이템 생성 및 검증
-     * 2. 주문 생성 (PENDING 상태)
-     * 3. 재고 차감
-     * 4. Payment 생성 및 계산
-     * 5. 포인트 차감 (Payment를 통해)
-     * 6. 주문 저장 및 상태 변경
+     * 주문 생성
      */
-    public Order createOrder(User user, List<OrderItemRequest> orderItemRequests) {
-        // 1. 주문 아이템 생성 및 검증
-        List<OrderItem> orderItems = orderItemRequests.stream()
-                .map(request -> {
-                    Product product = productRepository.findById(request.getProductId())
-                            .orElseThrow(() -> new CoreException(
-                                    ErrorType.NOT_FOUND,
-                                    "[productId = " + request.getProductId() + "] Product를 찾을 수 없습니다."
-                            ));
-
-                    // 상품 판매 가능 여부 확인
-                    if (!product.getIsSellable()) {
-                        throw new CoreException(
-                                ErrorType.BAD_REQUEST,
-                                "[productId = " + request.getProductId() + "] 판매 불가능한 상품입니다."
-                        );
-                    }
-
-                    BigDecimal unitPrice = product.getPrice();
-                    BigDecimal totalAmount = unitPrice.multiply(BigDecimal.valueOf(request.getQuantity()));
-
-                    return OrderItem.builder()
-                            .quantity(request.getQuantity())
-                            .unitPrice(unitPrice)
-                            .totalAmount(totalAmount)
-                            .product(product)
-                            .build();
-                })
-                .toList();
-
-        // 2. 주문 금액 계산
-        BigDecimal totalPrice = orderItems.stream()
-                .map(OrderItem::getTotalAmount)
-                .reduce(BigDecimal.ZERO, BigDecimal::add);
-
-        BigDecimal discountAmount = BigDecimal.ZERO; // 할인 로직은 추후 구현
-        BigDecimal shippingFee = BigDecimal.ZERO; // 배송비 로직은 추후 구현
-
-        // 3. 주문 생성 (PENDING 상태, finalAmount는 임시로 0 설정)
-        Order order = Order.builder()
-                .finalAmount(BigDecimal.ZERO) // Payment에서 계산된 값으로 업데이트됨
-                .totalPrice(totalPrice)
-                .discountAmount(discountAmount)
-                .shippingFee(shippingFee)
-                .orderStatus(OrderStatus.PENDING)
-                .user(user)
-                .orderItems(orderItems)
-                .build();
-
-        // 4. 재고 차감 (재고 부족 시 예외 발생)
-        for (OrderItem orderItem : orderItems) {
-            stockService.decreaseQuantity(
-                    orderItem.getProduct().getId(),
-                    Long.valueOf(orderItem.getQuantity())
-            );
-        }
-
-        // 5. Payment 생성 및 계산
-        Payment payment = Payment.create(
-                order,
-                totalPrice,
-                discountAmount,
-                shippingFee,
-                PaymentMethod.POINT
-        );
-
-        // Payment에서 계산된 최종 금액을 Order에 적용
-        BigDecimal finalAmount = payment.getAmount();
-        order.setFinalAmount(finalAmount);
-
-        // 6. 포인트 조회
-        Point point = pointService.findByUserLoginId(user.getLoginId())
-                .orElseThrow(() -> new CoreException(
-                        ErrorType.NOT_FOUND,
-                        "[loginId = " + user.getLoginId() + "] Point를 찾을 수 없습니다."
-                ));
-
-        // 7. Payment를 통해 포인트 차감 처리 (포인트 부족 시 예외 발생)
-        payment.processWithPoint(point);
-
-        // 8. 주문 저장
-        Order savedOrder = orderRepository.save(order)
-                .orElseThrow(() -> new CoreException(
-                        ErrorType.INTERNAL_ERROR,
-                        "주문 저장에 실패했습니다."
-                ));
-
-        // 9. 주문 상태를 CONFIRMED로 변경
-        savedOrder.confirm();
-        
-        return savedOrder;
+    @Transactional
+    public Optional<Order> saveOrder(Order order) {
+        return orderRepository.save(order);
     }
 
     /**
      * 주문 조회
      */
+    @Transactional(readOnly = true)
     public Order findOrderById(Long orderId) {
         return orderRepository.findById(orderId)
                 .orElseThrow(() -> new CoreException(
@@ -141,29 +39,9 @@ public Order findOrderById(Long orderId) {
     /**
      * 유저의 주문 목록 조회
      */
+    @Transactional(readOnly = true)
     public List<Order> findOrdersByUser(User user) {
         return orderRepository.findByUser(user);
     }
-
-    /**
-     * 주문 아이템 요청 DTO
-     */
-    public static class OrderItemRequest {
-        private Long productId;
-        private Integer quantity;
-
-        public OrderItemRequest(Long productId, Integer quantity) {
-            this.productId = productId;
-            this.quantity = quantity;
-        }
-
-        public Long getProductId() {
-            return productId;
-        }
-
-        public Integer getQuantity() {
-            return quantity;
-        }
-    }
 }
 
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
index 709ed4f4e..e789529e3 100644
--- 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
@@ -1,6 +1,11 @@
 package com.loopers.domain.order;
 
 public enum OrderStatus {
-    PENDING, CONFIRMED, SHIPPING, DELIVERED, CANCELLED
+    PENDING,           // 주문 대기
+    PAYMENT_FAILED,    // 결제 실패 (요구사항에 명시됨)
+    CONFIRMED,         // 결제 완료/주문 확인
+    SHIPPING,          // 배송 중
+    DELIVERED,         // 배송 완료
+    CANCELLED          // 취소됨
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
index 6947db7e4..93fe9215a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
@@ -2,7 +2,6 @@
 
 import com.loopers.domain.BaseEntity;
 import com.loopers.domain.order.Order;
-import com.loopers.domain.point.Point;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -19,7 +18,7 @@
 @Getter
 public class Payment extends BaseEntity {
 
-    private String pgTransactionId;
+    // private String pgTransactionId;
     
     @Enumerated(EnumType.STRING)
     private PaymentMethod method;
@@ -27,7 +26,7 @@ public class Payment extends BaseEntity {
     private BigDecimal amount;
     
     @Enumerated(EnumType.STRING)
-    private PaymentStatus paymentStatus;
+    private PaymentStatus paymentStatus = PaymentStatus.PENDING;
 
     @OneToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "order_id")
@@ -35,119 +34,44 @@ public class Payment extends BaseEntity {
 
     @Builder
     private Payment(
-            String pgTransactionId,
+            // String pgTransactionId,
             PaymentMethod method,
-            BigDecimal amount,
             PaymentStatus paymentStatus,
             Order order
     ) {
-        this.pgTransactionId = pgTransactionId;
+        // this.pgTransactionId = pgTransactionId;
         this.method = method;
-        this.amount = amount;
         this.paymentStatus = paymentStatus;
         this.order = order;
-        guard();
-    }
-
-    public void setOrder(Order order) {
-        if (this.order != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Payment : Order가 이미 존재합니다.");
-        }
-        this.order = order;
-    }
-
-    /**
-     * 결제 금액 계산
-     * totalPrice - discountAmount + shippingFee = finalAmount
-     */
-    public static BigDecimal calculateFinalAmount(
-            BigDecimal totalPrice,
-            BigDecimal discountAmount,
-            BigDecimal shippingFee
-    ) {
-        if (totalPrice == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : totalPrice가 비어있을 수 없습니다.");
-        }
-        if (discountAmount == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : discountAmount가 비어있을 수 없습니다.");
-        }
-        if (shippingFee == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : shippingFee가 비어있을 수 없습니다.");
-        }
-
-        BigDecimal finalAmount = totalPrice.subtract(discountAmount).add(shippingFee);
-        
-        if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : 최종 결제 금액은 음수가 될 수 없습니다.");
+        if(order != null) {
+            this.amount = order.getFinalAmount();
         }
-        
-        return finalAmount;
-    }
-
-    /**
-     * Payment 생성 및 계산
-     */
-    public static Payment create(
-            Order order,
-            BigDecimal totalPrice,
-            BigDecimal discountAmount,
-            BigDecimal shippingFee,
-            PaymentMethod method
-    ) {
-        BigDecimal finalAmount = calculateFinalAmount(totalPrice, discountAmount, shippingFee);
-        
-        return Payment.builder()
-                .method(method)
-                .amount(finalAmount)
-                .paymentStatus(PaymentStatus.PENDING)
-                .order(order)
-                .build();
-    }
-
-    /**
-     * 포인트 결제 처리
-     * 포인트 차감 및 결제 상태 변경
-     */
-    public void processWithPoint(Point point) {
-        if (this.paymentStatus != PaymentStatus.PENDING) {
-            throw new CoreException(
-                    ErrorType.BAD_REQUEST,
-                    "Payment : PENDING 상태의 결제만 처리할 수 있습니다."
-            );
-        }
-
-        if (this.method != PaymentMethod.POINT) {
-            throw new CoreException(
-                    ErrorType.BAD_REQUEST,
-                    "Payment : 포인트 결제 방식이 아닙니다."
-            );
-        }
-
-        // 포인트 차감 (포인트 부족 시 예외 발생)
-        point.deduct(this.amount);
-
-        // 결제 상태를 COMPLETED로 변경
-        this.paymentStatus = PaymentStatus.COMPLETED;
+        guard();
     }
 
     @Override
     protected void guard() {
+        // method 검증: null이 아니어야 함
         if (method == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : method가 비어있을 수 없습니다.");
         }
 
+        // amount 검증: null이 아니어야 하며, 0 이상이어야 함
         if (amount == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : amount가 비어있을 수 없습니다.");
         } else if (amount.compareTo(BigDecimal.ZERO) < 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : amount는 음수가 될 수 없습니다.");
         }
 
+        // paymentStatus 검증: null이 아니어야 함
         if (paymentStatus == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : paymentStatus가 비어있을 수 없습니다.");
         }
 
-        // order는 생성 시점에는 null일 수 있음 (Payment.create()에서 설정)
-        // guard에서는 검증하지 않음
+        // order 검증: null이 아니어야 함 (결제 대상 주문 정보 필수)
+        if (order == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : order가 비어있을 수 없습니다.");
+        }
     }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
index a81f0c68b..db284ba0b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentStatus.java
@@ -1,6 +1,10 @@
 package com.loopers.domain.payment;
 
 public enum PaymentStatus {
-    PENDING, COMPLETED, FAILED, CANCELLED, REFUNDED
+    PENDING,       // 결제 대기
+    COMPLETED,     // 결제 완료
+    FAILED,        // 결제 실패
+    CANCELLED,     // 결제 취소
+    REFUNDED       // 환불됨
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
index d6c3b418b..b1f928f3d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
@@ -2,7 +2,6 @@
 
 import com.loopers.application.order.OrderFacade;
 import com.loopers.application.order.OrderInfo;
-import com.loopers.domain.order.OrderService;
 import com.loopers.interfaces.api.ApiResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
@@ -22,12 +21,7 @@ public ApiResponse<OrderDto.OrderResponse> createOrder(
             @RequestHeader(value = "X-USER-ID") String xUserId,
             @RequestBody OrderDto.CreateOrderRequest request
     ) {
-        // DTO를 도메인 서비스의 OrderItemRequest로 변환
-        List<OrderService.OrderItemRequest> orderItemRequests = request.items().stream()
-                .map(item -> new OrderService.OrderItemRequest(item.productId(), item.quantity()))
-                .toList();
-
-        OrderInfo orderInfo = orderFacade.createOrder(xUserId, orderItemRequests);
+        OrderInfo orderInfo = orderFacade.createOrder(xUserId, request);
         return ApiResponse.success(OrderDto.OrderResponse.from(orderInfo));
     }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderDto.java
index 08079cd2b..eead138b4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderDto.java
@@ -8,16 +8,17 @@
 
 public class OrderDto {
 
+    @Builder
+    public record OrderItemRequest(
+            Long productId,
+            Integer quantity
+    ) {}
+
     @Builder
     public record CreateOrderRequest(
             List<OrderItemRequest> items
-    ) {
-        @Builder
-        public record OrderItemRequest(
-                Long productId,
-                Integer quantity
-        ) {}
-    }
+            , List<Long> couponIds  // 쿠폰 ID 리스트
+    ) {}
 
     @Builder
     public record OrderResponse(
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..f9c3e9946
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
@@ -0,0 +1,407 @@
+package com.loopers.application.order;
+
+import com.loopers.application.user.UserFacade;
+import com.loopers.application.user.UserInfo;
+import com.loopers.domain.coupon.Coupon;
+import com.loopers.domain.coupon.CouponRepository;
+import com.loopers.domain.coupon.CouponType;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderStatus;
+import com.loopers.domain.point.Point;
+import com.loopers.domain.point.PointService;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.stock.Stock;
+import com.loopers.domain.stock.StockRepository;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.infrastructure.brand.BrandJpaRepository;
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.interfaces.api.order.OrderDto;
+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.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 java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("OrderFacade 통합 테스트")
+@SpringBootTest
+class OrderFacadeIntegrationTest {
+
+    @Autowired
+    private OrderFacade orderFacade;
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private ProductRepository productRepository;
+
+    @Autowired
+    private BrandJpaRepository brandJpaRepository;
+
+    @Autowired
+    private StockRepository stockRepository;
+
+    @Autowired
+    private com.loopers.domain.stock.StockService stockService;
+
+    @Autowired
+    private PointService pointService;
+
+    @Autowired
+    private CouponRepository couponRepository;
+
+    @Autowired
+    private com.loopers.domain.order.OrderService orderService;
+
+    @Autowired
+    private com.loopers.domain.coupon.CouponService couponService;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    private User testUser;
+    private Product testProduct;
+    private Long testProductId;
+    
+    private final String testLoginId = "test34";
+
+    @BeforeEach
+    void setUp() {
+        // 테스트용 User 생성
+        UserInfo userInfo = UserInfo.builder()
+                .loginId(testLoginId)
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+        userFacade.saveUser(userInfo);
+
+        testUser = userRepository.findByLoginId(testLoginId)
+                .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"));
+
+        // 테스트용 Brand 생성
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+        Brand savedBrand = brandJpaRepository.save(brand);
+
+        // 테스트용 Product 생성
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(10000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(savedBrand)
+                .build();
+        Product savedProduct = productRepository.save(product)
+                .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
+        testProduct = savedProduct;
+        testProductId = savedProduct.getId();
+
+        // 테스트용 Stock 생성 (재고 100개로 설정)
+        // Product 생성 시 Stock이 자동으로 생성되므로, 재고를 100개로 증가
+        Stock stock = stockRepository.findByProductId(testProductId)
+                .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
+        // 현재 재고를 확인하고 100개가 되도록 조정
+        long currentQuantity = stock.getQuantity();
+        if (currentQuantity < 100L) {
+            stockService.increaseQuantity(testProductId, 100L - currentQuantity);
+        }
+
+        // 테스트용 포인트 충전 (100000원)
+        pointService.charge(testLoginId, BigDecimal.valueOf(100000));
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("createOrder 테스트")
+    @Nested
+    class CreateOrderTest {
+
+        @DisplayName("성공 케이스: 모든 조건을 만족하는 주문 생성 성공")
+        @Test
+        void createOrder_withValidRequest_Success() {
+            // arrange
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(2)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            BigDecimal expectedTotalPrice = BigDecimal.valueOf(10000).multiply(BigDecimal.valueOf(2)); // 20000
+            BigDecimal expectedFinalAmount = expectedTotalPrice; // 할인 없음
+
+            // act
+            OrderInfo orderInfo = orderFacade.createOrder(testLoginId, request);
+
+            // assert
+            assertNotNull(orderInfo);
+            assertAll(
+                    () -> assertNotNull(orderInfo.id(), "주문 ID는 null이 아니어야 함"),
+                    () -> assertEquals(0, expectedTotalPrice.compareTo(orderInfo.totalPrice()), "총 가격이 일치해야 함"),
+                    () -> assertEquals(0, expectedFinalAmount.compareTo(orderInfo.finalAmount()), "최종 금액이 일치해야 함"),
+                    () -> assertEquals(OrderStatus.CONFIRMED, orderInfo.orderStatus(), "주문 상태는 CONFIRMED여야 함"),
+                    () -> assertEquals(testLoginId, orderInfo.userLoginId(), "사용자 로그인 ID가 일치해야 함"),
+                    () -> assertEquals(1, orderInfo.orderItems().size(), "주문 상품 수가 일치해야 함")
+            );
+
+            // 재고가 차감되었는지 확인
+            Stock stock = stockRepository.findByProductId(testProductId)
+                    .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
+            assertEquals(98L, stock.getQuantity(), "재고가 2개 차감되어야 함 (100 - 2 = 98)");
+
+            // 포인트가 차감되었는지 확인
+            Point point = pointService.findByUserLoginId(testLoginId)
+                    .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
+            BigDecimal expectedPoint = BigDecimal.valueOf(100000).subtract(expectedFinalAmount);
+            assertEquals(0, expectedPoint.compareTo(point.getAmount()), "포인트가 차감되어야 함");
+        }
+
+        @DisplayName("성공 케이스: 쿠폰 적용하여 주문 생성 성공")
+        @Test
+        void createOrder_withCoupon_Success() {
+            // arrange
+            // 쿠폰 생성
+            Coupon coupon = Coupon.builder()
+                    .couponType(CouponType.FIXED_AMOUNT)
+                    .discountValue(BigDecimal.valueOf(5000))
+                    .user(testUser)
+                    .build();
+            Coupon savedCoupon = couponRepository.save(coupon)
+                    .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(2)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(List.of(savedCoupon.getId()))
+                    .build();
+
+            BigDecimal expectedTotalPrice = BigDecimal.valueOf(10000).multiply(BigDecimal.valueOf(2)); // 20000
+            BigDecimal expectedDiscount = BigDecimal.valueOf(5000);
+            BigDecimal expectedFinalAmount = expectedTotalPrice.subtract(expectedDiscount); // 15000
+
+            // act
+            OrderInfo orderInfo = orderFacade.createOrder(testLoginId, request);
+
+            // assert
+            assertNotNull(orderInfo);
+            assertAll(
+                    () -> assertEquals(0, expectedTotalPrice.compareTo(orderInfo.totalPrice()), "총 가격이 일치해야 함"),
+                    () -> assertEquals(0, expectedDiscount.compareTo(orderInfo.discountAmount()), "할인 금액이 일치해야 함"),
+                    () -> assertEquals(0, expectedFinalAmount.compareTo(orderInfo.finalAmount()), "최종 금액이 일치해야 함")
+            );
+
+            // 쿠폰이 사용되었는지 확인
+            Coupon usedCoupon = couponRepository.findById(savedCoupon.getId())
+                    .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다"));
+            assertTrue(usedCoupon.getIsUsed(), "쿠폰은 사용된 상태여야 함");
+            assertNotNull(usedCoupon.getOrder(), "쿠폰의 order는 null이 아니어야 함");
+        }
+
+        @DisplayName("실패 케이스: 존재하지 않는 User로 주문 생성 시 NOT_FOUND 예외 발생")
+        @Test
+        void createOrder_withNonExistentUser_NotFound() {
+            // arrange
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(2)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder("nonexistent", request)
+            );
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("[loginId = nonexistent] User를 찾을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스: 존재하지 않는 Product로 주문 생성 시 NOT_FOUND 예외 발생")
+        @Test
+        void createOrder_withNonExistentProduct_NotFound() {
+            // arrange
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(99999L)
+                            .quantity(2)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("[productId = 99999] Product를 찾을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스: 재고 부족 시 BAD_REQUEST 예외 발생")
+        @Test
+        void createOrder_withInsufficientStock_BadRequest() {
+            // arrange
+            // 재고를 5개로 설정 (현재 재고를 확인하고 5개가 되도록 조정)
+            Stock stock = stockRepository.findByProductId(testProductId)
+                    .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
+            long currentQuantity = stock.getQuantity();
+            if (currentQuantity > 5L) {
+                // 재고를 5개로 만들기 위해 차감
+                stockService.decreaseQuantity(testProductId, currentQuantity - 5L);
+            } else if (currentQuantity < 5L) {
+                // 재고를 5개로 만들기 위해 증가
+                stockService.increaseQuantity(testProductId, 5L - currentQuantity);
+            }
+
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(10) // 재고보다 많은 수량
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("재고가 부족"));
+        }
+
+        @DisplayName("실패 케이스: 포인트 부족 시 BAD_REQUEST 예외 발생")
+        @Test
+        void createOrder_withInsufficientPoint_BadRequest() {
+            // arrange
+            // 포인트를 1000원으로 조정 (기존 포인트를 모두 차감 후 1000원 충전)
+            Point currentPoint = pointService.findByUserLoginId(testLoginId)
+                    .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
+            BigDecimal currentAmount = currentPoint.getAmount();
+            if (currentAmount.compareTo(BigDecimal.valueOf(1000)) > 0) {
+                pointService.deduct(testLoginId, currentAmount.subtract(BigDecimal.valueOf(1000)));
+            } else if (currentAmount.compareTo(BigDecimal.valueOf(1000)) < 0) {
+                pointService.charge(testLoginId, BigDecimal.valueOf(1000).subtract(currentAmount));
+            }
+
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(10) // 100000원 (포인트보다 많은 금액)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("포인트가 부족"));
+        }
+
+        @DisplayName("실패 케이스: 쿠폰 사용 불가 시 BAD_REQUEST 예외 발생")
+        @Test
+        void createOrder_withUnavailableCoupon_BadRequest() {
+            // arrange
+            // 쿠폰 생성
+            Coupon coupon = Coupon.builder()
+                    .couponType(CouponType.FIXED_AMOUNT)
+                    .discountValue(BigDecimal.valueOf(5000))
+                    .user(testUser)
+                    .build();
+            Coupon savedCoupon = couponRepository.save(coupon)
+                    .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+
+            // 쿠폰을 먼저 사용 (실제 Order를 생성하여 쿠폰 사용)
+            Order firstOrder = Order.builder()
+                    .discountAmount(BigDecimal.ZERO)
+                    .shippingFee(BigDecimal.ZERO)
+                    .user(testUser)
+                    .build();
+            firstOrder.addOrderItem(testProduct, 1);
+            
+            // Order 저장
+            Order savedFirstOrder = orderService.saveOrder(firstOrder)
+                    .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
+
+            // CouponService를 사용하여 쿠폰 사용 (guard() 검증을 통과하도록)
+            couponService.useCoupon(savedFirstOrder, savedCoupon.getId());
+
+            // 이미 사용된 쿠폰으로 다시 주문 생성 시도
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(2)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(List.of(savedCoupon.getId()))
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
new file mode 100644
index 000000000..d919e64c8
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
@@ -0,0 +1,200 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+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.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("OrderItem 테스트")
+public class OrderItemTest {
+
+    // 고정 fixture
+    private final Integer validQuantity = 2;
+
+    private Product createValidProduct() {
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+
+        // Product 생성 시 필드 초기화로 Stock이 자동 생성됨 (quantity=0L)
+        // Product.builder()로 생성하면 stock 필드가 자동으로 초기화됨
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(5000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(brand)
+                .build();
+
+        return product;
+    }
+
+    private Order createValidOrder() {
+        User user = User.builder()
+                .loginId("testuser1")
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+
+        Order order = Order.builder()
+                .discountAmount(BigDecimal.valueOf(1000))
+                .shippingFee(BigDecimal.valueOf(2000))
+                .user(user)
+                .build();
+
+        return order;
+    }
+
+    @DisplayName("OrderItem 엔티티 생성")
+    @Nested
+    class CreateOrderItemTest {
+
+        @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 OrderItem 객체 생성 성공")
+        @Test
+        void createOrderItem_withValidFields_Success() {
+            // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+            BigDecimal expectedUnitPrice = product.getPrice(); // 생성 시 product.getPrice()로 자동 설정됨
+            BigDecimal expectedTotalAmount = expectedUnitPrice.multiply(BigDecimal.valueOf(validQuantity)); // unitPrice * quantity로 자동 계산됨
+
+            // act
+            OrderItem orderItem = OrderItem.builder()
+                    .quantity(validQuantity)
+                    .product(product)
+                    .order(order)
+                    .build();
+
+            // assert
+            assertNotNull(orderItem);
+            assertAll(
+                    () -> assertEquals(validQuantity, orderItem.getQuantity()),
+                    () -> assertEquals(expectedUnitPrice, orderItem.getUnitPrice(), "unitPrice는 product.getPrice()로 자동 설정되어야 함"),
+                    () -> assertEquals(expectedTotalAmount, orderItem.getTotalAmount(), "totalAmount는 unitPrice * quantity로 자동 계산되어야 함"),
+                    () -> assertEquals(product, orderItem.getProduct()),
+                    () -> assertEquals(order, orderItem.getOrder())
+            );
+        }
+
+        @DisplayName("실패 케이스 : quantity가 null이면 예외 발생")
+        @Test
+        void createOrderItem_withNullQuantity_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(null)
+                            .product(product)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("quantity가 비어있을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : quantity가 0이면 예외 발생")
+        @Test
+        void createOrderItem_withZeroQuantity_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(0)
+                            .product(product)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("quantity는 0보다 커야 합니다"));
+        }
+
+        @DisplayName("실패 케이스 : quantity가 음수이면 예외 발생")
+        @Test
+        void createOrderItem_withNegativeQuantity_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(-1)
+                            .product(product)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("quantity는 0보다 커야 합니다"));
+        }
+
+        // unitPrice와 totalAmount는 자동으로 계산되므로 직접 설정할 수 없음
+        // 따라서 unitPrice와 totalAmount에 대한 실패 케이스 테스트는 제거됨
+
+        @DisplayName("실패 케이스 : product가 null이면 예외 발생")
+        @Test
+        void createOrderItem_withNullProduct_ThrowsException() {
+            // arrange
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(validQuantity)
+                            .product(null)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("product가 비어있을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : order가 null이면 예외 발생")
+        @Test
+        void createOrderItem_withNullOrder_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(validQuantity)
+                            .product(product)
+                            .order(null)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("order가 비어있을 수 없습니다"));
+        }
+    }
+
+}
+
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
deleted file mode 100644
index cdf60c891..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
+++ /dev/null
@@ -1,273 +0,0 @@
-package com.loopers.domain.order;
-
-import com.loopers.domain.brand.Brand;
-import com.loopers.domain.brand.BrandStatus;
-import com.loopers.domain.point.Point;
-import com.loopers.domain.point.PointService;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.product.ProductStatus;
-import com.loopers.domain.stock.Stock;
-import com.loopers.domain.stock.StockService;
-import com.loopers.domain.user.Gender;
-import com.loopers.domain.user.User;
-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.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.math.BigDecimal;
-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.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.*;
-
-@ExtendWith(MockitoExtension.class)
-@DisplayName("OrderService 테스트")
-class OrderServiceTest {
-
-    @Mock
-    private OrderRepository orderRepository;
-
-    @Mock
-    private ProductRepository productRepository;
-
-    @Mock
-    private StockService stockService;
-
-    @Mock
-    private PointService pointService;
-
-    @InjectMocks
-    private OrderService orderService;
-
-    private User user;
-    private Product product;
-    private Stock stock;
-    private Point point;
-
-    @BeforeEach
-    void setUp() {
-        user = User.builder()
-                .loginId("testuser1")
-                .email("test@test.com")
-                .birthday("1990-01-01")
-                .gender(Gender.MALE)
-                .build();
-
-        Brand brand = Brand.builder()
-                .name("Test Brand")
-                .description("Test Description")
-                .status(BrandStatus.ON_SALE)
-                .isVisible(true)
-                .isSellable(true)
-                .build();
-
-        stock = Stock.builder()
-                .quantity(100L)
-                .product(null)
-                .build();
-
-        product = Product.builder()
-                .name("Test Product")
-                .description("Test Description")
-                .price(BigDecimal.valueOf(10000))
-                .likeCount(0L)
-                .status(ProductStatus.ON_SALE)
-                .isVisible(true)
-                .isSellable(true)
-                .brand(brand)
-                .stock(null)
-                .build();
-
-        stock.setProduct(product);
-        product.setStock(stock);
-
-        point = Point.builder()
-                .amount(BigDecimal.valueOf(50000))
-                .user(user)
-                .build();
-
-        user.setPoint(point);
-        point.setUser(user);
-    }
-
-    @DisplayName("정상 주문 흐름")
-    @Nested
-    class NormalOrderFlow {
-
-        @Test
-        @DisplayName("성공: 정상적인 주문 생성")
-        void createOrder_success() {
-            // given
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(product.getId(), 2)
-            );
-
-            when(productRepository.findById(anyLong())).thenReturn(Optional.of(product));
-            doNothing().when(stockService).decreaseQuantity(anyLong(), anyLong());
-            when(pointService.findByUserLoginId(any())).thenReturn(Optional.of(point));
-            when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> {
-                Order order = invocation.getArgument(0);
-                return Optional.of(order);
-            });
-
-            // when
-            Order result = orderService.createOrder(user, requests);
-
-            // then
-            assertThat(result).isNotNull();
-            assertThat(result.getOrderStatus()).isEqualTo(OrderStatus.CONFIRMED);
-            assertThat(result.getOrderItems()).hasSize(1);
-            assertThat(result.getFinalAmount()).isEqualByComparingTo(BigDecimal.valueOf(20000));
-
-            verify(stockService, times(1)).decreaseQuantity(anyLong(), eq(2L));
-            verify(pointService, times(1)).findByUserLoginId(user.getLoginId());
-            verify(orderRepository, times(1)).save(any(Order.class));
-        }
-    }
-
-    @DisplayName("예외 주문 흐름")
-    @Nested
-    class ExceptionOrderFlow {
-
-        @Test
-        @DisplayName("실패: 상품을 찾을 수 없음")
-        void createOrder_productNotFound() {
-            // given
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(999L, 2)
-            );
-
-            when(productRepository.findById(999L)).thenReturn(Optional.empty());
-
-            // when & then
-            assertThatThrownBy(() -> orderService.createOrder(user, requests))
-                    .isInstanceOf(CoreException.class)
-                    .satisfies(exception -> {
-                        CoreException coreException = (CoreException) exception;
-                        assertThat(coreException.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
-                    });
-
-            verify(stockService, never()).decreaseQuantity(anyLong(), anyLong());
-            verify(pointService, never()).findByUserLoginId(any());
-        }
-
-        @Test
-        @DisplayName("실패: 판매 불가능한 상품")
-        void createOrder_productNotSellable() {
-            // given
-            Product unsellableProduct = Product.builder()
-                    .name("Unsellable Product")
-                    .description("Test")
-                    .price(BigDecimal.valueOf(10000))
-                    .likeCount(0L)
-                    .status(ProductStatus.ON_SALE)
-                    .isVisible(true)
-                    .isSellable(false) // 판매 불가
-                    .build();
-
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(unsellableProduct.getId(), 2)
-            );
-
-            when(productRepository.findById(anyLong())).thenReturn(Optional.of(unsellableProduct));
-
-            // when & then
-            assertThatThrownBy(() -> orderService.createOrder(user, requests))
-                    .isInstanceOf(CoreException.class)
-                    .satisfies(exception -> {
-                        CoreException coreException = (CoreException) exception;
-                        assertThat(coreException.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-                    });
-
-            verify(stockService, never()).decreaseQuantity(anyLong(), anyLong());
-        }
-
-        @Test
-        @DisplayName("실패: 재고 부족")
-        void createOrder_insufficientStock() {
-            // given
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(product.getId(), 200) // 재고보다 많은 수량
-            );
-
-            when(productRepository.findById(anyLong())).thenReturn(Optional.of(product));
-            doThrow(new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."))
-                    .when(stockService).decreaseQuantity(anyLong(), anyLong());
-
-            // when & then
-            assertThatThrownBy(() -> orderService.createOrder(user, requests))
-                    .isInstanceOf(CoreException.class)
-                    .satisfies(exception -> {
-                        CoreException coreException = (CoreException) exception;
-                        assertThat(coreException.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-                    });
-
-            verify(pointService, never()).findByUserLoginId(any());
-            verify(orderRepository, never()).save(any(Order.class));
-        }
-
-        @Test
-        @DisplayName("실패: 포인트 부족")
-        void createOrder_insufficientPoint() {
-            // given
-            Point lowPoint = Point.builder()
-                    .amount(BigDecimal.valueOf(1000)) // 주문 금액보다 적은 포인트
-                    .user(user)
-                    .build();
-
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(product.getId(), 2) // 20000원 주문
-            );
-
-            when(productRepository.findById(anyLong())).thenReturn(Optional.of(product));
-            doNothing().when(stockService).decreaseQuantity(anyLong(), anyLong());
-            when(pointService.findByUserLoginId(any())).thenReturn(Optional.of(lowPoint));
-
-            // when & then
-            assertThatThrownBy(() -> orderService.createOrder(user, requests))
-                    .isInstanceOf(CoreException.class)
-                    .satisfies(exception -> {
-                        CoreException coreException = (CoreException) exception;
-                        assertThat(coreException.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-                    });
-
-            verify(orderRepository, never()).save(any(Order.class));
-        }
-
-        @Test
-        @DisplayName("실패: 포인트를 찾을 수 없음")
-        void createOrder_pointNotFound() {
-            // given
-            List<OrderService.OrderItemRequest> requests = List.of(
-                    new OrderService.OrderItemRequest(product.getId(), 2)
-            );
-
-            when(productRepository.findById(anyLong())).thenReturn(Optional.of(product));
-            doNothing().when(stockService).decreaseQuantity(anyLong(), anyLong());
-            when(pointService.findByUserLoginId(any())).thenReturn(Optional.empty());
-
-            // when & then
-            assertThatThrownBy(() -> orderService.createOrder(user, requests))
-                    .isInstanceOf(CoreException.class)
-                    .satisfies(exception -> {
-                        CoreException coreException = (CoreException) exception;
-                        assertThat(coreException.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
-                    });
-
-            verify(orderRepository, never()).save(any(Order.class));
-        }
-    }
-}
-
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..004191128
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -0,0 +1,308 @@
+package com.loopers.domain.order;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+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.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Order 테스트")
+public class OrderTest {
+
+    // 고정 fixture
+    private final BigDecimal validDiscountAmount = BigDecimal.valueOf(1000);
+    private final BigDecimal validShippingFee = BigDecimal.valueOf(2000);
+
+    private User createValidUser() {
+        return User.builder()
+                .loginId("testuser1")
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+    }
+
+    private Product createValidProduct() {
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+
+        // Product 생성 시 필드 초기화로 Stock이 자동 생성됨 (quantity=0L)
+        // Product.builder()로 생성하면 stock 필드가 자동으로 초기화됨
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(5000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(brand)
+                .build();
+
+        return product;
+    }
+
+
+    @DisplayName("Order 엔티티 생성")
+    @Nested
+    class CreateOrderTest {
+
+        @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 Order 객체 생성 성공")
+        @Test
+        void createOrder_withValidFields_Success() {
+            // arrange
+            User user = createValidUser();
+            Product product = createValidProduct();
+            
+            // act
+            Order order = Order.builder()
+                    .discountAmount(validDiscountAmount)
+                    .shippingFee(validShippingFee)
+                    .user(user)
+                    .build();
+            
+            // OrderItem 추가
+            order.addOrderItem(product, 2);
+            
+            // 생성 시 자동 계산되는 값들
+            // product.getPrice() * quantity = 5000 * 2 = 10000
+            BigDecimal expectedTotalPrice = product.getPrice().multiply(BigDecimal.valueOf(2));
+            BigDecimal expectedFinalAmount = expectedTotalPrice
+                    .subtract(validDiscountAmount)
+                    .add(validShippingFee); // totalPrice - discountAmount + shippingFee
+
+            // assert
+            assertNotNull(order);
+            assertAll(
+                    () -> assertEquals(validDiscountAmount, order.getDiscountAmount()),
+                    () -> assertEquals(validShippingFee, order.getShippingFee()),
+                    () -> assertEquals(OrderStatus.PENDING, order.getOrderStatus(), "orderStatus는 기본값 PENDING이어야 함"),
+                    () -> assertEquals(user, order.getUser()),
+                    () -> assertEquals(1, order.getOrderItems().size()),
+                    // 자동 계산 검증
+                    () -> assertEquals(expectedTotalPrice, order.getTotalPrice(), "totalPrice는 orderItems의 totalAmount 합계로 자동 계산되어야 함"),
+                    () -> assertEquals(expectedFinalAmount, order.getFinalAmount(), "finalAmount는 totalPrice - discountAmount + shippingFee로 자동 계산되어야 함"),
+                    // 양방향 관계 설정 검증
+                    () -> assertEquals(order, order.getOrderItems().get(0).getOrder(), "각 OrderItem의 order가 Order를 참조해야 함 (양방향 관계)")
+            );
+        }
+
+
+        @DisplayName("실패 케이스 : discountAmount가 null이면 예외 발생")
+        @Test
+        void createOrder_withNullDiscountAmount_ThrowsException() {
+            // arrange
+            User user = createValidUser();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Order.builder()
+                            .discountAmount(null)
+                            .shippingFee(validShippingFee)
+                            .user(user)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("discountAmount가 비어있을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : discountAmount가 음수이면 예외 발생")
+        @Test
+        void createOrder_withNegativeDiscountAmount_ThrowsException() {
+            // arrange
+            User user = createValidUser();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Order.builder()
+                            .discountAmount(BigDecimal.valueOf(-1000))
+                            .shippingFee(validShippingFee)
+                            .user(user)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("discountAmount는 음수가 될 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : shippingFee가 null이면 예외 발생")
+        @Test
+        void createOrder_withNullShippingFee_ThrowsException() {
+            // arrange
+            User user = createValidUser();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Order.builder()
+                            .discountAmount(validDiscountAmount)
+                            .shippingFee(null)
+                            .user(user)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("shippingFee가 비어있을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : shippingFee가 음수이면 예외 발생")
+        @Test
+        void createOrder_withNegativeShippingFee_ThrowsException() {
+            // arrange
+            User user = createValidUser();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Order.builder()
+                            .discountAmount(validDiscountAmount)
+                            .shippingFee(BigDecimal.valueOf(-1000))
+                            .user(user)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("shippingFee는 음수가 될 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : user가 null이면 예외 발생")
+        @Test
+        void createOrder_withNullUser_ThrowsException() {
+            // arrange
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Order.builder()
+                            .discountAmount(validDiscountAmount)
+                            .shippingFee(validShippingFee)
+                            .user(null)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("user가 비어있을 수 없습니다"));
+        }
+
+    }
+
+    @DisplayName("Order addOrderItem 메서드")
+    @Nested
+    class AddOrderItemTest {
+
+        @DisplayName("성공 케이스 : PENDING 상태일 때 OrderItem 추가 성공")
+        @Test
+        void addOrderItem_whenStatusIsPending_Success() {
+            // arrange
+            User user = createValidUser();
+            Product product = createValidProduct();
+            
+            Order order = Order.builder()
+                    .discountAmount(validDiscountAmount)
+                    .shippingFee(validShippingFee)
+                    .user(user)
+                    .build();
+            
+            // 첫 번째 OrderItem 추가
+            order.addOrderItem(product, 2);
+            BigDecimal initialTotalPrice = order.getTotalPrice();
+            
+            // 두 번째 OrderItem 추가를 위한 예상 값 계산
+            BigDecimal newItemTotalAmount = product.getPrice().multiply(BigDecimal.valueOf(1));
+            BigDecimal expectedTotalPrice = initialTotalPrice.add(newItemTotalAmount);
+            BigDecimal expectedFinalAmount = expectedTotalPrice
+                    .subtract(validDiscountAmount)
+                    .add(validShippingFee);
+
+            // act
+            order.addOrderItem(product, 1);
+
+            // assert
+            assertAll(
+                    () -> assertEquals(2, order.getOrderItems().size(), "OrderItem이 추가되어야 함"),
+                    () -> assertEquals(expectedTotalPrice, order.getTotalPrice(), "totalPrice는 기존 값 + 새 OrderItem의 totalAmount여야 함"),
+                    () -> assertEquals(expectedFinalAmount, order.getFinalAmount(), "finalAmount는 totalPrice - discountAmount + shippingFee로 재계산되어야 함")
+            );
+        }
+
+        @DisplayName("실패 케이스 : PENDING 상태가 아닐 때 OrderItem 추가하면 예외 발생")
+        @Test
+        void addOrderItem_whenStatusIsNotPending_ThrowsException() {
+            // arrange
+            User user = createValidUser();
+            Product product = createValidProduct();
+            
+            Order order = Order.builder()
+                    .discountAmount(validDiscountAmount)
+                    .shippingFee(validShippingFee)
+                    .user(user)
+                    .build();
+            
+            // 상태를 CONFIRMED로 변경
+            order.confirm();
+
+            int initialSize = order.getOrderItems().size();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    order.addOrderItem(product, 1)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("PENDING 상태의 주문만 주문 상품을 추가할 수 있습니다"));
+            
+            // OrderItem이 추가되지 않았는지 확인
+            assertEquals(initialSize, order.getOrderItems().size(), "예외 발생 시 OrderItem이 추가되지 않아야 함");
+        }
+
+        @DisplayName("성공 케이스 : 여러 OrderItem을 순차적으로 추가해도 계산이 올바르게 됨")
+        @Test
+        void addOrderItem_multipleItems_Success() {
+            // arrange
+            User user = createValidUser();
+            Product product = createValidProduct();
+            
+            Order order = Order.builder()
+                    .discountAmount(validDiscountAmount)
+                    .shippingFee(validShippingFee)
+                    .user(user)
+                    .build();
+            
+            // 첫 번째 OrderItem 추가
+            order.addOrderItem(product, 2);
+
+            BigDecimal initialTotalPrice = order.getTotalPrice();
+            BigDecimal item1TotalAmount = product.getPrice().multiply(BigDecimal.valueOf(1));
+            BigDecimal item2TotalAmount = product.getPrice().multiply(BigDecimal.valueOf(2));
+            BigDecimal expectedTotalPrice = initialTotalPrice
+                    .add(item1TotalAmount)
+                    .add(item2TotalAmount);
+            BigDecimal expectedFinalAmount = expectedTotalPrice
+                    .subtract(validDiscountAmount)
+                    .add(validShippingFee);
+
+            // act
+            order.addOrderItem(product, 1);
+            order.addOrderItem(product, 2);
+
+            // assert
+            assertAll(
+                    () -> assertEquals(3, order.getOrderItems().size(), "2개의 OrderItem이 추가되어 총 3개가 되어야 함"),
+                    () -> assertEquals(expectedTotalPrice, order.getTotalPrice(), "totalPrice는 모든 OrderItem의 totalAmount 합계여야 함"),
+                    () -> assertEquals(expectedFinalAmount, order.getFinalAmount(), "finalAmount는 totalPrice - discountAmount + shippingFee로 계산되어야 함")
+            );
+        }
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
new file mode 100644
index 000000000..e20f631c1
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
@@ -0,0 +1,164 @@
+package com.loopers.domain.payment;
+
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+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.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Payment 테스트")
+public class PaymentTest {
+
+    // 고정 fixture
+    private final PaymentMethod validMethod = PaymentMethod.POINT;
+    private final PaymentStatus validPaymentStatus = PaymentStatus.PENDING;
+
+    private User createValidUser() {
+        return User.builder()
+                .loginId("testuser1")
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+    }
+
+    private Product createValidProduct() {
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(5000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(brand)
+                .build();
+
+        return product;
+    }
+
+    private Order createValidOrder() {
+        User user = createValidUser();
+        Product product = createValidProduct();
+
+        Order order = Order.builder()
+                .discountAmount(BigDecimal.ZERO)
+                .shippingFee(BigDecimal.ZERO)
+                .user(user)
+                .build();
+        
+        // OrderItem 추가
+        order.addOrderItem(product, 2);
+
+        return order;
+    }
+
+    @DisplayName("Payment 엔티티 생성")
+    @Nested
+    class CreatePaymentTest {
+
+        @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 Payment 객체 생성 성공")
+        @Test
+        void createPayment_withValidFields_Success() {
+            // arrange
+            Order order = createValidOrder();
+            BigDecimal expectedAmount = order.getFinalAmount(); // 생성 시 order.getFinalAmount()로 자동 설정됨
+
+            // act
+            Payment payment = Payment.builder()
+                    .method(validMethod)
+                    .paymentStatus(validPaymentStatus)
+                    .order(order)
+                    .build();
+
+            // assert
+            assertNotNull(payment);
+            assertAll(
+                    () -> assertEquals(validMethod, payment.getMethod()),
+                    () -> assertEquals(expectedAmount, payment.getAmount(), "amount는 order.getFinalAmount()로 자동 설정되어야 함"),
+                    () -> assertEquals(validPaymentStatus, payment.getPaymentStatus()),
+                    () -> assertEquals(order, payment.getOrder())
+            );
+        }
+
+        @DisplayName("실패 케이스 : method가 null이면 예외 발생")
+        @Test
+        void createPayment_withNullMethod_ThrowsException() {
+            // arrange
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Payment.builder()
+                            .method(null)
+                            .paymentStatus(validPaymentStatus)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("method가 비어있을 수 없습니다"));
+        }
+
+
+        @DisplayName("실패 케이스 : paymentStatus가 null이면 예외 발생")
+        @Test
+        void createPayment_withNullPaymentStatus_ThrowsException() {
+            // arrange
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Payment.builder()
+                            .method(validMethod)
+                            .paymentStatus(null)
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("paymentStatus가 비어있을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스 : order가 null이면 예외 발생")
+        @Test
+        void createPayment_withNullOrder_ThrowsException() {
+            // arrange
+            // order가 null이면 amount도 null이 되고, guard()에서 amount null 체크가 order null 체크보다 먼저 발생
+            // 따라서 order가 null인 경우 amount null 예외가 먼저 발생할 수 있음
+            
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    Payment.builder()
+                            .method(validMethod)
+                            .paymentStatus(validPaymentStatus)
+                            .order(null)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            // guard() 순서에 따라 amount null 예외 또는 order null 예외가 발생할 수 있음
+            assertTrue(exception.getCustomMessage().contains("amount가 비어있을 수 없습니다") || 
+                      exception.getCustomMessage().contains("order가 비어있을 수 없습니다"));
+        }
+    }
+}
+

From 134186a97ff57fff04577fd5a51267d8efa8e605 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 02:51:45 +0900
Subject: [PATCH 05/17] =?UTF-8?q?[Round=204]=20=EC=BF=A0=ED=8F=B0=20?=
 =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20=EC=BF=A0?=
 =?UTF-8?q?=ED=8F=B0=20=EC=82=AC=EC=9A=A9=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?=
 =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=A1=B0=EC=B9=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/order/OrderFacade.java        |   7 +
 .../com/loopers/domain/coupon/Coupon.java     | 106 ++++
 .../domain/coupon/CouponRepository.java       |  15 +
 .../loopers/domain/coupon/CouponService.java  |  95 ++++
 .../com/loopers/domain/coupon/CouponType.java |  32 ++
 .../coupon/CouponJpaRepository.java           |  12 +
 .../coupon/CouponQueryRepository.java         |  41 ++
 .../coupon/CouponRepositoryImpl.java          |  45 ++
 .../domain/coupon/CouponServiceTest.java      | 457 ++++++++++++++++++
 9 files changed, 810 insertions(+)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponType.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
index 914228184..9d757b385 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
@@ -83,6 +83,13 @@ public OrderInfo createOrder(String loginId, OrderDto.CreateOrderRequest request
                         "Order 저장에 실패했습니다."
                 ));
 
+        // 4. 쿠폰 적용 (포인트 차감 전에 할인 적용)
+        if (request.couponIds() != null && !request.couponIds().isEmpty()) {
+                for(Long couponId : request.couponIds()) {
+                        couponService.useCoupon(savedOrder, couponId);
+                }
+        }
+
         // 5. 포인트 차감 (쿠폰 할인 적용 후 최종 금액으로)
         pointService.deduct(loginId, savedOrder.getFinalAmount());
         
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
new file mode 100644
index 000000000..48facf082
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
@@ -0,0 +1,106 @@
+package com.loopers.domain.coupon;
+
+import com.loopers.domain.BaseEntity;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.user.User;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Entity
+@Table(name = "coupon")
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class Coupon extends BaseEntity {
+
+    @Enumerated(EnumType.STRING)
+    private CouponType couponType;
+
+    private BigDecimal discountValue;
+
+    private Boolean isUsed = false;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "user_id")
+    private User user;
+
+    @OneToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "order_id")
+    private Order order;
+
+    @Builder
+    private Coupon(
+            CouponType couponType,
+            BigDecimal discountValue,
+            User user
+    ) {
+        this.couponType = couponType;
+        this.discountValue = discountValue;
+        this.user = user;
+        guard();
+    }
+
+    /**
+     * 쿠폰 할인 금액 계산
+     * @param totalPrice 할인을 적용할 총 금액
+     * @return 할인 금액
+     */
+    public BigDecimal calculateDiscount(BigDecimal totalPrice) {
+        if (totalPrice == null || totalPrice.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "Coupon : 할인을 적용할 금액이 올바르지 않습니다."
+            );
+        }
+
+        // if문 없이 enum의 메소드 호출
+        return couponType.calculateDiscount(this.discountValue, totalPrice);
+    }
+
+    @Override
+    protected void guard() {
+        // couponType 검증
+        if (couponType == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : couponType이 비어있을 수 없습니다.");
+        }
+
+        // discountValue 검증
+        if (discountValue == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : discountValue가 비어있을 수 없습니다.");
+        }else if (discountValue.compareTo(BigDecimal.ZERO) < 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : discountValue는 음수가 될 수 없습니다.");
+        }
+
+        // PERCENTAGE 타입인 경우 할인율 검증
+        if (couponType == CouponType.PERCENTAGE && discountValue.compareTo(BigDecimal.valueOf(100)) > 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 할인율은 100을 초과할 수 없습니다.");
+        }
+
+        // isUsed 검증
+        if (isUsed == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : isUsed가 비어있을 수 없습니다.");
+        }
+
+        // isUsed와 order의 관계 검증
+        // isUsed가 false면 order는 null이어야 함
+        if (!isUsed && order != null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 미사용 쿠폰은 order가 null이어야 합니다.");
+        }
+        // isUsed가 true면 order는 null이면 안 됨
+        if (isUsed && order == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 사용된 쿠폰은 order가 비어있을 수 없습니다.");
+        }
+
+        // user 검증: null이 아니어야 함
+        if (user == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : user가 비어있을 수 없습니다.");
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
new file mode 100644
index 000000000..9c1c12999
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
@@ -0,0 +1,15 @@
+package com.loopers.domain.coupon;
+
+import com.loopers.domain.user.User;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface CouponRepository {
+    Optional<Coupon> save(Coupon coupon);
+    Optional<Coupon> findById(Long couponId);
+    List<Coupon> findByUser(User user);
+    List<Coupon> findByUserAndIsUsedFalse(User user);
+    long useCoupon(Long couponId, Long orderId, Long userId);
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
new file mode 100644
index 000000000..06fded989
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
@@ -0,0 +1,95 @@
+package com.loopers.domain.coupon;
+
+import com.loopers.domain.order.Order;
+import com.loopers.domain.user.User;
+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;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Component
+public class CouponService {
+
+    private final CouponRepository couponRepository;
+
+    /**
+     * 쿠폰 저장
+     */
+    @Transactional
+    public Optional<Coupon> saveCoupon(Coupon coupon) {
+        return couponRepository.save(coupon);
+    }
+
+    /**
+     * 쿠폰 조회
+     */
+    @Transactional(readOnly = true)
+    public Coupon findById(Long couponId) {
+        return couponRepository.findById(couponId)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.NOT_FOUND,
+                        "[couponId = " + couponId + "] Coupon을 찾을 수 없습니다."
+                ));
+    }
+
+    /**
+     * 사용자의 쿠폰 목록 조회
+     */
+    @Transactional(readOnly = true)
+    public List<Coupon> findByUser(User user) {
+        return couponRepository.findByUser(user);
+    }
+
+    /**
+     * 사용자의 미사용 쿠폰 목록 조회
+     */
+    @Transactional(readOnly = true)
+    public List<Coupon> findAvailableCouponsByUser(User user) {
+        return couponRepository.findByUserAndIsUsedFalse(user);
+    }
+
+    /**
+     * 주문에 쿠폰 적용 (동시성 안전)
+     * 쿠폰 ID를 받아서 검증하고 할인을 적용
+     * @param order 주문
+     * @param couponId 쿠폰 ID
+     */
+    @Transactional
+    public void useCoupon(Order order, Long couponId) {
+        // 1. 쿠폰 정보 조회 (할인 계산용)
+        Coupon coupon = this.findById(couponId);
+
+        // 2. 쿠폰 소유자 확인
+        if (!coupon.getUser().getId().equals(order.getUser().getId())) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "[couponId = " + couponId + "] 본인의 쿠폰만 사용할 수 있습니다."
+            );
+        }
+
+        // 3. 원자적 UPDATE로 쿠폰 사용 처리 (동시성 안전)
+        long updatedRows = couponRepository.useCoupon(
+                couponId,
+                order.getId(),
+                order.getUser().getId()
+        );
+
+        if (updatedRows == 0) {
+            throw new CoreException(
+                    ErrorType.BAD_REQUEST,
+                    "[couponId = " + couponId + "] 쿠폰을 사용할 수 없습니다. (이미 사용됨 or 삭제됨 or 주문 상태 오류)"
+            );
+        }
+
+        // 4. 할인 계산 및 적용
+        BigDecimal couponDiscount = coupon.calculateDiscount(order.getTotalPrice());
+        order.applyDiscount(couponDiscount);
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponType.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponType.java
new file mode 100644
index 000000000..be120ddf8
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponType.java
@@ -0,0 +1,32 @@
+package com.loopers.domain.coupon;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
+public enum CouponType {
+    FIXED_AMOUNT {  // 정가 할인
+        @Override
+        public BigDecimal calculateDiscount(BigDecimal discountValue, BigDecimal totalPrice) {
+            return discountValue.min(totalPrice); // 할인액이 총액보다 클 수 없음
+        }
+    },
+    PERCENTAGE {    // 할인율
+        @Override
+        public BigDecimal calculateDiscount(BigDecimal discountValue, BigDecimal totalPrice) {
+            // 할인율 검증
+            if (discountValue.compareTo(BigDecimal.ZERO) < 0 || discountValue.compareTo(BigDecimal.valueOf(100)) > 0) {
+                throw new CoreException(
+                        ErrorType.BAD_REQUEST,
+                        "Coupon : 할인율은 0~100 사이의 값이어야 합니다."
+                );
+            }
+            return totalPrice.multiply(discountValue)
+                    .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+        }
+    };
+
+    public abstract BigDecimal calculateDiscount(BigDecimal discountValue, BigDecimal totalPrice);
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
new file mode 100644
index 000000000..acf542db3
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
@@ -0,0 +1,12 @@
+package com.loopers.infrastructure.coupon;
+
+import com.loopers.domain.coupon.Coupon;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface CouponJpaRepository extends JpaRepository<Coupon, Long> {
+    List<Coupon> findByUser_Id(Long userId);
+    List<Coupon> findByUser_IdAndIsUsedFalse(Long userId);
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
new file mode 100644
index 000000000..f985126aa
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
@@ -0,0 +1,41 @@
+package com.loopers.infrastructure.coupon;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import static com.loopers.domain.coupon.QCoupon.coupon;
+
+@RequiredArgsConstructor
+@Component
+public class CouponQueryRepository {
+
+    private final JPAQueryFactory queryFactory;
+
+    /**
+     * 쿠폰 사용 처리 (동시성 안전)
+     * @param couponId 쿠폰 ID
+     * @param orderId 주문 ID
+     * @param userId 사용자 ID
+     * @return 업데이트된 행 수 (1이면 성공, 0이면 쿠폰 사용 불가)
+     */
+    public long useCoupon(Long couponId, Long orderId, Long userId) {
+        if (couponId == null || orderId == null || userId == null) {
+            return 0L;
+        }
+
+        return queryFactory
+                .update(coupon)
+                .set(coupon.isUsed, true)
+                .set(coupon.order.id, orderId)
+                .where(
+                        coupon.id.eq(couponId)
+                        .and(coupon.isUsed.eq(false))  // 미사용 쿠폰만
+                        .and(coupon.order.isNull())  // order가 null이어야 함 (미사용 쿠폰은 order가 null)
+                        .and(coupon.user.id.eq(userId))  // 소유자 확인
+                        .and(coupon.deletedAt.isNull())  // soft delete 확인
+                )
+                .execute();
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
new file mode 100644
index 000000000..3b0c136a9
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
@@ -0,0 +1,45 @@
+package com.loopers.infrastructure.coupon;
+
+import com.loopers.domain.coupon.Coupon;
+import com.loopers.domain.coupon.CouponRepository;
+import com.loopers.domain.user.User;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Component
+public class CouponRepositoryImpl implements CouponRepository {
+
+    private final CouponJpaRepository couponJpaRepository;
+    private final CouponQueryRepository couponQueryRepository;
+
+    @Override
+    public Optional<Coupon> save(Coupon coupon) {
+        Coupon savedCoupon = couponJpaRepository.save(coupon);
+        return Optional.of(savedCoupon);
+    }
+
+    @Override
+    public Optional<Coupon> findById(Long couponId) {
+        return couponJpaRepository.findById(couponId);
+    }
+
+    @Override
+    public List<Coupon> findByUser(User user) {
+        return couponJpaRepository.findByUser_Id(user.getId());
+    }
+
+    @Override
+    public List<Coupon> findByUserAndIsUsedFalse(User user) {
+        return couponJpaRepository.findByUser_IdAndIsUsedFalse(user.getId());
+    }
+
+    @Override
+    public long useCoupon(Long couponId, Long orderId, Long userId) {
+        return couponQueryRepository.useCoupon(couponId, orderId, userId);
+    }
+}
+
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
new file mode 100644
index 000000000..b6970cb15
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
@@ -0,0 +1,457 @@
+package com.loopers.domain.coupon;
+
+import com.loopers.application.user.UserFacade;
+import com.loopers.application.user.UserInfo;
+import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderService;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.User;
+import com.loopers.domain.user.UserRepository;
+import com.loopers.infrastructure.brand.BrandJpaRepository;
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
+import com.loopers.domain.product.ProductStatus;
+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.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 java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("Coupon Service 테스트")
+@SpringBootTest
+class CouponServiceTest {
+
+    @Autowired
+    private CouponService couponService;
+
+    @Autowired
+    private CouponRepository couponRepository;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private OrderService orderService;
+
+    @Autowired
+    private ProductRepository productRepository;
+
+    @Autowired
+    private BrandJpaRepository brandJpaRepository;
+
+    @Autowired
+    private UserFacade userFacade;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    private User test34;
+    private Order testOrder;
+    private Long testCouponId;
+
+    @BeforeEach
+    void setUp() {
+        // 테스트용 User 생성
+        UserInfo userInfo = UserInfo.builder()
+                .loginId("test34")
+                .email("test@test.com")
+                .birthday("1990-01-01")
+                .gender(Gender.MALE)
+                .build();
+        userFacade.saveUser(userInfo);
+
+        test34 = userRepository.findByLoginId("test34")
+                .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"));
+
+        // 테스트용 Brand 생성
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+        Brand savedBrand = brandJpaRepository.save(brand);
+
+        // 테스트용 Product 생성
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(10000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(savedBrand)
+                .build();
+        Product savedProduct = productRepository.save(product)
+                .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
+
+        // 테스트용 Order 생성
+        testOrder = Order.builder()
+                .discountAmount(BigDecimal.ZERO)
+                .shippingFee(BigDecimal.ZERO)
+                .user(test34)
+                .build();
+        
+        // OrderItem 추가
+        testOrder.addOrderItem(savedProduct, 10);
+
+        testOrder = orderService.saveOrder(testOrder)
+                .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
+
+        // 테스트용 Coupon 생성
+        Coupon coupon = Coupon.builder()
+                .couponType(CouponType.FIXED_AMOUNT)
+                .discountValue(BigDecimal.valueOf(5000))
+                .user(test34)
+                .build();
+
+        Coupon savedCoupon = couponRepository.save(coupon)
+                .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+        testCouponId = savedCoupon.getId();
+    }
+
+    @AfterEach
+    void tearDown() {
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("useCoupon 테스트")
+    @Nested
+    class UseCouponTest {
+
+        @DisplayName("성공 케이스: 모든 조건을 만족하는 쿠폰 사용 성공")
+        @Test
+        void useCoupon_withValidCoupon_Success() {
+            // arrange
+            Order order = createTestOrder();
+            BigDecimal initialTotalPrice = order.getTotalPrice();
+            BigDecimal expectedDiscount = BigDecimal.valueOf(5000); // FIXED_AMOUNT 5000원
+
+            // act
+            couponService.useCoupon(order, testCouponId);
+
+            // assert
+            Coupon usedCoupon = couponRepository.findById(testCouponId)
+                    .orElseThrow(() -> new RuntimeException("쿠폰을 찾을 수 없습니다"));
+            
+            assertAll(
+                    () -> assertTrue(usedCoupon.getIsUsed(), "쿠폰은 사용된 상태여야 함"),
+                    () -> assertNotNull(usedCoupon.getOrder(), "쿠폰의 order는 null이 아니어야 함"),
+                    () -> assertEquals(order.getId(), usedCoupon.getOrder().getId(), "쿠폰의 order ID가 일치해야 함"),
+                    () -> assertEquals(0, initialTotalPrice.subtract(expectedDiscount).compareTo(order.getFinalAmount()), "주문의 할인이 적용되어야 함")
+            );
+        }
+
+        @DisplayName("실패 케이스: 존재하지 않는 쿠폰 사용 시 NOT_FOUND 예외 발생")
+        @Test
+        void useCoupon_withNonExistentCoupon_NotFound() {
+            // arrange
+            Order order = createTestOrder();
+            Long nonExistentCouponId = 99999L;
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    couponService.useCoupon(order, nonExistentCouponId)
+            );
+
+            assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("[couponId = " + nonExistentCouponId + "] Coupon을 찾을 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스: 다른 사용자의 쿠폰 사용 시 BAD_REQUEST 예외 발생")
+        @Test
+        void useCoupon_withOtherUserCoupon_BadRequest() {
+            // arrange
+            // 다른 사용자 생성
+            UserInfo otherUserInfo = UserInfo.builder()
+                    .loginId("other34")
+                    .email("other@test.com")
+                    .birthday("1990-01-01")
+                    .gender(Gender.MALE)
+                    .build();
+            userFacade.saveUser(otherUserInfo);
+            User otherUser = userRepository.findByLoginId("other34")
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"));
+
+            // 다른 사용자의 쿠폰 생성
+            Coupon otherUserCoupon = Coupon.builder()
+                    .couponType(CouponType.FIXED_AMOUNT)
+                    .discountValue(BigDecimal.valueOf(5000))
+                    .user(otherUser)
+                    .build();
+            Coupon savedOtherUserCoupon = couponRepository.save(otherUserCoupon)
+                    .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+
+            Order order = createTestOrder(); // test34의 주문
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    couponService.useCoupon(order, savedOtherUserCoupon.getId())
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("본인의 쿠폰만 사용할 수 있습니다"));
+        }
+
+        @DisplayName("실패 케이스: 이미 사용된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
+        @Test
+        void useCoupon_withUsedCoupon_BadRequest() {
+            // arrange
+            Order firstOrder = createTestOrder();
+            couponService.useCoupon(firstOrder, testCouponId); // 첫 번째 사용
+
+            Order secondOrder = createTestOrder(); // 두 번째 주문
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    couponService.useCoupon(secondOrder, testCouponId)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+        }
+
+        @DisplayName("실패 케이스: 삭제된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
+        @Test
+        void useCoupon_withDeletedCoupon_BadRequest() {
+            // arrange
+            // 쿠폰 삭제 (soft delete)
+            Coupon coupon = couponRepository.findById(testCouponId)
+                    .orElseThrow(() -> new RuntimeException("쿠폰을 찾을 수 없습니다"));
+            coupon.delete();
+            couponRepository.save(coupon);
+
+            Order order = createTestOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    couponService.useCoupon(order, testCouponId)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+        }
+    }
+
+    @DisplayName("useCoupon 동시성 테스트")
+    @Nested
+    class UseCouponConcurrencyTest {
+
+        @DisplayName("성공 케이스: 여러 스레드가 동시에 같은 쿠폰을 사용하려고 할 때 정확히 하나만 성공")
+        @Test
+        void useCoupon_concurrentRequests_onlyOneSuccess() throws InterruptedException {
+            // arrange
+            int threadCount = 10;
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+            List<Exception> exceptions = new ArrayList<>();
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        // 각 스레드마다 새로운 Order 생성
+                        Order order = createTestOrder();
+                        couponService.useCoupon(order, testCouponId);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                        exceptions.add(e);
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Coupon finalCoupon = couponRepository.findById(testCouponId)
+                    .orElseThrow(() -> new RuntimeException("쿠폰을 찾을 수 없습니다"));
+
+            assertEquals(1, successCount.get(),
+                    String.format("성공한 스레드 수는 1이어야 함: 실제=%d", successCount.get()));
+            assertEquals(threadCount - 1, failureCount.get(),
+                    String.format("실패한 스레드 수는 %d이어야 함: 실제=%d", threadCount - 1, failureCount.get()));
+            assertTrue(finalCoupon.getIsUsed(),
+                    String.format("쿠폰은 사용된 상태여야 함: 실제=%s", finalCoupon.getIsUsed()));
+
+            // 실패한 경우는 쿠폰 사용 불가 예외여야 함
+            long couponUnavailableExceptions = exceptions.stream()
+                    .filter(e -> e instanceof CoreException)
+                    .filter(e -> ((CoreException) e).getErrorType() == ErrorType.BAD_REQUEST)
+                    .filter(e -> e.getMessage().contains("쿠폰을 사용할 수 없습니다"))
+                    .count();
+            assertEquals(failureCount.get(), couponUnavailableExceptions,
+                    String.format("쿠폰 사용 불가 예외 수는 실패 수와 일치해야 함: 예상=%d, 실제=%d", failureCount.get(), couponUnavailableExceptions));
+        }
+
+        @DisplayName("성공 케이스: 쿠폰 사용이 원자적으로 처리되어 Lost Update가 발생하지 않음")
+        @Test
+        void useCoupon_atomicOperation_noLostUpdate() throws InterruptedException {
+            // arrange
+            int threadCount = 20;
+            int couponCount = 20; // 각 스레드가 다른 쿠폰을 사용
+
+            // 여러 개의 쿠폰 생성
+            List<Long> couponIds = new ArrayList<>();
+            for (int i = 0; i < couponCount; i++) {
+                Coupon coupon = Coupon.builder()
+                        .couponType(CouponType.FIXED_AMOUNT)
+                        .discountValue(BigDecimal.valueOf(5000))
+                        .user(test34)
+                        .build();
+                Coupon savedCoupon = couponRepository.save(coupon)
+                        .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+                couponIds.add(savedCoupon.getId());
+            }
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                final int index = i;
+                executor.submit(() -> {
+                    try {
+                        Order order = createTestOrder();
+                        Long couponId = couponIds.get(index % couponCount);
+                        couponService.useCoupon(order, couponId);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        // 실패는 무시 (쿠폰 사용 불가 등)
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            // 사용된 쿠폰 수 확인
+            long usedCouponCount = couponIds.stream()
+                    .mapToLong(couponId -> {
+                        Coupon coupon = couponRepository.findById(couponId)
+                                .orElseThrow(() -> new RuntimeException("쿠폰을 찾을 수 없습니다"));
+                        return coupon.getIsUsed() ? 1 : 0;
+                    })
+                    .sum();
+
+            // Lost Update가 발생하지 않았는지 확인
+            // 성공한 쿠폰 적용 수만큼 정확히 쿠폰이 사용되어야 함
+            assertEquals(successCount.get(), usedCouponCount,
+                    String.format("Lost Update가 발생하지 않아야 함: 성공한 쿠폰 적용 수=%d, 사용된 쿠폰 수=%d", successCount.get(), usedCouponCount));
+        }
+
+        @DisplayName("성공 케이스: 동시에 여러 스레드가 쿠폰을 사용해도 중복 사용되지 않음")
+        @Test
+        void useCoupon_concurrentRequests_noDuplicateUsage() throws InterruptedException {
+            // arrange
+            int threadCount = 25; // 쿠폰 1개에 대해 25개 스레드가 동시에 사용 시도
+
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch latch = new CountDownLatch(threadCount);
+            AtomicInteger successCount = new AtomicInteger(0);
+            AtomicInteger failureCount = new AtomicInteger(0);
+
+            // act
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    try {
+                        Order order = createTestOrder();
+                        couponService.useCoupon(order, testCouponId);
+                        successCount.incrementAndGet();
+                    } catch (Exception e) {
+                        failureCount.incrementAndGet();
+                    } finally {
+                        latch.countDown();
+                    }
+                });
+            }
+
+            // wait for all threads to complete
+            assertTrue(latch.await(10, TimeUnit.SECONDS), "모든 스레드가 10초 내에 완료되지 않았습니다.");
+            executor.shutdown();
+
+            // assert
+            Coupon finalCoupon = couponRepository.findById(testCouponId)
+                    .orElseThrow(() -> new RuntimeException("쿠폰을 찾을 수 없습니다"));
+
+            // 쿠폰이 중복 사용되지 않아야 함 (정확히 1번만 사용)
+            assertEquals(1, successCount.get(),
+                    String.format("성공한 쿠폰 사용 수는 1이어야 함: 실제=%d", successCount.get()));
+            assertEquals(threadCount - 1, failureCount.get(),
+                    String.format("실패한 쿠폰 사용 수는 %d이어야 함: 실제=%d", threadCount - 1, failureCount.get()));
+            assertTrue(finalCoupon.getIsUsed(),
+                    String.format("쿠폰은 사용된 상태여야 함: 실제=%s", finalCoupon.getIsUsed()));
+            // 성공한 쿠폰 사용 수만큼 정확히 쿠폰이 사용되어야 함
+            assertTrue(finalCoupon.getIsUsed() && successCount.get() == 1,
+                    String.format("쿠폰이 정확히 1번만 사용되어야 함: 성공한 사용 수=%d, 쿠폰 사용 상태=%s", successCount.get(), finalCoupon.getIsUsed()));
+        }
+    }
+
+    private Order createTestOrder() {
+        // 테스트용 Order 생성 (각 스레드마다 새로운 Order)
+        Brand brand = Brand.builder()
+                .name("Test Brand")
+                .description("Test Description")
+                .status(BrandStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+        Brand savedBrand = brandJpaRepository.save(brand);
+
+        Product product = Product.builder()
+                .name("Test Product")
+                .description("Test Description")
+                .price(BigDecimal.valueOf(10000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .brand(savedBrand)
+                .build();
+        Product savedProduct = productRepository.save(product)
+                .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
+
+        Order order = Order.builder()
+                .discountAmount(BigDecimal.ZERO)
+                .shippingFee(BigDecimal.ZERO)
+                .user(test34)
+                .build();
+        
+        // OrderItem 추가
+        order.addOrderItem(savedProduct, 10);
+
+        return orderService.saveOrder(order)
+                .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
+    }
+}
+

From 391f49842ed97280b50c68a341689980a4e6e210 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 21 Nov 2025 14:14:35 +0900
Subject: [PATCH 06/17] =?UTF-8?q?[Round=204]=20=EC=A3=BC=EB=AC=B8=20API=20?=
 =?UTF-8?q?=ED=8C=8C=EC=82=AC=EB=93=9C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../point/PointQueryRepository.java           |   4 +-
 .../stock/StockQueryRepository.java           |   2 +-
 ...egrationTest.java => OrderFacadeTest.java} | 165 +++++++++++++++++-
 3 files changed, 167 insertions(+), 4 deletions(-)
 rename apps/commerce-api/src/test/java/com/loopers/application/order/{OrderFacadeIntegrationTest.java => OrderFacadeTest.java} (67%)

diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
index 50731d1b1..48cabc545 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
@@ -29,8 +29,8 @@ public long deduct(String loginId, BigDecimal deductAmount) {
                 .update(point)
                 .set(point.amount, point.amount.subtract(deductAmount))
                 .where(
-                        point.user.loginId.eq(loginId)
-                        .and(point.amount.goe(deductAmount))  // 포인트가 충분한 경우만
+                        point.user.loginId.eq(loginId)          // 사용자 로그인 ID와 일치하는 경우
+                        .and(point.amount.goe(deductAmount))    // 포인트가 충분한 경우만
                 )
                 .execute();
     }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
index b1c03c8fe..9f31079c4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
@@ -27,7 +27,7 @@ public long decreaseQuantity(Long productId, Long decreaseQuantity) {
                 .update(stock)
                 .set(stock.quantity, stock.quantity.subtract(decreaseQuantity))
                 .where(
-                        stock.product.id.eq(productId)
+                        stock.product.id.eq(productId)              // 상품 ID와 일치하는 경우
                         .and(stock.quantity.goe(decreaseQuantity))  // 재고가 충분한 경우만
                 )
                 .execute();
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/OrderFacadeTest.java
similarity index 67%
rename from apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
index f9c3e9946..c506505f7 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
@@ -31,16 +31,22 @@
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ConcurrentLinkedQueue;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 @DisplayName("OrderFacade 통합 테스트")
 @SpringBootTest
-class OrderFacadeIntegrationTest {
+class OrderFacadeTest {
 
     @Autowired
     private OrderFacade orderFacade;
@@ -78,6 +84,9 @@ class OrderFacadeIntegrationTest {
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
+    @Autowired
+    private TransactionTemplate transactionTemplate;
+
     private User testUser;
     private Product testProduct;
     private Long testProductId;
@@ -403,5 +412,159 @@ void createOrder_withUnavailableCoupon_BadRequest() {
             assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
         }
     }
+
+    @DisplayName("동시성 테스트")
+    @Nested
+    class ConcurrencyTest {
+
+        @DisplayName("여러 주문이 동시에 들어와도 재고/포인트/쿠폰이 일관되게 처리된다")
+        @Test
+        void createOrder_concurrentRequests_consistencyMaintained() throws InterruptedException {
+            // arrange
+            int initialStock = 10;
+            int couponCount = 5;
+            Stock stock = stockRepository.findByProductId(testProductId)
+                    .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
+            long currentQuantity = stock.getQuantity();
+            if (currentQuantity > initialStock) {
+                stockService.decreaseQuantity(testProductId, currentQuantity - initialStock);
+            } else if (currentQuantity < initialStock) {
+                stockService.increaseQuantity(testProductId, initialStock - currentQuantity);
+            }
+
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(1)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(new ArrayList<>())
+                    .build();
+
+            List<Long> couponIds = new ArrayList<>();
+            for (int i = 0; i < couponCount; i++) {
+                Coupon coupon = Coupon.builder()
+                        .couponType(CouponType.FIXED_AMOUNT)
+                        .discountValue(BigDecimal.valueOf(5000))
+                        .user(testUser)
+                        .build();
+                Long couponId = couponRepository.save(coupon)
+                        .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"))
+                        .getId();
+                couponIds.add(couponId);
+            }
+            ConcurrentLinkedQueue<Long> couponQueue = new ConcurrentLinkedQueue<>(couponIds);
+
+            int threadCount = 20;
+            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+            CountDownLatch ready = new CountDownLatch(threadCount);
+            CountDownLatch start = new CountDownLatch(1);
+            CountDownLatch done = new CountDownLatch(threadCount);
+
+            List<OrderInfo> successOrders = Collections.synchronizedList(new ArrayList<>());
+            List<Throwable> failures = Collections.synchronizedList(new ArrayList<>());
+
+            for (int i = 0; i < threadCount; i++) {
+                executor.submit(() -> {
+                    ready.countDown();
+                    try {
+                        start.await();
+                        Long couponId = couponQueue.poll();
+                        OrderDto.CreateOrderRequest actualRequest = couponId == null
+                                ? request
+                                : OrderDto.CreateOrderRequest.builder()
+                                    .items(items)
+                                    .couponIds(List.of(couponId))
+                                    .build();
+                        OrderInfo orderInfo = orderFacade.createOrder(testLoginId, actualRequest);
+                        successOrders.add(orderInfo);
+                    } catch (Throwable t) {
+                        failures.add(t);
+                    } finally {
+                        done.countDown();
+                    }
+                });
+            }
+
+            ready.await();
+            start.countDown();
+            done.await();
+            executor.shutdownNow();
+
+            // assert
+            assertEquals(initialStock, successOrders.size(), "재고 수만큼만 주문이 성공해야 함");
+            assertTrue(failures.size() >= threadCount - initialStock, "초과 주문은 실패해야 함");
+            failures.forEach(failure -> {
+                assertTrue(failure instanceof CoreException, "실패는 CoreException이어야 함");
+                CoreException exception = (CoreException) failure;
+                assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType(), "실패 타입은 BAD_REQUEST여야 함");
+                assertTrue(exception.getCustomMessage().contains("재고가 부족"), "실패 메시지는 재고 부족이어야 함");
+            });
+
+            Stock finalStock = stockRepository.findByProductId(testProductId)
+                    .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
+            assertEquals(0L, finalStock.getQuantity(), "재고는 0이어야 함");
+
+            BigDecimal productPrice = testProduct.getPrice();
+            BigDecimal couponDiscount = BigDecimal.valueOf(5000);
+            
+            // 쿠폰 검증은 트랜잭션 내에서 수행
+            long usedCouponCount = verifyCoupons(couponIds, initialStock, couponDiscount);
+
+            long nonCouponSuccessCount = initialStock - usedCouponCount;
+            BigDecimal totalCouponOrders = productPrice.subtract(couponDiscount)
+                    .multiply(BigDecimal.valueOf(usedCouponCount));
+            BigDecimal totalNonCouponOrders = productPrice
+                    .multiply(BigDecimal.valueOf(nonCouponSuccessCount));
+            BigDecimal expectedPoint = BigDecimal.valueOf(100000)
+                    .subtract(totalCouponOrders.add(totalNonCouponOrders));
+            Point point = pointService.findByUserLoginId(testLoginId)
+                    .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
+            assertEquals(0, expectedPoint.compareTo(point.getAmount()), "사용된 포인트가 정확해야 함");
+
+            successOrders.forEach(orderInfo -> {
+                assertEquals(OrderStatus.CONFIRMED, orderInfo.orderStatus(), "성공 주문 상태 검증");
+                assertEquals(1, orderInfo.orderItems().size(), "주문 상품 수는 1개");
+                if (orderInfo.discountAmount().compareTo(BigDecimal.ZERO) > 0) {
+                    assertEquals(0, couponDiscount.compareTo(orderInfo.discountAmount()), "쿠폰 할인 금액 일치");
+                } else {
+                    assertEquals(BigDecimal.ZERO, orderInfo.discountAmount(), "쿠폰이 없으면 할인 없음");
+                }
+                BigDecimal expectedFinalAmount = productPrice.subtract(orderInfo.discountAmount());
+                assertEquals(0, expectedFinalAmount.compareTo(orderInfo.finalAmount()), "최종 금액 검증");
+            });
+        }
+
+        private long verifyCoupons(List<Long> couponIds, int initialStock, BigDecimal couponDiscount) {
+            Long result = transactionTemplate.execute(status -> {
+                long usedCouponCount = couponIds.stream()
+                        .filter(id -> id != null)
+                        .map(id -> couponRepository.findById(id)
+                                .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다")))
+                        .filter(Coupon::getIsUsed)
+                        .peek(coupon -> {
+                            assertNotNull(coupon.getOrder(), "쿠폰은 주문과 연결되어야 함");
+                        })
+                        .count();
+                assertTrue(usedCouponCount <= initialStock, "사용된 쿠폰 수는 성공 주문 수를 초과할 수 없음");
+
+                couponIds.stream()
+                        .filter(id -> id != null)
+                        .forEach(id -> {
+                            Coupon coupon = couponRepository.findById(id)
+                                    .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다"));
+                            if (coupon.getIsUsed()) {
+                                Order order = coupon.getOrder();
+                                assertEquals(couponDiscount.compareTo(order.getDiscountAmount()), 0, "쿠폰 사용 주문 할인 금액 검증");
+                            }
+                        });
+                
+                return usedCouponCount;
+            });
+            return result != null ? result : 0L;
+        }
+    }
 }
 

From 8d562a9030cee250c5276117d6c0962afaaee1df Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 01:23:33 +0900
Subject: [PATCH 07/17] =?UTF-8?q?[Round=205]=20Example=20=ED=8C=8C?=
 =?UTF-8?q?=EC=9D=BC=EB=93=A4=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/example/ExampleFacade.java    |  17 ---
 .../application/example/ExampleInfo.java      |  13 --
 .../loopers/domain/example/ExampleModel.java  |  44 -------
 .../domain/example/ExampleRepository.java     |   7 --
 .../domain/example/ExampleService.java        |  20 ---
 .../example/ExampleJpaRepository.java         |   6 -
 .../example/ExampleRepositoryImpl.java        |  19 ---
 .../api/example/ExampleV1ApiSpec.java         |  19 ---
 .../api/example/ExampleV1Controller.java      |  28 -----
 .../interfaces/api/example/ExampleV1Dto.java  |  15 ---
 .../domain/example/ExampleModelTest.java      |  65 ----------
 .../ExampleServiceIntegrationTest.java        |  72 -----------
 .../interfaces/api/ExampleV1ApiE2ETest.java   | 114 ------------------
 13 files changed, 439 deletions(-)
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
deleted file mode 100644
index 552a9ad62..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleFacade {
-    private final ExampleService exampleService;
-
-    public ExampleInfo getExample(Long id) {
-        ExampleModel example = exampleService.getExample(id);
-        return ExampleInfo.from(example);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
deleted file mode 100644
index 877aba96c..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-
-public record ExampleInfo(Long id, String name, String description) {
-    public static ExampleInfo from(ExampleModel model) {
-        return new ExampleInfo(
-            model.getId(),
-            model.getName(),
-            model.getDescription()
-        );
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
deleted file mode 100644
index c588c4a8a..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.domain.BaseEntity;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-
-@Entity
-@Table(name = "example")
-public class ExampleModel extends BaseEntity {
-
-    private String name;
-    private String description;
-
-    protected ExampleModel() {}
-
-    public ExampleModel(String name, String description) {
-        if (name == null || name.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "이름은 비어있을 수 없습니다.");
-        }
-        if (description == null || description.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-
-        this.name = name;
-        this.description = description;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void update(String newDescription) {
-        if (newDescription == null || newDescription.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-        this.description = newDescription;
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
deleted file mode 100644
index 3625e5662..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.loopers.domain.example;
-
-import java.util.Optional;
-
-public interface ExampleRepository {
-    Optional<ExampleModel> find(Long id);
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
deleted file mode 100644
index c0e8431e8..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.loopers.domain.example;
-
-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;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleService {
-
-    private final ExampleRepository exampleRepository;
-
-    @Transactional(readOnly = true)
-    public ExampleModel getExample(Long id) {
-        return exampleRepository.find(id)
-            .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] 예시를 찾을 수 없습니다."));
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
deleted file mode 100644
index ce6d3ead0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ExampleJpaRepository extends JpaRepository<ExampleModel, Long> {}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
deleted file mode 100644
index 37f2272f0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleRepository;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.Optional;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleRepositoryImpl implements ExampleRepository {
-    private final ExampleJpaRepository exampleJpaRepository;
-
-    @Override
-    public Optional<ExampleModel> find(Long id) {
-        return exampleJpaRepository.findById(id);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
deleted file mode 100644
index 219e3101e..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.interfaces.api.ApiResponse;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-@Tag(name = "Example V1 API", description = "Loopers 예시 API 입니다.")
-public interface ExampleV1ApiSpec {
-
-    @Operation(
-        summary = "예시 조회",
-        description = "ID로 예시를 조회합니다."
-    )
-    ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @Schema(name = "예시 ID", description = "조회할 예시의 ID")
-        Long exampleId
-    );
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
deleted file mode 100644
index 917376016..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleFacade;
-import com.loopers.application.example.ExampleInfo;
-import com.loopers.interfaces.api.ApiResponse;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RequiredArgsConstructor
-@RestController
-@RequestMapping("/api/v1/examples")
-public class ExampleV1Controller implements ExampleV1ApiSpec {
-
-    private final ExampleFacade exampleFacade;
-
-    @GetMapping("/{exampleId}")
-    @Override
-    public ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @PathVariable(value = "exampleId") Long exampleId
-    ) {
-        ExampleInfo info = exampleFacade.getExample(exampleId);
-        ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info);
-        return ApiResponse.success(response);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
deleted file mode 100644
index 4ecf0eea5..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleInfo;
-
-public class ExampleV1Dto {
-    public record ExampleResponse(Long id, String name, String description) {
-        public static ExampleResponse from(ExampleInfo info) {
-            return new ExampleResponse(
-                info.id(),
-                info.name(),
-                info.description()
-            );
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
deleted file mode 100644
index 44ca7576e..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.loopers.domain.example;
-
-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.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-class ExampleModelTest {
-    @DisplayName("예시 모델을 생성할 때, ")
-    @Nested
-    class Create {
-        @DisplayName("제목과 설명이 모두 주어지면, 정상적으로 생성된다.")
-        @Test
-        void createsExampleModel_whenNameAndDescriptionAreProvided() {
-            // arrange
-            String name = "제목";
-            String description = "설명";
-
-            // act
-            ExampleModel exampleModel = new ExampleModel(name, description);
-
-            // assert
-            assertAll(
-                () -> assertThat(exampleModel.getId()).isNotNull(),
-                () -> assertThat(exampleModel.getName()).isEqualTo(name),
-                () -> assertThat(exampleModel.getDescription()).isEqualTo(description)
-            );
-        }
-
-        @DisplayName("제목이 빈칸으로만 이루어져 있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenTitleIsBlank() {
-            // arrange
-            String name = "   ";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel(name, "설명");
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-
-        @DisplayName("설명이 비어있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenDescriptionIsEmpty() {
-            // arrange
-            String description = "";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel("제목", description);
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
deleted file mode 100644
index bbd5fdbe1..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-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 static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-@SpringBootTest
-class ExampleServiceIntegrationTest {
-    @Autowired
-    private ExampleService exampleService;
-
-    @Autowired
-    private ExampleJpaRepository exampleJpaRepository;
-
-    @Autowired
-    private DatabaseCleanUp databaseCleanUp;
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("예시를 조회할 때,")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-
-            // act
-            ExampleModel result = exampleService.getExample(exampleModel.getId());
-
-            // assert
-            assertAll(
-                () -> assertThat(result).isNotNull(),
-                () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, NOT_FOUND 예외가 발생한다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = 999L; // Assuming this ID does not exist
-
-            // act
-            CoreException exception = assertThrows(CoreException.class, () -> {
-                exampleService.getExample(invalidId);
-            });
-
-            // assert
-            assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
deleted file mode 100644
index 1bb3dba65..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.loopers.interfaces.api;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-import com.loopers.interfaces.api.example.ExampleV1Dto;
-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.boot.test.web.client.TestRestTemplate;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-
-import java.util.function.Function;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-class ExampleV1ApiE2ETest {
-
-    private static final Function<Long, String> ENDPOINT_GET = id -> "/api/v1/examples/" + id;
-
-    private final TestRestTemplate testRestTemplate;
-    private final ExampleJpaRepository exampleJpaRepository;
-    private final DatabaseCleanUp databaseCleanUp;
-
-    @Autowired
-    public ExampleV1ApiE2ETest(
-        TestRestTemplate testRestTemplate,
-        ExampleJpaRepository exampleJpaRepository,
-        DatabaseCleanUp databaseCleanUp
-    ) {
-        this.testRestTemplate = testRestTemplate;
-        this.exampleJpaRepository = exampleJpaRepository;
-        this.databaseCleanUp = databaseCleanUp;
-    }
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("GET /api/v1/examples/{id}")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-            String requestUrl = ENDPOINT_GET.apply(exampleModel.getId());
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
-                () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("숫자가 아닌 ID 로 요청하면, 400 BAD_REQUEST 응답을 받는다.")
-        @Test
-        void throwsBadRequest_whenIdIsNotProvided() {
-            // arrange
-            String requestUrl = "/api/v1/examples/나나";
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, 404 NOT_FOUND 응답을 받는다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = -1L;
-            String requestUrl = ENDPOINT_GET.apply(invalidId);
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
-            );
-        }
-    }
-}

From 78a0caf1ed6db9fd33fc09b29254e8d673e45c8c Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 01:34:30 +0900
Subject: [PATCH 08/17] =?UTF-8?q?[Round=205]=20=EC=97=94=ED=8B=B0=ED=8B=B0?=
 =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EC=B0=B8=EC=A1=B0=EB=A5=BC=20String=20?=
 =?UTF-8?q?id=20=EC=B0=B8=EC=A1=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/application/like/LikeFacade.java  |  12 +-
 .../application/order/OrderFacade.java        |  74 +++++++----
 .../loopers/application/order/OrderInfo.java  |  10 +-
 .../application/point/PointFacade.java        |  14 +-
 .../loopers/application/user/UserFacade.java  |  28 +++-
 .../loopers/application/user/UserInfo.java    |  12 +-
 .../java/com/loopers/domain/brand/Brand.java  |  16 ---
 .../com/loopers/domain/coupon/Coupon.java     |  38 ++----
 .../domain/coupon/CouponRepository.java       |   6 +-
 .../loopers/domain/coupon/CouponService.java  |   9 +-
 .../com/loopers/domain/like/entity/Like.java  |  38 ++----
 .../loopers/domain/like/entity/LikeId.java    |   5 -
 .../java/com/loopers/domain/order/Order.java  |  28 ++--
 .../com/loopers/domain/order/OrderItem.java   |  78 ++++++-----
 .../loopers/domain/order/OrderRepository.java |   4 +-
 .../loopers/domain/order/OrderService.java    |   5 +-
 .../com/loopers/domain/payment/Payment.java   |  29 ++---
 .../java/com/loopers/domain/point/Point.java  |  16 +--
 .../loopers/domain/point/PointRepository.java |   6 +-
 .../loopers/domain/point/PointService.java    |  22 ++--
 .../java/com/loopers/domain/stock/Stock.java  |  16 +--
 .../java/com/loopers/domain/user/User.java    |  15 ---
 .../loopers/domain/user/UserRepository.java   |   1 +
 .../com/loopers/domain/user/UserService.java  |   5 +
 .../coupon/CouponJpaRepository.java           |   4 +-
 .../coupon/CouponQueryRepository.java         |   6 +-
 .../coupon/CouponRepositoryImpl.java          |   9 +-
 .../order/OrderJpaRepository.java             |   3 +-
 .../order/OrderRepositoryImpl.java            |   5 +-
 .../point/PointJpaRepository.java             |   2 +-
 .../point/PointQueryRepository.java           |  12 +-
 .../point/PointRepositoryImpl.java            |  12 +-
 .../stock/StockQueryRepository.java           |   4 +-
 .../user/UserRepositoryImpl.java              |   5 +
 .../application/order/OrderFacadeTest.java    | 121 +++++++++++++++---
 .../application/user/UserFacadeTest.java      |  10 +-
 .../domain/coupon/CouponServiceTest.java      |  30 +++--
 .../com/loopers/domain/like/LikeTest.java     |  42 +++---
 .../loopers/domain/order/OrderItemTest.java   | 115 +++++++++++++----
 .../com/loopers/domain/order/OrderTest.java   |  24 ++--
 .../loopers/domain/payment/PaymentTest.java   |  43 +++----
 .../com/loopers/domain/point/PointTest.java   |  10 +-
 .../com/loopers/domain/stock/StockTest.java   |  12 +-
 .../com/loopers/domain/user/UserTest.java     |   4 -
 44 files changed, 559 insertions(+), 401 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
index 8bef282f2..78196f7fb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
@@ -20,11 +20,11 @@ public class LikeFacade {
      * 상품 좋아요 등록
      */
     @Transactional
-    public void saveProductLike(String loginId, Long productId) {
-        User user = userService.findUserByLoginId(loginId)
+    public void saveProductLike(Long userId, Long productId) {
+        User user = userService.findById(userId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND,
-                        "[loginId = " + loginId + "] User를 찾을 수 없습니다."
+                        "[userId = " + userId + "] User를 찾을 수 없습니다."
                 ));
 
         likeService.saveProductLike(user, productId);
@@ -34,11 +34,11 @@ public void saveProductLike(String loginId, Long productId) {
      * 상품 좋아요 취소
      */
     @Transactional
-    public void deleteProductLike(String loginId, Long productId) {
-        User user = userService.findUserByLoginId(loginId)
+    public void deleteProductLike(Long userId, Long productId) {
+        User user = userService.findById(userId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND,
-                        "[loginId = " + loginId + "] User를 찾을 수 없습니다."
+                        "[userId = " + userId + "] User를 찾을 수 없습니다."
                 ));
 
         likeService.deleteProductLike(user, 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
index 9d757b385..6555c6e1c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
@@ -37,16 +37,27 @@ public class OrderFacade {
      * 주문 생성
      */
     @Transactional
-    public OrderInfo createOrder(String loginId, OrderDto.CreateOrderRequest request) {
+    public OrderInfo createOrder(Long userId, OrderDto.CreateOrderRequest request) {
+
+        // 0. 주문 항목이 1개 이상이어야 함
+        if (request.items() == null || request.items().isEmpty()) {
+                throw new CoreException(
+                        ErrorType.BAD_REQUEST,
+                        "주문 항목이 1개 이상이어야 합니다."
+                );
+        }
+
+        // 1. 주문 항목 리스트 조회
         List<OrderDto.OrderItemRequest> orderItemRequests = request.items();
-        // 1. User 객체 조회
-        User user = userService.findUserByLoginId(loginId)
+
+        // 2. User 객체 조회 (userId 검증용)
+        User user = userService.findById(userId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND, 
-                        "[loginId = " + loginId + "] User를 찾을 수 없습니다."
+                        "[userId = " + userId + "] User를 찾을 수 없습니다."
                 ));
 
-        // 2. Product 조회 및 재고 차감
+        // 3. Product 조회 및 재고 차감
         Map<Long, Product> productMap = new HashMap<>();
         orderItemRequests.stream()
                 .sorted(Comparator.comparing(OrderDto.OrderItemRequest::productId))  // productId로 정렬 (데드락 방지)
@@ -63,48 +74,59 @@ public OrderInfo createOrder(String loginId, OrderDto.CreateOrderRequest request
                     stockService.decreaseQuantity(itemRequest.productId(), Long.valueOf(itemRequest.quantity()));
                 });
 
-        // 3. 주문 생성
+        // 4. 주문 생성
         Order order = Order.builder()
                 .discountAmount(BigDecimal.ZERO)  // 초기 할인 금액은 0 (쿠폰 적용 전)
                 .shippingFee(BigDecimal.ZERO)     // 배송비는 기본값 0 (필요시 비즈니스 로직으로 계산)
-                .user(user)
+                .userId(userId)
                 .build();
         
-        // 3-1. OrderItem 리스트 생성 및 적용 (Order를 통해서만 생성)
+        // 4-1. OrderItem 리스트 생성 및 적용 (Order를 통해서만 생성)
+        // 주문 시점의 상품 정보를 스냅샷으로 저장
         orderItemRequests.forEach(itemRequest -> {
             Product product = productMap.get(itemRequest.productId());
-            order.addOrderItem(product, itemRequest.quantity());
+            order.addOrderItem(
+                    product.getId(),
+                    product.getName(),
+                    product.getPrice(),
+                    itemRequest.quantity()
+            );
         });
 
-        // 3-2. 주문 먼저 저장 (쿠폰 적용을 위해 order.getId()가 필요)
+        // 4-2. 주문 먼저 저장 (쿠폰 적용을 위해 order.getId()가 필요)
         Order savedOrder = orderService.saveOrder(order)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.INTERNAL_ERROR,
                         "Order 저장에 실패했습니다."
                 ));
 
-        // 4. 쿠폰 적용 (포인트 차감 전에 할인 적용)
+        // 5. 쿠폰 적용 (포인트 차감 전에 할인 적용)
         if (request.couponIds() != null && !request.couponIds().isEmpty()) {
                 for(Long couponId : request.couponIds()) {
                         couponService.useCoupon(savedOrder, couponId);
                 }
         }
 
-        // 5. 포인트 차감 (쿠폰 할인 적용 후 최종 금액으로)
-        pointService.deduct(loginId, savedOrder.getFinalAmount());
+        // 6. 포인트 차감 (쿠폰 할인 적용 후 최종 금액으로)
+
+        // 만약 최종 금액이 0보다 작으면 포인트 차감하지 않음
+        BigDecimal finalAmount = savedOrder.getFinalAmount();
+        if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
+            pointService.deduct(userId, finalAmount);
+        }
         
-        // 6. 주문 상태 변경
+        // 7. 주문 상태 변경
         savedOrder.confirm();
 
-        // 7. 주문 상태 변경 후 다시 저장
+        // 8. 주문 상태 변경 후 다시 저장
         savedOrder = orderService.saveOrder(savedOrder)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.INTERNAL_ERROR,
                         "Order 저장에 실패했습니다."
                 ));
 
-        // 주문 정보 반환
-        return OrderInfo.from(savedOrder);
+        // 주문 정보 반환 (userLoginId는 User에서 조회)
+        return OrderInfo.from(savedOrder, user.getLoginId());
     }
 
     /**
@@ -113,23 +135,29 @@ public OrderInfo createOrder(String loginId, OrderDto.CreateOrderRequest request
     @Transactional(readOnly = true)
     public OrderInfo getOrder(Long orderId) {
         Order order = orderService.findOrderById(orderId);
-        return OrderInfo.from(order);
+        // userId로 User 조회하여 loginId 가져오기
+        User user = userService.findById(order.getUserId())
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.NOT_FOUND,
+                        "[userId = " + order.getUserId() + "] User를 찾을 수 없습니다."
+                ));
+        return OrderInfo.from(order, user.getLoginId());
     }
 
     /**
      * 유저의 주문 목록 조회
      */
     @Transactional(readOnly = true)
-    public List<OrderInfo> getOrders(String loginId) {
-        User user = userService.findUserByLoginId(loginId)
+    public List<OrderInfo> getOrders(Long userId) {
+        User user = userService.findById(userId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND,
-                        "[loginId = " + loginId + "] User를 찾을 수 없습니다."
+                        "[userId = " + userId + "] User를 찾을 수 없습니다."
                 ));
 
-        List<Order> orders = orderService.findOrdersByUser(user);
+        List<Order> orders = orderService.findOrdersByUserId(userId);
         return orders.stream()
-                .map(OrderInfo::from)
+                .map(order -> OrderInfo.from(order, user.getLoginId()))
                 .toList();
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
index c732e7c10..04971b7ee 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
@@ -19,7 +19,7 @@ public record OrderInfo(
         String userLoginId,
         List<OrderItemInfo> orderItems
 ) {
-    public static OrderInfo from(Order order) {
+    public static OrderInfo from(Order order, String userLoginId) {
         return OrderInfo.builder()
                 .id(order.getId())
                 .finalAmount(order.getFinalAmount())
@@ -27,7 +27,7 @@ public static OrderInfo from(Order order) {
                 .discountAmount(order.getDiscountAmount())
                 .shippingFee(order.getShippingFee())
                 .orderStatus(order.getOrderStatus())
-                .userLoginId(order.getUser().getLoginId())
+                .userLoginId(userLoginId)
                 .orderItems(order.getOrderItems().stream()
                         .map(OrderItemInfo::from)
                         .toList())
@@ -47,10 +47,10 @@ public static OrderItemInfo from(OrderItem orderItem) {
             return OrderItemInfo.builder()
                     .id(orderItem.getId())
                     .quantity(orderItem.getQuantity())
-                    .unitPrice(orderItem.getUnitPrice())
+                    .unitPrice(orderItem.getProductPrice()) // productPrice를 unitPrice로 매핑
                     .totalAmount(orderItem.getTotalAmount())
-                    .productId(orderItem.getProduct().getId())
-                    .productName(orderItem.getProduct().getName())
+                    .productId(orderItem.getProductId())
+                    .productName(orderItem.getProductName())
                     .build();
         }
     }
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 6a529631c..a4e83e1df 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
@@ -18,21 +18,21 @@ public class PointFacade {
 
     // 포인트 조회
     @Transactional(readOnly = true)
-    public PointInfo getPoint(String loginId){
-        Point point = pointService.findByUserLoginId(loginId)
-                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] Point를 찾을 수 없습니다."));
+    public PointInfo getPoint(Long userId){
+        Point point = pointService.findByUserId(userId)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[userId = " + userId + "] Point를 찾을 수 없습니다."));
         return PointInfo.from(point);
     }
 
     // 포인트 충전
     @Transactional
-    public PointInfo charge(String loginId, BigDecimal chargeAmount) {
+    public PointInfo charge(Long userId, BigDecimal chargeAmount) {
         // 동시성 안전하게 포인트 충전
-        pointService.charge(loginId, chargeAmount);
+        pointService.charge(userId, chargeAmount);
         
         // 충전 후 최신 포인트 정보 조회
-        Point point = pointService.findByUserLoginId(loginId)
-                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] 포인트를 충전 후 Point 객체를 찾을 수 없습니다."));
+        Point point = pointService.findByUserId(userId)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[userId = " + userId + "] 포인트를 충전 후 Point 객체를 찾을 수 없습니다."));
         return PointInfo.from(point);
     }
 }
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 774290d56..ef3cfb8d6 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
@@ -1,5 +1,6 @@
 package com.loopers.application.user;
 
+import com.loopers.domain.point.Point;
 import com.loopers.domain.point.PointService;
 import com.loopers.domain.user.User;
 import com.loopers.domain.user.UserService;
@@ -44,18 +45,33 @@ public UserInfo saveUser(UserInfo userInfo) {
         User savedUser = userService.saveUser(user)
                 .orElseThrow(() -> new CoreException(ErrorType.INTERNAL_ERROR, "User 를 저장하지 못했습니다."));
 
-        return UserInfo.from(savedUser);
+        // User 저장 후 Point 별도 생성
+        Point point = Point.builder()
+                .userId(savedUser.getId())
+                .build();
+        Point savedPoint = pointService.savePoint(point)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
+                        "Point 저장에 실패했습니다."
+                ));
+
+        return UserInfo.from(savedUser, savedPoint);
     }
 
     /**
      * 회원 조회 (User 정보 가져오기)
-     * @param loginId
+     * @param userId
      * @return
      */
     @Transactional(readOnly = true)
-    public UserInfo getUser(String loginId){
-        User user = userService.findUserByLoginId(loginId)
-                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[loginId = " + loginId + "] User 를 찾지 못했습니다."));
-        return UserInfo.from(user);
+    public UserInfo getUser(Long userId){
+        User user = userService.findById(userId)
+                .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[userId = " + userId + "] User 를 찾지 못했습니다."));
+        
+        // Point 별도 조회
+        Point point = pointService.findByUserId(userId)
+                .orElse(null); // Point가 없을 수도 있음
+        
+        return UserInfo.from(user, point);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java
index 65af7c44a..50fe60714 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java
@@ -20,7 +20,17 @@ public static UserInfo from(User user) {
                 .email(user.getEmail())
                 .birthday(user.getBirthday())
                 .gender(user.getGender())
-                .point(user.getPoint())
+                .point(null)
+                .build();
+    }
+
+    public static UserInfo from(User user, Point point) {
+        return UserInfo.builder()
+                .loginId(user.getLoginId())
+                .email(user.getEmail())
+                .birthday(user.getBirthday())
+                .gender(user.getGender())
+                .point(point)
                 .build();
     }
 
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
index a239a66ef..dcbe765a9 100644
--- 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
@@ -1,7 +1,6 @@
 package com.loopers.domain.brand;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.product.Product;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -10,8 +9,6 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-import java.util.List;
-
 @Entity
 @Table(name = "brand")
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -25,9 +22,6 @@ public class Brand extends BaseEntity {
     private Boolean isVisible;
     private Boolean isSellable;
 
-    @OneToMany(mappedBy = "brand")
-    List<Product> products;
-
     @Builder
     private Brand(
         String name
@@ -35,22 +29,12 @@ private Brand(
         , BrandStatus status
         , Boolean isVisible
         , Boolean isSellable
-        , List<Product> products
     ) {
         this.name = name;
         this.description = description;
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
-        this.products = products;
-    }
-
-    // 상품 목록 필드를 세팅 (초기화 위해 있음)
-    public void setPoint(List<Product> products) {
-        if(this.products != null) {
-            throw new CoreException(ErrorType.CONFLICT, "Brand : List<Product> 가 이미 존재합니다.");
-        }
-        this.products = products;
     }
 
     // 유효성 검사
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
index 48facf082..46b50fc2b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java
@@ -1,8 +1,6 @@
 package com.loopers.domain.coupon;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.order.Order;
-import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -21,28 +19,20 @@ public class Coupon extends BaseEntity {
 
     @Enumerated(EnumType.STRING)
     private CouponType couponType;
-
     private BigDecimal discountValue;
-
     private Boolean isUsed = false;
-
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "user_id")
-    private User user;
-
-    @OneToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "order_id")
-    private Order order;
+    private Long userId;
+    private Long orderId;
 
     @Builder
     private Coupon(
             CouponType couponType,
             BigDecimal discountValue,
-            User user
+            Long userId
     ) {
         this.couponType = couponType;
         this.discountValue = discountValue;
-        this.user = user;
+        this.userId = userId;
         guard();
     }
 
@@ -87,19 +77,19 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : isUsed가 비어있을 수 없습니다.");
         }
 
-        // isUsed와 order의 관계 검증
-        // isUsed가 false면 order는 null이어야 함
-        if (!isUsed && order != null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 미사용 쿠폰은 order가 null이어야 합니다.");
+        // isUsed와 orderId의 관계 검증
+        // isUsed가 false면 orderId는 null이어야 함
+        if (!isUsed && orderId != null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 미사용 쿠폰은 orderId가 null이어야 합니다.");
         }
-        // isUsed가 true면 order는 null이면 안 됨
-        if (isUsed && order == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 사용된 쿠폰은 order가 비어있을 수 없습니다.");
+        // isUsed가 true면 orderId는 null이면 안 됨
+        if (isUsed && orderId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : 사용된 쿠폰은 orderId가 비어있을 수 없습니다.");
         }
 
-        // user 검증: null이 아니어야 함
-        if (user == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : user가 비어있을 수 없습니다.");
+        // userId 검증: null이 아니어야 함
+        if (userId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Coupon : userId가 비어있을 수 없습니다.");
         }
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
index 9c1c12999..bdece38a2 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponRepository.java
@@ -1,15 +1,13 @@
 package com.loopers.domain.coupon;
 
-import com.loopers.domain.user.User;
-
 import java.util.List;
 import java.util.Optional;
 
 public interface CouponRepository {
     Optional<Coupon> save(Coupon coupon);
     Optional<Coupon> findById(Long couponId);
-    List<Coupon> findByUser(User user);
-    List<Coupon> findByUserAndIsUsedFalse(User user);
+    List<Coupon> findByUserId(Long userId);
+    List<Coupon> findByUserIdAndIsUsedFalse(Long userId);
     long useCoupon(Long couponId, Long orderId, Long userId);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
index 06fded989..d135005cb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
@@ -1,7 +1,6 @@
 package com.loopers.domain.coupon;
 
 import com.loopers.domain.order.Order;
-import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
@@ -42,16 +41,16 @@ public Coupon findById(Long couponId) {
      * 사용자의 쿠폰 목록 조회
      */
     @Transactional(readOnly = true)
-    public List<Coupon> findByUser(User user) {
-        return couponRepository.findByUser(user);
+    public List<Coupon> findByUserId(Long userId) {
+        return couponRepository.findByUserId(userId);
     }
 
     /**
      * 사용자의 미사용 쿠폰 목록 조회
      */
     @Transactional(readOnly = true)
-    public List<Coupon> findAvailableCouponsByUser(User user) {
-        return couponRepository.findByUserAndIsUsedFalse(user);
+    public List<Coupon> findAvailableCouponsByUserId(Long userId) {
+        return couponRepository.findByUserIdAndIsUsedFalse(userId);
     }
 
     /**
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
index fa0fc3c89..e1c1561b7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/Like.java
@@ -1,7 +1,6 @@
 package com.loopers.domain.like.entity;
 
 import com.loopers.domain.AuditEntity;
-import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -19,34 +18,14 @@ public class Like extends AuditEntity {
     @EmbeddedId
     private LikeId likeId;
 
-    @MapsId("userId")
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "user_id")
-    private User user;
-
     @Builder
     private Like(
-        User user
+        Long userId
         , Long likeTargetId
         , LikeTargetType likeTargetType
     ) {
-        // 입력값 선검증으로 NPE 방지 및 메시지 일관화
-        if (user == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Like : user가 비어있을 수 없습니다.");
-        }
-        if (likeTargetId == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId가 비어있을 수 없습니다.");
-        }
-        if (likeTargetId <= 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetId는 양수여야 합니다.");
-        }
-        if (likeTargetType == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeTargetType이 비어있을 수 없습니다.");
-        }
-
-        this.user = user;
         this.likeId = LikeId.builder()
-            .userId(user.getId())
+            .userId(userId)
             .likeTargetId(likeTargetId)
             .likeTargetType(likeTargetType)
             .build();
@@ -56,9 +35,16 @@ private Like(
     // 유효성 검사
     @Override
     protected void guard() {
-        // user 유효성 검사
-        if(user == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Like : user가 비어있을 수 없습니다.");
+        // likeId 유효성 검사
+        if(likeId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : likeId가 비어있을 수 없습니다.");
+        }
+
+        // userId 유효성 검사
+        if(likeId.getUserId() == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : userId가 비어있을 수 없습니다.");
+        } else if(likeId.getUserId() <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Like : userId는 양수여야 합니다.");
         }
 
         // likeTargetId 유효성 검사
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
index f95f5759e..5cd119eb3 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/entity/LikeId.java
@@ -2,7 +2,6 @@
 
 import java.io.Serializable;
 
-import jakarta.persistence.Column;
 import jakarta.persistence.Embeddable;
 import jakarta.persistence.EnumType;
 import jakarta.persistence.Enumerated;
@@ -21,14 +20,10 @@
 @Builder
 public class LikeId implements Serializable {
     
-    @Column(name = "user_id")
     private Long userId;
-
-    @Column(name = "like_target_id")
     private Long likeTargetId;
 
     @Enumerated(EnumType.STRING)
-    @Column(name = "like_target_type")
     private LikeTargetType likeTargetType;
 
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
index 8e84f1906..b3b5f9f8c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
@@ -1,8 +1,6 @@
 package com.loopers.domain.order;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -29,9 +27,7 @@ public class Order extends BaseEntity {
     @Enumerated(EnumType.STRING)
     private OrderStatus orderStatus = OrderStatus.PENDING;
 
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "user_id")
-    private User user;
+    private Long userId;
 
     @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
     private List<OrderItem> orderItems = new ArrayList<>();
@@ -40,11 +36,11 @@ public class Order extends BaseEntity {
     private Order(
             BigDecimal discountAmount,
             BigDecimal shippingFee,
-            User user
+            Long userId
     ) {
         this.discountAmount = discountAmount;
         this.shippingFee = shippingFee;
-        this.user = user;
+        this.userId = userId;
 
         // 유효성 검사
         guard();
@@ -75,26 +71,32 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : orderStatus가 비어있을 수 없습니다.");
         }
 
-        // user 검증: null이 아니어야 함 (주문한 사용자 정보 필수)
-        if (user == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Order : user가 비어있을 수 없습니다.");
+        // userId 검증: null이 아니어야 함 (주문한 사용자 정보 필수)
+        if (userId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Order : userId가 비어있을 수 없습니다.");
         }
     }
 
     /**
      * 주문 상품 추가
      * 주문 상품을 리스트에 추가하고, 양방향 관계를 설정함
+     * @param productId 상품 ID
+     * @param productName 주문 시점의 상품명 (스냅샷)
+     * @param productPrice 주문 시점의 상품 가격 (스냅샷)
+     * @param quantity 수량
      */
-    public void addOrderItem(Product product, Integer quantity) {
+    public void addOrderItem(Long productId, String productName, BigDecimal productPrice, Integer quantity) {
         
         // 주문 상태 검증
         if(this.orderStatus != OrderStatus.PENDING) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Order : PENDING 상태의 주문만 주문 상품을 추가할 수 있습니다.");
         }
 
-        // OrderItem 생성
+        // OrderItem 생성 (스냅샷 데이터 사용)
         OrderItem orderItem = OrderItem.builder()
-            .product(product)
+            .productId(productId)
+            .productName(productName)
+            .productPrice(productPrice)
             .quantity(quantity)
             .order(this)
             .build();
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
index ee0724622..96dc1695d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
@@ -1,7 +1,6 @@
 package com.loopers.domain.order;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.product.Product;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -18,33 +17,39 @@
 @Getter
 public class OrderItem extends BaseEntity {
 
+    // Product 객체 대신 ID와 스냅샷 데이터 저장
+    private Long productId;
+    private String productName;
+    private BigDecimal productPrice;
     private Integer quantity;
-    private BigDecimal unitPrice = BigDecimal.ZERO;
-    private BigDecimal totalAmount = BigDecimal.ZERO;
+    private BigDecimal totalAmount;
 
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "order_id")
     private Order order;
 
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "product_id")
-    private Product product;
-
     @Builder
     private OrderItem(
             Integer quantity,
-            Product product,
+            Long productId,
+            String productName,
+            BigDecimal productPrice,
             Order order
     ) {
+        // 필드 세팅
         this.quantity = quantity;
-        this.product = product;
+        this.productId = productId;
+        this.productName = productName;
+        this.productPrice = productPrice;
+        if(quantity != null && productPrice != null) {
+            this.totalAmount = this.productPrice.multiply(BigDecimal.valueOf(quantity));
+        } else {
+            this.totalAmount = null;
+        }
         this.order = order;
-        // 유효성 검사
-        guard();
 
-        // 계산하기
-        this.unitPrice = product.getPrice();
-        this.totalAmount = this.unitPrice.multiply(BigDecimal.valueOf(quantity));
+        // 모든 필드 일관성 검증
+        guard();
     }
 
     @Override
@@ -56,46 +61,35 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : quantity는 0보다 커야 합니다.");
         }
         
-        // product 검증: null이 아니어야 함 (주문 상품 정보 필수)
-        if (product == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : product가 비어있을 수 없습니다.");
+        // productId 검증: null이 아니어야 함 (주문 상품 ID 필수)
+        if (productId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : productId가 비어있을 수 없습니다.");
+        }
+
+        // productName 검증: null이 아니어야 함 (주문 시점 상품명 필수)
+        if (productName == null || productName.isBlank()) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : productName이 비어있을 수 없습니다.");
+        }
+
+        // productPrice 검증: null이 아니어야 하며, 0보다 커야 함
+        if (productPrice == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : productPrice가 비어있을 수 없습니다.");
+        } else if (productPrice.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : productPrice는 0보다 커야 합니다.");
         }
 
         // order 검증: null이 아니어야 함 (주문 정보 필수)
         if (order == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : order가 비어있을 수 없습니다.");
         }
-        
-        // unitPrice 검증: null이고 0보다 커야 함
-        if (unitPrice == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : unitPrice가 비어있을 수 없습니다.");
-        } else if (unitPrice.compareTo(BigDecimal.ZERO) < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : unitPrice는 0보다 커야 합니다.");
-        }
 
-        // totalAmount 검증: null이고 0보다 커야 함
+        // totalAmount 검증: null이 아니어야 하며, 0보다 커야 함
         if (totalAmount == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : totalAmount가 비어있을 수 없습니다.");
-        } else if (totalAmount.compareTo(BigDecimal.ZERO) < 0) {
+        } else if (totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
             throw new CoreException(ErrorType.BAD_REQUEST, "OrderItem : totalAmount는 0보다 커야 합니다.");
         }
     }
-
-    // Order를 통해서만 OrderItem을 생성할 수 있도록 하는 메서드
-    // order는 null일 수 있음 (나중에 setOrder로 설정 가능)
-    // public static OrderItem create(Order order, Integer quantity, Product product) {
-    //     OrderItem orderItem = new OrderItem(quantity, product);
-    //     orderItem.order = order;
-    //     return orderItem;
-    // }
-
-    // public void setOrder(Order order) {
-    //     if (this.order != null && !this.order.equals(order)) {
-    //         throw new CoreException(ErrorType.CONFLICT, "OrderItem : Order가 이미 존재합니다.");
-    //     }
-    //     this.order = order;
-    // }
-
     
 }
 
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
index e568a22b5..07a046d68 100644
--- 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
@@ -1,13 +1,11 @@
 package com.loopers.domain.order;
 
-import com.loopers.domain.user.User;
-
 import java.util.List;
 import java.util.Optional;
 
 public interface OrderRepository {
     Optional<Order> save(Order order);
     Optional<Order> findById(Long id);
-    List<Order> findByUser(User user);
+    List<Order> findByUserId(Long userId);
 }
 
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
index fd17b0802..432c3209b 100644
--- 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
@@ -1,6 +1,5 @@
 package com.loopers.domain.order;
 
-import com.loopers.domain.user.User;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import lombok.RequiredArgsConstructor;
@@ -40,8 +39,8 @@ public Order findOrderById(Long orderId) {
      * 유저의 주문 목록 조회
      */
     @Transactional(readOnly = true)
-    public List<Order> findOrdersByUser(User user) {
-        return orderRepository.findByUser(user);
+    public List<Order> findOrdersByUserId(Long userId) {
+        return orderRepository.findByUserId(userId);
     }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
index 93fe9215a..573a563b4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java
@@ -1,7 +1,6 @@
 package com.loopers.domain.payment;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.order.Order;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -26,26 +25,24 @@ public class Payment extends BaseEntity {
     private BigDecimal amount;
     
     @Enumerated(EnumType.STRING)
-    private PaymentStatus paymentStatus = PaymentStatus.PENDING;
+    private PaymentStatus paymentStatus;
 
-    @OneToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "order_id")
-    private Order order;
+    @Column(unique = true)
+    private Long orderId;
 
     @Builder
     private Payment(
             // String pgTransactionId,
             PaymentMethod method,
             PaymentStatus paymentStatus,
-            Order order
+            Long orderId,
+            BigDecimal amount
     ) {
         // this.pgTransactionId = pgTransactionId;
         this.method = method;
-        this.paymentStatus = paymentStatus;
-        this.order = order;
-        if(order != null) {
-            this.amount = order.getFinalAmount();
-        }
+        this.paymentStatus = (paymentStatus != null) ? paymentStatus : PaymentStatus.PENDING;
+        this.orderId = orderId;
+        this.amount = amount;
         guard();
     }
 
@@ -56,6 +53,11 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : method가 비어있을 수 없습니다.");
         }
 
+        // orderId 검증: null이 아니어야 함 (결제 대상 주문 정보 필수)
+        if (orderId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : orderId가 비어있을 수 없습니다.");
+        }
+
         // amount 검증: null이 아니어야 하며, 0 이상이어야 함
         if (amount == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : amount가 비어있을 수 없습니다.");
@@ -67,11 +69,6 @@ protected void guard() {
         if (paymentStatus == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Payment : paymentStatus가 비어있을 수 없습니다.");
         }
-
-        // order 검증: null이 아니어야 함 (결제 대상 주문 정보 필수)
-        if (order == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Payment : order가 비어있을 수 없습니다.");
-        }
     }
 }
 
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 f9418ddaf..62fbe2936 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,7 +1,6 @@
 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.*;
@@ -20,15 +19,14 @@ public class Point extends BaseEntity {
 
     private BigDecimal amount = BigDecimal.ZERO;    // 포인트 잔액 
 
-    @OneToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "user_id")
-    private User user;                              // 회원 엔티티
+    @Column(unique = true)
+    private Long userId;                              // 회원 ID
 
     @Builder
     private Point(
-        User user
+        Long userId
     ) {
-        this.user = user;
+        this.userId = userId;
         guard();
     }
 
@@ -44,9 +42,9 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Point : amount는 음수가 될 수 없습니다.");
         }
 
-        // user : Null 인지 검사
-       if(user == null) {
-           throw new CoreException(ErrorType.BAD_REQUEST, "Point : user가 Null 이 되면 안 됩니다.");
+        // userId : Null 인지 검사
+       if(userId == null) {
+           throw new CoreException(ErrorType.BAD_REQUEST, "Point : userId가 Null 이 되면 안 됩니다.");
        }
     }
 
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 13bb6128c..0196340bd 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
@@ -4,8 +4,8 @@
 import java.util.Optional;
 
 public interface PointRepository {
-    Optional<Point> findByUser_loginId(String loginId);
+    Optional<Point> findByUserId(Long userId);
     Optional<Point> save(Point point);
-    long deduct(String loginId, BigDecimal deductAmount);
-    long chargeAmount(String loginId, BigDecimal chargeAmount);
+    long deduct(Long userId, BigDecimal deductAmount);
+    long chargeAmount(Long userId, BigDecimal chargeAmount);
 }
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 3179c443e..93bb1e8bc 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
@@ -16,8 +16,8 @@ public class PointService {
     private final PointRepository pointRepository;
 
     @Transactional(readOnly = true)
-    public Optional<Point> findByUserLoginId(String loginId) {
-        return pointRepository.findByUser_loginId(loginId);
+    public Optional<Point> findByUserId(Long userId) {
+        return pointRepository.findByUserId(userId);
     }
 
     @Transactional
@@ -27,12 +27,12 @@ public Optional<Point> savePoint(Point point) {
 
     /**
      * 포인트 차감 (동시성 안전)
-     * @param loginId 사용자 로그인 ID
+     * @param userId 사용자 ID (DB PK)
      * @param deductAmount 차감할 금액
      * @throws CoreException 차감량이 0 이하이거나, 포인트가 부족한 경우
      */
     @Transactional
-    public void deduct(String loginId, BigDecimal deductAmount) {
+    public void deduct(Long userId, BigDecimal deductAmount) {
         // 차감량 유효성 검사
         if (deductAmount == null || deductAmount.compareTo(BigDecimal.ZERO) <= 0) {
             throw new CoreException(
@@ -42,14 +42,14 @@ public void deduct(String loginId, BigDecimal deductAmount) {
         }
 
         // QueryDSL로 동시성 안전하게 업데이트
-        long updatedRows = pointRepository.deduct(loginId, deductAmount);
+        long updatedRows = pointRepository.deduct(userId, deductAmount);
         
         if (updatedRows == 0L) {
             // 포인트 조회하여 상세 메시지 제공
-            Point point = pointRepository.findByUser_loginId(loginId)
+            Point point = pointRepository.findByUserId(userId)
                     .orElseThrow(() -> new CoreException(
                             ErrorType.NOT_FOUND,
-                            "[loginId = " + loginId + "] Point를 찾을 수 없습니다."
+                            "[userId = " + userId + "] Point를 찾을 수 없습니다."
                     ));
             
             throw new CoreException(
@@ -61,12 +61,12 @@ public void deduct(String loginId, BigDecimal deductAmount) {
 
     /**
      * 포인트 충전 (동시성 안전)
-     * @param loginId 사용자 로그인 ID
+     * @param userId 사용자 ID (DB PK)
      * @param chargeAmount 충전할 금액
      * @throws CoreException 충전량이 0 이하이거나, Point를 찾을 수 없는 경우
      */
     @Transactional
-    public void charge(String loginId, BigDecimal chargeAmount) {
+    public void charge(Long userId, BigDecimal chargeAmount) {
         // 충전량 유효성 검사
         if (chargeAmount == null || chargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
             throw new CoreException(
@@ -76,12 +76,12 @@ public void charge(String loginId, BigDecimal chargeAmount) {
         }
 
         // QueryDSL로 동시성 안전하게 업데이트
-        long updatedRows = pointRepository.chargeAmount(loginId, chargeAmount);
+        long updatedRows = pointRepository.chargeAmount(userId, chargeAmount);
         
         if (updatedRows == 0L) {
             throw new CoreException(
                     ErrorType.NOT_FOUND,
-                    "[loginId = " + loginId + "] 포인트를 충전할 Point 객체를 찾을 수 없습니다."
+                    "[userId = " + userId + "] 포인트를 충전할 Point 객체를 찾을 수 없습니다."
             );
         }
     }
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
index 33fb25091..5171406c7 100644
--- 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
@@ -1,7 +1,6 @@
 package com.loopers.domain.stock;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.product.Product;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -18,17 +17,16 @@ public class Stock extends BaseEntity {
 
     private Long quantity;
 
-    @OneToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "product_id")
-    Product product;
+    @Column(unique = true)
+    private Long productId;
 
     @Builder
     private Stock(
         Long quantity
-        , Product product
+        , Long productId
     ) {
         this.quantity = quantity;
-        this.product = product;
+        this.productId = productId;
         
         // 유효성 검사
         guard();
@@ -44,9 +42,9 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Stock : quantity 는 음수가 될 수 없습니다.");
         }
 
-        // product 유효성 검사
-        if(product == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Stock : product 가 비어있을 수 없습니다.");
+        // productId 유효성 검사
+        if(productId == null) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "Stock : productId 가 비어있을 수 없습니다.");
         }
     }
 
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 df93f7093..58bfbe85e 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,9 +1,6 @@
 package com.loopers.domain.user;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.coupon.Coupon;
-import com.loopers.domain.like.entity.Like;
-import com.loopers.domain.point.Point;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -12,9 +9,6 @@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-import java.util.ArrayList;
-import java.util.List;
-
 @Entity
 @Table(name = "user")
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -28,15 +22,6 @@ public class User extends BaseEntity {
     @Enumerated(EnumType.STRING)
     private Gender gender;
 
-    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
-    private Point point = Point.builder().user(this).build();
-
-    @OneToMany(mappedBy = "user")
-    private List<Like> likes = new ArrayList<>();
-
-    @OneToMany(mappedBy = "user")
-    private List<Coupon> coupons = new ArrayList<>();
-
     @Builder
     private User(
         String loginId
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 9c401b4bf..29c208a36 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
@@ -5,4 +5,5 @@
 public interface UserRepository {
     Optional<User> save(User user);
     Optional<User> findByLoginId(String loginId);
+    Optional<User> findById(Long userId);
 }
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 5df001b08..9fd95e2c0 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
@@ -21,4 +21,9 @@ public Optional<User> saveUser(User user) {
     public Optional<User> findUserByLoginId(String loginId) {
         return userRepository.findByLoginId(loginId);
     }
+
+    @Transactional(readOnly = true)
+    public Optional<User> findById(Long userId) {
+        return userRepository.findById(userId);
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
index acf542db3..04a1dde5a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponJpaRepository.java
@@ -6,7 +6,7 @@
 import java.util.List;
 
 public interface CouponJpaRepository extends JpaRepository<Coupon, Long> {
-    List<Coupon> findByUser_Id(Long userId);
-    List<Coupon> findByUser_IdAndIsUsedFalse(Long userId);
+    List<Coupon> findByUserId(Long userId);
+    List<Coupon> findByUserIdAndIsUsedFalse(Long userId);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
index f985126aa..d3709aa31 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponQueryRepository.java
@@ -27,12 +27,12 @@ public long useCoupon(Long couponId, Long orderId, Long userId) {
         return queryFactory
                 .update(coupon)
                 .set(coupon.isUsed, true)
-                .set(coupon.order.id, orderId)
+                .set(coupon.orderId, orderId)
                 .where(
                         coupon.id.eq(couponId)
                         .and(coupon.isUsed.eq(false))  // 미사용 쿠폰만
-                        .and(coupon.order.isNull())  // order가 null이어야 함 (미사용 쿠폰은 order가 null)
-                        .and(coupon.user.id.eq(userId))  // 소유자 확인
+                        .and(coupon.orderId.isNull())  // orderId 가 null이어야 함 (미사용 쿠폰은 orderId 가 null)
+                        .and(coupon.userId.eq(userId))  // 소유자 확인
                         .and(coupon.deletedAt.isNull())  // soft delete 확인
                 )
                 .execute();
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
index 3b0c136a9..e4547ff27 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/coupon/CouponRepositoryImpl.java
@@ -2,7 +2,6 @@
 
 import com.loopers.domain.coupon.Coupon;
 import com.loopers.domain.coupon.CouponRepository;
-import com.loopers.domain.user.User;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
@@ -28,13 +27,13 @@ public Optional<Coupon> findById(Long couponId) {
     }
 
     @Override
-    public List<Coupon> findByUser(User user) {
-        return couponJpaRepository.findByUser_Id(user.getId());
+    public List<Coupon> findByUserId(Long userId) {
+        return couponJpaRepository.findByUserId(userId);
     }
 
     @Override
-    public List<Coupon> findByUserAndIsUsedFalse(User user) {
-        return couponJpaRepository.findByUser_IdAndIsUsedFalse(user.getId());
+    public List<Coupon> findByUserIdAndIsUsedFalse(Long userId) {
+        return couponJpaRepository.findByUserIdAndIsUsedFalse(userId);
     }
 
     @Override
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
index 07a1bf7de..92cec32a7 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
@@ -1,12 +1,11 @@
 package com.loopers.infrastructure.order;
 
 import com.loopers.domain.order.Order;
-import com.loopers.domain.user.User;
 import org.springframework.data.jpa.repository.JpaRepository;
 
 import java.util.List;
 
 public interface OrderJpaRepository extends JpaRepository<Order, Long> {
-    List<Order> findByUser(User user);
+    List<Order> findByUserId(Long userId);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
index 8a8ac7d57..1ba1b91a8 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
@@ -2,7 +2,6 @@
 
 import com.loopers.domain.order.Order;
 import com.loopers.domain.order.OrderRepository;
-import com.loopers.domain.user.User;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
@@ -27,8 +26,8 @@ public Optional<Order> findById(Long id) {
     }
 
     @Override
-    public List<Order> findByUser(User user) {
-        return orderJpaRepository.findByUser(user);
+    public List<Order> findByUserId(Long userId) {
+        return orderJpaRepository.findByUserId(userId);
     }
 }
 
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 36faba058..e1ab87277 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
@@ -6,5 +6,5 @@
 import java.util.Optional;
 
 public interface PointJpaRepository extends JpaRepository<Point, Integer> {
-    Optional<Point> findByUser_loginId(String loginId);
+    Optional<Point> findByUserId(Long userId);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
index 48cabc545..4a1e881b5 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointQueryRepository.java
@@ -16,11 +16,11 @@ public class PointQueryRepository {
 
     /**
      * 포인트 차감 (동시성 안전)
-     * @param loginId 사용자 로그인 ID
+     * @param userId 사용자 ID (DB PK)
      * @param deductAmount 차감할 금액
      * @return 업데이트된 행 수 (1이면 성공, 0이면 포인트 부족 또는 사용자 없음)
      */
-    public long deduct(String loginId, BigDecimal deductAmount) {
+    public long deduct(Long userId, BigDecimal deductAmount) {
         if (deductAmount == null || deductAmount.compareTo(BigDecimal.ZERO) <= 0) {
             return 0L;
         }
@@ -29,7 +29,7 @@ public long deduct(String loginId, BigDecimal deductAmount) {
                 .update(point)
                 .set(point.amount, point.amount.subtract(deductAmount))
                 .where(
-                        point.user.loginId.eq(loginId)          // 사용자 로그인 ID와 일치하는 경우
+                        point.userId.eq(userId)                 // 사용자 ID와 일치하는 경우
                         .and(point.amount.goe(deductAmount))    // 포인트가 충분한 경우만
                 )
                 .execute();
@@ -37,11 +37,11 @@ public long deduct(String loginId, BigDecimal deductAmount) {
 
     /**
      * 포인트 충전 (동시성 안전)
-     * @param loginId 사용자 로그인 ID
+     * @param userId 사용자 ID (DB PK)
      * @param chargeAmount 충전할 금액
      * @return 업데이트된 행 수 (1이면 성공, 0이면 사용자 없음)
      */
-    public long chargeAmount(String loginId, BigDecimal chargeAmount) {
+    public long chargeAmount(Long userId, BigDecimal chargeAmount) {
         if (chargeAmount == null || chargeAmount.compareTo(BigDecimal.ZERO) <= 0) {
             return 0L;
         }
@@ -49,7 +49,7 @@ public long chargeAmount(String loginId, BigDecimal chargeAmount) {
         return queryFactory
                 .update(point)
                 .set(point.amount, point.amount.add(chargeAmount))
-                .where(point.user.loginId.eq(loginId))
+                .where(point.userId.eq(userId))
                 .execute();
     }
 }
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 e0012ed9f..bac3c1b02 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
@@ -16,8 +16,8 @@ public class PointRepositoryImpl implements PointRepository {
     private final PointQueryRepository pointQueryRepository;
 
     @Override
-    public Optional<Point> findByUser_loginId(String loginId) {
-        return pointJpaRepository.findByUser_loginId(loginId);
+    public Optional<Point> findByUserId(Long userId) {
+        return pointJpaRepository.findByUserId(userId);
     }
 
     @Override
@@ -27,12 +27,12 @@ public Optional<Point> save(Point point) {
     }
 
     @Override
-    public long deduct(String loginId, BigDecimal deductAmount) {
-        return pointQueryRepository.deduct(loginId, deductAmount);
+    public long deduct(Long userId, BigDecimal deductAmount) {
+        return pointQueryRepository.deduct(userId, deductAmount);
     }
 
     @Override
-    public long chargeAmount(String loginId, BigDecimal chargeAmount) {
-        return pointQueryRepository.chargeAmount(loginId, chargeAmount);
+    public long chargeAmount(Long userId, BigDecimal chargeAmount) {
+        return pointQueryRepository.chargeAmount(userId, chargeAmount);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
index 9f31079c4..7609fd556 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/stock/StockQueryRepository.java
@@ -27,7 +27,7 @@ public long decreaseQuantity(Long productId, Long decreaseQuantity) {
                 .update(stock)
                 .set(stock.quantity, stock.quantity.subtract(decreaseQuantity))
                 .where(
-                        stock.product.id.eq(productId)              // 상품 ID와 일치하는 경우
+                        stock.productId.eq(productId)              // 상품 ID와 일치하는 경우
                         .and(stock.quantity.goe(decreaseQuantity))  // 재고가 충분한 경우만
                 )
                 .execute();
@@ -47,7 +47,7 @@ public long increaseQuantity(Long productId, Long increaseQuantity) {
         return queryFactory
                 .update(stock)
                 .set(stock.quantity, stock.quantity.add(increaseQuantity))
-                .where(stock.product.id.eq(productId))
+                .where(stock.productId.eq(productId))
                 .execute();
     }
 }
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 e0fe6c743..2d56059d0 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
@@ -23,4 +23,9 @@ public Optional<User> save(User user) {
     public Optional<User> findByLoginId(String loginId) {
         return userJpaRepository.findByLoginId(loginId);
     }
+
+    @Override
+    public Optional<User> findById(Long userId) {
+        return userJpaRepository.findById(userId);
+    }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
index c506505f7..c79a5c3fc 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
@@ -132,15 +132,16 @@ void setUp() {
         testProduct = savedProduct;
         testProductId = savedProduct.getId();
 
-        // 테스트용 Stock 생성 (재고 100개로 설정)
-        // Product 생성 시 Stock이 자동으로 생성되므로, 재고를 100개로 증가
-        Stock stock = stockRepository.findByProductId(testProductId)
-                .orElseThrow(() -> new RuntimeException("Stock을 찾을 수 없습니다"));
-        // 현재 재고를 확인하고 100개가 되도록 조정
-        long currentQuantity = stock.getQuantity();
-        if (currentQuantity < 100L) {
-            stockService.increaseQuantity(testProductId, 100L - currentQuantity);
-        }
+        // Product 저장 후 Stock 별도 생성
+        Stock stock = Stock.builder()
+                .product(savedProduct)
+                .quantity(0L)
+                .build();
+        stockService.saveStock(stock)
+                .orElseThrow(() -> new RuntimeException("Stock 저장 실패"));
+
+        // 테스트용 Stock 재고를 100개로 설정
+        stockService.increaseQuantity(testProductId, 100L);
 
         // 테스트용 포인트 충전 (100000원)
         pointService.charge(testLoginId, BigDecimal.valueOf(100000));
@@ -363,9 +364,9 @@ void createOrder_withInsufficientPoint_BadRequest() {
             assertTrue(exception.getCustomMessage().contains("포인트가 부족"));
         }
 
-        @DisplayName("실패 케이스: 쿠폰 사용 불가 시 BAD_REQUEST 예외 발생")
+        @DisplayName("실패 케이스: 이미 사용된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
         @Test
-        void createOrder_withUnavailableCoupon_BadRequest() {
+        void createOrder_withUsedCoupon_BadRequest() {
             // arrange
             // 쿠폰 생성
             Coupon coupon = Coupon.builder()
@@ -382,13 +383,13 @@ void createOrder_withUnavailableCoupon_BadRequest() {
                     .shippingFee(BigDecimal.ZERO)
                     .user(testUser)
                     .build();
-            firstOrder.addOrderItem(testProduct, 1);
+            firstOrder.addOrderItem(testProduct.getId(), testProduct.getName(), testProduct.getPrice(), 1);
             
             // Order 저장
             Order savedFirstOrder = orderService.saveOrder(firstOrder)
                     .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
 
-            // CouponService를 사용하여 쿠폰 사용 (guard() 검증을 통과하도록)
+            // CouponService를 사용하여 쿠폰 사용
             couponService.useCoupon(savedFirstOrder, savedCoupon.getId());
 
             // 이미 사용된 쿠폰으로 다시 주문 생성 시도
@@ -409,7 +410,95 @@ void createOrder_withUnavailableCoupon_BadRequest() {
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+            assertTrue(exception.getCustomMessage().contains("이미 사용된 쿠폰입니다"),
+                    String.format("예상 메시지: '이미 사용된 쿠폰입니다', 실제 메시지: %s", exception.getCustomMessage()));
+        }
+
+        @DisplayName("실패 케이스: 다른 사용자의 쿠폰 사용 시 BAD_REQUEST 예외 발생")
+        @Test
+        void createOrder_withOtherUserCoupon_BadRequest() {
+            // arrange
+            // 다른 사용자 생성
+            UserInfo otherUserInfo = UserInfo.builder()
+                    .loginId("other34")
+                    .email("other@test.com")
+                    .birthday("1990-01-01")
+                    .gender(Gender.MALE)
+                    .build();
+            userFacade.saveUser(otherUserInfo);
+            User otherUser = userRepository.findByLoginId("other34")
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"));
+
+            // 다른 사용자의 쿠폰 생성
+            Coupon otherUserCoupon = Coupon.builder()
+                    .couponType(CouponType.FIXED_AMOUNT)
+                    .discountValue(BigDecimal.valueOf(5000))
+                    .user(otherUser)
+                    .build();
+            Coupon savedOtherUserCoupon = couponRepository.save(otherUserCoupon)
+                    .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+
+            // testUser가 다른 사용자의 쿠폰을 사용하려고 시도
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(1)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(List.of(savedOtherUserCoupon.getId()))
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertTrue(exception.getCustomMessage().contains("본인 쿠폰 아닙니다"),
+                    String.format("예상 메시지: '본인 쿠폰 아닙니다', 실제 메시지: %s", exception.getCustomMessage()));
+        }
+
+        @DisplayName("실패 케이스: 삭제된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
+        @Test
+        void createOrder_withDeletedCoupon_BadRequest() {
+            // arrange
+            // 쿠폰 생성
+            Coupon coupon = Coupon.builder()
+                    .couponType(CouponType.FIXED_AMOUNT)
+                    .discountValue(BigDecimal.valueOf(5000))
+                    .user(testUser)
+                    .build();
+            Coupon savedCoupon = couponRepository.save(coupon)
+                    .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
+
+            // 쿠폰 삭제 (soft delete)
+            savedCoupon.delete();
+            couponRepository.save(savedCoupon);
+
+            // 삭제된 쿠폰으로 주문 생성 시도
+            List<OrderDto.OrderItemRequest> items = List.of(
+                    OrderDto.OrderItemRequest.builder()
+                            .productId(testProductId)
+                            .quantity(1)
+                            .build()
+            );
+            OrderDto.CreateOrderRequest request = OrderDto.CreateOrderRequest.builder()
+                    .items(items)
+                    .couponIds(List.of(savedCoupon.getId()))
+                    .build();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    orderFacade.createOrder(testLoginId, request)
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            // 삭제된 쿠폰의 경우 "사용 불가능한 쿠폰입니다" 또는 다른 메시지가 나올 수 있음
+            assertTrue(exception.getCustomMessage().contains("사용 불가능한 쿠폰입니다") || 
+                       exception.getCustomMessage().contains("이미 사용된 쿠폰입니다"),
+                    String.format("예상 메시지: '사용 불가능한 쿠폰입니다' 또는 '이미 사용된 쿠폰입니다', 실제 메시지: %s", exception.getCustomMessage()));
         }
     }
 
@@ -497,7 +586,7 @@ void createOrder_concurrentRequests_consistencyMaintained() throws InterruptedEx
             assertEquals(initialStock, successOrders.size(), "재고 수만큼만 주문이 성공해야 함");
             assertTrue(failures.size() >= threadCount - initialStock, "초과 주문은 실패해야 함");
             failures.forEach(failure -> {
-                assertTrue(failure instanceof CoreException, "실패는 CoreException이어야 함");
+                assertInstanceOf(CoreException.class, failure, "실패는 CoreException이어야 함");
                 CoreException exception = (CoreException) failure;
                 assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType(), "실패 타입은 BAD_REQUEST여야 함");
                 assertTrue(exception.getCustomMessage().contains("재고가 부족"), "실패 메시지는 재고 부족이어야 함");
@@ -557,7 +646,7 @@ private long verifyCoupons(List<Long> couponIds, int initialStock, BigDecimal co
                                     .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다"));
                             if (coupon.getIsUsed()) {
                                 Order order = coupon.getOrder();
-                                assertEquals(couponDiscount.compareTo(order.getDiscountAmount()), 0, "쿠폰 사용 주문 할인 금액 검증");
+                                assertEquals(0, couponDiscount.compareTo(order.getDiscountAmount()), "쿠폰 사용 주문 할인 금액 검증");
                             }
                         });
                 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
index 172ff949a..e4cfbd657 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
@@ -62,7 +62,9 @@ void saveUser_withValidFields_Success() {
             assertEquals(validEmail, userInfo.email());
             assertEquals(validBirthday, userInfo.birthday());
             assertEquals(validGender, userInfo.gender());
-            assertTrue(validPoint.compareTo(userInfo.point().getAmount()) == 0);
+            // Point는 별도로 생성되므로 null이 아니고 초기값이 0인지 확인
+            assertNotNull(userInfo.point(), "Point가 생성되어야 함");
+            assertTrue(validPoint.compareTo(userInfo.point().getAmount()) == 0, "Point의 초기 amount는 0이어야 함");
         }
 
         @DisplayName("실패 케이스 : 이미 저장된 User 를 다시 저장하면 실패")
@@ -103,8 +105,10 @@ void getUser_idExists_Success() {
                     .birthday(validBirthday)
                     .gender(validGender)
                     .build();
-            UserInfo userInfo = userFacade.saveUser(userinfo);
+            userFacade.saveUser(userinfo);
+            
             // act
+            UserInfo userInfo = userFacade.getUser(validLoginId);
 
             // assert
             assertNotNull(userInfo);
@@ -113,6 +117,8 @@ void getUser_idExists_Success() {
                     , () -> assertEquals(userInfo.email(), validEmail)
                     , () -> assertEquals(userInfo.birthday(), validBirthday)
                     , () -> assertEquals(userInfo.gender(), validGender)
+                    , () -> assertNotNull(userInfo.point(), "Point가 조회되어야 함")
+                    , () -> assertTrue(validPoint.compareTo(userInfo.point().getAmount()) == 0, "Point의 초기 amount는 0이어야 함")
             );
         }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
index b6970cb15..2dd49d9b6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
@@ -26,6 +26,7 @@
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -112,7 +113,7 @@ void setUp() {
                 .build();
         
         // OrderItem 추가
-        testOrder.addOrderItem(savedProduct, 10);
+        testOrder.addOrderItem(savedProduct.getId(), savedProduct.getName(), savedProduct.getPrice(), 10);
 
         testOrder = orderService.saveOrder(testOrder)
                 .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
@@ -174,7 +175,8 @@ void useCoupon_withNonExistentCoupon_NotFound() {
             );
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[couponId = " + nonExistentCouponId + "] Coupon을 찾을 수 없습니다"));
+            assertEquals("[couponId = " + nonExistentCouponId + "] Coupon을 찾을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: '[couponId = %d] Coupon을 찾을 수 없습니다', 실제 메시지: %s", nonExistentCouponId, exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스: 다른 사용자의 쿠폰 사용 시 BAD_REQUEST 예외 발생")
@@ -209,7 +211,8 @@ void useCoupon_withOtherUserCoupon_BadRequest() {
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("본인의 쿠폰만 사용할 수 있습니다"));
+            assertEquals("본인 쿠폰 아닙니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: '본인 쿠폰 아닙니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스: 이미 사용된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
@@ -227,7 +230,8 @@ void useCoupon_withUsedCoupon_BadRequest() {
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+            assertEquals("이미 사용된 쿠폰입니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: '이미 사용된 쿠폰입니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스: 삭제된 쿠폰 사용 시 BAD_REQUEST 예외 발생")
@@ -248,7 +252,9 @@ void useCoupon_withDeletedCoupon_BadRequest() {
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("쿠폰을 사용할 수 없습니다"));
+            // 삭제된 쿠폰의 경우 "사용 불가 쿠폰" 또는 "이미 사용된 쿠폰" 메시지가 나올 수 있음
+            assertEquals("사용 불가능한 쿠폰입니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: '사용 불가능한 쿠폰입니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
     }
 
@@ -304,10 +310,18 @@ void useCoupon_concurrentRequests_onlyOneSuccess() throws InterruptedException {
             long couponUnavailableExceptions = exceptions.stream()
                     .filter(e -> e instanceof CoreException)
                     .filter(e -> ((CoreException) e).getErrorType() == ErrorType.BAD_REQUEST)
-                    .filter(e -> e.getMessage().contains("쿠폰을 사용할 수 없습니다"))
+                    .filter(e -> {
+                        String message = ((CoreException) e).getCustomMessage();
+                        return message.contains("이미 사용된 쿠폰") || message.contains("사용 불가 쿠폰");
+                    })
                     .count();
             assertEquals(failureCount.get(), couponUnavailableExceptions,
-                    String.format("쿠폰 사용 불가 예외 수는 실패 수와 일치해야 함: 예상=%d, 실제=%d", failureCount.get(), couponUnavailableExceptions));
+                    String.format("쿠폰 사용 불가 예외 수는 실패 수와 일치해야 함: 예상=%d, 실제=%d. 실제 예외들: %s", 
+                            failureCount.get(), couponUnavailableExceptions, 
+                            exceptions.stream()
+                                    .filter(e -> e instanceof CoreException)
+                                    .map(e -> ((CoreException) e).getCustomMessage())
+                                    .toList()));
         }
 
         @DisplayName("성공 케이스: 쿠폰 사용이 원자적으로 처리되어 Lost Update가 발생하지 않음")
@@ -448,7 +462,7 @@ private Order createTestOrder() {
                 .build();
         
         // OrderItem 추가
-        order.addOrderItem(savedProduct, 10);
+        order.addOrderItem(savedProduct.getId(), savedProduct.getName(), savedProduct.getPrice(), 10);
 
         return orderService.saveOrder(order)
                 .orElseThrow(() -> new RuntimeException("Order 저장 실패"));
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
index 2d6303288..9982f2131 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
@@ -2,13 +2,14 @@
 
 import com.loopers.domain.like.entity.Like;
 import com.loopers.domain.like.entity.LikeTargetType;
+import com.loopers.domain.user.Gender;
 import com.loopers.domain.user.User;
 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.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -22,16 +23,29 @@ class CreateLikeTest {
         final Long validLikeTargetId = 1L;
         final LikeTargetType validLikeTargetType = LikeTargetType.PRODUCT;
 
+        // 테스트 헬퍼 메서드
+        private User createUserWithId(Long id) {
+            User user = User.builder()
+                    .loginId("validId123") // 실제 유효한 값 필요
+                    .email("test@test.com")
+                    .birthday("1990-01-01")
+                    .gender(Gender.MALE)
+                    .build(); // 여기서 User의 guard() 로직도 함께 검증됨!
+            
+            // ID 강제 주입 (ReflectionTestUtils 등 사용)
+            ReflectionTestUtils.setField(user, "id", id);
+            return user;
+        }
+
         @DisplayName("성공 케이스: 필드가 모두 유효하면 Like 객체 생성 성공")
         @Test
         void createLike_withValidFields_Success() {
             // arrange
-            User user = Mockito.mock(User.class);
-            Mockito.when(user.getId()).thenReturn(1L);
+            User user = createUserWithId(1L);
 
             // act
             Like like = Like.builder()
-                    .user(user)
+                    .userId(user.getId())
                     .likeTargetId(validLikeTargetId)
                     .likeTargetType(validLikeTargetType)
                     .build();
@@ -39,7 +53,8 @@ void createLike_withValidFields_Success() {
             // assert
             assertNotNull(like);
             assertAll(
-                    () -> assertNotNull(like.getUser()),
+                    () -> assertNotNull(like.getLikeId().getUserId()),
+                    () -> assertEquals(user.getId(), like.getLikeId().getUserId()),
                     () -> assertEquals(validLikeTargetId, like.getLikeId().getLikeTargetId()),
                     () -> assertEquals(validLikeTargetType, like.getLikeId().getLikeTargetType())
             );
@@ -51,7 +66,7 @@ void createLike_withNullUser_BadRequest() {
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
                 Like.builder()
-                        .user(null)
+                        .userId(null)
                         .likeTargetId(validLikeTargetId)
                         .likeTargetType(validLikeTargetType)
                         .build();
@@ -65,13 +80,12 @@ void createLike_withNullUser_BadRequest() {
         @Test
         void createLike_withNullLikeTargetId_BadRequest() {
             // arrange
-            User user = Mockito.mock(User.class);
-            Mockito.when(user.getId()).thenReturn(1L);
+            User user = createUserWithId(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
                 Like.builder()
-                        .user(user)
+                        .userId(user.getId())
                         .likeTargetId(null)
                         .likeTargetType(validLikeTargetType)
                         .build();
@@ -85,13 +99,12 @@ void createLike_withNullLikeTargetId_BadRequest() {
         @Test
         void createLike_withInvalidLikeTargetId_BadRequest() {
             // arrange
-            User user = Mockito.mock(User.class);
-            Mockito.when(user.getId()).thenReturn(1L);
+            User user = createUserWithId(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
                 Like.builder()
-                        .user(user)
+                        .userId(user.getId())
                         .likeTargetId(0L)
                         .likeTargetType(validLikeTargetType)
                         .build();
@@ -105,13 +118,12 @@ void createLike_withInvalidLikeTargetId_BadRequest() {
         @Test
         void createLike_withNullLikeTargetType_BadRequest() {
             // arrange
-            User user = Mockito.mock(User.class);
-            Mockito.when(user.getId()).thenReturn(1L);
+            User user = createUserWithId(1L);
 
             // act & assert
             CoreException result = assertThrows(CoreException.class, () -> {
                 Like.builder()
-                        .user(user)
+                        .userId(user.getId())
                         .likeTargetId(validLikeTargetId)
                         .likeTargetType(null)
                         .build();
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
index d919e64c8..8735acc18 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
@@ -11,6 +11,7 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 
@@ -31,8 +32,6 @@ private Product createValidProduct() {
                 .isSellable(true)
                 .build();
 
-        // Product 생성 시 필드 초기화로 Stock이 자동 생성됨 (quantity=0L)
-        // Product.builder()로 생성하면 stock 필드가 자동으로 초기화됨
         Product product = Product.builder()
                 .name("Test Product")
                 .description("Test Description")
@@ -40,8 +39,11 @@ private Product createValidProduct() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(brand)
+                .brandId(brand.getId())
                 .build();
+        
+        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        ReflectionTestUtils.setField(product, "id", 1L);
 
         return product;
     }
@@ -57,7 +59,7 @@ private Order createValidOrder() {
         Order order = Order.builder()
                 .discountAmount(BigDecimal.valueOf(1000))
                 .shippingFee(BigDecimal.valueOf(2000))
-                .user(user)
+                .userId(user.getId())
                 .build();
 
         return order;
@@ -73,13 +75,15 @@ void createOrderItem_withValidFields_Success() {
             // arrange
             Product product = createValidProduct();
             Order order = createValidOrder();
-            BigDecimal expectedUnitPrice = product.getPrice(); // 생성 시 product.getPrice()로 자동 설정됨
-            BigDecimal expectedTotalAmount = expectedUnitPrice.multiply(BigDecimal.valueOf(validQuantity)); // unitPrice * quantity로 자동 계산됨
+            BigDecimal expectedProductPrice = product.getPrice(); // 생성 시 productPrice로 설정됨
+            BigDecimal expectedTotalAmount = expectedProductPrice.multiply(BigDecimal.valueOf(validQuantity)); // productPrice * quantity로 자동 계산됨
 
             // act
             OrderItem orderItem = OrderItem.builder()
                     .quantity(validQuantity)
-                    .product(product)
+                    .productId(product.getId())
+                    .productName(product.getName())
+                    .productPrice(product.getPrice())
                     .order(order)
                     .build();
 
@@ -87,9 +91,10 @@ void createOrderItem_withValidFields_Success() {
             assertNotNull(orderItem);
             assertAll(
                     () -> assertEquals(validQuantity, orderItem.getQuantity()),
-                    () -> assertEquals(expectedUnitPrice, orderItem.getUnitPrice(), "unitPrice는 product.getPrice()로 자동 설정되어야 함"),
-                    () -> assertEquals(expectedTotalAmount, orderItem.getTotalAmount(), "totalAmount는 unitPrice * quantity로 자동 계산되어야 함"),
-                    () -> assertEquals(product, orderItem.getProduct()),
+                    () -> assertEquals(expectedTotalAmount, orderItem.getTotalAmount(), "totalAmount는 productPrice * quantity로 자동 계산되어야 함"),
+                    () -> assertEquals(product.getId(), orderItem.getProductId()),
+                    () -> assertEquals(product.getName(), orderItem.getProductName()),
+                    () -> assertEquals(product.getPrice(), orderItem.getProductPrice()),
                     () -> assertEquals(order, orderItem.getOrder())
             );
         }
@@ -105,13 +110,16 @@ void createOrderItem_withNullQuantity_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     OrderItem.builder()
                             .quantity(null)
-                            .product(product)
+                            .productId(product.getId())
+                            .productName(product.getName())
+                            .productPrice(product.getPrice())
                             .order(order)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("quantity가 비어있을 수 없습니다"));
+            assertEquals("OrderItem : quantity가 비어있을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : quantity가 비어있을 수 없습니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스 : quantity가 0이면 예외 발생")
@@ -125,13 +133,16 @@ void createOrderItem_withZeroQuantity_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     OrderItem.builder()
                             .quantity(0)
-                            .product(product)
+                            .productId(product.getId())
+                            .productName(product.getName())
+                            .productPrice(product.getPrice())
                             .order(order)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("quantity는 0보다 커야 합니다"));
+            assertEquals("OrderItem : quantity는 0보다 커야 합니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : quantity는 0보다 커야 합니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스 : quantity가 음수이면 예외 발생")
@@ -145,35 +156,88 @@ void createOrderItem_withNegativeQuantity_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     OrderItem.builder()
                             .quantity(-1)
-                            .product(product)
+                            .productId(product.getId())
+                            .productName(product.getName())
+                            .productPrice(product.getPrice())
                             .order(order)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("quantity는 0보다 커야 합니다"));
+            assertEquals("OrderItem : quantity는 0보다 커야 합니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : quantity는 0보다 커야 합니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
-        // unitPrice와 totalAmount는 자동으로 계산되므로 직접 설정할 수 없음
-        // 따라서 unitPrice와 totalAmount에 대한 실패 케이스 테스트는 제거됨
+        // productPrice와 totalAmount는 자동으로 계산되므로 직접 설정할 수 없음
+        // 따라서 productPrice와 totalAmount에 대한 실패 케이스 테스트는 제거됨
 
-        @DisplayName("실패 케이스 : product가 null이면 예외 발생")
+        @DisplayName("실패 케이스 : productId가 null이면 예외 발생")
         @Test
-        void createOrderItem_withNullProduct_ThrowsException() {
+        void createOrderItem_withNullProductId_ThrowsException() {
             // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(validQuantity)
+                            .productId(null)
+                            .productName(product.getName())
+                            .productPrice(product.getPrice())
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertEquals("OrderItem : productId가 비어있을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : productId가 비어있을 수 없습니다.', 실제 메시지: %s", exception.getCustomMessage()));
+        }
+
+        @DisplayName("실패 케이스 : productName이 null이면 예외 발생")
+        @Test
+        void createOrderItem_withNullProductName_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
+            Order order = createValidOrder();
+
+            // act & assert
+            CoreException exception = assertThrows(CoreException.class, () ->
+                    OrderItem.builder()
+                            .quantity(validQuantity)
+                            .productId(product.getId())
+                            .productName(null)
+                            .productPrice(product.getPrice())
+                            .order(order)
+                            .build()
+            );
+
+            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
+            assertEquals("OrderItem : productName이 비어있을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : productName이 비어있을 수 없습니다.', 실제 메시지: %s", exception.getCustomMessage()));
+        }
+
+        @DisplayName("실패 케이스 : productPrice가 null이면 예외 발생")
+        @Test
+        void createOrderItem_withNullProductPrice_ThrowsException() {
+            // arrange
+            Product product = createValidProduct();
             Order order = createValidOrder();
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
                     OrderItem.builder()
                             .quantity(validQuantity)
-                            .product(null)
+                            .productId(product.getId())
+                            .productName(product.getName())
+                            .productPrice(null)
                             .order(order)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("product가 비어있을 수 없습니다"));
+            assertEquals("OrderItem : productPrice가 비어있을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : productPrice가 비어있을 수 없습니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
 
         @DisplayName("실패 케이스 : order가 null이면 예외 발생")
@@ -186,13 +250,16 @@ void createOrderItem_withNullOrder_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     OrderItem.builder()
                             .quantity(validQuantity)
-                            .product(product)
+                            .productId(product.getId())
+                            .productName(product.getName())
+                            .productPrice(product.getPrice())
                             .order(null)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("order가 비어있을 수 없습니다"));
+            assertEquals("OrderItem : order가 비어있을 수 없습니다.", exception.getCustomMessage(),
+                    String.format("예상 메시지: 'OrderItem : order가 비어있을 수 없습니다.', 실제 메시지: %s", exception.getCustomMessage()));
         }
     }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
index 004191128..bb155698a 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -11,6 +11,7 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 
@@ -41,8 +42,6 @@ private Product createValidProduct() {
                 .isSellable(true)
                 .build();
 
-        // Product 생성 시 필드 초기화로 Stock이 자동 생성됨 (quantity=0L)
-        // Product.builder()로 생성하면 stock 필드가 자동으로 초기화됨
         Product product = Product.builder()
                 .name("Test Product")
                 .description("Test Description")
@@ -53,6 +52,9 @@ private Product createValidProduct() {
                 .brand(brand)
                 .build();
 
+        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        ReflectionTestUtils.setField(product, "id", 1L);
+
         return product;
     }
 
@@ -72,11 +74,11 @@ void createOrder_withValidFields_Success() {
             Order order = Order.builder()
                     .discountAmount(validDiscountAmount)
                     .shippingFee(validShippingFee)
-                    .user(user)
+                    .userId(user.getId())
                     .build();
             
             // OrderItem 추가
-            order.addOrderItem(product, 2);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 2);
             
             // 생성 시 자동 계산되는 값들
             // product.getPrice() * quantity = 5000 * 2 = 10000
@@ -91,7 +93,7 @@ void createOrder_withValidFields_Success() {
                     () -> assertEquals(validDiscountAmount, order.getDiscountAmount()),
                     () -> assertEquals(validShippingFee, order.getShippingFee()),
                     () -> assertEquals(OrderStatus.PENDING, order.getOrderStatus(), "orderStatus는 기본값 PENDING이어야 함"),
-                    () -> assertEquals(user, order.getUser()),
+                    () -> assertEquals(user.getId(), order.getUserId()),
                     () -> assertEquals(1, order.getOrderItems().size()),
                     // 자동 계산 검증
                     () -> assertEquals(expectedTotalPrice, order.getTotalPrice(), "totalPrice는 orderItems의 totalAmount 합계로 자동 계산되어야 함"),
@@ -216,7 +218,7 @@ void addOrderItem_whenStatusIsPending_Success() {
                     .build();
             
             // 첫 번째 OrderItem 추가
-            order.addOrderItem(product, 2);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 2);
             BigDecimal initialTotalPrice = order.getTotalPrice();
             
             // 두 번째 OrderItem 추가를 위한 예상 값 계산
@@ -227,7 +229,7 @@ void addOrderItem_whenStatusIsPending_Success() {
                     .add(validShippingFee);
 
             // act
-            order.addOrderItem(product, 1);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 1);
 
             // assert
             assertAll(
@@ -257,7 +259,7 @@ void addOrderItem_whenStatusIsNotPending_ThrowsException() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    order.addOrderItem(product, 1)
+                    order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 1)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -281,7 +283,7 @@ void addOrderItem_multipleItems_Success() {
                     .build();
             
             // 첫 번째 OrderItem 추가
-            order.addOrderItem(product, 2);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 2);
 
             BigDecimal initialTotalPrice = order.getTotalPrice();
             BigDecimal item1TotalAmount = product.getPrice().multiply(BigDecimal.valueOf(1));
@@ -294,8 +296,8 @@ void addOrderItem_multipleItems_Success() {
                     .add(validShippingFee);
 
             // act
-            order.addOrderItem(product, 1);
-            order.addOrderItem(product, 2);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 1);
+            order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 2);
 
             // assert
             assertAll(
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
index e20f631c1..59b3ad8d7 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
@@ -12,6 +12,7 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 
@@ -52,6 +53,9 @@ private Product createValidProduct() {
                 .brand(brand)
                 .build();
 
+        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        ReflectionTestUtils.setField(product, "id", 1L);
+
         return product;
     }
 
@@ -62,11 +66,11 @@ private Order createValidOrder() {
         Order order = Order.builder()
                 .discountAmount(BigDecimal.ZERO)
                 .shippingFee(BigDecimal.ZERO)
-                .user(user)
+                .userId(user.getId())
                 .build();
         
         // OrderItem 추가
-        order.addOrderItem(product, 2);
+        order.addOrderItem(product.getId(), product.getName(), product.getPrice(), 2);
 
         return order;
     }
@@ -86,7 +90,8 @@ void createPayment_withValidFields_Success() {
             Payment payment = Payment.builder()
                     .method(validMethod)
                     .paymentStatus(validPaymentStatus)
-                    .order(order)
+                    .orderId(order.getId())
+                    .amount(order.getFinalAmount())
                     .build();
 
             // assert
@@ -95,7 +100,7 @@ void createPayment_withValidFields_Success() {
                     () -> assertEquals(validMethod, payment.getMethod()),
                     () -> assertEquals(expectedAmount, payment.getAmount(), "amount는 order.getFinalAmount()로 자동 설정되어야 함"),
                     () -> assertEquals(validPaymentStatus, payment.getPaymentStatus()),
-                    () -> assertEquals(order, payment.getOrder())
+                    () -> assertEquals(order.getId(), payment.getOrderId())
             );
         }
 
@@ -110,7 +115,8 @@ void createPayment_withNullMethod_ThrowsException() {
                     Payment.builder()
                             .method(null)
                             .paymentStatus(validPaymentStatus)
-                            .order(order)
+                            .orderId(order.getId())
+                            .amount(order.getFinalAmount())
                             .build()
             );
 
@@ -118,26 +124,6 @@ void createPayment_withNullMethod_ThrowsException() {
             assertTrue(exception.getCustomMessage().contains("method가 비어있을 수 없습니다"));
         }
 
-
-        @DisplayName("실패 케이스 : paymentStatus가 null이면 예외 발생")
-        @Test
-        void createPayment_withNullPaymentStatus_ThrowsException() {
-            // arrange
-            Order order = createValidOrder();
-
-            // act & assert
-            CoreException exception = assertThrows(CoreException.class, () ->
-                    Payment.builder()
-                            .method(validMethod)
-                            .paymentStatus(null)
-                            .order(order)
-                            .build()
-            );
-
-            assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("paymentStatus가 비어있을 수 없습니다"));
-        }
-
         @DisplayName("실패 케이스 : order가 null이면 예외 발생")
         @Test
         void createPayment_withNullOrder_ThrowsException() {
@@ -150,14 +136,15 @@ void createPayment_withNullOrder_ThrowsException() {
                     Payment.builder()
                             .method(validMethod)
                             .paymentStatus(validPaymentStatus)
-                            .order(null)
+                            .orderId(null)
+                            .amount(BigDecimal.ZERO)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            // guard() 순서에 따라 amount null 예외 또는 order null 예외가 발생할 수 있음
+            // guard() 순서에 따라 amount null 예외 또는 orderId null 예외가 발생할 수 있음
             assertTrue(exception.getCustomMessage().contains("amount가 비어있을 수 없습니다") || 
-                      exception.getCustomMessage().contains("order가 비어있을 수 없습니다"));
+                      exception.getCustomMessage().contains("orderId가 비어있을 수 없습니다"));
         }
     }
 }
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 4dac0d163..97872dee6 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
@@ -36,14 +36,14 @@ void createPoint_withValidFields_Success() {
 
             // act
             Point point = Point.builder()
-                    .user(user)
+                    .userId(user.getId())
                     .build();
 
             // assert
             assertNotNull(point);
             assertAll(
                     () -> assertEquals(BigDecimal.ZERO, point.getAmount(), "amount는 필드 초기화로 BigDecimal.ZERO가 기본값"),
-                    () -> assertEquals(user, point.getUser())
+                    () -> assertEquals(user.getId(), point.getUserId())
             );
         }
 
@@ -53,12 +53,12 @@ void createPoint_withNullUser_ThrowsException() {
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
                     Point.builder()
-                            .user(null)
+                            .userId(null)
                             .build()
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("user가 Null 이 되면 안 됩니다"));
+            assertTrue(exception.getCustomMessage().contains("userId가 Null 이 되면 안 됩니다"));
         }
 
         // @DisplayName("실패 케이스: amount가 null이면 예외 발생")
@@ -129,7 +129,7 @@ void createPoint_withZeroAmount_Success() {
 
             // act
             Point point = Point.builder()
-                    .user(user)
+                    .userId(user.getId())
                     .build();
 
             // assert
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
index 38d4ffb09..212c462dc 100644
--- 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
@@ -36,7 +36,7 @@ private Product createValidProduct() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(brand)
+                .brandId(brand.getId())
                 .build();
 
         return product;
@@ -55,14 +55,14 @@ void createStock_withValidFields_Success() {
             // act
             Stock stock = Stock.builder()
                     .quantity(validQuantity)
-                    .product(product)
+                    .productId(product.getId())
                     .build();
 
             // assert
             assertNotNull(stock);
             assertAll(
                     () -> assertEquals(validQuantity, stock.getQuantity()),
-                    () -> assertEquals(product, stock.getProduct())
+                    () -> assertEquals(product.getId(), stock.getProductId())
             );
         }
 
@@ -76,7 +76,7 @@ void createStock_withNullQuantity_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     Stock.builder()
                             .quantity(null)
-                            .product(product)
+                            .productId(product.getId())
                             .build()
             );
 
@@ -94,7 +94,7 @@ void createStock_withNegativeQuantity_ThrowsException() {
             CoreException exception = assertThrows(CoreException.class, () ->
                     Stock.builder()
                             .quantity(-1L)
-                            .product(product)
+                            .productId(product.getId())
                             .build()
             );
 
@@ -111,7 +111,7 @@ void createStock_withZeroQuantity_Success() {
             // act
             Stock stock = Stock.builder()
                     .quantity(0L)
-                    .product(product)
+                    .productId(product.getId())
                     .build();
 
             // assert
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 bbc5f8157..4091fea0b 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
@@ -21,13 +21,11 @@ class CreateUserTest {
         final String validEmail = "bobby34@naver.com";
         final String validBirthday = "1994-04-08";
         final Gender validGender = Gender.MALE;
-        final BigDecimal validPoint = BigDecimal.valueOf(0);
 
         @DisplayName("성공 케이스 : 필드가 모두 형식에 맞으면 User 객체 생성 성공")
         @Test
         void createUser_withValidFields_Success() {
             // arrange & act
-            // User 생성 시 Point가 자동으로 생성됨 (필드 초기화)
             User user = User.builder()
                     .loginId(validLoginId)
                     .email(validEmail)
@@ -42,8 +40,6 @@ void createUser_withValidFields_Success() {
                     , () -> assertEquals(user.getEmail(), validEmail)
                     , () -> assertEquals(user.getBirthday(), validBirthday)
                     , () -> assertEquals(user.getGender(), validGender)
-                    , () -> assertNotNull(user.getPoint(), "Point가 자동으로 생성되어야 함")
-                    , () -> assertEquals(user.getPoint().getAmount(), validPoint, "Point의 초기 amount는 0이어야 함")
             );
         }
 

From 8c6487d138dace8f6f62b529e61947694c21fcd2 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 01:36:59 +0900
Subject: [PATCH 09/17] =?UTF-8?q?[Round=205]=20X-USER-ID=20=EB=A5=BC=20Lon?=
 =?UTF-8?q?g=20id=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../java/com/loopers/interfaces/api/like/LikeApiSpec.java | 4 ++--
 .../com/loopers/interfaces/api/like/LikeController.java   | 4 ++--
 .../com/loopers/interfaces/api/order/OrderApiSpec.java    | 6 +++---
 .../com/loopers/interfaces/api/order/OrderController.java | 4 ++--
 .../com/loopers/interfaces/api/point/PointApiSpec.java    | 4 ++--
 .../com/loopers/interfaces/api/point/PointController.java | 4 ++--
 .../java/com/loopers/interfaces/api/user/UserApiSpec.java | 4 ++--
 .../com/loopers/interfaces/api/user/UserController.java   | 2 +-
 .../support/interceptor/AccessControlInterceptor.java     | 8 +++++++-
 9 files changed, 23 insertions(+), 17 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeApiSpec.java
index 6c0e4d238..b4eae6576 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeApiSpec.java
@@ -19,7 +19,7 @@ ApiResponse<Object> saveProductLike(
                     , in = ParameterIn.HEADER
                     , description = "요청자 사용자 ID 헤더"
             )
-            String xUserId,
+            Long xUserId,
             @Parameter(
                     name = "productId"
                     , in = ParameterIn.PATH
@@ -38,7 +38,7 @@ ApiResponse<Object> deleteProductLike(
                     , in = ParameterIn.HEADER
                     , description = "요청자 사용자 ID 헤더"
             )
-            String xUserId,
+            Long xUserId,
             @Parameter(
                     name = "productId"
                     , in = ParameterIn.PATH
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeController.java
index 48f27a708..104e03052 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeController.java
@@ -15,7 +15,7 @@ public class LikeController implements LikeApiSpec {
     @PostMapping("/products/{productId}")
     @Override
     public ApiResponse<Object> saveProductLike(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
             , @PathVariable Long productId
     ) {
         likeFacade.saveProductLike(xUserId, productId);
@@ -25,7 +25,7 @@ public ApiResponse<Object> saveProductLike(
     @DeleteMapping("/products/{productId}")
     @Override
     public ApiResponse<Object> deleteProductLike(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
             , @PathVariable Long productId
     ) {
         likeFacade.deleteProductLike(xUserId, productId);
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderApiSpec.java
index 4069dc2f8..fabbd0b2f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderApiSpec.java
@@ -22,14 +22,14 @@ ApiResponse<OrderDto.OrderResponse> createOrder(
                     in = ParameterIn.HEADER,
                     description = "요청자 사용자 ID 헤더"
             )
-            String xUserId,
+            Long xUserId,
             @Schema(name = "주문 요청", description = "주문 요청 DTO")
             OrderDto.CreateOrderRequest request
     );
 
     @Operation(
             summary = "유저의 주문 목록 조회",
-            description = "로그인 ID로 주문 목록을 조회합니다."
+            description = "회원 ID로 주문 목록을 조회합니다."
     )
     ApiResponse<List<OrderDto.OrderResponse>> getOrders(
             @Parameter(
@@ -37,7 +37,7 @@ ApiResponse<List<OrderDto.OrderResponse>> getOrders(
                     in = ParameterIn.HEADER,
                     description = "요청자 사용자 ID 헤더"
             )
-            String xUserId
+            Long xUserId
     );
 
     @Operation(
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
index b1f928f3d..21b6f3ba5 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderController.java
@@ -18,7 +18,7 @@ public class OrderController implements OrderApiSpec {
     @PostMapping("/")
     @Override
     public ApiResponse<OrderDto.OrderResponse> createOrder(
-            @RequestHeader(value = "X-USER-ID") String xUserId,
+            @RequestHeader(value = "X-USER-ID") Long xUserId,
             @RequestBody OrderDto.CreateOrderRequest request
     ) {
         OrderInfo orderInfo = orderFacade.createOrder(xUserId, request);
@@ -28,7 +28,7 @@ public ApiResponse<OrderDto.OrderResponse> createOrder(
     @GetMapping("/")
     @Override
     public ApiResponse<List<OrderDto.OrderResponse>> getOrders(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
     ) {
         List<OrderInfo> orderInfos = orderFacade.getOrders(xUserId);
         List<OrderDto.OrderResponse> responses = orderInfos.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 82f5adb32..96da23067 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
@@ -22,7 +22,7 @@ ApiResponse<PointDto.PointResponse> getPoint(
                     , in = ParameterIn.HEADER
                     , description = "요청자 사용자 ID 헤더"
             )
-            String xUserId
+            Long xUserId
     );
 
     @Operation(
@@ -35,7 +35,7 @@ ApiResponse<PointDto.PointResponse> charge(
                     , in = ParameterIn.HEADER
                     , description = "요청자 사용자 ID 헤더"
             )
-            String xUserId,
+            Long xUserId,
 
             @Schema(name = "충전할 포인트", description = "충전할 포인트 액수")
             PointDto.PointRequest pointRequest
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 47dab044e..95b0f9bd0 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
@@ -16,7 +16,7 @@ public class PointController implements PointApiSpec {
     @GetMapping("/")
     @Override
     public ApiResponse<PointDto.PointResponse> getPoint(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
     ) {
         PointInfo pointInfo = pointFacade.getPoint(xUserId);
         return ApiResponse.success(PointDto.PointResponse.from(pointInfo));
@@ -25,7 +25,7 @@ public ApiResponse<PointDto.PointResponse> getPoint(
     @PostMapping("/charge")
     @Override
     public ApiResponse<PointDto.PointResponse> charge(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
             , @RequestBody PointDto.PointRequest pointRequest
     ) {
         PointInfo pointInfo = pointFacade.charge(xUserId, pointRequest.amount());
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 82197bf23..48060e8b3 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
@@ -21,7 +21,7 @@ ApiResponse<UserDto.UserResponse> saveUser(
 
     @Operation(
             summary = "회원 조회"
-            , description = "로그인 ID 로 회원 조회합니다."
+            , description = "회원 ID 로 회원 조회합니다."
     )
     ApiResponse<UserDto.UserResponse> getUser(
             @Parameter(
@@ -29,6 +29,6 @@ ApiResponse<UserDto.UserResponse> getUser(
                     , in = ParameterIn.HEADER
                     , description = "요청자 사용자 ID 헤더"
             )
-            String xUserId
+            Long xUserId
     );
 }
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 e66246162..42dda3af2 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> saveUser(
     @GetMapping("/me")
     @Override
     public ApiResponse<UserDto.UserResponse> getUser(
-            @RequestHeader(value = "X-USER-ID") String xUserId
+            @RequestHeader(value = "X-USER-ID") Long xUserId
     ) {
         UserInfo userInfo = userFacade.getUser(xUserId);
         UserDto.UserResponse userResponse = UserDto.UserResponse.from(userInfo);
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/interceptor/AccessControlInterceptor.java b/apps/commerce-api/src/main/java/com/loopers/support/interceptor/AccessControlInterceptor.java
index b962cd579..cf2f9a3c8 100644
--- a/apps/commerce-api/src/main/java/com/loopers/support/interceptor/AccessControlInterceptor.java
+++ b/apps/commerce-api/src/main/java/com/loopers/support/interceptor/AccessControlInterceptor.java
@@ -5,7 +5,6 @@
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.stereotype.Component;
-import org.springframework.util.AntPathMatcher;
 import org.springframework.web.servlet.HandlerInterceptor;
 
 @Component
@@ -28,6 +27,13 @@ public boolean preHandle(
             throw new CoreException(ErrorType.BAD_REQUEST, requestPath + " API 요청에 X-USER-ID 가 꼭 필요합니다.");
         }
 
+        // String을 Long으로 파싱하여 유효성 검사
+        try {
+            Long.parseLong(userIdHeader);
+        } catch (NumberFormatException e) {
+            throw new CoreException(ErrorType.BAD_REQUEST, requestPath + " API 요청에 X-USER-ID 는 숫자(Long) 형식이어야 합니다.");
+        }
+
         return true;
     }
 }

From 02d03892b7f1e06d9a1dbec62ac649ed9db5c3c9 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 01:38:50 +0900
Subject: [PATCH 10/17] =?UTF-8?q?[Round=205]=20=EB=A6=AC=EB=B7=B0=20?=
 =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/doc/week4/03-class-diagram.md         |  4 +--
 .../loopers/domain/coupon/CouponService.java  | 34 +++++++++----------
 .../com/loopers/domain/product/Product.java   | 24 ++-----------
 .../application/like/LikeFacadeTest.java      | 28 ++++++++++++---
 .../domain/coupon/CouponServiceTest.java      |  2 +-
 .../domain/point/PointServiceTest.java        |  7 ++--
 .../domain/stock/StockServiceTest.java        | 13 +++++--
 7 files changed, 61 insertions(+), 51 deletions(-)

diff --git a/apps/commerce-api/src/doc/week4/03-class-diagram.md b/apps/commerce-api/src/doc/week4/03-class-diagram.md
index e29ac6c42..4c043d9c8 100644
--- a/apps/commerce-api/src/doc/week4/03-class-diagram.md
+++ b/apps/commerce-api/src/doc/week4/03-class-diagram.md
@@ -58,7 +58,7 @@ classDiagram
         -ZonedDateTime createdAt
         -ZonedDateTime updatedAt
         -ZonedDateTime deletedAt
-        +setPoint(List~Product~ products)
+        +setProducts(List~Product~ products)
     }
     class Like {
         -LikeId likeId
@@ -173,7 +173,7 @@ classDiagram
     Order "1" --> "1..*" OrderItem
     Order "1" --> "0..1" Payment
     Coupon "0..1" --> "0..1" Order
-    Like "*--" "1" LikeId
+    Like *-- LikeId
     
     UserService ..> User : uses
     PointService ..> Point : uses
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
index d135005cb..21284b12c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java
@@ -61,32 +61,32 @@ public List<Coupon> findAvailableCouponsByUserId(Long userId) {
      */
     @Transactional
     public void useCoupon(Order order, Long couponId) {
-        // 1. 쿠폰 정보 조회 (할인 계산용)
-        Coupon coupon = this.findById(couponId);
-
-        // 2. 쿠폰 소유자 확인
-        if (!coupon.getUser().getId().equals(order.getUser().getId())) {
-            throw new CoreException(
-                    ErrorType.BAD_REQUEST,
-                    "[couponId = " + couponId + "] 본인의 쿠폰만 사용할 수 있습니다."
-            );
-        }
 
-        // 3. 원자적 UPDATE로 쿠폰 사용 처리 (동시성 안전)
+        // 1. 원자적 UPDATE로 쿠폰 사용 처리 (동시성 안전)
         long updatedRows = couponRepository.useCoupon(
                 couponId,
                 order.getId(),
-                order.getUser().getId()
+                order.getUserId()
         );
 
+        // 2. 실패했을 때만 상세 원인 파악 (Lazy Validation)
         if (updatedRows == 0) {
-            throw new CoreException(
-                    ErrorType.BAD_REQUEST,
-                    "[couponId = " + couponId + "] 쿠폰을 사용할 수 없습니다. (이미 사용됨 or 삭제됨 or 주문 상태 오류)"
-            );
+            Coupon coupon = this.findById(couponId); // 이때 조회!
+            
+            // 소유자가 다른지?
+            if (!coupon.getUserId().equals(order.getUserId())) {
+                 throw new CoreException(ErrorType.BAD_REQUEST, "본인 쿠폰 아닙니다.");
+            }
+            // 이미 사용했는지?
+            if (coupon.getIsUsed()) {
+                 throw new CoreException(ErrorType.BAD_REQUEST, "이미 사용된 쿠폰입니다.");
+            }
+            
+            throw new CoreException(ErrorType.BAD_REQUEST, "사용 불가능한 쿠폰입니다.");
         }
 
-        // 4. 할인 계산 및 적용
+        // 3. 할인 계산 및 적용
+        Coupon coupon = this.findById(couponId);
         BigDecimal couponDiscount = coupon.calculateDiscount(order.getTotalPrice());
         order.applyDiscount(couponDiscount);
     }
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
index 64ba6b83d..0245b042e 100644
--- 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
@@ -1,8 +1,6 @@
 package com.loopers.domain.product;
 
 import com.loopers.domain.BaseEntity;
-import com.loopers.domain.brand.Brand;
-import com.loopers.domain.stock.Stock;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import jakarta.persistence.*;
@@ -27,13 +25,7 @@ public class Product extends BaseEntity {
     private ProductStatus status;
     private Boolean isVisible;
     private Boolean isSellable;
-
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "brand_id")
-    private Brand brand;
-
-    @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
-    private Stock stock = Stock.builder().product(this).quantity(0L).build();
+    private Long brandId;
 
     @Builder
     private Product(
@@ -43,7 +35,7 @@ private Product(
         , ProductStatus status
         , Boolean isVisible
         , Boolean isSellable
-        , Brand brand
+        , Long brandId
     ) {
         this.name = name;
         this.description = description;
@@ -51,15 +43,10 @@ private Product(
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
-        this.brand = brand;
+        this.brandId = brandId;
         guard();
     }
 
-    // 브랜드 필드를 세팅
-    public void setBrand(Brand brand) {
-        this.brand = brand;
-    }
-
     // 유효성 검사
     @Override
     protected void guard() {
@@ -99,11 +86,6 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Product : isSellable 가 비어있을 수 없습니다.");
         }
 
-        // stock 유효성 검사
-        if(stock == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Product : stock 가 비어있을 수 없습니다.");
-        }
-
         // brand nullable
     }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
index 5187ab345..1d277f199 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
@@ -25,6 +25,7 @@
 
 import java.math.BigDecimal;
 import java.util.Optional;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -168,25 +169,42 @@ void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException
             // act
             int threads = 10;
             ExecutorService executorService = Executors.newFixedThreadPool(threads);
-            CountDownLatch latch = new CountDownLatch(threads);
+            CountDownLatch readyLatch = new CountDownLatch(threads);
+            CountDownLatch startLatch = new CountDownLatch(1);
+            CountDownLatch doneLatch = new CountDownLatch(threads);
+            ConcurrentLinkedQueue<Throwable> exceptions = new ConcurrentLinkedQueue<>();
 
-            // 스레드 실행행
+            // 스레드 실행
             for(int i = 1; i <= threads; i++) {
                 executorService.execute(() -> {
                     try {
+                        // 준비 완료 신호
+                        readyLatch.countDown();
+                        // 모든 스레드가 준비될 때까지 대기
+                        startLatch.await();
+                        // 실제 작업 수행
                         likeFacade.saveProductLike(validLoginId, productId);
+                    } catch (Throwable e) {
+                        exceptions.offer(e);
                     } finally {
-                        latch.countDown();
+                        doneLatch.countDown();
                     }
                 });
             }
             
-            // 모든 스레드 마칠 때까지 대기
-            latch.await();
+            // 모든 스레드가 준비될 때까지 대기
+            readyLatch.await();
+            // 동시에 시작하도록 신호
+            startLatch.countDown();
+            // 모든 스레드가 완료될 때까지 대기
+            doneLatch.await();
             executorService.shutdown();
 
             // assert
             
+            // 워커 스레드에서 발생한 예외가 없는지 확인
+            assertThat(exceptions).isEmpty();
+            
             // Like 한 개만 저장되어 있어야 함
             long count = likeJpaRepository.findAll().stream()
                     .filter(like -> like.getUser().getId().equals(userId)
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
index 2dd49d9b6..dc05bc0e8 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
@@ -272,7 +272,7 @@ void useCoupon_concurrentRequests_onlyOneSuccess() throws InterruptedException {
             CountDownLatch latch = new CountDownLatch(threadCount);
             AtomicInteger successCount = new AtomicInteger(0);
             AtomicInteger failureCount = new AtomicInteger(0);
-            List<Exception> exceptions = new ArrayList<>();
+            List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
 
             // act
             for (int i = 0; i < threadCount; i++) {
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
index 3ee67e305..4505eeed4 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
@@ -17,6 +17,7 @@
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -190,7 +191,7 @@ void deduct_concurrentRequests_success() throws InterruptedException {
             CountDownLatch latch = new CountDownLatch(threadCount);
             AtomicInteger successCount = new AtomicInteger(0);
             AtomicInteger failureCount = new AtomicInteger(0);
-            List<Exception> exceptions = new ArrayList<>();
+            List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
 
             // act
             for (int i = 0; i < threadCount; i++) {
@@ -237,8 +238,8 @@ void deduct_insufficientPoint_partialSuccess() throws InterruptedException {
             CountDownLatch latch = new CountDownLatch(threadCount);
             AtomicInteger successCount = new AtomicInteger(0);
             AtomicInteger failureCount = new AtomicInteger(0);
-            List<Exception> exceptions = new ArrayList<>();
-
+            List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
+            
             // act
             for (int i = 0; i < threadCount; i++) {
                 executor.submit(() -> {
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
index c350fe625..be1a20801 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
@@ -19,6 +19,7 @@
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -76,6 +77,14 @@ void setUp() {
                 .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
         productId = savedProduct.getId();
 
+        // Product 저장 후 Stock 별도 생성
+        Stock stock = Stock.builder()
+                .product(savedProduct)
+                .quantity(0L)
+                .build();
+        stockService.saveStock(stock)
+                .orElseThrow(() -> new RuntimeException("Stock 저장 실패"));
+
         // Stock quantity 세팅
         stockService.increaseQuantity(productId, initialStockQuantity);
     }
@@ -101,7 +110,7 @@ void decreaseQuantity_concurrentRequests_success() throws InterruptedException {
             CountDownLatch latch = new CountDownLatch(threadCount);
             AtomicInteger successCount = new AtomicInteger(0);
             AtomicInteger failureCount = new AtomicInteger(0);
-            List<Exception> exceptions = new ArrayList<>();
+            List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
 
             // act
             for (int i = 0; i < threadCount; i++) {
@@ -148,7 +157,7 @@ void decreaseQuantity_insufficientStock_partialSuccess() throws InterruptedExcep
             CountDownLatch latch = new CountDownLatch(threadCount);
             AtomicInteger successCount = new AtomicInteger(0);
             AtomicInteger failureCount = new AtomicInteger(0);
-            List<Exception> exceptions = new ArrayList<>();
+            List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
 
             // act
             for (int i = 0; i < threadCount; i++) {

From 847e0de6d1510ab9098d0698082d243b10db861d Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 01:39:35 +0900
Subject: [PATCH 11/17] =?UTF-8?q?[Round=205]=20Material=20View=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/CommerceApiApplication.java   |   3 +
 .../application/product/ProductFacade.java    |  53 ++++----
 .../application/product/ProductInfo.java      |  52 --------
 .../product/ProductViewEventHandler.java      | 117 ++++++++++++++++++
 .../product/ProductViewSyncScheduler.java     | 102 +++++++++++++++
 .../loopers/domain/like/LikeRepository.java   |   7 ++
 .../com/loopers/domain/like/LikeService.java  |   7 ++
 .../domain/product/ProductCondition.java      |   4 +-
 .../domain/product/ProductRepository.java     |   4 +-
 .../domain/product/ProductService.java        |  61 ++-------
 .../loopers/domain/product/ProductView.java   |  49 ++++++++
 .../domain/product/ProductViewRepository.java |  12 ++
 .../product/event/ProductCreatedEvent.java    |   5 +
 .../product/event/ProductDeletedEvent.java    |   5 +
 .../product/event/ProductLikeCountEvent.java  |   5 +
 .../like/LikeQueryRepository.java             |  18 ++-
 .../like/LikeRepositoryImpl.java              |   5 +
 .../product/ProductQueryRepository.java       | 106 +++++++---------
 .../product/ProductRepositoryImpl.java        |  11 +-
 .../product/ProductViewJpaRepository.java     |   8 ++
 .../product/ProductViewRepositoryImpl.java    |  49 ++++++++
 .../api/product/ProductApiSpec.java           |   4 +-
 .../api/product/ProductController.java        |  15 ++-
 .../interfaces/api/product/ProductDto.java    |  43 +++----
 24 files changed, 512 insertions(+), 233 deletions(-)
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
index 9027b51bf..73d040408 100644
--- a/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
+++ b/apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
@@ -4,10 +4,13 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.scheduling.annotation.EnableAsync;
 import java.util.TimeZone;
 
 @ConfigurationPropertiesScan
 @SpringBootApplication
+@EnableAsync
+@org.springframework.scheduling.annotation.EnableScheduling
 public class CommerceApiApplication {
 
     @PostConstruct
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
index 0af649e42..184acb2e6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
@@ -3,13 +3,14 @@
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductCondition;
 import com.loopers.domain.product.ProductService;
+import com.loopers.domain.product.ProductView;
+import com.loopers.domain.stock.Stock;
+import com.loopers.domain.stock.StockService;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 
 import lombok.RequiredArgsConstructor;
 
-import java.util.Optional;
-
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
@@ -20,51 +21,51 @@
 public class ProductFacade {
 
     private final ProductService productService;
+    private final StockService stockService;
 
     /**
-     * 상품 목록 조회
+     * 상품 목록 조회 (ProductView 기반)
      */
     @Transactional(readOnly = true)
-    public Page<ProductInfo> getProducts(ProductCondition condition, Pageable pageable) {
-        Page<Product> products = productService.findProducts(condition, pageable);
-        return products.map(ProductInfo::from);
+    public Page<ProductView> getProductViews(ProductCondition condition, Pageable pageable) {
+        return productService.findProductViews(condition, pageable);
     }
 
     /**
-     * 상품 상세 조회 (Product + Brand 조합)
-     * 도메인 서비스에서 Product + Brand 조합 로직을 처리하도록 위임
+     * 상품 상세 조회 (ProductView 기반)
      */
     @Transactional(readOnly = true)
-    public ProductInfo getProduct(Long productId) {
-        Product product = productService.findById(productId)
+    public ProductView getProductView(Long productId) {
+        return productService.findProductViewById(productId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND,
-                        "[productId = " + productId + "] Product를 찾을 수 없습니다."
+                        "상품을 찾지 못했습니다."
                 ));
-        return ProductInfo.from(product);
     }
 
     /**
      * 상품 저장
      */
     @Transactional
-    public ProductInfo saveProduct(ProductInfo productInfo) {
-        
-        Product product = Product.builder()
-                .name(productInfo.name())
-                .description(productInfo.description())
-                .price(productInfo.price())
-                .status(productInfo.status())
-                .isVisible(productInfo.isVisible())
-                .isSellable(productInfo.isSellable())
-                .build();
-
+    public Product saveProduct(Product product) {
         Product savedProduct = productService.saveProduct(product)
-                .orElseThrow(() -> new com.loopers.support.error.CoreException(
-                        com.loopers.support.error.ErrorType.INTERNAL_ERROR,
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
                         "Product 저장에 실패했습니다."
                 ));
-        return ProductInfo.from(savedProduct);
+
+        // Product 저장 후 Stock 별도 생성
+        Stock stock = Stock.builder()
+                .productId(savedProduct.getId())
+                .quantity(0L)
+                .build();
+        stockService.saveStock(stock)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
+                        "Stock 저장에 실패했습니다."
+                ));
+
+        return savedProduct;
     }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
deleted file mode 100644
index 2ccd03781..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.loopers.application.product;
-
-import com.loopers.application.brand.BrandInfo;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductService;
-import com.loopers.domain.product.ProductStatus;
-import lombok.Builder;
-
-import java.math.BigDecimal;
-
-@Builder
-public record ProductInfo(
-        Long id
-        , String name
-        , String description
-        , BigDecimal price
-        , Long likeCount
-        , ProductStatus status
-        , Boolean isVisible
-        , Boolean isSellable
-        , BrandInfo brand
-) {
-    public static ProductInfo from(Product product) {
-        return ProductInfo.builder()
-                .id(product.getId())
-                .name(product.getName())
-                .description(product.getDescription())
-                .price(product.getPrice())
-                .likeCount(product.getLikeCount())
-                .status(product.getStatus())
-                .isVisible(product.getIsVisible())
-                .isSellable(product.getIsSellable())
-                .brand(product.getBrand() != null ? BrandInfo.from(product.getBrand()) : null)
-                .build();
-    }
-
-    public static ProductInfo from(ProductService.ProductWithBrand productWithBrand) {
-        Product product = productWithBrand.getProduct();
-        return ProductInfo.builder()
-                .id(product.getId())
-                .name(product.getName())
-                .description(product.getDescription())
-                .price(product.getPrice())
-                .likeCount(product.getLikeCount())
-                .status(product.getStatus())
-                .isVisible(product.getIsVisible())
-                .isSellable(product.getIsSellable())
-                .brand(BrandInfo.from(productWithBrand.getBrand()))
-                .build();
-    }
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
new file mode 100644
index 000000000..2aba2cd97
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
@@ -0,0 +1,117 @@
+package com.loopers.application.product;
+
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.like.LikeRepository;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.ProductViewRepository;
+import com.loopers.domain.product.event.ProductCreatedEvent;
+import com.loopers.domain.product.event.ProductDeletedEvent;
+import com.loopers.domain.product.event.ProductLikeCountEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class ProductViewEventHandler {
+
+    private final ProductViewRepository productViewRepository;
+    private final LikeRepository likeRepository;
+    private final ProductRepository productRepository;
+    private final BrandRepository brandRepository;
+
+    /**
+     * 좋아요 수 변경 이벤트 처리
+     * Like 테이블에서 COUNT(*)를 수행하여 정합성을 보장
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional
+    public void handleProductLikeCountEvent(ProductLikeCountEvent event) {
+        Long productId = event.productId();
+        
+        try {
+            // 실제 Like 테이블에서 Count 조회 (정합성 확보)
+            long realCount = likeRepository.countByProductId(productId);
+
+            // ProductView 업데이트
+            productViewRepository.updateLikeCount(productId, realCount);
+            
+            log.debug("ProductView likeCount updated: productId={}, likeCount={}", productId, realCount);
+        } catch (Exception e) {
+            log.error("Failed to update ProductView likeCount: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 생성 이벤트 처리
+     * Product + Brand 조회 후 ProductView 생성
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional
+    public void handleProductCreatedEvent(ProductCreatedEvent event) {
+        Long productId = event.productId();
+        Long brandId = event.brandId();
+        
+        try {
+            // Product 조회
+            Product product = productRepository.findById(productId)
+                    .orElseThrow(() -> new IllegalStateException("Product not found: " + productId));
+
+            // Brand 조회
+            String brandName = null;
+            if (brandId != null) {
+                brandName = brandRepository.findById(brandId)
+                        .map(brand -> brand.getName())
+                        .orElse(null);
+            }
+
+            // LikeCount 조회
+            long likeCount = likeRepository.countByProductId(productId);
+
+            // ProductView 생성
+            ProductView productView = ProductView.builder()
+                    .id(product.getId())
+                    .name(product.getName())
+                    .price(product.getPrice())
+                    .likeCount(likeCount)
+                    .brandId(brandId)
+                    .brandName(brandName)
+                    .status(product.getStatus())
+                    .build();
+
+            productViewRepository.save(productView);
+            
+            log.debug("ProductView created: productId={}, brandId={}", productId, brandId);
+        } catch (Exception e) {
+            log.error("Failed to create ProductView: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 삭제 이벤트 처리 (Soft Delete)
+     * ProductView에서 해당 상품 제거
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional
+    public void handleProductDeletedEvent(ProductDeletedEvent event) {
+        Long productId = event.productId();
+
+        try {
+            productViewRepository.deleteById(productId);
+            log.debug("ProductView deleted: productId={}", productId);
+        } catch (Exception e) {
+            log.error("Failed to delete ProductView: productId={}", productId, e);
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java
new file mode 100644
index 000000000..98fe084f2
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java
@@ -0,0 +1,102 @@
+package com.loopers.application.product;
+
+import com.loopers.domain.brand.QBrand;
+import com.loopers.domain.like.LikeRepository;
+import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.QProduct;
+import com.loopers.domain.product.QProductView;
+import com.loopers.infrastructure.product.ProductViewJpaRepository;
+import com.querydsl.core.Tuple;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * ProductView 테이블 동기화 스케줄러
+ * 
+ * 하이브리드 방식:
+ * - 평소: 이벤트 기반 실시간 동기화 (ProductCreatedEvent, ProductDeletedEvent, ProductLikeCountEvent)
+ * - 주기적: 전체 재생성으로 정합성 보장 및 누락된 동기화 보정
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class ProductViewSyncScheduler {
+
+    private final JPAQueryFactory queryFactory;
+    private final ProductViewJpaRepository productViewJpaRepository;
+    private final LikeRepository likeRepository;
+
+    private final QProduct product = QProduct.product;
+    private final QBrand brand = QBrand.brand;
+    private final QProductView productView = QProductView.productView;
+
+    /**
+     * 매일 새벽 3시에 ProductView 전체 재생성
+     * 데이터 정합성 보장 + 누락된 동기화 보정
+     */
+    @Scheduled(cron = "0 0 3 * * *") // 매일 03:00
+    @Transactional
+    public void fullRefreshProductView() {
+        log.info("ProductView 전체 재생성 시작");
+
+        try {
+            // 1. 기존 데이터 전체 삭제 (QueryDSL)
+            long deletedCount = queryFactory
+                    .delete(productView)
+                    .execute();
+
+            log.info("ProductView 전체 삭제 완료: {} rows", deletedCount);
+
+            // 2. Product + Brand JOIN하여 전체 재생성
+            List<Tuple> results = queryFactory
+                    .select(
+                            product.id,
+                            product.name,
+                            product.price,
+                            product.brandId,
+                            brand.name,  // brandName
+                            product.status
+                    )
+                    .from(product)
+                    .leftJoin(brand).on(product.brandId.eq(brand.id)
+                            .and(brand.deletedAt.isNull()))
+                    .where(product.deletedAt.isNull())  // Soft Delete 필터링
+                    .fetch();
+
+            // 3. ProductView로 변환 (LikeCount는 개별 조회)
+            List<ProductView> views = results.stream()
+                    .map(tuple -> {
+                        Long productId = tuple.get(product.id);
+                        
+                        // LikeCount 조회
+                        long likeCount = likeRepository.countByProductId(productId);
+                        
+                        return ProductView.builder()
+                                .id(productId)
+                                .name(tuple.get(product.name))
+                                .price(tuple.get(product.price))
+                                .likeCount(likeCount)
+                                .brandId(tuple.get(product.brandId))
+                                .brandName(tuple.get(brand.name))
+                                .status(tuple.get(product.status))
+                                .build();
+                    })
+                    .toList();
+
+            // 4. 배치 저장
+            productViewJpaRepository.saveAll(views);
+
+            log.info("ProductView 전체 재생성 완료: {} rows", views.size());
+        } catch (Exception e) {
+            log.error("ProductView 전체 재생성 실패", e);
+            throw e;  // 트랜잭션 롤백을 위해 예외 재발생
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
index 1ecb246ec..dee01c515 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
@@ -16,5 +16,12 @@ public interface LikeRepository {
      * INSERT ... ON DUPLICATE KEY UPDATE를 사용하여 원자적으로 처리
      */
     long insertOrIgnore(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
+    
+    /**
+     * 상품에 대한 좋아요 수 조회
+     * @param productId 상품 ID
+     * @return 좋아요 수
+     */
+    long countByProductId(Long productId);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
index 91104a733..f8d05d7d1 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
@@ -2,8 +2,10 @@
 
 import com.loopers.domain.like.entity.LikeTargetType;
 import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.product.event.ProductLikeCountEvent;
 import com.loopers.domain.user.User;
 import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -13,6 +15,7 @@ public class LikeService {
 
     private final LikeRepository likeRepository;
     private final ProductRepository productRepository;
+    private final ApplicationEventPublisher eventPublisher;
 
     /**
      * 상품 좋아요 등록
@@ -28,6 +31,8 @@ public void saveProductLike(User user, Long productId) {
         // 실제로 새로 생성된 경우에만 likeCount 증가
         if (affectedRows == 1) {
             productRepository.incrementLikeCount(productId);
+            // 이벤트 발행: ProductSummary 동기화를 위해
+            eventPublisher.publishEvent(new ProductLikeCountEvent(productId));
         }
     }
 
@@ -45,6 +50,8 @@ public void deleteProductLike(User user, Long productId) {
         // 실제로 삭제된 경우에만 likeCount 감소
         if (deletedRows > 0) {
             productRepository.decrementLikeCount(productId);
+            // 이벤트 발행: ProductSummary 동기화를 위해
+            eventPublisher.publishEvent(new ProductLikeCountEvent(productId));
         }
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java
index ded85af92..c5b2d019d 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java
@@ -1,14 +1,12 @@
 package com.loopers.domain.product;
 
-import java.math.BigDecimal;
 import java.time.ZonedDateTime;
 
 import lombok.Builder;
 
 @Builder
 public record ProductCondition(
-    BigDecimal price
-    , Long likeCount
+    Long brandId
     , ZonedDateTime createdAt
     , String sort
 ) {}
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
index 2a8d02edc..c82887928 100644
--- 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
@@ -9,8 +9,8 @@ public interface ProductRepository {
 
     Optional<Product> save(Product product);
     Optional<Product> findById(Long productId);
-    Optional<Product> findByIdWithBrand(Long productId);
-    Page<Product> findProducts(ProductCondition condition, Pageable pageable);
+    Page<ProductView> findProductViews(ProductCondition condition, Pageable pageable);
+    Optional<ProductView> findProductViewById(Long productId);
     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
index 3e6a5968e..4fefecc3d 100644
--- 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
@@ -1,9 +1,8 @@
 package com.loopers.domain.product;
 
-import com.loopers.domain.brand.Brand;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
+import com.loopers.domain.product.event.ProductCreatedEvent;
 import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Component;
@@ -16,10 +15,16 @@
 public class ProductService {
 
     private final ProductRepository productRepository;
+    private final ApplicationEventPublisher eventPublisher;
 
     @Transactional
     public Optional<Product> saveProduct(Product product) {
-        return productRepository.save(product);
+        Optional<Product> saved = productRepository.save(product);
+        saved.ifPresent(p -> {
+            // 이벤트 발행: ProductView 생성을 위해
+            eventPublisher.publishEvent(new ProductCreatedEvent(p.getId(), p.getBrandId()));
+        });
+        return saved;
     }
 
     @Transactional(readOnly = true)
@@ -28,52 +33,12 @@ public Optional<Product> findById(Long productId) {
     }
 
     @Transactional(readOnly = true)
-    public Page<Product> findProducts(ProductCondition condition, Pageable pageable) {
-        return productRepository.findProducts(condition, pageable);
+    public Page<ProductView> findProductViews(ProductCondition condition, Pageable pageable) {
+        return productRepository.findProductViews(condition, pageable);
     }
 
-    /**
-     * 상품과 브랜드 정보를 조합하여 조회
-     * 도메인 서비스에서 Product + Brand 조합 로직 처리
-     * Repository에서 fetch join을 통해 Brand를 함께 조회
-     */
     @Transactional(readOnly = true)
-    public ProductWithBrand getProductDetailWithBrand(Long productId) {
-        Product product = productRepository.findByIdWithBrand(productId)
-                .orElseThrow(() -> new CoreException(
-                        ErrorType.NOT_FOUND,
-                        "[productId = " + productId + "] Product를 찾을 수 없습니다."
-                ));
-
-        Brand brand = product.getBrand();
-        if (brand == null) {
-            throw new CoreException(
-                    ErrorType.NOT_FOUND,
-                    "[productId = " + productId + "] Product에 Brand 정보가 없습니다."
-            );
-        }
-
-        return new ProductWithBrand(product, brand);
-    }
-
-    /**
-     * Product와 Brand를 조합한 도메인 객체
-     */
-    public static class ProductWithBrand {
-        private final Product product;
-        private final Brand brand;
-
-        public ProductWithBrand(Product product, Brand brand) {
-            this.product = product;
-            this.brand = brand;
-        }
-
-        public Product getProduct() {
-            return product;
-        }
-
-        public Brand getBrand() {
-            return brand;
-        }
+    public Optional<ProductView> findProductViewById(Long productId) {
+        return productRepository.findProductViewById(productId);
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
new file mode 100644
index 000000000..05d4a2b1b
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
@@ -0,0 +1,49 @@
+package com.loopers.domain.product;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.ZonedDateTime;
+
+@Entity
+@Table(name = "product_summary", indexes = {
+    @Index(name = "idx_ps_like_desc", columnList = "likeCount DESC"),
+    @Index(name = "idx_ps_brand_like", columnList = "brandId, likeCount DESC")
+})
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+public class ProductView {
+
+    @Id
+    private Long id; // Original Product ID
+
+    private String name;
+    private BigDecimal price;
+    private Long likeCount;
+
+    private Long brandId;
+    private String brandName;
+
+    @Enumerated(EnumType.STRING)
+    private ProductStatus status;
+
+    @Column(name = "created_at")
+    private ZonedDateTime createdAt;
+
+    // 업데이트 편의 메서드
+    public void updateLikeCount(Long newCount) {
+        this.likeCount = newCount;
+    }
+
+    public void updateBrandName(String brandName) {
+        this.brandName = brandName;
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
new file mode 100644
index 000000000..2f24aff37
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
@@ -0,0 +1,12 @@
+package com.loopers.domain.product;
+
+import java.util.Optional;
+
+public interface ProductViewRepository {
+
+    Optional<ProductView> save(ProductView productView);
+    Optional<ProductView> findById(Long id);
+    void updateLikeCount(Long id, Long count);
+    void deleteById(Long id);
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
new file mode 100644
index 000000000..9739e2c2a
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
@@ -0,0 +1,5 @@
+package com.loopers.domain.product.event;
+
+public record ProductCreatedEvent(Long productId, Long brandId) {
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
new file mode 100644
index 000000000..e2b21b0b0
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
@@ -0,0 +1,5 @@
+package com.loopers.domain.product.event;
+
+public record ProductDeletedEvent(Long productId) {
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
new file mode 100644
index 000000000..ba46eba5e
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
@@ -0,0 +1,5 @@
+package com.loopers.domain.product.event;
+
+public record ProductLikeCountEvent(Long productId) {
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
index dc7aadcac..c5ca45c17 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeQueryRepository.java
@@ -22,7 +22,7 @@ public class LikeQueryRepository {
      * @param userId 사용자 ID
      * @param likeTargetId 좋아요 대상 ID
      * @param likeTargetType 좋아요 대상 타입
-     * @return 영향받은 행 수 (1: INSERT 또는 UPDATE 성공, 2: UPDATE 성공)
+     * @return 영향받은 행 수 (1: INSERT 성공, 0: INSERT 실패)
      */
     public long insertOrIgnore(Long userId, Long likeTargetId, String likeTargetType) {
 
@@ -56,5 +56,21 @@ public long deleteByUserIdAndLikeTargetId(Long userId, Long likeTargetId, String
                         .and(like.likeId.likeTargetType.eq(targetType)))
                 .execute();
     }
+
+    /**
+     * 상품에 대한 좋아요 수 조회
+     * 
+     * @param productId 상품 ID
+     * @return 좋아요 수
+     */
+    public long countByProductId(Long productId) {
+        Long count = queryFactory
+                .select(like.count())
+                .from(like)
+                .where(like.likeId.likeTargetId.eq(productId)
+                        .and(like.likeId.likeTargetType.eq(LikeTargetType.PRODUCT)))
+                .fetchOne();
+        return count != null ? count : 0L;
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
index ab9fca2ed..4198b1127 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
@@ -36,5 +36,10 @@ public Optional<Like> findByUserIdAndLikeTargetId(Long userId, Long likeTargetId
     public long insertOrIgnore(Long userId, Long likeTargetId, LikeTargetType likeTargetType) {
         return likeQueryRepository.insertOrIgnore(userId, likeTargetId, likeTargetType.name());
     }
+
+    @Override
+    public long countByProductId(Long productId) {
+        return likeQueryRepository.countByProductId(productId);
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
index 2c967c4b9..fa43da3ab 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
@@ -1,8 +1,9 @@
 package com.loopers.infrastructure.product;
 
-import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.ProductView;
 import com.loopers.domain.product.QProduct;
+import com.loopers.domain.product.QProductView;
 import com.querydsl.core.BooleanBuilder;
 import com.querydsl.core.types.OrderSpecifier;
 import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -14,7 +15,6 @@
 
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 
 @RequiredArgsConstructor
 @Component
@@ -22,52 +22,49 @@ public class ProductQueryRepository {
 
     private final JPAQueryFactory queryFactory;
     private final QProduct product = QProduct.product;
+    private final QProductView productView = QProductView.productView;
 
-    public Page<Product> findProducts(ProductCondition condition, Pageable pageable) {
-    
-    // 1. where 조건 생성 (간결하게)
-    BooleanBuilder whereCondition = new BooleanBuilder();
-    
-    // if (condition.price() != null) {
-    //     whereCondition.and(product.price.eq(condition.price()));
-    // }
-    // if (condition.likeCount() != null) {
-    //     whereCondition.and(product.likeCount.goe(condition.likeCount()));
-    // }
-    // if (condition.createdAt() != null) {
-    //     whereCondition.and(product.createdAt.goe(condition.createdAt()));
-    // }
-    
-    // 2. 정렬 조건
-    final Map<String, OrderSpecifier<?>> ORDER_BY_MAP = Map.of(
-        "latest", product.createdAt.desc(),
-        "price_asc", product.price.asc(),
-        "likes_desc", product.likeCount.desc()
-    );
-    
-    OrderSpecifier<?> orderSpecifier = ORDER_BY_MAP.getOrDefault(
-        condition.sort(),
-        ORDER_BY_MAP.get("latest")
-    );
-    
-    // 3. 데이터 조회
-    List<Product> products = queryFactory
-            .selectFrom(product)
-            .where(whereCondition)
-            .orderBy(orderSpecifier)
-            .offset(pageable.getOffset())
-            .limit(pageable.getPageSize())
-            .fetch();
-    
-    // 4. 전체 개수 조회
-    Long total = queryFactory
-            .select(product.count())
-            .from(product)
-            .where(whereCondition)
-            .fetchOne();
-    
-    return new PageImpl<>(products, pageable, total != null ? total : 0L);
-}
+    /**
+     * ProductView 기반 조회
+     * Materialized View를 사용하여 빠른 조회 성능 제공
+     */
+    public Page<ProductView> findProductViews(ProductCondition condition, Pageable pageable) {
+        // 1. where 조건 생성
+        BooleanBuilder whereCondition = new BooleanBuilder();
+        
+        if (condition.brandId() != null) {
+            whereCondition.and(productView.brandId.eq(condition.brandId()));
+        }
+        
+        // 2. 정렬 조건
+        final Map<String, OrderSpecifier<?>> ORDER_BY_MAP = Map.of(
+            "price_asc", productView.price.asc(),
+            "likes_desc", productView.likeCount.desc()
+        );
+        
+        OrderSpecifier<?> orderSpecifier = ORDER_BY_MAP.getOrDefault(
+            condition.sort(),
+            productView.likeCount.desc() // 기본값: 좋아요 순
+        );
+        
+        // 3. 데이터 조회 (ProductView 사용)
+        List<ProductView> products = queryFactory
+                .selectFrom(productView)
+                .where(whereCondition)
+                .orderBy(orderSpecifier)
+                .offset(pageable.getOffset())
+                .limit(pageable.getPageSize())
+                .fetch();
+        
+        // 4. 전체 개수 조회
+        Long total = queryFactory
+                .select(productView.count())
+                .from(productView)
+                .where(whereCondition)
+                .fetchOne();
+        
+        return new PageImpl<>(products, pageable, total != null ? total : 0L);
+    }
 
     /**
      * 상품 좋아요 수 증가 (동시성 안전)
@@ -98,21 +95,6 @@ public void decrementLikeCount(Long productId) {
                 .execute();
     }
 
-    /**
-     * 상품 ID로 상품과 브랜드 정보를 함께 조회 (fetch join)
-     * 도메인 서비스에서 Product + Brand 조합 로직을 위해 사용
-     * 
-     * @param productId 상품 ID
-     * @return Product (Brand 포함)
-     */
-    public Optional<Product> findByIdWithBrand(Long productId) {
-        Product result = queryFactory
-                .selectFrom(product)
-                .leftJoin(product.brand).fetchJoin()
-                .where(product.id.eq(productId))
-                .fetchOne();
-        return Optional.ofNullable(result);
-    }
 
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
index 56e74c041..8888232b4 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
@@ -3,6 +3,8 @@
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductCondition;
 import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.ProductViewRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -16,6 +18,7 @@ public class ProductRepositoryImpl implements ProductRepository {
 
     private final ProductJpaRepository productJpaRepository;
     private final ProductQueryRepository productQueryRepository;
+    private final ProductViewRepository productViewRepository;
 
     @Override
     public Optional<Product> save(Product product) {
@@ -29,13 +32,13 @@ public Optional<Product> findById(Long productId) {
     }
 
     @Override
-    public Optional<Product> findByIdWithBrand(Long productId) {
-        return productQueryRepository.findByIdWithBrand(productId);
+    public Page<ProductView> findProductViews(ProductCondition condition, Pageable pageable) {
+        return productQueryRepository.findProductViews(condition, pageable);
     }
 
     @Override
-    public Page<Product> findProducts(ProductCondition condition, Pageable pageable) {
-        return productQueryRepository.findProducts(condition, pageable);
+    public Optional<ProductView> findProductViewById(Long productId) {
+        return productViewRepository.findById(productId);
     }
 
     @Override
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
new file mode 100644
index 000000000..496930238
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
@@ -0,0 +1,8 @@
+package com.loopers.infrastructure.product;
+
+import com.loopers.domain.product.ProductView;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ProductViewJpaRepository extends JpaRepository<ProductView, Long> {
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
new file mode 100644
index 000000000..9542d73c7
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
@@ -0,0 +1,49 @@
+package com.loopers.infrastructure.product;
+
+import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.ProductViewRepository;
+import com.loopers.domain.product.QProductView;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Component
+public class ProductViewRepositoryImpl implements ProductViewRepository {
+
+    private final ProductViewJpaRepository productViewJpaRepository;
+    private final JPAQueryFactory queryFactory;
+
+    @Override
+    public Optional<ProductView> save(ProductView productView) {
+        ProductView saved = productViewJpaRepository.save(productView);
+        return Optional.of(saved);
+    }
+
+    @Override
+    public Optional<ProductView> findById(Long id) {
+        return productViewJpaRepository.findById(id);
+    }
+
+    @Override
+    public void updateLikeCount(Long id, Long count) {
+        QProductView productView = QProductView.productView;
+        queryFactory
+                .update(productView)
+                .set(productView.likeCount, count)
+                .where(productView.id.eq(id))
+                .execute();
+    }
+
+    @Override
+    public void deleteById(Long id) {
+        QProductView productView = QProductView.productView;
+        queryFactory
+                .delete(productView)
+                .where(productView.id.eq(id))
+                .execute();
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductApiSpec.java
index 4a10383ca..2e9cac1bc 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductApiSpec.java
@@ -15,13 +15,13 @@ public interface ProductApiSpec {
             summary = "상품 목록 조회"
             , description = "상품 목록을 페이징하여 조회합니다. 검색 조건과 정렬 옵션을 지원합니다."
     )
-    ApiResponse<Page<ProductDto.ProductResponse>> getProducts(
+    ApiResponse<ProductDto.PageResponse<ProductDto.ProductResponse>> getProducts(
             @Schema(name = "검색 조건", description = "상품 검색 조건 및 페이징 정보")
             ProductDto.SearchRequest request
     );
 
     @Operation(
-            summary = "상품 상세 조회"
+            summary = "상품 정보 조회"
             , description = "상품 ID로 상품 상세 정보를 조회합니다."
     )
     ApiResponse<ProductDto.ProductResponse> getProduct(
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
index f1a8531d2..1f3552e50 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
@@ -8,6 +8,7 @@
 import org.springframework.data.domain.Pageable;
 import org.springframework.web.bind.annotation.*;
 import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.ProductView;
 
 @RequiredArgsConstructor
 @RestController
@@ -18,7 +19,7 @@ public class ProductController implements ProductApiSpec {
 
     @GetMapping
     @Override
-    public ApiResponse<Page<ProductDto.ProductResponse>> getProducts(
+    public ApiResponse<ProductDto.PageResponse<ProductDto.ProductResponse>> getProducts(
             @ModelAttribute ProductDto.SearchRequest request
     ) {
         // DTO → Domain 변환
@@ -31,10 +32,11 @@ public ApiResponse<Page<ProductDto.ProductResponse>> getProducts(
         );
         
         // Facade 호출 및 변환
-        var productInfoPage = productFacade.getProducts(condition, pageable);
+        var productViewPage = productFacade.getProductViews(condition, pageable);
 
         // PageResponse로 변환
-        return ApiResponse.success(productInfoPage.map(ProductDto.ProductResponse::from));
+        Page<ProductDto.ProductResponse> responsePage = productViewPage.map(ProductDto.ProductResponse::from);
+        return ApiResponse.success(ProductDto.PageResponse.from(responsePage));
     }
 
     @GetMapping("/{productId}")
@@ -42,11 +44,8 @@ public ApiResponse<Page<ProductDto.ProductResponse>> getProducts(
     public ApiResponse<ProductDto.ProductResponse> getProduct(
             @PathVariable Long productId
     ) {
-        // Facade 호출 (단일 객체)
-        var productInfo = productFacade.getProduct(productId);
-        
-        // Info → DTO 변환 후 반환
-        return ApiResponse.success(ProductDto.ProductResponse.from(productInfo));
+        ProductView productView = productFacade.getProductView(productId);
+        return ApiResponse.success(ProductDto.ProductResponse.from(productView));
     }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
index c16c8e40f..e3e451f17 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
@@ -1,9 +1,8 @@
 package com.loopers.interfaces.api.product;
 
-import com.loopers.application.product.ProductInfo;
 import com.loopers.domain.product.ProductCondition;
 import com.loopers.domain.product.ProductStatus;
-import com.loopers.interfaces.api.brand.BrandDto;
+import com.loopers.domain.product.ProductView;
 import lombok.Builder;
 import org.springframework.data.domain.Page;
 
@@ -16,8 +15,7 @@ public class ProductDto {
     @Builder
     public record SearchRequest(
             // 검색 조건
-            BigDecimal price,
-            Long likeCount,
+            Long brandId,
             ZonedDateTime createdAt,
             
             // 페이징
@@ -26,12 +24,11 @@ public record SearchRequest(
             String sort
     ) {
         public ProductCondition toCondition() {
-            return new ProductCondition(
-                    price,
-                    likeCount,
-                    createdAt,
-                    sort != null ? sort : "latest"
-            );
+            return ProductCondition.builder()
+                    .brandId(brandId)
+                    .createdAt(createdAt)
+                    .sort(sort != null ? sort : "likes_desc")
+                    .build();
         }
     }
 
@@ -39,25 +36,21 @@ public ProductCondition toCondition() {
     public record ProductResponse(
             Long id,
             String name,
-            String description,
             BigDecimal price,
             Long likeCount,
-            ProductStatus status,
-            Boolean isVisible,
-            Boolean isSellable,
-            BrandDto.BrandResponse brand
+            Long brandId,
+            String brandName,
+            ProductStatus status
     ) {
-        public static ProductResponse from(ProductInfo productInfo) {
+        public static ProductResponse from(ProductView productView) {
             return ProductResponse.builder()
-                    .id(productInfo.id())
-                    .name(productInfo.name())
-                    .description(productInfo.description())
-                    .price(productInfo.price())
-                    .likeCount(productInfo.likeCount())
-                    .status(productInfo.status())
-                    .isVisible(productInfo.isVisible())
-                    .isSellable(productInfo.isSellable())
-                    .brand(productInfo.brand() != null ? BrandDto.BrandResponse.from(productInfo.brand()) : null)
+                    .id(productView.getId())
+                    .name(productView.getName())
+                    .price(productView.getPrice())
+                    .likeCount(productView.getLikeCount())
+                    .brandId(productView.getBrandId())
+                    .brandName(productView.getBrandName())
+                    .status(productView.getStatus())
                     .build();
         }
     }

From cfd93fb9bd84ca75163aba9d0e54d66b814c0953 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 02:01:07 +0900
Subject: [PATCH 12/17] =?UTF-8?q?[Round=205]=20LikeFacadeTest=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/like/LikeFacadeTest.java      | 84 ++++++++-----------
 1 file changed, 35 insertions(+), 49 deletions(-)

diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
index 1d277f199..e9c9e85a2 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
@@ -1,7 +1,6 @@
 package com.loopers.application.like;
 
 import com.loopers.application.product.ProductFacade;
-import com.loopers.application.product.ProductInfo;
 import com.loopers.application.user.UserFacade;
 import com.loopers.application.user.UserInfo;
 import com.loopers.domain.like.entity.Like;
@@ -10,7 +9,6 @@
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.product.ProductStatus;
 import com.loopers.domain.user.Gender;
-import com.loopers.domain.user.User;
 import com.loopers.domain.user.UserService;
 import com.loopers.infrastructure.like.LikeJpaRepository;
 import com.loopers.support.error.CoreException;
@@ -76,43 +74,39 @@ class saveProductLikeTest {
         @Test
         void saveProductLike_withValidUserAndProduct_Success() {
             // arrange
-            createAndSaveUser(validLoginId);
-            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
-            Long userId = user.getId();
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
-            Long productId = productInfo.id();
+            Long userId = createAndSaveUser(validLoginId);
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
 
             // act
-            likeFacade.saveProductLike(validLoginId, productId);
+            likeFacade.saveProductLike(userId, productId);
 
             // assert
             Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertTrue(savedLike.isPresent());
-            assertEquals(userId, savedLike.get().getUser().getId());
+            assertEquals(userId, savedLike.get().getLikeId().getUserId());
             assertEquals(productId, savedLike.get().getLikeId().getLikeTargetId());
             assertEquals(LikeTargetType.PRODUCT, savedLike.get().getLikeId().getLikeTargetType());
             
             // Product.likeCount가 1 증가했는지 확인
-            Product product = productRepository.findById(productId).orElseThrow();
-            assertEquals(1L, product.getLikeCount(), "좋아요 등록 시 likeCount가 1 증가해야 합니다.");
+            Product savedProduct = productRepository.findById(productId).orElseThrow();
+            assertEquals(1L, savedProduct.getLikeCount(), "좋아요 등록 시 likeCount가 1 증가해야 합니다.");
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 사용자로 좋아요 등록 시 NOT_FOUND 예외 발생")
         @Test
         void saveProductLike_withNonExistentUser_NotFound() {
             // arrange
-            String nonExistentLoginId = "nonexistent";
             Long productId = 1L;
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () -> {
-                likeFacade.saveProductLike(nonExistentLoginId, productId);
+                likeFacade.saveProductLike(999L, productId);
             });
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] User를 찾을 수 없습니다."));
+            assertTrue(exception.getCustomMessage().contains("[userId = 999] User를 찾을 수 없습니다."));
         }
     }
 
@@ -124,18 +118,15 @@ class SaveProductLikeConcurrencyTest {
         @Test
         void saveProductLike_duplicateLike_Idempotent() {
             // arrange
-            createAndSaveUser(validLoginId);
-            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
-            Long userId = user.getId();
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
-            Long productId = productInfo.id();
+            Long userId = createAndSaveUser(validLoginId);
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
 
             // act
-            likeFacade.saveProductLike(validLoginId, productId);
+            likeFacade.saveProductLike(userId, productId);
             Product productAfterFirst = productRepository.findById(productId).orElseThrow();
             Long likeCountAfterFirst = productAfterFirst.getLikeCount();
             
-            likeFacade.saveProductLike(validLoginId, productId); // 중복 등록
+            likeFacade.saveProductLike(userId, productId); // 중복 등록
 
             // assert
             Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
@@ -144,7 +135,7 @@ void saveProductLike_duplicateLike_Idempotent() {
             assertTrue(savedLike.isPresent());
             // 멱등성: 중복 등록해도 하나만 존재
             long count = likeJpaRepository.findAll().stream()
-                    .filter(like -> like.getUser().getId().equals(userId)
+                    .filter(like -> like.getLikeId().getUserId().equals(userId)
                             && like.getLikeId().getLikeTargetId().equals(productId)
                             && like.getLikeId().getLikeTargetType() == LikeTargetType.PRODUCT)
                     .count();
@@ -160,11 +151,8 @@ void saveProductLike_duplicateLike_Idempotent() {
         @Test
         void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException {
             // arrange
-            createAndSaveUser(validLoginId);
-            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
-            Long userId = user.getId();
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
-            Long productId = productInfo.id();
+            Long userId = createAndSaveUser(validLoginId);
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
 
             // act
             int threads = 10;
@@ -183,7 +171,7 @@ void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException
                         // 모든 스레드가 준비될 때까지 대기
                         startLatch.await();
                         // 실제 작업 수행
-                        likeFacade.saveProductLike(validLoginId, productId);
+                        likeFacade.saveProductLike(userId, productId);
                     } catch (Throwable e) {
                         exceptions.offer(e);
                     } finally {
@@ -207,7 +195,7 @@ void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException
             
             // Like 한 개만 저장되어 있어야 함
             long count = likeJpaRepository.findAll().stream()
-                    .filter(like -> like.getUser().getId().equals(userId)
+                    .filter(like -> like.getLikeId().getUserId().equals(userId)
                             && like.getLikeId().getLikeTargetId().equals(productId)
                             && like.getLikeId().getLikeTargetType() == LikeTargetType.PRODUCT)
                     .count();
@@ -229,18 +217,15 @@ class DeleteProductLikeTest {
         @Test
         void deleteProductLike_withExistingLike_Success() {
             // arrange
-            createAndSaveUser(validLoginId);
-            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
-            Long userId = user.getId();
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
-            Long productId = productInfo.id();
-            likeFacade.saveProductLike(validLoginId, productId);
+            Long userId = createAndSaveUser(validLoginId);
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
+            likeFacade.saveProductLike(userId, productId);
             
             Product productBeforeDelete = productRepository.findById(productId).orElseThrow();
             Long likeCountBeforeDelete = productBeforeDelete.getLikeCount();
 
             // act
-            likeFacade.deleteProductLike(validLoginId, productId);
+            likeFacade.deleteProductLike(userId, productId);
 
             // assert
             Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
@@ -258,18 +243,15 @@ void deleteProductLike_withExistingLike_Success() {
         @Test
         void deleteProductLike_withNonExistentLike_Idempotent() {
             // arrange
-            createAndSaveUser(validLoginId);
-            User user = userService.findUserByLoginId(validLoginId).orElseThrow();
-            Long userId = user.getId();
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 5L);
-            Long productId = productInfo.id();
+            Long userId = createAndSaveUser(validLoginId);
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 5L);
             
             Product productBeforeDelete = productRepository.findById(productId).orElseThrow();
             Long likeCountBeforeDelete = productBeforeDelete.getLikeCount();
 
             // act & assert - 예외 없이 처리되어야 함
             assertDoesNotThrow(() -> {
-                likeFacade.deleteProductLike(validLoginId, productId);
+                likeFacade.deleteProductLike(userId, productId);
             });
 
             // assert
@@ -288,31 +270,34 @@ void deleteProductLike_withNonExistentLike_Idempotent() {
         @Test
         void deleteProductLike_withNonExistentUser_NotFound() {
             // arrange
-            String nonExistentLoginId = "nonexistent";
+            Long nonExistentUserId = 999L;
             Long productId = 1L;
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () -> {
-                likeFacade.deleteProductLike(nonExistentLoginId, productId);
+                likeFacade.deleteProductLike(nonExistentUserId, productId);
             });
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] User를 찾을 수 없습니다."));
+            assertTrue(exception.getCustomMessage().contains("[userId = 999] User를 찾을 수 없습니다."));
         }
     }
 
     // 테스트 헬퍼 메서드
-    private UserInfo createAndSaveUser(String loginId) {
+    private Long createAndSaveUser(String loginId) {
         UserInfo userInfo = UserInfo.builder()
                 .loginId(loginId)
                 .email(validEmail)
                 .birthday(validBirthday)
                 .gender(validGender)
                 .build();
-        return userFacade.saveUser(userInfo);
+        userFacade.saveUser(userInfo);
+        return userService.findUserByLoginId(loginId)
+                .orElseThrow(() -> new RuntimeException("사용자 저장 후 조회 실패"))
+                .getId();
     }
 
-    private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long likeCount) {
+    private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount) {
         Product product = Product.builder()
                 .name(name)
                 .description("테스트 설명")
@@ -331,7 +316,8 @@ private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long lik
             throw new RuntimeException("likeCount 설정 실패", e);
         }
 
-        return productFacade.saveProduct(ProductInfo.from(product));
+        Product savedProduct = productFacade.saveProduct(product);
+        return savedProduct.getId();
     }
 }
 

From e99d35897ee709962cbbdd4f5e1e11b3259d8131 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 02:09:45 +0900
Subject: [PATCH 13/17] =?UTF-8?q?[Round=205]=20=EB=AA=A8=EB=93=A0=20Test?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/order/OrderFacadeTest.java    |  65 +++++------
 .../application/point/PointFacadeTest.java    |  34 ++++--
 .../product/ProductFacadeTest.java            | 101 +++++++++---------
 .../application/user/UserFacadeTest.java      |   9 +-
 .../domain/coupon/CouponServiceTest.java      |  18 ++--
 .../com/loopers/domain/order/OrderTest.java   |  18 ++--
 .../loopers/domain/payment/PaymentTest.java   |   2 +-
 .../domain/point/PointServiceTest.java        |  41 ++++---
 .../domain/stock/StockServiceTest.java        |   4 +-
 9 files changed, 160 insertions(+), 132 deletions(-)

diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
index c79a5c3fc..0b6f98ed6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeTest.java
@@ -4,8 +4,10 @@
 import com.loopers.application.user.UserInfo;
 import com.loopers.domain.coupon.Coupon;
 import com.loopers.domain.coupon.CouponRepository;
+import com.loopers.domain.coupon.CouponService;
 import com.loopers.domain.coupon.CouponType;
 import com.loopers.domain.order.Order;
+import com.loopers.domain.order.OrderService;
 import com.loopers.domain.order.OrderStatus;
 import com.loopers.domain.point.Point;
 import com.loopers.domain.point.PointService;
@@ -13,6 +15,7 @@
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.stock.Stock;
 import com.loopers.domain.stock.StockRepository;
+import com.loopers.domain.stock.StockService;
 import com.loopers.domain.user.Gender;
 import com.loopers.domain.user.User;
 import com.loopers.domain.user.UserRepository;
@@ -67,7 +70,7 @@ class OrderFacadeTest {
     private StockRepository stockRepository;
 
     @Autowired
-    private com.loopers.domain.stock.StockService stockService;
+    private StockService stockService;
 
     @Autowired
     private PointService pointService;
@@ -76,10 +79,10 @@ class OrderFacadeTest {
     private CouponRepository couponRepository;
 
     @Autowired
-    private com.loopers.domain.order.OrderService orderService;
+    private OrderService orderService;
 
     @Autowired
-    private com.loopers.domain.coupon.CouponService couponService;
+    private CouponService couponService;
 
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
@@ -125,7 +128,7 @@ void setUp() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(savedBrand)
+                .brandId(savedBrand.getId())
                 .build();
         Product savedProduct = productRepository.save(product)
                 .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
@@ -134,7 +137,7 @@ void setUp() {
 
         // Product 저장 후 Stock 별도 생성
         Stock stock = Stock.builder()
-                .product(savedProduct)
+                .productId(savedProduct.getId())
                 .quantity(0L)
                 .build();
         stockService.saveStock(stock)
@@ -144,7 +147,7 @@ void setUp() {
         stockService.increaseQuantity(testProductId, 100L);
 
         // 테스트용 포인트 충전 (100000원)
-        pointService.charge(testLoginId, BigDecimal.valueOf(100000));
+        pointService.charge(testUser.getId(), BigDecimal.valueOf(100000));
     }
 
     @AfterEach
@@ -175,7 +178,7 @@ void createOrder_withValidRequest_Success() {
             BigDecimal expectedFinalAmount = expectedTotalPrice; // 할인 없음
 
             // act
-            OrderInfo orderInfo = orderFacade.createOrder(testLoginId, request);
+            OrderInfo orderInfo = orderFacade.createOrder(testUser.getId(), request);
 
             // assert
             assertNotNull(orderInfo);
@@ -194,7 +197,7 @@ void createOrder_withValidRequest_Success() {
             assertEquals(98L, stock.getQuantity(), "재고가 2개 차감되어야 함 (100 - 2 = 98)");
 
             // 포인트가 차감되었는지 확인
-            Point point = pointService.findByUserLoginId(testLoginId)
+            Point point = pointService.findByUserId(testUser.getId())
                     .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
             BigDecimal expectedPoint = BigDecimal.valueOf(100000).subtract(expectedFinalAmount);
             assertEquals(0, expectedPoint.compareTo(point.getAmount()), "포인트가 차감되어야 함");
@@ -208,7 +211,7 @@ void createOrder_withCoupon_Success() {
             Coupon coupon = Coupon.builder()
                     .couponType(CouponType.FIXED_AMOUNT)
                     .discountValue(BigDecimal.valueOf(5000))
-                    .user(testUser)
+                    .userId(testUser.getId())
                     .build();
             Coupon savedCoupon = couponRepository.save(coupon)
                     .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -229,7 +232,7 @@ void createOrder_withCoupon_Success() {
             BigDecimal expectedFinalAmount = expectedTotalPrice.subtract(expectedDiscount); // 15000
 
             // act
-            OrderInfo orderInfo = orderFacade.createOrder(testLoginId, request);
+            OrderInfo orderInfo = orderFacade.createOrder(testUser.getId(), request);
 
             // assert
             assertNotNull(orderInfo);
@@ -243,7 +246,7 @@ void createOrder_withCoupon_Success() {
             Coupon usedCoupon = couponRepository.findById(savedCoupon.getId())
                     .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다"));
             assertTrue(usedCoupon.getIsUsed(), "쿠폰은 사용된 상태여야 함");
-            assertNotNull(usedCoupon.getOrder(), "쿠폰의 order는 null이 아니어야 함");
+            assertNotNull(usedCoupon.getOrderId(), "쿠폰의 orderId는 null이 아니어야 함");
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 User로 주문 생성 시 NOT_FOUND 예외 발생")
@@ -263,11 +266,11 @@ void createOrder_withNonExistentUser_NotFound() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder("nonexistent", request)
+                    orderFacade.createOrder(99999L, request)
             );
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[loginId = nonexistent] User를 찾을 수 없습니다"));
+            assertTrue(exception.getCustomMessage().contains("[userId = 99999] User를 찾을 수 없습니다"));
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 Product로 주문 생성 시 NOT_FOUND 예외 발생")
@@ -287,7 +290,7 @@ void createOrder_withNonExistentProduct_NotFound() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
@@ -323,7 +326,7 @@ void createOrder_withInsufficientStock_BadRequest() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -335,13 +338,13 @@ void createOrder_withInsufficientStock_BadRequest() {
         void createOrder_withInsufficientPoint_BadRequest() {
             // arrange
             // 포인트를 1000원으로 조정 (기존 포인트를 모두 차감 후 1000원 충전)
-            Point currentPoint = pointService.findByUserLoginId(testLoginId)
+            Point currentPoint = pointService.findByUserId(testUser.getId())
                     .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
             BigDecimal currentAmount = currentPoint.getAmount();
             if (currentAmount.compareTo(BigDecimal.valueOf(1000)) > 0) {
-                pointService.deduct(testLoginId, currentAmount.subtract(BigDecimal.valueOf(1000)));
+                pointService.deduct(testUser.getId(), currentAmount.subtract(BigDecimal.valueOf(1000)));
             } else if (currentAmount.compareTo(BigDecimal.valueOf(1000)) < 0) {
-                pointService.charge(testLoginId, BigDecimal.valueOf(1000).subtract(currentAmount));
+                pointService.charge(testUser.getId(), BigDecimal.valueOf(1000).subtract(currentAmount));
             }
 
             List<OrderDto.OrderItemRequest> items = List.of(
@@ -357,7 +360,7 @@ void createOrder_withInsufficientPoint_BadRequest() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -372,7 +375,7 @@ void createOrder_withUsedCoupon_BadRequest() {
             Coupon coupon = Coupon.builder()
                     .couponType(CouponType.FIXED_AMOUNT)
                     .discountValue(BigDecimal.valueOf(5000))
-                    .user(testUser)
+                    .userId(testUser.getId())
                     .build();
             Coupon savedCoupon = couponRepository.save(coupon)
                     .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -381,7 +384,7 @@ void createOrder_withUsedCoupon_BadRequest() {
             Order firstOrder = Order.builder()
                     .discountAmount(BigDecimal.ZERO)
                     .shippingFee(BigDecimal.ZERO)
-                    .user(testUser)
+                    .userId(testUser.getId())
                     .build();
             firstOrder.addOrderItem(testProduct.getId(), testProduct.getName(), testProduct.getPrice(), 1);
             
@@ -406,7 +409,7 @@ void createOrder_withUsedCoupon_BadRequest() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -433,7 +436,7 @@ void createOrder_withOtherUserCoupon_BadRequest() {
             Coupon otherUserCoupon = Coupon.builder()
                     .couponType(CouponType.FIXED_AMOUNT)
                     .discountValue(BigDecimal.valueOf(5000))
-                    .user(otherUser)
+                    .userId(otherUser.getId())
                     .build();
             Coupon savedOtherUserCoupon = couponRepository.save(otherUserCoupon)
                     .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -452,7 +455,7 @@ void createOrder_withOtherUserCoupon_BadRequest() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -468,7 +471,7 @@ void createOrder_withDeletedCoupon_BadRequest() {
             Coupon coupon = Coupon.builder()
                     .couponType(CouponType.FIXED_AMOUNT)
                     .discountValue(BigDecimal.valueOf(5000))
-                    .user(testUser)
+                    .userId(testUser.getId())
                     .build();
             Coupon savedCoupon = couponRepository.save(coupon)
                     .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -491,7 +494,7 @@ void createOrder_withDeletedCoupon_BadRequest() {
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    orderFacade.createOrder(testLoginId, request)
+                    orderFacade.createOrder(testUser.getId(), request)
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
@@ -537,7 +540,7 @@ void createOrder_concurrentRequests_consistencyMaintained() throws InterruptedEx
                 Coupon coupon = Coupon.builder()
                         .couponType(CouponType.FIXED_AMOUNT)
                         .discountValue(BigDecimal.valueOf(5000))
-                        .user(testUser)
+                        .userId(testUser.getId())
                         .build();
                 Long couponId = couponRepository.save(coupon)
                         .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"))
@@ -567,7 +570,7 @@ void createOrder_concurrentRequests_consistencyMaintained() throws InterruptedEx
                                     .items(items)
                                     .couponIds(List.of(couponId))
                                     .build();
-                        OrderInfo orderInfo = orderFacade.createOrder(testLoginId, actualRequest);
+                        OrderInfo orderInfo = orderFacade.createOrder(testUser.getId(), actualRequest);
                         successOrders.add(orderInfo);
                     } catch (Throwable t) {
                         failures.add(t);
@@ -609,7 +612,7 @@ void createOrder_concurrentRequests_consistencyMaintained() throws InterruptedEx
                     .multiply(BigDecimal.valueOf(nonCouponSuccessCount));
             BigDecimal expectedPoint = BigDecimal.valueOf(100000)
                     .subtract(totalCouponOrders.add(totalNonCouponOrders));
-            Point point = pointService.findByUserLoginId(testLoginId)
+            Point point = pointService.findByUserId(testUser.getId())
                     .orElseThrow(() -> new RuntimeException("Point를 찾을 수 없습니다"));
             assertEquals(0, expectedPoint.compareTo(point.getAmount()), "사용된 포인트가 정확해야 함");
 
@@ -634,7 +637,7 @@ private long verifyCoupons(List<Long> couponIds, int initialStock, BigDecimal co
                                 .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다")))
                         .filter(Coupon::getIsUsed)
                         .peek(coupon -> {
-                            assertNotNull(coupon.getOrder(), "쿠폰은 주문과 연결되어야 함");
+                            assertNotNull(coupon.getOrderId(), "쿠폰은 주문과 연결되어야 함");
                         })
                         .count();
                 assertTrue(usedCouponCount <= initialStock, "사용된 쿠폰 수는 성공 주문 수를 초과할 수 없음");
@@ -645,7 +648,7 @@ private long verifyCoupons(List<Long> couponIds, int initialStock, BigDecimal co
                             Coupon coupon = couponRepository.findById(id)
                                     .orElseThrow(() -> new RuntimeException("Coupon을 찾을 수 없습니다"));
                             if (coupon.getIsUsed()) {
-                                Order order = coupon.getOrder();
+                                Order order = orderService.findOrderById(coupon.getOrderId());
                                 assertEquals(0, couponDiscount.compareTo(order.getDiscountAmount()), "쿠폰 사용 주문 할인 금액 검증");
                             }
                         });
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
index ef02a7c0f..1a463d6e3 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/point/PointFacadeTest.java
@@ -3,6 +3,7 @@
 import com.loopers.application.user.UserFacade;
 import com.loopers.application.user.UserInfo;
 import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.UserService;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
@@ -27,6 +28,9 @@ public class PointFacadeTest {
     @Autowired
     private UserFacade userFacade;
 
+    @Autowired
+    private UserService userService;
+
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
@@ -55,9 +59,13 @@ void getPoint_withValidUser_Success() {
                     .gender(validGender)
                     .build();
             userFacade.saveUser(userInfo);
+            
+            Long userId = userService.findUserByLoginId(validLoginId)
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"))
+                    .getId();
 
             // act
-            PointInfo result = pointFacade.getPoint(validLoginId);
+            PointInfo result = pointFacade.getPoint(userId);
 
             // assert
             assertNotNull(result);
@@ -68,15 +76,15 @@ void getPoint_withValidUser_Success() {
         @Test
         void getPoint_withNonExistentUser_NotFound() {
             // arrange
-            String nonExistentLoginId = "nonexistent";
+            Long nonExistentUserId = 99999L;
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    pointFacade.getPoint(nonExistentLoginId)
+                    pointFacade.getPoint(nonExistentUserId)
             );
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("[loginId = " + nonExistentLoginId + "] Point를 찾을 수 없습니다."));
+            assertTrue(exception.getCustomMessage().contains("[userId = " + nonExistentUserId + "] Point를 찾을 수 없습니다."));
         }
     }
 
@@ -95,12 +103,16 @@ void charge_withValidUser_Success() {
                     .gender(validGender)
                     .build();
             userFacade.saveUser(userInfo);
+            
+            Long userId = userService.findUserByLoginId(validLoginId)
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"))
+                    .getId();
 
             BigDecimal chargeAmount = BigDecimal.valueOf(1000);
             BigDecimal expectedAmount = BigDecimal.valueOf(1000);
 
             // act
-            PointInfo result = pointFacade.charge(validLoginId, chargeAmount);
+            PointInfo result = pointFacade.charge(userId, chargeAmount);
 
             // assert
             assertNotNull(result);
@@ -118,14 +130,18 @@ void charge_multipleCharges_accumulates() {
                     .gender(validGender)
                     .build();
             userFacade.saveUser(userInfo);
+            
+            Long userId = userService.findUserByLoginId(validLoginId)
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"))
+                    .getId();
 
             BigDecimal firstCharge = BigDecimal.valueOf(1000);
             BigDecimal secondCharge = BigDecimal.valueOf(2000);
             BigDecimal expectedAmount = BigDecimal.valueOf(3000);
 
             // act
-            pointFacade.charge(validLoginId, firstCharge);
-            PointInfo result = pointFacade.charge(validLoginId, secondCharge);
+            pointFacade.charge(userId, firstCharge);
+            PointInfo result = pointFacade.charge(userId, secondCharge);
 
             // assert
             assertNotNull(result);
@@ -136,12 +152,12 @@ void charge_multipleCharges_accumulates() {
         @Test
         void charge_withNonExistentUser_NotFound() {
             // arrange
-            String nonExistentLoginId = "nonexistent";
+            Long nonExistentUserId = 99999L;
             BigDecimal chargeAmount = BigDecimal.valueOf(1000);
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () ->
-                    pointFacade.charge(nonExistentLoginId, chargeAmount)
+                    pointFacade.charge(nonExistentUserId, chargeAmount)
             );
 
             assertEquals(ErrorType.NOT_FOUND, exception.getErrorType());
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
index 2cecbf1c5..4671da805 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
@@ -3,6 +3,7 @@
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductCondition;
 import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.ProductView;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
@@ -40,19 +41,19 @@ void tearDown() {
         databaseCleanUp.truncateAllTables();
     }
 
-    @DisplayName("getProducts 메서드")
+    @DisplayName("getProductViews 메서드")
     @Nested
-    class GetProductsTest {
+    class GetProductViewsTest {
 
         @DisplayName("성공 케이스: sort가 'latest'이면 생성일시 내림차순으로 정렬된 상품 목록을 반환한다")
         @Test
-        void getProducts_withSortLatest_returnsProductsSortedByCreatedAtDesc() throws InterruptedException {
+        void getProductViews_withSortLatest_returnsProductsSortedByCreatedAtDesc() throws InterruptedException {
             // arrange
-            ProductInfo product1 = createAndSaveProduct("상품1", BigDecimal.valueOf(10000L), 10L);
+            createAndSaveProduct("상품1", BigDecimal.valueOf(10000L), 10L);
             Thread.sleep(10); // 생성 시간 차이를 위해
-            ProductInfo product2 = createAndSaveProduct("상품2", BigDecimal.valueOf(20000L), 20L);
+            createAndSaveProduct("상품2", BigDecimal.valueOf(20000L), 20L);
             Thread.sleep(10);
-            ProductInfo product3 = createAndSaveProduct("상품3", BigDecimal.valueOf(30000L), 30L);
+            createAndSaveProduct("상품3", BigDecimal.valueOf(30000L), 30L);
 
             Pageable pageable = PageRequest.of(0, 10);
             ProductCondition condition = ProductCondition.builder()
@@ -60,25 +61,25 @@ void getProducts_withSortLatest_returnsProductsSortedByCreatedAtDesc() throws In
                     .build();
 
             // act
-            Page<ProductInfo> result = productFacade.getProducts(condition, pageable);
+            Page<ProductView> result = productFacade.getProductViews(condition, pageable);
 
             // assert
             assertAll(
                     () -> assertThat(result.getContent()).hasSize(3),
-                    () -> assertThat(result.getContent().get(0).name()).isEqualTo("상품3"), // 최신순
-                    () -> assertThat(result.getContent().get(1).name()).isEqualTo("상품2"),
-                    () -> assertThat(result.getContent().get(2).name()).isEqualTo("상품1"),
+                    () -> assertThat(result.getContent().get(0).getName()).isEqualTo("상품3"), // 최신순
+                    () -> assertThat(result.getContent().get(1).getName()).isEqualTo("상품2"),
+                    () -> assertThat(result.getContent().get(2).getName()).isEqualTo("상품1"),
                     () -> assertThat(result.getTotalElements()).isEqualTo(3L)
             );
         }
 
         @DisplayName("성공 케이스: sort가 'price_asc'이면 가격 오름차순으로 정렬된 상품 목록을 반환한다")
         @Test
-        void getProducts_withSortPriceAsc_returnsProductsSortedByPriceAsc() {
+        void getProductViews_withSortPriceAsc_returnsProductsSortedByPriceAsc() {
             // arrange
-            ProductInfo product1 = createAndSaveProduct("상품1", BigDecimal.valueOf(30000L), 10L);
-            ProductInfo product2 = createAndSaveProduct("상품2", BigDecimal.valueOf(10000L), 20L);
-            ProductInfo product3 = createAndSaveProduct("상품3", BigDecimal.valueOf(20000L), 30L);
+            createAndSaveProduct("상품1", BigDecimal.valueOf(30000L), 10L);
+            createAndSaveProduct("상품2", BigDecimal.valueOf(10000L), 20L);
+            createAndSaveProduct("상품3", BigDecimal.valueOf(20000L), 30L);
 
             Pageable pageable = PageRequest.of(0, 10);
             ProductCondition condition = ProductCondition.builder()
@@ -86,24 +87,24 @@ void getProducts_withSortPriceAsc_returnsProductsSortedByPriceAsc() {
                     .build();
 
             // act
-            Page<ProductInfo> result = productFacade.getProducts(condition, pageable);
+            Page<ProductView> result = productFacade.getProductViews(condition, pageable);
 
             // assert
             assertAll(
                     () -> assertThat(result.getContent()).hasSize(3),
-                    () -> assertThat(result.getContent().get(0).price()).isEqualByComparingTo(BigDecimal.valueOf(10000L)),
-                    () -> assertThat(result.getContent().get(1).price()).isEqualByComparingTo(BigDecimal.valueOf(20000L)),
-                    () -> assertThat(result.getContent().get(2).price()).isEqualByComparingTo(BigDecimal.valueOf(30000L))
+                    () -> assertThat(result.getContent().get(0).getPrice()).isEqualByComparingTo(BigDecimal.valueOf(10000L)),
+                    () -> assertThat(result.getContent().get(1).getPrice()).isEqualByComparingTo(BigDecimal.valueOf(20000L)),
+                    () -> assertThat(result.getContent().get(2).getPrice()).isEqualByComparingTo(BigDecimal.valueOf(30000L))
             );
         }
 
         @DisplayName("성공 케이스: sort가 'likes_desc'이면 좋아요 수 내림차순으로 정렬된 상품 목록을 반환한다")
         @Test
-        void getProducts_withSortLikesDesc_returnsProductsSortedByLikesDesc() {
+        void getProductViews_withSortLikesDesc_returnsProductsSortedByLikesDesc() {
             // arrange
-            ProductInfo product1 = createAndSaveProduct("상품1", BigDecimal.valueOf(10000L), 10L);
-            ProductInfo product2 = createAndSaveProduct("상품2", BigDecimal.valueOf(20000L), 30L);
-            ProductInfo product3 = createAndSaveProduct("상품3", BigDecimal.valueOf(30000L), 20L);
+            createAndSaveProduct("상품1", BigDecimal.valueOf(10000L), 10L);
+            createAndSaveProduct("상품2", BigDecimal.valueOf(20000L), 30L);
+            createAndSaveProduct("상품3", BigDecimal.valueOf(30000L), 20L);
 
             Pageable pageable = PageRequest.of(0, 10);
             ProductCondition condition = ProductCondition.builder()
@@ -111,20 +112,20 @@ void getProducts_withSortLikesDesc_returnsProductsSortedByLikesDesc() {
                     .build();
 
             // act
-            Page<ProductInfo> result = productFacade.getProducts(condition, pageable);
+            Page<ProductView> result = productFacade.getProductViews(condition, pageable);
 
             // assert
             assertAll(
                     () -> assertThat(result.getContent()).hasSize(3),
-                    () -> assertThat(result.getContent().get(0).likeCount()).isEqualTo(30L),
-                    () -> assertThat(result.getContent().get(1).likeCount()).isEqualTo(20L),
-                    () -> assertThat(result.getContent().get(2).likeCount()).isEqualTo(10L)
+                    () -> assertThat(result.getContent().get(0).getLikeCount()).isEqualTo(30L),
+                    () -> assertThat(result.getContent().get(1).getLikeCount()).isEqualTo(20L),
+                    () -> assertThat(result.getContent().get(2).getLikeCount()).isEqualTo(10L)
             );
         }
 
         @DisplayName("성공 케이스: 페이징이 제대로 동작한다")
         @Test
-        void getProducts_withPaging_returnsPagedResults() {
+        void getProductViews_withPaging_returnsPagedResults() {
             // arrange
             for (int i = 1; i <= 5; i++) {
                 createAndSaveProduct("상품" + i, BigDecimal.valueOf(10000L * i), 10L);
@@ -136,7 +137,7 @@ void getProducts_withPaging_returnsPagedResults() {
                     .build();
 
             // act
-            Page<ProductInfo> result = productFacade.getProducts(condition, pageable);
+            Page<ProductView> result = productFacade.getProductViews(condition, pageable);
 
             // assert
             assertAll(
@@ -149,53 +150,50 @@ void getProducts_withPaging_returnsPagedResults() {
         }
     }
 
-    @DisplayName("getProduct 메서드")
+    @DisplayName("getProductView 메서드")
     @Nested
-    class GetProductTest {
+    class GetProductViewTest {
 
-        @DisplayName("성공 케이스: 존재하는 Product ID로 조회하면 해당 ProductInfo를 반환한다")
+        @DisplayName("성공 케이스: 존재하는 Product ID로 조회하면 해당 ProductView를 반환한다")
         @Test
-        void getProduct_withValidId_returnsProductInfo() {
+        void getProductView_withValidId_returnsProductView() {
             // arrange
-            ProductInfo productInfo = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 10L);
-            Long productId = productInfo.id();
+            Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 10L);
 
             // act
-            ProductInfo result = productFacade.getProduct(productId);
+            ProductView result = productFacade.getProductView(productId);
 
             // assert
             assertAll(
                     () -> assertThat(result).isNotNull(),
-                    () -> assertThat(result.id()).isEqualTo(productId),
-                    () -> assertThat(result.name()).isEqualTo("테스트 상품"),
-                    () -> assertThat(result.price()).isEqualByComparingTo(BigDecimal.valueOf(10000L)),
-                    () -> assertThat(result.likeCount()).isEqualTo(10L),
-                    () -> assertThat(result.status()).isEqualTo(ProductStatus.ON_SALE),
-                    () -> assertThat(result.isVisible()).isTrue(),
-                    () -> assertThat(result.isSellable()).isTrue()
+                    () -> assertThat(result.getId()).isEqualTo(productId),
+                    () -> assertThat(result.getName()).isEqualTo("테스트 상품"),
+                    () -> assertThat(result.getPrice()).isEqualByComparingTo(BigDecimal.valueOf(10000L)),
+                    () -> assertThat(result.getLikeCount()).isEqualTo(10L),
+                    () -> assertThat(result.getStatus()).isEqualTo(ProductStatus.ON_SALE)
             );
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 Product ID로 조회하면 NOT_FOUND 예외가 발생한다")
         @Test
-        void getProduct_withInvalidId_throwsNotFoundException() {
+        void getProductView_withInvalidId_throwsNotFoundException() {
             // arrange
             Long invalidId = 999L;
 
             // act & assert
             CoreException exception = assertThrows(CoreException.class, () -> {
-                productFacade.getProduct(invalidId);
+                productFacade.getProductView(invalidId);
             });
 
             assertAll(
                     () -> assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND),
-                    () -> assertThat(exception.getCustomMessage()).contains("[productId = " + invalidId + "] Product를 찾을 수 없습니다.")
+                    () -> assertThat(exception.getCustomMessage()).contains("상품을 찾지 못했습니다.")
             );
         }
     }
 
     // 테스트 헬퍼 메서드
-    private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long likeCount) {
+    private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount) {
         Product product = Product.builder()
                 .name(name)
                 .description("테스트 설명")
@@ -205,12 +203,10 @@ private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long lik
                 .isSellable(true)
                 .build();
 
-        // 먼저 Product 저장
-        ProductInfo savedProductInfo = productFacade.saveProduct(ProductInfo.from(product));
+        // Product 저장
+        Product savedProduct = productFacade.saveProduct(product);
         
         // 저장 후 likeCount를 reflection으로 설정
-        Product savedProduct = productRepository.findById(savedProductInfo.id())
-                .orElseThrow(() -> new RuntimeException("Product를 찾을 수 없습니다"));
         try {
             Field likeCountField = Product.class.getDeclaredField("likeCount");
             likeCountField.setAccessible(true);
@@ -220,9 +216,8 @@ private ProductInfo createAndSaveProduct(String name, BigDecimal price, Long lik
             throw new RuntimeException("likeCount 설정 실패", e);
         }
         
-        // 최종 ProductInfo 반환
-        return ProductInfo.from(productRepository.findById(savedProductInfo.id())
-                .orElseThrow(() -> new RuntimeException("Product를 찾을 수 없습니다")));
+        // productId 반환
+        return savedProduct.getId();
     }
 }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
index e4cfbd657..1939dd676 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.java
@@ -107,8 +107,12 @@ void getUser_idExists_Success() {
                     .build();
             userFacade.saveUser(userinfo);
             
+            Long userId = userService.findUserByLoginId(validLoginId)
+                    .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"))
+                    .getId();
+            
             // act
-            UserInfo userInfo = userFacade.getUser(validLoginId);
+            UserInfo userInfo = userFacade.getUser(userId);
 
             // assert
             assertNotNull(userInfo);
@@ -126,10 +130,11 @@ void getUser_idExists_Success() {
         @Test
         void getUser_idNotExists_getNull() {
             // arrange
+            Long nonExistentUserId = 99999L;
 
             // act
             CoreException result = assertThrows(CoreException.class
-                    , () -> userFacade.getUser(validLoginId));
+                    , () -> userFacade.getUser(nonExistentUserId));
 
             // assert
             assertEquals(ErrorType.NOT_FOUND, result.getErrorType());
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
index dc05bc0e8..3a2fc1ddb 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java
@@ -100,7 +100,7 @@ void setUp() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(savedBrand)
+                .brandId(savedBrand.getId())
                 .build();
         Product savedProduct = productRepository.save(product)
                 .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
@@ -109,7 +109,7 @@ void setUp() {
         testOrder = Order.builder()
                 .discountAmount(BigDecimal.ZERO)
                 .shippingFee(BigDecimal.ZERO)
-                .user(test34)
+                    .userId(test34.getId())
                 .build();
         
         // OrderItem 추가
@@ -122,7 +122,7 @@ void setUp() {
         Coupon coupon = Coupon.builder()
                 .couponType(CouponType.FIXED_AMOUNT)
                 .discountValue(BigDecimal.valueOf(5000))
-                .user(test34)
+                    .userId(test34.getId())
                 .build();
 
         Coupon savedCoupon = couponRepository.save(coupon)
@@ -156,8 +156,8 @@ void useCoupon_withValidCoupon_Success() {
             
             assertAll(
                     () -> assertTrue(usedCoupon.getIsUsed(), "쿠폰은 사용된 상태여야 함"),
-                    () -> assertNotNull(usedCoupon.getOrder(), "쿠폰의 order는 null이 아니어야 함"),
-                    () -> assertEquals(order.getId(), usedCoupon.getOrder().getId(), "쿠폰의 order ID가 일치해야 함"),
+                    () -> assertNotNull(usedCoupon.getOrderId(), "쿠폰의 orderId는 null이 아니어야 함"),
+                    () -> assertEquals(order.getId(), usedCoupon.getOrderId(), "쿠폰의 order ID가 일치해야 함"),
                     () -> assertEquals(0, initialTotalPrice.subtract(expectedDiscount).compareTo(order.getFinalAmount()), "주문의 할인이 적용되어야 함")
             );
         }
@@ -198,7 +198,7 @@ void useCoupon_withOtherUserCoupon_BadRequest() {
             Coupon otherUserCoupon = Coupon.builder()
                     .couponType(CouponType.FIXED_AMOUNT)
                     .discountValue(BigDecimal.valueOf(5000))
-                    .user(otherUser)
+                    .userId(otherUser.getId())
                     .build();
             Coupon savedOtherUserCoupon = couponRepository.save(otherUserCoupon)
                     .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -337,7 +337,7 @@ void useCoupon_atomicOperation_noLostUpdate() throws InterruptedException {
                 Coupon coupon = Coupon.builder()
                         .couponType(CouponType.FIXED_AMOUNT)
                         .discountValue(BigDecimal.valueOf(5000))
-                        .user(test34)
+                        .userId(test34.getId())
                         .build();
                 Coupon savedCoupon = couponRepository.save(coupon)
                         .orElseThrow(() -> new RuntimeException("Coupon 저장 실패"));
@@ -450,7 +450,7 @@ private Order createTestOrder() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(savedBrand)
+                .brandId(savedBrand.getId())
                 .build();
         Product savedProduct = productRepository.save(product)
                 .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
@@ -458,7 +458,7 @@ private Order createTestOrder() {
         Order order = Order.builder()
                 .discountAmount(BigDecimal.ZERO)
                 .shippingFee(BigDecimal.ZERO)
-                .user(test34)
+                    .userId(test34.getId())
                 .build();
         
         // OrderItem 추가
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
index bb155698a..eca3ceada 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -49,7 +49,7 @@ private Product createValidProduct() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(brand)
+                .brandId(brand.getId())
                 .build();
 
         // ID 강제 주입 (ReflectionTestUtils 등 사용)
@@ -115,7 +115,7 @@ void createOrder_withNullDiscountAmount_ThrowsException() {
                     Order.builder()
                             .discountAmount(null)
                             .shippingFee(validShippingFee)
-                            .user(user)
+                            .userId(user.getId())
                             .build()
             );
 
@@ -134,7 +134,7 @@ void createOrder_withNegativeDiscountAmount_ThrowsException() {
                     Order.builder()
                             .discountAmount(BigDecimal.valueOf(-1000))
                             .shippingFee(validShippingFee)
-                            .user(user)
+                            .userId(user.getId())
                             .build()
             );
 
@@ -153,7 +153,7 @@ void createOrder_withNullShippingFee_ThrowsException() {
                     Order.builder()
                             .discountAmount(validDiscountAmount)
                             .shippingFee(null)
-                            .user(user)
+                            .userId(user.getId())
                             .build()
             );
 
@@ -172,7 +172,7 @@ void createOrder_withNegativeShippingFee_ThrowsException() {
                     Order.builder()
                             .discountAmount(validDiscountAmount)
                             .shippingFee(BigDecimal.valueOf(-1000))
-                            .user(user)
+                            .userId(user.getId())
                             .build()
             );
 
@@ -190,7 +190,7 @@ void createOrder_withNullUser_ThrowsException() {
                     Order.builder()
                             .discountAmount(validDiscountAmount)
                             .shippingFee(validShippingFee)
-                            .user(null)
+                            .userId(null)
                             .build()
             );
 
@@ -214,7 +214,7 @@ void addOrderItem_whenStatusIsPending_Success() {
             Order order = Order.builder()
                     .discountAmount(validDiscountAmount)
                     .shippingFee(validShippingFee)
-                    .user(user)
+                    .userId(user.getId())
                     .build();
             
             // 첫 번째 OrderItem 추가
@@ -249,7 +249,7 @@ void addOrderItem_whenStatusIsNotPending_ThrowsException() {
             Order order = Order.builder()
                     .discountAmount(validDiscountAmount)
                     .shippingFee(validShippingFee)
-                    .user(user)
+                    .userId(user.getId())
                     .build();
             
             // 상태를 CONFIRMED로 변경
@@ -279,7 +279,7 @@ void addOrderItem_multipleItems_Success() {
             Order order = Order.builder()
                     .discountAmount(validDiscountAmount)
                     .shippingFee(validShippingFee)
-                    .user(user)
+                    .userId(user.getId())
                     .build();
             
             // 첫 번째 OrderItem 추가
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
index 59b3ad8d7..cbeabb094 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
@@ -50,7 +50,7 @@ private Product createValidProduct() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(brand)
+                .brandId(brand.getId())
                 .build();
 
         // ID 강제 주입 (ReflectionTestUtils 등 사용)
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
index 4505eeed4..fcea049a0 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceTest.java
@@ -3,6 +3,7 @@
 import com.loopers.application.user.UserFacade;
 import com.loopers.application.user.UserInfo;
 import com.loopers.domain.user.Gender;
+import com.loopers.domain.user.UserService;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
@@ -40,10 +41,14 @@ class PointServiceTest {
     @Autowired
     private UserFacade userFacade;
 
+    @Autowired
+    private UserService userService;
+
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
     private final String testLoginId = "bobby34";
+    private Long testUserId;
     private final BigDecimal initialPointAmount = BigDecimal.valueOf(100000);
 
     @BeforeEach
@@ -56,9 +61,13 @@ void setUp() {
                 .gender(Gender.MALE)
                 .build();
         userFacade.saveUser(userInfo);
+        
+        testUserId = userService.findUserByLoginId(testLoginId)
+                .orElseThrow(() -> new RuntimeException("User를 찾을 수 없습니다"))
+                .getId();
 
         // 포인트 충전
-        pointService.charge(testLoginId, initialPointAmount);
+        pointService.charge(testUserId, initialPointAmount);
     }
 
     @AfterEach
@@ -78,7 +87,7 @@ void charge_inputZero_BadRequest() {
 
             // act
             CoreException result = assertThrows(CoreException.class,
-                    () -> pointService.charge(testLoginId, requestPoint)
+                    () -> pointService.charge(testUserId, requestPoint)
             );
 
             // assert
@@ -94,7 +103,7 @@ void charge_inputBelowZero_BadRequest() {
 
             // act
             CoreException result = assertThrows(CoreException.class,
-                    () -> pointService.charge(testLoginId, requestPoint)
+                    () -> pointService.charge(testUserId, requestPoint)
             );
 
             // assert
@@ -114,10 +123,10 @@ void deduct_validAmount_Success() {
             BigDecimal deductAmount = BigDecimal.valueOf(5000);
 
             // act
-            pointService.deduct(testLoginId, deductAmount);
+            pointService.deduct(testUserId, deductAmount);
 
             // assert
-            BigDecimal finalAmount = pointService.findByUserLoginId(testLoginId)
+            BigDecimal finalAmount = pointService.findByUserId(testUserId)
                     .map(Point::getAmount)
                     .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
             BigDecimal expectedAmount = initialPointAmount.subtract(deductAmount);
@@ -132,7 +141,7 @@ void deduct_inputZero_BadRequest() {
 
             // act
             CoreException result = assertThrows(CoreException.class,
-                    () -> pointService.deduct(testLoginId, requestPoint)
+                    () -> pointService.deduct(testUserId, requestPoint)
             );
 
             // assert
@@ -148,7 +157,7 @@ void deduct_inputBelowZero_BadRequest() {
 
             // act
             CoreException result = assertThrows(CoreException.class,
-                    () -> pointService.deduct(testLoginId, requestPoint)
+                    () -> pointService.deduct(testUserId, requestPoint)
             );
 
             // assert
@@ -164,7 +173,7 @@ void deduct_insufficientPoint_BadRequest() {
 
             // act
             CoreException result = assertThrows(CoreException.class,
-                    () -> pointService.deduct(testLoginId, requestPoint)
+                    () -> pointService.deduct(testUserId, requestPoint)
             );
 
             // assert
@@ -197,7 +206,7 @@ void deduct_concurrentRequests_success() throws InterruptedException {
             for (int i = 0; i < threadCount; i++) {
                 executor.submit(() -> {
                     try {
-                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        pointService.deduct(testUserId, deductAmountPerThread);
                         successCount.incrementAndGet();
                     } catch (Exception e) {
                         failureCount.incrementAndGet();
@@ -213,7 +222,7 @@ void deduct_concurrentRequests_success() throws InterruptedException {
             executor.shutdown();
 
             // assert
-            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+            BigDecimal finalPoint = pointRepository.findByUserId(testUserId)
                     .map(Point::getAmount)
                     .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
 
@@ -244,7 +253,7 @@ void deduct_insufficientPoint_partialSuccess() throws InterruptedException {
             for (int i = 0; i < threadCount; i++) {
                 executor.submit(() -> {
                     try {
-                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        pointService.deduct(testUserId, deductAmountPerThread);
                         successCount.incrementAndGet();
                     } catch (Exception e) {
                         failureCount.incrementAndGet();
@@ -260,7 +269,7 @@ void deduct_insufficientPoint_partialSuccess() throws InterruptedException {
             executor.shutdown();
 
             // assert
-            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+            BigDecimal finalPoint = pointRepository.findByUserId(testUserId)
                     .map(Point::getAmount)
                     .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
 
@@ -300,7 +309,7 @@ void deduct_atomicOperation_noLostUpdate() throws InterruptedException {
             for (int i = 0; i < threadCount; i++) {
                 executor.submit(() -> {
                     try {
-                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        pointService.deduct(testUserId, deductAmountPerThread);
                         successCount.incrementAndGet();
                     } catch (Exception e) {
                         // 실패는 무시 (포인트 부족 등)
@@ -315,7 +324,7 @@ void deduct_atomicOperation_noLostUpdate() throws InterruptedException {
             executor.shutdown();
 
             // assert
-            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+            BigDecimal finalPoint = pointRepository.findByUserId(testUserId)
                     .map(Point::getAmount)
                     .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
 
@@ -344,7 +353,7 @@ void deduct_concurrentRequests_noNegativePoint() throws InterruptedException {
             for (int i = 0; i < threadCount; i++) {
                 executor.submit(() -> {
                     try {
-                        pointService.deduct(testLoginId, deductAmountPerThread);
+                        pointService.deduct(testUserId, deductAmountPerThread);
                         successCount.incrementAndGet();
                     } catch (Exception e) {
                         failureCount.incrementAndGet();
@@ -359,7 +368,7 @@ void deduct_concurrentRequests_noNegativePoint() throws InterruptedException {
             executor.shutdown();
 
             // assert
-            BigDecimal finalPoint = pointRepository.findByUser_loginId(testLoginId)
+            BigDecimal finalPoint = pointRepository.findByUserId(testUserId)
                     .map(Point::getAmount)
                     .orElseThrow(() -> new RuntimeException("포인트를 찾을 수 없습니다"));
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
index be1a20801..3567ffda0 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/stock/StockServiceTest.java
@@ -71,7 +71,7 @@ void setUp() {
                 .status(ProductStatus.ON_SALE)
                 .isVisible(true)
                 .isSellable(true)
-                .brand(savedBrand)
+                .brandId(savedBrand.getId())
                 .build();
         Product savedProduct = productRepository.save(product)
                 .orElseThrow(() -> new RuntimeException("Product 저장 실패"));
@@ -79,7 +79,7 @@ void setUp() {
 
         // Product 저장 후 Stock 별도 생성
         Stock stock = Stock.builder()
-                .product(savedProduct)
+                .productId(savedProduct.getId())
                 .quantity(0L)
                 .build();
         stockService.saveStock(stock)

From cdc92d4371f54e3fb81f9b34f0784d79b7e6b379 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 21:29:45 +0900
Subject: [PATCH 14/17] =?UTF-8?q?[Round=205]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
 =?UTF-8?q?=20=EA=B3=A0=EC=B9=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../product/ProductViewEventHandler.java      |  8 +++--
 .../java/com/loopers/domain/brand/Brand.java  |  1 +
 .../like/LikeJpaRepository.java               |  4 +--
 .../like/LikeRepositoryImpl.java              |  2 +-
 .../product/ProductQueryRepository.java       |  5 +--
 .../product/ProductViewRepositoryImpl.java    |  5 +++
 .../application/like/LikeFacadeTest.java      |  8 ++---
 .../product/ProductFacadeTest.java            | 34 +++++++++++++++++++
 .../com/loopers/domain/like/LikeTest.java     |  2 +-
 .../loopers/domain/order/OrderItemTest.java   |  8 ++++-
 .../com/loopers/domain/order/OrderTest.java   | 14 ++++++--
 .../loopers/domain/payment/PaymentTest.java   | 22 ++++++++----
 .../com/loopers/domain/point/PointTest.java   |  8 ++++-
 .../com/loopers/domain/stock/StockTest.java   |  7 ++++
 .../loopers/interfaces/api/PointE2ETest.java  |  7 ++--
 15 files changed, 107 insertions(+), 28 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
index 2aba2cd97..d27a0db46 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
@@ -13,6 +13,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.event.TransactionPhase;
 import org.springframework.transaction.event.TransactionalEventListener;
@@ -33,7 +34,7 @@ public class ProductViewEventHandler {
      */
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void handleProductLikeCountEvent(ProductLikeCountEvent event) {
         Long productId = event.productId();
         
@@ -56,7 +57,7 @@ public void handleProductLikeCountEvent(ProductLikeCountEvent event) {
      */
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void handleProductCreatedEvent(ProductCreatedEvent event) {
         Long productId = event.productId();
         Long brandId = event.brandId();
@@ -86,6 +87,7 @@ public void handleProductCreatedEvent(ProductCreatedEvent event) {
                     .brandId(brandId)
                     .brandName(brandName)
                     .status(product.getStatus())
+                    .createdAt(product.getCreatedAt())
                     .build();
 
             productViewRepository.save(productView);
@@ -102,7 +104,7 @@ public void handleProductCreatedEvent(ProductCreatedEvent event) {
      */
     @Async
     @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void handleProductDeletedEvent(ProductDeletedEvent event) {
         Long productId = event.productId();
 
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
index dcbe765a9..c68271a3d 100644
--- 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
@@ -35,6 +35,7 @@ private Brand(
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
+        guard();
     }
 
     // 유효성 검사
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
index 290a3c8d9..534334466 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
@@ -9,7 +9,7 @@
 import java.util.Optional;
 
 public interface LikeJpaRepository extends JpaRepository<Like, LikeId> {
-    Optional<Like> findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
-    void deleteByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
+    Optional<Like> findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
+    void deleteByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(Long userId, Long likeTargetId, LikeTargetType likeTargetType);
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
index 4198b1127..81f696f38 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
@@ -29,7 +29,7 @@ public long deleteByUserIdAndLikeTargetId(Long userId, Long likeTargetId, LikeTa
 
     @Override
     public Optional<Like> findByUserIdAndLikeTargetId(Long userId, Long likeTargetId, LikeTargetType likeTargetType) {
-        return likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(userId, likeTargetId, likeTargetType);
+        return likeJpaRepository.findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(userId, likeTargetId, likeTargetType);
     }
 
     @Override
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
index fa43da3ab..9664b9347 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
@@ -39,12 +39,13 @@ public Page<ProductView> findProductViews(ProductCondition condition, Pageable p
         // 2. 정렬 조건
         final Map<String, OrderSpecifier<?>> ORDER_BY_MAP = Map.of(
             "price_asc", productView.price.asc(),
-            "likes_desc", productView.likeCount.desc()
+            "likes_desc", productView.likeCount.desc(),
+            "latest", productView.createdAt.desc()
         );
         
         OrderSpecifier<?> orderSpecifier = ORDER_BY_MAP.getOrDefault(
             condition.sort(),
-            productView.likeCount.desc() // 기본값: 좋아요 순
+            productView.createdAt.desc() // 기본값: 최신순
         );
         
         // 3. 데이터 조회 (ProductView 사용)
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
index 9542d73c7..157ce79ef 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
@@ -6,6 +6,7 @@
 import com.querydsl.jpa.impl.JPAQueryFactory;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Optional;
 
@@ -17,17 +18,20 @@ public class ProductViewRepositoryImpl implements ProductViewRepository {
     private final JPAQueryFactory queryFactory;
 
     @Override
+    @Transactional
     public Optional<ProductView> save(ProductView productView) {
         ProductView saved = productViewJpaRepository.save(productView);
         return Optional.of(saved);
     }
 
     @Override
+    @Transactional(readOnly = true)
     public Optional<ProductView> findById(Long id) {
         return productViewJpaRepository.findById(id);
     }
 
     @Override
+    @Transactional
     public void updateLikeCount(Long id, Long count) {
         QProductView productView = QProductView.productView;
         queryFactory
@@ -38,6 +42,7 @@ public void updateLikeCount(Long id, Long count) {
     }
 
     @Override
+    @Transactional
     public void deleteById(Long id) {
         QProductView productView = QProductView.productView;
         queryFactory
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
index e9c9e85a2..79ddb3e8e 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
@@ -81,7 +81,7 @@ void saveProductLike_withValidUserAndProduct_Success() {
             likeFacade.saveProductLike(userId, productId);
 
             // assert
-            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
+            Optional<Like> savedLike = likeJpaRepository.findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertTrue(savedLike.isPresent());
@@ -129,7 +129,7 @@ void saveProductLike_duplicateLike_Idempotent() {
             likeFacade.saveProductLike(userId, productId); // 중복 등록
 
             // assert
-            Optional<Like> savedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
+            Optional<Like> savedLike = likeJpaRepository.findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertTrue(savedLike.isPresent());
@@ -228,7 +228,7 @@ void deleteProductLike_withExistingLike_Success() {
             likeFacade.deleteProductLike(userId, productId);
 
             // assert
-            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
+            Optional<Like> deletedLike = likeJpaRepository.findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertFalse(deletedLike.isPresent());
@@ -255,7 +255,7 @@ void deleteProductLike_withNonExistentLike_Idempotent() {
             });
 
             // assert
-            Optional<Like> deletedLike = likeJpaRepository.findByUser_IdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
+            Optional<Like> deletedLike = likeJpaRepository.findByLikeId_UserIdAndLikeId_LikeTargetIdAndLikeId_LikeTargetType(
                     userId, productId, LikeTargetType.PRODUCT
             );
             assertFalse(deletedLike.isPresent());
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
index 4671da805..b9a5ee494 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
@@ -33,6 +33,9 @@ class ProductFacadeTest {
     @Autowired
     private com.loopers.domain.product.ProductRepository productRepository;
 
+    @Autowired
+    private com.loopers.domain.product.ProductViewRepository productViewRepository;
+
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
@@ -216,8 +219,39 @@ private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount)
             throw new RuntimeException("likeCount 설정 실패", e);
         }
         
+        // ProductView가 생성될 때까지 대기 (비동기 이벤트 핸들러 완료 대기)
+        waitForProductViewCreation(savedProduct.getId());
+        
+        // ProductView의 likeCount 업데이트 (테스트용)
+        // 실제로는 Like 엔티티를 생성해야 하지만, 테스트 편의를 위해 직접 업데이트
+        productViewRepository.updateLikeCount(savedProduct.getId(), likeCount);
+        
         // productId 반환
         return savedProduct.getId();
     }
+
+    /**
+     * ProductView가 생성될 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewCreation(Long productId) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                if (productViewRepository.findById(productId).isPresent()) {
+                    return; // ProductView가 생성되었음
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView 생성 대기 중 인터럽트 발생", e);
+            }
+        }
+        
+        throw new RuntimeException("ProductView 생성 대기 시간 초과: productId=" + productId);
+    }
 }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
index 9982f2131..e724c987d 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
@@ -73,7 +73,7 @@ void createLike_withNullUser_BadRequest() {
             });
 
             assertEquals(ErrorType.BAD_REQUEST, result.getErrorType());
-            assertEquals("Like : user가 비어있을 수 없습니다.", result.getCustomMessage());
+            assertEquals("Like : userId가 비어있을 수 없습니다.", result.getCustomMessage());
         }
 
         @DisplayName("실패 케이스: likeTargetId가 null이면 BAD_REQUEST 예외 발생")
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
index 8735acc18..83f3ebf74 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
@@ -31,6 +31,9 @@ private Product createValidProduct() {
                 .isVisible(true)
                 .isSellable(true)
                 .build();
+        
+        // Brand ID 강제 주입
+        ReflectionTestUtils.setField(brand, "id", 1L);
 
         Product product = Product.builder()
                 .name("Test Product")
@@ -42,7 +45,7 @@ private Product createValidProduct() {
                 .brandId(brand.getId())
                 .build();
         
-        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        // Product ID 강제 주입
         ReflectionTestUtils.setField(product, "id", 1L);
 
         return product;
@@ -55,6 +58,9 @@ private Order createValidOrder() {
                 .birthday("1990-01-01")
                 .gender(Gender.MALE)
                 .build();
+        
+        // ID 강제 주입
+        ReflectionTestUtils.setField(user, "id", 1L);
 
         Order order = Order.builder()
                 .discountAmount(BigDecimal.valueOf(1000))
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
index eca3ceada..0e84639c4 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
@@ -25,12 +25,17 @@ public class OrderTest {
     private final BigDecimal validShippingFee = BigDecimal.valueOf(2000);
 
     private User createValidUser() {
-        return User.builder()
+        User user = User.builder()
                 .loginId("testuser1")
                 .email("test@test.com")
                 .birthday("1990-01-01")
                 .gender(Gender.MALE)
                 .build();
+        
+        // ID 강제 주입
+        ReflectionTestUtils.setField(user, "id", 1L);
+        
+        return user;
     }
 
     private Product createValidProduct() {
@@ -42,6 +47,9 @@ private Product createValidProduct() {
                 .isSellable(true)
                 .build();
 
+        // Brand ID 강제 주입
+        ReflectionTestUtils.setField(brand, "id", 1L);
+        
         Product product = Product.builder()
                 .name("Test Product")
                 .description("Test Description")
@@ -52,7 +60,7 @@ private Product createValidProduct() {
                 .brandId(brand.getId())
                 .build();
 
-        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        // Product ID 강제 주입
         ReflectionTestUtils.setField(product, "id", 1L);
 
         return product;
@@ -195,7 +203,7 @@ void createOrder_withNullUser_ThrowsException() {
             );
 
             assertEquals(ErrorType.BAD_REQUEST, exception.getErrorType());
-            assertTrue(exception.getCustomMessage().contains("user가 비어있을 수 없습니다"));
+            assertTrue(exception.getCustomMessage().contains("userId가 비어있을 수 없습니다"));
         }
 
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
index cbeabb094..0978a8cf6 100644
--- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentTest.java
@@ -26,12 +26,17 @@ public class PaymentTest {
     private final PaymentStatus validPaymentStatus = PaymentStatus.PENDING;
 
     private User createValidUser() {
-        return User.builder()
+        User user = User.builder()
                 .loginId("testuser1")
                 .email("test@test.com")
                 .birthday("1990-01-01")
                 .gender(Gender.MALE)
                 .build();
+        
+        // ID 강제 주입
+        ReflectionTestUtils.setField(user, "id", 1L);
+        
+        return user;
     }
 
     private Product createValidProduct() {
@@ -42,6 +47,9 @@ private Product createValidProduct() {
                 .isVisible(true)
                 .isSellable(true)
                 .build();
+        
+        // Brand ID 강제 주입
+        ReflectionTestUtils.setField(brand, "id", 1L);
 
         Product product = Product.builder()
                 .name("Test Product")
@@ -53,7 +61,7 @@ private Product createValidProduct() {
                 .brandId(brand.getId())
                 .build();
 
-        // ID 강제 주입 (ReflectionTestUtils 등 사용)
+        // Product ID 강제 주입
         ReflectionTestUtils.setField(product, "id", 1L);
 
         return product;
@@ -83,15 +91,15 @@ class CreatePaymentTest {
         @Test
         void createPayment_withValidFields_Success() {
             // arrange
-            Order order = createValidOrder();
-            BigDecimal expectedAmount = order.getFinalAmount(); // 생성 시 order.getFinalAmount()로 자동 설정됨
+            Long orderId = 1L;
+            BigDecimal expectedAmount = BigDecimal.valueOf(1000); // 생성 시 order.getFinalAmount()로 자동 설정됨
 
             // act
             Payment payment = Payment.builder()
                     .method(validMethod)
                     .paymentStatus(validPaymentStatus)
-                    .orderId(order.getId())
-                    .amount(order.getFinalAmount())
+                    .orderId(orderId)
+                    .amount(expectedAmount)
                     .build();
 
             // assert
@@ -100,7 +108,7 @@ void createPayment_withValidFields_Success() {
                     () -> assertEquals(validMethod, payment.getMethod()),
                     () -> assertEquals(expectedAmount, payment.getAmount(), "amount는 order.getFinalAmount()로 자동 설정되어야 함"),
                     () -> assertEquals(validPaymentStatus, payment.getPaymentStatus()),
-                    () -> assertEquals(order.getId(), payment.getOrderId())
+                    () -> assertEquals(orderId, payment.getOrderId())
             );
         }
 
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 97872dee6..648a5ddfc 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
@@ -7,6 +7,7 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 
@@ -16,12 +17,17 @@
 public class PointTest {
 
     private User createValidUser() {
-        return User.builder()
+        User user = User.builder()
                 .loginId("testuser1")
                 .email("test@test.com")
                 .birthday("1990-01-01")
                 .gender(Gender.MALE)
                 .build();
+        
+        // ID 강제 주입
+        ReflectionTestUtils.setField(user, "id", 1L);
+        
+        return user;
     }
 
     @DisplayName("Point 엔티티 생성")
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
index 212c462dc..0993b991a 100644
--- 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
@@ -9,6 +9,7 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
 
@@ -28,6 +29,9 @@ private Product createValidProduct() {
                 .isVisible(true)
                 .isSellable(true)
                 .build();
+        
+        // Brand ID 강제 주입
+        ReflectionTestUtils.setField(brand, "id", 1L);
 
         Product product = Product.builder()
                 .name("Test Product")
@@ -38,6 +42,9 @@ private Product createValidProduct() {
                 .isSellable(true)
                 .brandId(brand.getId())
                 .build();
+        
+        // Product ID 강제 주입
+        ReflectionTestUtils.setField(product, "id", 1L);
 
         return product;
     }
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointE2ETest.java
index e5fad7f43..1213d43c8 100644
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointE2ETest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointE2ETest.java
@@ -55,6 +55,7 @@ public PointE2ETest(
     @AfterEach
     void tearDown() {databaseCleanUp.truncateAllTables();}
 
+    final Long validUserId = 1L;
     final String validLoginId = "bobby34";
     final String validEmail = "bobby34@naver.com";
     final String validBirthday = "1994-04-08";
@@ -82,7 +83,7 @@ void getUserPoint_withValidFields_Success() {
 
             String requestUrl = ENDPOINT + "/";
             HttpHeaders headers = new HttpHeaders();
-            headers.add("X-USER-ID", validLoginId);
+            headers.add("X-USER-ID", validUserId.toString());
             ParameterizedTypeReference<ApiResponse<PointDto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
 
             // act
@@ -148,7 +149,7 @@ void addUserPoint_add1000_getTotalPoints() {
 
             String requestUrl = ENDPOINT + "/charge";
             HttpHeaders headers = new HttpHeaders();
-            headers.add("X-USER-ID", validLoginId);
+            headers.add("X-USER-ID", validUserId.toString());
             ParameterizedTypeReference<ApiResponse<PointDto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
 
             // act
@@ -177,7 +178,7 @@ void addPoint_withNoUser_BadRequest() {
             // act
             String requestUrl = ENDPOINT + "/charge";
             HttpHeaders headers = new HttpHeaders();
-            headers.add("X-USER-ID", validLoginId);
+            headers.add("X-USER-ID", validUserId.toString());
             ParameterizedTypeReference<ApiResponse<PointDto.PointResponse>> responseType = new ParameterizedTypeReference<>() {};
 
             // act

From ef57797e6ed5fb49ffad3361ca237e0345321049 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Wed, 26 Nov 2025 22:34:51 +0900
Subject: [PATCH 15/17] =?UTF-8?q?[Round=205]=20=EC=9E=90=EC=9E=98=ED=95=9C?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/application/product/ProductViewEventHandler.java | 5 +----
 .../src/main/java/com/loopers/domain/product/Product.java    | 4 +++-
 .../main/java/com/loopers/domain/product/ProductView.java    | 2 +-
 3 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
index d27a0db46..6478dd0de 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
@@ -75,15 +75,12 @@ public void handleProductCreatedEvent(ProductCreatedEvent event) {
                         .orElse(null);
             }
 
-            // LikeCount 조회
-            long likeCount = likeRepository.countByProductId(productId);
-
             // ProductView 생성
             ProductView productView = ProductView.builder()
                     .id(product.getId())
                     .name(product.getName())
                     .price(product.getPrice())
-                    .likeCount(likeCount)
+                    .likeCount(product.getLikeCount())  // 새로 생성된 상품이므로 좋아요 수는 0
                     .brandId(brandId)
                     .brandName(brandName)
                     .status(product.getStatus())
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
index 0245b042e..d7e053395 100644
--- 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
@@ -20,7 +20,7 @@ public class Product extends BaseEntity {
     private String name;
     private String description;
     private BigDecimal price;
-    private Long likeCount = 0L;
+    private Long likeCount;
     @Enumerated(EnumType.STRING)
     private ProductStatus status;
     private Boolean isVisible;
@@ -32,6 +32,7 @@ private Product(
         String name
         , String description
         , BigDecimal price
+        , Long likeCount
         , ProductStatus status
         , Boolean isVisible
         , Boolean isSellable
@@ -40,6 +41,7 @@ private Product(
         this.name = name;
         this.description = description;
         this.price = price;
+        this.likeCount = likeCount != null ? likeCount : 0L;
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
index 05d4a2b1b..1f7522fb9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
@@ -11,7 +11,7 @@
 import java.time.ZonedDateTime;
 
 @Entity
-@Table(name = "product_summary", indexes = {
+@Table(name = "product_view", indexes = {
     @Index(name = "idx_ps_like_desc", columnList = "likeCount DESC"),
     @Index(name = "idx_ps_brand_like", columnList = "brandId, likeCount DESC")
 })

From b28ccfde51a246aba25364e0d59bcd03a9d29c2f Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 28 Nov 2025 16:01:45 +0900
Subject: [PATCH 16/17] =?UTF-8?q?[Round=205]=20Redis=20=EC=BA=90=EC=8B=B1?=
 =?UTF-8?q?=20=EA=B5=AC=ED=98=84,=20Product=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?=
 =?UTF-8?q?=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/product/ProductFacade.java    |  44 +-
 .../product/ProductViewEventHandler.java      | 116 ----
 .../com/loopers/domain/like/LikeService.java  |  18 +-
 .../com/loopers/domain/product/Product.java   |  10 -
 .../domain/product/ProductRepository.java     |   4 +-
 .../domain/product/ProductService.java        |  31 +-
 .../domain/product/ProductViewRepository.java |  12 -
 .../product/cache/ProductCacheService.java    | 537 ++++++++++++++++++
 .../product/event/ProductCreatedEvent.java    |   5 -
 .../product/event/ProductDeletedEvent.java    |   5 -
 .../domain/product/event/ProductEventDto.java |  46 ++
 .../product/event/ProductLikeCountEvent.java  |   5 -
 .../event/ProductViewEventHandler.java        | 187 ++++++
 .../product/{ => view}/ProductCondition.java  |   2 +-
 .../product/{ => view}/ProductView.java       |   9 +-
 .../product/view/ProductViewRepository.java   |  18 +
 .../view}/ProductViewSyncScheduler.java       |   6 +-
 .../product/ProductQueryRepository.java       |  50 +-
 .../product/ProductRepositoryImpl.java        |  16 +-
 .../product/ProductViewJpaRepository.java     |   2 +-
 .../product/ProductViewRepositoryImpl.java    |  37 +-
 .../api/product/ProductController.java        |   4 +-
 .../interfaces/api/product/ProductDto.java    |   4 +-
 .../loopers/support/config/CacheConfig.java   |  62 ++
 .../support/config/CacheProperties.java       |  28 +
 .../src/main/resources/application.yml        |  17 +
 .../application/like/LikeFacadeTest.java      | 124 ++--
 .../product/ProductFacadeTest.java            | 220 ++++++-
 .../loopers/domain/product/ProductTest.java   |   1 -
 .../ProductCacheServiceIntegrationTest.java   | 257 +++++++++
 .../cache/ProductCacheServiceTest.java        | 263 +++++++++
 .../event/ProductViewEventHandlerTest.java    | 312 ++++++++++
 modules/redis/src/main/resources/redis.yml    |   9 +-
 .../RedisTestContainersConfig.java            |  14 +-
 .../loopers/config/jackson/JacksonConfig.java |   4 +
 35 files changed, 2178 insertions(+), 301 deletions(-)
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/cache/ProductCacheService.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductEventDto.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewEventHandler.java
 rename apps/commerce-api/src/main/java/com/loopers/domain/product/{ => view}/ProductCondition.java (80%)
 rename apps/commerce-api/src/main/java/com/loopers/domain/product/{ => view}/ProductView.java (68%)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewRepository.java
 rename apps/commerce-api/src/main/java/com/loopers/{application/product => domain/product/view}/ProductViewSyncScheduler.java (93%)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/support/config/CacheConfig.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/support/config/CacheProperties.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/event/ProductViewEventHandlerTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
index 184acb2e6..2fa4df793 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
@@ -1,9 +1,10 @@
 package com.loopers.application.product;
 
 import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.view.ProductCondition;
 import com.loopers.domain.product.ProductService;
-import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.cache.ProductCacheService;
 import com.loopers.domain.stock.Stock;
 import com.loopers.domain.stock.StockService;
 import com.loopers.support.error.CoreException;
@@ -22,21 +23,22 @@ public class ProductFacade {
 
     private final ProductService productService;
     private final StockService stockService;
+    private final ProductCacheService productCacheService;
 
     /**
-     * 상품 목록 조회 (ProductView 기반)
+     * 상품 목록 조회 (ProductView 기반, Two-Tier Caching 적용)
      */
     @Transactional(readOnly = true)
     public Page<ProductView> getProductViews(ProductCondition condition, Pageable pageable) {
-        return productService.findProductViews(condition, pageable);
+        return productCacheService.getProductViews(condition, pageable);
     }
 
     /**
-     * 상품 상세 조회 (ProductView 기반)
+     * 상품 상세 조회 (ProductView 기반, Two-Tier Caching 적용)
      */
     @Transactional(readOnly = true)
     public ProductView getProductView(Long productId) {
-        return productService.findProductViewById(productId)
+        return productCacheService.getProductView(productId)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.NOT_FOUND,
                         "상품을 찾지 못했습니다."
@@ -44,17 +46,17 @@ public ProductView getProductView(Long productId) {
     }
 
     /**
-     * 상품 저장
+     * 상품 생성
      */
     @Transactional
-    public Product saveProduct(Product product) {
-        Product savedProduct = productService.saveProduct(product)
+    public Product createProduct(Product product) {
+        Product savedProduct = productService.createProduct(product)
                 .orElseThrow(() -> new CoreException(
                         ErrorType.INTERNAL_ERROR,
-                        "Product 저장에 실패했습니다."
+                        "Product 생성에 실패했습니다."
                 ));
 
-        // Product 저장 후 Stock 별도 생성
+        // Product 생성 후 Stock 별도 생성
         Stock stock = Stock.builder()
                 .productId(savedProduct.getId())
                 .quantity(0L)
@@ -67,5 +69,25 @@ public Product saveProduct(Product product) {
 
         return savedProduct;
     }
+
+    /**
+     * 상품 수정
+     */
+    @Transactional
+    public Product updateProduct(Product product) {
+        return productService.updateProduct(product)
+                .orElseThrow(() -> new CoreException(
+                        ErrorType.INTERNAL_ERROR,
+                        "Product 수정에 실패했습니다."
+                ));
+    }
+
+    /**
+     * 상품 삭제 (Soft Delete)
+     */
+    @Transactional
+    public void deleteProduct(Long productId) {
+        productService.deleteProduct(productId);
+    }
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
deleted file mode 100644
index 6478dd0de..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewEventHandler.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.loopers.application.product;
-
-import com.loopers.domain.brand.BrandRepository;
-import com.loopers.domain.like.LikeRepository;
-import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.product.ProductView;
-import com.loopers.domain.product.ProductViewRepository;
-import com.loopers.domain.product.event.ProductCreatedEvent;
-import com.loopers.domain.product.event.ProductDeletedEvent;
-import com.loopers.domain.product.event.ProductLikeCountEvent;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Propagation;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.transaction.event.TransactionPhase;
-import org.springframework.transaction.event.TransactionalEventListener;
-
-@Slf4j
-@RequiredArgsConstructor
-@Component
-public class ProductViewEventHandler {
-
-    private final ProductViewRepository productViewRepository;
-    private final LikeRepository likeRepository;
-    private final ProductRepository productRepository;
-    private final BrandRepository brandRepository;
-
-    /**
-     * 좋아요 수 변경 이벤트 처리
-     * Like 테이블에서 COUNT(*)를 수행하여 정합성을 보장
-     */
-    @Async
-    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional(propagation = Propagation.REQUIRES_NEW)
-    public void handleProductLikeCountEvent(ProductLikeCountEvent event) {
-        Long productId = event.productId();
-        
-        try {
-            // 실제 Like 테이블에서 Count 조회 (정합성 확보)
-            long realCount = likeRepository.countByProductId(productId);
-
-            // ProductView 업데이트
-            productViewRepository.updateLikeCount(productId, realCount);
-            
-            log.debug("ProductView likeCount updated: productId={}, likeCount={}", productId, realCount);
-        } catch (Exception e) {
-            log.error("Failed to update ProductView likeCount: productId={}", productId, e);
-        }
-    }
-
-    /**
-     * 상품 생성 이벤트 처리
-     * Product + Brand 조회 후 ProductView 생성
-     */
-    @Async
-    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional(propagation = Propagation.REQUIRES_NEW)
-    public void handleProductCreatedEvent(ProductCreatedEvent event) {
-        Long productId = event.productId();
-        Long brandId = event.brandId();
-        
-        try {
-            // Product 조회
-            Product product = productRepository.findById(productId)
-                    .orElseThrow(() -> new IllegalStateException("Product not found: " + productId));
-
-            // Brand 조회
-            String brandName = null;
-            if (brandId != null) {
-                brandName = brandRepository.findById(brandId)
-                        .map(brand -> brand.getName())
-                        .orElse(null);
-            }
-
-            // ProductView 생성
-            ProductView productView = ProductView.builder()
-                    .id(product.getId())
-                    .name(product.getName())
-                    .price(product.getPrice())
-                    .likeCount(product.getLikeCount())  // 새로 생성된 상품이므로 좋아요 수는 0
-                    .brandId(brandId)
-                    .brandName(brandName)
-                    .status(product.getStatus())
-                    .createdAt(product.getCreatedAt())
-                    .build();
-
-            productViewRepository.save(productView);
-            
-            log.debug("ProductView created: productId={}, brandId={}", productId, brandId);
-        } catch (Exception e) {
-            log.error("Failed to create ProductView: productId={}", productId, e);
-        }
-    }
-
-    /**
-     * 상품 삭제 이벤트 처리 (Soft Delete)
-     * ProductView에서 해당 상품 제거
-     */
-    @Async
-    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
-    @Transactional(propagation = Propagation.REQUIRES_NEW)
-    public void handleProductDeletedEvent(ProductDeletedEvent event) {
-        Long productId = event.productId();
-
-        try {
-            productViewRepository.deleteById(productId);
-            log.debug("ProductView deleted: productId={}", productId);
-        } catch (Exception e) {
-            log.error("Failed to delete ProductView: productId={}", productId, e);
-        }
-    }
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
index f8d05d7d1..be5761858 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
@@ -1,8 +1,7 @@
 package com.loopers.domain.like;
 
 import com.loopers.domain.like.entity.LikeTargetType;
-import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.product.event.ProductLikeCountEvent;
+import com.loopers.domain.product.event.ProductEventDto;
 import com.loopers.domain.user.User;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.ApplicationEventPublisher;
@@ -14,7 +13,6 @@
 public class LikeService {
 
     private final LikeRepository likeRepository;
-    private final ProductRepository productRepository;
     private final ApplicationEventPublisher eventPublisher;
 
     /**
@@ -28,11 +26,10 @@ public void saveProductLike(User user, Long productId) {
         // 반환값: 1 = 새로 생성됨, 2 = 이미 존재하여 UPDATE만 됨
         long affectedRows = likeRepository.insertOrIgnore(user.getId(), productId, LikeTargetType.PRODUCT);
         
-        // 실제로 새로 생성된 경우에만 likeCount 증가
+        // 실제로 새로 생성된 경우에만 이벤트 발행
+        // Redis 캐시에 Write-Through로 좋아요 수 증가
         if (affectedRows == 1) {
-            productRepository.incrementLikeCount(productId);
-            // 이벤트 발행: ProductSummary 동기화를 위해
-            eventPublisher.publishEvent(new ProductLikeCountEvent(productId));
+                    eventPublisher.publishEvent(ProductEventDto.LikeCount.increment(productId));
         }
     }
 
@@ -47,11 +44,10 @@ public void deleteProductLike(User user, Long productId) {
                 user.getId(), productId, LikeTargetType.PRODUCT
         );
         
-        // 실제로 삭제된 경우에만 likeCount 감소
+        // 실제로 삭제된 경우에만 이벤트 발행
+        // Redis 캐시에 Write-Through로 좋아요 수 감소
         if (deletedRows > 0) {
-            productRepository.decrementLikeCount(productId);
-            // 이벤트 발행: ProductSummary 동기화를 위해
-            eventPublisher.publishEvent(new ProductLikeCountEvent(productId));
+                    eventPublisher.publishEvent(ProductEventDto.LikeCount.decrement(productId));
         }
     }
 }
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
index d7e053395..4f1e7d235 100644
--- 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
@@ -20,7 +20,6 @@ public class Product extends BaseEntity {
     private String name;
     private String description;
     private BigDecimal price;
-    private Long likeCount;
     @Enumerated(EnumType.STRING)
     private ProductStatus status;
     private Boolean isVisible;
@@ -32,7 +31,6 @@ private Product(
         String name
         , String description
         , BigDecimal price
-        , Long likeCount
         , ProductStatus status
         , Boolean isVisible
         , Boolean isSellable
@@ -41,7 +39,6 @@ private Product(
         this.name = name;
         this.description = description;
         this.price = price;
-        this.likeCount = likeCount != null ? likeCount : 0L;
         this.status = status;
         this.isVisible = isVisible;
         this.isSellable = isSellable;
@@ -66,13 +63,6 @@ protected void guard() {
             throw new CoreException(ErrorType.BAD_REQUEST, "Product : price 는 음수가 될 수 없습니다.");
         }
 
-        // likeCount 유효성 검사
-        if(likeCount == null) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Product : likeCount 가 비어있을 수 없습니다.");
-        } else if(likeCount < 0) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "Product : likeCount 는 음수가 될 수 없습니다.");
-        }
-
         // status 유효성 검사
         if(status == null) {
             throw new CoreException(ErrorType.BAD_REQUEST, "Product : status 가 비어있을 수 없습니다.");
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
index c82887928..1c68c5a0d 100644
--- 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
@@ -1,5 +1,7 @@
 package com.loopers.domain.product;
 
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 
@@ -11,7 +13,5 @@ public interface ProductRepository {
     Optional<Product> findById(Long productId);
     Page<ProductView> findProductViews(ProductCondition condition, Pageable pageable);
     Optional<ProductView> findProductViewById(Long productId);
-    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
index 4fefecc3d..34a19f373 100644
--- 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
@@ -1,6 +1,8 @@
 package com.loopers.domain.product;
 
-import com.loopers.domain.product.event.ProductCreatedEvent;
+import com.loopers.domain.product.event.ProductEventDto;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.domain.Page;
@@ -18,15 +20,38 @@ public class ProductService {
     private final ApplicationEventPublisher eventPublisher;
 
     @Transactional
-    public Optional<Product> saveProduct(Product product) {
+    public Optional<Product> createProduct(Product product) {
         Optional<Product> saved = productRepository.save(product);
         saved.ifPresent(p -> {
             // 이벤트 발행: ProductView 생성을 위해
-            eventPublisher.publishEvent(new ProductCreatedEvent(p.getId(), p.getBrandId()));
+            eventPublisher.publishEvent(new ProductEventDto.Created(p.getId(), p.getBrandId()));
         });
         return saved;
     }
 
+    @Transactional
+    public Optional<Product> updateProduct(Product product) {
+        Optional<Product> saved = productRepository.save(product);
+        saved.ifPresent(p -> {
+            // 이벤트 발행: ProductView 업데이트를 위해
+            eventPublisher.publishEvent(new ProductEventDto.Updated(p.getId(), p.getBrandId()));
+        });
+        return saved;
+    }
+
+    @Transactional
+    public void deleteProduct(Long productId) {
+        Optional<Product> productOpt = productRepository.findById(productId);
+        productOpt.ifPresent(product -> {
+            // Soft Delete: BaseEntity의 delete() 메서드 사용
+            product.delete();
+            productRepository.save(product);
+            
+            // 이벤트 발행: ProductView 삭제 및 캐시 Evict를 위해
+            eventPublisher.publishEvent(new ProductEventDto.Deleted(productId));
+        });
+    }
+
     @Transactional(readOnly = true)
     public Optional<Product> findById(Long productId) {
         return productRepository.findById(productId);
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
deleted file mode 100644
index 2f24aff37..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductViewRepository.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.loopers.domain.product;
-
-import java.util.Optional;
-
-public interface ProductViewRepository {
-
-    Optional<ProductView> save(ProductView productView);
-    Optional<ProductView> findById(Long id);
-    void updateLikeCount(Long id, Long count);
-    void deleteById(Long id);
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/cache/ProductCacheService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/cache/ProductCacheService.java
new file mode 100644
index 000000000..d35e69796
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/cache/ProductCacheService.java
@@ -0,0 +1,537 @@
+package com.loopers.domain.product.cache;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.infrastructure.product.ProductQueryRepository;
+import com.loopers.support.config.CacheProperties;
+
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ProductCacheService {
+
+    private static final String PRODUCT_LIST_KEY_PREFIX = "products::list::";
+    private static final String PRODUCT_INFO_KEY_PREFIX = "product:info:";
+    private static final String PRODUCT_STAT_KEY_PREFIX = "product:stat:";
+    private static final String LIKE_COUNT_FIELD = "likeCount";
+
+    private final RedisTemplate<String, Object> productCacheRedisTemplate;
+    private final RedisTemplate<String, String> productListCacheRedisTemplate;
+    private final ProductQueryRepository productQueryRepository;
+    private final ProductViewRepository productViewRepository;
+    private final CacheProperties cacheProperties;
+    private final ObjectMapper objectMapper;
+
+    // 애플리케이션 시작 시 랜덤하게 선택된 Hot ID Set (초기화는 @PostConstruct에서)
+    private Set<Long> hotBrandIds;
+    private Set<Long> hotProductIds;
+
+    /**
+     * 애플리케이션 시작 시 Hot ID Set 초기화
+     */
+    @PostConstruct
+    public void initHotIds() {
+        hotBrandIds = generateRandomIds(
+                cacheProperties.hotTargets().brandIdRange().min(),
+                cacheProperties.hotTargets().brandIdRange().max(),
+                cacheProperties.hotTargets().brandIdRange().count()
+        );
+        hotProductIds = generateRandomIds(
+                cacheProperties.hotTargets().productIdRange().min(),
+                cacheProperties.hotTargets().productIdRange().max(),
+                cacheProperties.hotTargets().productIdRange().count()
+        );
+        log.info("Hot Brand IDs initialized: {} IDs in range [{}, {}]", 
+                hotBrandIds.size(), 
+                cacheProperties.hotTargets().brandIdRange().min(),
+                cacheProperties.hotTargets().brandIdRange().max());
+        log.info("Hot Product IDs initialized: {} IDs in range [{}, {}]", 
+                hotProductIds.size(),
+                cacheProperties.hotTargets().productIdRange().min(),
+                cacheProperties.hotTargets().productIdRange().max());
+    }
+
+    /**
+     * 범위에서 순차적으로 지정된 개수만큼 ID 선택 (테스트 가능성을 위해 랜덤 제거)
+     */
+    private Set<Long> generateRandomIds(Long min, Long max, Integer count) {
+        if (min == null || max == null || count == null || min > max || count <= 0) {
+            return Collections.emptySet();
+        }
+
+        long rangeSize = max - min + 1;
+        int actualCount = Math.min(count, (int) rangeSize); // 범위보다 많이 요청하면 전체 범위 사용
+
+        Set<Long> ids = new LinkedHashSet<>();
+        
+        // 순차적으로 ID 생성 (1부터 count까지)
+        for (long i = min; i < min + actualCount; i++) {
+            ids.add(i);
+        }
+
+        return Collections.unmodifiableSet(ids);
+    }
+
+    /**
+     * 상품 목록 조회 (Two-Tier Caching)
+     * L1: ID 리스트 캐싱
+     * L2: 상품 상세 정보 캐싱
+     */
+    public Page<ProductView> getProductViews(ProductCondition condition, Pageable pageable) {
+        // 1. L1: ID 리스트 조회 (캐시 키 생성)
+        String listKey = buildListKey(condition, pageable);
+        boolean isHotPage = isHotPage(condition, pageable);
+
+        List<Long> productIds = getProductIdsFromCache(listKey, condition, pageable, isHotPage);
+
+        if (productIds.isEmpty()) {
+            // 캐시 미스: DB에서 조회
+            Page<ProductView> dbPage = productQueryRepository.findProductViews(condition, pageable);
+            productIds = dbPage.getContent().stream()
+                    .map(ProductView::getId)
+                    .toList();
+
+            // Cold Page는 락 없이 캐시 저장
+            if (!isHotPage) {
+                saveProductIdsToCache(listKey, productIds);
+            } else {
+                // Hot Page는 PER 적용하여 저장
+                saveProductIdsToCacheWithPER(listKey, productIds);
+            }
+        }
+
+        // 2. L2: 상품 상세 정보 조회 (MGET)
+        List<ProductView> productViews = getProductViewsByIds(productIds);
+
+        // 3. 전체 개수 조회 (효율적인 count 쿼리 사용)
+        long total = productQueryRepository.countProductViews(condition);
+
+        return new PageImpl<>(productViews, pageable, total);
+    }
+
+    /**
+     * 상품 상세 조회
+     * Hot Product에는 PER(Probabilistic Early Recomputation) 적용
+     */
+    public Optional<ProductView> getProductView(Long productId) {
+        // L2 캐시에서 조회
+        String infoKey = PRODUCT_INFO_KEY_PREFIX + productId;
+        String statKey = PRODUCT_STAT_KEY_PREFIX + productId;
+
+        ProductView cached = getProductViewFromCache(infoKey, statKey);
+        if (cached != null) {
+            // Hot Product인 경우 PER 적용: 만료 임박 시 비동기로 갱신
+            if (isHotProduct(productId)) {
+                Long ttl = productCacheRedisTemplate.getExpire(infoKey);
+                if (ttl != null && ttl > 0 && shouldRefreshProductEarly(ttl)) {
+                    refreshProductViewCacheAsync(productId, infoKey, statKey);
+                }
+            }
+            return Optional.of(cached);
+        }
+
+        // 캐시 미스: DB에서 조회 후 캐시 저장
+        Optional<ProductView> dbView = productViewRepository.findById(productId);
+
+        if (dbView.isPresent()) {
+            ProductView productView = dbView.get();
+            saveProductViewToCache(productView, infoKey, statKey);
+            return Optional.of(productView);
+        }
+
+        return Optional.empty();
+    }
+
+    /**
+     * PER: 상품 상세 캐시 조기 갱신 여부 결정
+     * TTL이 짧아질수록 갱신 확률 증가
+     */
+    private boolean shouldRefreshProductEarly(Long remainingTtl) {
+        int baseTtl = cacheProperties.ttl().infoSeconds();
+        double probability = 1.0 - ((double) remainingTtl / baseTtl);
+        return ThreadLocalRandom.current().nextDouble() < probability;
+    }
+
+    /**
+     * 비동기로 상품 상세 캐시 갱신 (PER)
+     */
+    @Async
+    public void refreshProductViewCacheAsync(Long productId, String infoKey, String statKey) {
+        try {
+            Optional<ProductView> dbView = productViewRepository.findById(productId);
+            if (dbView.isPresent()) {
+                saveProductViewToCache(dbView.get(), infoKey, statKey);
+                log.debug("PER: Product cache refreshed early for productId={}", productId);
+            }
+        } catch (Exception e) {
+            log.warn("PER: Failed to refresh product cache early: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * L1: ID 리스트 캐시에서 조회
+     */
+    private List<Long> getProductIdsFromCache(String listKey, ProductCondition condition, Pageable pageable, boolean isHotPage) {
+        String cachedIdsJson = productListCacheRedisTemplate.opsForValue().get(listKey);
+        
+        if (cachedIdsJson != null) {
+            // PER 적용: Hot Page인 경우 확률적으로 조기 갱신
+            if (isHotPage) {
+                Long ttl = productListCacheRedisTemplate.getExpire(listKey);
+                if (ttl != null && ttl > 0 && shouldRefreshEarly(ttl)) {
+                    // 백그라운드에서 갱신 (비동기)
+                    refreshProductIdsCacheAsync(listKey, condition, pageable);
+                }
+            }
+
+            try {
+                return objectMapper.readValue(cachedIdsJson, new TypeReference<List<Long>>() {});
+            } catch (Exception e) {
+                log.warn("Failed to deserialize product IDs from cache: key={}", listKey, e);
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
+    /**
+     * PER: 확률적 조기 갱신 여부 결정
+     * TTL이 짧아질수록 갱신 확률 증가
+     */
+    private boolean shouldRefreshEarly(Long remainingTtl) {
+        int baseTtl = cacheProperties.ttl().listSeconds();
+        double probability = 1.0 - ((double) remainingTtl / baseTtl);
+        return ThreadLocalRandom.current().nextDouble() < probability;
+    }
+
+    /**
+     * 비동기로 ID 리스트 캐시 갱신 (PER - Probabilistic Early Recomputation)
+     * @Async를 사용하여 유저 응답 지연 없이 백그라운드에서 갱신
+     */
+    @Async
+    public void refreshProductIdsCacheAsync(String listKey, ProductCondition condition, Pageable pageable) {
+        try {
+            Page<ProductView> dbPage = productQueryRepository.findProductViews(condition, pageable);
+            List<Long> productIds = dbPage.getContent().stream()
+                    .map(ProductView::getId)
+                    .toList();
+            saveProductIdsToCacheWithPER(listKey, productIds);
+            log.debug("PER: Cache refreshed early for key={}", listKey);
+        } catch (Exception e) {
+            log.warn("PER: Failed to refresh cache early: key={}", listKey, e);
+        }
+    }
+
+    /**
+     * ID 리스트를 캐시에 저장 (Cold Page)
+     */
+    private void saveProductIdsToCache(String listKey, List<Long> productIds) {
+        try {
+            String json = objectMapper.writeValueAsString(productIds);
+            productListCacheRedisTemplate.opsForValue().set(
+                    listKey,
+                    json,
+                    Duration.ofSeconds(cacheProperties.ttl().listSeconds())
+            );
+        } catch (Exception e) {
+            log.warn("Failed to cache product IDs: key={}", listKey, e);
+        }
+    }
+
+    /**
+     * ID 리스트를 캐시에 저장 (Hot Page, PER 적용)
+     */
+    private void saveProductIdsToCacheWithPER(String listKey, List<Long> productIds) {
+        try {
+            String json = objectMapper.writeValueAsString(productIds);
+            // PER: TTL에 랜덤 요소 추가하여 동시 만료 방지
+            int baseTtl = cacheProperties.ttl().listSeconds();
+            int randomOffset = ThreadLocalRandom.current().nextInt(0, 60); // 0~60초 랜덤
+            productListCacheRedisTemplate.opsForValue().set(
+                    listKey,
+                    json,
+                    Duration.ofSeconds(baseTtl + randomOffset)
+            );
+        } catch (Exception e) {
+            log.warn("Failed to cache product IDs with PER: key={}", listKey, e);
+        }
+    }
+
+    /**
+     * L2: 상품 상세 정보 조회 (MGET)
+     */
+    private List<ProductView> getProductViewsByIds(List<Long> productIds) {
+        if (productIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // MGET으로 Info를 한번에 조회
+        List<String> infoKeys = productIds.stream()
+                .map(id -> PRODUCT_INFO_KEY_PREFIX + id)
+                .toList();
+
+        List<Object> infoValues = productCacheRedisTemplate.opsForValue().multiGet(infoKeys);
+        
+        // Stat은 각 키마다 개별 조회 (Hash 구조 특성상)
+        List<Object> statValues = new ArrayList<>();
+        for (Long productId : productIds) {
+            String statKey = PRODUCT_STAT_KEY_PREFIX + productId;
+            Object statValue = productCacheRedisTemplate.opsForHash().get(statKey, LIKE_COUNT_FIELD);
+            statValues.add(statValue);
+        }
+
+        // 캐시 미스인 항목들을 수집
+        List<Long> missIds = new ArrayList<>();
+        List<ProductView> result = new ArrayList<>();
+
+        for (int i = 0; i < productIds.size(); i++) {
+            Long productId = productIds.get(i);
+            Object infoValue = infoValues != null && i < infoValues.size() ? infoValues.get(i) : null;
+            Object statValue = statValues != null && i < statValues.size() ? statValues.get(i) : null;
+
+            if (infoValue != null) {
+                try {
+                    ProductView productView = objectMapper.convertValue(infoValue, ProductView.class);
+                    
+                    // Stat에서 좋아요 수 가져오기
+                    if (statValue != null) {
+                        Long likeCount = parseLikeCount(statValue);
+                        if (likeCount != null) {
+                            productView.updateLikeCount(likeCount);
+                        }
+                    } else {
+                        // Stat이 없으면 DB에서 조회하여 채우기 (Backfill)
+                        Long likeCount = getLikeCountFromDb(productId);
+                        if (likeCount != null) {
+                            productView.updateLikeCount(likeCount);
+                            // Stat 캐시에 저장
+                            saveStatToCache(productId, likeCount);
+                        }
+                    }
+
+                    result.add(productView);
+                } catch (Exception e) {
+                    log.warn("Failed to deserialize ProductView from cache: productId={}", productId, e);
+                    missIds.add(productId);
+                }
+            } else {
+                missIds.add(productId);
+            }
+        }
+
+        // 캐시 미스 항목들을 DB에서 조회하여 채우기 (Backfill)
+        if (!missIds.isEmpty()) {
+            List<ProductView> dbViews = getProductViewsFromDb(missIds);
+            for (ProductView dbView : dbViews) {
+                String infoKey = PRODUCT_INFO_KEY_PREFIX + dbView.getId();
+                String statKey = PRODUCT_STAT_KEY_PREFIX + dbView.getId();
+                saveProductViewToCache(dbView, infoKey, statKey);
+                result.add(dbView);
+            }
+        }
+
+        // 원래 순서 유지
+        Map<Long, ProductView> viewMap = result.stream()
+                .collect(Collectors.toMap(ProductView::getId, v -> v));
+        return productIds.stream()
+                .map(viewMap::get)
+                .filter(Objects::nonNull)
+                .toList();
+    }
+
+    /**
+     * 단일 상품 상세 정보 캐시에서 조회
+     */
+    private ProductView getProductViewFromCache(String infoKey, String statKey) {
+        Object infoValue = productCacheRedisTemplate.opsForValue().get(infoKey);
+        if (infoValue == null) {
+            return null;
+        }
+
+        try {
+            ProductView productView = objectMapper.convertValue(infoValue, ProductView.class);
+            
+            // Stat에서 좋아요 수 가져오기
+            Object statValue = productCacheRedisTemplate.opsForHash().get(statKey, LIKE_COUNT_FIELD);
+            if (statValue != null) {
+                Long likeCount = parseLikeCount(statValue);
+                if (likeCount != null) {
+                    productView.updateLikeCount(likeCount);
+                }
+            }
+
+            return productView;
+        } catch (Exception e) {
+            log.warn("Failed to deserialize ProductView from cache: key={}", infoKey, e);
+            return null;
+        }
+    }
+
+    /**
+     * 상품 상세 정보를 캐시에 저장
+     */
+    private void saveProductViewToCache(ProductView productView, String infoKey, String statKey) {
+        try {
+            // Info 저장 (긴 TTL)
+            productCacheRedisTemplate.opsForValue().set(
+                    infoKey,
+                    productView,
+                    Duration.ofSeconds(cacheProperties.ttl().infoSeconds())
+            );
+
+            // Stat 저장 (짧은 TTL, PER 적용) - String.valueOf()로 직렬화
+            int baseTtl = cacheProperties.ttl().statSeconds();
+            int randomOffset = ThreadLocalRandom.current().nextInt(0, 60);
+            productCacheRedisTemplate.opsForHash().put(statKey, LIKE_COUNT_FIELD, String.valueOf(productView.getLikeCount()));
+            productCacheRedisTemplate.expire(statKey, Duration.ofSeconds(baseTtl + randomOffset));
+        } catch (Exception e) {
+            log.warn("Failed to cache ProductView: productId={}", productView.getId(), e);
+        }
+    }
+
+    /**
+     * Stat만 캐시에 저장
+     */
+    private void saveStatToCache(Long productId, Long likeCount) {
+        try {
+            String statKey = PRODUCT_STAT_KEY_PREFIX + productId;
+            int baseTtl = cacheProperties.ttl().statSeconds();
+            int randomOffset = ThreadLocalRandom.current().nextInt(0, 60);
+            // String.valueOf()로 직렬화
+            productCacheRedisTemplate.opsForHash().put(statKey, LIKE_COUNT_FIELD, String.valueOf(likeCount));
+            productCacheRedisTemplate.expire(statKey, Duration.ofSeconds(baseTtl + randomOffset));
+        } catch (Exception e) {
+            log.warn("Failed to cache stat: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * DB에서 상품 상세 정보 조회
+     */
+    private List<ProductView> getProductViewsFromDb(List<Long> productIds) {
+        if (productIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // ProductViewRepository를 통해 IN 쿼리로 한번에 조회
+        return productViewRepository.findByIds(productIds);
+    }
+
+    /**
+     * DB에서 좋아요 수 조회
+     */
+    private Long getLikeCountFromDb(Long productId) {
+        // ProductViewRepository에서 조회
+        return productViewRepository.findById(productId)
+                .map(ProductView::getLikeCount)
+                .orElse(0L);
+    }
+
+    /**
+     * 좋아요 수 파싱
+     */
+    private Long parseLikeCount(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Long) {
+            return (Long) value;
+        }
+        if (value instanceof Integer) {
+            return ((Integer) value).longValue();
+        }
+        if (value instanceof String) {
+            try {
+                return Long.parseLong((String) value);
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Hot Page 여부 확인
+     * application.yml의 hot-page-range 설정에 따라 Hot 페이지 범위 결정
+     * 메인 페이지(brandId 없음) 또는 Hot Brand의 페이지만 Hot으로 처리
+     */
+    private boolean isHotPage(ProductCondition condition, Pageable pageable) {
+        Integer hotPageRange = cacheProperties.hotTargets().hotPageRange();
+        if (hotPageRange == null || hotPageRange <= 0) {
+            return false;
+        }
+
+        // 페이지 범위 확인 (1~N 페이지, page 0 ~ N-1)
+        int pageNumber = pageable.getPageNumber();
+        if (pageNumber < 0 || pageNumber >= hotPageRange) {
+            return false;
+        }
+
+        // 메인 페이지(brandId 없음)는 항상 Hot
+        if (condition.brandId() == null) {
+            return true;
+        }
+
+        // 브랜드 페이지는 Hot Brand인 경우만 Hot
+        return isHotBrand(condition.brandId());
+    }
+
+    /**
+     * Hot Brand 여부 확인
+     * application.yml의 brand-id-range 설정에 따라 판단
+     */
+    public boolean isHotBrand(Long brandId) {
+        if (brandId == null || hotBrandIds == null) {
+            return false;
+        }
+        return hotBrandIds.contains(brandId);
+    }
+
+    /**
+     * Hot Product 여부 확인
+     * application.yml의 product-id-range 설정에 따라 판단
+     */
+    public boolean isHotProduct(Long productId) {
+        if (productId == null || hotProductIds == null) {
+            return false;
+        }
+        return hotProductIds.contains(productId);
+    }
+
+
+    /**
+     * 목록 캐시 키 생성
+     */
+    private String buildListKey(ProductCondition condition, Pageable pageable) {
+        StringBuilder key = new StringBuilder(PRODUCT_LIST_KEY_PREFIX);
+        
+        if (condition.brandId() != null) {
+            key.append("brand:").append(condition.brandId()).append("::");
+        }
+        
+        key.append("sort:").append(condition.sort() != null ? condition.sort() : "latest").append("::");
+        key.append("page:").append(pageable.getPageNumber()).append("::");
+        key.append("size:").append(pageable.getPageSize());
+        
+        return key.toString();
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
deleted file mode 100644
index 9739e2c2a..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductCreatedEvent.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.loopers.domain.product.event;
-
-public record ProductCreatedEvent(Long productId, Long brandId) {
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
deleted file mode 100644
index e2b21b0b0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductDeletedEvent.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.loopers.domain.product.event;
-
-public record ProductDeletedEvent(Long productId) {
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductEventDto.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductEventDto.java
new file mode 100644
index 000000000..36dc0d652
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductEventDto.java
@@ -0,0 +1,46 @@
+package com.loopers.domain.product.event;
+
+/**
+ * Product 관련 이벤트 DTO
+ * 모든 Product 이벤트를 하나의 클래스로 통합 관리
+ */
+public class ProductEventDto {
+
+    /**
+     * 상품 생성 이벤트
+     */
+    public record Created(Long productId, Long brandId) {
+    }
+
+    /**
+     * 상품 수정 이벤트
+     */
+    public record Updated(Long productId, Long brandId) {
+    }
+
+    /**
+     * 상품 삭제 이벤트
+     */
+    public record Deleted(Long productId) {
+    }
+
+    /**
+     * 상품 좋아요 수 변경 이벤트
+     */
+    public record LikeCount(Long productId, long delta) {
+        /**
+         * 좋아요 증가 (+1)
+         */
+        public static LikeCount increment(Long productId) {
+            return new LikeCount(productId, 1L);
+        }
+
+        /**
+         * 좋아요 감소 (-1)
+         */
+        public static LikeCount decrement(Long productId) {
+            return new LikeCount(productId, -1L);
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
deleted file mode 100644
index ba46eba5e..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductLikeCountEvent.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.loopers.domain.product.event;
-
-public record ProductLikeCountEvent(Long productId) {
-}
-
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewEventHandler.java
new file mode 100644
index 000000000..aa1ff57ff
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/event/ProductViewEventHandler.java
@@ -0,0 +1,187 @@
+package com.loopers.domain.product.event;
+
+import com.loopers.domain.brand.BrandRepository;
+import com.loopers.domain.like.LikeRepository;
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductRepository;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class ProductViewEventHandler {
+
+    private static final String PRODUCT_INFO_KEY_PREFIX = "product:info:";
+    private static final String PRODUCT_STAT_KEY_PREFIX = "product:stat:";
+    private static final String LIKE_COUNT_FIELD = "likeCount";
+
+    private final ProductViewRepository productViewRepository;
+    private final LikeRepository likeRepository;
+    private final ProductRepository productRepository;
+    private final BrandRepository brandRepository;
+    private final RedisTemplate<String, Object> productCacheRedisTemplate;
+
+    /**
+     * 좋아요 수 변경 이벤트 처리
+     * Write-Through 전략: Redis 캐시가 존재할 때만 원자적으로 증가/감소, DB에도 반영
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void handleProductLikeCountEvent(ProductEventDto.LikeCount event) {
+        Long productId = event.productId();
+        long delta = event.delta();
+        
+        try {
+            // Redis 캐시 존재 여부 먼저 확인 (Write-Through: 캐시 없으면 무시)
+            String statKey = PRODUCT_STAT_KEY_PREFIX + productId;
+            Object existingValue = productCacheRedisTemplate.opsForHash().get(statKey, LIKE_COUNT_FIELD);
+            
+            if (existingValue != null) {
+                // 캐시가 존재하면 값을 가져와서 증가/감소 후 다시 저장 (String 직렬화 일관성 유지)
+                long currentCount = Long.parseLong(existingValue.toString());
+                long newCount = Math.max(0, currentCount + delta); // 음수 방지
+                productCacheRedisTemplate.opsForHash().put(statKey, LIKE_COUNT_FIELD, String.valueOf(newCount));
+                log.debug("Redis cache likeCount updated: productId={}, delta={}, newCount={}", productId, delta, newCount);
+            } else {
+                log.debug("Redis cache not found for likeCount: productId={}, skipping cache update", productId);
+            }
+
+            // DB에도 반영 (정합성 확보)
+            long realCount = likeRepository.countByProductId(productId);
+            productViewRepository.updateLikeCount(productId, realCount);
+            
+            log.debug("ProductView likeCount updated: productId={}, likeCount={}", productId, realCount);
+        } catch (Exception e) {
+            log.error("Failed to update ProductView likeCount: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 생성 이벤트 처리
+     * Write-Around 전략: 캐시에 직접 쓰지 않음, TTL 끝나고 다시 가져오면 그때 반영
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void handleProductCreatedEvent(ProductEventDto.Created event) {
+        Long productId = event.productId();
+        Long brandId = event.brandId();
+        
+        try {
+            // Product 조회
+            Product product = productRepository.findById(productId)
+                    .orElseThrow(() -> new IllegalStateException("Product not found: " + productId));
+
+            // Brand 조회
+            String brandName = null;
+            if (brandId != null) {
+                brandName = brandRepository.findById(brandId)
+                        .map(brand -> brand.getName())
+                        .orElse(null);
+            }
+
+            // ProductView 생성
+            ProductView productView = ProductView.builder()
+                    .id(product.getId())
+                    .name(product.getName())
+                    .price(product.getPrice())
+                    .likeCount(0L)  // 새로 생성된 상품이므로 좋아요 수는 0
+                    .brandId(brandId)
+                    .brandName(brandName)
+                    .status(product.getStatus())
+                    .createdAt(product.getCreatedAt())
+                    .build();
+
+            productViewRepository.save(productView);
+            
+            // Write-Around: 캐시에 직접 쓰지 않음 (TTL 끝나고 다시 가져오면 그때 반영)
+            log.debug("ProductView created (Write-Around): productId={}, brandId={}", productId, brandId);
+        } catch (Exception e) {
+            log.error("Failed to create ProductView: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 수정 이벤트 처리
+     * Evict 전략: 캐시가 있으면 삭제 (다음 조회 시 최신 데이터로 갱신)
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void handleProductUpdatedEvent(ProductEventDto.Updated event) {
+        Long productId = event.productId();
+        Long brandId = event.brandId();
+
+        try {
+            // Product 조회
+            Product product = productRepository.findById(productId)
+                    .orElseThrow(() -> new IllegalStateException("Product not found: " + productId));
+
+            // Brand 조회
+            String brandName = null;
+            if (brandId != null) {
+                brandName = brandRepository.findById(brandId)
+                        .map(brand -> brand.getName())
+                        .orElse(null);
+            }
+
+            // ProductView 업데이트
+            productViewRepository.update(
+                    productId,
+                    product.getName(),
+                    product.getPrice(),
+                    brandId,
+                    brandName,
+                    product.getStatus()
+            );
+
+            // Evict: 캐시가 있으면 삭제 (캐시 없으면 무시)
+            String infoKey = PRODUCT_INFO_KEY_PREFIX + productId;
+            Boolean deleted = productCacheRedisTemplate.delete(infoKey);
+            if (Boolean.TRUE.equals(deleted)) {
+                log.debug("Redis cache evicted: productId={}, key={}", productId, infoKey);
+            }
+
+            log.debug("ProductView updated: productId={}, brandId={}", productId, brandId);
+        } catch (Exception e) {
+            log.error("Failed to update ProductView: productId={}", productId, e);
+        }
+    }
+
+    /**
+     * 상품 삭제 이벤트 처리 (Soft Delete)
+     * Evict 전략: 캐시 삭제
+     */
+    @Async
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void handleProductDeletedEvent(ProductEventDto.Deleted event) {
+        Long productId = event.productId();
+
+        try {
+            productViewRepository.deleteById(productId);
+            
+            // Evict: 캐시 삭제
+            String infoKey = PRODUCT_INFO_KEY_PREFIX + productId;
+            String statKey = PRODUCT_STAT_KEY_PREFIX + productId;
+            productCacheRedisTemplate.delete(infoKey);
+            productCacheRedisTemplate.delete(statKey);
+            
+            log.debug("ProductView deleted and cache evicted: productId={}", productId);
+        } catch (Exception e) {
+            log.error("Failed to delete ProductView: productId={}", productId, e);
+        }
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductCondition.java
similarity index 80%
rename from apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java
rename to apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductCondition.java
index c5b2d019d..408773faa 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductCondition.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductCondition.java
@@ -1,4 +1,4 @@
-package com.loopers.domain.product;
+package com.loopers.domain.product.view;
 
 import java.time.ZonedDateTime;
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductView.java
similarity index 68%
rename from apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
rename to apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductView.java
index 1f7522fb9..bd764f10e 100644
--- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductView.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductView.java
@@ -1,5 +1,6 @@
-package com.loopers.domain.product;
+package com.loopers.domain.product.view;
 
+import com.loopers.domain.product.ProductStatus;
 import jakarta.persistence.*;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
@@ -12,8 +13,12 @@
 
 @Entity
 @Table(name = "product_view", indexes = {
+    @Index(name = "idx_ps_latest", columnList = "createdAt DESC"),
     @Index(name = "idx_ps_like_desc", columnList = "likeCount DESC"),
-    @Index(name = "idx_ps_brand_like", columnList = "brandId, likeCount DESC")
+    @Index(name = "idx_ps_price_asc", columnList = "price ASC"),
+    @Index(name = "idx_ps_brand_latest", columnList = "brandId, createdAt DESC"),
+    @Index(name = "idx_ps_brand_like_desc", columnList = "brandId, likeCount DESC"),
+    @Index(name = "idx_ps_brand_price_asc", columnList = "brandId, price ASC")
 })
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewRepository.java
new file mode 100644
index 000000000..130e9302f
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewRepository.java
@@ -0,0 +1,18 @@
+package com.loopers.domain.product.view;
+
+import com.loopers.domain.product.ProductStatus;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Optional;
+
+public interface ProductViewRepository {
+
+    Optional<ProductView> save(ProductView productView);
+    Optional<ProductView> findById(Long id);
+    List<ProductView> findByIds(List<Long> ids);
+    void updateLikeCount(Long id, Long count);
+    void update(Long id, String name, BigDecimal price, Long brandId, String brandName, ProductStatus status);
+    void deleteById(Long id);
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewSyncScheduler.java
similarity index 93%
rename from apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java
rename to apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewSyncScheduler.java
index 98fe084f2..2dee5e172 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductViewSyncScheduler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/view/ProductViewSyncScheduler.java
@@ -1,10 +1,8 @@
-package com.loopers.application.product;
+package com.loopers.domain.product.view;
 
 import com.loopers.domain.brand.QBrand;
 import com.loopers.domain.like.LikeRepository;
-import com.loopers.domain.product.ProductView;
 import com.loopers.domain.product.QProduct;
-import com.loopers.domain.product.QProductView;
 import com.loopers.infrastructure.product.ProductViewJpaRepository;
 import com.querydsl.core.Tuple;
 import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -20,7 +18,7 @@
  * ProductView 테이블 동기화 스케줄러
  * 
  * 하이브리드 방식:
- * - 평소: 이벤트 기반 실시간 동기화 (ProductCreatedEvent, ProductDeletedEvent, ProductLikeCountEvent)
+ * - 평소: 이벤트 기반 실시간 동기화 (ProductEventDto.Created, ProductEventDto.Deleted, ProductEventDto.LikeCount)
  * - 주기적: 전체 재생성으로 정합성 보장 및 누락된 동기화 보정
  */
 @Slf4j
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
index 9664b9347..9c223330c 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductQueryRepository.java
@@ -1,9 +1,8 @@
 package com.loopers.infrastructure.product;
 
-import com.loopers.domain.product.ProductCondition;
-import com.loopers.domain.product.ProductView;
-import com.loopers.domain.product.QProduct;
-import com.loopers.domain.product.QProductView;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.QProductView;
 import com.querydsl.core.BooleanBuilder;
 import com.querydsl.core.types.OrderSpecifier;
 import com.querydsl.jpa.impl.JPAQueryFactory;
@@ -21,7 +20,6 @@
 public class ProductQueryRepository {
 
     private final JPAQueryFactory queryFactory;
-    private final QProduct product = QProduct.product;
     private final QProductView productView = QProductView.productView;
 
     /**
@@ -68,34 +66,22 @@ public Page<ProductView> findProductViews(ProductCondition condition, Pageable p
     }
 
     /**
-     * 상품 좋아요 수 증가 (동시성 안전)
-     * 원자적으로 like_count를 1 증가시킴
-     * 
-     * @param productId 상품 ID
+     * ProductView 전체 개수만 조회 (효율적인 count 쿼리)
      */
-    public void incrementLikeCount(Long productId) {
-        queryFactory
-                .update(product)
-                .set(product.likeCount, product.likeCount.add(1L))
-                .where(product.id.eq(productId))
-                .execute();
-    }
-
-    /**
-     * 상품 좋아요 수 감소 (동시성 안전)
-     * 원자적으로 like_count를 1 감소시킴 (0 미만으로는 감소하지 않음)
-     * 
-     * @param productId 상품 ID
-     */
-    public void decrementLikeCount(Long productId) {
-        queryFactory
-                .update(product)
-                .set(product.likeCount, product.likeCount.subtract(1L))
-                .where(product.id.eq(productId)
-                        .and(product.likeCount.gt(0L)))
-                .execute();
+    public long countProductViews(ProductCondition condition) {
+        BooleanBuilder whereCondition = new BooleanBuilder();
+        
+        if (condition.brandId() != null) {
+            whereCondition.and(productView.brandId.eq(condition.brandId()));
+        }
+        
+        Long total = queryFactory
+                .select(productView.count())
+                .from(productView)
+                .where(whereCondition)
+                .fetchOne();
+        
+        return total != null ? total : 0L;
     }
-
-
 }
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
index 8888232b4..a2d71eb6f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
@@ -1,10 +1,10 @@
 package com.loopers.infrastructure.product;
 
 import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.view.ProductCondition;
 import com.loopers.domain.product.ProductRepository;
-import com.loopers.domain.product.ProductView;
-import com.loopers.domain.product.ProductViewRepository;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
@@ -40,14 +40,4 @@ public Page<ProductView> findProductViews(ProductCondition condition, Pageable p
     public Optional<ProductView> findProductViewById(Long productId) {
         return productViewRepository.findById(productId);
     }
-
-    @Override
-    public void incrementLikeCount(Long productId) {
-        productQueryRepository.incrementLikeCount(productId);
-    }
-
-    @Override
-    public void decrementLikeCount(Long productId) {
-        productQueryRepository.decrementLikeCount(productId);
-    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
index 496930238..7315ca7d6 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewJpaRepository.java
@@ -1,6 +1,6 @@
 package com.loopers.infrastructure.product;
 
-import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.view.ProductView;
 import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface ProductViewJpaRepository extends JpaRepository<ProductView, Long> {
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
index 157ce79ef..1d298fd3e 100644
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
+++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductViewRepositoryImpl.java
@@ -1,13 +1,16 @@
 package com.loopers.infrastructure.product;
 
-import com.loopers.domain.product.ProductView;
-import com.loopers.domain.product.ProductViewRepository;
-import com.loopers.domain.product.QProductView;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.domain.product.view.QProductView;
 import com.querydsl.jpa.impl.JPAQueryFactory;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.util.List;
 import java.util.Optional;
 
 @RequiredArgsConstructor
@@ -30,6 +33,19 @@ public Optional<ProductView> findById(Long id) {
         return productViewJpaRepository.findById(id);
     }
 
+    @Override
+    @Transactional(readOnly = true)
+    public List<ProductView> findByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return List.of();
+        }
+        QProductView productView = QProductView.productView;
+        return queryFactory
+                .selectFrom(productView)
+                .where(productView.id.in(ids))
+                .fetch();
+    }
+
     @Override
     @Transactional
     public void updateLikeCount(Long id, Long count) {
@@ -41,6 +57,21 @@ public void updateLikeCount(Long id, Long count) {
                 .execute();
     }
 
+    @Override
+    @Transactional
+    public void update(Long id, String name, BigDecimal price, Long brandId, String brandName, ProductStatus status) {
+        QProductView productView = QProductView.productView;
+        queryFactory
+                .update(productView)
+                .set(productView.name, name)
+                .set(productView.price, price)
+                .set(productView.brandId, brandId)
+                .set(productView.brandName, brandName)
+                .set(productView.status, status)
+                .where(productView.id.eq(id))
+                .execute();
+    }
+
     @Override
     @Transactional
     public void deleteById(Long id) {
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
index 1f3552e50..654a1b71b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductController.java
@@ -7,8 +7,8 @@
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.web.bind.annotation.*;
-import com.loopers.domain.product.ProductCondition;
-import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
 
 @RequiredArgsConstructor
 @RestController
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
index e3e451f17..8e33d293e 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductDto.java
@@ -1,8 +1,8 @@
 package com.loopers.interfaces.api.product;
 
-import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.view.ProductCondition;
 import com.loopers.domain.product.ProductStatus;
-import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.view.ProductView;
 import lombok.Builder;
 import org.springframework.data.domain.Page;
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/config/CacheConfig.java b/apps/commerce-api/src/main/java/com/loopers/support/config/CacheConfig.java
new file mode 100644
index 000000000..e95df1391
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/support/config/CacheConfig.java
@@ -0,0 +1,62 @@
+package com.loopers.support.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@EnableConfigurationProperties(CacheProperties.class)
+public class CacheConfig {
+
+    /**
+     * RedisTemplate for caching ProductInfo objects (JSON serialization)
+     * Uses master connection for write operations
+     */
+    @Bean
+    public RedisTemplate<String, Object> productCacheRedisTemplate(
+            @Qualifier("redisConnectionMaster") LettuceConnectionFactory connectionFactory,
+            ObjectMapper objectMapper
+    ) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+        
+        StringRedisSerializer stringSerializer = new StringRedisSerializer();
+        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
+        
+        template.setKeySerializer(stringSerializer);
+        template.setValueSerializer(jsonSerializer);
+        template.setHashKeySerializer(stringSerializer);
+        template.setHashValueSerializer(stringSerializer);
+        
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    /**
+     * RedisTemplate for caching product ID lists (String serialization)
+     * Uses master connection for write operations
+     */
+    @Bean
+    public RedisTemplate<String, String> productListCacheRedisTemplate(
+            @Qualifier("redisConnectionMaster") LettuceConnectionFactory connectionFactory
+    ) {
+        RedisTemplate<String, String> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+        
+        StringRedisSerializer serializer = new StringRedisSerializer();
+        template.setKeySerializer(serializer);
+        template.setValueSerializer(serializer);
+        template.setHashKeySerializer(serializer);
+        template.setHashValueSerializer(serializer);
+        
+        template.afterPropertiesSet();
+        return template;
+    }
+}
+
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/config/CacheProperties.java b/apps/commerce-api/src/main/java/com/loopers/support/config/CacheProperties.java
new file mode 100644
index 000000000..ca6a67ff8
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/support/config/CacheProperties.java
@@ -0,0 +1,28 @@
+package com.loopers.support.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "app.cache")
+public record CacheProperties(
+        HotTargets hotTargets,
+        Ttl ttl
+) {
+    public record HotTargets(
+            IdRange brandIdRange,
+            IdRange productIdRange,
+            Integer hotPageRange
+    ) {
+        public record IdRange(
+                Long min,
+                Long max,
+                Integer count
+        ) {}
+    }
+
+    public record Ttl(
+            int listSeconds,
+            int infoSeconds,
+            int statSeconds
+    ) {}
+}
+
diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml
index 484c070d0..e72178404 100644
--- a/apps/commerce-api/src/main/resources/application.yml
+++ b/apps/commerce-api/src/main/resources/application.yml
@@ -29,6 +29,23 @@ springdoc:
   swagger-ui:
     path: /swagger-ui.html
 
+app:
+  cache:
+    hot-targets:
+      brand-id-range:
+        min: 1
+        max: 10000
+        count: 500  # Hot 브랜드 ID 개수 (min~max 범위에서 랜덤 선택)
+      product-id-range:
+        min: 1
+        max: 100000
+        count: 5000  # Hot 상품 ID 개수 (min~max 범위에서 랜덤 선택)
+      hot-page-range: 3  # Hot Page 범위 (1~N 페이지, 0-based이므로 0~N-1 페이지가 Hot)
+    ttl:
+      list-seconds: 300  # 목록 캐시 TTL (5분)
+      info-seconds: 3600  # 상품 정보 캐시 TTL (1시간)
+      stat-seconds: 600  # 통계 캐시 TTL (10분)
+
 ---
 spring:
   config:
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
index 79ddb3e8e..2c28a1caa 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java
@@ -8,6 +8,8 @@
 import com.loopers.domain.product.Product;
 import com.loopers.domain.product.ProductRepository;
 import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
 import com.loopers.domain.user.Gender;
 import com.loopers.domain.user.UserService;
 import com.loopers.infrastructure.like.LikeJpaRepository;
@@ -53,6 +55,9 @@ class LikeFacadeTest {
     @Autowired
     private ProductRepository productRepository;
 
+    @Autowired
+    private ProductViewRepository productViewRepository;
+
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
@@ -89,9 +94,10 @@ void saveProductLike_withValidUserAndProduct_Success() {
             assertEquals(productId, savedLike.get().getLikeId().getLikeTargetId());
             assertEquals(LikeTargetType.PRODUCT, savedLike.get().getLikeId().getLikeTargetType());
             
-            // Product.likeCount가 1 증가했는지 확인
-            Product savedProduct = productRepository.findById(productId).orElseThrow();
-            assertEquals(1L, savedProduct.getLikeCount(), "좋아요 등록 시 likeCount가 1 증가해야 합니다.");
+            // ProductView.likeCount가 1 증가했는지 확인 (비동기 이벤트 완료 대기)
+            waitForProductViewLikeCountUpdate(productId, 1L);
+            ProductView productView = productViewRepository.findById(productId).orElseThrow();
+            assertEquals(1L, productView.getLikeCount(), "좋아요 등록 시 ProductView의 likeCount가 1 증가해야 합니다.");
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 사용자로 좋아요 등록 시 NOT_FOUND 예외 발생")
@@ -123,8 +129,9 @@ void saveProductLike_duplicateLike_Idempotent() {
 
             // act
             likeFacade.saveProductLike(userId, productId);
-            Product productAfterFirst = productRepository.findById(productId).orElseThrow();
-            Long likeCountAfterFirst = productAfterFirst.getLikeCount();
+            waitForProductViewLikeCountUpdate(productId, 1L);
+            ProductView productViewAfterFirst = productViewRepository.findById(productId).orElseThrow();
+            Long likeCountAfterFirst = productViewAfterFirst.getLikeCount();
             
             likeFacade.saveProductLike(userId, productId); // 중복 등록
 
@@ -142,9 +149,10 @@ void saveProductLike_duplicateLike_Idempotent() {
             assertThat(count).isEqualTo(1);
             
             // 멱등성: 중복 등록 시 likeCount가 증가하지 않아야 함
-            Product productAfterSecond = productRepository.findById(productId).orElseThrow();
-            assertEquals(likeCountAfterFirst, productAfterSecond.getLikeCount(), 
-                    "중복 등록 시 likeCount가 증가하지 않아야 합니다.");
+            waitForProductViewLikeCountUpdate(productId, 1L);
+            ProductView productViewAfterSecond = productViewRepository.findById(productId).orElseThrow();
+            assertEquals(likeCountAfterFirst, productViewAfterSecond.getLikeCount(), 
+                    "중복 등록 시 ProductView의 likeCount가 증가하지 않아야 합니다.");
         }
 
         @DisplayName("동시성 테스트: 동일한 좋아요 요청이 동시에 와도 likeCount는 한 번만 증가해야 한다")
@@ -201,10 +209,11 @@ void saveProductLike_concurrentRequests_Idempotent() throws InterruptedException
                     .count();
             assertThat(count).isEqualTo(1);
             
-            // Product.likeCount가 1만 증가했는지 확인
-            Product productAfterConcurrent = productRepository.findById(productId).orElseThrow();
-            assertEquals(1L, productAfterConcurrent.getLikeCount(), 
-                    "동시성 요청 시 likeCount가 1 증가해야 합니다.");
+            // ProductView.likeCount가 1만 증가했는지 확인 (비동기 이벤트 완료 대기)
+            waitForProductViewLikeCountUpdate(productId, 1L);
+            ProductView productViewAfterConcurrent = productViewRepository.findById(productId).orElseThrow();
+            assertEquals(1L, productViewAfterConcurrent.getLikeCount(), 
+                    "동시성 요청 시 ProductView의 likeCount가 1 증가해야 합니다.");
         }
 
     }
@@ -220,9 +229,10 @@ void deleteProductLike_withExistingLike_Success() {
             Long userId = createAndSaveUser(validLoginId);
             Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 0L);
             likeFacade.saveProductLike(userId, productId);
+            waitForProductViewLikeCountUpdate(productId, 1L);
             
-            Product productBeforeDelete = productRepository.findById(productId).orElseThrow();
-            Long likeCountBeforeDelete = productBeforeDelete.getLikeCount();
+            ProductView productViewBeforeDelete = productViewRepository.findById(productId).orElseThrow();
+            Long likeCountBeforeDelete = productViewBeforeDelete.getLikeCount();
 
             // act
             likeFacade.deleteProductLike(userId, productId);
@@ -233,10 +243,11 @@ void deleteProductLike_withExistingLike_Success() {
             );
             assertFalse(deletedLike.isPresent());
             
-            // Product.likeCount가 1 감소했는지 확인
-            Product productAfterDelete = productRepository.findById(productId).orElseThrow();
-            assertEquals(likeCountBeforeDelete - 1, productAfterDelete.getLikeCount(), 
-                    "좋아요 삭제 시 likeCount가 1 감소해야 합니다.");
+            // ProductView.likeCount가 1 감소했는지 확인 (비동기 이벤트 완료 대기)
+            waitForProductViewLikeCountUpdate(productId, 0L);
+            ProductView productViewAfterDelete = productViewRepository.findById(productId).orElseThrow();
+            assertEquals(likeCountBeforeDelete - 1, productViewAfterDelete.getLikeCount(), 
+                    "좋아요 삭제 시 ProductView의 likeCount가 1 감소해야 합니다.");
         }
 
         @DisplayName("멱등성 테스트: 존재하지 않는 좋아요를 취소해도 예외 없이 처리")
@@ -246,8 +257,8 @@ void deleteProductLike_withNonExistentLike_Idempotent() {
             Long userId = createAndSaveUser(validLoginId);
             Long productId = createAndSaveProduct("테스트 상품", BigDecimal.valueOf(10000L), 5L);
             
-            Product productBeforeDelete = productRepository.findById(productId).orElseThrow();
-            Long likeCountBeforeDelete = productBeforeDelete.getLikeCount();
+            ProductView productViewBeforeDelete = productViewRepository.findById(productId).orElseThrow();
+            Long likeCountBeforeDelete = productViewBeforeDelete.getLikeCount();
 
             // act & assert - 예외 없이 처리되어야 함
             assertDoesNotThrow(() -> {
@@ -261,9 +272,10 @@ void deleteProductLike_withNonExistentLike_Idempotent() {
             assertFalse(deletedLike.isPresent());
             
             // 멱등성: 존재하지 않는 좋아요 삭제 시 likeCount가 변하지 않아야 함
-            Product productAfterDelete = productRepository.findById(productId).orElseThrow();
-            assertEquals(likeCountBeforeDelete, productAfterDelete.getLikeCount(), 
-                    "존재하지 않는 좋아요 삭제 시 likeCount가 변하지 않아야 합니다.");
+            // 이벤트가 발행되지 않으므로 ProductView의 likeCount는 변하지 않음
+            ProductView productViewAfterDelete = productViewRepository.findById(productId).orElseThrow();
+            assertEquals(likeCountBeforeDelete, productViewAfterDelete.getLikeCount(), 
+                    "존재하지 않는 좋아요 삭제 시 ProductView의 likeCount가 변하지 않아야 합니다.");
         }
 
         @DisplayName("실패 케이스: 존재하지 않는 사용자로 좋아요 취소 시 NOT_FOUND 예외 발생")
@@ -307,17 +319,65 @@ private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount)
                 .isSellable(true)
                 .build();
 
-        // 리플렉션을 사용하여 likeCount 설정 (Builder에 파라미터가 없으므로)
-        try {
-            java.lang.reflect.Field likeCountField = Product.class.getDeclaredField("likeCount");
-            likeCountField.setAccessible(true);
-            likeCountField.set(product, likeCount);
-        } catch (Exception e) {
-            throw new RuntimeException("likeCount 설정 실패", e);
+        Product savedProduct = productFacade.createProduct(product);
+        
+        // ProductView가 생성될 때까지 대기 (비동기 이벤트 핸들러 완료 대기)
+        waitForProductViewCreation(savedProduct.getId());
+        
+        // ProductView의 likeCount 업데이트 (테스트용)
+        // 실제로는 Like 엔티티를 생성해야 하지만, 테스트 편의를 위해 직접 업데이트
+        productViewRepository.updateLikeCount(savedProduct.getId(), likeCount);
+        
+        return savedProduct.getId();
+    }
+
+    /**
+     * ProductView가 생성될 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewCreation(Long productId) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                if (productViewRepository.findById(productId).isPresent()) {
+                    return; // ProductView가 생성되었음
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView 생성 대기 중 인터럽트 발생", e);
+            }
         }
+        
+        throw new RuntimeException("ProductView 생성 대기 시간 초과: productId=" + productId);
+    }
 
-        Product savedProduct = productFacade.saveProduct(product);
-        return savedProduct.getId();
+    /**
+     * ProductView의 likeCount가 예상 값에 도달할 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewLikeCountUpdate(Long productId, Long expectedCount) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                Optional<ProductView> productViewOpt = productViewRepository.findById(productId);
+                if (productViewOpt.isPresent() && productViewOpt.get().getLikeCount().equals(expectedCount)) {
+                    return; // ProductView의 likeCount가 예상 값에 도달함
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView likeCount 업데이트 대기 중 인터럽트 발생", e);
+            }
+        }
+        
+        throw new RuntimeException("ProductView likeCount 업데이트 대기 시간 초과: productId=" + productId + ", expectedCount=" + expectedCount);
     }
 }
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
index b9a5ee494..5d4320a2d 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
@@ -1,12 +1,20 @@
 package com.loopers.application.product;
 
+import com.loopers.domain.brand.Brand;
+import com.loopers.domain.brand.BrandStatus;
 import com.loopers.domain.product.Product;
-import com.loopers.domain.product.ProductCondition;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.ProductService;
 import com.loopers.domain.product.ProductStatus;
-import com.loopers.domain.product.ProductView;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.infrastructure.brand.BrandJpaRepository;
 import com.loopers.support.error.CoreException;
 import com.loopers.support.error.ErrorType;
 import com.loopers.utils.DatabaseCleanUp;
+import com.loopers.utils.RedisCleanUp;
+
+import java.lang.reflect.Field;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -17,7 +25,6 @@
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 
-import java.lang.reflect.Field;
 import java.math.BigDecimal;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -31,16 +38,23 @@ class ProductFacadeTest {
     private ProductFacade productFacade;
 
     @Autowired
-    private com.loopers.domain.product.ProductRepository productRepository;
+    private ProductService productService;
+
+    @Autowired
+    private BrandJpaRepository brandJpaRepository;
 
     @Autowired
-    private com.loopers.domain.product.ProductViewRepository productViewRepository;
+    private ProductViewRepository productViewRepository;
 
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
 
+    @Autowired
+    private RedisCleanUp redisCleanUp;
+
     @AfterEach
     void tearDown() {
+        redisCleanUp.truncateAll();
         databaseCleanUp.truncateAllTables();
     }
 
@@ -195,6 +209,127 @@ void getProductView_withInvalidId_throwsNotFoundException() {
         }
     }
 
+    @DisplayName("updateProduct 메서드")
+    @Nested
+    class UpdateProductTest {
+
+        @DisplayName("성공 케이스: 상품 정보 수정 후 ProductView가 업데이트된다")
+        @Test
+        void updateProduct_withValidProduct_updatesProductView() {
+            // arrange
+            Long productId = createAndSaveProduct("원본 상품명", BigDecimal.valueOf(10000L), 10L);
+            
+            // Product 조회
+            Product product = productService.findById(productId)
+                    .orElseThrow(() -> new RuntimeException("Product not found"));
+
+            // Product 업데이트 (새로운 필드 값으로 재빌드)
+            Product updatedProduct = Product.builder()
+                    .name("수정된 상품명")
+                    .description("수정된 설명")
+                    .price(BigDecimal.valueOf(20000L))
+                    .status(ProductStatus.SOLD_OUT)
+                    .isVisible(true)
+                    .isSellable(false)
+                    .brandId(product.getBrandId())
+                    .build();
+            
+            // ID 설정 (리플렉션 사용)
+            setIdUsingReflection(updatedProduct, productId);
+            
+            // act
+            productFacade.updateProduct(updatedProduct);
+            
+            // ProductView 업데이트 대기
+            waitForProductViewUpdate(productId, "수정된 상품명");
+
+            // assert
+            ProductView productView = productViewRepository.findById(productId)
+                    .orElseThrow(() -> new RuntimeException("ProductView not found"));
+            
+            assertAll(
+                    () -> assertThat(productView.getName()).isEqualTo("수정된 상품명"),
+                    () -> assertThat(productView.getPrice()).isEqualByComparingTo(BigDecimal.valueOf(20000L)),
+                    () -> assertThat(productView.getStatus()).isEqualTo(ProductStatus.SOLD_OUT),
+                    () -> assertThat(productView.getLikeCount()).isEqualTo(10L) // 좋아요 수는 변경되지 않음
+            );
+        }
+
+        @DisplayName("성공 케이스: 브랜드 변경 시 ProductView의 brandName이 업데이트된다")
+        @Test
+        void updateProduct_withBrandChange_updatesBrandName() {
+            // arrange
+            // 브랜드 생성
+            Brand brand1 = Brand.builder()
+                    .name("브랜드1")
+                    .description("브랜드1 설명")
+                    .status(BrandStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .build();
+            Brand savedBrand1 = brandJpaRepository.save(brand1);
+
+            Brand brand2 = Brand.builder()
+                    .name("브랜드2")
+                    .description("브랜드2 설명")
+                    .status(BrandStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .build();
+            Brand savedBrand2 = brandJpaRepository.save(brand2);
+
+            // 상품 생성 (브랜드1로)
+            Product product = Product.builder()
+                    .name("테스트 상품")
+                    .description("테스트 설명")
+                    .price(BigDecimal.valueOf(10000L))
+                    .status(ProductStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .brandId(savedBrand1.getId())
+                    .build();
+            Product savedProduct = productFacade.createProduct(product);
+            Long productId = savedProduct.getId();
+            
+            // ProductView 생성 대기
+            waitForProductViewCreation(productId);
+            
+            // 초기 ProductView 확인
+            ProductView initialView = productViewRepository.findById(productId)
+                    .orElseThrow(() -> new RuntimeException("ProductView not found"));
+            assertThat(initialView.getBrandName()).isEqualTo("브랜드1");
+
+            // Product 업데이트 (브랜드2로 변경)
+            Product updatedProduct = Product.builder()
+                    .name("테스트 상품")
+                    .description("테스트 설명")
+                    .price(BigDecimal.valueOf(10000L))
+                    .status(ProductStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .brandId(savedBrand2.getId())
+                    .build();
+            
+            // ID 설정 (리플렉션 사용)
+            setIdUsingReflection(updatedProduct, productId);
+
+            // act
+            productFacade.updateProduct(updatedProduct);
+            
+            // ProductView 업데이트 대기 (브랜드 변경 확인)
+            waitForProductViewBrandUpdate(productId, savedBrand2.getId());
+
+            // assert
+            ProductView updatedView = productViewRepository.findById(productId)
+                    .orElseThrow(() -> new RuntimeException("ProductView not found"));
+            
+            assertAll(
+                    () -> assertThat(updatedView.getBrandId()).isEqualTo(savedBrand2.getId()),
+                    () -> assertThat(updatedView.getBrandName()).isEqualTo("브랜드2")
+            );
+        }
+    }
+
     // 테스트 헬퍼 메서드
     private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount) {
         Product product = Product.builder()
@@ -207,17 +342,7 @@ private Long createAndSaveProduct(String name, BigDecimal price, Long likeCount)
                 .build();
 
         // Product 저장
-        Product savedProduct = productFacade.saveProduct(product);
-        
-        // 저장 후 likeCount를 reflection으로 설정
-        try {
-            Field likeCountField = Product.class.getDeclaredField("likeCount");
-            likeCountField.setAccessible(true);
-            likeCountField.set(savedProduct, likeCount);
-            productRepository.save(savedProduct); // 다시 저장하여 likeCount 반영
-        } catch (Exception e) {
-            throw new RuntimeException("likeCount 설정 실패", e);
-        }
+        Product savedProduct = productFacade.createProduct(product);
         
         // ProductView가 생성될 때까지 대기 (비동기 이벤트 핸들러 완료 대기)
         waitForProductViewCreation(savedProduct.getId());
@@ -253,5 +378,68 @@ private void waitForProductViewCreation(Long productId) {
         
         throw new RuntimeException("ProductView 생성 대기 시간 초과: productId=" + productId);
     }
+
+    /**
+     * ProductView가 업데이트될 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewUpdate(Long productId, String expectedName) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                ProductView productView = productViewRepository.findById(productId).orElse(null);
+                if (productView != null && expectedName.equals(productView.getName())) {
+                    return; // ProductView가 업데이트되었음
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView 업데이트 대기 중 인터럽트 발생", e);
+            }
+        }
+        
+        throw new RuntimeException("ProductView 업데이트 대기 시간 초과: productId=" + productId);
+    }
+
+    /**
+     * ProductView의 브랜드가 업데이트될 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewBrandUpdate(Long productId, Long expectedBrandId) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                ProductView productView = productViewRepository.findById(productId).orElse(null);
+                if (productView != null && expectedBrandId.equals(productView.getBrandId())) {
+                    return; // ProductView 브랜드가 업데이트되었음
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView 브랜드 업데이트 대기 중 인터럽트 발생", e);
+            }
+        }
+        
+        throw new RuntimeException("ProductView 브랜드 업데이트 대기 시간 초과: productId=" + productId);
+    }
+
+    /**
+     * 리플렉션을 사용하여 BaseEntity의 id 필드를 설정하는 헬퍼 메서드
+     */
+    private void setIdUsingReflection(Object entity, Long id) {
+        try {
+            Field idField = entity.getClass().getSuperclass().getDeclaredField("id");
+            idField.setAccessible(true);
+            idField.set(entity, id);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new RuntimeException("Failed to set ID using reflection", e);
+        }
+    }
 }
 
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
index 6598c861d..c3cf6a4d5 100644
--- 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
@@ -43,7 +43,6 @@ void createProduct_withValidFields_Success() {
                     () -> assertEquals(product.getName(), validName),
                     () -> assertEquals(product.getDescription(), validDescription),
                     () -> assertEquals(product.getPrice(), validPrice),
-                    () -> assertEquals(0L, product.getLikeCount(), "likeCount는 필드 초기화로 0L이 기본값"),
                     () -> assertEquals(product.getStatus(), validStatus),
                     () -> assertEquals(product.getIsVisible(), validIsVisible),
                     () -> assertEquals(product.getIsSellable(), validIsSellable)
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceIntegrationTest.java
new file mode 100644
index 000000000..2f7fb43cc
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceIntegrationTest.java
@@ -0,0 +1,257 @@
+package com.loopers.domain.product.cache;
+
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.utils.DatabaseCleanUp;
+import com.loopers.utils.RedisCleanUp;
+import org.junit.jupiter.api.*;
+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.redis.core.RedisTemplate;
+
+import java.math.BigDecimal;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("ProductCacheService 통합 테스트")
+@SpringBootTest
+class ProductCacheServiceIntegrationTest {
+
+    @Autowired
+    private ProductCacheService productCacheService;
+    
+    @Autowired
+    private ProductViewRepository productViewRepository;
+    
+    @Autowired
+    private RedisTemplate<String, Object> productCacheRedisTemplate;
+    
+    @Autowired
+    private RedisTemplate<String, String> productListCacheRedisTemplate;
+    
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+    
+    @Autowired
+    private RedisCleanUp redisCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        redisCleanUp.truncateAll();
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("Hot Page 캐싱 테스트")
+    @Nested
+    class HotPageTest {
+
+        @DisplayName("Hot Page 첫 요청 시 L1+L2 캐시에 저장한다")
+        @Test
+        void hotPage_firstRequest_cachesL1AndL2() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10); // Hot Page (page 0, brandId 없음)
+
+            // act
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+
+            // assert
+            assertThat(result.getContent()).hasSize(1);
+            assertThat(result.getContent().get(0).getName()).isEqualTo("테스트 상품");
+            
+            // L1 캐시 확인
+            String listKey = "products::list::sort:latest::page:0::size:10";
+            String cachedIds = productListCacheRedisTemplate.opsForValue().get(listKey);
+            assertThat(cachedIds).isNotNull();
+            assertThat(cachedIds).contains("1");
+            
+            // L2 캐시 확인
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:1");
+            assertThat(cachedInfo).isNotNull();
+        }
+
+        @DisplayName("Hot Page 두 번째 요청 시 캐시에서 가져온다")
+        @Test
+        void hotPage_secondRequest_fetchesFromCache() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10);
+
+            // 첫 번째 조회 (캐시 웜업)
+            productCacheService.getProductViews(condition, pageable);
+            
+            // DB 데이터 변경 (캐시와 다른 값으로)
+            productViewRepository.update(1L, "변경된 상품", BigDecimal.valueOf(20000), null, null, ProductStatus.ON_SALE);
+
+            // act - 두 번째 조회 (캐시 히트)
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+
+            // assert - 캐시된 값이 반환됨 (변경 전 값)
+            assertThat(result.getContent().get(0).getName()).isEqualTo("테스트 상품");
+        }
+
+        @DisplayName("Hot Page Info 캐시 삭제 후 Backfill하여 최신 데이터를 가져온다")
+        @Test
+        void hotPage_afterInfoEviction_backfillsLatestData() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10);
+
+            // 첫 번째 조회 (캐시 웜업)
+            productCacheService.getProductViews(condition, pageable);
+            
+            // DB 데이터 변경
+            productViewRepository.update(1L, "변경된 상품", BigDecimal.valueOf(20000), null, null, ProductStatus.ON_SALE);
+            
+            // L2 Info 캐시만 삭제 (Evict 시뮬레이션)
+            productCacheRedisTemplate.delete("product:info:1");
+
+            // act
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+
+            // assert - 최신 DB 데이터로 Backfill됨
+            assertThat(result.getContent().get(0).getName()).isEqualTo("변경된 상품");
+        }
+    }
+
+    @DisplayName("Cold Page 캐싱 테스트")
+    @Nested
+    class ColdPageTest {
+
+        @DisplayName("Cold Page 캐시 미스 시 Look Aside로 L1, L2 캐시 모두 저장된다")
+        @Test
+        void coldPage_cacheMiss_savesBothL1AndL2WithLookAside() {
+            // arrange
+            // Cold Brand(1000L)로 ProductView 생성 (page=5에서 조회하려면 최소 50개 이상 필요)
+            // 60개 생성하여 page=5 (offset 50)에서도 데이터가 나오도록 함
+            for (long i = 1; i <= 60; i++) {
+                createAndSaveProductView(i, "테스트 상품 " + i, 1000L);
+            }
+            ProductCondition condition = ProductCondition.builder()
+                    .brandId(1000L) // Cold Brand
+                    .sort("latest")
+                    .build();
+            Pageable pageable = PageRequest.of(5, 10); // Cold Page (page 5, hotPageRange=3이므로 0~2는 Hot)
+
+            // act
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+
+            // assert
+            assertThat(result.getContent()).hasSize(10); // page=5, size=10이므로 10개 반환
+            
+            // L1 캐시는 Look Aside로 저장됨
+            String listKey = "products::list::brand:1000::sort:latest::page:5::size:10";
+            String cachedIds = productListCacheRedisTemplate.opsForValue().get(listKey);
+            assertThat(cachedIds).isNotNull(); // Look Aside로 캐시 저장됨
+            
+            // L2 캐시도 Look Aside로 저장됨 (Cold Page에서도 getProductViews 호출 시 저장)
+            // 결과에 포함된 첫 번째 상품의 ID를 확인
+            Long firstProductId = result.getContent().get(0).getId();
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:" + firstProductId);
+            assertThat(cachedInfo).isNotNull(); // Look Aside로 캐시 저장됨
+            
+            // 참고: Cold Page에서 이벤트 발생 시 자동 Backfill은 없음
+            // (이것은 ProductViewEventHandler 테스트에서 확인)
+        }
+    }
+
+    @DisplayName("Stat 캐시 테스트")
+    @Nested
+    class StatCacheTest {
+
+        @DisplayName("좋아요 수는 product:stat에서 가져온다")
+        @Test
+        void likeCount_fetchedFromStatCache() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            
+            // 첫 번째 조회로 캐시 웜업
+            productCacheService.getProductView(1L);
+            
+            // Stat 캐시 직접 수정
+            productCacheRedisTemplate.opsForHash().put("product:stat:1", "likeCount", "999");
+
+            // act
+            Optional<ProductView> result = productCacheService.getProductView(1L);
+
+            // assert
+            assertThat(result).isPresent();
+            assertThat(result.get().getLikeCount()).isEqualTo(999L);
+        }
+    }
+
+    @DisplayName("Hot/Cold TTL 테스트")
+    @Nested
+    class HotColdTtlTest {
+
+        @DisplayName("Hot Page는 PER이 적용된 TTL로 저장된다")
+        @Test
+        void hotPage_savedWithPERTtl() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10); // Hot Page (page 0)
+
+            // act
+            productCacheService.getProductViews(condition, pageable);
+
+            // assert
+            String listKey = "products::list::sort:latest::page:0::size:10";
+            Long ttl = productListCacheRedisTemplate.getExpire(listKey);
+            
+            // PER 적용으로 TTL이 300~360초 범위 (baseTtl + 0~60초 랜덤)
+            assertThat(ttl).isBetween(295L, 365L);
+        }
+
+        @DisplayName("Cold Page는 기본 TTL로 저장된다")
+        @Test
+        void coldPage_savedWithBaseTtl() {
+            // arrange
+            createAndSaveProductView(1L, "테스트 상품");
+            ProductCondition condition = ProductCondition.builder()
+                    .brandId(1000L) // Cold Brand
+                    .sort("latest")
+                    .build();
+            Pageable pageable = PageRequest.of(5, 10); // Cold Page (page 5)
+
+            // act
+            productCacheService.getProductViews(condition, pageable);
+
+            // assert
+            String listKey = "products::list::brand:1000::sort:latest::page:5::size:10";
+            Long ttl = productListCacheRedisTemplate.getExpire(listKey);
+            
+            // 기본 TTL 300초
+            assertThat(ttl).isLessThanOrEqualTo(300L);
+        }
+    }
+
+    private ProductView createAndSaveProductView(Long id, String name) {
+        return createAndSaveProductView(id, name, 1L);
+    }
+
+    private ProductView createAndSaveProductView(Long id, String name, Long brandId) {
+        ProductView productView = ProductView.builder()
+                .id(id)
+                .name(name)
+                .price(BigDecimal.valueOf(10000))
+                .likeCount(10L)
+                .brandId(brandId)
+                .brandName("테스트 브랜드")
+                .status(ProductStatus.ON_SALE)
+                .createdAt(ZonedDateTime.now())
+                .build();
+        return productViewRepository.save(productView).orElseThrow();
+    }
+}
+
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceTest.java
new file mode 100644
index 000000000..675ae085b
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/cache/ProductCacheServiceTest.java
@@ -0,0 +1,263 @@
+package com.loopers.domain.product.cache;
+
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.view.ProductCondition;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.testcontainers.RedisTestContainersConfig;
+import com.loopers.utils.DatabaseCleanUp;
+import com.loopers.utils.RedisCleanUp;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.math.BigDecimal;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("ProductCacheService 통합 테스트")
+@SpringBootTest
+@Import(RedisTestContainersConfig.class)
+class ProductCacheServiceTest {
+
+    @Autowired
+    private ProductCacheService productCacheService;
+    
+    @Autowired
+    private ProductViewRepository productViewRepository;
+    
+    @Autowired
+    private RedisTemplate<String, Object> productCacheRedisTemplate;
+    
+    @Autowired
+    private RedisTemplate<String, String> productListCacheRedisTemplate;
+    
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+    
+    @Autowired
+    private RedisCleanUp redisCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        redisCleanUp.truncateAll();
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("Hot 판단 테스트")
+    @Nested
+    class HotDetectionTest {
+
+        @DisplayName("brandId가 1~500 범위에 있으면 Hot Brand로 판단한다")
+        @Test
+        void isHotBrand_withHotBrandId_returnsTrue() {
+            // act & assert
+            assertThat(productCacheService.isHotBrand(1L)).isTrue();
+            assertThat(productCacheService.isHotBrand(250L)).isTrue();
+            assertThat(productCacheService.isHotBrand(500L)).isTrue();
+        }
+
+        @DisplayName("brandId가 500 초과이면 Cold Brand로 판단한다")
+        @Test
+        void isHotBrand_withColdBrandId_returnsFalse() {
+            // act & assert
+            assertThat(productCacheService.isHotBrand(501L)).isFalse();
+            assertThat(productCacheService.isHotBrand(1000L)).isFalse();
+        }
+
+        @DisplayName("productId가 1~5000 범위에 있으면 Hot Product로 판단한다")
+        @Test
+        void isHotProduct_withHotProductId_returnsTrue() {
+            // act & assert
+            assertThat(productCacheService.isHotProduct(1L)).isTrue();
+            assertThat(productCacheService.isHotProduct(2500L)).isTrue();
+            assertThat(productCacheService.isHotProduct(5000L)).isTrue();
+        }
+
+        @DisplayName("productId가 5000 초과이면 Cold Product로 판단한다")
+        @Test
+        void isHotProduct_withColdProductId_returnsFalse() {
+            // act & assert
+            assertThat(productCacheService.isHotProduct(5001L)).isFalse();
+            assertThat(productCacheService.isHotProduct(10000L)).isFalse();
+        }
+    }
+
+    @DisplayName("Hot Page 캐싱 테스트")
+    @Nested
+    class HotPageCacheTest {
+
+        @DisplayName("Hot Page 캐시 히트 시 DB 조회하지 않는다")
+        @Test
+        void hotPage_cacheHit_doesNotQueryDb() throws InterruptedException {
+            // arrange
+            createAndSaveProductView(1L, "상품1");
+            createAndSaveProductView(2L, "상품2");
+            createAndSaveProductView(3L, "상품3");
+            
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10); // Hot Page (page 0, brandId 없음)
+            
+            // 첫 번째 조회로 캐시 웜업
+            productCacheService.getProductViews(condition, pageable);
+            
+            // DB 데이터 변경 (캐시와 다른 값으로)
+            productViewRepository.update(1L, "변경된 상품1", BigDecimal.valueOf(20000), null, null, ProductStatus.ON_SALE);
+            
+            // act - 두 번째 조회 (캐시 히트)
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+            
+            // assert - 캐시된 값이 반환됨 (변경 전 값)
+            assertThat(result.getContent()).hasSize(3);
+            assertThat(result.getContent().get(2).getName()).isEqualTo("상품1"); // 캐시된 값
+        }
+
+        @DisplayName("Hot Page 캐시 미스 시 DB 조회 후 Backfill한다")
+        @Test
+        void hotPage_cacheMiss_queriesDbAndBackfills() {
+            // arrange
+            createAndSaveProductView(1L, "상품1");
+            createAndSaveProductView(2L, "상품2");
+            
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10); // Hot Page (page 0)
+            
+            // act - 첫 번째 조회 (캐시 미스, DB 조회 후 Backfill)
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+            
+            // assert
+            assertThat(result.getContent()).hasSize(2);
+            
+            // L1 캐시 확인
+            String listKey = "products::list::sort:latest::page:0::size:10";
+            String cachedIds = productListCacheRedisTemplate.opsForValue().get(listKey);
+            assertThat(cachedIds).isNotNull(); // PER 적용된 저장
+            
+            // L2 캐시 확인 (Backfill로 저장됨)
+            Object cachedInfo1 = productCacheRedisTemplate.opsForValue().get("product:info:1");
+            Object cachedInfo2 = productCacheRedisTemplate.opsForValue().get("product:info:2");
+            assertThat(cachedInfo1).isNotNull(); // Backfill로 저장됨
+            assertThat(cachedInfo2).isNotNull(); // Backfill로 저장됨
+        }
+
+        @DisplayName("Hot Page L2 부분 미스 시 Backfill한다")
+        @Test
+        void hotPage_partialL2Miss_backfillsFromDb() {
+            // arrange
+            createAndSaveProductView(1L, "상품1");
+            createAndSaveProductView(2L, "상품2");
+            createAndSaveProductView(3L, "상품3");
+            
+            ProductCondition condition = ProductCondition.builder().sort("latest").build();
+            Pageable pageable = PageRequest.of(0, 10); // Hot Page
+            
+            // 첫 번째 조회로 L1, L2 캐시 웜업
+            productCacheService.getProductViews(condition, pageable);
+            
+            // L2 Info 캐시만 삭제 (ID 3만)
+            productCacheRedisTemplate.delete("product:info:3");
+            
+            // act - 두 번째 조회 (L1 히트, L2 부분 미스)
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+            
+            // assert
+            assertThat(result.getContent()).hasSize(3);
+            
+            // ID 3의 L2 캐시가 Backfill로 저장되었는지 확인
+            Object cachedInfo3 = productCacheRedisTemplate.opsForValue().get("product:info:3");
+            assertThat(cachedInfo3).isNotNull(); // Backfill로 저장됨
+        }
+    }
+
+    @DisplayName("Cold Page 캐싱 테스트")
+    @Nested
+    class ColdPageCacheTest {
+
+        @DisplayName("Cold Page 캐시 미스 시 Look Aside로 L1, L2 캐시 모두 저장된다")
+        @Test
+        void coldPage_cacheMiss_savesBothL1AndL2WithLookAside() {
+            // arrange
+            // Cold Brand(1000L)로 ProductView 생성 (page=5에서 조회하려면 최소 50개 이상 필요)
+            // 60개 생성하여 page=5 (offset 50)에서도 데이터가 나오도록 함
+            for (long i = 1; i <= 60; i++) {
+                createAndSaveProductView(i, "테스트 상품 " + i, 1000L);
+            }
+            ProductCondition condition = ProductCondition.builder()
+                    .brandId(1000L) // Cold Brand
+                    .sort("latest")
+                    .build();
+            Pageable pageable = PageRequest.of(5, 10); // Cold Page (page 5)
+            
+            // act
+            Page<ProductView> result = productCacheService.getProductViews(condition, pageable);
+            
+            // assert
+            assertThat(result.getContent()).hasSize(10); // page=5, size=10이므로 10개 반환
+            
+            // L1 캐시는 Look Aside로 저장됨
+            String listKey = "products::list::brand:1000::sort:latest::page:5::size:10";
+            String cachedIds = productListCacheRedisTemplate.opsForValue().get(listKey);
+            assertThat(cachedIds).isNotNull(); // Look Aside로 캐시 저장됨
+            
+            // L2 캐시도 Look Aside로 저장됨 (Cold Page에서도 getProductViews 호출 시 저장)
+            // 결과에 포함된 첫 번째 상품의 ID를 확인
+            Long firstProductId = result.getContent().get(0).getId();
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:" + firstProductId);
+            assertThat(cachedInfo).isNotNull(); // Look Aside로 캐시 저장됨
+            
+            // 참고: Cold Page에서 이벤트 발생 시 자동 Backfill은 없음
+            // (이것은 ProductViewEventHandler 테스트에서 확인)
+        }
+    }
+
+    @DisplayName("L2 캐시 테스트")
+    @Nested
+    class L2CacheTest {
+
+        @DisplayName("Info와 Stat 캐시를 조합하여 반환한다")
+        @Test
+        void getProductView_combinesInfoAndStat() {
+            // arrange
+            Long productId = 1L; // Hot Product (1~5000 범위)
+            createAndSaveProductView(productId, "테스트 상품");
+            
+            // 첫 번째 조회로 캐시 웜업
+            productCacheService.getProductView(productId);
+            
+            // Stat 캐시 직접 수정
+            productCacheRedisTemplate.opsForHash().put("product:stat:" + productId, "likeCount", "999");
+            
+            // act
+            Optional<ProductView> result = productCacheService.getProductView(productId);
+            
+            // assert
+            assertThat(result).isPresent();
+            assertThat(result.get().getLikeCount()).isEqualTo(999L);
+        }
+    }
+
+    private ProductView createAndSaveProductView(Long id, String name) {
+        return createAndSaveProductView(id, name, 1L);
+    }
+
+    private ProductView createAndSaveProductView(Long id, String name, Long brandId) {
+        ProductView productView = ProductView.builder()
+                .id(id)
+                .name(name)
+                .price(BigDecimal.valueOf(10000))
+                .likeCount(10L)
+                .brandId(brandId)
+                .brandName("테스트 브랜드")
+                .status(ProductStatus.ON_SALE)
+                .createdAt(ZonedDateTime.now())
+                .build();
+        return productViewRepository.save(productView).orElseThrow();
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/event/ProductViewEventHandlerTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/event/ProductViewEventHandlerTest.java
new file mode 100644
index 000000000..1c48a6474
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/event/ProductViewEventHandlerTest.java
@@ -0,0 +1,312 @@
+package com.loopers.domain.product.event;
+
+import com.loopers.domain.product.Product;
+import com.loopers.domain.product.ProductService;
+import com.loopers.domain.product.ProductStatus;
+import com.loopers.domain.product.cache.ProductCacheService;
+import com.loopers.domain.product.view.ProductView;
+import com.loopers.domain.product.view.ProductViewRepository;
+import com.loopers.utils.DatabaseCleanUp;
+import com.loopers.utils.RedisCleanUp;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("ProductViewEventHandler 테스트")
+@SpringBootTest
+class ProductViewEventHandlerTest {
+
+    @Autowired
+    private ProductViewEventHandler eventHandler;
+    
+    @Autowired
+    private ProductCacheService productCacheService;
+    
+    @Autowired
+    private ProductService productService;
+    
+    @Autowired
+    private ProductViewRepository productViewRepository;
+    
+    @Autowired
+    private RedisTemplate<String, Object> productCacheRedisTemplate;
+    
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+    
+    @Autowired
+    private RedisCleanUp redisCleanUp;
+
+    @AfterEach
+    void tearDown() {
+        redisCleanUp.truncateAll();
+        databaseCleanUp.truncateAllTables();
+    }
+
+    @DisplayName("Write-Through: 좋아요 수 변경 테스트")
+    @Nested
+    class WriteThroughTest {
+
+        @DisplayName("캐시가 존재할 때 좋아요 증가하면 캐시 값이 증가한다")
+        @Test
+        void likeIncrement_withExistingCache_incrementsValue() throws InterruptedException {
+            // arrange
+            // 상품 생성 및 ProductView 생성 대기
+            Product product = createAndSaveProduct("테스트 상품");
+            Long productId = product.getId();
+            waitForProductViewCreation(productId);
+            
+            // 캐시 웜업 (Stat 캐시 생성)
+            productCacheService.getProductView(productId);
+            waitForAsyncProcessing();
+            
+            // 초기 Stat 캐시 확인
+            Object initialCount = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(initialCount).isNotNull();
+            long oldCount = initialCount != null ? Long.parseLong(initialCount.toString()) : 0L;
+            
+            // act - 좋아요 증가 이벤트 발행
+            eventHandler.handleProductLikeCountEvent(ProductEventDto.LikeCount.increment(productId));
+            waitForAsyncProcessing();
+            
+            // assert
+            Object cachedCount = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedCount).isNotNull();
+            long newCount = cachedCount != null ? Long.parseLong(cachedCount.toString()) : 0L;
+            assertThat(newCount).isEqualTo(oldCount + 1);
+        }
+
+        @DisplayName("캐시가 존재할 때 좋아요 감소하면 캐시 값이 감소한다")
+        @Test
+        void likeDecrement_withExistingCache_decrementsValue() throws InterruptedException {
+            // arrange
+            // 상품 생성 및 ProductView 생성 대기
+            Product product = createAndSaveProduct("테스트 상품");
+            Long productId = product.getId();
+            waitForProductViewCreation(productId);
+            
+            // 캐시 웜업 (Stat 캐시 생성)
+            productCacheService.getProductView(productId);
+            waitForAsyncProcessing();
+            
+            // 초기값을 10으로 설정
+            productCacheRedisTemplate.opsForHash().put("product:stat:" + productId, "likeCount", "10");
+            
+            // act - 좋아요 감소 이벤트 발행
+            eventHandler.handleProductLikeCountEvent(ProductEventDto.LikeCount.decrement(productId));
+            waitForAsyncProcessing();
+            
+            // assert
+            Object cachedCount = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedCount).isEqualTo("9");
+        }
+
+        @DisplayName("좋아요 감소 시 음수가 되지 않는다")
+        @Test
+        void likeDecrement_doesNotGoBelowZero() throws InterruptedException {
+            // arrange
+            // 상품 생성 및 ProductView 생성 대기
+            Product product = createAndSaveProduct("테스트 상품");
+            Long productId = product.getId();
+            waitForProductViewCreation(productId);
+            
+            // 캐시 웜업 (Stat 캐시 생성)
+            productCacheService.getProductView(productId);
+            waitForAsyncProcessing();
+            
+            // 초기값을 0으로 설정
+            productCacheRedisTemplate.opsForHash().put("product:stat:" + productId, "likeCount", "0");
+            
+            // act - 좋아요 감소 이벤트 발행
+            eventHandler.handleProductLikeCountEvent(ProductEventDto.LikeCount.decrement(productId));
+            waitForAsyncProcessing();
+            
+            // assert
+            Object cachedCount = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedCount).isEqualTo("0");
+        }
+
+        @DisplayName("캐시가 없으면 캐시 업데이트를 건너뛴다 (Write-Around)")
+        @Test
+        void likeChange_withoutCache_skipsCache() throws InterruptedException {
+            // arrange - 캐시 없음 (상품만 생성)
+            Product product = createAndSaveProduct("테스트 상품");
+            Long productId = product.getId();
+            
+            // act - 좋아요 증가 이벤트 발행
+            eventHandler.handleProductLikeCountEvent(ProductEventDto.LikeCount.increment(productId));
+            waitForAsyncProcessing();
+            
+            // assert - 캐시가 생성되지 않음
+            Object cachedCount = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedCount).isNull();
+        }
+    }
+
+    @DisplayName("Write-Around: 상품 생성 테스트")
+    @Nested
+    class WriteAroundTest {
+
+        @DisplayName("상품 생성 시 캐시에 쓰지 않는다")
+        @Test
+        void productCreated_doesNotWriteToCache() throws InterruptedException {
+            // arrange
+            Product product = Product.builder()
+                    .name("새 상품")
+                    .description("설명")
+                    .price(BigDecimal.valueOf(10000))
+                    .status(ProductStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .build();
+
+            // act - 상품 생성 (이벤트 발행)
+            productService.createProduct(product);
+            waitForAsyncProcessing();
+            
+            Long productId = product.getId();
+            
+            // assert - ProductView는 DB에 생성되었지만 캐시에는 없음
+            Optional<ProductView> productView = productViewRepository.findById(productId);
+            assertThat(productView).isPresent();
+            
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:" + productId);
+            assertThat(cachedInfo).isNull(); // Write-Around: 캐시에 쓰지 않음
+        }
+    }
+
+    @DisplayName("Evict: 캐시 삭제 테스트")
+    @Nested
+    class EvictTest {
+
+        @DisplayName("상품 수정 시 Info 캐시가 삭제된다")
+        @Test
+        void productUpdate_evictsInfoCache() throws InterruptedException {
+            // arrange
+            Product product = createAndSaveProduct("원본 상품");
+            Long productId = product.getId();
+            waitForProductViewCreation(productId);
+            
+            // 캐시 웜업
+            productCacheService.getProductView(productId);
+            waitForAsyncProcessing();
+            
+            // 캐시 존재 확인
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:" + productId);
+            assertThat(cachedInfo).isNotNull();
+            
+            // act - 상품 수정 (이벤트 발행)
+            Product updatedProduct = Product.builder()
+                    .name("수정된 상품")
+                    .description("수정된 설명")
+                    .price(BigDecimal.valueOf(20000))
+                    .status(ProductStatus.ON_SALE)
+                    .isVisible(true)
+                    .isSellable(true)
+                    .brandId(product.getBrandId())
+                    .build();
+            
+            // ID 설정 (리플렉션 사용)
+            setIdUsingReflection(updatedProduct, productId);
+            productService.updateProduct(updatedProduct);
+            waitForAsyncProcessing();
+            
+            // assert - Info 캐시가 삭제됨
+            Object cachedInfoAfterUpdate = productCacheRedisTemplate.opsForValue().get("product:info:" + productId);
+            assertThat(cachedInfoAfterUpdate).isNull();
+        }
+
+        @DisplayName("상품 삭제 시 Info와 Stat 캐시가 모두 삭제된다")
+        @Test
+        void productDelete_evictsBothCaches() throws InterruptedException {
+            // arrange
+            Product product = createAndSaveProduct("삭제될 상품");
+            Long productId = product.getId();
+            waitForProductViewCreation(productId);
+            
+            // 캐시 웜업
+            productCacheService.getProductView(productId);
+            waitForAsyncProcessing();
+            
+            // 캐시 존재 확인
+            Object cachedInfo = productCacheRedisTemplate.opsForValue().get("product:info:" + productId);
+            Object cachedStat = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedInfo).isNotNull();
+            assertThat(cachedStat).isNotNull();
+            
+            // act - 상품 삭제 (이벤트 발행)
+            productService.deleteProduct(productId);
+            waitForAsyncProcessing();
+            
+            // assert - Info와 Stat 캐시가 모두 삭제됨
+            Object cachedInfoAfterDelete = productCacheRedisTemplate.opsForValue().get("product:info:" + productId);
+            Object cachedStatAfterDelete = productCacheRedisTemplate.opsForHash().get("product:stat:" + productId, "likeCount");
+            assertThat(cachedInfoAfterDelete).isNull();
+            assertThat(cachedStatAfterDelete).isNull();
+        }
+    }
+
+    // 테스트 헬퍼 메서드
+    private Product createAndSaveProduct(String name) {
+        Product product = Product.builder()
+                .name(name)
+                .description("테스트 설명")
+                .price(BigDecimal.valueOf(10000))
+                .status(ProductStatus.ON_SALE)
+                .isVisible(true)
+                .isSellable(true)
+                .build();
+        return productService.createProduct(product).orElseThrow();
+    }
+
+    /**
+     * 비동기 이벤트 핸들러 완료 대기
+     */
+    private void waitForAsyncProcessing() throws InterruptedException {
+        Thread.sleep(200); // @Async 메서드 완료 대기
+    }
+
+    /**
+     * ProductView가 생성될 때까지 대기하는 헬퍼 메서드
+     * 비동기 이벤트 핸들러가 완료될 때까지 폴링
+     */
+    private void waitForProductViewCreation(Long productId) {
+        int maxAttempts = 50; // 최대 5초 대기 (100ms * 50)
+        int attempt = 0;
+        
+        while (attempt < maxAttempts) {
+            try {
+                if (productViewRepository.findById(productId).isPresent()) {
+                    return; // ProductView가 생성되었음
+                }
+                Thread.sleep(100); // 100ms 대기
+                attempt++;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("ProductView 생성 대기 중 인터럽트 발생", e);
+            }
+        }
+        
+        throw new RuntimeException("ProductView 생성 대기 시간 초과: productId=" + productId);
+    }
+
+    /**
+     * 리플렉션을 사용하여 BaseEntity의 id 필드를 설정하는 헬퍼 메서드
+     */
+    private void setIdUsingReflection(Object entity, Long id) {
+        try {
+            java.lang.reflect.Field idField = entity.getClass().getSuperclass().getDeclaredField("id");
+            idField.setAccessible(true);
+            idField.set(entity, id);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new RuntimeException("Failed to set ID using reflection", e);
+        }
+    }
+}
+
diff --git a/modules/redis/src/main/resources/redis.yml b/modules/redis/src/main/resources/redis.yml
index 10a260463..ab8988294 100644
--- a/modules/redis/src/main/resources/redis.yml
+++ b/modules/redis/src/main/resources/redis.yml
@@ -19,12 +19,13 @@ spring.config.activate.on-profile: local, test
 
 datasource:
   redis:
+    database: 0
     master:
-      host: localhost
-      port: 6379
+      host: ${REDIS_MASTER_HOST:localhost}
+      port: ${REDIS_MASTER_PORT:6379}
     replicas:
-      - host: localhost
-        port: 6380
+      - host: ${REDIS_REPLICA_1_HOST:localhost}
+        port: ${REDIS_REPLICA_1_PORT:6380}
 
 ---
 spring.config.activate.on-profile: dev
diff --git a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
index 35bf94f06..b16bef6b7 100644
--- a/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
+++ b/modules/redis/src/testFixtures/java/com/loopers/testcontainers/RedisTestContainersConfig.java
@@ -10,13 +10,11 @@ public class RedisTestContainersConfig {
 
     static {
         redisContainer.start();
-    }
-
-    public RedisTestContainersConfig() {
-        System.setProperty("datasource.redis.database", "0");
-        System.setProperty("datasource.redis.master.host", redisContainer.getHost());
-        System.setProperty("datasource.redis.master.port", String.valueOf(redisContainer.getFirstMappedPort()));
-        System.setProperty("datasource.redis.replicas[0].host", redisContainer.getHost());
-        System.setProperty("datasource.redis.replicas[0].port", String.valueOf(redisContainer.getFirstMappedPort()));
+        
+        // System Property는 static 블록에서 설정해야 Spring Boot가 YAML을 읽기 전에 적용됨
+        System.setProperty("REDIS_MASTER_HOST", redisContainer.getHost());
+        System.setProperty("REDIS_MASTER_PORT", String.valueOf(redisContainer.getFirstMappedPort()));
+        System.setProperty("REDIS_REPLICA_1_HOST", redisContainer.getHost());
+        System.setProperty("REDIS_REPLICA_1_PORT", String.valueOf(redisContainer.getFirstMappedPort()));
     }
 }
diff --git a/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java b/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
index 4f8bda6f4..8a2175879 100644
--- a/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
+++ b/supports/jackson/src/main/java/com/loopers/config/jackson/JacksonConfig.java
@@ -4,6 +4,7 @@
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -19,6 +20,9 @@ public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
         return builder -> {
             // Classpath 내의 모든 Jackson 모듈 자동 등록
             builder.findModulesViaServiceLoader(true);
+            
+            // JavaTimeModule 명시적 등록 (ZonedDateTime 등 지원)
+            builder.modules(new JavaTimeModule());
 
             // Serialization Features
             builder.serializationInclusion(JsonInclude.Include.NON_NULL);

From fc59cc83f34792e009097a08ab43e38b4c82bb40 Mon Sep 17 00:00:00 2001
From: Yim Yoobin <yoobinbkk@naver.com>
Date: Fri, 28 Nov 2025 16:30:09 +0900
Subject: [PATCH 17/17] =?UTF-8?q?[Round=205]=20=EC=BA=90=EC=8B=9C=20TTL=20?=
 =?UTF-8?q?=EC=A1=B0=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/src/main/resources/application.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/apps/commerce-api/src/main/resources/application.yml b/apps/commerce-api/src/main/resources/application.yml
index e72178404..7eeb6a903 100644
--- a/apps/commerce-api/src/main/resources/application.yml
+++ b/apps/commerce-api/src/main/resources/application.yml
@@ -42,9 +42,9 @@ app:
         count: 5000  # Hot 상품 ID 개수 (min~max 범위에서 랜덤 선택)
       hot-page-range: 3  # Hot Page 범위 (1~N 페이지, 0-based이므로 0~N-1 페이지가 Hot)
     ttl:
-      list-seconds: 300  # 목록 캐시 TTL (5분)
+      list-seconds: 60  # 목록 캐시 TTL (1분)
       info-seconds: 3600  # 상품 정보 캐시 TTL (1시간)
-      stat-seconds: 600  # 통계 캐시 TTL (10분)
+      stat-seconds: 60  # 통계 캐시 TTL (1분)
 
 ---
 spring:
