-
Notifications
You must be signed in to change notification settings - Fork 341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
move : 프로젝트 변경으로 인한 코드 이동 #187
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# 자판기 | ||
|
||
## 프로그램 흐름 | ||
|
||
1. 자판기의 보유 금액을 입력한다. | ||
1. 자판기가 보유한 금액으로 코인을 랜덤 생성한다. | ||
- 숫자가 아닌 입력에서 예외 발생 | ||
- 음수를 입력 시 예외 발생 | ||
- 10원단위로 나누어 떨어지지 않는 경우 예외 발생 | ||
2. 생성된 코인을 출력한다. | ||
2. 자판기에서 판매할 상품 명과 가격, 수량을 입력한다. | ||
1. 상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어 세미콜론(;)으로 구분한다. | ||
- 입력 형식이 잘못된 경우 예외 발생 | ||
- 수량이 0, 또는 음수인 경우 예외 발생 | ||
2. 상품 가격은 100원부터 시작하며, 10원으로 나누어떨어져야 한다. | ||
- 올바르지 않은 상품 가격 입력시 예외 발생 | ||
3. 투입 금액을 입력한다. | ||
- 숫자가 아닌 입력에서 예외 발생 | ||
- 음수 입력시 예외 발생 | ||
- 0을 입력하면 예외 발생 | ||
4. 구매할 상품명을 입력한다. | ||
1. 매번 자판기에 남은 투입 금액을 출력한다. | ||
2. 구매할 상품 명을 입력받는다. | ||
- 존재하지 않는 상품명 입력시 예외 발생 | ||
- 재고가 남아있지 않은 메뉴 구매시 예외 발생 | ||
- 남은 투입 금액으로 구매할 수 없는 제품일 경우 예외 발생 | ||
5. 더이상 판매가 불가능할 경우 잔돈을 반환한다. | ||
1. 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다. |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package vendingmachine; | ||
|
||
import java.util.EnumMap; | ||
import java.util.List; | ||
import vendingmachine.domain.VendingMachine; | ||
import vendingmachine.domain.coin.Coin; | ||
import vendingmachine.domain.coin.Coins; | ||
import vendingmachine.domain.coin.generator.RandomCoinGenerator; | ||
import vendingmachine.domain.menu.Menu; | ||
import vendingmachine.domain.menu.Menus; | ||
import vendingmachine.domain.money.Cash; | ||
import vendingmachine.domain.money.Money; | ||
import vendingmachine.exception.RetryHandler; | ||
import vendingmachine.view.InputView; | ||
import vendingmachine.view.OutputView; | ||
|
||
public class VendingMachineController { | ||
private final InputView inputView = new InputView(); | ||
private final OutputView outputView = new OutputView(); | ||
private final RetryHandler handler = new RetryHandler(); | ||
|
||
public void run() { | ||
VendingMachine vendingMachine = initVendingMachine(); | ||
purchase(vendingMachine); | ||
} | ||
|
||
private void purchase(VendingMachine vendingMachine) { | ||
|
||
Cash remainCash = handler.getOrRetry(this::insertCash); | ||
|
||
while (vendingMachine.canPurchase(remainCash)) { | ||
handler.runOrRetry(() -> { | ||
purchaseMenu(vendingMachine, remainCash); | ||
}); | ||
} | ||
returnCoin(vendingMachine, remainCash.toMoney()); | ||
} | ||
Comment on lines
+27
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 직관적이고 반복문의 사용을 최소화한 로직으로 보입니다. 좋네요. 😄😄 |
||
|
||
private Cash insertCash() { | ||
int money = inputView.insertMoney(); | ||
return new Cash(money); | ||
} | ||
|
||
private void returnCoin(VendingMachine vendingMachine, Money money) { | ||
outputView.printRemainMoney(money.getPrice()); | ||
EnumMap<Coin, Integer> returnCoin = vendingMachine.returnCoin(money); | ||
outputView.printReturnCoins(returnCoin); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혁수님 로직을 보며, 제 코드에 문제점을 발견하게 되었네요. 미션을 진행할 때, |
||
private void purchaseMenu(VendingMachine vendingMachine, Cash remainCash) { | ||
outputView.printRemainMoney(remainCash.getPrice()); | ||
String inputMenu = inputView.purchaseMenu(); | ||
vendingMachine.purchase(inputMenu, remainCash); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 함수형 인터페이스에 대해, 람다식으로 전달하는 방법보다 메서드 참조로 전달하는 방식이 정말 깔끔하고 좋은 것 같습니다.
정말 명확하고 관리하기 용이한 코드인 것 같습니다 👍👍 |
||
private VendingMachine initVendingMachine() { | ||
Coins coins = handler.getOrRetry(this::makeCoins); | ||
Menus menus = handler.getOrRetry(this::makeMenus); | ||
return new VendingMachine(coins, menus); | ||
} | ||
|
||
private Coins makeCoins() { | ||
int initCoins = inputView.getMachineMoney(); | ||
Coins coins = new Coins(initCoins, new RandomCoinGenerator()); | ||
outputView.printMachineCoin(coins); | ||
return coins; | ||
} | ||
|
||
private Menus makeMenus() { | ||
List<String> inputMenus = inputView.getMenus(); | ||
List<Menu> menus = inputMenus.stream() | ||
.map(Menu::from) | ||
.toList(); | ||
return new Menus(menus); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package vendingmachine.domain; | ||
|
||
import java.util.EnumMap; | ||
import vendingmachine.domain.coin.Coin; | ||
import vendingmachine.domain.coin.Coins; | ||
import vendingmachine.domain.menu.Menus; | ||
import vendingmachine.domain.money.Cash; | ||
import vendingmachine.domain.money.Money; | ||
|
||
public class VendingMachine { | ||
private final Coins coins; | ||
private final Menus menus; | ||
|
||
public VendingMachine(Coins coins, Menus menus) { | ||
this.coins = coins; | ||
this.menus = menus; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
를 확인하기 위한 메서드인 것 같아요!
최종적으로, 자판기에서 상품을 구매할 수 있는지 검증하기 위한 협력은 아래와 같은 흐름인 것으로 판단됩니다. 객체지향 관점에서 역할 분리가 잘 되어 있는 코드라 느껴졌습니다 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위에 코멘트 드린 내용과 이어지는 내용입니다. 아래의 코멘트는 정답이 없는 영역이라, 가벼운 제안 + 혁수님 생각이 궁금하여 남겨드립니다. 최근에 우아한 형제들 세미나를 통해 이런 내용을 들은적이 있어요.
말씀드린 내용은 객체 유효성 검증에 대한 내용이라 조금 다른 주제이긴 하지만,
따라서 해당 검증 로직은 자판기에서 모두 수행하도록 하고(약간의 절차지향), 검증을 위해 필요한 기능들만 관련 객체들이 API로 제공해주는 방식으로 리팩터링한다면 응집도를 높여주고, 한 눈에 확인할 수 있는 장점도 있을 것 같습니다. 이 부분에 대해 혁수님 의견이 궁금합니다 😊 흥미로운 주제라 생각되어, 다른 분들도 의견 있으시다면 편히 코멘트 남겨주세요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위에서 언급해 주신 네이밍과 관련된 내용은 저 역시 구현하면서 많이 아쉬웠던 부분이에요. 지금의 제 생각으론 제 코드를 리팩토링 한다고 해도 저의 코드에선 상품을 더군다나, 다만, 이렇게 생각한 이유는 제가 제 코드를 많이 보면서 어느 정도 제 코드에 가스라이팅 당한 것도 있다고 생각하기에 준기님의 생각이 다르다면 얼마든지 편하게 말씀해 주시면 좋을 것 같습니다 ㅎㅎㅎ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @june-777 menu라는 객체를 생성했다면 구매할 수 있는지 검증은 메뉴의 책임이라고 생각합니다. |
||
public boolean canPurchase(Cash cash) { | ||
return menus.canPurchase(cash); | ||
} | ||
|
||
public void purchase(String menuName, Cash cash) { | ||
menus.purchase(menuName, cash); | ||
} | ||
|
||
public EnumMap<Coin, Integer> returnCoin(Money remainMoney) { | ||
return coins.returnCoins(remainMoney); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package vendingmachine.domain.coin; | ||
|
||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import vendingmachine.exception.VendingMachineException; | ||
|
||
public enum Coin { | ||
COIN_500(500), | ||
COIN_100(100), | ||
COIN_50(50), | ||
COIN_10(10); | ||
|
||
private static final Map<Integer, Coin> coins = new HashMap<>(); | ||
|
||
static { | ||
Arrays.stream(values()) | ||
.forEach(coin -> coins.put(coin.amount, coin)); | ||
} | ||
|
||
private final int amount; | ||
|
||
Coin(final int amount) { | ||
this.amount = amount; | ||
} | ||
|
||
// 추가 기능 구현 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍👍 참고로 Optional의 값이 없는 경우
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 해당 기준이 적절하다고 생각합니다!! 좋은 것 같네요 👍 ㅎㅎ |
||
public static Coin from(int amount) { | ||
return Optional.ofNullable(coins.get(amount)) | ||
.orElseThrow(VendingMachineException.INVALID_COIN_PRICE::makeException); | ||
} | ||
Comment on lines
+29
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. orElseThrow 활용법 하나 배워갑니다. 🏃♂️🏃♂️ |
||
|
||
public int getAmount() { | ||
return amount; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package vendingmachine.domain.coin; | ||
|
||
import java.util.EnumMap; | ||
import vendingmachine.domain.coin.generator.CoinGenerator; | ||
import vendingmachine.domain.money.Money; | ||
import vendingmachine.exception.VendingMachineException; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제 코드를 리마인드하며 새로운 궁금증이 생겨 추가로 코멘트 남깁니다. 제 코드는
public class Coins {
private final EnumMap<Coin, Integer> coinCounts;
public Coins(EnumMap<Coin, Integer> coinCounts) {
this.coinCounts = coinCounts;
}
// something.. 와 같은 형태를 띄고 있습니다. 제 구현 방식의 장점은
단점으로는
혁수님의 구현 방식은 Coins에서 CoinsGenerator를 호출하여 생성하는 로직이다 보니, 위에 열거드린 제 구현 방식의 장 / 단점이 뒤바뀌어진다고 느껴졌습니다. 야구 게임 미션과 같이 // Domain
public class BaseballCollection {
private final List<Integer> baseball;
public static BaseballCollection ofComputerBaseball(NumberGenerator numberGenerator) {
return new BaseballCollection(numberGenerator);
}
// something
}
// Test Code
@Test
@DisplayName("[성공 테스트] 3스트라이크")
void getHintSuccessTest1() {
// given
List<Integer> computerNums = Lists.newArrayList(1, 3, 7);
// BaseballCollection 내부에서 Generator를 통해 생성하지만, 테스트에 큰 지장 없음
BaseballCollection computerBalls = BaseballCollection.ofComputerBaseball(() -> computerNums.remove(0));
// something test 테스트 코드에는 큰 지장이 없어서, 지금까지 생각 못했던 부분인 것 같습니다. 쓰다보니 내용이 길어져,, 요약 드리면
두 가지로 나뉘는 것 같고, 이 부분에 대해 혁수님의 생각이 궁금합니다 🤔🤔🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오, 이 부분에 대해서 구현할 때 깊이 생각해 보지 않은 것 같은데요. 준기님이 말씀해 주신 테스트 관점에서 먼저 생각을 해 봤어요. 확실히 테스트 하기 좋은 코드가 될 것 같아요. 그리고, 준기님의 코드에서 검증 로직을 찾는 것도 그렇게 어렵진 않았고요. 그리고, 마지막으로 제 코드를 리팩토링 한다면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @june-777 자판기에서 거슬러진 돈을 바로 사용자에게 할당하는 방식으로 구현했고, 조금 더 단순하게 접근할 수 있었던 부분이 장점이라고 여겨져요. |
||
public class Coins { | ||
public static final int PRINCE_UNIT = 10; | ||
private final EnumMap<Coin, Integer> coins; | ||
|
||
public Coins(int money, CoinGenerator generator) { | ||
validateMoney(money); | ||
this.coins = generator.generate(money); | ||
} | ||
|
||
private void validateMoney(int money) { | ||
if (money < 0) { | ||
throw VendingMachineException.MONEY_MUST_NOT_NEGATIVE.makeException(); | ||
} | ||
|
||
if (money % PRINCE_UNIT != 0) { | ||
throw VendingMachineException.INVALID_MONEY_UNIT.makeException(PRINCE_UNIT); | ||
} | ||
} | ||
|
||
public int getCoinCount(Coin coin) { | ||
return coins.get(coin); | ||
} | ||
|
||
public EnumMap<Coin, Integer> returnCoins(Money remainMoney) { | ||
EnumMap<Coin, Integer> returnCoins = new EnumMap<>(Coin.class); | ||
int change = remainMoney.getPrice(); | ||
for (Coin coin : Coin.values()) { | ||
int count = 0; | ||
while (change > coin.getAmount() && coins.get(coin) > 0) { | ||
change -= coin.getAmount(); | ||
coins.put(coin, coins.get(coin) - 1); | ||
count++; | ||
} | ||
if (count != 0) { | ||
returnCoins.put(coin, count); | ||
} | ||
} | ||
return returnCoins; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package vendingmachine.domain.coin.generator; | ||
|
||
import java.util.Arrays; | ||
import java.util.EnumMap; | ||
import java.util.List; | ||
import vendingmachine.domain.coin.Coin; | ||
|
||
public interface CoinGenerator { | ||
List<Integer> coinAmounts = Arrays.stream(Coin.values()) | ||
.map(Coin::getAmount) | ||
.toList(); | ||
Comment on lines
+9
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구현방식이 신기해서 잠깐 고민했습니다. 테스트를 고려하면 지금처럼 분리된 방식이 현명할 듯 하네요. |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혁수님의 구현을 몇 번 봐보니, Java 8 부터 도입된 제가 알기로 저는 그래서 아직까지는 인터페이스의 default 메서드를 활용해야겠다는 순간은 못느낀 것 같아요! 혁수님은 어떤 장점이 있을 것으로 판단하여 default 메서드를 적극 활용하시는지 궁금해요! 😊 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 이 부분은 그리고 이렇게 작성을 하는 와중에 " 저 역시 아직 |
||
default EnumMap<Coin, Integer> generate(int money) { | ||
EnumMap<Coin, Integer> coins = initCoins(); | ||
while (money > 0) { | ||
Coin coin = pickCoin(); | ||
if(coin.getAmount() <= money){ | ||
coins.put(coin, coins.get(coin) + 1); | ||
money -= coin.getAmount(); | ||
} | ||
} | ||
return coins; | ||
} | ||
|
||
private static EnumMap<Coin, Integer> initCoins() { | ||
EnumMap<Coin, Integer> coins = new EnumMap<>(Coin.class); | ||
Arrays.stream(Coin.values()) | ||
.forEach(coin -> coins.put(coin, 0)); | ||
return coins; | ||
} | ||
|
||
Coin pickCoin(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package vendingmachine.domain.coin.generator; | ||
|
||
import camp.nextstep.edu.missionutils.Randoms; | ||
import vendingmachine.domain.coin.Coin; | ||
|
||
public class RandomCoinGenerator implements CoinGenerator { | ||
@Override | ||
public Coin pickCoin() { | ||
int coinAmount = Randoms.pickNumberInList(coinAmounts); | ||
return Coin.from(coinAmount); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zangsu
해당 객체들을 주입받아서 가지고 오는 방법은 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저 역시 인스턴스 필드로 가지고 있는 부분들을 생성 시점에서 의존성 주입을 받는게 좋을까? 라는 고민을 했었습니다!
그리고, 실제로 2주차였나 3주차 프리코스 미션을 수행할 때는
View
의 부분을 추상화 하여 의존성을 주입받도록 만들어 보기도 하였고요.사실, 우리가 항상 스터디때 말하는 것 처럼 "정답은 없"겠지만, 그래도 정답에 가까운 부분은 추상화 제공 후 의존성을 주입받는 것이라 생각해요.
아시는 바와 같이, 그리고 창혁님이 평소 의도하시는 바와 같이 혹시나 해당 부분에 다양한 변경 구현체가 필요할 때 손쉽게 구현 클래스를 갈아끼울 수 있다는 장점이 있죠. 그래서 창혁님의 방법처럼 AppConfig를 사용하는 것은 정말 좋아보입니다.
다만, 저는 이번 최종 코딩테스트를 진행하는 동안 "코딩테스트 5시간동안 변경이 되지 않을 부분들"에 대한 추상화 및 의존성 주입은 과감하게 타협하기로 하였습니다.
우리가 추상화를 제공하고 의존성을 주입받도록 하는 것은 어디까지나 "이후의 확장성"을 고려한 부분이기도 하고, 추후 확장 가능성이 생겼을 때 해당 부분에 추상화를 제공하도록 리팩토링 하는 것은 어려운 작업은 아니라고 생각했거든요.
그리고, 무엇보다도 제가 AppConfig를 사용해 의존성을 주입받는 방법을 많이 사용해 보지 않다 보니 의존성을 주입받는 코드를 작성하는게 조금 어색하기도 하고요.
간단하게 줄이자면, 해당 방법은 정말 좋아 보이지만, 이 부분을 타협하는 대신 다른 부분을 더 신경쓰기로 했다고 이해해 주시면 좋을 것 같습니다!