ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,63 @@
+package store.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.util.Map;
+import store.common.constants.ErrorConstants;
+import store.common.constants.MessageConstants;
+import store.common.constants.StringConstants;
+import store.view.utils.InputParser;
+import store.view.utils.InputValidation;
+
+public class InputView {
+
+    public static Map<String, Long> askPurchaseProduct() {
+        System.out.println(MessageConstants.PRODUCT_PURCHASE_MESSAGE);
+        while (true) {
+            try {
+                String input = Console.readLine();
+                String[] inputSplit = InputValidation.validatePurchaseInput(input);
+                System.out.println();
+                return InputParser.purchaseInputParser(inputSplit);
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellPromotionNotApplicable(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_NOT_APPLY_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    private static String getYesOrNoAnswer() {
+        while(true) {
+            try {
+                String answer = Console.readLine();
+                System.out.println();
+                if (!(answer.equals(StringConstants.NO) || answer.equals(StringConstants.YES))) {
+                    throw new IllegalArgumentException(ErrorConstants.INPUT_FORMAT_ERROR_MESSAGE);
+                }
+                return answer;
+            } catch(IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellFreeProductProvide(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_PROVIDE_FREE_PRODUCT_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    public static String askMembershipDiscount() {
+        System.out.println(MessageConstants.MEMBERSHIP_DISCOUNT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+
+    public static String buyAnotherProduct() {
+        System.out.println(MessageConstants.EXIT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+}

리뷰: parser를 뷰에서 쓰는 경우는 보통 '단순한 작업일때' 또는 '서비스 레이어로 분리하는게 비대하다고 생각될때'라고 생각합니다. 만약 해당 로직을 확장한다면 뷰의 역할이 많아질수도 있을것 같아요!
@@ -0,0 +1,11 @@
+package store.common.constants;
+
+public class ErrorConstants {
+    public static final String PRODUCT_PURCHASE_FORMAT_ERROR_MESSAGE = "[ERROR] 올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요.";
+    public static final String NOT_EXIST_PRODUCT_ERROR_MESSAGE = "[ERROR] 존재하지 않는 상품입니다. 다시 입력해 주세요.";
+    public static final String INVENTORY_SHORT_ERROR_MESSAGE = "[ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요.";
+    public static final String INPUT_FORMAT_ERROR_MESSAGE = "[ERROR] 잘못된 입력입니다. 다시 입력해 주세요.";
+    public static final String FILE_NOT_FOUND = "[ERROR] 파일을 찾을 수 없습니다.";
+    public static final String NOT_EXIST_PROMOTION_ERROR_MESSAGE = "[ERROR] 프로모션을 찾을 수 없습니다.";
+    public static final String NOT_EXIST_CASE_ERROR_MESSAGE = "[ERROR] 프로모션 재고 수량에 문제가 있습니다. 데이터를 확인해주세요";
+}

리뷰: 4주차 미션 조건에 열거형을 사용하는것이 있었던걸로 기억합니다! 다시 로직을 구성해볼때 고려해보는 것은 어떨까요?
@@ -0,0 +1,23 @@
+package store.service;
+
+import java.util.List;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.domain.Receipt;
+
+public interface StoreService {
+    Receipt buy(String name, Long quantity, List<Product> inventory, List<Promotion> promotions);
+
+    Long[] calculateTotalReceipts(List<Receipt> receipts);
+
+    Long calculateMembershipDiscount(Long price, String answer);
+
+    Receipt getProductFreeOrNot(Product promotionProduct, Long quantity, Long freeQuantity, String answer);
+
+    Receipt buyInSufficientPromotionStockOrNot(Product promotionProduct, Product nonPromotionProduct,
+                                               Long quantity, Long freeQuantity, String answer, Long promotionNotAppliedQuantity);
+
+    long calculatePromotionNotAppliedQuantity(Long quantity, Long promotionProductQuantity, Promotion promotion);
+
+    long calculateActualFreeQuantity(Long promotionProductQuantity, Promotion promotion);
+}

리뷰: 이번에 저는 인터페이스로 분리하는걸 실패했는데, 잘 적용해주셨네요! 어떤 것을 추상화했는지 보면서 '어디까지 추상화를 해야할까'에 대한 고민에 도움이 됐던것 같습니다!
@@ -0,0 +1,24 @@
+package store.common.constants;
+
+public class NumberConstants {
+    //product row index
+    public static final int PRODUCT_NAME_INDEX = 0;
+    public static final int PRODUCT_PRICE_INDEX = 1;
+    public static final int PRODUCT_QUANTITY_INDEX = 2;
+    public static final int PRODUCT_PROMOTION_INDEX = 3;
+
+    //promotion row index
+    public static final int PROMOTION_NAME_INDEX = 0;
+    public static final int PROMOTION_BUY_INDEX = 1;
+    public static final int PROMOTION_GET_INDEX = 2;
+    public static final int PROMOTION_START_DATE_INDEX = 3;
+    public static final int PROMOTION_END_DATE_INDEX = 4;
+
+    public static final long MAX_MEMBERSHIP_DISCOUNT = 8000L;
+    public static final long NO_MEMBERSHIP_DISCOUNT = 0L;
+    public static final double MEMBERSHIP_DISCOUNT_RATIO = 0.3;
+
+    public static final int RECEIPT_THREE_SECTION_WIDTH = 12;
+    public static final int RECEIPT_TWO_SECTION_WIDTH = 24;
+
+}

리뷰: long, double 타입으로 둔 이유가 있을까요?? 상수여서 바뀔일도 없고 20억이 넘어가는 큰 수를 만질 일이 없을 것 같아서요 뒤에서도 요걸 많이 쓰신 것 같은데 이유가 궁금해요!
@@ -0,0 +1,50 @@
+package store.domain;
+
+import static store.common.constants.NumberConstants.PRODUCT_NAME_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PRICE_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PROMOTION_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_QUANTITY_INDEX;
+
+import store.common.constants.ErrorConstants;
+
+public class Product {
+    private final String name;
+    private final Long price;
+    private Long quantity;
+    private final String promotion;
+
+    public Product(String[] productsInfo) {
+        String name = productsInfo[PRODUCT_NAME_INDEX];
+        long price = Long.parseLong(productsInfo[PRODUCT_PRICE_INDEX]);
+        long quantity = Long.parseLong(productsInfo[PRODUCT_QUANTITY_INDEX]);
+        String promotion = productsInfo[PRODUCT_PROMOTION_INDEX];
+
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public void buy(Long quantity) {
+        if (quantity > this.quantity) {
+            throw new IllegalArgumentException(ErrorConstants.INVENTORY_SHORT_ERROR_MESSAGE);
+        }
+        this.quantity -= quantity;
+    }
+
+    public Long getPrice() {
+        return price;
+    }
+
+    public Long getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

리뷰: 재고 확인을 별도의 메소드로 분리하면 어떨까요?
@@ -0,0 +1,8 @@
+package store.common.constants;
+
+public class AddressConstants {
+    public static final String productFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/products.md";
+    public static final String promotionFilePath =
+            "/Users/hanwool/WooTeco/java-convenience-store-7-hanwool1643/src/main/resources/promotions.md";
+}

리뷰: 동의합니다
@@ -0,0 +1,12 @@
+package store.service;
+
+import java.util.List;
+import java.util.Scanner;
+import store.domain.Product;
+import store.domain.Promotion;
+
+public interface FileService {
+    List<Product> extractProduct(Scanner productsFile);
+
+    List<Promotion> extractPromotion(Scanner promotionsFile);
+}

리뷰: 저도 잘 몰랐는데 Scanner를 인자로 받는건 구현 세부 사항에 대한 의존을 유발하므로, 추상화 수준을 높여 파일 경로나 문자열 등을 받도록 하면 좋다고 합니다!
@@ -0,0 +1,50 @@
+package store.domain;
+
+import static store.common.constants.NumberConstants.PRODUCT_NAME_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PRICE_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_PROMOTION_INDEX;
+import static store.common.constants.NumberConstants.PRODUCT_QUANTITY_INDEX;
+
+import store.common.constants.ErrorConstants;
+
+public class Product {
+    private final String name;
+    private final Long price;
+    private Long quantity;
+    private final String promotion;
+
+    public Product(String[] productsInfo) {
+        String name = productsInfo[PRODUCT_NAME_INDEX];
+        long price = Long.parseLong(productsInfo[PRODUCT_PRICE_INDEX]);
+        long quantity = Long.parseLong(productsInfo[PRODUCT_QUANTITY_INDEX]);
+        String promotion = productsInfo[PRODUCT_PROMOTION_INDEX];
+
+        this.name = name;
+        this.price = price;
+        this.quantity = quantity;
+        this.promotion = promotion;
+    }
+
+    public void buy(Long quantity) {
+        if (quantity > this.quantity) {
+            throw new IllegalArgumentException(ErrorConstants.INVENTORY_SHORT_ERROR_MESSAGE);
+        }
+        this.quantity -= quantity;
+    }
+
+    public Long getPrice() {
+        return price;
+    }
+
+    public Long getQuantity() {
+        return quantity;
+    }
+
+    public String getPromotion() {
+        return promotion;
+    }
+
+    public String getName() {
+        return name;
+    }
+}

리뷰: 저도 이 부분에 대해 궁금합니다! 배열 인덱스를 직접 사용하는 것은 실수의 여지가 있다고 판단되는데 DTO나 Builder 패턴 사용은 어떤가요?
@@ -0,0 +1,63 @@
+package store.view;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.util.Map;
+import store.common.constants.ErrorConstants;
+import store.common.constants.MessageConstants;
+import store.common.constants.StringConstants;
+import store.view.utils.InputParser;
+import store.view.utils.InputValidation;
+
+public class InputView {
+
+    public static Map<String, Long> askPurchaseProduct() {
+        System.out.println(MessageConstants.PRODUCT_PURCHASE_MESSAGE);
+        while (true) {
+            try {
+                String input = Console.readLine();
+                String[] inputSplit = InputValidation.validatePurchaseInput(input);
+                System.out.println();
+                return InputParser.purchaseInputParser(inputSplit);
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellPromotionNotApplicable(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_NOT_APPLY_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    private static String getYesOrNoAnswer() {
+        while(true) {
+            try {
+                String answer = Console.readLine();
+                System.out.println();
+                if (!(answer.equals(StringConstants.NO) || answer.equals(StringConstants.YES))) {
+                    throw new IllegalArgumentException(ErrorConstants.INPUT_FORMAT_ERROR_MESSAGE);
+                }
+                return answer;
+            } catch(IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public static String tellFreeProductProvide(String name, Long quantity) {
+        System.out.printf(MessageConstants.PROMOTION_PROVIDE_FREE_PRODUCT_MESSAGE, name, quantity);
+        System.out.println();
+        return getYesOrNoAnswer();
+    }
+
+    public static String askMembershipDiscount() {
+        System.out.println(MessageConstants.MEMBERSHIP_DISCOUNT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+
+    public static String buyAnotherProduct() {
+        System.out.println(MessageConstants.EXIT_MESSAGE);
+        return getYesOrNoAnswer();
+    }
+}

리뷰: 보통 유틸리티 메서드인 경우 static으로 선언하는데, view의 모든 메서드를 static으로 선언하신 이유가 궁금합니다!
@@ -0,0 +1,23 @@
+package store.service;
+
+import java.util.List;
+import store.domain.Product;
+import store.domain.Promotion;
+import store.domain.Receipt;
+
+public interface StoreService {
+    Receipt buy(String name, Long quantity, List<Product> inventory, List<Promotion> promotions);
+
+    Long[] calculateTotalReceipts(List<Receipt> receipts);
+
+    Long calculateMembershipDiscount(Long price, String answer);
+
+    Receipt getProductFreeOrNot(Product promotionProduct, Long quantity, Long freeQuantity, String answer);
+
+    Receipt buyInSufficientPromotionStockOrNot(Product promotionProduct, Product nonPromotionProduct,
+                                               Long quantity, Long freeQuantity, String answer, Long promotionNotAppliedQuantity);
+
+    long calculatePromotionNotAppliedQuantity(Long quantity, Long promotionProductQuantity, Promotion promotion);
+
+    long calculateActualFreeQuantity(Long promotionProductQuantity, Promotion promotion);
+}

리뷰: 서비스를 인터페이스로 구현한 점이 인상적이에요.
인터페이스는 메서드 수가 적으면 적을수록 좋다고 들었는데, 이는 메서드 수가 많아질수록 성급한 추상화일 확률이 커지기 때문이라고 생각합니다.
추상화의 정도를 정하는 건 늘 어려운 것 같아요... @hanwool1643 님 덕분에 어느 정도로 추상화를 하면 좋을지 고민해 볼 수 있었습니다!
@@ -0,0 +1,34 @@
+package store.service;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+import store.common.constants.StringConstants;
+import store.domain.Product;
+import store.domain.Promotion;
+
+public class FileServiceImpl implements FileService {
+    @Override
+    public List<Product> extractProduct(Scanner productsFile) {
+        List<Product> inventory = productsFile.tokens()
+                // 첫 번째 header row 필터
+                .filter(line -> !Objects.equals(line, StringConstants.PRODUCTS_FILE_HEADER))
+                .map(line -> new Product(line.split(StringConstants.COMMA)))
+                .collect(Collectors.toList());
+        productsFile.close();
+
+        return inventory;
+    }
+    @Override
+    public List<Promotion> extractPromotion(Scanner promotionsFile) {
+        List<Promotion> promotions = promotionsFile.tokens()
+                // 첫 번째 header row 필터
+                .filter(line -> !Objects.equals(line, StringConstants.PROMOTIONS_FILE_HEADER))
+                .map(line -> new Promotion(line.split(StringConstants.COMMA)))
+                .collect(Collectors.toList());
+        promotionsFile.close();
+
+        return promotions;
+    }
+}

리뷰: 첫 번째 라인은 이런 방식으로도 처리할 수 있군요! 저는 첫 번째 라인을 읽고 그냥 버린 뒤, 다음 라인부터 처리하는 방법을 사용했어요.
리뷰를 하다 보니 Stream의 skip 메서드를 활용하신 분도 계시더라고요!
리뷰하며 다양한 방법을 비교할 수 있어 참 흥미로운 것 같아요.
@@ -0,0 +1,77 @@
+package store.view.utils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import store.common.constants.StringConstants;
+
+public class InputParser {
+    public static String inventoryParser(String name, Long price, Long quantity, String promotion) {
+        StringBuilder stringBuilderWithDash = new StringBuilder(giveBlankToEnd(StringConstants.DASH));
+
+        return appendAll(
+                stringBuilderWithDash,
+                giveBlankToEnd(name),
+                giveBlankToEnd(parsePrice(price)),
+                giveBlankToEnd(parseQuantity(quantity)),
+                parsePromotion(promotion)
+        ).toString();
+    }
+
+    public static Map<String, Long> purchaseInputParser(String[] inputArray) {
+        Map<String, Long> purchaseDetail = new HashMap<>();
+        Arrays.stream(inputArray).forEach(input -> {
+            String bracketsRemoved = input.replace("[", "").replace("]", "");
+            String[] inputSplit = bracketsRemoved.split(StringConstants.DASH);
+            purchaseDetail.put(inputSplit[0], Long.parseLong(inputSplit[1]));
+        });
+        return purchaseDetail;
+    }
+
+    public static StringBuilder giveCommaToPrice(String[] splitPrice) {
+        StringBuilder priceWithComma = new StringBuilder();
+        for (int i = splitPrice.length - 1 ; i >= 0; i--) {
+            priceWithComma.insert(0, splitPrice[i]);
+            if ((splitPrice.length - i) % 3 == 0 && i != 0) {
+                priceWithComma.insert(0, StringConstants.COMMA);
+            }
+        }
+        return priceWithComma;
+    }
+
+    private static String giveBlankToEnd(String name) {
+        return name + StringConstants.BLANK;
+    }
+
+    private static String parsePromotion(String promotion) {
+        String promotionNullConverted = promotion;
+        if (Objects.equals(promotion, "null")) {
+            promotionNullConverted = "";
+        }
+        return promotionNullConverted;
+    }
+
+    private static String parseQuantity(Long quantity) {
+        String quantityWithPieces = quantity.toString() + StringConstants.PIECES;
+        if (quantity == 0) {
+            quantityWithPieces = StringConstants.OUT_OF_STOCK;
+        }
+        return quantityWithPieces;
+    }
+
+    private static String parsePrice(Long price) {
+        if (price >= 1000) {
+            String[] splitPrice = price.toString().split("");
+            return giveCommaToPrice(splitPrice).append(StringConstants.WON).toString();
+        }
+        return price + StringConstants.WON;
+    }
+
+    private static StringBuilder appendAll(StringBuilder stringbuilder, String... strings) {
+        for (String string : strings) {
+            stringbuilder.append(string);
+        }
+        return stringbuilder;
+    }
+}

리뷰: giveCommaToPrice에서 String[]을 받는 부분이 직관성이나 효율성이 조금 떨어져 보이는 것 같아요! 그냥 숫자를 받아서 처리를 전담해도 좋았을 것 같아요
@@ -0,0 +1,77 @@
+package store.view.utils;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import store.common.constants.StringConstants;
+
+public class InputParser {
+    public static String inventoryParser(String name, Long price, Long quantity, String promotion) {
+        StringBuilder stringBuilderWithDash = new StringBuilder(giveBlankToEnd(StringConstants.DASH));
+
+        return appendAll(
+                stringBuilderWithDash,
+                giveBlankToEnd(name),
+                giveBlankToEnd(parsePrice(price)),
+                giveBlankToEnd(parseQuantity(quantity)),
+                parsePromotion(promotion)
+        ).toString();
+    }
+
+    public static Map<String, Long> purchaseInputParser(String[] inputArray) {
+        Map<String, Long> purchaseDetail = new HashMap<>();
+        Arrays.stream(inputArray).forEach(input -> {
+            String bracketsRemoved = input.replace("[", "").replace("]", "");
+            String[] inputSplit = bracketsRemoved.split(StringConstants.DASH);
+            purchaseDetail.put(inputSplit[0], Long.parseLong(inputSplit[1]));
+        });
+        return purchaseDetail;
+    }
+
+    public static StringBuilder giveCommaToPrice(String[] splitPrice) {
+        StringBuilder priceWithComma = new StringBuilder();
+        for (int i = splitPrice.length - 1 ; i >= 0; i--) {
+            priceWithComma.insert(0, splitPrice[i]);
+            if ((splitPrice.length - i) % 3 == 0 && i != 0) {
+                priceWithComma.insert(0, StringConstants.COMMA);
+            }
+        }
+        return priceWithComma;
+    }
+
+    private static String giveBlankToEnd(String name) {
+        return name + StringConstants.BLANK;
+    }
+
+    private static String parsePromotion(String promotion) {
+        String promotionNullConverted = promotion;
+        if (Objects.equals(promotion, "null")) {
+            promotionNullConverted = "";
+        }
+        return promotionNullConverted;
+    }
+
+    private static String parseQuantity(Long quantity) {
+        String quantityWithPieces = quantity.toString() + StringConstants.PIECES;
+        if (quantity == 0) {
+            quantityWithPieces = StringConstants.OUT_OF_STOCK;
+        }
+        return quantityWithPieces;
+    }
+
+    private static String parsePrice(Long price) {
+        if (price >= 1000) {
+            String[] splitPrice = price.toString().split("");
+            return giveCommaToPrice(splitPrice).append(StringConstants.WON).toString();
+        }
+        return price + StringConstants.WON;
+    }
+
+    private static StringBuilder appendAll(StringBuilder stringbuilder, String... strings) {
+        for (String string : strings) {
+            stringbuilder.append(string);
+        }
+        return stringbuilder;
+    }
+}

리뷰: 1000을 상수로 빼면 좋을 것 같아요! 포멧팅 중에서도 , 붙이는게 있어서 그거 써도 좋을 것 같아요
@@ -1,5 +1,8 @@
 package vendingmachine;
 
+import java.util.Arrays;
+import java.util.List;
+
 public enum Coin {
     COIN_500(500),
     COIN_100(100),
@@ -12,5 +15,20 @@ public enum Coin {
         this.amount = amount;
     }
 
-    // 추가 기능 구현
+    public static Coin findByAmount(int amount) {
+        for (Coin coin : Coin.values()) {
+            if (coin.getAmount() == amount) {
+                return coin;
+            }
+        }
+        throw new IllegalArgumentException("No such coin");
+    }
+
+    public static List<Coin> getSortedCoins() {
+        return Arrays.stream(Coin.values()).sorted((o1, o2) -> o2.getAmount() - o1.getAmount()).toList();
+    }
+
+    public int getAmount() {
+        return amount;
+    }
 }

리뷰: 공백 라인을 좀 더 두시면 가독성이 좋을 것 같습니당
@@ -0,0 +1,57 @@
+package vendingmachine.controller;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import vendingmachine.domain.Product;
+import vendingmachine.validator.InputValidator;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class RetryInputUtil {
+    private final InputValidator inputValidator;
+
+    public RetryInputUtil(InputValidator inputValidator) {
+        this.inputValidator = inputValidator;
+    }
+
+    public int getVendingMachineBalance() {
+        return retryLogics(InputView::getVendingMachineBalance, InputParser::parseInt, inputValidator::validateBalance);
+    }
+
+    public List<Product> getProducts() {
+        return retryLogics(InputView::getProducts, InputParser::parseProducts, inputValidator::validateProducts);
+    }
+
+    public int getInputAmount() {
+        return retryLogics(InputView::getInputAmount, InputParser::parseInt, inputValidator::validateInputAmount);
+    }
+
+    private <T> T retryLogics(Supplier<String> userInputReader, Function<String, T> parser,
+                              Consumer<T> validator) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                T parsedInput = parser.apply(userInput);
+                validator.accept(parsedInput);
+                return parsedInput;
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(error.getMessage());
+            }
+        }
+    }
+
+    private String retryLogics(Supplier<String> userInputReader, Consumer<String> validator) {
+        while (true) {
+            try {
+                String userInput = userInputReader.get();
+                validator.accept(userInput);
+                return userInput;
+            } catch (IllegalArgumentException error) {
+                OutputView.printError(error.getMessage());
+            }
+
+        }
+    }
+}

리뷰: 이건 사용되지 않는 것 같은데 아닐까용?