Skip to content
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

2단계 - 리팩터링(메뉴) #169

Open
wants to merge 35 commits into
base: chr0m3
Choose a base branch
from
Open

2단계 - 리팩터링(메뉴) #169

wants to merge 35 commits into from

Conversation

chr0m3
Copy link

@chr0m3 chr0m3 commented Oct 24, 2022

시간이 얼마 남지 않았다보니 마음이 급해 충분히 잘 되었을지 모르겠네요..
그래도 최선을 다해보았습니다.


public final UUID productId;

public final Price pricePerUnit;
Copy link
Author

Choose a reason for hiding this comment

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

단순히 price를 사용하는 경우 메뉴 하나의 가격인지, 메뉴 가격에 수량을 곱한 소계에 해당하는 값인지 혼동되어 명확하게 네이밍했습니다.

import org.springframework.stereotype.Service;

@Service
public class MenuDisplayPolicy {
Copy link
Author

Choose a reason for hiding this comment

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

이 때는 별도의 클래스로 분리했지만 실제로 Menu aggrigate만을 사용하므로 Menu 내부에 두어도 문제가 없을 것 같다는 생각을 했습니다. 별도의 클래스로 분리하는 것과 내부에 두는 것 서로 장단점이 있는 것 같습니다.

Copy link

Choose a reason for hiding this comment

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

현재 구현해주신 코드는 Menu Aggregate만을 사용하게 되지만, 가격 변경시 메뉴에 속한 상품의 합보다 적거나 같아야한다.와 같은 정책이 지켜지지 않을것 같아요.
메뉴가 가지고 있는 가격이 현시점에 상품 가격이라고 확신할수 없는 구조 입니다.

@@ -19,28 +19,28 @@ public class Menu {

private final List<MenuProduct> menuProducts;

private Boolean isVisible;
private Boolean displayed;
Copy link
Author

@chr0m3 chr0m3 Oct 24, 2022

Choose a reason for hiding this comment

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

굳이 isVisible로 바꿔야만 하는 이유가 없어서 불필요한 데이터베이스 변경을 만들지 않기 위해 displayed를 그대로 사용하기로 했습니다.

@@ -6,7 +6,6 @@
import kitchenpos.common.name.Name;
import kitchenpos.common.price.Price;
import kitchenpos.menu.tobe.domain.service.MenuDisplayPolicy;
import kitchenpos.menu.tobe.domain.vo.MenuGroup;
Copy link
Author

Choose a reason for hiding this comment

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

구현을 하면서 MenuGroupid가 필요하다는 것을 알게 되었고, 그렇게 되면 MenuGroup entity와 vo의 차이가 없어져 entity를 그대로 사용하기로 했습니다.
다만 추후에 MenuGroup의 이름을 변경할 수 있게 되거나 하는 요구사항이 생긴다면 MenuGroup entity는 더이상 불변이 아니게 될 것이므로 이 때는 다시 불변인 VO를 추가하게 될 수도 있을 것 같습니다.

@Component
public static class MenuProductToMenuProductEntityConverter {

public MenuProductEntity convert(final MenuProduct menuProduct, final Menu menu) {
Copy link
Author

Choose a reason for hiding this comment

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

기존의 Converter 인터페이스로 커버가 안되는 케이스가 발생해버렸습니다.

@@ -7,9 +7,9 @@

public interface MenuGroupRepository {

MenuGroup save(MenuGroup menuGroup);
MenuGroup save(final MenuGroup menuGroup);
Copy link
Author

Choose a reason for hiding this comment

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

코틀린이 그립네요..

Copy link

Choose a reason for hiding this comment

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

동감합니다 😢

this.nameFactory = nameFactory;
}

public Menu convert(
Copy link
Author

Choose a reason for hiding this comment

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

여기서도 기존의 Converter 인터페이스로 커버되지 않는 케이스가 발생했습니다.

@@ -34,5 +37,11 @@ public MenuProduct convert(final MenuProductEntity menuProductEntity) {
new MenuProductQuantity(menuProductEntity.quantity)
);
}

public List<MenuProduct> convert(final Collection<MenuProductEntity> menuProductEntities) {
Copy link
Author

Choose a reason for hiding this comment

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

데이터베이스에서 조회하는 경우 Collection에 포함된 엔티티를 모두 변환해야 할 필요가 있어 추가했습니다.

final List<MenuProductEntity> menuProducts = this.menuProductToMenuProductEntityConverter.convert(
menu
);
if (menuEntity.id != null) {
Copy link
Author

Choose a reason for hiding this comment

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

식별자로 사용할 수 있는 seq가 있기 때문에 더 나은 방법으로도 구현이 가능하겠지만 일단은 @ElementCollection처럼 모두 삭제하고 새로 만드는 방식을 택했습니다.

}

@Override
public List<Menu> findAll() {
Copy link
Author

Choose a reason for hiding this comment

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

현재 구현은 조회해온 menu의 수 만큼 추가로 쿼리하게 되므로 n+1 쿼리 문제가 있긴 합니다.

@@ -63,7 +63,7 @@ public void hide() {

public void setPrice(final Price price) {
if (!MenuDisplayPolicy.isDisplayable(this)) {
this.hide();
throw new IllegalArgumentException("메뉴 노출 정책에 따라 가격을 변경할 수 없습니다");
Copy link
Author

@chr0m3 chr0m3 Oct 24, 2022

Choose a reason for hiding this comment

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

이전 과제에서 모델링할 당시 가격 변경시 정책을 위반하게 되면 메뉴를 숨기도록 해서 이를 따라 구현했는데, 정책을 위반하도록 가격을 변경할 수는 없는 것이 맞는 것으로 판단되어(기존 구현이 그렇게 되어 있으므로) 그렇게 변경했습니다.

@@ -21,7 +22,7 @@ public MenuGroupRestController(final MenuGroupService menuGroupService) {
}

@PostMapping
public ResponseEntity<MenuGroup> create(@RequestBody final MenuGroup request) {
public ResponseEntity<MenuGroup> create(@RequestBody final CreateMenuGroupCommand request) {
Copy link
Author

Choose a reason for hiding this comment

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

도메인 모델을 그대로 받는 대신 요청하는 행위에 맞는 DTO를 도입해야 할 필요성이 느껴져 DTO를 추가했습니다.

Copy link

Choose a reason for hiding this comment

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

DTO 도입 좋습니다 ㅎㅎ 😄

throw new IllegalArgumentException();
}
public MenuGroup create(final CreateMenuGroupCommand request) {
final Name name = this.nameFactory.create(request.name);
Copy link
Author

Choose a reason for hiding this comment

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

이제 DTO를 받은 뒤 제대로 NameFactory를 사용해 Name을 생성하게 됩니다.

@@ -36,6 +36,9 @@ public Menu(
this.price = price;
this.menuGroup = menuGroup;
this.menuProducts = menuProducts;
if (!MenuDisplayPolicy.isDisplayable(this)) {
Copy link
Author

Choose a reason for hiding this comment

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

테스트코드를 보니 메뉴 노출 정책에 부합하지 않으면 메뉴 생성 자체가 불가능해야 하는 것으로 보여 Menu의 생성 시점에 이를 검사하도록 했습니다.

Copy link

Choose a reason for hiding this comment

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

MenuDisplayPolicy를 static 메소드의 호출로 검증하는것과 파라미터 인자로 받아 검증하는것은 어떤 차이가 있을까요 ?

@@ -172,6 +173,7 @@ void display() {
assertThat(actual.displayed()).isTrue();
}

@Disabled
Copy link
Author

Choose a reason for hiding this comment

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

그런데 이 경우 메뉴의 가격이 상품 가격의 합보다 높으면 메뉴 생성 자체가 불가능하므로 일부 케이스에 대한 테스트가 불가능해져서 어떻게 해결하면 좋을지 고민입니다.

throw new IllegalArgumentException();
}
final List<MenuProduct> menuProducts = menuProductRequests.stream()
Copy link
Author

Choose a reason for hiding this comment

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

기존 구현에는 command에 포함된 가격(아마 클라이언트에서 요청한 가격)을 그대로 사용한다는 문제점이 있었습니다.
생성 시점의 상품 가격을 직접 조회하여 반영하도록 개선했습니다.

@@ -12,10 +11,8 @@ private MenuGroupEntityConverter() {
}

@Component
public static class MenuGroupToMenuGroupEntityConverter
implements Converter<MenuGroup, MenuGroupEntity> {
public static class MenuGroupToMenuGroupEntityConverter {
Copy link
Author

Choose a reason for hiding this comment

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

😢

Copy link

@sah3122 sah3122 left a comment

Choose a reason for hiding this comment

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

메뉴 리팩터링 구현 잘해주셨습니다 👍
몇가지 코멘트 남겨두었는데 확인 부탁드려요 !
추가로 남아있는 미션을 조급하게 진행하지 않아도 됩니다 ㅎㅎ
강의 기간이 끝나도 리뷰는 진행할수 있습니다 😄

@@ -21,7 +22,7 @@ public MenuGroupRestController(final MenuGroupService menuGroupService) {
}

@PostMapping
public ResponseEntity<MenuGroup> create(@RequestBody final MenuGroup request) {
public ResponseEntity<MenuGroup> create(@RequestBody final CreateMenuGroupCommand request) {
Copy link

Choose a reason for hiding this comment

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

DTO 도입 좋습니다 ㅎㅎ 😄

if (Objects.isNull(menuProductRequests) || menuProductRequests.isEmpty()) {
throw new IllegalArgumentException();
}
final List<Product> products = productRepository.findAllByIdIn(
final Map<UUID, Price> productPrices = productRepository.findAllByIdIn(
Copy link

Choose a reason for hiding this comment

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

Menu Application Layer에서 Product Context와의 결합을 제거할수 있는 방법을 고민해보세요 😄

final MenuGroup menuGroup = menuGroupRepository.findById(request.getMenuGroupId())
public Menu create(final CreateMenuCommand command) {
final Price price = new Price(command.price);
final MenuGroup menuGroup = menuGroupRepository.findById(command.menuGroupId)
Copy link

Choose a reason for hiding this comment

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

동일한 Context에서 Root Aggregate간 결합을 허용할건지, 결합을 제거할건지 고민해보시는것도 권장 드립니다 😄

@@ -7,9 +7,9 @@

public interface MenuGroupRepository {

MenuGroup save(MenuGroup menuGroup);
MenuGroup save(final MenuGroup menuGroup);
Copy link

Choose a reason for hiding this comment

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

동감합니다 😢

import org.springframework.stereotype.Service;

@Service
public class MenuDisplayPolicy {
Copy link

Choose a reason for hiding this comment

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

현재 구현해주신 코드는 Menu Aggregate만을 사용하게 되지만, 가격 변경시 메뉴에 속한 상품의 합보다 적거나 같아야한다.와 같은 정책이 지켜지지 않을것 같아요.
메뉴가 가지고 있는 가격이 현시점에 상품 가격이라고 확신할수 없는 구조 입니다.

this.displayed = false;
}

public void setPrice(final Price price) {
Copy link

Choose a reason for hiding this comment

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

setter 네이밍을 사용하기 보다는 의미있는 네이밍을 고민해보세요 😄

@@ -36,6 +36,9 @@ public Menu(
this.price = price;
this.menuGroup = menuGroup;
this.menuProducts = menuProducts;
if (!MenuDisplayPolicy.isDisplayable(this)) {
Copy link

Choose a reason for hiding this comment

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

MenuDisplayPolicy를 static 메소드의 호출로 검증하는것과 파라미터 인자로 받아 검증하는것은 어떤 차이가 있을까요 ?

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

public class MenuEntityConverter {
Copy link

Choose a reason for hiding this comment

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

entity domain 간 분리를 위한 converter 도입 좋습니다 ㅎㅎ


public MenuEntity convert(final Menu menu) {
final MenuEntity menuEntity = new MenuEntity();
menuEntity.id = menu.id;
Copy link

Choose a reason for hiding this comment

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

public 인자로 정의한 이유가 있을까요 ?

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