diff --git a/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogBrandFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandService.java similarity index 57% rename from apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogBrandFacade.java rename to apps/commerce-api/src/main/java/com/loopers/application/brand/BrandService.java index 4cdc2f177..94f1a63b8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogBrandFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandService.java @@ -1,4 +1,4 @@ -package com.loopers.application.catalog; +package com.loopers.application.brand; import com.loopers.domain.brand.Brand; import com.loopers.domain.brand.BrandRepository; @@ -6,6 +6,9 @@ import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; /** * 브랜드 조회 파사드. @@ -18,9 +21,36 @@ */ @RequiredArgsConstructor @Component -public class CatalogBrandFacade { +public class BrandService { private final BrandRepository brandRepository; + /** + * 브랜드 ID로 브랜드를 조회합니다. + * + * @param brandId 브랜드 ID + * @return 조회된 브랜드 + * @throws CoreException 브랜드를 찾을 수 없는 경우 + */ + @Transactional(readOnly = true) + public Brand getBrand(Long brandId) { + return brandRepository.findById(brandId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다.")); + } + + /** + * 브랜드 ID 목록으로 브랜드 목록을 조회합니다. + *

+ * 배치 조회를 통해 N+1 쿼리 문제를 해결합니다. + *

+ * + * @param brandIds 조회할 브랜드 ID 목록 + * @return 조회된 브랜드 목록 + */ + @Transactional(readOnly = true) + public List getBrands(List brandIds) { + return brandRepository.findAllById(brandIds); + } + /** * 브랜드 정보를 조회합니다. * @@ -28,9 +58,8 @@ public class CatalogBrandFacade { * @return 브랜드 정보 * @throws CoreException 브랜드를 찾을 수 없는 경우 */ - public BrandInfo getBrand(Long brandId) { - Brand brand = brandRepository.findById(brandId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다.")); + public BrandInfo getBrandInfo(Long brandId) { + Brand brand = getBrand(brandId); return BrandInfo.from(brand); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java similarity index 85% rename from apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogProductFacade.java rename to apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java index 1ebf5c394..f46e74301 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/catalog/CatalogFacade.java @@ -1,10 +1,11 @@ package com.loopers.application.catalog; +import com.loopers.application.brand.BrandService; +import com.loopers.application.product.ProductCacheService; +import com.loopers.application.product.ProductService; import com.loopers.domain.brand.Brand; -import com.loopers.domain.brand.BrandRepository; import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductDetail; -import com.loopers.domain.product.ProductRepository; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -25,9 +26,9 @@ */ @RequiredArgsConstructor @Component -public class CatalogProductFacade { - private final ProductRepository productRepository; - private final BrandRepository brandRepository; +public class CatalogFacade { + private final BrandService brandService; + private final ProductService productService; private final ProductCacheService productCacheService; /** @@ -54,8 +55,8 @@ public ProductInfoList getProducts(Long brandId, String sort, int page, int size } // 캐시에 없으면 DB에서 조회 - long totalCount = productRepository.countAll(brandId); - List products = productRepository.findAll(brandId, normalizedSort, page, size); + long totalCount = productService.countAll(brandId); + List products = productService.findAll(brandId, normalizedSort, page, size); if (products.isEmpty()) { ProductInfoList emptyResult = new ProductInfoList(List.of(), totalCount, page, size); @@ -72,7 +73,7 @@ public ProductInfoList getProducts(Long brandId, String sort, int page, int size .toList(); // 브랜드 배치 조회 및 Map으로 변환 (O(1) 조회를 위해) - Map brandMap = brandRepository.findAllById(brandIds).stream() + Map brandMap = brandService.getBrands(brandIds).stream() .collect(Collectors.toMap(Brand::getId, brand -> brand)); // 상품 정보 변환 (이미 조회한 Product 재사용) @@ -116,12 +117,10 @@ public ProductInfo getProduct(Long productId) { } // 캐시에 없으면 DB에서 조회 - Product product = productRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); + Product product = productService.getProduct(productId); // 브랜드 조회 - Brand brand = brandRepository.findById(product.getBrandId()) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "브랜드를 찾을 수 없습니다.")); + Brand brand = brandService.getBrand(product.getBrandId()); // ✅ Product.likeCount 필드 사용 (비동기 집계된 값) Long likesCount = product.getLikeCount(); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java similarity index 89% rename from apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java rename to apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java index ec0b09d2b..b99503199 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java @@ -1,5 +1,9 @@ -package com.loopers.domain.coupon; +package com.loopers.application.coupon; +import com.loopers.domain.coupon.Coupon; +import com.loopers.domain.coupon.CouponRepository; +import com.loopers.domain.coupon.UserCoupon; +import com.loopers.domain.coupon.UserCouponRepository; import com.loopers.domain.coupon.discount.CouponDiscountStrategyFactory; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -9,10 +13,10 @@ import org.springframework.transaction.annotation.Transactional; /** - * 쿠폰 도메인 서비스. + * 쿠폰 애플리케이션 서비스. *

- * 쿠폰 조회, 사용 등의 도메인 로직을 처리합니다. - * Repository에 의존하며 비즈니스 규칙을 캡슐화합니다. + * 쿠폰 조회, 사용 등의 애플리케이션 로직을 처리합니다. + * Repository에 의존하며 트랜잭션 관리 및 동시성 제어를 담당합니다. *

* * @author Loopers 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/heart/HeartFacade.java similarity index 86% rename from apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java rename to apps/commerce-api/src/main/java/com/loopers/application/heart/HeartFacade.java index de21d46b5..f562072d2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/heart/HeartFacade.java @@ -1,12 +1,12 @@ -package com.loopers.application.like; +package com.loopers.application.heart; -import com.loopers.application.catalog.ProductCacheService; +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; -import com.loopers.domain.like.LikeRepository; import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductRepository; import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.springframework.transaction.annotation.Transactional; @@ -29,10 +29,10 @@ */ @RequiredArgsConstructor @Component -public class LikeFacade { - private final LikeRepository likeRepository; - private final UserRepository userRepository; - private final ProductRepository productRepository; +public class HeartFacade { + private final LikeService likeService; + private final UserService userService; + private final ProductService productService; private final ProductCacheService productCacheService; /** @@ -69,7 +69,7 @@ public void addLike(String userId, Long productId) { // 먼저 일반 조회로 중복 체크 (대부분의 경우 빠르게 처리) // ⚠️ 주의: 애플리케이션 레벨 체크만으로는 race condition을 완전히 방지할 수 없음 // 동시에 두 요청이 들어오면 둘 다 "없음"으로 판단 → 둘 다 저장 시도 가능 - Optional existingLike = likeRepository.findByUserIdAndProductId(user.getId(), productId); + Optional existingLike = likeService.getLike(user.getId(), productId); if (existingLike.isPresent()) { return; } @@ -79,7 +79,7 @@ public void addLike(String userId, Long productId) { // @Transactional이 없어도 save() 호출 시 자동 트랜잭션으로 예외를 catch할 수 있음 Like like = Like.of(user.getId(), productId); try { - likeRepository.save(like); + likeService.save(like); // 좋아요 추가 성공 시 로컬 캐시의 델타 증가 productCacheService.incrementLikeCountDelta(productId); } catch (org.springframework.dao.DataIntegrityViolationException e) { @@ -105,13 +105,13 @@ public void removeLike(String userId, Long productId) { User user = loadUser(userId); loadProduct(productId); - Optional like = likeRepository.findByUserIdAndProductId(user.getId(), productId); + Optional like = likeService.getLike(user.getId(), productId); if (like.isEmpty()) { return; } try { - likeRepository.delete(like.get()); + likeService.delete(like.get()); // 좋아요 취소 성공 시 로컬 캐시의 델타 감소 productCacheService.decrementLikeCountDelta(productId); } catch (Exception e) { @@ -144,7 +144,7 @@ public List getLikedProducts(String userId) { User user = loadUser(userId); // 사용자의 좋아요 목록 조회 - List likes = likeRepository.findAllByUserId(user.getId()); + List likes = likeService.getLikesByUserId(user.getId()); if (likes.isEmpty()) { return List.of(); @@ -156,7 +156,7 @@ public List getLikedProducts(String userId) { .toList(); // ✅ 배치 조회로 N+1 쿼리 문제 해결 - Map productMap = productRepository.findAllById(productIds).stream() + Map productMap = productService.getProducts(productIds).stream() .collect(Collectors.toMap(Product::getId, product -> product)); // 요청한 상품 ID와 조회된 상품 수가 일치하는지 확인 @@ -180,17 +180,11 @@ public List getLikedProducts(String userId) { } private User loadUser(String userId) { - User user = userRepository.findByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); - } - return user; + return userService.getUser(userId); } private Product loadProduct(Long productId) { - return productRepository.findById(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, - String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", productId))); + return productService.getProduct(productId); } /** diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeService.java new file mode 100644 index 000000000..f421433cd --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeService.java @@ -0,0 +1,70 @@ +package com.loopers.application.like; + +import com.loopers.domain.like.Like; +import com.loopers.domain.like.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * 좋아요 애플리케이션 서비스. + *

+ * 좋아요 조회, 저장, 삭제 등의 애플리케이션 로직을 처리합니다. + * Repository에 의존하며 트랜잭션 관리를 담당합니다. + *

+ * + * @author Loopers + * @version 1.0 + */ +@RequiredArgsConstructor +@Component +public class LikeService { + private final LikeRepository likeRepository; + + /** + * 사용자 ID와 상품 ID로 좋아요를 조회합니다. + * + * @param userId 사용자 ID + * @param productId 상품 ID + * @return 조회된 좋아요를 담은 Optional + */ + @Transactional(readOnly = true) + public Optional getLike(Long userId, Long productId) { + return likeRepository.findByUserIdAndProductId(userId, productId); + } + + /** + * 좋아요를 저장합니다. + * + * @param like 저장할 좋아요 + * @return 저장된 좋아요 + */ + @Transactional + public Like save(Like like) { + return likeRepository.save(like); + } + + /** + * 좋아요를 삭제합니다. + * + * @param like 삭제할 좋아요 + */ + @Transactional + public void delete(Like like) { + likeRepository.delete(like); + } + + /** + * 사용자 ID로 좋아요한 상품 목록을 조회합니다. + * + * @param userId 사용자 ID + * @return 좋아요 목록 + */ + @Transactional(readOnly = true) + public List getLikesByUserId(Long userId) { + return likeRepository.findAllByUserId(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/application/order/OrderService.java similarity index 92% rename from apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java rename to apps/commerce-api/src/main/java/com/loopers/application/order/OrderService.java index e74c024d7..18f9f8c2e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderService.java @@ -1,5 +1,9 @@ -package com.loopers.domain.order; +package com.loopers.application.order; +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderRepository; +import com.loopers.domain.order.OrderStatus; import com.loopers.domain.payment.PaymentStatus; import com.loopers.domain.product.Product; import com.loopers.domain.user.Point; @@ -15,9 +19,10 @@ import java.util.Optional; /** - * 주문 도메인 서비스. + * 주문 애플리케이션 서비스. *

- * 주문의 기본 CRUD 및 상태 변경을 담당합니다. + * 주문의 기본 CRUD 및 상태 변경을 담당하는 애플리케이션 서비스입니다. + * Repository에 의존하며 트랜잭션 관리를 담당합니다. *

* * @author Loopers @@ -60,7 +65,7 @@ public Order getById(Long orderId) { * @return 조회된 주문 (없으면 Optional.empty()) */ @Transactional(readOnly = true) - public Optional findById(Long orderId) { + public Optional getOrder(Long orderId) { return orderRepository.findById(orderId); } @@ -71,7 +76,7 @@ public Optional findById(Long orderId) { * @return 해당 사용자의 주문 목록 */ @Transactional(readOnly = true) - public List findAllByUserId(Long userId) { + public List getOrdersByUserId(Long userId) { return orderRepository.findAllByUserId(userId); } @@ -82,7 +87,7 @@ public List findAllByUserId(Long userId) { * @return 해당 상태의 주문 목록 */ @Transactional(readOnly = true) - public List findAllByStatus(OrderStatus status) { + public List getOrdersByStatus(OrderStatus status) { return orderRepository.findAllByStatus(status); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRequestCommand.java similarity index 96% rename from apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java rename to apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRequestCommand.java index 3834136a6..b028afc3f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PaymentRequestCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentRequestCommand.java @@ -1,4 +1,4 @@ -package com.loopers.application.purchasing; +package com.loopers.application.payment; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java similarity index 93% rename from apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java rename to apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java index b7308675c..773c9b27d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentService.java @@ -1,6 +1,6 @@ -package com.loopers.domain.payment; +package com.loopers.application.payment; -import com.loopers.application.purchasing.PaymentRequestCommand; +import com.loopers.domain.payment.*; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -15,10 +15,10 @@ import java.util.Optional; /** - * 결제 도메인 서비스. + * 결제 애플리케이션 서비스. *

- * 결제의 생성, 조회, 상태 변경 및 PG 연동을 담당합니다. - * 도메인 로직은 Payment 엔티티에 위임하며, Service는 조회/저장 및 PG 연동을 담당합니다. + * 결제의 생성, 조회, 상태 변경 및 PG 연동을 담당하는 애플리케이션 서비스입니다. + * 도메인 로직은 Payment 엔티티에 위임하며, Service는 조회/저장, 트랜잭션 관리 및 PG 연동을 담당합니다. *

* * @author Loopers @@ -30,7 +30,7 @@ public class PaymentService { private final PaymentRepository paymentRepository; - private final PaymentGateway paymentGateway; // 인터페이스에 의존 (DIP 준수) + private final PaymentGateway paymentGateway; private final PaymentFailureClassifier paymentFailureClassifier; @Value("${payment.callback.base-url}") @@ -123,8 +123,7 @@ public Payment create( */ @Transactional public void toSuccess(Long paymentId, LocalDateTime completedAt) { - Payment payment = paymentRepository.findById(paymentId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제를 찾을 수 없습니다.")); + Payment payment = getPayment(paymentId); payment.toSuccess(completedAt); // Entity에 위임 paymentRepository.save(payment); } @@ -142,8 +141,7 @@ public void toSuccess(Long paymentId, LocalDateTime completedAt) { */ @Transactional public void toFailed(Long paymentId, String failureReason, LocalDateTime completedAt) { - Payment payment = paymentRepository.findById(paymentId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제를 찾을 수 없습니다.")); + Payment payment = getPayment(paymentId); payment.toFailed(failureReason, completedAt); // Entity에 위임 paymentRepository.save(payment); } @@ -156,7 +154,7 @@ public void toFailed(Long paymentId, String failureReason, LocalDateTime complet * @throws CoreException 결제를 찾을 수 없는 경우 */ @Transactional(readOnly = true) - public Payment findById(Long paymentId) { + public Payment getPayment(Long paymentId) { return paymentRepository.findById(paymentId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제를 찾을 수 없습니다.")); } @@ -168,7 +166,7 @@ public Payment findById(Long paymentId) { * @return 조회된 Payment (없으면 Optional.empty()) */ @Transactional(readOnly = true) - public Optional findByOrderId(Long orderId) { + public Optional getPaymentByOrderId(Long orderId) { return paymentRepository.findByOrderId(orderId); } @@ -179,7 +177,7 @@ public Optional findByOrderId(Long orderId) { * @return 해당 사용자의 결제 목록 */ @Transactional(readOnly = true) - public List findAllByUserId(Long userId) { + public List getPaymentsByUserId(Long userId) { return paymentRepository.findAllByUserId(userId); } @@ -190,7 +188,7 @@ public List findAllByUserId(Long userId) { * @return 해당 상태의 결제 목록 */ @Transactional(readOnly = true) - public List findAllByStatus(PaymentStatus status) { + public List getPaymentsByStatus(PaymentStatus status) { return paymentRepository.findAllByStatus(status); } @@ -286,7 +284,7 @@ public PaymentStatus getPaymentStatus(String userId, Long orderId) { */ @Transactional public void handleCallback(Long orderId, String transactionKey, PaymentStatus status, String reason) { - Optional paymentOpt = findByOrderId(orderId); + Optional paymentOpt = getPaymentByOrderId(orderId); if (paymentOpt.isEmpty()) { log.warn("콜백 처리 시 결제를 찾을 수 없습니다. (orderId: {})", orderId); return; @@ -326,7 +324,7 @@ public void recoverAfterTimeout(String userId, Long orderId, Duration delayDurat // 결제 상태 조회 PaymentStatus status = getPaymentStatus(userId, orderId); - Optional paymentOpt = findByOrderId(orderId); + Optional paymentOpt = getPaymentByOrderId(orderId); if (paymentOpt.isEmpty()) { log.warn("복구 시 결제를 찾을 수 없습니다. (orderId: {})", orderId); diff --git a/apps/commerce-api/src/main/java/com/loopers/application/pointwallet/PointWalletFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/pointwallet/PointWalletFacade.java deleted file mode 100644 index 1e1d171a1..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/pointwallet/PointWalletFacade.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.loopers.application.pointwallet; - -import com.loopers.domain.user.Point; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 포인트 지갑 파사드. - *

- * 포인트 조회 및 충전 유즈케이스를 처리하는 애플리케이션 서비스입니다. - * 트랜잭션 경계를 관리하여 데이터 일관성을 보장합니다. - *

- * - * @author Loopers - * @version 1.0 - */ -@RequiredArgsConstructor -@Component -public class PointWalletFacade { - private final UserRepository userRepository; - - /** - * 사용자의 포인트를 조회합니다. - * - * @param userId 조회할 사용자 ID - * @return 포인트 정보 - * @throws CoreException 사용자를 찾을 수 없는 경우 - */ - public PointsInfo getPoints(String userId) { - User user = userRepository.findByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); - } - return PointsInfo.from(user); - } - - /** - * 사용자의 포인트를 충전합니다. - *

- * 트랜잭션 내에서 실행되어 데이터 일관성을 보장합니다. - *

- * - * @param userId 충전할 사용자 ID - * @param amount 충전할 포인트 금액 - * @return 충전된 포인트 정보 - * @throws CoreException 사용자를 찾을 수 없거나 충전 금액이 유효하지 않은 경우 - */ - @Transactional - public PointsInfo chargePoint(String userId, Long amount) { - User user = userRepository.findByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); - } - Point point = Point.of(amount); - user.receivePoint(point); - User savedUser = userRepository.save(user); - return PointsInfo.from(savedUser); - } - - /** - * 포인트 정보를 담는 레코드. - * - * @param userId 사용자 ID - * @param balance 포인트 잔액 - */ - public record PointsInfo(String userId, Long balance) { - /** - * User 엔티티로부터 PointsInfo를 생성합니다. - * - * @param user 사용자 엔티티 - * @return 생성된 PointsInfo - */ - public static PointsInfo from(User user) { - return new PointsInfo(user.getUserId(), user.getPoint().getValue()); - } - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductCacheService.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java similarity index 98% rename from apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductCacheService.java rename to apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java index 8428efa50..4a1e43a23 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/catalog/ProductCacheService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java @@ -1,7 +1,9 @@ -package com.loopers.application.catalog; +package com.loopers.application.product; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.loopers.application.catalog.ProductInfo; +import com.loopers.application.catalog.ProductInfoList; import com.loopers.domain.product.ProductDetail; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java new file mode 100644 index 000000000..82703ba2e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java @@ -0,0 +1,108 @@ +package com.loopers.application.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +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.util.List; + +/** + * 상품 애플리케이션 서비스. + *

+ * 상품 조회, 저장 등의 애플리케이션 로직을 처리합니다. + * Repository에 의존하며 트랜잭션 관리를 담당합니다. + *

+ * + * @author Loopers + * @version 1.0 + */ +@RequiredArgsConstructor +@Component +public class ProductService { + private final ProductRepository productRepository; + + /** + * 상품 ID로 상품을 조회합니다. + * + * @param productId 조회할 상품 ID + * @return 조회된 상품 + * @throws CoreException 상품을 찾을 수 없는 경우 + */ + @Transactional(readOnly = true) + public Product getProduct(Long productId) { + return productRepository.findById(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, + String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", productId))); + } + + /** + * 상품 ID 목록으로 상품 목록을 조회합니다. + *

+ * 배치 조회를 통해 N+1 쿼리 문제를 해결합니다. + *

+ * + * @param productIds 조회할 상품 ID 목록 + * @return 조회된 상품 목록 + */ + @Transactional(readOnly = true) + public List getProducts(List productIds) { + return productRepository.findAllById(productIds); + } + + /** + * 상품 ID로 상품을 조회합니다. (비관적 락) + *

+ * 동시성 제어가 필요한 경우 사용합니다. (예: 재고 차감) + *

+ * + * @param productId 조회할 상품 ID + * @return 조회된 상품 + * @throws CoreException 상품을 찾을 수 없는 경우 + */ + @Transactional + public Product getProductForUpdate(Long productId) { + return productRepository.findByIdForUpdate(productId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, + String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", productId))); + } + + /** + * 상품 목록을 저장합니다. + * + * @param products 저장할 상품 목록 + */ + @Transactional + public void saveAll(List products) { + products.forEach(productRepository::save); + } + + /** + * 상품 목록을 조회합니다. + * + * @param brandId 브랜드 ID (null이면 전체 조회) + * @param sort 정렬 기준 (latest, price_asc, likes_desc) + * @param page 페이지 번호 (0부터 시작) + * @param size 페이지당 상품 수 + * @return 상품 목록 + */ + @Transactional(readOnly = true) + public List findAll(Long brandId, String sort, int page, int size) { + return productRepository.findAll(brandId, sort, page, size); + } + + /** + * 상품 목록의 총 개수를 조회합니다. + * + * @param brandId 브랜드 ID (null이면 전체 조회) + * @return 상품 총 개수 + */ + @Transactional(readOnly = true) + public long countAll(Long brandId) { + return productRepository.countAll(brandId); + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java index 3da1072dd..15a1fc404 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java @@ -2,17 +2,17 @@ import com.loopers.domain.order.Order; import com.loopers.domain.order.OrderItem; -import com.loopers.domain.order.OrderService; +import com.loopers.application.order.OrderService; import com.loopers.domain.order.OrderStatus; import com.loopers.domain.product.Product; -import com.loopers.domain.product.ProductService; +import com.loopers.application.product.ProductService; import com.loopers.domain.user.Point; import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.domain.coupon.CouponService; +import com.loopers.application.user.UserService; +import com.loopers.application.coupon.CouponService; import com.loopers.infrastructure.payment.PaymentGatewayDto; import com.loopers.domain.payment.PaymentRequestResult; -import com.loopers.domain.payment.PaymentService; +import com.loopers.application.payment.PaymentService; import com.loopers.domain.payment.Payment; import com.loopers.domain.payment.PaymentStatus; import com.loopers.domain.payment.CardType; @@ -38,8 +38,12 @@ /** * 구매 파사드. *

- * 주문 생성과 결제(포인트 차감), 재고 조정, 외부 연동을 조율한다. + * 주문 생성과 결제(포인트 차감), 재고 조정, 외부 연동을 조율하는 애플리케이션 서비스입니다. + * 여러 도메인 서비스를 조합하여 구매 유즈케이스를 처리합니다. *

+ * + * @author Loopers + * @version 1.0 */ @Slf4j @RequiredArgsConstructor @@ -120,7 +124,7 @@ public OrderInfo createOrder(String userId, List commands, Lon // - userId는 UNIQUE 인덱스가 있어 Lock 범위 최소화 (Record Lock만 적용) // - Lost Update 방지: 동시 주문 시 포인트 중복 차감 방지 (금전적 손실 방지) // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음 - User user = userService.findByUserIdForUpdate(userId); + User user = userService.getUserForUpdate(userId); // ✅ Deadlock 방지: 상품 ID를 정렬하여 일관된 락 획득 순서 보장 // 여러 상품을 주문할 때, 항상 동일한 순서로 락을 획득하여 deadlock 방지 @@ -144,7 +148,7 @@ public OrderInfo createOrder(String userId, List commands, Lon // - Lost Update 방지: 동시 주문 시 재고 음수 방지 및 정확한 차감 보장 (재고 oversell 방지) // - 트랜잭션 내부에 외부 I/O 없음, lock holding time 매우 짧음 // - ✅ 정렬된 순서로 락 획득하여 deadlock 방지 - Product product = productService.findByIdForUpdate(productId); + Product product = productService.getProductForUpdate(productId); productMap.put(productId, product); } @@ -296,7 +300,7 @@ public void cancelOrder(Order order, User user) { } // ✅ Deadlock 방지: User 락을 먼저 획득하여 createOrder와 동일한 락 획득 순서 보장 - User lockedUser = userService.findByUserIdForUpdate(user.getUserId()); + User lockedUser = userService.getUserForUpdate(user.getUserId()); if (lockedUser == null) { throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); } @@ -311,7 +315,7 @@ public void cancelOrder(Order order, User user) { // 정렬된 순서대로 상품 락 획득 (Deadlock 방지) Map productMap = new java.util.HashMap<>(); for (Long productId : sortedProductIds) { - Product product = productService.findByIdForUpdate(productId); + Product product = productService.getProductForUpdate(productId); productMap.put(productId, product); } @@ -321,7 +325,7 @@ public void cancelOrder(Order order, User user) { .toList(); // 실제로 사용된 포인트만 환불 (Payment에서 확인) - Long refundPointAmount = paymentService.findByOrderId(order.getId()) + Long refundPointAmount = paymentService.getPaymentByOrderId(order.getId()) .map(Payment::getUsedPoint) .orElse(0L); @@ -341,8 +345,8 @@ public void cancelOrder(Order order, User user) { */ @Transactional(readOnly = true) public List getOrders(String userId) { - User user = userService.findByUserId(userId); - List orders = orderService.findAllByUserId(user.getId()); + User user = userService.getUser(userId); + List orders = orderService.getOrdersByUserId(user.getId()); return orders.stream() .map(OrderInfo::from) .toList(); @@ -357,7 +361,7 @@ public List getOrders(String userId) { */ @Transactional(readOnly = true) public OrderInfo getOrder(String userId, Long orderId) { - User user = userService.findByUserId(userId); + User user = userService.getUser(userId); Order order = orderService.getById(orderId); if (!order.getUserId().equals(user.getId())) { @@ -483,7 +487,7 @@ public boolean updateOrderStatusByPaymentResult( String reason ) { try { - Order order = orderService.findById(orderId).orElse(null); + Order order = orderService.getOrder(orderId).orElse(null); if (order == null) { log.warn("주문 상태 업데이트 시 주문을 찾을 수 없습니다. (orderId: {})", orderId); @@ -509,7 +513,7 @@ public boolean updateOrderStatusByPaymentResult( return true; } else if (paymentStatus == PaymentStatus.FAILED) { // 결제 실패: 주문 취소 및 리소스 원복 - User user = userService.findById(order.getUserId()); + User user = userService.getUserById(order.getUserId()); if (user == null) { log.warn("주문 상태 업데이트 시 사용자를 찾을 수 없습니다. (orderId: {}, userId: {})", orderId, order.getUserId()); @@ -551,7 +555,7 @@ public void updateOrderStatusToCompleted(Long orderId, String transactionKey) { } // Payment 상태 업데이트 (PaymentService 사용) - paymentService.findByOrderId(orderId).ifPresent(payment -> { + paymentService.getPaymentByOrderId(orderId).ifPresent(payment -> { if (payment.getStatus() == PaymentStatus.PENDING) { paymentService.toSuccess(payment.getId(), java.time.LocalDateTime.now()); } @@ -741,7 +745,7 @@ private PaymentGatewayDto.TransactionStatus verifyCallbackWithPgInquiry( // User의 userId (String)를 가져오기 위해 User 조회 User user; try { - user = userService.findById(userId); + user = userService.getUserById(userId); } catch (CoreException e) { log.warn("콜백 검증 시 사용자를 찾을 수 없습니다. 콜백 정보를 사용합니다. (orderId: {}, userId: {})", orderId, userId); @@ -864,7 +868,7 @@ private void handlePaymentFailure(String userId, Long orderId, String errorCode, // 사용자 조회 (Service를 통한 접근) User user; try { - user = userService.findByUserId(userId); + user = userService.getUser(userId); } catch (CoreException e) { log.warn("결제 실패 처리 시 사용자를 찾을 수 없습니다. (userId: {}, orderId: {})", userId, orderId); return; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java deleted file mode 100644 index 293505b15..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpFacade.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.loopers.application.signup; - -import com.loopers.domain.user.Gender; -import com.loopers.domain.user.Point; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import jakarta.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -import java.util.Locale; - -/** - * 회원가입 파사드. - *

- * 회원가입 시 사용자 생성을 처리하는 애플리케이션 서비스입니다. - * 사용자 생성 시 포인트는 자동으로 0으로 초기화됩니다. - * 트랜잭션 경계를 관리하여 데이터 일관성을 보장합니다. - *

- * - * @author Loopers - * @version 1.0 - */ -@RequiredArgsConstructor -@Component -public class SignUpFacade { - private final UserService userService; - - /** - * 회원가입을 처리합니다. - *

- * 사용자를 생성하며, 포인트는 자동으로 0으로 초기화됩니다. - * 전체 과정이 하나의 트랜잭션으로 처리됩니다. - *

- * - * @param userId 사용자 ID - * @param email 이메일 주소 - * @param birthDateStr 생년월일 (yyyy-MM-dd) - * @param genderStr 성별 문자열 (MALE 또는 FEMALE) - * @return 생성된 사용자 정보 - * @throws CoreException gender 값이 유효하지 않거나, 유효성 검증 실패 또는 중복 ID 존재 시 - */ - @Transactional - public SignUpInfo signUp(String userId, String email, String birthDateStr, String genderStr) { - Gender gender = parseGender(genderStr); - Point point = Point.of(0L); - User user = userService.create(userId, email, birthDateStr, gender, point); - return SignUpInfo.from(user); - } - - /** - * 성별 문자열을 Gender enum으로 변환합니다. - *

- * 도메인 진입점에서 방어 로직을 제공하여 NPE를 방지합니다. - *

- * - * @param genderStr 성별 문자열 - * @return Gender enum - * @throws CoreException gender 값이 null이거나 유효하지 않은 경우 - */ - private Gender parseGender(String genderStr) { - if (genderStr == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "gender 값이 올바르지 않습니다."); - } - try { - String genderValue = genderStr.trim().toUpperCase(Locale.ROOT); - return Gender.valueOf(genderValue); - } catch (IllegalArgumentException e) { - throw new CoreException(ErrorType.BAD_REQUEST, "gender 값이 올바르지 않습니다."); - } - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java deleted file mode 100644 index c84caf7a3..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/signup/SignUpInfo.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.loopers.application.signup; - -import com.loopers.domain.user.Gender; -import com.loopers.domain.user.User; - -import java.time.LocalDate; - -/** - * 회원가입 결과 정보를 담는 레코드. - *

- * User 도메인 엔티티로부터 생성된 불변 데이터 전송 객체입니다. - *

- * - * @param id 사용자 엔티티 ID - * @param userId 사용자 ID - * @param email 이메일 주소 - * @param birthDate 생년월일 - * @param gender 성별 - * @author Loopers - * @version 1.0 - */ -public record SignUpInfo(Long id, String userId, String email, LocalDate birthDate, Gender gender) { - /** - * User 엔티티로부터 SignUpInfo를 생성합니다. - * - * @param user 변환할 사용자 엔티티 - * @return 생성된 SignUpInfo - */ - public static SignUpInfo from(User user) { - return new SignUpInfo( - user.getId(), - user.getUserId(), - user.getEmail(), - user.getBirthDate(), - user.getGender() - ); - } -} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserService.java similarity index 59% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java rename to apps/commerce-api/src/main/java/com/loopers/application/user/UserService.java index 8c6d062e2..1b6689d34 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserService.java @@ -1,5 +1,9 @@ -package com.loopers.domain.user; +package com.loopers.application.user; +import com.loopers.domain.user.Gender; +import com.loopers.domain.user.Point; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserRepository; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -8,10 +12,10 @@ import org.springframework.transaction.annotation.Transactional; /** - * 사용자 도메인 서비스. + * 사용자 애플리케이션 서비스. *

- * 사용자 생성, 조회 등의 도메인 로직을 처리합니다. - * Repository에 의존하며 데이터 무결성 제약 조건을 처리합니다. + * 사용자 생성, 조회, 포인트 관리 등의 애플리케이션 로직을 처리합니다. + * Repository에 의존하며 트랜잭션 관리 및 데이터 무결성 제약 조건을 처리합니다. *

* * @author Loopers @@ -52,7 +56,7 @@ public User create(String userId, String email, String birthDateStr, Gender gend * @throws CoreException 사용자를 찾을 수 없는 경우 */ @Transactional(readOnly = true) - public User findByUserId(String userId) { + public User getUser(String userId) { User user = userRepository.findByUserId(userId); if (user == null) { throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); @@ -71,7 +75,7 @@ public User findByUserId(String userId) { * @throws CoreException 사용자를 찾을 수 없는 경우 */ @Transactional - public User findByUserIdForUpdate(String userId) { + public User getUserForUpdate(String userId) { User user = userRepository.findByUserIdForUpdate(userId); if (user == null) { throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); @@ -87,7 +91,7 @@ public User findByUserIdForUpdate(String userId) { * @throws CoreException 사용자를 찾을 수 없는 경우 */ @Transactional(readOnly = true) - public User findById(Long id) { + public User getUserById(Long id) { User user = userRepository.findById(id); if (user == null) { throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); @@ -105,4 +109,55 @@ public User findById(Long id) { public User save(User user) { return userRepository.save(user); } + + /** + * 사용자의 포인트를 조회합니다. + * + * @param userId 조회할 사용자 ID + * @return 포인트 정보 + * @throws CoreException 사용자를 찾을 수 없는 경우 + */ + @Transactional(readOnly = true) + public PointsInfo getPoints(String userId) { + User user = getUser(userId); + return PointsInfo.from(user); + } + + /** + * 사용자의 포인트를 충전합니다. + *

+ * 트랜잭션 내에서 실행되어 데이터 일관성을 보장합니다. + *

+ * + * @param userId 충전할 사용자 ID + * @param amount 충전할 포인트 금액 + * @return 충전된 포인트 정보 + * @throws CoreException 사용자를 찾을 수 없거나 충전 금액이 유효하지 않은 경우 + */ + @Transactional + public PointsInfo chargePoint(String userId, Long amount) { + User user = getUser(userId); + Point point = Point.of(amount); + user.receivePoint(point); + User savedUser = save(user); + return PointsInfo.from(savedUser); + } + + /** + * 포인트 정보를 담는 레코드. + * + * @param userId 사용자 ID + * @param balance 포인트 잔액 + */ + public record PointsInfo(String userId, Long balance) { + /** + * User 엔티티로부터 PointsInfo를 생성합니다. + * + * @param user 사용자 엔티티 + * @return 생성된 PointsInfo + */ + public static PointsInfo from(User user) { + return new PointsInfo(user.getUserId(), user.getPoint().getValue()); + } + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/userinfo/UserInfoFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/userinfo/UserInfoFacade.java deleted file mode 100644 index ceae04eef..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/application/userinfo/UserInfoFacade.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.loopers.application.userinfo; - -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * 사용자 정보 조회 파사드. - *

- * 사용자 정보 조회 유즈케이스를 처리하는 애플리케이션 서비스입니다. - *

- * - * @author Loopers - * @version 1.0 - */ -@RequiredArgsConstructor -@Component -public class UserInfoFacade { - private final UserRepository userRepository; - - /** - * 사용자 ID로 사용자 정보를 조회합니다. - * - * @param userId 조회할 사용자 ID - * @return 조회된 사용자 정보 - * @throws CoreException 사용자를 찾을 수 없는 경우 - */ - public UserInfo getUserInfo(String userId) { - User user = userRepository.findByUserId(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); - } - return UserInfo.from(user); - } - - /** - * 사용자 정보를 담는 레코드. - * - * @param userId 사용자 ID - * @param email 이메일 주소 - * @param birthDate 생년월일 - * @param gender 성별 - */ - public record UserInfo( - String userId, - String email, - java.time.LocalDate birthDate, - com.loopers.domain.user.Gender gender - ) { - /** - * User 엔티티로부터 UserInfo를 생성합니다. - * - * @param user 사용자 엔티티 - * @return 생성된 UserInfo - */ - public static UserInfo from(User user) { - return new UserInfo( - user.getUserId(), - user.getEmail(), - user.getBirthDate(), - user.getGender() - ); - } - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/config/batch/LikeCountSyncBatchConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/batch/LikeCountSyncBatchConfig.java index 6bf72da5c..d92ccd4e8 100644 --- a/apps/commerce-api/src/main/java/com/loopers/config/batch/LikeCountSyncBatchConfig.java +++ b/apps/commerce-api/src/main/java/com/loopers/config/batch/LikeCountSyncBatchConfig.java @@ -1,6 +1,6 @@ package com.loopers.config.batch; -import com.loopers.application.catalog.ProductCacheService; +import com.loopers.application.product.ProductCacheService; import com.loopers.domain.like.LikeRepository; import com.loopers.domain.product.ProductRepository; import lombok.RequiredArgsConstructor; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java index a8f2864d8..1612276d4 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/payment/PaymentGateway.java @@ -1,6 +1,6 @@ package com.loopers.domain.payment; -import com.loopers.application.purchasing.PaymentRequestCommand; +import com.loopers.application.payment.PaymentRequestCommand; /** * 결제 게이트웨이 인터페이스. 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 deleted file mode 100644 index dde8b402b..000000000 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.loopers.domain.product; - -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.util.List; - -/** - * 상품 도메인 서비스. - *

- * 상품 조회, 저장 등의 도메인 로직을 처리합니다. - * Repository에 의존하며 비즈니스 규칙을 캡슐화합니다. - *

- * - * @author Loopers - * @version 1.0 - */ -@RequiredArgsConstructor -@Component -public class ProductService { - private final ProductRepository productRepository; - - /** - * 상품 ID로 상품을 조회합니다. (비관적 락) - *

- * 동시성 제어가 필요한 경우 사용합니다. (예: 재고 차감) - *

- * - * @param productId 조회할 상품 ID - * @return 조회된 상품 - * @throws CoreException 상품을 찾을 수 없는 경우 - */ - @Transactional - public Product findByIdForUpdate(Long productId) { - return productRepository.findByIdForUpdate(productId) - .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, - String.format("상품을 찾을 수 없습니다. (상품 ID: %d)", productId))); - } - - /** - * 상품 목록을 저장합니다. - * - * @param products 저장할 상품 목록 - */ - @Transactional - public void saveAll(List products) { - products.forEach(productRepository::save); - } -} - diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java index 5d4b994fa..ee81c5438 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PaymentGatewayImpl.java @@ -1,7 +1,7 @@ package com.loopers.infrastructure.payment; import com.loopers.domain.payment.PaymentGateway; -import com.loopers.application.purchasing.PaymentRequestCommand; +import com.loopers.application.payment.PaymentRequestCommand; import com.loopers.domain.payment.PaymentRequestResult; import com.loopers.domain.payment.PaymentStatus; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; diff --git a/apps/commerce-api/src/main/java/com/loopers/application/scheduler/LikeCountSyncScheduler.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/scheduler/LikeCountSyncScheduler.java similarity index 98% rename from apps/commerce-api/src/main/java/com/loopers/application/scheduler/LikeCountSyncScheduler.java rename to apps/commerce-api/src/main/java/com/loopers/infrastructure/scheduler/LikeCountSyncScheduler.java index 4621b4fef..a4e144a47 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/scheduler/LikeCountSyncScheduler.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/scheduler/LikeCountSyncScheduler.java @@ -1,4 +1,4 @@ -package com.loopers.application.scheduler; +package com.loopers.infrastructure.scheduler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Controller.java index a56cc1c63..32fc066ad 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Controller.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.catalog; -import com.loopers.application.catalog.CatalogBrandFacade; +import com.loopers.application.brand.BrandService; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -22,7 +22,7 @@ @RequestMapping("/api/v1/brands") public class BrandV1Controller { - private final CatalogBrandFacade catalogBrandFacade; + private final BrandService brandService; /** * 브랜드 정보를 조회합니다. @@ -32,7 +32,7 @@ public class BrandV1Controller { */ @GetMapping("/{brandId}") public ApiResponse getBrand(@PathVariable Long brandId) { - CatalogBrandFacade.BrandInfo brandInfo = catalogBrandFacade.getBrand(brandId); + BrandService.BrandInfo brandInfo = brandService.getBrand(brandId); return ApiResponse.success(BrandV1Dto.BrandResponse.from(brandInfo)); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Dto.java index 2bc497615..53e8fa13a 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/BrandV1Dto.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.catalog; -import com.loopers.application.catalog.CatalogBrandFacade; +import com.loopers.application.brand.BrandService; /** * 브랜드 조회 API v1의 데이터 전송 객체(DTO) 컨테이너. @@ -22,7 +22,7 @@ public record BrandResponse(Long brandId, String name) { * @param brandInfo 브랜드 정보 * @return 생성된 응답 객체 */ - public static BrandResponse from(CatalogBrandFacade.BrandInfo brandInfo) { + public static BrandResponse from(BrandService.BrandInfo brandInfo) { return new BrandResponse(brandInfo.id(), brandInfo.name()); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Controller.java index 4dc38d439..c275b3b7d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/catalog/ProductV1Controller.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.catalog; -import com.loopers.application.catalog.CatalogProductFacade; +import com.loopers.application.catalog.CatalogFacade; import com.loopers.application.catalog.ProductInfo; import com.loopers.application.catalog.ProductInfoList; import com.loopers.interfaces.api.ApiResponse; @@ -25,7 +25,7 @@ @RequestMapping("/api/v1/products") public class ProductV1Controller { - private final CatalogProductFacade catalogProductFacade; + private final CatalogFacade catalogFacade; /** * 상품 목록을 조회합니다. @@ -43,7 +43,7 @@ public ApiResponse getProducts( @RequestParam(required = false, defaultValue = "0") int page, @RequestParam(required = false, defaultValue = "20") int size ) { - ProductInfoList result = catalogProductFacade.getProducts(brandId, sort, page, size); + ProductInfoList result = catalogFacade.getProducts(brandId, sort, page, size); return ApiResponse.success(ProductV1Dto.ProductsResponse.from(result)); } @@ -55,7 +55,7 @@ public ApiResponse getProducts( */ @GetMapping("/{productId}") public ApiResponse getProduct(@PathVariable Long productId) { - ProductInfo productInfo = catalogProductFacade.getProduct(productId); + ProductInfo productInfo = catalogFacade.getProduct(productId); return ApiResponse.success(ProductV1Dto.ProductResponse.from(productInfo)); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java index 2935b424d..640c909e3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.like; -import com.loopers.application.like.LikeFacade; +import com.loopers.application.heart.HeartFacade; import com.loopers.interfaces.api.ApiResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -25,7 +25,7 @@ @RequestMapping("/api/v1/like/products") public class LikeV1Controller { - private final LikeFacade likeFacade; + private final HeartFacade heartFacade; /** * 상품에 좋아요를 추가합니다. @@ -39,7 +39,7 @@ public ApiResponse addLike( @RequestHeader("X-USER-ID") String userId, @PathVariable Long productId ) { - likeFacade.addLike(userId, productId); + heartFacade.addLike(userId, productId); return ApiResponse.success(null); } @@ -55,7 +55,7 @@ public ApiResponse removeLike( @RequestHeader("X-USER-ID") String userId, @PathVariable Long productId ) { - likeFacade.removeLike(userId, productId); + heartFacade.removeLike(userId, productId); return ApiResponse.success(null); } @@ -69,7 +69,7 @@ public ApiResponse removeLike( public ApiResponse getLikedProducts( @RequestHeader("X-USER-ID") String userId ) { - var likedProducts = likeFacade.getLikedProducts(userId); + var likedProducts = heartFacade.getLikedProducts(userId); return ApiResponse.success(LikeV1Dto.LikedProductsResponse.from(likedProducts)); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java index 1fc6f20f0..e154c036b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.like; -import com.loopers.application.like.LikeFacade; +import com.loopers.application.heart.HeartFacade; import java.util.List; @@ -25,7 +25,7 @@ public record LikedProductsResponse( * @param likedProducts 좋아요한 상품 목록 * @return 생성된 응답 객체 */ - public static LikedProductsResponse from(List likedProducts) { + public static LikedProductsResponse from(List likedProducts) { return new LikedProductsResponse( likedProducts.stream() .map(LikedProductResponse::from) @@ -58,7 +58,7 @@ public record LikedProductResponse( * @param likedProduct 좋아요한 상품 정보 * @return 생성된 응답 객체 */ - public static LikedProductResponse from(LikeFacade.LikedProduct likedProduct) { + public static LikedProductResponse from(HeartFacade.LikedProduct likedProduct) { return new LikedProductResponse( likedProduct.productId(), likedProduct.name(), diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Controller.java index 4efc043ca..e0ffa6f26 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Controller.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.pointwallet; -import com.loopers.application.pointwallet.PointWalletFacade; +import com.loopers.application.user.UserService; import com.loopers.interfaces.api.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -25,7 +25,7 @@ @RequestMapping("/api/v1") public class PointWalletV1Controller { - private final PointWalletFacade pointWalletFacade; + private final UserService userService; /** * 현재 사용자의 포인트를 조회합니다. @@ -38,7 +38,7 @@ public class PointWalletV1Controller { public ApiResponse getMyPoints( @RequestHeader("X-USER-ID") String userId ) { - PointWalletFacade.PointsInfo pointsInfo = pointWalletFacade.getPoints(userId); + UserService.PointsInfo pointsInfo = userService.getPoints(userId); return ApiResponse.success(PointWalletV1Dto.PointsResponse.from(pointsInfo)); } @@ -55,7 +55,7 @@ public ApiResponse chargePoints( @RequestHeader("X-USER-ID") String userId, @Valid @RequestBody PointWalletV1Dto.ChargeRequest request ) { - PointWalletFacade.PointsInfo pointsInfo = pointWalletFacade.chargePoint(userId, request.amount()); + UserService.PointsInfo pointsInfo = userService.chargePoint(userId, request.amount()); return ApiResponse.success(PointWalletV1Dto.PointsResponse.from(pointsInfo)); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Dto.java index 461605598..c6e7c97b6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/pointwallet/PointWalletV1Dto.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api.pointwallet; -import com.loopers.application.pointwallet.PointWalletFacade; +import com.loopers.application.user.UserService; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; @@ -24,7 +24,7 @@ public record PointsResponse(String userId, Long balance) { * @param pointsInfo 포인트 정보 * @return 생성된 응답 객체 */ - public static PointsResponse from(PointWalletFacade.PointsInfo pointsInfo) { + public static PointsResponse from(UserService.PointsInfo pointsInfo) { return new PointsResponse(pointsInfo.userId(), pointsInfo.balance()); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeConcurrencyTest.java b/apps/commerce-api/src/test/java/com/loopers/application/heart/HeartFacadeConcurrencyTest.java similarity index 93% rename from apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeConcurrencyTest.java rename to apps/commerce-api/src/test/java/com/loopers/application/heart/HeartFacadeConcurrencyTest.java index 1e7b42394..fc9afe984 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeConcurrencyTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/heart/HeartFacadeConcurrencyTest.java @@ -1,4 +1,4 @@ -package com.loopers.application.like; +package com.loopers.application.heart; import com.loopers.domain.brand.Brand; import com.loopers.domain.brand.BrandRepository; @@ -37,10 +37,10 @@ @SpringBootTest @Import(MySqlTestContainersConfig.class) @DisplayName("LikeFacade 동시성 테스트") -class LikeFacadeConcurrencyTest { +class HeartFacadeConcurrencyTest { @Autowired - private LikeFacade likeFacade; + private HeartFacade heartFacade; @Autowired private UserRepository userRepository; @@ -116,7 +116,7 @@ void concurrencyTest_likeShouldBeProperlyCounted() throws InterruptedException { for (User user : users) { executorService.submit(() -> { try { - likeFacade.addLike(user.getUserId(), productId); + heartFacade.addLike(user.getUserId(), productId); successCount.incrementAndGet(); } catch (Exception e) { synchronized (exceptions) { @@ -159,7 +159,7 @@ void concurrencyTest_sameUserMultipleRequests_shouldBeCountedCorrectly() throws for (int i = 0; i < concurrentRequestCount; i++) { executorService.submit(() -> { try { - likeFacade.addLike(userId, productId); + heartFacade.addLike(userId, productId); successCount.incrementAndGet(); } catch (Exception e) { synchronized (exceptions) { @@ -233,19 +233,19 @@ void concurrencyTest_transactionReadOnlyAndUniqueConstraintServeDifferentPurpose String userId2 = user2.getUserId(); // user1이 상품1, 상품2에 좋아요를 이미 누른 상태 - likeFacade.addLike(userId1, product1.getId()); - likeFacade.addLike(userId1, product2.getId()); + heartFacade.addLike(userId1, product1.getId()); + heartFacade.addLike(userId1, product2.getId()); ExecutorService executorService = Executors.newFixedThreadPool(20); CountDownLatch latch = new CountDownLatch(20); - List> allResults = new ArrayList<>(); + List> allResults = new ArrayList<>(); // act // 여러 스레드에서 동시에 조회를 수행 for (int i = 0; i < 10; i++) { executorService.submit(() -> { try { - List result = likeFacade.getLikedProducts(userId1); + List result = heartFacade.getLikedProducts(userId1); synchronized (allResults) { allResults.add(result); } @@ -267,14 +267,14 @@ void concurrencyTest_transactionReadOnlyAndUniqueConstraintServeDifferentPurpose if (index % 2 == 0) { // user2가 상품1에 좋아요 추가 try { - likeFacade.addLike(userId2, product1.getId()); + heartFacade.addLike(userId2, product1.getId()); } catch (Exception e) { // 이미 좋아요가 있으면 무시 } } else { // user2가 상품2에 좋아요 추가 try { - likeFacade.addLike(userId2, product2.getId()); + heartFacade.addLike(userId2, product2.getId()); } catch (Exception e) { // 이미 좋아요가 있으면 무시 } @@ -310,25 +310,25 @@ void concurrencyTest_transactionReadOnlyAndUniqueConstraintServeDifferentPurpose // 참고: allResults는 동기화 이전에 조회된 결과이므로 likesCount가 0일 수 있습니다. // 이 테스트는 @Transactional(readOnly = true)의 일관성 보장을 검증하는 것이 목적이므로, // 동시성 테스트 중 조회된 결과의 상품 ID 일관성만 확인합니다. - for (List result : allResults) { + for (List result : allResults) { // user1의 좋아요 목록에는 상품1, 상품2가 포함되어야 함 List resultProductIds = result.stream() - .map(LikeFacade.LikedProduct::productId) + .map(HeartFacade.LikedProduct::productId) .sorted() .toList(); assertThat(resultProductIds).contains(product1.getId(), product2.getId()); } // 최종 상태 확인 (동기화 후) - List finalResult = likeFacade.getLikedProducts(userId1); + List finalResult = heartFacade.getLikedProducts(userId1); List finalProductIds = finalResult.stream() - .map(LikeFacade.LikedProduct::productId) + .map(HeartFacade.LikedProduct::productId) .sorted() .toList(); assertThat(finalProductIds).containsExactlyInAnyOrder(product1.getId(), product2.getId()); // 동기화 후에는 정확한 좋아요 수가 반영되어야 함 - for (LikeFacade.LikedProduct likedProduct : finalResult) { + for (HeartFacade.LikedProduct likedProduct : finalResult) { assertThat(likedProduct.likesCount()).isGreaterThan(0); } } 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/heart/HeartFacadeTest.java similarity index 89% rename from apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java rename to apps/commerce-api/src/test/java/com/loopers/application/heart/HeartFacadeTest.java index 148fe1968..b8edf53c0 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/heart/HeartFacadeTest.java @@ -1,6 +1,6 @@ -package com.loopers.application.like; +package com.loopers.application.heart; -import com.loopers.application.catalog.ProductCacheService; +import com.loopers.application.product.ProductCacheService; import com.loopers.domain.like.Like; import com.loopers.domain.like.LikeRepository; import com.loopers.domain.product.Product; @@ -28,7 +28,7 @@ import static org.mockito.Mockito.never; @DisplayName("LikeFacade 좋아요 등록/취소/중복 방지 흐름 검증") -class LikeFacadeTest { +class HeartFacadeTest { @Mock private LikeRepository likeRepository; @@ -43,7 +43,7 @@ class LikeFacadeTest { private ProductCacheService productCacheService; @InjectMocks - private LikeFacade likeFacade; + private HeartFacade heartFacade; private static final String DEFAULT_USER_ID = "testuser"; private static final Long DEFAULT_USER_INTERNAL_ID = 1L; @@ -63,7 +63,7 @@ void addLike_success() { .thenReturn(Optional.empty()); // act - likeFacade.addLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); + heartFacade.addLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); // assert verify(likeRepository).save(any(Like.class)); @@ -79,7 +79,7 @@ void removeLike_success() { .thenReturn(Optional.of(like)); // act - likeFacade.removeLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); + heartFacade.removeLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); // assert verify(likeRepository).delete(like); @@ -94,7 +94,7 @@ void addLike_isIdempotent() { .thenReturn(Optional.of(Like.of(DEFAULT_USER_INTERNAL_ID, DEFAULT_PRODUCT_ID))); // act - likeFacade.addLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); + heartFacade.addLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); // assert - save는 한 번만 호출되어야 함 (중복 방지) verify(likeRepository, never()).save(any(Like.class)); @@ -109,7 +109,7 @@ void removeLike_isIdempotent() { .thenReturn(Optional.empty()); // 좋아요 없음 // act - 좋아요가 없는 상태에서 취소 시도 - likeFacade.removeLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); + heartFacade.removeLike(DEFAULT_USER_ID, DEFAULT_PRODUCT_ID); // assert - 예외가 발생하지 않아야 함 (멱등성 보장) verify(likeRepository).findByUserIdAndProductId(DEFAULT_USER_INTERNAL_ID, DEFAULT_PRODUCT_ID); @@ -124,7 +124,7 @@ void addLike_userNotFound() { when(userRepository.findByUserId(unknownUserId)).thenReturn(null); // act & assert - assertThatThrownBy(() -> likeFacade.addLike(unknownUserId, DEFAULT_PRODUCT_ID)) + assertThatThrownBy(() -> heartFacade.addLike(unknownUserId, DEFAULT_PRODUCT_ID)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } @@ -138,7 +138,7 @@ void addLike_productNotFound() { when(productRepository.findById(nonExistentProductId)).thenReturn(Optional.empty()); // act & assert - assertThatThrownBy(() -> likeFacade.addLike(DEFAULT_USER_ID, nonExistentProductId)) + assertThatThrownBy(() -> heartFacade.addLike(DEFAULT_USER_ID, nonExistentProductId)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } @@ -166,13 +166,13 @@ void getLikedProducts_success() { .thenReturn(List.of(product1, product2)); // act - List result = likeFacade.getLikedProducts(DEFAULT_USER_ID); + List result = heartFacade.getLikedProducts(DEFAULT_USER_ID); // assert assertThat(result).hasSize(2); - assertThat(result).extracting(LikeFacade.LikedProduct::productId) + assertThat(result).extracting(HeartFacade.LikedProduct::productId) .containsExactlyInAnyOrder(productId1, productId2); - assertThat(result).extracting(LikeFacade.LikedProduct::likesCount) + assertThat(result).extracting(HeartFacade.LikedProduct::likesCount) .containsExactlyInAnyOrder(5L, 3L); } @@ -184,7 +184,7 @@ void getLikedProducts_emptyList() { when(likeRepository.findAllByUserId(DEFAULT_USER_INTERNAL_ID)).thenReturn(List.of()); // act - List result = likeFacade.getLikedProducts(DEFAULT_USER_ID); + List result = heartFacade.getLikedProducts(DEFAULT_USER_ID); // assert assertThat(result).isEmpty(); @@ -212,7 +212,7 @@ void getLikedProducts_productNotFound() { .thenReturn(List.of(product1)); // product1만 반환 (nonExistentProductId는 없음) // act & assert - assertThatThrownBy(() -> likeFacade.getLikedProducts(DEFAULT_USER_ID)) + assertThatThrownBy(() -> heartFacade.getLikedProducts(DEFAULT_USER_ID)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } @@ -225,7 +225,7 @@ void getLikedProducts_userNotFound() { when(userRepository.findByUserId(unknownUserId)).thenReturn(null); // act & assert - assertThatThrownBy(() -> likeFacade.getLikedProducts(unknownUserId)) + assertThatThrownBy(() -> heartFacade.getLikedProducts(unknownUserId)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/signup/SignUpFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/signup/SignUpFacadeIntegrationTest.java deleted file mode 100644 index c827cae1d..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/application/signup/SignUpFacadeIntegrationTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.loopers.application.signup; - -import com.loopers.domain.user.Gender; -import com.loopers.domain.user.User; -import com.loopers.domain.user.UserTestFixture; -import com.loopers.infrastructure.user.UserJpaRepository; -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.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -@SpringBootTest -@DisplayName("SignUpFacade 통합 테스트") -class SignUpFacadeIntegrationTest { - @Autowired - private SignUpFacade signUpFacade; - - @MockitoSpyBean - private UserJpaRepository userJpaRepository; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("회원 가입에 관한 통합 테스트") - @Nested - class SignUp { - @DisplayName("회원가입시 User 저장이 수행된다.") - @ParameterizedTest - @EnumSource(Gender.class) - void returnsSignUpInfo_whenValidIdIsProvided(Gender gender) { - // arrange - String userId = UserTestFixture.ValidUser.USER_ID; - String email = UserTestFixture.ValidUser.EMAIL; - String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - Mockito.reset(userJpaRepository); - - // act - SignUpInfo signUpInfo = signUpFacade.signUp(userId, email, birthDate, gender.name()); - - // assert - assertAll( - () -> assertThat(signUpInfo).isNotNull(), - () -> assertThat(signUpInfo.userId()).isEqualTo(userId), - () -> verify(userJpaRepository, times(1)).save(any(User.class)) - ); - } - - @DisplayName("이미 가입된 ID로 회원가입 시도 시, 실패한다.") - @ParameterizedTest - @EnumSource(Gender.class) - void fails_whenDuplicateUserIdExists(Gender gender) { - // arrange - String userId = UserTestFixture.ValidUser.USER_ID; - String email = UserTestFixture.ValidUser.EMAIL; - String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); - - // act - CoreException result = assertThrows(CoreException.class, () -> - signUpFacade.signUp(userId, email, birthDate, gender.name()) - ); - - // assert - assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); - } - } -} - diff --git a/apps/commerce-api/src/test/java/com/loopers/application/pointwallet/PointWalletFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/UserServiceIntegrationTest.java similarity index 53% rename from apps/commerce-api/src/test/java/com/loopers/application/pointwallet/PointWalletFacadeIntegrationTest.java rename to apps/commerce-api/src/test/java/com/loopers/application/user/UserServiceIntegrationTest.java index cebc13975..db7a1de10 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/pointwallet/PointWalletFacadeIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/user/UserServiceIntegrationTest.java @@ -1,8 +1,10 @@ -package com.loopers.application.pointwallet; +package com.loopers.application.user; -import com.loopers.application.signup.SignUpFacade; import com.loopers.domain.user.Gender; +import com.loopers.domain.user.Point; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserTestFixture; +import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import com.loopers.utils.DatabaseCleanUp; @@ -12,21 +14,27 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @SpringBootTest -@DisplayName("PointWalletFacade 통합 테스트") -class PointWalletFacadeIntegrationTest { +@DisplayName("UserService 통합 테스트") +class UserServiceIntegrationTest { @Autowired - private PointWalletFacade pointWalletFacade; + private UserService userService; - @Autowired - private SignUpFacade signUpFacade; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; @Autowired private DatabaseCleanUp databaseCleanUp; @@ -36,6 +44,57 @@ void tearDown() { databaseCleanUp.truncateAllTables(); } + /** + * 테스트용 사용자를 생성합니다. + */ + private void createUser(String userId, String email, String birthDate, Gender gender) { + userService.create(userId, email, birthDate, gender, Point.of(0L)); + } + + @DisplayName("회원 가입에 관한 통합 테스트") + @Nested + class SignUp { + @DisplayName("회원가입시 User 저장이 수행된다.") + @ParameterizedTest + @EnumSource(Gender.class) + void createsUser_whenValidIdIsProvided(Gender gender) { + // arrange + String userId = UserTestFixture.ValidUser.USER_ID; + String email = UserTestFixture.ValidUser.EMAIL; + String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; + Mockito.reset(userJpaRepository); + + // act + User user = userService.create(userId, email, birthDate, gender, Point.of(0L)); + + // assert + assertAll( + () -> assertThat(user).isNotNull(), + () -> assertThat(user.getUserId()).isEqualTo(userId), + () -> verify(userJpaRepository, times(1)).save(any(User.class)) + ); + } + + @DisplayName("이미 가입된 ID로 회원가입 시도 시, 실패한다.") + @ParameterizedTest + @EnumSource(Gender.class) + void fails_whenDuplicateUserIdExists(Gender gender) { + // arrange + String userId = UserTestFixture.ValidUser.USER_ID; + String email = UserTestFixture.ValidUser.EMAIL; + String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; + userService.create(userId, email, birthDate, gender, Point.of(0L)); + + // act + CoreException result = assertThrows(CoreException.class, () -> + userService.create(userId, email, birthDate, gender, Point.of(0L)) + ); + + // assert + assertThat(result.getErrorType()).isEqualTo(ErrorType.CONFLICT); + } + } + @DisplayName("포인트 조회에 관한 통합 테스트") @Nested class PointInfo { @@ -47,10 +106,10 @@ void returnsPoints_whenUserExists(Gender gender) { String userId = UserTestFixture.ValidUser.USER_ID; String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); + createUser(userId, email, birthDate, gender); // act - PointWalletFacade.PointsInfo pointsInfo = pointWalletFacade.getPoints(userId); + UserService.PointsInfo pointsInfo = userService.getPoints(userId); // assert assertAll( @@ -67,7 +126,7 @@ void throwsException_whenUserDoesNotExist() { String userId = "unknown"; // act & assert - assertThatThrownBy(() -> pointWalletFacade.getPoints(userId)) + assertThatThrownBy(() -> userService.getPoints(userId)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } @@ -84,11 +143,11 @@ void chargesPoints_success(Gender gender) { String userId = UserTestFixture.ValidUser.USER_ID; String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); + createUser(userId, email, birthDate, gender); Long chargeAmount = 10_000L; // act - PointWalletFacade.PointsInfo pointsInfo = pointWalletFacade.chargePoint(userId, chargeAmount); + UserService.PointsInfo pointsInfo = userService.chargePoint(userId, chargeAmount); // assert assertAll( @@ -106,10 +165,9 @@ void throwsException_whenUserDoesNotExist() { Long chargeAmount = 10_000L; // act & assert - assertThatThrownBy(() -> pointWalletFacade.chargePoint(userId, chargeAmount)) + assertThatThrownBy(() -> userService.chargePoint(userId, chargeAmount)) .isInstanceOf(CoreException.class) .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); } } } - diff --git a/apps/commerce-api/src/test/java/com/loopers/application/userinfo/UserInfoFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/userinfo/UserInfoFacadeIntegrationTest.java deleted file mode 100644 index 78efa5e30..000000000 --- a/apps/commerce-api/src/test/java/com/loopers/application/userinfo/UserInfoFacadeIntegrationTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.loopers.application.userinfo; - -import com.loopers.application.signup.SignUpFacade; -import com.loopers.domain.user.Gender; -import com.loopers.domain.user.UserTestFixture; -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.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -@SpringBootTest -@DisplayName("UserInfoFacade 통합 테스트") -class UserInfoFacadeIntegrationTest { - @Autowired - private UserInfoFacade userInfoFacade; - - @Autowired - private SignUpFacade signUpFacade; - - @Autowired - private DatabaseCleanUp databaseCleanUp; - - @AfterEach - void tearDown() { - databaseCleanUp.truncateAllTables(); - } - - @DisplayName("회원 조회에 관한 통합 테스트") - @Nested - class UserInfo { - @DisplayName("해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.") - @ParameterizedTest - @EnumSource(Gender.class) - void returnsUserInfo_whenUserExists(Gender gender) { - // arrange - String userId = UserTestFixture.ValidUser.USER_ID; - String email = UserTestFixture.ValidUser.EMAIL; - String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); - - // act - UserInfoFacade.UserInfo userInfo = userInfoFacade.getUserInfo(userId); - - // assert - assertAll( - () -> assertThat(userInfo).isNotNull(), - () -> assertThat(userInfo.userId()).isEqualTo(userId), - () -> assertThat(userInfo.email()).isEqualTo(email), - () -> assertThat(userInfo.birthDate()).isEqualTo(LocalDate.parse(birthDate)), - () -> assertThat(userInfo.gender()).isEqualTo(gender) - ); - } - - @DisplayName("해당 ID 의 회원이 존재하지 않을 경우, 예외가 발생한다.") - @Test - void throwsException_whenUserDoesNotExist() { - // arrange - String userId = "unknown"; - - // act & assert - assertThatThrownBy(() -> userInfoFacade.getUserInfo(userId)) - .isInstanceOf(CoreException.class) - .hasFieldOrPropertyWithValue("errorType", ErrorType.NOT_FOUND); - } - } -} - 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 c15742303..e0c867d89 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 @@ -1,5 +1,6 @@ package com.loopers.domain.coupon; +import com.loopers.application.coupon.CouponService; import com.loopers.domain.coupon.discount.CouponDiscountStrategy; import com.loopers.domain.coupon.discount.CouponDiscountStrategyFactory; import com.loopers.support.error.CoreException; 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 index 1dab0a951..b259c2dd2 100644 --- 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 @@ -1,5 +1,6 @@ package com.loopers.domain.order; +import com.loopers.application.order.OrderService; import com.loopers.domain.payment.PaymentStatus; import com.loopers.domain.product.Product; import com.loopers.domain.user.Gender; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java index 963eab173..daf03ca99 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/payment/PaymentServiceTest.java @@ -1,6 +1,7 @@ package com.loopers.domain.payment; -import com.loopers.application.purchasing.PaymentRequestCommand; +import com.loopers.application.payment.PaymentService; +import com.loopers.application.payment.PaymentRequestCommand; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.BeforeEach; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java index 2ae4afeb2..1b1682d8a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java @@ -1,5 +1,6 @@ package com.loopers.domain.product; +import com.loopers.application.product.ProductService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.DisplayName; diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java index 087413f54..e907edbbe 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.java @@ -1,5 +1,6 @@ package com.loopers.domain.user; +import com.loopers.application.user.UserService; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import org.junit.jupiter.api.DisplayName; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointWalletV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointWalletV1ApiE2ETest.java index c2629fb78..559a26477 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointWalletV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointWalletV1ApiE2ETest.java @@ -1,7 +1,8 @@ package com.loopers.interfaces.api; -import com.loopers.application.signup.SignUpFacade; +import com.loopers.application.user.UserService; import com.loopers.domain.user.Gender; +import com.loopers.domain.user.Point; import com.loopers.domain.user.UserTestFixture; import com.loopers.interfaces.api.pointwallet.PointWalletV1Dto; import com.loopers.utils.DatabaseCleanUp; @@ -31,17 +32,17 @@ public class PointWalletV1ApiE2ETest { private static final String ENDPOINT_POINTS = "/api/v1/me/points"; private final TestRestTemplate testRestTemplate; - private final SignUpFacade signUpFacade; + private final UserService userService; private final DatabaseCleanUp databaseCleanUp; @Autowired public PointWalletV1ApiE2ETest( TestRestTemplate testRestTemplate, - SignUpFacade signUpFacade, + UserService userService, DatabaseCleanUp databaseCleanUp ) { this.testRestTemplate = testRestTemplate; - this.signUpFacade = signUpFacade; + this.userService = userService; this.databaseCleanUp = databaseCleanUp; } @@ -61,7 +62,7 @@ void returnsPoints_whenUserExists(Gender gender) { String userId = UserTestFixture.ValidUser.USER_ID; String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); + userService.create(userId, email, birthDate, gender, Point.of(0L)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -145,7 +146,7 @@ void returnsChargedBalance_whenUserExists(Gender gender) { String userId = UserTestFixture.ValidUser.USER_ID; String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); + userService.create(userId, email, birthDate, gender, Point.of(0L)); Long chargeAmount = 1000L; PointWalletV1Dto.ChargeRequest requestBody = new PointWalletV1Dto.ChargeRequest(chargeAmount); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java index a09516c93..9c597b09b 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PurchasingV1ApiE2ETest.java @@ -1,6 +1,6 @@ package com.loopers.interfaces.api; -import com.loopers.application.pointwallet.PointWalletFacade; +import com.loopers.application.user.UserService; import com.loopers.application.signup.SignUpFacade; import com.loopers.domain.brand.Brand; import com.loopers.domain.brand.BrandRepository; @@ -72,7 +72,7 @@ public class PurchasingV1ApiE2ETest { private SignUpFacade signUpFacade; @Autowired - private PointWalletFacade pointWalletFacade; + private UserService userService; @Autowired private ProductRepository productRepository; @@ -108,7 +108,7 @@ private HttpEntity createOrderRequest(Long produc String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; signUpFacade.signUp(userId, email, birthDate, Gender.MALE.name()); - pointWalletFacade.chargePoint(userId, 500_000L); + userService.chargePoint(userId, 500_000L); Brand brand = Brand.of("테스트 브랜드"); Brand savedBrand = brandRepository.save(brand); @@ -240,7 +240,7 @@ void returns200_whenPaymentCallbackSuccess() { String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; signUpFacade.signUp(userId, email, birthDate, Gender.MALE.name()); - pointWalletFacade.chargePoint(userId, 500_000L); + userService.chargePoint(userId, 500_000L); Brand brand = Brand.of("테스트 브랜드"); Brand savedBrand = brandRepository.save(brand); @@ -345,7 +345,7 @@ void returns200_whenPaymentCallbackFailure() { String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; signUpFacade.signUp(userId, email, birthDate, Gender.MALE.name()); - pointWalletFacade.chargePoint(userId, 500_000L); + userService.chargePoint(userId, 500_000L); Brand brand = Brand.of("테스트 브랜드"); Brand savedBrand = brandRepository.save(brand); @@ -456,7 +456,7 @@ void returns200_whenOrderStatusRecovered() { String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; signUpFacade.signUp(userId, email, birthDate, Gender.MALE.name()); - pointWalletFacade.chargePoint(userId, 500_000L); + userService.chargePoint(userId, 500_000L); Brand brand = Brand.of("테스트 브랜드"); Brand savedBrand = brandRepository.save(brand); diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserInfoV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserInfoV1ApiE2ETest.java index f9d33a421..6cea33855 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserInfoV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserInfoV1ApiE2ETest.java @@ -1,7 +1,8 @@ package com.loopers.interfaces.api; -import com.loopers.application.signup.SignUpFacade; +import com.loopers.application.user.UserService; import com.loopers.domain.user.Gender; +import com.loopers.domain.user.Point; import com.loopers.domain.user.UserTestFixture; import com.loopers.interfaces.api.userinfo.UserInfoV1Dto; import com.loopers.utils.DatabaseCleanUp; @@ -31,17 +32,17 @@ public class UserInfoV1ApiE2ETest { private static final String ENDPOINT_ME = "/api/v1/me"; private final TestRestTemplate testRestTemplate; - private final SignUpFacade signUpFacade; + private final UserService userService; private final DatabaseCleanUp databaseCleanUp; @Autowired public UserInfoV1ApiE2ETest( TestRestTemplate testRestTemplate, - SignUpFacade signUpFacade, + UserService userService, DatabaseCleanUp databaseCleanUp ) { this.testRestTemplate = testRestTemplate; - this.signUpFacade = signUpFacade; + this.userService = userService; this.databaseCleanUp = databaseCleanUp; } @@ -61,7 +62,7 @@ void returnsUserInfo_whenUserExists(Gender gender) { String userId = UserTestFixture.ValidUser.USER_ID; String email = UserTestFixture.ValidUser.EMAIL; String birthDate = UserTestFixture.ValidUser.BIRTH_DATE; - signUpFacade.signUp(userId, email, birthDate, gender.name()); + userService.create(userId, email, birthDate, gender, Point.of(0L)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON);