@@ -0,0 +1,107 @@
+package store.model.domain;
+
+import camp.nextstep.edu.missionutils.DateTimes;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import store.dto.PurchaseResponse;
+
+public class Promotion {
+
+    private static final String DATE_FORMAT = "yyyy-MM-dd";
+    public static final int NO_PARTIAL = 0;
+
+    private final int buy;
+    private final int get;
+    private final List<String> promotionInformation;
+
+    public Promotion(List<String> promotionInformation) {
+        this.promotionInformation = promotionInformation;
+        this.buy = Integer.parseInt(promotionInformation.get(1));
+        this.get = Integer.parseInt(promotionInformation.get(2));
+    }
+
+    public boolean isOnPromotion() {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+        String startDateInformation = promotionInformation.get(3);
+        LocalDateTime startDate = LocalDate.parse(startDateInformation, formatter).atStartOfDay();
+        String endDateInformation = promotionInformation.get(4);
+        LocalDateTime endDate = LocalDate.parse(endDateInformation, formatter).atStartOfDay();
+        LocalDateTime today = DateTimes.now();
+        return (today.isAfter(startDate) && today.isBefore(endDate));
+    }
+
+    public PurchaseResponse discount(int requestQuantity, int stockQuantity) {
+        int promotionCount = requestQuantity / (buy + get);
+        int restCount = requestQuantity % (buy + get);
+        return getResponse(requestQuantity, stockQuantity, promotionCount, restCount);
+    }
+
+    private PurchaseResponse getResponse(int requestQuantity, int stockQuantity, int promotionCount, int restCount) {
+        if (isInsufficientStock(requestQuantity, stockQuantity)) {
+            return responseInsufficientStock(requestQuantity, stockQuantity);
+        }
+        if (isSuccessPromotionPurchase(restCount)) {
+            return responseSuccessPromotionPurchase(promotionCount, restCount);
+        }
+        if (isNeedFreeRemind(restCount)) {
+            return responseInsufficientFreeStockOrFreeRemind(requestQuantity, stockQuantity, promotionCount, restCount);
+        }
+        return responsePartialAvailable(promotionCount, restCount);
+    }
+
+    private PurchaseResponse responsePartialAvailable(int promotionCount, int restCount) {
+        return new PurchaseResponse(PurchaseResponseCode.PROMOTION_PARTIAL_UNAVAILABLE, promotionCount, restCount);
+    }
+
+    private boolean isInsufficientStock(int requestQuantity, int stockQuantity) {
+        return requestQuantity > stockQuantity;
+    }
+
+    private PurchaseResponse responseInsufficientStock(int requestQuantity, int stockQuantity) {
+        int promotionCount = stockQuantity / (buy + get);
+        int restCount = stockQuantity % (buy + get) + (requestQuantity - stockQuantity);
+        return new PurchaseResponse(PurchaseResponseCode.PROMOTION_PARTIAL_UNAVAILABLE, promotionCount, restCount);
+    }
+
+    private boolean isSuccessPromotionPurchase(int restCount) {
+        return restCount == NO_PARTIAL;
+    }
+
+    private PurchaseResponse responseSuccessPromotionPurchase(int promotionCount, int restCount) {
+        return new PurchaseResponse(PurchaseResponseCode.PURCHASE_SUCCESS, promotionCount, restCount);
+    }
+
+    private boolean isNeedFreeRemind(int restCount) {
+        return restCount == buy;
+    }
+
+    private boolean isInsufficientFreeStock(int requestQuantity, int stockQuantity) {
+        return requestQuantity == stockQuantity;
+    }
+
+    private PurchaseResponse responseInsufficientFreeStockOrFreeRemind(int requestQuantity, int stockQuantity,
+                                                                       int promotionCount, int restCount) {
+        if (isInsufficientFreeStock(requestQuantity, stockQuantity)) {
+            return responseInsufficientFreeStock(promotionCount, restCount);
+        }
+        return responseFreeRemind(promotionCount, restCount);
+    }
+
+    private PurchaseResponse responseInsufficientFreeStock(int promotionCount, int restCount) {
+        return new PurchaseResponse(PurchaseResponseCode.PROMOTION_PARTIAL_UNAVAILABLE, promotionCount, restCount);
+    }
+
+    private PurchaseResponse responseFreeRemind(int promotionCount, int restCount) {
+        return new PurchaseResponse(PurchaseResponseCode.FREE_PRODUCT_REMIND, promotionCount, restCount);
+    }
+
+    public String getPromotionName() {
+        return promotionInformation.getFirst();
+    }
+
+    public int getAppliedQuantity() {
+        return buy + get;
+    }
+}

리뷰: 사실, 위의 '인프라'와 '도메인' 중 더 중요한 것이 덜 중요한 것을 의존하지 않게 하자! 는 말과도 같은 맥락인데요! 해당 Dto 는 도메인보다 덜 중요하니, Dto 의 변경이 도메인에 영향을 미치게 하면 어떨까? 하는 이야기였답니다.

사실 작성 해 주신 방식은 아주 깔끔하게 잘 설계되어 있지만, 해당 방식(DTO를 만들어서 리턴하는)은 service에서 하는 일에 가깝지 않나? 싶은 의문도 있습니다. 도메인은 더 작고, 중요한 데이터 조작과 관리를 하고 서비스에서 해당 데이터들을 모아서 리턴하면 어떨까 해요. 

도메인의 메서드 하나가 하나의 데이터만 조작하고, 리턴하고, 해당 메서드들을 public 으로 만들어서 외부와 상호작용하게 만드는 것이죠. 외부(아마도 서비스)에서는 이 public 메서드를 조합해서 커다란 데이터 덩어리(DTO라고 부르는)를 만들고, 이를 컨트롤러 측에 넘겨서 최종적으로는 view 에 뿌려주는 식은 어떨까요? 😄