@@ -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;
+    }
+}

리뷰: > 생성자를 호출할 때 파싱을하고 넘겨주는 쪽

이 맞습니다 😄 제가 표현이 모호했네요

예를 들어 Promotion 을 초기화하는, 즉 생성하는 방법이 현재는 List<String> 을 통해서 생성하는 한 가지 방법만이 있지만, 추가 요구 사항이(기능의 추가, 수정 삭제) 생겨서 Promotion 을 다르게 초기화 해야 하는 경우 또 다른 생성자가 정의되어야 할 것 같아요! 이 때 프로그래머는 선택을 해야 하는데

1. private 생성자를 정의하고, public static 메서드를(정적 팩토리) 통해 생성하도록 한다.
2. public 생성자를 두고, 호출하는 측에서 데이터를 가공(parse)한 후 생성자를 호출하게 한다

두 가지 방법이 있을 것 같아요.

1,2번 모두 상황에 따라 어느 쪽을 선호 해야 할 지가 다르겠지만, 어플리케이션에 추가 요구사항이 생기고, 그에 따라 기능을 확장/수정/삭제 해야 하는 상황이 생기는(이 부분이 제가 말씀드린 "운영코드에서의 확장" 입니다) 것을 고려 했을 때 저는 가능한 Promotion 클래스의 필수 필드(final field)를 생성자로 두는 것을 선호하고 있습니다. 테스트 코드의 경우에도, 테스트에서 이 값, 저 값 넣어보며 테스트하기가 좀 더 편하고, 직관적이라고 생각해요 😄 

더불어 1번 정적 팩토리 메서드를 사용하는 경우 "이 데이터는 이 방식으로 초기화되어야 해" 를 명시하는 효과가 있는 것 같은데, 해당 부분에서 Promotion 도메인이이 인프라적인 요구사항 (마크다운 파일을 List<String> 데이터를 이용하라는 요구사항)에 의해서만 초기화 되는 사실을 도메인이 알고 있는 형태, 즉 의존하고 있는 형태가 되는 것  같습니다. 물론, 이 방법이 항상 나쁜 것은 아니며 편의에 의해서 얼마든지 사용되어도 좋다고 생각은 합니다만! 더 중요한 모듈(도메인) 이 덜 중요한 모듈(인프라)에 의존하지 않게 만들면 좀 더 유연한 확장이 가능 할 것 습니다.