Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dbbff11
docs(README): 기능목록 작성
ha0day Dec 1, 2022
d7a9381
docs(README): 기능목록 수정
ha0day Dec 2, 2022
eb32acb
feat(InputView): 자판기가 보유하고 있는 금액 입력 받고 검증하는 기능 구현
ha0day Dec 2, 2022
96c7f60
feat(Balance): 자판기가 보유하고 있는 금액으로 동전을 무작위로 생성하는 기능 구현
ha0day Dec 2, 2022
59a9700
feat(OutputVIew): 자판기가 보유하고 있는 동전 출력하는 기능 구현
ha0day Dec 2, 2022
a055ae3
feat(InputVIew): 상품명, 가격, 수량 입력 받는 기능 구현
ha0day Dec 2, 2022
bacd922
feat(VendingMachineController): 입력 받은 상품 정보 저장하는 기능 구현
ha0day Dec 2, 2022
aa1c9ae
feat(Validator): 입력 받은 상품 정보 검증하는 기능 구현
ha0day Dec 2, 2022
ec3b9f6
feat(InputView): 투입 금액 입력 받고 검증하는 기능 구현
ha0day Dec 2, 2022
7974545
feat(InputView): 구매 상품 입력 받는 기능 구현
ha0day Dec 2, 2022
93e8abd
feat(Validator): 입력 받은 구매 상품 검증하는 기능 구현
ha0day Dec 2, 2022
bfe1a10
feat(VendingMachineController): 상품 구매하는 기능 구현
ha0day Dec 2, 2022
4b3c3f3
feat(VendingMachineController): 잔돈 반환 여부 확인하는 기능 구현
ha0day Dec 2, 2022
d673a01
feat(Balance): 잔돈 계산하는 기능 구현
ha0day Dec 2, 2022
9de7ea4
feat(OutputView): 투입금액, 잔돈 출력하는 기능 구현
ha0day Dec 2, 2022
507b4c8
feat(VendingMachineController): 자판기 작동하는 기능 구현
ha0day Dec 2, 2022
1951b18
feat: 동전, 가격, 상품 클래스 생성
ha0day Dec 2, 2022
93f7616
docs(README): 기능목록 업데이트
ha0day Dec 2, 2022
d4d7b1a
feat(Application): 자판기 작동하는 기능 구현
ha0day Dec 2, 2022
2c530a5
test(ApplicationTest): 보유 금액 예외 테스트 코드 작성
ha0day Dec 2, 2022
4cbd3b8
test(CoinTest): 동전 생성이 정상적으로 되는지 테스트 코드 작성
ha0day Dec 2, 2022
dee099a
fix: 테스트 코드 통과하도록 Coin, Balance 코드 수정
ha0day Dec 12, 2022
ec2a163
refactor(Price): 검증 관련 코드 Validator로 이동
ha0day Dec 12, 2022
9345282
refactor(Product): 검증 관련 코드 Validator로 이동
ha0day Dec 12, 2022
4ef0101
refactor(ErrorMessage): 기본 에러 메시지 열거형 생성
ha0day Dec 12, 2022
356d745
refactor(ProductErrorMessage): 상품 관련 에러 메시지 열거형 생성
ha0day Dec 12, 2022
f568e50
refactor(InputMessage): 입력 안내 메시지 관련 열거형 생성
ha0day Dec 12, 2022
0e3ba71
refactor(NumberConsts): 상수 모음 클래스 생성
ha0day Dec 12, 2022
0885bd8
chore: 작은 수정사항 반영
ha0day Dec 12, 2022
d8d3fc9
refactor(Validator): 생성한 열거 상수 적용
ha0day Dec 12, 2022
5c84a60
refactor(InputView): 생성한 열거 상수 적용
ha0day Dec 12, 2022
762c909
refactor(Application): VendingMachineController 객체로 생성하여 구현
ha0day Dec 12, 2022
1a24493
refactor(VendingMachieController): 메서드 분리
ha0day Dec 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 자판기

## 기능 목록


- 자판기가 보유하고 있는 금액 입력 받기
- 입력값 검증
- 숫자인지
- 10의 배수인지


- 자판기가 보유하고 있는 금액으로 동전을 무작위로 생성


- 자판기가 보유하고 있는 동전 출력


- 상품명, 가격, 수량 입력 받기
- 입력값 검증
- 숫자인지
- 100원 이상인지
- 10원으로 나누어 떨어지는지


- 투입 금액 입력 받기
- 입력값 검증
- 숫자인지
- 양수인지


- 구매할 상품 입력 받기


- 입력 받은 상품 구매 가능 여부 확인
- 상품의 품절 여부
- 잔액이 충분한지
- 판매하는 상품인지


- 상품 구매
- 남은 투입금액 계산
- 남은 상품 수량 계산


- 잔돈 반환 여부 확인
- 잔돈 반환
- 남은 금액이 남은 상품의 최저 가격보다 적은 경우
- 모든 상품이 소진된 경우
- 잔돈 반환 X
- 상품 추가 구매


- 잔돈 계산
- 잔돈 전체 반환 가능 여부 확인
- 잔돈이 자판기 보유금액보다 금액이 작은 경우 > 전체 반환
- 잔돈이 자판기 보유금액보다 금액이 큰 경우 > 잔돈으로 반환할 수 있는 금액(보유금액)만 반환

- 잔돈 출력
7 changes: 6 additions & 1 deletion src/main/java/vendingmachine/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package vendingmachine;

import vendingmachine.controller.VendingMachineController;


public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
VendingMachineController vendingMachineController=new VendingMachineController();
vendingMachineController.setVendingMachine();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main 메서드에서 set -> runVendingMachine() 이라는 함수를 통해서 어떻게 보면 두가지 일을 하고 있는것 같아요. 코드를 봤는데, controller에서 run메서드 실행 전에 set은 반드시 되어야 하니까 run 메서드 안에서 setVendingMachine 메서드를 호출하는 방식이 좋지 않을까 생각해봐요!

vendingMachineController.runVendingMachine();
}
}
14 changes: 13 additions & 1 deletion src/main/java/vendingmachine/Coin.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@ public enum Coin {
this.amount = amount;
}

// 추가 기능 구현
public int get() {
return amount;
}

public static Coin getEqualCoin(int amount) {
for (Coin coin : Coin.values()) {
if (coin.amount == amount) {
return coin;
}
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package vendingmachine.controller;

import vendingmachine.model.Balance;
import vendingmachine.model.Product;
import vendingmachine.model.Validator;
import vendingmachine.view.InputView;
import vendingmachine.view.OutputView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static vendingmachine.model.Validator.validateProduct;

public class VendingMachineController {

private static Balance balance;
private static Map<String, Product> products;
private static int amountOfInput;

public void setVendingMachine() {
balance = new Balance(InputView.readBalance());
Copy link

@juno-junho juno-junho Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 static을 통해 클래스 변수로 지정하신 특별한 이유가 있나요?
balance를 인스턴스 변수로 선언하고 setter를 통해서가 아니라 생성자를 통해서 this.balance = new Balance(InputView...) 로 설정해주면 setVendingMachine 메서드를 없앨수도 있을것 같아요.

static 변수(클래스 변수)를 피해야 하는 이유는 여러가지지만 제가 명확히 기억하는 것은 2가지 인데,

  1. 사용하지 않더라도 프로그램 시작과 끝까지 메모리 내에 존재해 메모리 효율성이 떨어진다.
  2. 동시성 문제를 고려해줘야 한다. -> 한 클래스에 여러 곳에서 접근해 로직을 실행하는 도중 다른 곳에서 또 접근한다면 thread safe하지 않은 문제가 발생해서 Map도 ConcurrentMap 타입을 사용하는 것으로 저는 알고 있어요!
    추가적인 자료는 구글에 찾아봐도 나오지만 첨부해 두겠습니다!
    static 사용을 피해야 하는 이유

**추가
InputView랑 OutputView를 생성자로 받아 생성한다면 STATIC 메서드를 없앨 수 있을거같아요


다시 보니 setVendingMachine이 자판기 설정을 하는 메서드네요.. 나름 의미가 있는 메서드라는걸 코드 보면서 느꼈습니다. 단순히 객체를 초기화해주는게 아니었네요 (자판기 보유 동전 설정 + 상품 설정 역할을 하네요..)

OutputView.printBalanceCoin(balance.createCoin());
saveProducts();
amountOfInput = InputView.readAmountOfInput();
OutputView.printAmountOfInput(amountOfInput);
}

public void runVendingMachine() {
while (canBuy()) {
buy();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 받으면 buy()애서 구매할 상품명을 입력 먼저 받고 아래 OutputView.printAmountOfInput(amountOfInput); 통해서 투입금액이 출력되지 않나요? 순서가 바뀐것 같은데 실행은 잘 되시나보네요

그 위에서 한번 출력을 하고 시작하네요

OutputView.printAmountOfInput(amountOfInput);
}
OutputView.printChange(balance.calculateChangeCoin(amountOfInput));
}

private boolean canBuy() {
List<Integer> leftProductsPrice = new ArrayList<Integer>();
for (Map.Entry<String, Product> entry : products.entrySet()) {
if (entry.getValue().stockIsLeft()) {
leftProductsPrice.add(entry.getValue().getPrice());
}
}
if (leftProductsPrice.isEmpty()) {
return false;
}
return amountOfInput >= leftProductsPrice.stream().mapToInt(Integer::intValue).min().getAsInt();
}


public void saveProducts() throws IllegalArgumentException {
products = new HashMap<>();
try {
String[] str = InputView.readProductInfo().split(";");
for (String s : str) {
String[] productInfo = s.substring(1, s.length() - 1).split(",");
validateProduct(productInfo, products);
products.put(productInfo[0], new Product(productInfo[1], productInfo[2]));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한게 Product 클래스를 생성하면 name과 price, 수량을 갖는 필드를 만들어 주는 것이 일반적인데, 이렇게 name은 product 클래스에 필드로 뺀 특별한 이유가 있나요? 그럼 굳이 Map<product이름, Product>가 필요 없어질것같아요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 제가 Map을 좀 좋아하는 것 같아요..ㅋ ㅋㅋㅋㅋ 뭔가 이름을 key로 두면 필요한 정보 빼오거나 중복 체크하기 편한 느낌..? 근데 사실 저도 따로 빼는 게 조금 부자연스럽다고 생각하긴 했어요

}
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
saveProducts();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 다시 입력 받는것 또한 빼먹었네요..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 입력 받는 게 매우 귀찮은 부분이었죠..하하

}
}


private void buy() {
try {
String buyingProduct = InputView.readBuyingProduct();
Validator.validateBuyingProduct(buyingProduct, products, amountOfInput);
amountOfInput -= products.get(buyingProduct).getPrice();
products.get(buyingProduct).reduceAmount();
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
buy();
}
}
}

52 changes: 52 additions & 0 deletions src/main/java/vendingmachine/model/Balance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package vendingmachine.model;

import camp.nextstep.edu.missionutils.Randoms;
import vendingmachine.Coin;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

public class Balance {
private final int balance;
List<Integer> numbers = new ArrayList<>();
private final Map<Coin, Integer> coins = new EnumMap<>(Coin.class);
private final Map<Coin, Integer> changeCoins = new EnumMap<>(Coin.class);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이런게 있는 줄 몰랐어요 ㅋㅋ 저는 LinkedHashMap 써서 Coin enum 순서대로 넣어줬거든요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 처음 써봤어요ㅋㅋㅋㅋ 오히려 LinkedHashMap은 한번도 안 써봤네요..


public Balance(int balance) {
this.balance = balance;
for (Coin coin : Coin.values()) {
numbers.add(coin.get());
coins.put(coin, 0);
}
}

public Map<Coin, Integer> createCoin() {
int tmp = balance;

while (tmp > 0) {
int randomCoinAmount = Randoms.pickNumberInList(numbers);
if (tmp >= randomCoinAmount) {
Coin key = Coin.getEqualCoin(randomCoinAmount);
coins.put(key, coins.get(key) + 1);
tmp = tmp - randomCoinAmount;
}
}
return coins;
}

public Map<Coin, Integer> calculateChangeCoin(int change) {
if (change > balance) {
return coins;
}
for (Coin coin : Coin.values()) {
int numberOfBalanceCoin = coins.get(coin);
int neededCoins = change / coin.get();
int inputCoins = Math.min(neededCoins, numberOfBalanceCoin);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전 왜 이 생각을 못했죠
사실 이거 어떻게 구현하셨는지가 제일 궁금했는데 진짜 간단하게 하셨네요

전 neededCoin과 numberOfBalanceCoin 을 if문으로 경우 나눠서 처리해줬거든요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조금이라도 도움이 되었다니 다행이에요!

changeCoins.put(coin, inputCoins);
change -= coin.get() * inputCoins;
}
return changeCoins;
}
}
16 changes: 16 additions & 0 deletions src/main/java/vendingmachine/model/Price.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package vendingmachine.model;

import static vendingmachine.model.Validator.validateNum;
import static vendingmachine.model.Validator.validatePrice;

public class Price {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 클래스 뺀 이유가 있으신가요? Product안에서 int로 처리해줘도 될것 같은데 특별한 이유가 있는지 궁금해요

Copy link
Author

@ha0day ha0day Dec 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특별한 이유가 있다기 보다는 얼마 전에 원시 타입 포장 관련 글(https://tecoble.techcourse.co.kr/post/2020-05-29-wrap-primitive-type/)을 보고 처음 설계할 때 적용해 보려고 분리를 했었는데..
리팩토링 하다 보니 결국 검증 과정도 Validator 클래스로 다 넘겨서ㅋㅋㅋ 저도 끝에는 굳이 분리할 필요가 있었나 생각하긴 했어요ㅎㅎ

private final int price;
Price(String price) {
this.price = validateNum(price);
validatePrice(this.price);
}

public int get() {
return price;
}
}
27 changes: 27 additions & 0 deletions src/main/java/vendingmachine/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package vendingmachine.model;

import static vendingmachine.model.Validator.validateNegative;
import static vendingmachine.model.Validator.validateNum;

public class Product {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

product에 이름 필드를 넣는게 어떨까요?

private final Price price;
private int amount;

public Product(String price, String amount) throws IllegalArgumentException {
this.price = new Price(price);
this.amount = validateNum(amount);
validateNegative(this.amount);
}

public boolean stockIsLeft() {
return amount > 0;
}

public void reduceAmount() {
amount--;
}

public int getPrice() {
return price.get();
}
}
81 changes: 81 additions & 0 deletions src/main/java/vendingmachine/model/Validator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package vendingmachine.model;

import java.util.Map;

import static vendingmachine.util.NumberConsts.MIN_PRICE;
import static vendingmachine.util.NumberConsts.MIN_UNIT;
import static vendingmachine.util.NumberConsts.NUMBER_OF_ELEMENTS;
import static vendingmachine.util.message.ErrorMessage.INVALID_FORMAT;
import static vendingmachine.util.message.ErrorMessage.INVALID_PRICE;
import static vendingmachine.util.message.ErrorMessage.INVALID_UNIT;
import static vendingmachine.util.message.ErrorMessage.IS_NEGATIVE;
import static vendingmachine.util.message.ErrorMessage.NOT_NUMBER;
import static vendingmachine.util.message.ProductErrorMessage.INSUFFICIENT_BALANCE;
import static vendingmachine.util.message.ProductErrorMessage.INVALID_NAME;
import static vendingmachine.util.message.ProductErrorMessage.NOT_FOR_SALE;
import static vendingmachine.util.message.ProductErrorMessage.OUT_OF_STOCK;

public class Validator {

public static int validateBalance(String input) throws IllegalArgumentException {
int balance = validateNum(input);
validateNegative(balance);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 음수인것을 validate를 안해줬네요 ㅎㅎ

validateInvalidUnit(balance);
return balance;
}

public static int validateAmountOfInput(String input) throws IllegalArgumentException {
int amountOfInput = validateNum(input);
validateNegative(amountOfInput);
return amountOfInput;
}

public static void validatePrice(int price) {
if (price < MIN_PRICE) {
throw new IllegalArgumentException(INVALID_PRICE.fullMessage());
}
validateInvalidUnit(price);
}

public static void validateProduct(String[] productInfo, Map<String, Product> products) throws IllegalArgumentException {
if (productInfo.length != NUMBER_OF_ELEMENTS) {
throw new IllegalArgumentException(INVALID_FORMAT.fullMessage());
}
if (products.containsKey(productInfo[0])) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상품명이 중복되는 경우또한 저는 빼먹었네요

throw new IllegalArgumentException(INVALID_NAME.fullMessage());
}
}

public static void validateBuyingProduct(String buyingProduct, Map<String, Product> products,
int amountOfInput) throws IllegalArgumentException {
if (!products.containsKey(buyingProduct)) {
throw new IllegalArgumentException(NOT_FOR_SALE.fullMessage());
}
if (products.get(buyingProduct).getPrice() > amountOfInput) {
throw new IllegalArgumentException(INSUFFICIENT_BALANCE.fullMessage());
}
if (!products.get(buyingProduct).stockIsLeft()) {
throw new IllegalArgumentException(OUT_OF_STOCK.fullMessage());
}
}

public static int validateNum(String input) throws IllegalArgumentException {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(NOT_NUMBER.fullMessage());
}
}

public static void validateNegative(int num) throws IllegalArgumentException {
if (num < 0) {
throw new IllegalArgumentException(IS_NEGATIVE.fullMessage());
}
}

public static void validateInvalidUnit(int num) throws IllegalArgumentException {
if (num % MIN_UNIT != 0) {
throw new IllegalArgumentException(INVALID_UNIT.fullMessage());
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/vendingmachine/util/NumberConsts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package vendingmachine.util;

public class NumberConsts {
public static final int MIN_PRICE = 100;
public static final int MIN_UNIT = 10;
public static final int NUMBER_OF_ELEMENTS = 3;
}
23 changes: 23 additions & 0 deletions src/main/java/vendingmachine/util/message/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package vendingmachine.util.message;

import static vendingmachine.util.NumberConsts.MIN_PRICE;
import static vendingmachine.util.NumberConsts.MIN_UNIT;

public enum ErrorMessage {
ERROR_MESSAGE("[ERROR] "),
INVALID_PRICE(String.format("%d원 이상이어야 합니다.", MIN_PRICE)),
INVALID_UNIT(String.format("%d원으로 나누어떨어져야 합니다.", MIN_UNIT)),
INVALID_FORMAT("형식에 맞지 않습니다."),
NOT_NUMBER("형식에 맞지 않습니다."),
IS_NEGATIVE("음수를 입력할 수 없습니다.");

private final String message;

ErrorMessage(String message) {
this.message = message;
}

public String fullMessage() {
return ERROR_MESSAGE.message + message;
}
}
Loading