ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,50 @@
+package menu.domain;
+
+import static menu.exception.ErrorMessage.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CantEatingMenus {
+    private static final int MAXIMUM_MENUS_LENGTH = 2;
+
+    private List<Menu> cantEatingMenus = new ArrayList<>();
+
+    public CantEatingMenus(List<String> cantEatingMenus) {
+        validateMenusLength(cantEatingMenus);
+        validateIsMenuExist(cantEatingMenus);
+        this.cantEatingMenus = cantEatingMenus(cantEatingMenus);
+    }
+
+    private void validateMenusLength(List<String> cantEatingMenus) {
+        if(!(cantEatingMenus.size() <= MAXIMUM_MENUS_LENGTH)) {
+            throw new IllegalArgumentException(INVALID_MENU_COUNT.getMessage());
+        }
+    }
+
+    private void validateIsMenuExist(List<String> cantEatingMenus) {
+       if(!isMenuExist(cantEatingMenus)) {
+           throw new IllegalArgumentException(NOT_EXIST_MENU.getMessage());
+       }
+    }
+
+    public List<Menu> getCantEatingMenus() {
+        return cantEatingMenus;
+    }
+
+    private List<Menu> cantEatingMenus(List<String> cantEatingMenus) {
+        return cantEatingMenus.stream()
+            .map(Menu::new)
+            .collect(Collectors.toList());
+    }
+
+    private boolean isMenuExist(List<String> cantEatingMenus) {
+        List<String> allMenus = Arrays.stream(Category.values())
+            .flatMap(category -> category.getMenus().stream())
+            .collect(Collectors.toList());
+
+        return cantEatingMenus.stream().anyMatch(allMenus::contains);
+    }
+}

리뷰: 방어적 복사가 필요해 보입니다 🤔
@@ -0,0 +1,35 @@
+package menu.domain;
+
+import static menu.exception.ErrorMessage.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class Coaches {
+    private static final int MINIMUM_COACH_COUNT = 2;
+    private static final int MAXIMUM_COACH_COUNT = 5;
+
+    private List<Coach> coaches = new ArrayList<>();
+
+    public Coaches(List<String> coachNames) {
+        validateCoachesCount(coachNames);
+        this.coaches = getCoaches(coachNames);
+    }
+
+    private void validateCoachesCount(List<String> coachNames) {
+        if(!(coachNames.size() >= MINIMUM_COACH_COUNT && coachNames.size() <= MAXIMUM_COACH_COUNT)) {
+            throw new IllegalArgumentException(INVALID_COACHES_COUNT.getMessage());
+        }
+    }
+
+    private List<Coach> getCoaches(List<String> coachNames) {
+        return coachNames.stream()
+            .map(Coach::new)
+            .collect(Collectors.toList());
+    }
+
+    public String getCoachName(int index) {
+        return coaches.get(index).getCoachName();
+    }
+}

리뷰: 전체적으로 짧은 시간안에 검증 로직을 잘 작성하신 것 같아요 👍
@@ -0,0 +1,43 @@
+package menu.domain;
+
+import java.util.Arrays;
+import java.util.List;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+public enum Category {
+    JAPANESE("일식", Arrays.asList("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼")),
+    KOREAN("한식", Arrays.asList("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음")),
+    CHINESE("중식", Arrays.asList("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채")),
+    ASIAN("아시안", Arrays.asList("팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜")),
+    WESTERN("양식", Arrays.asList("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"));;
+
+    private final String categoryName;
+    private final List<String> menus;
+
+    Category(String categoryName, List<String> menus) {
+        this.categoryName = categoryName;
+        this.menus = menus;
+    }
+
+    public String getCategoryName() {
+        return categoryName;
+    }
+
+    public List<String> getMenus() {
+        return menus;
+    }
+
+    public static Category randomCategory() {
+        int randomIndex = Randoms.pickNumberInRange(0, 4);
+        return Category.values()[randomIndex];
+    }
+
+    public static List<String> getMenusByCategory(Category category) {
+        return category.menus;
+    }
+
+    public static String getMenu(List<String> menus) {
+        return Randoms.shuffle(menus).get(0);
+    }
+}

리뷰: 문제에서는 난수가 1부터 5였던 것 같은데 이렇게 사용해도 문제가 없었나요?
@@ -0,0 +1,22 @@
+package menu.exception;
+
+public enum ErrorMessage {
+    INVALID_COACH_NAME_LENGTH("코치의 이름은 최소 2글자, 최대 4글자여야 합니다."),
+    INVALID_COACHES_COUNT("코치는 최소 2명, 최대 5명여야 합니다."),
+    INVALID_MENU_COUNT("못 먹는 메뉴는 최소 0개, 최대 2개여야 합니다."),
+    NOT_EXIST_MENU("존재하지 않는 메뉴입니다."),
+    ;
+
+    private static final String PREFIX = "[ERROR] ";
+    private static final String POSTFIX = " 다시 입력해 주세요.";
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage(Object... args) {
+        return String.format(PREFIX + message + POSTFIX, args);
+    }
+}

리뷰: 에러 메세지도 깔끔하게 분리하셨네요 😮
@@ -34,27 +34,35 @@ BUILD SUCCESSFUL in 0s
 
 한 주의 점심 메뉴를 추천해 주는 서비스다.
 
-- 코치들은 월, 화, 수, 목, 금요일에 점심 식사를 같이 한다.
-- 메뉴를 추천하는 과정은 아래와 같이 이뤄진다.
-  1. 월요일에 추천할 카테고리를 무작위로 정한다.
-  2. 각 코치가 월요일에 먹을 메뉴를 추천한다.
-  3. 화, 수, 목, 금요일에 대해 i, ii 과정을 반복한다.
-- 코치의 이름은 최소 2글자, 최대 4글자이다.
-- 코치는 최소 2명, 최대 5명까지 식사를 함께 한다.
-- 각 코치는 최소 0개, 최대 2개의 못 먹는 메뉴가 있다. (`,` 로 구분해서 입력한다.)
-  - 먹지 못하는 메뉴가 없으면 빈 값을 입력한다.
-  - 추천을 못하는 경우는 발생하지 않으니 고려하지 않아도 된다.
-- 한 주에 같은 카테고리는 최대 2회까지만 고를 수 있다.
-- 각 코치에게 한 주에 중복되지 않는 메뉴를 추천해야 한다.
+- [x] 코치들은 월, 화, 수, 목, 금요일에 점심 식사를 같이 한다.
+- [x] 메뉴를 추천하는 과정은 아래와 같이 이뤄진다.
+  - [x] 월요일에 추천할 카테고리를 무작위로 정한다.
+  - [x] 각 코치가 월요일에 먹을 메뉴를 추천한다.
+  - [x] 화, 수, 목, 금요일에 대해 i, ii 과정을 반복한다.
+- [x] 코치의 이름은 최소 2글자, 최대 4글자이다.
+- [x] 코치는 최소 2명, 최대 5명까지 식사를 함께 한다.
+- [x] 각 코치는 최소 0개, 최대 2개의 못 먹는 메뉴가 있다. (`,` 로 구분해서 입력한다.)
+  - [x] 먹지 못하는 메뉴가 없으면 빈 값을 입력한다.
+  - [x] 추천을 못하는 경우는 발생하지 않으니 고려하지 않아도 된다.
+- [x] 한 주에 같은 카테고리는 최대 2회까지만 고를 수 있다.
+- [x] 각 코치에게 한 주에 중복되지 않는 메뉴를 추천해야 한다.
   - 예시)
-    - 구구: 비빔밥, 김치찌개, 쌈밥, 규동, 우동 → 한식을 3회 먹으므로 불가능
-    - 토미: 비빔밥, 비빔밥, 규동, 우동, 볶음면 → 한 코치가 같은 메뉴를 먹으므로 불가능
-    - 제임스: 비빔밥, 김치찌개, 스시, 가츠동, 짜장면 → 매일 다른 메뉴를 먹으므로 가능
-    - 포코: 비빔밥, 김치찌개, 스시, 가츠동, 짜장면 → 제임스와 메뉴가 같지만, 포코는 매번 다른 메뉴를 먹으므로 가능
-- 메뉴 추천을 완료하면 프로그램이 종료된다.
-- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시
+    - [x] 구구: 비빔밥, 김치찌개, 쌈밥, 규동, 우동 → 한식을 3회 먹으므로 불가능
+    - [x] 토미: 비빔밥, 비빔밥, 규동, 우동, 볶음면 → 한 코치가 같은 메뉴를 먹으므로 불가능
+    - [x] 제임스: 비빔밥, 김치찌개, 스시, 가츠동, 짜장면 → 매일 다른 메뉴를 먹으므로 가능
+    - [x] 포코: 비빔밥, 김치찌개, 스시, 가츠동, 짜장면 → 제임스와 메뉴가 같지만, 포코는 매번 다른 메뉴를 먹으므로 가능
+- [x] 메뉴 추천을 완료하면 프로그램이 종료된다.
+- [x] 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시
   받는다.
-  - `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.
+  - [x] `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.
+
+과제 시작 시간: 14:30  <br>
+TDD 종료 : 17:17 (2시간 47분) <br>
+MVP 완성 : 19:13 (TDD + 1시간 56분) <br>
+리팩토링 종료 : 19:29 (MVP 완성 + 16분) <br>
+총 4시간 59분 <br>
+
+아쉬운 점 : 함수형 인터페이스 적용, Service 클래스 도입, 리팩토링
 
 ### 입출력 요구 사항
 

리뷰: PR 설명을 읽어보니 TDD 만족도가 높아보이던데, 지난주에 비해 과정이나 느낌이 달라진 게 있었나요??
@@ -0,0 +1,75 @@
+package menu.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import menu.domain.CantEatingMenus;
+import menu.domain.Category;
+import menu.domain.Coaches;
+import menu.domain.Menu;
+import menu.util.GenerateCategory;
+import menu.util.GenerateMenus;
+import menu.view.InputView;
+import menu.view.OutputView;
+
+public class RecommendController {
+    private Coaches coaches;
+    private CantEatingMenus cantEatingMenus;
+
+    public void start() {
+        OutputView.start();
+        execute();
+        OutputView.finish();
+    }
+
+    private void execute() {
+        List<String> inputCoachNames = getCoachNames();
+        getCantEatingMenus(inputCoachNames);
+        getResult(inputCoachNames);
+    }
+
+    private List<String> getCoachNames() {
+        List<String> inputCoachNames = new ArrayList<>();
+
+        while (true) {
+            try {
+                OutputView.inputCoachNames();
+                inputCoachNames = InputView.inputCoachNames();
+                coaches = new Coaches(inputCoachNames);
+                break;
+            } catch (IllegalArgumentException e) {
+                OutputView.printErrorMessage(e);
+            }
+        }
+        return inputCoachNames;
+    }
+
+    private void getCantEatingMenus(List<String> inputCoachNames) {
+        for (int index = 0; index < inputCoachNames.size(); index++) {
+            while (true) {
+                try {
+                    OutputView.inputCantEatingMenus(coaches.getCoachName(index));
+                    List<String> inputCantEatingMenus = InputView.inputCantEatingMenus();
+                    cantEatingMenus = new CantEatingMenus(inputCantEatingMenus);
+                    break;
+                } catch (IllegalArgumentException e) {
+                    OutputView.printErrorMessage(e);
+                }
+            }
+        }
+    }
+
+    private static List<Category> getCategories() {
+        List<Category> categories = GenerateCategory.generate();
+        OutputView.printDayAndCategory(categories);
+        return categories;
+    }
+
+    private void getResult(List<String> inputCoachNames) {
+        List<Category> categories = getCategories();
+        for (int index = 0; index < inputCoachNames.size(); index++) {
+            List<Menu> menus = GenerateMenus.generate(categories, cantEatingMenus);
+            OutputView.printResult(coaches.getCoachName(index), menus);
+        }
+    }
+}

리뷰: for문을 사용하지 않는 방향은 없었을지 고민해보면 좋을 것 같아요!
@@ -0,0 +1,50 @@
+package menu.domain;
+
+import static menu.exception.ErrorMessage.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CantEatingMenus {
+    private static final int MAXIMUM_MENUS_LENGTH = 2;
+
+    private List<Menu> cantEatingMenus = new ArrayList<>();
+
+    public CantEatingMenus(List<String> cantEatingMenus) {
+        validateMenusLength(cantEatingMenus);
+        validateIsMenuExist(cantEatingMenus);
+        this.cantEatingMenus = cantEatingMenus(cantEatingMenus);
+    }
+
+    private void validateMenusLength(List<String> cantEatingMenus) {
+        if(!(cantEatingMenus.size() <= MAXIMUM_MENUS_LENGTH)) {
+            throw new IllegalArgumentException(INVALID_MENU_COUNT.getMessage());
+        }
+    }
+
+    private void validateIsMenuExist(List<String> cantEatingMenus) {
+       if(!isMenuExist(cantEatingMenus)) {
+           throw new IllegalArgumentException(NOT_EXIST_MENU.getMessage());
+       }
+    }
+
+    public List<Menu> getCantEatingMenus() {
+        return cantEatingMenus;
+    }
+
+    private List<Menu> cantEatingMenus(List<String> cantEatingMenus) {
+        return cantEatingMenus.stream()
+            .map(Menu::new)
+            .collect(Collectors.toList());
+    }
+
+    private boolean isMenuExist(List<String> cantEatingMenus) {
+        List<String> allMenus = Arrays.stream(Category.values())
+            .flatMap(category -> category.getMenus().stream())
+            .collect(Collectors.toList());
+
+        return cantEatingMenus.stream().anyMatch(allMenus::contains);
+    }
+}

리뷰: 필드, 변수, 메서드명이 전부 겹치니 이 클래스의 로직을 이해하기가 힘든 것 같아요 🥲
@@ -0,0 +1,50 @@
+package menu.domain;
+
+import static menu.exception.ErrorMessage.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CantEatingMenus {
+    private static final int MAXIMUM_MENUS_LENGTH = 2;
+
+    private List<Menu> cantEatingMenus = new ArrayList<>();
+
+    public CantEatingMenus(List<String> cantEatingMenus) {
+        validateMenusLength(cantEatingMenus);
+        validateIsMenuExist(cantEatingMenus);
+        this.cantEatingMenus = cantEatingMenus(cantEatingMenus);
+    }
+
+    private void validateMenusLength(List<String> cantEatingMenus) {
+        if(!(cantEatingMenus.size() <= MAXIMUM_MENUS_LENGTH)) {
+            throw new IllegalArgumentException(INVALID_MENU_COUNT.getMessage());
+        }
+    }
+
+    private void validateIsMenuExist(List<String> cantEatingMenus) {
+       if(!isMenuExist(cantEatingMenus)) {
+           throw new IllegalArgumentException(NOT_EXIST_MENU.getMessage());
+       }
+    }
+
+    public List<Menu> getCantEatingMenus() {
+        return cantEatingMenus;
+    }
+
+    private List<Menu> cantEatingMenus(List<String> cantEatingMenus) {
+        return cantEatingMenus.stream()
+            .map(Menu::new)
+            .collect(Collectors.toList());
+    }
+
+    private boolean isMenuExist(List<String> cantEatingMenus) {
+        List<String> allMenus = Arrays.stream(Category.values())
+            .flatMap(category -> category.getMenus().stream())
+            .collect(Collectors.toList());
+
+        return cantEatingMenus.stream().anyMatch(allMenus::contains);
+    }
+}

리뷰: @seongm1n 내부 메서드에서 `List<String>`을 `List<Menu>`로 변환하고 있는데 방어적 복사가 필요하다고 보시나요!?
@@ -0,0 +1,50 @@
+package menu.domain;
+
+import static menu.exception.ErrorMessage.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CantEatingMenus {
+    private static final int MAXIMUM_MENUS_LENGTH = 2;
+
+    private List<Menu> cantEatingMenus = new ArrayList<>();
+
+    public CantEatingMenus(List<String> cantEatingMenus) {
+        validateMenusLength(cantEatingMenus);
+        validateIsMenuExist(cantEatingMenus);
+        this.cantEatingMenus = cantEatingMenus(cantEatingMenus);
+    }
+
+    private void validateMenusLength(List<String> cantEatingMenus) {
+        if(!(cantEatingMenus.size() <= MAXIMUM_MENUS_LENGTH)) {
+            throw new IllegalArgumentException(INVALID_MENU_COUNT.getMessage());
+        }
+    }
+
+    private void validateIsMenuExist(List<String> cantEatingMenus) {
+       if(!isMenuExist(cantEatingMenus)) {
+           throw new IllegalArgumentException(NOT_EXIST_MENU.getMessage());
+       }
+    }
+
+    public List<Menu> getCantEatingMenus() {
+        return cantEatingMenus;
+    }
+
+    private List<Menu> cantEatingMenus(List<String> cantEatingMenus) {
+        return cantEatingMenus.stream()
+            .map(Menu::new)
+            .collect(Collectors.toList());
+    }
+
+    private boolean isMenuExist(List<String> cantEatingMenus) {
+        List<String> allMenus = Arrays.stream(Category.values())
+            .flatMap(category -> category.getMenus().stream())
+            .collect(Collectors.toList());
+
+        return cantEatingMenus.stream().anyMatch(allMenus::contains);
+    }
+}

리뷰: 요청들어온 메뉴명이 실제 메뉴중에 존재하는지 확인하는 메서드인 것 같아요.
이건 이 클래스의 관심사가 아닐 수도 있겠다는 생각이 드는데 어떻게 생각하시나요!?
@@ -0,0 +1,43 @@
+package menu.domain;
+
+import java.util.Arrays;
+import java.util.List;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+public enum Category {
+    JAPANESE("일식", Arrays.asList("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼")),
+    KOREAN("한식", Arrays.asList("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음")),
+    CHINESE("중식", Arrays.asList("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채")),
+    ASIAN("아시안", Arrays.asList("팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜")),
+    WESTERN("양식", Arrays.asList("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"));;
+
+    private final String categoryName;
+    private final List<String> menus;
+
+    Category(String categoryName, List<String> menus) {
+        this.categoryName = categoryName;
+        this.menus = menus;
+    }
+
+    public String getCategoryName() {
+        return categoryName;
+    }
+
+    public List<String> getMenus() {
+        return menus;
+    }
+
+    public static Category randomCategory() {
+        int randomIndex = Randoms.pickNumberInRange(0, 4);
+        return Category.values()[randomIndex];
+    }
+
+    public static List<String> getMenusByCategory(Category category) {
+        return category.menus;
+    }
+
+    public static String getMenu(List<String> menus) {
+        return Randoms.shuffle(menus).get(0);
+    }
+}

리뷰: 이런 방식으로 enum을 관리하는 것도 정말 좋은 것 같네요! 💯
@@ -0,0 +1,43 @@
+package menu.domain;
+
+import java.util.Arrays;
+import java.util.List;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+public enum Category {
+    JAPANESE("일식", Arrays.asList("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼")),
+    KOREAN("한식", Arrays.asList("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음")),
+    CHINESE("중식", Arrays.asList("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채")),
+    ASIAN("아시안", Arrays.asList("팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜")),
+    WESTERN("양식", Arrays.asList("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"));;
+
+    private final String categoryName;
+    private final List<String> menus;
+
+    Category(String categoryName, List<String> menus) {
+        this.categoryName = categoryName;
+        this.menus = menus;
+    }
+
+    public String getCategoryName() {
+        return categoryName;
+    }
+
+    public List<String> getMenus() {
+        return menus;
+    }
+
+    public static Category randomCategory() {
+        int randomIndex = Randoms.pickNumberInRange(0, 4);
+        return Category.values()[randomIndex];
+    }
+
+    public static List<String> getMenusByCategory(Category category) {
+        return category.menus;
+    }
+
+    public static String getMenu(List<String> menus) {
+        return Randoms.shuffle(menus).get(0);
+    }
+}

리뷰: 랜덤뽑기가 이 클래스의 관심사인지 고민해보면 좋을 것 같아요!
@@ -0,0 +1,43 @@
+package menu.domain;
+
+import java.util.Arrays;
+import java.util.List;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+public enum Category {
+    JAPANESE("일식", Arrays.asList("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼")),
+    KOREAN("한식", Arrays.asList("김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음")),
+    CHINESE("중식", Arrays.asList("깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채")),
+    ASIAN("아시안", Arrays.asList("팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜")),
+    WESTERN("양식", Arrays.asList("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"));;
+
+    private final String categoryName;
+    private final List<String> menus;
+
+    Category(String categoryName, List<String> menus) {
+        this.categoryName = categoryName;
+        this.menus = menus;
+    }
+
+    public String getCategoryName() {
+        return categoryName;
+    }
+
+    public List<String> getMenus() {
+        return menus;
+    }
+
+    public static Category randomCategory() {
+        int randomIndex = Randoms.pickNumberInRange(0, 4);
+        return Category.values()[randomIndex];
+    }
+
+    public static List<String> getMenusByCategory(Category category) {
+        return category.menus;
+    }
+
+    public static String getMenu(List<String> menus) {
+        return Randoms.shuffle(menus).get(0);
+    }
+}

리뷰: 메서드명에서 랜덤 뽑기임이 드러나지 않은 것 같아요! 일부러 숨기신 건가요??
@@ -0,0 +1,22 @@
+package menu.exception;
+
+public enum ErrorMessage {
+    INVALID_COACH_NAME_LENGTH("코치의 이름은 최소 2글자, 최대 4글자여야 합니다."),
+    INVALID_COACHES_COUNT("코치는 최소 2명, 최대 5명여야 합니다."),
+    INVALID_MENU_COUNT("못 먹는 메뉴는 최소 0개, 최대 2개여야 합니다."),
+    NOT_EXIST_MENU("존재하지 않는 메뉴입니다."),
+    ;
+
+    private static final String PREFIX = "[ERROR] ";
+    private static final String POSTFIX = " 다시 입력해 주세요.";
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage(Object... args) {
+        return String.format(PREFIX + message + POSTFIX, args);
+    }
+}

리뷰: 예외메시지 내의 상수를 외부에서 가져오면 좋을 것 같아요!
@@ -0,0 +1,27 @@
+package menu.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import menu.domain.Category;
+
+public class GenerateCategory {
+    public static List<Category> generate() {
+        List<Category> categories = new ArrayList<>();
+        Map<String, Integer> categoryCount = new HashMap<>();
+
+        while (categories.size() < 5) {
+            Category randomCategory = Category.randomCategory();
+            String categoryName = randomCategory.getCategoryName();
+
+            int count = categoryCount.getOrDefault(categoryName, 0);
+            if (count < 2) {
+                categories.add(randomCategory);
+                categoryCount.put(categoryName, count + 1);
+            }
+        }
+        return categories;
+    }
+}

리뷰: 조금 더 리팩토링해볼 수 있을 것 같아요..!
@@ -0,0 +1,33 @@
+package menu.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import menu.domain.CantEatingMenus;
+import menu.domain.Category;
+import menu.domain.Menu;
+
+public class GenerateMenus {
+    public static List<Menu> generate(List<Category> categories, CantEatingMenus cantEatingMenus) {
+        List<Menu> recommendMenus = new ArrayList<>();
+        do {
+            for (Category category : categories) {
+                getRecommendMenus(cantEatingMenus, category, recommendMenus);
+            }
+        } while (recommendMenus.size() > 5);
+        return recommendMenus;
+    }
+
+    private static void getRecommendMenus(CantEatingMenus cantEatingMenus, Category category, List<Menu> recommendMenus) {
+        if(!isIncludeNotEatingMenus(recommendMenus, cantEatingMenus)) {
+            List<String> menusByCategory = Category.getMenusByCategory(category);
+            String menu = Category.getMenu(menusByCategory);
+            recommendMenus.add(new Menu(menu));
+        }
+    }
+
+    private static boolean isIncludeNotEatingMenus(List<Menu> recommendMenus, CantEatingMenus cantEatingMenus) {
+        return cantEatingMenus.getCantEatingMenus().stream()
+            .anyMatch(recommendMenus::contains);
+    }
+}

리뷰: 이 클래스의 로직이 조금 난해한 감이 있어 보입니다.
다른 클래스를 더 신뢰하고 작업을 맡길 수 있다면 더 예쁜 코드가 나올 수 있을 것 같습니다!
@@ -0,0 +1,22 @@
+package menu.view;
+
+public enum OutputMessage {
+    START("점심 메뉴 추천을 시작합니다."),
+    INPUT_COACH_NAMES("\n코치의 이름을 입력해 주세요. (, 로 구분)"),
+    INPUT_CANT_EATING_MENU("\n%s(이)가 못 먹는 메뉴를 입력해 주세요.\n"),
+    RESULT_MESSAGE_START("\n메뉴 추천 결과입니다."),
+    DAY("[ 구분 | 월요일 | 화요일 | 수요일 | 목요일 | 금요일 ]"),
+    CATEGORY_RESULT("[ 카테고리"),
+    FINISH("\n\n추천을 완료했습니다."),
+    ;
+
+    private final String message;
+
+    OutputMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage(Object... args) {
+        return String.format(message, args);
+    }
+}

리뷰: String.format을 사용중인데 %n을 사용하지 않은 이유가 있나요!?
@@ -0,0 +1,48 @@
+package menu.view;
+
+import static menu.view.OutputMessage.*;
+
+import java.util.List;
+
+import menu.domain.Category;
+import menu.domain.Menu;
+
+public class OutputView {
+    public static void start() {
+        System.out.println(START.getMessage());
+    }
+
+    public static void inputCoachNames() {
+        System.out.println(INPUT_COACH_NAMES.getMessage());
+    }
+
+    public static void inputCantEatingMenus(String coachName) {
+        System.out.printf(INPUT_CANT_EATING_MENU.getMessage(coachName));
+    }
+
+    public static void printDayAndCategory(List<Category> categories) {
+        System.out.println(RESULT_MESSAGE_START.getMessage());
+        System.out.println(DAY.getMessage());
+        System.out.print(CATEGORY_RESULT.getMessage());
+        for(Category category : categories) {
+            System.out.printf(" | %s", category.getCategoryName());
+        }
+        System.out.print(" ]");
+    }
+
+    public static void printResult(String coachName, List<Menu> menus) {
+        System.out.printf("\n[ %s", coachName);
+        for(Menu menu : menus) {
+            System.out.printf(" | %s", menu.getMenuName());
+        }
+        System.out.print(" ]");
+    }
+
+    public static void finish() {
+        System.out.print(FINISH.getMessage());
+    }
+
+    public static void printErrorMessage(IllegalArgumentException e) {
+        System.out.println(e.getMessage());
+    }
+}

리뷰: printf와 println을 혼용하신 이유가 있을까요?!
서식문자는 getMessage() 안에서 전부 대체되지 않나요?

+) print와 println을 혼용하지 않으면 더 일관적인 코드가 될 것 같습니다!