Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
7321da3
feat: 도메인별 event μΆ”κ°€
minor7295 Dec 10, 2025
4e96a08
feat: DIP μ μš©ν•˜μ—¬ event publisher κ΅¬ν˜„
minor7295 Dec 10, 2025
a8535c0
refactor: μ’‹μ•„μš” 수 집계λ₯Ό μŠ€μΌ€μ€„ 기반 μ²˜λ¦¬ν•˜λŠ” κ²ƒμ—μ„œ 이벀트 기반 μ²˜λ¦¬ν•˜λŠ” κ²ƒμœΌλ‘œ λ³€κ²½
minor7295 Dec 10, 2025
6ccf099
feat: 쿠폰 이벀트 μ²˜λ¦¬ν•˜λŠ” 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
6d7a433
feat: order λ„λ©”μΈμ˜ 이벀트 처리 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
b4bc8b4
feat: payment λ„λ©”μΈμ˜ 이벀트 처리 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
39502cf
feat: point λ„λ©”μΈμ˜ 이벀트 처리 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
90ca6b9
feat: product λ„λ©”μΈμ˜ 이벀트 처리 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
0b28357
λ„λ©”μΈμ΄λ²€νŠΈ
minor7295 Dec 10, 2025
5d28cf6
refactor: HeartFacadeμ—μ„œ μ’‹μ•„μš” μ²˜λ¦¬μ‹œ Product μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλΉ„μŠ€λ₯Ό μ§μ ‘ν˜ΈμΆœν•˜μ§€ μ•Šκ³  이벀트 μ‚¬μš©ν•˜β€¦
minor7295 Dec 10, 2025
c3f55f6
refactor: PurchasingFacadeμ—μ„œ μ£Όλ¬Έ μ²˜λ¦¬μ‹œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλΉ„μŠ€λ₯Ό μ§μ ‘ν˜ΈμΆœν•˜μ§€ μ•Šκ³  이벀트 μ‚¬μš©ν•˜λŠ” 방식…
minor7295 Dec 10, 2025
cf08397
test: eventhandler에 λŒ€ν•œ ν…ŒμŠ€νŠΈ μΆ”κ°€
minor7295 Dec 10, 2025
f13ea3b
refactor: event handler ν…ŒμŠ€νŠΈμ— 맞좰 μ½”λ“œ μˆ˜μ •
minor7295 Dec 10, 2025
fcb9c37
feat: 데이터 ν”Œλž«νΌμœΌλ‘œ μ£Όλ¬Έ 데이터 μ „μ†‘ν•˜λŠ” 둜직 μΆ”κ°€
minor7295 Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.TimeZone;

@ConfigurationPropertiesScan
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableFeignClients
public class CommerceApiApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.loopers.application.coupon;

import com.loopers.domain.coupon.CouponEvent;
import com.loopers.domain.coupon.CouponEventPublisher;
import com.loopers.domain.order.OrderEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
* 쿠폰 이벀트 ν•Έλ“€λŸ¬.
* <p>
* μ£Όλ¬Έ 생성 이벀트λ₯Ό λ°›μ•„ 쿠폰 μ‚¬μš© 처리λ₯Ό μˆ˜ν–‰ν•˜λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‘œμ§μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
* </p>
* <p>
* <b>DDD/EDA 관점:</b>
* <ul>
* <li><b>μ±…μž„ 뢄리:</b> CouponServiceλŠ” 쿠폰 도메인 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직, CouponEventHandlerλŠ” 이벀트 처리 둜직</li>
* <li><b>이벀트 ν•Έλ“€λŸ¬:</b> 이벀트λ₯Ό λ°›μ•„μ„œ μ²˜λ¦¬ν•˜λŠ” 역할을 λͺ…ν™•νžˆ λ‚˜νƒ€λƒ„</li>
* <li><b>도메인 경계 μ€€μˆ˜:</b> 쿠폰 도메인은 쿠폰 적용 이벀트만 λ°œν–‰ν•˜κ³ , μ£Όλ¬Έ 도메인은 μžμ‹ μ˜ μƒνƒœλ₯Ό 관리</li>
* </ul>
* </p>
*
* @author Loopers
* @version 1.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CouponEventHandler {

private final CouponService couponService;
private final CouponEventPublisher couponEventPublisher;

/**
* μ£Όλ¬Έ 생성 이벀트λ₯Ό μ²˜λ¦¬ν•˜μ—¬ 쿠폰을 μ‚¬μš©ν•˜κ³  쿠폰 적용 이벀트λ₯Ό λ°œν–‰ν•©λ‹ˆλ‹€.
* <p>
* 쿠폰 μ½”λ“œκ°€ μžˆλŠ” κ²½μš°μ—λ§Œ 쿠폰 μ‚¬μš© 처리λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.
* 쿠폰 적용 ν›„ CouponApplied 이벀트λ₯Ό λ°œν–‰ν•˜μ—¬ μ£Όλ¬Έ 도메인이 μžμ‹ μ˜ μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•˜λ„λ‘ ν•©λ‹ˆλ‹€.
* </p>
*
* @param event μ£Όλ¬Έ 생성 이벀트
*/
@Transactional
public void handleOrderCreated(OrderEvent.OrderCreated event) {
// 쿠폰 μ½”λ“œκ°€ μ—†λŠ” 경우 μ²˜λ¦¬ν•˜μ§€ μ•ŠμŒ
if (event.couponCode() == null || event.couponCode().isBlank()) {
log.debug("쿠폰 μ½”λ“œκ°€ μ—†μ–΄ 쿠폰 μ‚¬μš© 처리λ₯Ό κ±΄λ„ˆλœλ‹ˆλ‹€. (orderId: {})", event.orderId());
return;
}

// 쿠폰 μ‚¬μš© 처리 (쿠폰 μ‚¬μš© λ§ˆν‚Ή 및 할인 κΈˆμ•‘ 계산)
Integer discountAmount = couponService.applyCoupon(
event.userId(),
event.couponCode(),
event.subtotal()
);

// βœ… 도메인 이벀트 λ°œν–‰: 쿠폰이 μ μš©λ˜μ—ˆμŒ (κ³Όκ±° 사싀)
// μ£Όλ¬Έ 도메인이 이 이벀트λ₯Ό κ΅¬λ…ν•˜μ—¬ μžμ‹ μ˜ μƒνƒœλ₯Ό μ—…λ°μ΄νŠΈν•¨
couponEventPublisher.publish(CouponEvent.CouponApplied.of(
event.orderId(),
event.userId(),
event.couponCode(),
discountAmount
));

log.info("쿠폰 μ‚¬μš© 처리 μ™„λ£Œ. (orderId: {}, couponCode: {}, discountAmount: {})",
event.orderId(), event.couponCode(), discountAmount);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public Integer applyCoupon(Long userId, String couponCode, Integer subtotal) {
// μ‚¬μš©μž 쿠폰 μ €μž₯ (version 체크 μžλ™ μˆ˜ν–‰)
// λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ λ¨Όμ € μˆ˜μ •ν–ˆλ‹€λ©΄ OptimisticLockException λ°œμƒ
userCouponRepository.save(userCoupon);
// βœ… flush()λ₯Ό λͺ…μ‹œμ μœΌλ‘œ ν˜ΈμΆœν•˜μ—¬ Optimistic Lock 체크λ₯Ό μ¦‰μ‹œ μˆ˜ν–‰
// flush() μ—†μ΄λŠ” νŠΈλžœμž­μ…˜ 컀밋 μ‹œμ μ— μ²΄ν¬λ˜λ―€λ‘œ, μ—¬λŸ¬ νŠΈλžœμž­μ…˜μ΄ λ™μ‹œμ— 성곡할 수 있음
userCouponRepository.flush();
} catch (ObjectOptimisticLockingFailureException e) {
// 낙관적 락 좩돌: λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ λ¨Όμ € 쿠폰을 μ‚¬μš©ν•¨
throw new CoreException(ErrorType.CONFLICT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.loopers.application.heart;

import com.loopers.application.like.LikeService;
import com.loopers.application.product.ProductCacheService;
import com.loopers.application.product.ProductService;
import com.loopers.application.user.UserService;
import com.loopers.domain.like.Like;
Expand All @@ -23,6 +22,14 @@
* <p>
* μ’‹μ•„μš” μΆ”κ°€, μ‚­μ œ, λͺ©λ‘ 쑰회 μœ μ¦ˆμΌ€μ΄μŠ€λ₯Ό μ²˜λ¦¬ν•˜λŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλΉ„μŠ€μž…λ‹ˆλ‹€.
* </p>
* <p>
* <b>EDA 원칙 μ€€μˆ˜:</b>
* <ul>
* <li><b>이벀트 기반:</b> Like 도메인 이벀트만 λ°œν–‰ν•˜κ³ , λ‹€λ₯Έ μ• κ·Έλ¦¬κ±°νŠΈλ₯Ό 직접 μˆ˜μ •ν•˜μ§€ μ•ŠμŒ</li>
* <li><b>λŠμŠ¨ν•œ κ²°ν•©:</b> Product, User μ• κ·Έλ¦¬κ±°νŠΈμ™€μ˜ 직접적인 μ˜μ‘΄μ„± μ΅œμ†Œν™”</li>
* <li><b>μ±…μž„ 뢄리:</b> μ’‹μ•„μš” λ„λ©”μΈλ§Œ κ΄€λ¦¬ν•˜κ³ , μƒν’ˆ μ’‹μ•„μš” 수 μ§‘κ³„λŠ” 이벀트 ν•Έλ“€λŸ¬μ—μ„œ 처리</li>
* </ul>
* </p>
*
* @author Loopers
* @version 1.0
Expand All @@ -31,9 +38,8 @@
@Component
public class HeartFacade {
private final LikeService likeService;
private final UserService userService;
private final ProductService productService;
private final ProductCacheService productCacheService;
private final UserService userService; // String userIdλ₯Ό Long id둜 λ³€ν™˜ν•˜λŠ” 데만 μ‚¬μš©
private final ProductService productService; // getLikedProducts 쑰회용으둜만 μ‚¬μš©

/**
* μƒν’ˆμ— μ’‹μ•„μš”λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
Expand All @@ -57,14 +63,23 @@ public class HeartFacade {
* <li><b>λΉ„μ¦ˆλ‹ˆμŠ€ 데이터 보호:</b> 쀑볡 μ’‹μ•„μš”λ‘œ μΈν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 데이터 μ˜€μ—Ό λ°©μ§€</li>
* </ul>
* </p>
* <p>
* <b>EDA 원칙:</b>
* <ul>
* <li><b>이벀트 기반:</b> LikeService.save()κ°€ LikeEvent.LikeAdded 이벀트λ₯Ό λ°œν–‰</li>
* <li><b>λŠμŠ¨ν•œ κ²°ν•©:</b> Product μ• κ·Έλ¦¬κ±°νŠΈλ₯Ό 직접 쑰회/μˆ˜μ •ν•˜μ§€ μ•ŠμŒ. 이벀트 ν•Έλ“€λŸ¬κ°€ μƒν’ˆ μ’‹μ•„μš” 수λ₯Ό μ—…λ°μ΄νŠΈ</li>
* <li><b>μ±…μž„ 뢄리:</b> μ’‹μ•„μš” λ„λ©”μΈλ§Œ κ΄€λ¦¬ν•˜κ³ , μƒν’ˆ μ’‹μ•„μš” 수 μ§‘κ³„λŠ” ProductEventHandlerμ—μ„œ 처리</li>
* </ul>
* </p>
*
* @param userId μ‚¬μš©μž ID (String)
* @param productId μƒν’ˆ ID
* @throws CoreException μ‚¬μš©μž λ˜λŠ” μƒν’ˆμ„ 찾을 수 μ—†λŠ” 경우
* @throws CoreException μ‚¬μš©μžλ₯Ό 찾을 수 μ—†λŠ” 경우
*/
public void addLike(String userId, Long productId) {
// βœ… EDA 원칙: UserServiceλŠ” String userIdλ₯Ό Long id둜 λ³€ν™˜ν•˜λŠ” 데만 μ‚¬μš©
// Product 쑴재 μ—¬λΆ€ 검증은 제거 (이벀트 ν•Έλ“€λŸ¬μ—μ„œ μ²˜λ¦¬ν•˜κ±°λ‚˜, μ™Έλž˜ν‚€ μ œμ•½μ‘°κ±΄μœΌλ‘œ 보μž₯)
User user = loadUser(userId);
loadProduct(productId);

// λ¨Όμ € 일반 쑰회둜 쀑볡 체크 (λŒ€λΆ€λΆ„μ˜ 경우 λΉ λ₯΄κ²Œ 처리)
// ⚠️ 주의: μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 레벨 μ²΄ν¬λ§ŒμœΌλ‘œλŠ” race condition을 μ™„μ „νžˆ λ°©μ§€ν•  수 μ—†μŒ
Expand All @@ -76,18 +91,16 @@ public void addLike(String userId, Long productId) {

// μ €μž₯ μ‹œλ„ (λ™μ‹œμ„± μƒν™©μ—μ„œλŠ” UNIQUE μ œμ•½μ‘°κ±΄ μœ„λ°˜ μ˜ˆμ™Έ λ°œμƒ κ°€λŠ₯)
// βœ… UNIQUE μ œμ•½μ‘°κ±΄μ΄ μ΅œμ’… 보호: DB λ ˆλ²¨μ—μ„œ 쀑볡 μ‚½μž…μ„ 물리적으둜 λ°©μ§€
// @Transactional이 없어도 save() 호좜 μ‹œ μžλ™ νŠΈλžœμž­μ…˜μœΌλ‘œ μ˜ˆμ™Έλ₯Ό catchν•  수 있음
// βœ… LikeService.save()κ°€ LikeEvent.LikeAdded 이벀트λ₯Ό λ°œν–‰
// βœ… ProductEventHandlerκ°€ 이벀트λ₯Ό κ΅¬λ…ν•˜μ—¬ μƒν’ˆ μ’‹μ•„μš” 수 및 μΊμ‹œ μ—…λ°μ΄νŠΈ
Like like = Like.of(user.getId(), productId);
try {
likeService.save(like);
// μ’‹μ•„μš” μΆ”κ°€ 성곡 μ‹œ 둜컬 μΊμ‹œμ˜ 델타 증가
productCacheService.incrementLikeCountDelta(productId);
} catch (org.springframework.dao.DataIntegrityViolationException e) {
// UNIQUE μ œμ•½μ‘°κ±΄ μœ„λ°˜ = 이미 μ €μž₯됨 (λ©±λ“±μ„± 보μž₯)
// λ™μ‹œμ— μ—¬λŸ¬ μš”μ²­μ΄ λ“€μ–΄μ™€μ„œ λͺ¨λ‘ "μ—†μŒ"으둜 νŒλ‹¨ν•˜κ³  μ €μž₯을 μ‹œλ„ν•  λ•Œ,
// 첫 번째만 μ„±κ³΅ν•˜κ³  λ‚˜λ¨Έμ§€λŠ” UNIQUE μ œμ•½μ‘°κ±΄ μœ„λ°˜ μ˜ˆμ™Έ λ°œμƒ
// 이미 μ’‹μ•„μš”κ°€ μ‘΄μž¬ν•˜λŠ” κ²½μš°μ΄λ―€λ‘œ 정상 처리둜 κ°„μ£Ό
// 둜컬 μΊμ‹œλŠ” μ—…λ°μ΄νŠΈν•˜μ§€ μ•ŠμŒ (이미 μ’‹μ•„μš”κ°€ μ‘΄μž¬ν•˜λ―€λ‘œ)
}
}

Expand All @@ -96,28 +109,36 @@ public void addLike(String userId, Long productId) {
* <p>
* 멱등성을 보μž₯ν•©λ‹ˆλ‹€. μ’‹μ•„μš”κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우 아무 μž‘μ—…λ„ μˆ˜ν–‰ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
* </p>
* <p>
* <b>EDA 원칙:</b>
* <ul>
* <li><b>이벀트 기반:</b> LikeService.delete()κ°€ LikeEvent.LikeRemoved 이벀트λ₯Ό λ°œν–‰</li>
* <li><b>λŠμŠ¨ν•œ κ²°ν•©:</b> Product μ• κ·Έλ¦¬κ±°νŠΈλ₯Ό 직접 쑰회/μˆ˜μ •ν•˜μ§€ μ•ŠμŒ. 이벀트 ν•Έλ“€λŸ¬κ°€ μƒν’ˆ μ’‹μ•„μš” 수λ₯Ό μ—…λ°μ΄νŠΈ</li>
* <li><b>μ±…μž„ 뢄리:</b> μ’‹μ•„μš” λ„λ©”μΈλ§Œ κ΄€λ¦¬ν•˜κ³ , μƒν’ˆ μ’‹μ•„μš” 수 μ§‘κ³„λŠ” ProductEventHandlerμ—μ„œ 처리</li>
* </ul>
* </p>
*
* @param userId μ‚¬μš©μž ID (String)
* @param productId μƒν’ˆ ID
* @throws CoreException μ‚¬μš©μž λ˜λŠ” μƒν’ˆμ„ 찾을 수 μ—†λŠ” 경우
* @throws CoreException μ‚¬μš©μžλ₯Ό 찾을 수 μ—†λŠ” 경우
*/
public void removeLike(String userId, Long productId) {
// βœ… EDA 원칙: UserServiceλŠ” String userIdλ₯Ό Long id둜 λ³€ν™˜ν•˜λŠ” 데만 μ‚¬μš©
// Product 쑴재 μ—¬λΆ€ 검증은 제거 (이벀트 ν•Έλ“€λŸ¬μ—μ„œ μ²˜λ¦¬ν•˜κ±°λ‚˜, μ™Έλž˜ν‚€ μ œμ•½μ‘°κ±΄μœΌλ‘œ 보μž₯)
User user = loadUser(userId);
loadProduct(productId);

Optional<Like> like = likeService.getLike(user.getId(), productId);
if (like.isEmpty()) {
return;
}

try {
// βœ… LikeService.delete()κ°€ LikeEvent.LikeRemoved 이벀트λ₯Ό λ°œν–‰
// βœ… ProductEventHandlerκ°€ 이벀트λ₯Ό κ΅¬λ…ν•˜μ—¬ μƒν’ˆ μ’‹μ•„μš” 수 및 μΊμ‹œ μ—…λ°μ΄νŠΈ
likeService.delete(like.get());
// μ’‹μ•„μš” μ·¨μ†Œ 성곡 μ‹œ 둜컬 μΊμ‹œμ˜ 델타 κ°μ†Œ
productCacheService.decrementLikeCountDelta(productId);
} catch (Exception e) {
// λ™μ‹œμ„± μƒν™©μ—μ„œ 이미 μ‚­μ œλœ 경우 λ“± μ˜ˆμ™Έ λ°œμƒ κ°€λŠ₯
// λ©±λ“±μ„± 보μž₯: 이미 μ‚­μ œλœ 경우 정상 처리둜 κ°„μ£Ό
// 둜컬 μΊμ‹œλŠ” μ—…λ°μ΄νŠΈν•˜μ§€ μ•ŠμŒ (이미 μ‚­μ œλ˜μ—ˆμœΌλ―€λ‘œ)
}
}

Expand All @@ -129,11 +150,18 @@ public void removeLike(String userId, Long productId) {
* <p>
* <b>μ’‹μ•„μš” 수 쑰회 μ „λž΅:</b>
* <ul>
* <li><b>비동기 집계:</b> Product.likeCount ν•„λ“œ μ‚¬μš© (μŠ€μΌ€μ€„λŸ¬λ‘œ 주기적 동기화)</li>
* <li><b>Eventually Consistent:</b> μ•½κ°„μ˜ μ§€μ—° ν—ˆμš© (μ΅œλŒ€ 5초)</li>
* <li><b>이벀트 기반 집계:</b> Product.likeCount ν•„λ“œ μ‚¬μš© (LikeEvent둜 μ‹€μ‹œκ°„ μ—…λ°μ΄νŠΈ)</li>
* <li><b>Strong Consistency:</b> 이벀트 기반으둜 μ‹€μ‹œκ°„ 반영</li>
* <li><b>μ„±λŠ₯ μ΅œμ ν™”:</b> COUNT(*) 쿼리 없이 컬럼만 읽으면 됨</li>
* </ul>
* </p>
* <p>
* <b>EDA 원칙:</b>
* <ul>
* <li><b>쑰회 νŠΉμ„±:</b> 쑰회 μΏΌλ¦¬λŠ” 이벀트둜 μ²˜λ¦¬ν•˜κΈ° μ–΄λ €μš°λ―€λ‘œ ProductService 의쑴 ν—ˆμš©</li>
* <li><b>μ΅œμ†Œ 의쑴:</b> 쑰회용으둜만 μ‚¬μš©ν•˜λ©°, μˆ˜μ • μž‘μ—…μ€ μˆ˜ν–‰ν•˜μ§€ μ•ŠμŒ</li>
* </ul>
* </p>
*
* @param userId μ‚¬μš©μž ID (String)
* @return μ’‹μ•„μš”ν•œ μƒν’ˆ λͺ©λ‘
Expand All @@ -156,6 +184,7 @@ public List<LikedProduct> getLikedProducts(String userId) {
.toList();

// βœ… 배치 쑰회둜 N+1 쿼리 문제 ν•΄κ²°
// ⚠️ 쑰회 νŠΉμ„±μƒ ProductService μ˜μ‘΄μ€ ν—ˆμš© (이벀트둜 μ²˜λ¦¬ν•˜κΈ° 어렀움)
Map<Long, Product> productMap = productService.getProducts(productIds).stream()
.collect(Collectors.toMap(Product::getId, product -> product));

Expand All @@ -165,7 +194,7 @@ public List<LikedProduct> getLikedProducts(String userId) {
}

// μ’‹μ•„μš” λͺ©λ‘μ„ μƒν’ˆ 정보와 μ’‹μ•„μš” μˆ˜μ™€ ν•¨κ»˜ λ³€ν™˜
// βœ… Product.likeCount ν•„λ“œ μ‚¬μš© (비동기 μ§‘κ³„λœ κ°’)
// βœ… Product.likeCount ν•„λ“œ μ‚¬μš© (이벀트 기반 μ‹€μ‹œκ°„ μ§‘κ³„λœ κ°’)
return likes.stream()
.map(like -> {
Product product = productMap.get(like.getProductId());
Expand All @@ -179,14 +208,20 @@ public List<LikedProduct> getLikedProducts(String userId) {
.toList();
}

/**
* String userIdλ₯Ό Long id둜 λ³€ν™˜ν•©λ‹ˆλ‹€.
* <p>
* EDA 원칙에 따라 μ΅œμ†Œν•œμ˜ UserService 의쑴만 μ‚¬μš©ν•©λ‹ˆλ‹€.
* </p>
*
* @param userId μ‚¬μš©μž ID (String)
* @return μ‚¬μš©μž μ—”ν‹°ν‹°
* @throws CoreException μ‚¬μš©μžλ₯Ό 찾을 수 μ—†λŠ” 경우
*/
private User loadUser(String userId) {
return userService.getUser(userId);
}

private Product loadProduct(Long productId) {
return productService.getProduct(productId);
}

/**
* μ’‹μ•„μš”ν•œ μƒν’ˆ 정보.
*
Expand Down Expand Up @@ -225,7 +260,7 @@ public static LikedProduct from(Product product) {
product.getPrice(),
product.getStock(),
product.getBrandId(),
product.getLikeCount() // βœ… Product.likeCount ν•„λ“œ μ‚¬μš© (비동기 μ§‘κ³„λœ κ°’)
product.getLikeCount() // βœ… Product.likeCount ν•„λ“œ μ‚¬μš© (이벀트 기반 μ‹€μ‹œκ°„ μ§‘κ³„λœ κ°’)
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.loopers.application.like;

import com.loopers.domain.like.Like;
import com.loopers.domain.like.LikeEvent;
import com.loopers.domain.like.LikeEventPublisher;
import com.loopers.domain.like.LikeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
Expand All @@ -23,6 +25,7 @@
@Component
public class LikeService {
private final LikeRepository likeRepository;
private final LikeEventPublisher likeEventPublisher;

/**
* μ‚¬μš©μž ID와 μƒν’ˆ ID둜 μ’‹μ•„μš”λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.
Expand All @@ -38,22 +41,36 @@ public Optional<Like> getLike(Long userId, Long productId) {

/**
* μ’‹μ•„μš”λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.
* <p>
* μ €μž₯ 성곡 μ‹œ μ’‹μ•„μš” μΆ”κ°€ 이벀트λ₯Ό λ°œν–‰ν•©λ‹ˆλ‹€.
* </p>
*
* @param like μ €μž₯ν•  μ’‹μ•„μš”
* @return μ €μž₯된 μ’‹μ•„μš”
*/
@Transactional
public Like save(Like like) {
return likeRepository.save(like);
Like savedLike = likeRepository.save(like);

// βœ… 도메인 이벀트 λ°œν–‰: μ’‹μ•„μš”κ°€ μΆ”κ°€λ˜μ—ˆμŒ (κ³Όκ±° 사싀)
likeEventPublisher.publish(LikeEvent.LikeAdded.from(savedLike));

return savedLike;
}

/**
* μ’‹μ•„μš”λ₯Ό μ‚­μ œν•©λ‹ˆλ‹€.
* <p>
* μ‚­μ œ 전에 μ’‹μ•„μš” μ·¨μ†Œ 이벀트λ₯Ό λ°œν–‰ν•©λ‹ˆλ‹€.
* </p>
*
* @param like μ‚­μ œν•  μ’‹μ•„μš”
*/
@Transactional
public void delete(Like like) {
// βœ… 도메인 이벀트 λ°œν–‰: μ’‹μ•„μš”κ°€ μ·¨μ†Œλ˜μ—ˆμŒ (κ³Όκ±° 사싀)
likeEventPublisher.publish(LikeEvent.LikeRemoved.from(like));

likeRepository.delete(like);
}

Expand Down
Loading