@@ -0,0 +1,73 @@
+package store.inventory.domain;
+
+import java.time.LocalDate;
+import java.util.Optional;
+import store.exception.AdditionalBenefitException;
+import store.exception.PromotionNotAvailableException;
+
+public class PromotionHandler {
+
+    private static final int ZERO = 0;
+
+    private final Stock stock;
+    private Optional<Promotion> promotion;
+
+    public PromotionHandler(final Promotion promotion, final Stock stock) {
+        this.promotion = Optional.ofNullable(promotion);
+        this.stock = stock;
+    }
+
+    public boolean isApplicable(final int quantity, final LocalDate today) {
+        return promotion.isPresent() && promotion.get().isApplicable(quantity, today);
+    }
+
+    public int calculateFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateFreeQuantity(quantity, stock.getPromotionStock())).orElse(0);
+    }
+
+    public void applyDiscount(final int quantity, String productName) {
+        final int promotionQuantity = calculatePromotionStock(quantity);
+        final int nonPromotionQuantity = quantity - promotionQuantity;
+
+        stock.drainDuringPromotionPeriod(promotionQuantity);
+        if (nonPromotionQuantity > ZERO) {
+            throw new PromotionNotAvailableException(productName, nonPromotionQuantity);
+        }
+    }
+
+    public void checkAdditionalBenefitEligibility(final int quantity, final String productName) {
+        final int additionalEligibleQuantity = calculateRemainingFreeQuantity(quantity);
+        if (isEligibleForAdditionalBenefit(additionalEligibleQuantity, quantity)) {
+            throw new AdditionalBenefitException(productName, additionalEligibleQuantity);
+        }
+    }
+
+    public void changePromotion(final Promotion promotion) {
+        this.promotion = Optional.ofNullable(promotion);
+    }
+
+    public String getPromotionName() {
+        return promotion.map(Promotion::getName).orElse("");
+    }
+
+    private int calculateRemainingFreeQuantity(final int quantity) {
+        return promotion.map(promo -> promo.calculateRemainingFreeQuantity(quantity)).orElse(ZERO);
+    }
+
+    private boolean isEligibleForAdditionalBenefit(final int additionalEligibleQuantity, final int quantity) {
+        final int availablePromotionStock = calculatePromotionStock(quantity);
+        final int totalRequiredStock = calculateTotalRequiredStock(additionalEligibleQuantity, availablePromotionStock);
+
+        return additionalEligibleQuantity > ZERO && stock.getPromotionStock() >= totalRequiredStock;
+    }
+
+    private int calculatePromotionStock(final int quantity) {
+        return promotion.map(promo -> promo.calculateAvailablePromotionStock(quantity, stock.getPromotionStock()))
+                .orElse(ZERO);
+    }
+
+    private int calculateTotalRequiredStock(final int additionalEligibleQuantity, final int availablePromotionStock) {
+        final int promotionSetSize = promotion.get().getPromotionSetSize();
+        return availablePromotionStock + additionalEligibleQuantity * promotionSetSize;
+    }
+}

리뷰: 0과 같이 비즈니스적으로 의미 없는 상수는 그냥 해당 클래스 내에서만 가지고 있도록 하는 것이 좋다고 생각했습니다. 
 그래도 자주 쓰는 것들은 클래스로 분리할까라는 생각도 잠깐 스쳐지나갔었는데 개인적으로 해당 주차 과제가 시간적으로 부족했어서 의미 없는 수들을 위해 따로 클래스까지 분리하고 작성할 시간 및 비용적 측면에서 ZERO의 재사용성을 최대로 끌어올릴 필요 없다고 결정했었습니다.