-
Notifications
You must be signed in to change notification settings - Fork 43
[3주차] 도메인 주도 설계 구현 - 정인철 #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a90b419
01f9a27
e3501eb
d582233
f2b25a7
264c468
ccbd991
f64462d
c55702a
29f2362
c4ee07d
5443b24
5fdbf76
2111134
3045da9
52e1b44
c15f5e1
51e2529
b6437c8
4fef080
08cea89
4694c57
327c9f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.loopers.application.brand; | ||
|
|
||
| import com.loopers.domain.brand.BrandModel; | ||
| import com.loopers.domain.brand.BrandService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class BrandFacade { | ||
|
|
||
| private final BrandService brandService; | ||
|
|
||
| @Transactional | ||
| public BrandInfo register(String name, String description, String imageUrl) { | ||
| BrandModel brand = brandService.register(name, description, imageUrl); | ||
| return BrandInfo.from(brand); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public BrandInfo getById(Long id) { | ||
| BrandModel brand = brandService.getById(id); | ||
| return BrandInfo.from(brand); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.loopers.application.brand; | ||
|
|
||
| import com.loopers.domain.brand.BrandModel; | ||
|
|
||
| public record BrandInfo( | ||
| Long id, | ||
| String name, | ||
| String description, | ||
| String imageUrl | ||
| ) { | ||
| public static BrandInfo from(BrandModel model) { | ||
| return new BrandInfo( | ||
| model.getId(), | ||
| model.getName(), | ||
| model.getDescription(), | ||
| model.getImageUrl() | ||
| ); | ||
| } | ||
| } |
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.loopers.application.like; | ||
|
|
||
| import com.loopers.domain.like.ProductLikeService; | ||
| import com.loopers.domain.member.MemberModel; | ||
| import com.loopers.domain.member.MemberService; | ||
| import com.loopers.domain.product.ProductService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class LikeFacade { | ||
|
|
||
| private final ProductLikeService productLikeService; | ||
| private final MemberService memberService; | ||
| private final ProductService productService; | ||
|
|
||
| @Transactional | ||
| public void addLike(String loginId, String password, Long productId) { | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
| productService.getById(productId); | ||
| productLikeService.addLike(member.getId(), productId); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void removeLike(String loginId, String password, Long productId) { | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
| productService.getById(productId); | ||
| productLikeService.removeLike(member.getId(), productId); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public long countByProductId(Long productId) { | ||
| return productLikeService.countByProductId(productId); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package com.loopers.application.member; | ||
|
|
||
| import com.loopers.domain.member.MemberModel; | ||
| import com.loopers.domain.member.MemberService; | ||
| import com.loopers.domain.point.PointModel; | ||
| import com.loopers.domain.point.PointService; | ||
| import com.loopers.domain.vo.Money; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class MemberFacade { | ||
|
|
||
| private final MemberService memberService; | ||
| private final PointService pointService; | ||
|
|
||
| @Transactional | ||
| public void register(String loginId, String password, String name, LocalDate birthDate, String email) { | ||
| MemberModel member = memberService.register(loginId, password, name, birthDate, email); | ||
| pointService.createPoint(member.getId()); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public MemberInfo getMyInfo(String loginId, String password) { | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
| PointModel point = pointService.getByMemberId(member.getId()); | ||
| return MemberInfo.of(member, point.getBalanceMoney()); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void chargePoint(String loginId, String password, long amount) { | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
| pointService.charge(member.getId(), Money.of(amount)); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void changePassword(String loginId, String currentPassword, String newPassword) { | ||
| memberService.changePassword(loginId, currentPassword, newPassword); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.loopers.application.member; | ||
|
|
||
| import com.loopers.domain.member.MemberModel; | ||
| import com.loopers.domain.vo.Money; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| public record MemberInfo( | ||
| String loginId, | ||
| String maskedName, | ||
| LocalDate birthDate, | ||
| String email, | ||
| Money point | ||
| ) { | ||
| public static MemberInfo of(MemberModel model, Money point) { | ||
| return new MemberInfo( | ||
| model.getLoginId(), | ||
| model.getMaskedName(), | ||
| model.getBirthDate(), | ||
| model.getEmail(), | ||
| point | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| import com.loopers.domain.brand.BrandService; | ||
| import com.loopers.domain.member.MemberModel; | ||
| import com.loopers.domain.member.MemberService; | ||
| import com.loopers.domain.order.OrderItemModel; | ||
| import com.loopers.domain.order.OrderModel; | ||
| import com.loopers.domain.order.OrderService; | ||
| import com.loopers.domain.point.PointService; | ||
| import com.loopers.domain.product.ProductModel; | ||
| import com.loopers.domain.product.ProductService; | ||
| import com.loopers.domain.vo.Money; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class OrderFacade { | ||
|
|
||
| private final OrderService orderService; | ||
| private final MemberService memberService; | ||
| private final ProductService productService; | ||
| private final BrandService brandService; | ||
| private final PointService pointService; | ||
|
|
||
| @Transactional | ||
| public OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) { | ||
| // 1. 회원 인증 | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
|
|
||
| // 2. 주문 항목 생성 (상품 스냅샷 + 재고 차감) | ||
| List<OrderItemModel> orderItems = new ArrayList<>(); | ||
| for (OrderItemRequest req : itemRequests) { | ||
| ProductModel product = productService.getById(req.productId()); | ||
| productService.decreaseStock(product.getId(), req.quantity()); | ||
|
|
||
| OrderItemModel item = new OrderItemModel( | ||
| product.getId(), | ||
| product.getName(), | ||
| brandService.getById(product.getBrandId()).getName(), | ||
| product.getPrice(), | ||
| req.quantity() | ||
| ); | ||
| orderItems.add(item); | ||
| } | ||
|
|
||
| // 3. 주문 생성 | ||
| OrderModel order = orderService.createOrder(member.getId(), orderItems); | ||
|
|
||
| // 4. 포인트 차감 | ||
| pointService.use(member.getId(), Money.of(order.getTotalAmount())); | ||
|
|
||
| List<OrderItemModel> savedItems = orderService.getOrderItems(order.getId()); | ||
| return OrderInfo.from(order, savedItems); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public OrderInfo getById(Long id) { | ||
| OrderModel order = orderService.getById(id); | ||
| List<OrderItemModel> items = orderService.getOrderItems(order.getId()); | ||
| return OrderInfo.from(order, items); | ||
|
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주문 단건 조회에 소유권 검증이 없어 타회원 주문이 노출될 수 있다. 현재는 주문 ID만 알면 누구의 주문이든 조회 가능한 구조라 개인정보/주문정보 유출 위험이 있다. 조회 시 요청자 식별 정보를 받아 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public List<OrderInfo> getByMember(String loginId, String password) { | ||
| MemberModel member = memberService.getMyInfo(loginId, password); | ||
| List<OrderModel> orders = orderService.getByMemberId(member.getId()); | ||
|
|
||
| return orders.stream() | ||
| .map(order -> { | ||
| List<OrderItemModel> items = orderService.getOrderItems(order.getId()); | ||
| return OrderInfo.from(order, items); | ||
| }) | ||
| .toList(); | ||
| } | ||
|
|
||
| public record OrderItemRequest( | ||
| Long productId, | ||
| int quantity | ||
| ) { | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| import com.loopers.domain.order.OrderItemModel; | ||
| import com.loopers.domain.order.OrderModel; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record OrderInfo( | ||
| Long id, | ||
| Long memberId, | ||
| int totalAmount, | ||
| List<OrderItemInfo> items | ||
| ) { | ||
| public record OrderItemInfo( | ||
| Long productId, | ||
| String productName, | ||
| String brandName, | ||
| int productPrice, | ||
| int quantity, | ||
| int totalAmount | ||
| ) { | ||
| public static OrderItemInfo from(OrderItemModel model) { | ||
| return new OrderItemInfo( | ||
| model.getProductId(), | ||
| model.getProductName(), | ||
| model.getBrandName(), | ||
| model.getProductPrice(), | ||
| model.getQuantity(), | ||
| model.getTotalAmount() | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public static OrderInfo from(OrderModel order, List<OrderItemModel> items) { | ||
| return new OrderInfo( | ||
| order.getId(), | ||
| order.getMemberId(), | ||
| order.getTotalAmount(), | ||
| items.stream().map(OrderItemInfo::from).toList() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.loopers.domain.brand.BrandModel; | ||
| import com.loopers.domain.product.ProductModel; | ||
|
|
||
| public record ProductDetailInfo( | ||
| Long id, | ||
| String name, | ||
| String description, | ||
| int price, | ||
| int stockQuantity, | ||
| String imageUrl, | ||
| String brandName, | ||
| long likeCount | ||
| ) { | ||
| public static ProductDetailInfo of(ProductModel product, BrandModel brand, long likeCount) { | ||
| return new ProductDetailInfo( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getDescription(), | ||
| product.getPrice(), | ||
| product.getStockQuantity(), | ||
| product.getImageUrl(), | ||
| brand.getName(), | ||
| likeCount | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주문 항목 입력 검증 부재로 500 응답이 발생할 수 있다.
운영 환경에서
itemRequests가null/empty이거나 항목의productId가null,quantity <= 0이면 현재는 NPE 또는 하위 예외로 흘러 일관된 오류 포맷이 깨질 수 있다. 메서드 시작 시CoreException(BAD_REQUEST)로 명시 검증을 추가하기 바란다.추가 테스트로
null, 빈 리스트,null productId,0/음수 quantity입력에 대한createOrder예외 케이스를 단위 테스트로 보강하기 바란다.🔧 제안 수정안
`@Transactional` public OrderInfo createOrder(String loginId, String password, List<OrderItemRequest> itemRequests) { + if (itemRequests == null || itemRequests.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목은 1개 이상이어야 한다."); + } + for (OrderItemRequest req : itemRequests) { + if (req == null || req.productId() == null || req.quantity() <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목 정보가 올바르지 않다."); + } + } // 1. 회원 인증 MemberModel member = memberService.getMyInfo(loginId, password);Based on learnings: In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format.
📝 Committable suggestion
🤖 Prompt for AI Agents