우아한테크코스 프리코스 미션을 해결하면서 도메인 주도 설계를 도전해 봤다.
어떤 문제상황이 있었고, 어떤 해결 방법을 사용했는지 정리했다.
1. 도메인 설계하기
프리코스의 마지막 미션은 편의점을 설계하고 구현하는 것이였다.
https://github.com/woowacourse-precourse/java-convenience-store-7
GitHub - woowacourse-precourse/java-convenience-store-7
Contribute to woowacourse-precourse/java-convenience-store-7 development by creating an account on GitHub.
github.com
미션의 요구사항을 한 줄로 요약하자면,
상품 정보를 바탕으로 각종 할인 정책이 적용된, 구매 시뮬의 결과를 출력하라.
였다.
가장 먼저 비즈니스 로직의 중심이 되는 도메인을 설계하고자 하였다.
이 미션에는 일반 상품, 프로모션 상품이란 두 종류가 존재한다.
각 상품의 종류 마다 해당하는 비즈니스 로직이 존재한다.
그래서 일반 상품, 프로모션 상품 이란 두개의 도메인을 만들기로 하였다.
public final class Item {
private final String id;
private final String name;
private final int price;
private final long stockQuantity;
public Item(String id, String name, int price, long stockQuantity) {
this.id = id;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
}
...
}
이건 Item이라는 일반 상품에 해당하는 도메인이다.
public final class PromotionItem {
private final String promotionItemId;
private final Item item;
private final PromotionRule promotionRule;
private final long promotionStockQuantity;
public PromotionItem(String promotionItemId, Item item, PromotionRule promotionRule, long promotionStockQuantity) {
this.promotionItemId = promotionItemId;
this.item = item;
this.promotionRule = promotionRule;
this.promotionStockQuantity = promotionStockQuantity;
}
...
}
이건 PromotionItem이라는 프로모션 상품에 해당하는 도메인이다.
일단, 가장 최소 단위가 되는 도메인에는 최대한 적은 필드만 들어가도록 의도했다.
특이하게, PromotionItem이 Item도메인에 의존하도록 설계해 보았다.
설계 의도는 Item이 존재 해야지 PromotionItem이 존재한다고 판단했고, PromotionItem에서 Item지식을 갖고 있으면 구매 수량 계산 등 비즈니스 로직을 추가하기 용이할 것 이라고 생각했기 때문이다.
다만, 이렇게 도메인을 설계하니 문제가 있었다. 가장 주요한 문제 몇가지를 설명하자면,,
1. Item과 PromotionItem은 서로 독립적인 관계이다.
2. 상품을 한번에 관리하는 root domain이 없어, 주문이 들어온 특정 상품을 결제 처리하기 복잡하다.
2. 종속성 분리하기
그래서 이들을 한번에 해결하기 위해 PromotionItem의 Item에 대한 종속성을 제거했다.
public class PromotionItem {
private final String promotionItemId;
private final String name;
private final PromotionRule promotionRule;
private final long stockQuantity;
private PromotionItem(String promotionItemId, String name, PromotionRule promotionRule, long stockQuantity) {
this.promotionItemId = promotionItemId;
this.name = name;
this.promotionRule = promotionRule;
this.stockQuantity = stockQuantity;
}
...
public PromotionItem purchase(long quantity) {
if (stockQuantity < quantity) {
throw new IllegalArgumentException();
}
return new PromotionItem(promotionItemId, name, promotionRule, stockQuantity - quantity);
}
}
Item에 대한 종속을 제거하여, promotion상품만을 구매하기 위한 로직을 추가할 수 있었다.
또한, Item과 PromotionItem을 한번에 관리하여 중앙에서 전체적 비즈니스 로직을 실행하는 도메인을 만들었다.
public class ItemInfo {
private final Item item;
private final PromotionItem promotionItem;
private final boolean isPromotion;
private ItemInfo(Item item, PromotionItem promotionItem, boolean isPromotion) {
this.item = item;
this.promotionItem = promotionItem;
this.isPromotion = isPromotion;
}
...
public ItemInfo purchase(long amount) {
if (!canPurchase(amount)) {
throw new DomainArgumentException(EXCEED_AMOUNT);
}
if (isPromotion) {
return handlePromotionPurchase(amount);
} else {
return purchaseGeneralItem(amount);
}
}
ItemInfo라는 상태를 갖고 있는 도메인에, 구매 수량에 따라 Item과 PromotionItem의 구매 비율을 조정하는 key-logic을 추가하였다.
Item과 PromotionItem이라는 두개의 최상위 도메인의 종속을 제거하고, 두 도메인을 한번에 접근해 로직을 수행할 수 있는 루트 도메인을 구현했다.
이를 통해 domain logic을 domain내에 구현할 수 있어 외부(구매 로직)에서 도메인 객체의 로직에 대한 지식 없이 외부의 로직을 수행했다.
-> 도메인 구조의 수정 방안을 더욱 생각해 보자면, PromotionItem과 Item의 가격 정보까지 서로 독립적으로 관리할 수 있도록 하여 완전한 독립을 구현하면 요구사항이 변해도 쉽게 대응할 수 있을 것 같다. (완전히 다른 개체로 인식하는 것이다)
이번 미션을 하면서 처음으로 혼자 DDD방식으로 설계해 보았다.
아직 완벽히 DDD를 수행할 수 있다고 생각하지는 않지만, 더욱 잘하고 싶어서
개발을 하며 고려한 점들을 쭉쭉 나열해보면서 정리하고자 한다.
'Backend' 카테고리의 다른 글
AWS lambda python 패키징 오류 타파 (0) | 2024.12.07 |
---|---|
전략 패턴을 사용하여 test에 적용하기 (0) | 2024.11.27 |
Interface를 테스트 하라? (0) | 2024.11.25 |
[MVC] Domain과 Model의 차이에 대하여 (1) | 2024.11.13 |
방어적 프로그래밍 (0) | 2024.11.06 |