ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,83 @@
+package store.dto.response;
+
+import static store.constant.ConvenienceConstant.*;
+import static store.constant.message.OutputMessage.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import store.model.domain.Stock;
+
+public class StocksResponse {
+
+    private final List<InnerStockResponse> stocksResponse;
+
+    private StocksResponse(List<InnerStockResponse> stocksResponse) {
+        this.stocksResponse = stocksResponse;
+    }
+
+    public static StocksResponse from(List<Stock> stocks) {
+        List<InnerStockResponse> newStocksResponse = new ArrayList<>();
+
+        for (Stock stock : stocks) {
+            newStocksResponse.add(InnerStockResponse.from(stock));
+        }
+        return new StocksResponse(newStocksResponse);
+    }
+
+    public List<InnerStockResponse> getStocksResponse() {
+        return stocksResponse;
+    }
+
+    public static class InnerStockResponse {
+
+        private final String name;
+        private final Integer price;
+        private final String promotionName;
+        private final String quantity;
+
+        private InnerStockResponse(String name, Integer price, String promotionName, String quantity) {
+            this.name = name;
+            this.price = price;
+            this.promotionName = promotionName;
+            this.quantity = quantity;
+        }
+
+        private static InnerStockResponse from(Stock stock) {
+            return new InnerStockResponse(
+                    stock.getProductName(),
+                    stock.getProductPrice(),
+                    formatPromotionName(stock.getPromotionName()),
+                    formatQuantity(stock.getQuantity()));
+        }
+
+        private static String formatPromotionName(String promotionName) {
+            if (promotionName.equals(NO_PROMOTION)) {
+                return EMPTY_STRING;
+            }
+            return promotionName;
+        }
+
+        private static String formatQuantity(Integer quantity) {
+            if (quantity <= 0) {
+                return NOT_IN_STOCK;
+            }
+            return String.format(STOCKS_COUNT.getMessage(), quantity);
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public Integer getPrice() {
+            return price;
+        }
+
+        public String getPromotionName() {
+            return promotionName;
+        }
+
+        public String getQuantity() {
+            return quantity;
+        }
+    }
+}
\ No newline at end of file

리뷰: stream을 사용할 수 있을 것 같아요!
@@ -1,7 +1,43 @@
 package store;
 
+import static store.enums.ErrorMessage.INVALID_INPUT;
+
+import camp.nextstep.edu.missionutils.Console;
+import java.io.IOException;
+import store.config.StoreConfig;
+import store.controller.PurchaseController;
+import store.controller.StoreController;
+
 public class Application {
+
+    private static final StoreController storeController = StoreConfig.getStoreController();
+    private static final PurchaseController purchaseController = StoreConfig.getPurchaseController();
+
     public static void main(String[] args) {
-        // TODO: 프로그램 구현
+        init();
+        try {
+            execute();
+        } finally {
+            Console.close();
+            storeController.clearFile();
+        }
+    }
+
+    private static void execute() {
+        boolean isRetry = true;
+        while (isRetry) {
+            storeController.explain();
+            purchaseController.purchase();
+            storeController.clearOrder();
+            isRetry = purchaseController.retry();
+        }
+    }
+
+    private static void init() {
+        try {
+            storeController.init();
+        } catch (IOException e) {
+            throw new IllegalArgumentException(INVALID_INPUT.getMessage());
+        }
     }
 }

리뷰: do-while 활용해도 좋았을 것 같습니다!
@@ -0,0 +1,87 @@
+package store.domain;
+
+import static store.enums.ErrorMessage.INVALID_FILE_FORMAT;
+import static store.enums.ErrorMessage.INVALID_INPUT;
+
+import java.time.LocalDate;
+import java.util.Objects;
+import store.domain.vo.PromotionDate;
+import store.exception.InvalidFileFormatException;
+import store.utils.StringUtils;
+
+public class Promotion {
+
+    private static final String NULL_NAME = "null";
+    private static final long GET_PROMOTION_QUANTITY = 1;
+
+    private final String name;
+    private final PromotionDate promotionDate;
+    private final long buyPromotionQuantity;
+
+    protected Promotion(String name, long buyQuantity, LocalDate startDate, LocalDate endDate) {
+        this.name = name;
+        this.promotionDate = new PromotionDate(startDate, endDate);
+        this.buyPromotionQuantity = buyQuantity;
+    }
+
+    public static Promotion create(String name, long buyQuantity, LocalDate startDate, LocalDate endDate) {
+        StringUtils.validName(name);
+        validPromotionName(name);
+        validPromotionQuantity(buyQuantity);
+        return new Promotion(name, buyQuantity, startDate, endDate);
+    }
+
+    private static void validPromotionQuantity(Long buyQuantity) {
+        if (buyQuantity <= 0) {
+            throw new InvalidFileFormatException(INVALID_FILE_FORMAT.getMessage());
+        }
+    }
+
+    private static void validPromotionName(String name) {
+        if (name.equalsIgnoreCase(NULL_NAME)) {
+            throw new InvalidFileFormatException(INVALID_INPUT.getMessage());
+        }
+    }
+
+    public boolean isValidPromotion(LocalDate currentDate) {
+        return promotionDate.isAvailable(currentDate);
+    }
+
+    protected long bundleSize() {
+        return GET_PROMOTION_QUANTITY + buyPromotionQuantity;
+    }
+
+    protected long totalGetQuantity(long promotionBundleCount) {
+        return promotionBundleCount * GET_PROMOTION_QUANTITY;
+    }
+
+    protected long getPromotionGiftQuantity(long requestQuantity) {
+        if ((requestQuantity + GET_PROMOTION_QUANTITY) % bundleSize() == 0) {
+            return GET_PROMOTION_QUANTITY;
+        }
+        return 0L;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Promotion promotion = (Promotion) o;
+        return buyPromotionQuantity == promotion.buyPromotionQuantity && Objects.equals(name, promotion.name)
+                && Objects.equals(promotionDate, promotion.promotionDate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, promotionDate, buyPromotionQuantity);
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}

리뷰: private이 아니라, protected인 이유가 궁금해요! 아래 정적팩터리메서드만 사용해도 될 것 같아보여서요!
@@ -0,0 +1,87 @@
+package store.domain;
+
+import static store.enums.ErrorMessage.INVALID_FILE_FORMAT;
+import static store.enums.ErrorMessage.INVALID_INPUT;
+
+import java.time.LocalDate;
+import java.util.Objects;
+import store.domain.vo.PromotionDate;
+import store.exception.InvalidFileFormatException;
+import store.utils.StringUtils;
+
+public class Promotion {
+
+    private static final String NULL_NAME = "null";
+    private static final long GET_PROMOTION_QUANTITY = 1;
+
+    private final String name;
+    private final PromotionDate promotionDate;
+    private final long buyPromotionQuantity;
+
+    protected Promotion(String name, long buyQuantity, LocalDate startDate, LocalDate endDate) {
+        this.name = name;
+        this.promotionDate = new PromotionDate(startDate, endDate);
+        this.buyPromotionQuantity = buyQuantity;
+    }
+
+    public static Promotion create(String name, long buyQuantity, LocalDate startDate, LocalDate endDate) {
+        StringUtils.validName(name);
+        validPromotionName(name);
+        validPromotionQuantity(buyQuantity);
+        return new Promotion(name, buyQuantity, startDate, endDate);
+    }
+
+    private static void validPromotionQuantity(Long buyQuantity) {
+        if (buyQuantity <= 0) {
+            throw new InvalidFileFormatException(INVALID_FILE_FORMAT.getMessage());
+        }
+    }
+
+    private static void validPromotionName(String name) {
+        if (name.equalsIgnoreCase(NULL_NAME)) {
+            throw new InvalidFileFormatException(INVALID_INPUT.getMessage());
+        }
+    }
+
+    public boolean isValidPromotion(LocalDate currentDate) {
+        return promotionDate.isAvailable(currentDate);
+    }
+
+    protected long bundleSize() {
+        return GET_PROMOTION_QUANTITY + buyPromotionQuantity;
+    }
+
+    protected long totalGetQuantity(long promotionBundleCount) {
+        return promotionBundleCount * GET_PROMOTION_QUANTITY;
+    }
+
+    protected long getPromotionGiftQuantity(long requestQuantity) {
+        if ((requestQuantity + GET_PROMOTION_QUANTITY) % bundleSize() == 0) {
+            return GET_PROMOTION_QUANTITY;
+        }
+        return 0L;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Promotion promotion = (Promotion) o;
+        return buyPromotionQuantity == promotion.buyPromotionQuantity && Objects.equals(name, promotion.name)
+                && Objects.equals(promotionDate, promotion.promotionDate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, promotionDate, buyPromotionQuantity);
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}

리뷰: 제가 주로 프로텍트를 사용하는 이유는 단위 테스트나 계층 내부에서만 사용할 수 있도록 하는게 목적이긴해요. 다만, 패키지 프라이빗을 쓰는게 더 올라는게 맞다고 생각하긴하네요!
@@ -0,0 +1,80 @@
+package store.controller;
+
+import static store.enums.PromptMessage.BUY;
+import static store.enums.PromptMessage.MEMBERSHIP_DISCOUNT;
+import static store.enums.PromptMessage.RETRY_PURCHASE;
+
+import store.dto.Message;
+import store.dto.OrderConfirmDto;
+import store.dto.OrderConfirmDtos;
+import store.enums.Confirmation;
+import store.service.PurchaseService;
+import store.utils.RecoveryUtils;
+import store.viewer.InputViewer;
+import store.viewer.OutputViewer;
+
+public class PurchaseController {
+
+    private final PurchaseService purchaseService;
+    private final InputViewer inputViewer;
+    private final OutputViewer outputViewer;
+
+    public PurchaseController(PurchaseService purchaseService, InputViewer inputViewer, OutputViewer outputViewer) {
+        this.purchaseService = purchaseService;
+        this.inputViewer = inputViewer;
+        this.outputViewer = outputViewer;
+    }
+
+    public void purchase() {
+        buy();
+        check();
+        printReceipt();
+    }
+
+    protected void buy() {
+        inputViewer.promptMessage(BUY);
+        RecoveryUtils.executeWithRetry(inputViewer::getInput, purchaseService::createOrderInfos);
+    }
+
+    protected void check() {
+        OrderConfirmDtos confirmDtos = purchaseService.check();
+        for (OrderConfirmDto confirmDto : confirmDtos.items()) {
+            if (confirmDto.problemQuantity() == 0) {
+                purchaseService.processQuantity(confirmDto);
+                continue;
+            }
+            confirmProblemQuantity(confirmDto);
+        }
+    }
+
+    protected void printReceipt() {
+        inputViewer.promptMessage(MEMBERSHIP_DISCOUNT);
+
+        Message receiptDto = RecoveryUtils.executeWithRetry(() -> Confirmation.from(inputViewer.getInput()),
+                purchaseService::getReceipt);
+
+        outputViewer.printResult(receiptDto);
+    }
+
+    public boolean retry() {
+        inputViewer.promptMessage(RETRY_PURCHASE);
+        Confirmation confirmation = RecoveryUtils.executeWithRetry(() -> Confirmation.from(inputViewer.getInput()));
+        return confirmation.isBool();
+    }
+
+    private void printProblemQuantity(OrderConfirmDto confirmDto) {
+        if (confirmDto.problemQuantity() < 0) {
+            inputViewer.promptNonDiscount(confirmDto.name(), -confirmDto.problemQuantity());
+        }
+
+        if (confirmDto.problemQuantity() > 0) {
+            inputViewer.promptFreeItem(confirmDto.name(), confirmDto.problemQuantity());
+        }
+    }
+
+    private void confirmProblemQuantity(OrderConfirmDto confirmDto) {
+        printProblemQuantity(confirmDto);
+        Confirmation confirmation = RecoveryUtils.executeWithRetry(() -> Confirmation.from(inputViewer.getInput()));
+        purchaseService.processProblemQuantity(confirmDto, confirmation);
+    }
+}

리뷰: 컨트롤러는 사용자 인터페이스와 서비스 계층간의 다리 역할을 담당 하고있어야 하는데 check()는 검증 부분이라 서비스로 분리 해야하지 않을까요?
@@ -165,3 +165,47 @@ public enum Coin {
 - **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다.
    - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다.
 - 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다.
+
+---
+
+## 기능 구현 목록
+- [x] [자판기] 사용자로부터 자판기가 보유하고 있는 금액을 입력받는 기능
+
+
+- [x] [자판기] 자판기가 보유하고 있는 금액만큼 동전을 무작위로 생성하는 기능
+  - [x] 투입 금액으로는 동전을 생성하지 않는다.
+
+
+- [x] [자판기] 자판기가 보유하고 있는 동전의 개수를 출력하는 기능
+
+
+- [x] [자판기] 사용자로부터 상품명, 가격, 수량을 입력받고, 상품을 추가하는 기능
+  - [x] [예외 상황] 개별 상품이 대괄호([])로 묶이지 않는 경우
+  - [x] [예외 상황] 상품명, 가격, 수량이 쉼표(,)로 구분되지 않는 경우
+  - [x] [예외 상황] 상품 가격이 100원부터 시작하지 않고, 10원으로 나누어 떨어지지지 않는 경우
+  - [x] [예외 상황] 상품명이 비어있는 경우
+  - [x] [예외 상황] 상품 수량이 0 미만인 경우
+
+
+- [x] [구매] 사용자로부터 투입 금액을 입력받는 기능
+
+
+- [x] [구매] 사용자로부터 구매할 상품명을 입력받는 기능
+
+
+- [x] [구매] 입력받은 투입 금액과 상품명을 이용해 구매를 진행하는 기능
+
+
+- [x] [잔돈 반환] 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려주는 기능
+  - [x] 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.
+  - [x] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
+  - [x] 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.
+  - [x] 반환되지 않은 금액은 자판기에 남는다.
+
+
+과제 시작 시간: 21:30
+<br>TDD 종료 : 01:58 (4시간 28분)
+<br>MVP 완성 : 02:13 (TDD + 15분)
+<br>5시간 종료 : 02:30
+<br>리팩토링 종료 : 03:10 (MVP 완성 + 40분)
+<br>총 5시간 40분

리뷰: 설계에 시간을 따로 투자하셨는지 궁금해요
@@ -165,3 +165,47 @@ public enum Coin {
 - **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다.
    - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다.
 - 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다.
+
+---
+
+## 기능 구현 목록
+- [x] [자판기] 사용자로부터 자판기가 보유하고 있는 금액을 입력받는 기능
+
+
+- [x] [자판기] 자판기가 보유하고 있는 금액만큼 동전을 무작위로 생성하는 기능
+  - [x] 투입 금액으로는 동전을 생성하지 않는다.
+
+
+- [x] [자판기] 자판기가 보유하고 있는 동전의 개수를 출력하는 기능
+
+
+- [x] [자판기] 사용자로부터 상품명, 가격, 수량을 입력받고, 상품을 추가하는 기능
+  - [x] [예외 상황] 개별 상품이 대괄호([])로 묶이지 않는 경우
+  - [x] [예외 상황] 상품명, 가격, 수량이 쉼표(,)로 구분되지 않는 경우
+  - [x] [예외 상황] 상품 가격이 100원부터 시작하지 않고, 10원으로 나누어 떨어지지지 않는 경우
+  - [x] [예외 상황] 상품명이 비어있는 경우
+  - [x] [예외 상황] 상품 수량이 0 미만인 경우
+
+
+- [x] [구매] 사용자로부터 투입 금액을 입력받는 기능
+
+
+- [x] [구매] 사용자로부터 구매할 상품명을 입력받는 기능
+
+
+- [x] [구매] 입력받은 투입 금액과 상품명을 이용해 구매를 진행하는 기능
+
+
+- [x] [잔돈 반환] 잔돈을 돌려줄 때 현재 보유한 최소 개수의 동전으로 잔돈을 돌려주는 기능
+  - [x] 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다.
+  - [x] 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
+  - [x] 남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.
+  - [x] 반환되지 않은 금액은 자판기에 남는다.
+
+
+과제 시작 시간: 21:30
+<br>TDD 종료 : 01:58 (4시간 28분)
+<br>MVP 완성 : 02:13 (TDD + 15분)
+<br>5시간 종료 : 02:30
+<br>리팩토링 종료 : 03:10 (MVP 완성 + 40분)
+<br>총 5시간 40분

리뷰: 새벽3시까지 쉬지않고 6시간 코딩은 좀.. 무섭네요 😵‍💫
@@ -0,0 +1,25 @@
+package vendingmachine.constant;
+
+public enum ErrorMessage {
+    INVALID_COIN_MONEY("[ERROR] 유효하지 않은 동전 금액입니다."),
+    INVALID_PRODUCT_NAME("[ERROR] 상품명은 비어 있을 수 없습니다."),
+    INVALID_PRODUCT_PRICE("[ERROR] 상품 가격은 100원부터 시작하며 10원으로 나누어 떨어져야 합니다."),
+    INVALID_PRODUCT_QUANTITY("[ERROR] 상품 수량은 0 이상이어야 합니다."),
+    PRODUCT_SOLD_OUT("[ERROR] 해당 상품은 품절되었습니다."),
+    PRODUCT_IS_NOT_EXIST("[ERROR] 해당 상품이 존재하지 않습니다."),
+    NOT_ENOUGH_MONEY("[ERROR] 구매 가능한 상품이 없습니다."),
+    MONEY_SHOULD_BE_NUMBER("[ERROR] 금액은 숫자여야 합니다."),
+    MONEY_SHOULD_BE_POSITIVE("[ERROR] 금액은 양수여야 합니다."),
+    INVALID_PRODUCT_FORMAT("[ERROR] 상품의 형식이 올바르지 않습니다."),
+    ;
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 공통되는 문구 `[ERROR]`는 상수화해도 좋았을 것 같아요!
@@ -0,0 +1,25 @@
+package vendingmachine.constant;
+
+public enum ErrorMessage {
+    INVALID_COIN_MONEY("[ERROR] 유효하지 않은 동전 금액입니다."),
+    INVALID_PRODUCT_NAME("[ERROR] 상품명은 비어 있을 수 없습니다."),
+    INVALID_PRODUCT_PRICE("[ERROR] 상품 가격은 100원부터 시작하며 10원으로 나누어 떨어져야 합니다."),
+    INVALID_PRODUCT_QUANTITY("[ERROR] 상품 수량은 0 이상이어야 합니다."),
+    PRODUCT_SOLD_OUT("[ERROR] 해당 상품은 품절되었습니다."),
+    PRODUCT_IS_NOT_EXIST("[ERROR] 해당 상품이 존재하지 않습니다."),
+    NOT_ENOUGH_MONEY("[ERROR] 구매 가능한 상품이 없습니다."),
+    MONEY_SHOULD_BE_NUMBER("[ERROR] 금액은 숫자여야 합니다."),
+    MONEY_SHOULD_BE_POSITIVE("[ERROR] 금액은 양수여야 합니다."),
+    INVALID_PRODUCT_FORMAT("[ERROR] 상품의 형식이 올바르지 않습니다."),
+    ;
+
+    private final String message;
+
+    ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}

리뷰: 예외 메시지 내의 숫자는 상수를 활용하면 좋을 것 같아요!
@@ -0,0 +1,21 @@
+package vendingmachine.constant;
+
+public enum OutputMessage {
+    INPUT_MACHINE_HAVE("자판기가 보유하고 있는 금액을 입력해 주세요."),
+    MACHINE_HAVE("\n자판기가 보유한 동전"),
+    INPUT_PRODUCTS("\n상품명과 가격, 수량을 입력해 주세요."),
+    INPUT_MONEY("\n투입 금액을 입력해 주세요."),
+    BUYING("\n투입 금액: %d원%n구매할 상품명을 입력해주세요."),
+    RETURN_CHANGE("\n잔돈 반환:"),
+    ;
+
+    private final String message;
+
+    OutputMessage(String message) {
+        this.message = message;
+    }
+
+    public String format(Object... args) {
+        return String.format(this.message, args);
+    }
+}

리뷰: %n을 활용하는것도 좋을 것 같아요!
@@ -0,0 +1,75 @@
+package vendingmachine.controller;
+
+import vendingmachine.model.domain.Coins;
+import vendingmachine.model.domain.Products;
+import vendingmachine.model.domain.VendingMachine;
+import vendingmachine.model.service.VendingMachineService;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class VendingMachineController {
+    private final VendingMachineService vendingMachineService;
+
+    public VendingMachineController(VendingMachineService vendingMachineService) {
+        this.vendingMachineService = vendingMachineService;
+    }
+
+    public void start() {
+        OutputView.promptInputMachineHave();
+        int money = InputView.vendingMachineHave();
+
+        VendingMachine vendingMachine = vendingMachineService.getVendingMachine(money);
+        OutputView.promptMachineHave(vendingMachine);
+
+        getProducts(vendingMachine);
+        int inputMoney = getInputMoney();
+
+        inputMoney = getInputMoney(vendingMachine, inputMoney);
+        getReturnChange(vendingMachine, inputMoney);
+    }
+
+    private static void getProducts(VendingMachine vendingMachine) {
+        OutputView.promptInputProducts();
+        Products products = InputView.buyProducts();
+        vendingMachine.addProducts(products);
+    }
+
+    private static int getInputMoney() {
+        OutputView.promptInputMoney();
+        return InputView.inputMoney();
+    }
+
+    private static int getInputMoney(VendingMachine vendingMachine, int inputMoney) {
+        while (inputMoney > 0) {
+            int minPrice = vendingMachine.findMinimumProductPrice();
+            if (inputMoney < minPrice) {
+                break;
+            }
+            if (vendingMachine.areProductsSoldOut()) {
+                break;
+            }
+
+            OutputView.promptBuying(inputMoney);
+            String buyProductName = InputView.buyProduct();
+
+            inputMoney = calculateInputMoney(vendingMachine, inputMoney, buyProductName);
+        }
+        return inputMoney;
+    }
+
+    private static void getReturnChange(VendingMachine vendingMachine, int inputMoney) {
+        Coins change = vendingMachine.returnChange(inputMoney);
+        OutputView.promptReturnChange(change);
+    }
+
+    private static int calculateInputMoney(VendingMachine vendingMachine, int inputMoney, String buyProductName) {
+        try {
+            int productPrice = vendingMachine.findProductPrice(buyProductName);
+            inputMoney -= productPrice;
+            vendingMachine.purchaseProduct(buyProductName);
+        } catch (IllegalArgumentException e) {
+            OutputView.promptErrorMessage(e.getMessage());
+        }
+        return inputMoney;
+    }
+}

리뷰: 책임을 조금 더 서비스에 위임했어도 좋았을 것 같아요!
@@ -0,0 +1,75 @@
+package vendingmachine.controller;
+
+import vendingmachine.model.domain.Coins;
+import vendingmachine.model.domain.Products;
+import vendingmachine.model.domain.VendingMachine;
+import vendingmachine.model.service.VendingMachineService;
+import vendingmachine.view.InputView;
+import vendingmachine.view.OutputView;
+
+public class VendingMachineController {
+    private final VendingMachineService vendingMachineService;
+
+    public VendingMachineController(VendingMachineService vendingMachineService) {
+        this.vendingMachineService = vendingMachineService;
+    }
+
+    public void start() {
+        OutputView.promptInputMachineHave();
+        int money = InputView.vendingMachineHave();
+
+        VendingMachine vendingMachine = vendingMachineService.getVendingMachine(money);
+        OutputView.promptMachineHave(vendingMachine);
+
+        getProducts(vendingMachine);
+        int inputMoney = getInputMoney();
+
+        inputMoney = getInputMoney(vendingMachine, inputMoney);
+        getReturnChange(vendingMachine, inputMoney);
+    }
+
+    private static void getProducts(VendingMachine vendingMachine) {
+        OutputView.promptInputProducts();
+        Products products = InputView.buyProducts();
+        vendingMachine.addProducts(products);
+    }
+
+    private static int getInputMoney() {
+        OutputView.promptInputMoney();
+        return InputView.inputMoney();
+    }
+
+    private static int getInputMoney(VendingMachine vendingMachine, int inputMoney) {
+        while (inputMoney > 0) {
+            int minPrice = vendingMachine.findMinimumProductPrice();
+            if (inputMoney < minPrice) {
+                break;
+            }
+            if (vendingMachine.areProductsSoldOut()) {
+                break;
+            }
+
+            OutputView.promptBuying(inputMoney);
+            String buyProductName = InputView.buyProduct();
+
+            inputMoney = calculateInputMoney(vendingMachine, inputMoney, buyProductName);
+        }
+        return inputMoney;
+    }
+
+    private static void getReturnChange(VendingMachine vendingMachine, int inputMoney) {
+        Coins change = vendingMachine.returnChange(inputMoney);
+        OutputView.promptReturnChange(change);
+    }
+
+    private static int calculateInputMoney(VendingMachine vendingMachine, int inputMoney, String buyProductName) {
+        try {
+            int productPrice = vendingMachine.findProductPrice(buyProductName);
+            inputMoney -= productPrice;
+            vendingMachine.purchaseProduct(buyProductName);
+        } catch (IllegalArgumentException e) {
+            OutputView.promptErrorMessage(e.getMessage());
+        }
+        return inputMoney;
+    }
+}

리뷰: 이부분은 도메인 객체의 관심사라고 볼 수도 있을 것 같습니다!