Skip to content

🚀 1단계 - 리팩터링(상품)#336

Open
jwoo-o wants to merge 7 commits intonext-step:jwoo-ofrom
jwoo-o:step1
Open

🚀 1단계 - 리팩터링(상품)#336
jwoo-o wants to merge 7 commits intonext-step:jwoo-ofrom
jwoo-o:step1

Conversation

@jwoo-o
Copy link

@jwoo-o jwoo-o commented Feb 17, 2025

안녕하세여 리뷰어님 1단계 리펙터링(상품)
리뷰 요청 드립니다.

product domain 기준으로 name, price를 일급객체로 분리하여
해당 객체에서 검증하도록 작성하였습니다.

리뷰 잘 부탁드립니다.

Copy link

@kkt219a kkt219a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 진우님 ! 😊
이번 미션을 함께하게 된 김규태입니다.
1단계 미션 잘 진행해주셨어요! 🔥
몇가지 코멘트를 남겼는데 확인 부탁드려요!
궁금하신 점 있으시면 편하게 댓글이나 DM으로 연락주세요.☺️

Comment on lines +50 to +63
final List<Menu> menus = menuRepository.findAllByProductId(productId);
for (final Menu menu : menus) {
BigDecimal sum = BigDecimal.ZERO;
for (final MenuProduct menuProduct : menu.getMenuProducts()) {
sum = sum.add(
menuProduct.getProduct()
.getPrice()
.multiply(BigDecimal.valueOf(menuProduct.getQuantity()))
);
}
if (menu.getPrice().compareTo(sum) > 0) {
menu.setDisplayed(false);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProductService의 메뉴 가격에 대한 검증책임을 분리해보면 어떨까요? :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음 단계인 메뉴 리펙터링부분 에서 해당 코드가 변경이 될거 같습니다!

Comment on lines +53 to +55
public void updatePrice(BigDecimal price) {
this.price = ProductPrice.from(price);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service에서는 changePrice, 도메인에서는 updatePrice라고 표현해주셨는데,
둘을 다르게 표현한 의도 있으실까요? :)
그리고 ProductPrice 대신 BigDecimal을 파라미터로 받게 한 이유도 궁금합니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request의 값을 넘겨주면 Product에서 값객체에 대한 생성에 대한 책임을 가져갈려고 하였습니다.

import kitchenpos.products.tobe.infra.PurgomalumClient;

@Embeddable
public class ProductName {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vo로 정의해보셨군요 👍👍

@@ -0,0 +1,5 @@
package kitchenpos.products.tobe.infra;

public interface PurgomalumClient {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurgomalumClient 인터페이스가 infra 패키지에 위치하고 있는데, ProductName 생성 시 직접 참조되고 있습니다. domain 패키지에 위치하는 것이 더 자연스러울 것 같은데 어떻게 생각하시나요? 그리고 진우님이 생각하시기에 비속어 검증은 도메인의 책임이 맞다고 보시는지 궁금합니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domain에 존재해야 되는거 같아요!
그리고 비속어 검증은 결국 product에 이름이 비속어 들어갈 수 없다는 비즈니스 규칙이므로 도메인의 책임인거 같아요!

import java.math.BigDecimal;

@Embeddable
public class ProductPrice {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProductPrice pp1 = ProductPrice.from(new BigDecimal(500L))
ProductPrice pp2 = ProductPrice.from(new BigDecimal(500L))

pp1과 pp2는 같을까요? :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

euqals and hashcode가 필요하겠군요

}

public static ProductPrice from(BigDecimal price) {
validate(price);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자에서 검증하시지 않은 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체 생성자 실행되기 전에 객체가 검증이 되는 흐름이 맞을거 같아 정적팩터리 메서드를 만들어서 정적팩터리에서 검증하였습니다.


private static void validate(BigDecimal price) {
if (price == null || price.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외도 커스텀화 해보면 어떨까요?

import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

class ProductServiceTest {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인 객체들에 대한 테스트를 작성해보면 좋을 것 같습니다!

Copy link

@kkt219a kkt219a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

피드백 잘 반영해주셨습니다!
몇가지 추가로 코멘트를 남겼는데 확인 부탁드릴게요!

Comment on lines +36 to +51
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ProductPrice that = (ProductPrice) o;
return Objects.equals(price, that.price);
}

@Override
public int hashCode() {
return Objects.hashCode(price);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProductPrice의 동등성을 보장해주셨군요 👍🏻
ProductName도 같은 맥락이라면 동등성을 보장해줘야 할 것 같아요!

Comment on lines +52 to +54
public void changePrice(BigDecimal price) {
this.price = ProductPrice.from(price);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 추후 price가 BigDecimal이 아닌 String도 지원해야한다면,
상품의 가격 변경을 오버로딩해야할 것 같아요 :)
그래서 상품이 가격을 생성할 책임까지 가져갈 필요가 있을까라고 생각되는데 어떻게 생각하실까요?


@BeforeEach
void setUp() {
purgomalumClient = Mockito.mock(PurgomalumClient.class);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProductService에서 Fake를 사용하셨던데, Mock을 사용하신 건 다른 의도가 있으실까요?

Comment on lines +23 to +45
@Test
@DisplayName("상품명이 null일 때 ProductNameRequiredException이 발생해야 한다.")
void testFrom_NullName_ThrowsProductNameRequiredException() {
// Arrange
String name = null;

// Act & Assert
assertThrows(ProductNameRequiredException.class, () -> {
ProductName.from(name, purgomalumClient);
});
}

@Test
@DisplayName("상품명이 비어 있을 때 ProductNameRequiredException이 발생해야 한다.")
void testFrom_EmptyName_ThrowsProductNameRequiredException() {
// Arrange
String name = "";

// Act & Assert
assertThrows(ProductNameRequiredException.class, () -> {
ProductName.from(name, purgomalumClient);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ParameterizedTest@NullAndEmptySource를 활용해보면 어떨까요? :)

}

@Test
@DisplayName("상품명에 비속어가 포함되어 있을 때 ProfanityException이 발생해야 한다.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금처럼 작성해주신 것도 좋지만,
작성하셨던 요구사항이나 모델링을 기반으로 간단하게 표현해보면 어떨까요? :)

상품명은 비속어를 포함할 수 없다.


@Table(name = "product")
@Entity
public class Product {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product에 대한 테스트가 빠진 것 같아요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants