-
Notifications
You must be signed in to change notification settings - Fork 34
상품, 브랜드, 좋아요, 주문 도메인 구현 #76
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?
상품, 브랜드, 좋아요, 주문 도메인 구현 #76
Conversation
- 유저, 포인트 단위, 통합, e2e 테스트 수정정 완료
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 풀 리퀘스트 분석Walkthrough전자상거래 플랫폼을 위한 완전한 도메인 구조 구현. 사용자, 상품, 브랜드, 좋아요, 주문, 포인트에 관한 도메인 모델, 서비스, 레포지토리 계층 추가. 애플리케이션 파사드, REST API 컨트롤러, DTO 계층 신규 구성. 설계 문서 및 E2E 테스트 포함. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant OrderV1Controller
participant OrderFacade
participant OrderService
participant ProductService
participant PointService
participant Repository
Client->>OrderV1Controller: POST /api/v1/orders (X-USER-ID, CreateOrderRequest)
OrderV1Controller->>OrderFacade: createOrder(userId, items)
OrderFacade->>OrderService: createOrder(user, itemRequests)
rect rgb(200, 220, 255)
Note over OrderService: 트랜잭션 블록
loop 각 주문 항목
OrderService->>ProductService: getProduct(productId)
ProductService-->>OrderService: ProductModel
OrderService->>ProductService: updateQuantity(productId, qty)
end
OrderService->>OrderService: 총 가격 계산
OrderService->>PointService: use(user, totalPrice)
OrderService->>Repository: save(orderModel)
end
Repository-->>OrderService: savedOrder
OrderService-->>OrderFacade: OrderModel
OrderFacade-->>OrderV1Controller: OrderInfo
OrderV1Controller-->>Client: ApiResponse(OrderResponse)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 세부 검토 대상:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 27
♻️ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
20-28: @notblank 검증이 UserId 타입에서 작동하지 않을 수 있습니다.
@NotBlank는CharSequence타입에만 적용됩니다.UserId가 커스텀 타입인 경우@NotNull을 사용하거나UserId내부에서 검증을 수행해야 합니다. (LikeV1Controller와 동일한 문제)
🧹 Nitpick comments (31)
.docs/design/03-class-diagram.md (1)
5-9: 포인트(point) 필드 타입 검토
User클래스의point필드가int타입으로 정의되어 있습니다. 포인트 적립이 누적되는 비즈니스 로직이라면int범위 초과로 인한 오버플로우 위험이 있습니다.
- 예상 최대 포인트 규모를 고려하여
long타입 사용을 검토하세요.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java (1)
7-13: 메서드 이름과 파라미터 타입을 일치시켜 주세요.
현재findByUserId가UserModel을 파라미터로 받아 이름과 구현 의도가 어긋납니다. 계약을 읽는 다른 개발자가 혼란을 겪기 쉬우니, 메서드명을findByUser등으로 바꾸거나UserId를 받도록 조정하는 편이 좋겠습니다.- List<OrderModel> findByUserId(UserModel user); + List<OrderModel> findByUser(UserModel user);apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1)
60-64: 필드명을 포함해 검증 오류 메시지를 전달하면 이해가 쉬워집니다.
기본 메시지만 이어붙이면 어떤 필드에서 오류가 발생했는지 파악하기 어렵습니다. 필드명을 함께 노출하고 메시지가 비어 있을 때는 코드 등으로 대체하면 클라이언트와 로그 양쪽 모두에서 진단이 빨라집니다.- String errorMessage = e.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage) + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(fieldError -> { + String message = fieldError.getDefaultMessage(); + return "%s: %s".formatted(fieldError.getField(), message != null ? message : fieldError.getCode()); + }) .collect(Collectors.joining(", "));apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (2)
18-18: 사용하지 않는 import를 제거하세요.Line 18의
java.time.LocalDateimport는 코드에서 사용되지 않습니다.-import java.time.LocalDate;
39-39: 포인트 사용 테스트가 누락되었습니다.TODO 주석이 포인트 사용(차감) 기능에 대한 테스트가 필요함을 나타냅니다.
PointModel이 포인트 사용 메서드를 제공한다면, 해당 기능에 대한 테스트를 추가해야 합니다.포인트 사용 테스트 코드를 생성하거나 이를 추적할 이슈를 생성하길 원하시나요?
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java (1)
10-10: 값 객체 타입을 일관되게 사용하세요.
findByUserId(Long id)의 파라미터 타입이Long이지만,UserJpaRepository.findByUserId(UserId userId)는UserId값 객체를 사용합니다. 도메인 일관성과 타입 안정성을 위해UserId값 객체를 사용하세요.다음과 같이 수정하세요:
-List<OrderModel> findByUserId(Long id); +List<OrderModel> findByUser_UserId(UserId userId);참고: Spring Data JPA는 중첩 속성 쿼리를 위해 언더스코어(
_) 표기법을 지원합니다.OrderModel의user.userId경로를 쿼리하려면findByUser_UserId를 사용하세요.apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java (1)
14-16: 중복 검증 로직을 제거하세요.
BrandFacade에서brandName의 null/blank 검증을 수행하지만,Brand생성자(Line 18)에서도 동일한 검증을 수행할 가능성이 높습니다. 파사드 계층에서의 중복 검증은 불필요합니다.도메인 객체(
Brand)의 생성자에서 검증을 담당하도록 하고, 파사드에서는 도메인 로직을 호출하기만 하세요.@Transactional(readOnly = true) public BrandInfo getBrand(String brandName) { - if (brandName == null || brandName.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "브랜드 이름은 필수입니다."); - } - Brand brand = new Brand(brandName); return BrandInfo.from(brand); }apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
14-20: 성별 값에 대한 검증 로직 추가를 권장합니다.현재 null/blank 체크만 수행하고 있지만,
BirthDate처럼 허용 가능한 성별 값("male", "female" 등)에 대한 검증이 필요합니다. 현재는 임의의 문자열(예: "xyz", "123")도 허용됩니다.다음과 같이 검증 로직 추가를 고려하세요:
public Gender(String gender) { //성별 체크 if (gender == null || gender.isBlank()) { throw new CoreException(ErrorType.BAD_REQUEST, "성별은 비어있을 수 없습니다."); } + if (!gender.matches("^(male|female|other)$")) { + throw new CoreException(ErrorType.BAD_REQUEST, "성별은 'male', 'female', 'other' 중 하나여야 합니다."); + } this.gender = gender; }apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java (1)
7-16: 도메인 Value Object가 애플리케이션 레이어에 노출되고 있습니다.
Brand와Money가 도메인 Value Object인데ProductInfo에 직접 노출되고 있습니다. 애플리케이션 레이어 DTO는 일반적으로 원시 타입이나 단순 타입을 사용하여 도메인 의존성을 최소화하는 것이 좋습니다.다음과 같이 단순 타입으로 변환하는 것을 고려하세요:
-public record ProductInfo(Long id, String name, Brand brand, Money price, Long likeCount) { +public record ProductInfo(Long id, String name, String brand, Integer price, Long likeCount) { public static ProductInfo from(ProductModel model) { return new ProductInfo( model.getId(), model.getName(), - model.getBrand(), - model.getPrice(), + model.getBrand().name(), + model.getPrice().value(), model.getLikeCount() ); } }apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java (1)
51-53: 불필요한 중복 검증을 제거하세요.Line 53의
findById().orElseThrow()는 실제 테스트 대상인 Line 57productService.getProduct(id)와 동일한 로직을 중복 검증합니다. arrange 단계에서는 데이터 준비만 하고, 실제 검증은 act/assert 단계에서 수행해야 합니다.void productService_whenGetProductIsNotFound() { // arrange Long id = 1L; - productJpaRepository.findById(id).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다.")); // act CoreException result = assertThrows(CoreException.class, () -> {apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java (1)
11-13: 중복된 접근자 메서드입니다.레코드는 이미
point()접근자를 자동으로 생성하므로,getPoint()메서드는 중복입니다. JavaBeans 스타일 getter가 특정 프레임워크에서 필요한 경우가 아니라면 제거하는 것이 좋습니다.중복 제거를 원하신다면 다음 diff를 적용하세요:
- public Money getPoint() { - return point; - }apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
25-31: 에러 메시지를 더 구체적으로 개선할 수 있습니다."존재하지 않는 요청입니다."는 너무 일반적인 메시지입니다. 사용자를 찾을 수 없다는 것을 명확히 표현하면 디버깅과 사용자 경험이 개선됩니다.
더 구체적인 메시지로 변경:
if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다."); + throw new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다."); }apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
28-34: equals 구현을 단순화할 수 있습니다.현재 null 체크에 삼항 연산자를 사용하고 있지만,
Objects.equals()를 사용하면 더 간결합니다.다음 diff를 적용하여 equals를 개선하세요:
+import java.util.Objects; + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserId userId1 = (UserId) o; - return userId != null ? userId.equals(userId1.userId) : userId1.userId == null; + return Objects.equals(userId, userId1.userId); }apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (2)
14-23: 형식 검증은 적절하나 날짜 유효성은 검증하지 않습니다.현재 정규식은
yyyy-MM-dd형식만 검증하지만, 실제 날짜 유효성(예: 2025-13-99)은 검증하지 않습니다. 현재 구현도 동작하지만, 더 엄격한 검증이 필요하다면LocalDate.parse()를 사용할 수 있습니다.날짜 유효성까지 검증하려면 다음 diff를 적용하세요:
+import java.time.LocalDate; +import java.time.format.DateTimeParseException; + public BirthDate(String birthDate) { //생년월일 validation check if (birthDate == null || birthDate.isBlank()) { throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다."); } - if (!birthDate.matches("^\\d{4}-\\d{2}-\\d{2}$")) { + try { + LocalDate.parse(birthDate); + } catch (DateTimeParseException e) { throw new CoreException(ErrorType.BAD_REQUEST, "생년월일이 `yyyy-MM-dd` 형식에 맞아야 합니다."); } this.birthDate = birthDate; }
29-35: equals 구현을 단순화할 수 있습니다.
Objects.equals()를 사용하면 더 간결하고 읽기 쉬운 코드가 됩니다.다음 diff를 적용하세요:
+import java.util.Objects; + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BirthDate birthDate1 = (BirthDate) o; - return birthDate != null ? birthDate.equals(birthDate1.birthDate) : birthDate1.birthDate == null; + return Objects.equals(birthDate, birthDate1.birthDate); }apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (2)
20-33: 포인트 조회 로직을 개선하세요.
getPoint메서드에서new PointModel(user, new Money(0))를 생성하는 것은 불필요하고 혼란스럽습니다.PointService.findPoint가PointModel을 인자로 받지만 실제로는user필드만 사용하므로, 서비스 메서드 시그니처를findPoint(UserModel user)또는findByUser(UserModel user)로 변경하는 것을 권장합니다.다음과 같이 개선할 수 있습니다:
PointService.java 수정:
-public PointModel findPoint(PointModel point) { - UserModel requestUser = point.getUser(); +public PointModel findByUser(UserModel user) { var foundUser = userRepository.findById(requestUser.getId()); if (foundUser == null) { return null; } return pointRepository.findPoint(foundUser.get()).orElse(null); }PointFacade.java 수정:
public PointInfo getPoint(UserId userId) { UserModel user = userService.getUser(userId); if (user == null) { throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다."); } - PointModel pointModel = new PointModel(user, new Money(0)); - PointModel point = pointService.findPoint(pointModel); + PointModel point = pointService.findByUser(user); if (point == null) { throw new CoreException(ErrorType.NOT_FOUND, "포인트 정보가 없습니다."); } return PointInfo.from(point); }
35-45: 포인트 충전 후 불필요한 조회를 제거하세요.충전 후 다시 포인트를 조회하는 로직(lines 43-44)이 비효율적입니다.
PointService.charge메서드가 충전된PointModel을 반환하도록 수정하면 불필요한 데이터베이스 조회를 줄일 수 있습니다.다음과 같이 개선할 수 있습니다:
PointService.java 수정:
@Transactional -public void charge(PointModel point) { +public PointModel charge(PointModel point) { UserModel user = point.getUser(); var foundUser = userRepository.findById(user.getId()) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저가 존재하지 않습니다.")); var existing = pointRepository.findPoint(foundUser); if (existing.isPresent()) { existing.get().charge(point.getPoint()); - pointRepository.save(existing.get()); - return; + return pointRepository.save(existing.get()); } - pointRepository.save(new PointModel(foundUser, point.getPoint())); + return pointRepository.save(new PointModel(foundUser, point.getPoint())); }PointFacade.java 수정:
public PointInfo chargePoint(UserId userId, Money point) { UserModel user = userService.getUser(userId); if (user == null) { throw new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 요청입니다."); } PointModel pointModel = new PointModel(user, point); - pointService.charge(pointModel); - - PointModel charged = pointService.findPoint(new PointModel(user, point)); + PointModel charged = pointService.charge(pointModel); return PointInfo.from(charged); }apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
48-54: 중복된 포인트 검증 조건을 정리해 주세요.
Line 48에서 이미this.point.value() < usePoint.value()조건으로 부족분을 차단하고 있어, Line 52의usePoint.value() > this.point.value()분기는 동일 조건을 다시 검사하는 unreachable 코드가 됩니다. 불필요한 분기는 유지보수 시 혼선을 줄 수 있으니 제거하는 편이 좋겠습니다.- if (usePoint.value() > this.point.value()) { - throw new CoreException(ErrorType.BAD_REQUEST, "사용 금액이 보유 포인트를 초과합니다."); - }apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java (1)
17-31: 헤더 파라미터 위치를 명시해 주세요.
@Parameter기본값은 쿼리 파라미터로 문서화되기 때문에, 현재 상태로는 Swagger UI에서X-USER-ID가 쿼리 파라미터로 노출됩니다. 실제 헤더 기반 호출과 문서가 어긋나 혼선을 줄 수 있으니in = ParameterIn.HEADER(필요 시ParameterInimport 추가) 또는 구현부와 동일하게@RequestHeader를 붙여 명시적으로 표시해 주세요.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java (1)
17-39: 모든 UserId 파라미터에 헤더 위치를 지정해 주세요.
Point API와 동일하게@Parameter만 사용하면 Swagger가X-USER-ID를 쿼리 파라미터로 표기합니다.createOrder와getUserOrders의 UserId 모두in = ParameterIn.HEADER(또는@RequestHeader)를 지정해 실제 호출 방식과 문서가 일치하도록 보완해 주세요.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
34-62: Switch 표현식을 사용한 리팩터링 고려현재 switch 문은 Java 14+의 향상된 switch 표현식을 사용하여 더 간결하게 작성할 수 있습니다.
다음과 같이 리팩터링을 고려해보세요:
private Pageable convertSortToPageable(String sort, Pageable pageable) { - Sort.Direction direction; - String property; - - switch (sort) { - case "latest": - property = "id"; - direction = Sort.Direction.DESC; - break; - case "price_asc": - property = "price"; - direction = Sort.Direction.ASC; - break; - case "likes_desc": - // likes_desc는 메모리에서 정렬하므로 여기서는 기본 정렬 사용 - property = "id"; - direction = Sort.Direction.DESC; - break; - default: - property = "id"; - direction = Sort.Direction.DESC; - } + var sortConfig = switch (sort) { + case "price_asc" -> new Object() { String property = "price"; Sort.Direction direction = Sort.Direction.ASC; }; + case "likes_desc", "latest", default -> new Object() { String property = "id"; Sort.Direction direction = Sort.Direction.DESC; }; + }; return org.springframework.data.domain.PageRequest.of( pageable.getPageNumber(), pageable.getPageSize(), - Sort.by(direction, property) + Sort.by(sortConfig.direction, sortConfig.property) ); }apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
17-20: 일관성 있는 에러 처리 패턴 고려
getUser메서드가 사용자를 찾지 못했을 때null을 반환하는 반면,signUp은 중복 시 예외를 던집니다. 호출하는 코드(UserFacade, PointFacade 등)에서 매번 null 체크 후 예외를 던지고 있으므로, 서비스 레이어에서 일관되게 예외를 던지는 것이 더 명확할 수 있습니다.다음과 같이 리팩터링을 고려해보세요:
@Transactional(readOnly = true) public UserModel getUser(UserId userId) { - return userRepository.find(userId).orElse(null); + return userRepository.find(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 유저입니다.")); }이렇게 하면 파사드 레이어에서 null 체크를 제거할 수 있어 코드가 더 간결해집니다.
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
35-38: 중복된 예외 처리 확인
ProductService.getQuantity()를 확인한 결과, 해당 메서드는 내부에서 이미 상품이 없을 때NOT_FOUND예외를 던지고 있습니다. 따라서 Line 37의orElseThrow는 실행되지 않는 중복 코드입니다.다음과 같이 단순화할 수 있습니다:
public Quantity getQuantity(Long id) { - return productService.getQuantity(id).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다.")); + return productService.getQuantity(id).orElseThrow(); }또는 ProductService.getQuantity가 이미 예외를 던지므로 Optional을 반환하지 않고 Quantity를 직접 반환하도록 수정하는 것이 더 명확할 수 있습니다.
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
28-34: 스트림 API 간소화 고려Java 16 이상을 사용 중이라면,
.collect(Collectors.toList())를.toList()로 단순화할 수 있습니다.다음과 같이 리팩터링할 수 있습니다:
public List<ProductModel> findLikedProductsByUser(UserModel user) { return likeJpaRepository.findByUser(user).stream() .map(LikeModel::getProduct) - .collect(Collectors.toList()); + .toList(); }apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
27-27: Void 응답에 null 대신 의미 있는 응답 고려.
ApiResponse.success(null)은 작동하지만, 성공 여부를 명시적으로 나타내는 응답 객체를 반환하는 것이 더 명확할 수 있습니다.apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
17-26: 형식 검증 추가를 고려하세요.필드의 존재 여부만 검증하고 있습니다. 다음 검증을 추가하는 것이 좋습니다:
gender:@Pattern또는 enum으로 허용 값 제한birthDate:@Pattern으로 날짜 형식 검증 (예: yyyy-MM-dd)예시:
public record SignupRequest( @NotBlank(message = "userId는 필수입니다.") String userId, - @NotBlank(message = "email은 필수입니다.") + @NotBlank(message = "email은 필수입니다.") + @Email(message = "올바른 이메일 형식이 아닙니다.") String email, - @NotBlank(message = "gender는 필수입니다.") + @NotBlank(message = "gender는 필수입니다.") + @Pattern(regexp = "^(MALE|FEMALE|OTHER)$", message = "올바른 성별 값이 아닙니다.") String gender, - @NotBlank(message = "birthDate는 필수입니다.") + @NotBlank(message = "birthDate는 필수입니다.") + @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "날짜 형식은 yyyy-MM-dd여야 합니다.") String birthDate ) {}apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (2)
38-41: 좋아요 수 조회 로직 중복.Lines 38-41과 Lines 81-83에서 동일한 패턴으로 좋아요 수를 조회하고 설정합니다. 이를 별도 메서드로 추출하여 중복을 제거하는 것이 좋습니다.
예시:
private void enrichProductsWithLikeCounts(List<ProductModel> products) { Map<Long, Long> likeCounts = likeRepository .countByProductIdsLiked(products.stream() .map(ProductModel::getId) .collect(Collectors.toSet())); products.forEach(product -> product.setLikeCount(likeCounts.getOrDefault(product.getId(), 0L))); }Also applies to: 77-85
62-67: Optional 래핑이 불필요합니다.메서드가 상품이 없으면 예외를 던지므로,
Optional.of()로 래핑하는 것은 불필요합니다. 항상 값이 있는 경우에만 반환되므로 직접Quantity를 반환하는 것이 더 명확합니다.-@Transactional(readOnly = true) -public Optional<Quantity> getQuantity(Long id) { +@Transactional(readOnly = true) +public Quantity getQuantity(Long id) { ProductModel product = productRepository.findById(id) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품이 존재하지 않습니다.")); - return Optional.of(product.getQuantity()); + return product.getQuantity(); }이 경우 ProductFacade도 함께 수정이 필요합니다.
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
72-72: OrderItemRequest에 검증 제약 조건 추가 권장.레코드에 검증 어노테이션을 추가하면 API 레이어에서 더 일찍 오류를 잡을 수 있습니다.
-public record OrderItemRequest(Long productId, Integer quantity) {} +public record OrderItemRequest( + @NotNull(message = "상품 ID는 필수입니다.") + Long productId, + @NotNull(message = "수량은 필수입니다.") + @Min(value = 1, message = "수량은 1개 이상이어야 합니다.") + Integer quantity +) {}필요한 import 추가:
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Min;apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java (1)
42-44: OrderItemRequest에 검증 어노테이션 추가 고려.현재 검증이 없어 null 또는 유효하지 않은 값이 서비스 계층까지 전달될 수 있습니다. OrderService.OrderItemRequest와 동일한 검증을 추가하는 것이 좋습니다.
public record CreateOrderRequest(List<OrderItemRequest> items) { - public record OrderItemRequest(Long productId, Integer quantity) {} + public record OrderItemRequest( + @NotNull(message = "상품 ID는 필수입니다.") + Long productId, + @NotNull(message = "수량은 필수입니다.") + @Min(value = 1, message = "수량은 1개 이상이어야 합니다.") + Integer quantity + ) {} }필요한 import 추가:
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Min;apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
28-65: 검증 로직 중복을 제거하세요.
toggleLike,addLike,removeLike메서드에서 사용자 및 상품 검증 로직이 동일하게 반복됩니다. 이를 별도의 private 메서드로 추출하여 중복을 제거하는 것이 좋습니다.예시 리팩토링:
+ private UserModel validateAndGetUser(UserId userId) { + UserModel user = userService.getUser(userId); + if (user == null) { + throw new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다."); + } + return user; + } + + private ProductModel validateAndGetProduct(Long productId) { + ProductModel product = productService.getProduct(productId); + if (product == null) { + throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."); + } + return product; + } @Transactional public void toggleLike(UserId userId, Long productId) { - UserModel user = userService.getUser(userId); - if (user == null) { - throw new CoreException(ErrorType.NOT_FOUND, "유저를 찾을 수 없습니다."); - } - ProductModel product = productService.getProduct(productId); - if (product == null) { - throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."); - } + UserModel user = validateAndGetUser(userId); + ProductModel product = validateAndGetProduct(productId); likeService.toggleLike(user, product); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (81)
.docs/design/01-requirements.md(1 hunks).docs/design/02-sequence-diagrams.md(1 hunks).docs/design/03-class-diagram.md(1 hunks).docs/design/04-erd.md(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/like/LikeModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java(2 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1ApiSpec.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java(1 hunks)apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java(1 hunks)apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-31T14:57:09.157Z
Learnt from: minor7295
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 22
File: apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointsV1Controller.java:24-34
Timestamp: 2025-10-31T14:57:09.157Z
Learning: In the loopers-spring-java-template repository, the X-USER-ID header-based user identification is temporarily used for learning purposes and will be replaced with proper authentication (Spring Security) in a future PR. Do not flag this as a security issue until proper authentication is implemented.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/user/UserId.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java
📚 Learning: 2025-11-12T13:04:50.782Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 72
File: core/domain/src/main/java/com/loopers/core/domain/productlike/vo/ProductLikeId.java:5-7
Timestamp: 2025-11-12T13:04:50.782Z
Learning: In the com.loopers codebase, domain entity ID value objects (e.g., ProductLikeId, OrderItemId, ProductId, PaymentId, OrderId, BrandId) are system-generated identifiers and do not require pattern validation (regex, length checks). They are implemented as simple records with a String value and an empty() factory method returning null for unsaved entities. This differs from UserIdentifier, which is a user-supplied login ID that requires format validation. Domain IDs should not be flagged for missing validation logic in the create() method.
<!-- [add_learning]
UserIdentifier와 같은 사용자 입력 ID와 ProductLikeId, OrderItemId 등의 도메인 ID는 검증 패턴이 다릅니다. UserIdentifier는 사용자가 입력하는 로그인 ID로서 정규식, 길이 등의 형식 검증이 필요하지만, 도메인 ID는 시스템에서 생성하는 식별자(UUID, DB 생성 ID)이므로 패턴 검증이 불필요합니다. 도메인 ID VO는 단순한 record와 empty() 팩토리 메서드만으로 충분합니다.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/user/Email.javaapps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.javaapps/commerce-api/src/main/java/com/loopers/domain/user/Gender.javaapps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.javaapps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.javaapps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java
📚 Learning: 2025-10-31T02:20:33.781Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 15
File: core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java:16-27
Timestamp: 2025-10-31T02:20:33.781Z
Learning: In UserIdentifier and similar value objects, when the constructor performs only null-checking while the static create() method performs full validation (regex, length, etc.), this is an intentional pattern for schema evolution. The constructor is used by the persistence layer to reconstruct domain objects from the database (no validation needed for already-validated legacy data), while create() is used by the application layer to create new domain objects (with validation for new data). This allows backward compatibility when validation rules change in production without requiring migration of all existing database records.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java
📚 Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
🧬 Code graph analysis (46)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
RequiredArgsConstructor(21-86)apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
RequiredArgsConstructor(14-39)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(14-83)
apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (2)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
RequiredArgsConstructor(11-31)apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
RequiredArgsConstructor(13-50)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
Embeddable(7-38)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
Entity(8-49)
apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
RequiredArgsConstructor(14-57)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
RequiredArgsConstructor(14-46)apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
RequiredArgsConstructor(11-31)apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
RequiredArgsConstructor(11-30)apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
RequiredArgsConstructor(11-25)
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
RequiredArgsConstructor(12-79)apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
RequiredArgsConstructor(18-90)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
RequiredArgsConstructor(13-49)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (4)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
DisplayName(16-103)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (3)
DisplayName(48-154)DisplayName(156-182)SpringBootTest(26-183)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (1)
DisplayName(15-53)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (2)
DisplayName(22-74)DisplayName(76-126)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
DisplayName(16-103)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (2)
DisplayName(48-154)DisplayName(156-182)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
Entity(15-60)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
RequiredArgsConstructor(12-79)apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
RequiredArgsConstructor(18-90)apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
RequiredArgsConstructor(16-59)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java (1)
LikeV1Dto(8-37)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
RequiredArgsConstructor(14-46)apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (1)
RequiredArgsConstructor(11-30)apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
RequiredArgsConstructor(14-57)apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
RequiredArgsConstructor(14-32)
apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
RequiredArgsConstructor(14-43)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
RequiredArgsConstructor(21-86)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(14-83)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
DisplayName(21-41)apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (3)
DisplayName(67-114)DisplayName(116-167)SpringBootTest(28-168)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
Embeddable(7-38)apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
Embeddable(7-40)
apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (2)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
RequiredArgsConstructor(17-70)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
RequiredArgsConstructor(18-73)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
RequiredArgsConstructor(14-43)apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
RequiredArgsConstructor(21-86)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
ProductV1Dto(7-47)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
PointV1Dto(6-19)apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
SpringBootTest(30-165)
apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
RequiredArgsConstructor(11-31)apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
RequiredArgsConstructor(14-57)apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
RequiredArgsConstructor(11-25)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
RequiredArgsConstructor(18-73)apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (1)
RequiredArgsConstructor(12-32)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (1)
RequiredArgsConstructor(17-59)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (1)
RequiredArgsConstructor(18-90)apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
RequiredArgsConstructor(16-59)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
RequiredArgsConstructor(13-49)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (2)
DisplayName(48-86)DisplayName(88-102)apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java (2)
DisplayName(67-114)DisplayName(116-167)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
DisplayName(16-103)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (2)
DisplayName(22-74)DisplayName(76-126)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
DisplayName(21-41)apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
DisplayName(51-92)DisplayName(94-128)apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (2)
DisplayName(66-117)DisplayName(119-164)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
UserV1Dto(6-27)
apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java (1)
Embeddable(7-37)apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
Embeddable(7-40)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java (1)
Entity(20-60)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (1)
RequiredArgsConstructor(14-43)apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java (1)
RequiredArgsConstructor(14-39)apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
RequiredArgsConstructor(14-83)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (1)
RequiredArgsConstructor(14-46)apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
RequiredArgsConstructor(14-57)apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
RequiredArgsConstructor(14-32)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Dto.java (1)
PointV1Dto(6-19)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (3)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
RequiredArgsConstructor(17-70)apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
RequiredArgsConstructor(18-73)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java (1)
OrderV1Dto(8-45)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
DisplayName(21-41)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
DisplayName(20-40)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (1)
DisplayName(58-234)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java (1)
RequiredArgsConstructor(12-79)apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
RequiredArgsConstructor(16-59)apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java (1)
RequiredArgsConstructor(13-49)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java (1)
DisplayName(21-41)apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
DisplayName(48-86)DisplayName(88-102)SpringBootTest(26-103)apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (3)
DisplayName(51-92)DisplayName(94-128)SpringBootTest(24-129)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
RequiredArgsConstructor(13-50)apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java (1)
BrandV1Dto(5-11)
apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (3)
apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
Embeddable(7-38)apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
Embeddable(7-40)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
Embeddable(7-37)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemModel.java (1)
Entity(16-63)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java (3)
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java (1)
DisplayName(16-103)apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
DisplayName(20-40)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemModelTest.java (1)
DisplayName(15-53)
apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java (3)
DisplayName(48-86)DisplayName(88-102)SpringBootTest(26-103)apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java (1)
DisplayName(14-120)
apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
RequiredArgsConstructor(11-31)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
RequiredArgsConstructor(13-40)apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java (1)
RequiredArgsConstructor(14-57)apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java (1)
RequiredArgsConstructor(11-25)
apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
Embeddable(7-37)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (4)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java (1)
RequiredArgsConstructor(11-31)apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java (1)
RequiredArgsConstructor(13-40)apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (1)
RequiredArgsConstructor(14-32)apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.java (1)
UserV1Dto(6-27)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (2)
apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java (1)
DisplayName(20-40)apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java (2)
DisplayName(58-234)SpringBootTest(33-235)
apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java (1)
apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java (1)
Entity(15-60)
apps/commerce-api/src/main/java/com/loopers/domain/product/Brand.java (6)
apps/commerce-api/src/main/java/com/loopers/domain/common/Money.java (1)
Embeddable(7-37)apps/commerce-api/src/main/java/com/loopers/domain/common/Quantity.java (1)
Embeddable(7-37)apps/commerce-api/src/main/java/com/loopers/domain/user/BirthDate.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Email.java (1)
Embeddable(7-41)apps/commerce-api/src/main/java/com/loopers/domain/user/Gender.java (1)
Embeddable(7-38)apps/commerce-api/src/main/java/com/loopers/domain/user/UserId.java (1)
Embeddable(7-40)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (3)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
RequiredArgsConstructor(17-70)apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java (1)
RequiredArgsConstructor(12-32)apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java (1)
RequiredArgsConstructor(17-59)
apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java
Outdated
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
Show resolved
Hide resolved
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java
Outdated
Show resolved
Hide resolved
| @DisplayName("상품 다건 조회 시 상품이 없으면 NOT_FOUND 예외가 발생한다.") | ||
| @Test | ||
| void productService_whenGetProductsIsNotFound() { | ||
| // arrange | ||
| productJpaRepository.save(new ProductModel("제목", new Brand("Apple"), new Money(10000), new Quantity(10))); | ||
|
|
||
| } |
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.
테스트가 불완전합니다.
arrange 단계에서 상품을 저장하지만, act/assert 단계가 없어 실제로 아무것도 검증하지 않습니다. DisplayName에 "상품이 없으면 NOT_FOUND 예외가 발생한다"고 명시되어 있으나 구현이 누락되었습니다.
테스트를 완성하거나 불필요하면 제거하세요:
@DisplayName("상품 다건 조회 시 상품이 없으면 NOT_FOUND 예외가 발생한다.")
@Test
void productService_whenGetProductsIsNotFound() {
- // arrange
- productJpaRepository.save(new ProductModel("제목", new Brand("Apple"), new Money(10000), new Quantity(10)));
-
+ // act
+ CoreException result = assertThrows(CoreException.class, () -> {
+ productService.getProducts(/* appropriate parameters */);
+ });
+
+ // assert
+ assertThat(result.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java
around lines 40 to 46, the test only contains arrange code and is missing the
act/assert steps; remove or adjust the saved product so the repository is empty
(or explicitly clear it), then call the service method that retrieves multiple
products (act) and assert that it throws the expected NOT_FOUND exception
(assert). Ensure you use the test framework's exception assertion (e.g.,
assertThrows) and include any necessary parameters for the service call to
reproduce the "no products" case.
📌 Summary
상품(Product), 브랜드(Brand), 좋아요(Like), 주문(Order) 도메인 기능 구현 및 테스트 코드 작성
💬 리뷰 포인트
✅ Checklist
🏷 Product / Brand 도메인
[x] 상품 정보 객체는 브랜드 정보, 좋아요 수를 포함한다.
[x] 상품의 정렬 조건(latest, price_asc, likes_desc) 을 고려한 조회 기능을 설계했다
[x] 상품은 재고를 가지고 있고, 주문 시 차감할 수 있어야 한다
[x] 재고는 감소만 가능하며 음수 방지는 도메인 레벨에서 처리된다
👍 Like 도메인
[x] 좋아요는 유저와 상품 간의 관계로 별도 도메인으로 분리했다
[x] 중복 좋아요 방지를 위한 멱등성 처리가 구현되었다
[x] 상품의 좋아요 수는 상품 상세/목록 조회에서 함께 제공된다
[x] 단위 테스트에서 좋아요 등록/취소/중복 방지 흐름을 검증했다
🛒 Order 도메인
[x] 주문은 여러 상품을 포함할 수 있으며, 각 상품의 수량을 명시한다
[x] 주문 시 상품의 재고 차감, 유저 포인트 차감 등을 수행한다
[x] 재고 부족, 포인트 부족 등 예외 흐름을 고려해 설계되었다
[x] 단위 테스트에서 정상 주문 / 예외 주문 흐름을 모두 검증했다
🧩 도메인 서비스
[x] 도메인 간 협력 로직은 Domain Service에 위치시켰다
[x] 상품 상세 조회 시 Product + Brand 정보 조합은 도메인 서비스에서 처리했다
[x] 복합 유스케이스는 Application Layer에 존재하고, 도메인 로직은 위임되었다
[x] 도메인 서비스는 상태 없이, 도메인 객체의 협력 중심으로 설계되었다
🧱 소프트웨어 아키텍처 & 설계
[ ] 전체 프로젝트의 구성은 아래 아키텍처를 기반으로 구성되었다 (Application → Domain ← Infrastructure)
[ ] Application Layer는 도메인 객체를 조합해 흐름을 orchestration 했다
[ ] 핵심 비즈니스 로직은 Entity, VO, Domain Service 에 위치한다
[ ] Repository Interface는 Domain Layer 에 정의되고, 구현체는 Infra에 위치한다
[ ] 패키지는 계층 + 도메인 기준으로 구성되었다 (/domain/order, /application/like 등)
[ ] 테스트는 외부 의존성을 분리하고, Fake/Stub 등을 사용해 단위 테스트가 가능하게 구성되었다