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

리뷰: 저도 이 부분을 예외로 만들까 고민을 많이 했었는데, 예외는 예외로만 사용을 해야한다고 생각을 해서 이렇게 구현하지 않았는데 이렇게 보니 예외라고 생각할 수도 있을 것 같아요. 확실히 예외로 구현하는게 깔끔해 보이네요!