@@ -0,0 +1,98 @@
+package christmas.service;
+
+import static christmas.constants.event.EventRule.MAX_MENU_AMOUNT;
+import static christmas.constants.menu.MenuType.DRINKS;
+import static christmas.exception.ErrorCode.INVALID_MENU_ORDER;
+
+import christmas.constants.menu.Menu;
+import christmas.constants.menu.MenuType;
+import christmas.dto.SingleMenu;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class MenuService {
+    private final Map<Menu, Integer> menuScript;
+
+    public MenuService() {
+        this.menuScript = new EnumMap<>(Menu.class);
+    }
+
+    public void order(List<SingleMenu> singleMenus) {
+        validate(singleMenus);
+        singleMenus.forEach(singleOrder -> {
+            Menu menu = Menu.from(singleOrder.menu());
+            menuScript.put(menu, singleOrder.amount());
+        });
+    }
+
+    public int getOrderPrice() {
+        return menuScript.keySet()
+                .stream()
+                .mapToInt(menu -> menu.getPrice() * menuScript.get(menu))
+                .sum();
+    }
+
+    private void validate(List<SingleMenu> singleMenus) {
+        validateDuplicate(singleMenus);
+        validatePerMenuAmount(singleMenus);
+        validateOnlyDrink(singleMenus);
+        validateTotalMenuAmount(singleMenus);
+    }
+
+    private void validateDuplicate(List<SingleMenu> singleMenus) {
+        int count = (int) getMenuStream(singleMenus)
+                .distinct()
+                .count();
+
+        if (count != singleMenus.size()) {
+            throw new IllegalArgumentException(INVALID_MENU_ORDER.getMessage());
+        }
+    }
+
+    private void validateOnlyDrink(List<SingleMenu> singleMenus) {
+        int drinks = (int) getMenuStream(singleMenus)
+                .filter(menu -> menu.getMenuType() == DRINKS)
+                .count();
+
+        if (drinks > 0 && drinks == singleMenus.size()) {
+            throw new IllegalArgumentException(INVALID_MENU_ORDER.getMessage());
+        }
+    }
+
+    private Stream<Menu> getMenuStream(List<SingleMenu> singleMenus) {
+        return singleMenus.stream()
+                .map(singleOrder -> Menu.from(singleOrder.menu()));
+    }
+
+    private void validatePerMenuAmount(List<SingleMenu> singleMenus) {
+        boolean present = singleMenus.stream()
+                .anyMatch(singleOrder -> singleOrder.amount() < 1);
+        if (present) {
+            throw new IllegalArgumentException(INVALID_MENU_ORDER.getMessage());
+        }
+    }
+
+    private void validateTotalMenuAmount(List<SingleMenu> singleMenus) {
+        int totalAmount = singleMenus.stream()
+                .mapToInt(SingleMenu::amount)
+                .sum();
+        if (totalAmount >= MAX_MENU_AMOUNT.getValue()) {
+            throw new IllegalArgumentException(INVALID_MENU_ORDER.getMessage());
+        }
+    }
+
+    public int getAmountByMenu(MenuType menuType) {
+        return menuScript.keySet()
+                .stream()
+                .filter(target -> target.getMenuType() == menuType)
+                .mapToInt(menuScript::get)
+                .sum();
+    }
+
+    public Map<Menu, Integer> getMenuScript() {
+        return Collections.unmodifiableMap(menuScript);
+    }
+}

리뷰: 일반적으로 validate 검증 테스트는 public 메서드를 테스트하여 진행하는데, 모든 private 메서드가 동일한 예외를 던지고 있기 때문에 메시지 내용을 확인하는 등 세부적인 검증 절차가 필요하다 생각합니다. `IllegalArgumentException`이 정확히 어느 지점에서 발생했는지 알기 어려우니까요!

하지만 이번 미션은 `에러문 내용이 2가지`로 정해져 있기 때문에, 이전 미션같이 `에러문 내용으로 세부적인 테스트`를 하기가 어려웠습니다. 그래서 저는 세부 검증을 위해 **커스텀 예외**를 도입하여 이 문제를 해결했었는데, 참고하시면 좋을 것 같습니다 :)

조금 오버엔지니어링 같은 생각도 들어 올바른 접근 방식인지는 잘 모르겠네요 ㅎㅎ..