Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 2 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ plugins {
id 'java'
}

group 'camp.nextstep.edu'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}

dependencies {
implementation 'com.github.woowacourse-projects:mission-utils:1.0.0'
implementation 'com.github.woowacourse-projects:mission-utils:1.1.0'
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
languageVersion = JavaLanguageVersion.of(17)
}
}

Expand Down
28 changes: 28 additions & 0 deletions docs/README.md
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. 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다.
3 changes: 2 additions & 1 deletion src/main/java/vendingmachine/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
VendingMachineController controller = new VendingMachineController();
controller.run();
}
}
16 changes: 0 additions & 16 deletions src/main/java/vendingmachine/Coin.java

This file was deleted.

76 changes: 76 additions & 0 deletions src/main/java/vendingmachine/VendingMachineController.java
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();
Comment on lines +18 to +20
Copy link

Choose a reason for hiding this comment

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

@zangsu
해당 객체들을 주입받아서 가지고 오는 방법은 어떻게 생각하시나요?

Copy link
Author

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를 사용해 의존성을 주입받는 방법을 많이 사용해 보지 않다 보니 의존성을 주입받는 코드를 작성하는게 조금 어색하기도 하고요.


간단하게 줄이자면, 해당 방법은 정말 좋아 보이지만, 이 부분을 타협하는 대신 다른 부분을 더 신경쓰기로 했다고 이해해 주시면 좋을 것 같습니다!


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
Copy link

Choose a reason for hiding this comment

The 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);
}

Choose a reason for hiding this comment

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

혁수님 로직을 보며, 제 코드에 문제점을 발견하게 되었네요.
제 코드는 구매할 상품명을 입력할 때, 없는 메뉴인 경우 무한루프가 정상 동작하지만, 재고가 떨어진 메뉴는 프로그램이 종료하는 문제가 있었습니다. 😂😂

미션을 진행할 때, ExceptionHandler(RetryHandler)에 태울 로직의 묶음 단위를 꼼꼼히 파악하는게 정말 중요할 것 같습니다,, 👍

private void purchaseMenu(VendingMachine vendingMachine, Cash remainCash) {
outputView.printRemainMoney(remainCash.getPrice());
String inputMenu = inputView.purchaseMenu();
vendingMachine.purchase(inputMenu, remainCash);
}

Choose a reason for hiding this comment

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

함수형 인터페이스에 대해, 람다식으로 전달하는 방법보다 메서드 참조로 전달하는 방식이 정말 깔끔하고 좋은 것 같습니다.

  1. 무한 루프의 단위를 메서드로 분리하여 구현한다.
  2. 메서드 참조로 Handler에 태운다

정말 명확하고 관리하기 용이한 코드인 것 같습니다 👍👍

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);
}
}
30 changes: 30 additions & 0 deletions src/main/java/vendingmachine/domain/VendingMachine.java
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;
}

Choose a reason for hiding this comment

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

남은 금액이 상품의 최저 가격보다 적거나, 모든 상품이 소진된 경우 바로 잔돈을 돌려준다.

를 확인하기 위한 메서드인 것 같아요!

VendingMachine -> Menus (canPurchase): 구매 가능한지 확인한다.
Menus -> Menu (canPurchase): 메뉴가 소진되었는지 확인한다.
Menu -> Cash (canPurchase): 현재 금액으로 구매할 수 있는지 확인한다.

최종적으로, 자판기에서 상품을 구매할 수 있는지 검증하기 위한 협력은 아래와 같은 흐름인 것으로 판단됩니다.
VendingMachine -> Menus -> Menu -> Cash

객체지향 관점에서 역할 분리가 잘 되어 있는 코드라 느껴졌습니다 👍
하지만 모든 메서드 네이밍이 canPurchase이기 때문에, 흐름을 하나씩 따라가며 세부 구현을 확인해야 정확히 어떤 로직인지 파악이 가능한 아쉬움도 느껴졌어요! 메서드 네이밍을 의도에 명확하게 수정하시면 더 파악하기 쉬울 것 같습니다 :)

Choose a reason for hiding this comment

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

위에 코멘트 드린 내용과 이어지는 내용입니다. 아래의 코멘트는 정답이 없는 영역이라, 가벼운 제안 + 혁수님 생각이 궁금하여 남겨드립니다.

최근에 우아한 형제들 세미나를 통해 이런 내용을 들은적이 있어요.

주제: Validation 위치
Validate 로직이 적다면 객체 안에서 수행하는게 맞는데, 그 객체를 Validate 하기 위해 여러 객체를 참조해야 한다면, 응집도가 확 떨어져버리는 트레이드 오프가 있다. 역할 분리가 잘 되어 있지만, 어떤 유효성 검증인지 파악하기 위해 여러 클래스를 가봐야 알 수 있기 때문이다. 따라서 때로는 절차지향적인 방식도 도움이 될 수 있다. ~~~

말씀드린 내용은 객체 유효성 검증에 대한 내용이라 조금 다른 주제이긴 하지만, 자판기에서 상품을 구매할 수 있는지 검증하기 위한 협력도 뭔가 같은 결이라고 문득 느껴졌습니다.

자판기에서 상품을 구매할 수 있는지 확인하는 검증로직에 대해, 여러 클래스들을 오가며 확인해야하다보니 응집도가 떨어지는 것 아닌가?라는 생각이 들었어요.

따라서 해당 검증 로직은 자판기에서 모두 수행하도록 하고(약간의 절차지향), 검증을 위해 필요한 기능들만 관련 객체들이 API로 제공해주는 방식으로 리팩터링한다면 응집도를 높여주고, 한 눈에 확인할 수 있는 장점도 있을 것 같습니다. 이 부분에 대해 혁수님 의견이 궁금합니다 😊

흥미로운 주제라 생각되어, 다른 분들도 의견 있으시다면 편히 코멘트 남겨주세요!

Copy link
Author

Choose a reason for hiding this comment

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

위에서 언급해 주신 네이밍과 관련된 내용은 저 역시 구현하면서 많이 아쉬웠던 부분이에요.
다만 보시는 것 처럼 VendingMachine이 구매 가능 여부를 알 수 있는 방법은 menus에게 물어보는 방법밖에 없기에 menus에 똑같은 질문을 다시 전달해 주는 느낌인데요. 이 상황에서 어떤 식으로 네이밍을 구분해 줄 수 있을 지 고민이네요.


지금의 제 생각으론 제 코드를 리팩토링 한다고 해도 canPurchase() 로직은 지금의 형태에서 크게 바꿀 것 같진 않아요.

저의 코드에선 상품을 Cash로 구매할 수 있을 지 확인하기 위해선 각각의 판매 항목들을 순회하면서 하나라도 구매 가능한 항목이 존재하는지를 확인해야 해요.
그리고 이 각각의 항목들은 List<>를 래핑한 일급 컬렉션 Menus가 가지고 있고요.
Menus를 필드 값으로 가지고 있는 VendingMachine이 구매 가능 여부를 알 수 있는 방법은 Menus에게 구매 가능 여부를 물어보는 방법 이외엔 없다고 생각합니다.

더군다나, Menus.canPurchase()를 호출하는 행위 자체가 해당 행동을 위한 필요 API를 Menus가 적절히 제공해 주고 있는 것이라고 생각했어요.

다만, 이렇게 생각한 이유는 제가 제 코드를 많이 보면서 어느 정도 제 코드에 가스라이팅 당한 것도 있다고 생각하기에 준기님의 생각이 다르다면 얼마든지 편하게 말씀해 주시면 좋을 것 같습니다 ㅎㅎㅎ

Copy link

Choose a reason for hiding this comment

The 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);
}
}
37 changes: 37 additions & 0 deletions src/main/java/vendingmachine/domain/coin/Coin.java
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;
}

// 추가 기능 구현

Choose a reason for hiding this comment

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

👍👍

참고로 Optional의 값이 없는 경우 orElseThrow로 예외를 발생시킬지, 비어있는 Optional을 반환할지 고민했었는데

  1. 없는 값일 때 추가적인 로직이 필요하다 -> 후자
  2. 없는 값이면 그냥 예외상황이다 -> 전자
    로 기준을 잡게 되었습니다 :) 참고하시면 좋을 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

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

orElseThrow 활용법 하나 배워갑니다. 🏃‍♂️🏃‍♂️


public int getAmount() {
return amount;
}
}
47 changes: 47 additions & 0 deletions src/main/java/vendingmachine/domain/coin/Coins.java
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;

Choose a reason for hiding this comment

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

제 코드를 리마인드하며 새로운 궁금증이 생겨 추가로 코멘트 남깁니다.

제 코드는 Coins을 생성하기 위해 Service -> CoinChanger를 거칩니다.
CoinChanger에서

  1. 보유금액으로 동전을 교환할 수 있는지 검증하고
  2. 보유 금액에 대한 EnumMap<Coin, Integer>를 생성하여 반환합니다.
  3. Coins의 생성자 파라미터는 EnumMap<Coin, Integer>만을 받음으로써 결과적으로
public class Coins {
    private final EnumMap<Coin, Integer> coinCounts;

    public Coins(EnumMap<Coin, Integer> coinCounts) {
        this.coinCounts = coinCounts;
    }

    // something..

와 같은 형태를 띄고 있습니다.

제 구현 방식의 장점은

  1. Domain 객체가 갖고 있는 public API에 대해, 테스트하기 굉장히 용이하다고 느껴졌습니다. (생성자 파라미터가 EnumMap만을 인자로 받고 있기 때문입니다.)
  2. Coins의 핵심 역할인 남아있는 금액을 동전으로 거슬러준다는 꽤나 까다로운 구현이 요구되었고, 혁수님과 비슷한 방식으로 2중 Loop문에 무한 반복까지 발생 가능한 형태가 되어, 반드시 테스트가 필요한 로직이었습니다.
  3. 여기서 1번이 큰 강점으로 다가왔습니다.

단점으로는

  1. 보유금액으로 동전을 교환할 수 있는지 검증하는 로직이 서비스 계층 (CoinChangerImpl)로 위치하게 되어, 다른 개발자가 봤을 때 검증로직을 추적하기 어려울 수도 있겠다는 생각이 들었습니다.
  2. Coins 객체에서 Coins생성 (EnumMap<Coin, Integer>) 에 대한 책임을 지지 않는 것이 좋은 코드인지는 잘 모르겠네요.

혁수님의 구현 방식은 Coins에서 CoinsGenerator를 호출하여 생성하는 로직이다 보니, 위에 열거드린 제 구현 방식의 장 / 단점이 뒤바뀌어진다고 느껴졌습니다.

야구 게임 미션과 같이 Generator 로직이 간단한 경우 아래와 같이

// 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

테스트 코드에는 큰 지장이 없어서, 지금까지 생각 못했던 부분인 것 같습니다.
하지만 자판기 미션은 Generator로직이 꽤나 복잡하여, Coins객체에서 Generator를 호출하여 생성하는 방식은 테스트하기 어려울 수 있겠다는 생각도 들었습니다 🤔


쓰다보니 내용이 길어져,, 요약 드리면

  1. Domain 객체는 최대한 순수하게 유지한다. (검증 / public API / 내부 구현만 존재)
  2. Domain 객체를 생성하기 위한 Generator 를 통해 생성한다. 그 외 1번과 동일

두 가지로 나뉘는 것 같고, 이 부분에 대해 혁수님의 생각이 궁금합니다 🤔🤔🤔

Copy link
Author

Choose a reason for hiding this comment

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

오, 이 부분에 대해서 구현할 때 깊이 생각해 보지 않은 것 같은데요.
결국 변경될 수 있는 객체 생성 방식에 대해 이미 생성한 객체를 파라미터로 전달 받느냐 VS 객체 생성 전략 자체를 파라미터로 전달 받아 내부에서 값을 생성하느냐 의 차이가 되겠네요.

준기님이 말씀해 주신 테스트 관점에서 먼저 생각을 해 봤어요.
준기님의 경우 테스트 결과를 확인하기 위해 단순히 CoinGenerator가 생성해 줬을 EnumMap만을 전달해 주면 되는 반면, 저는 EnumMap의 결과를 반환하는 CoinGenerator 클래스를 별도로 구현해 주어야 했겠네요.

확실히 테스트 하기 좋은 코드가 될 것 같아요.

그리고, 준기님의 코드에서 검증 로직을 찾는 것도 그렇게 어렵진 않았고요.

그리고, 마지막으로 Coins 객체가 객체 생성에 대한 책임을 가져야 하는가? (must) 라고 묻는다면 저는 아니다. 라고 대답할 것 같습니다. 우리가 작성한 Coins는 결국 EnumMap을 래핑한 일급 컬렉션의 역할을 잘 수행하는 것이 가장 중요하다고 생각해요.
Coins의 책임은 외부에 Coin와 관련된 API를 제공해 주는 것이지, 객체 생성은 어떤 식으로 진행하든 문제는 없을 것 같은데요!

제 코드를 리팩토링 한다면 Coin을 생성하는 로직을 조금 더 밖으로 분리해 낼 것 같습니다.
준기님의 구현처럼요!

Copy link

Choose a reason for hiding this comment

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

@june-777
저는 거스름돈을 거슬러 주는 자판기, 거스름돈을 받는 사용자로 큰 틀의 도메인을 분리했어요. 각자 돈을 담을 수 있는 주머니 혹은 지갑이 있다고 가정하고 Coins 객체를 각각 할당했습니다.

자판기에서 거슬러진 돈을 바로 사용자에게 할당하는 방식으로 구현했고, 조금 더 단순하게 접근할 수 있었던 부분이 장점이라고 여겨져요.

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
Copy link

Choose a reason for hiding this comment

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

구현방식이 신기해서 잠깐 고민했습니다. 테스트를 고려하면 지금처럼 분리된 방식이 현명할 듯 하네요.


Choose a reason for hiding this comment

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

혁수님의 구현을 몇 번 봐보니, Java 8 부터 도입된 인터페이스 default 메서드를 적극 활용하신다고 느껴졌어요!

제가 알기로 default 메서드 도입 배경은
자바 라이브러리, 스프링 등 자바 기반 생태계에서 인터페이스A, 구현체1, 구현체2, ...구현체10 인 상황일 때, 인터페이스A에 대한 오퍼레이션 시그니쳐를 새롭게 추가하기 부담되니 (수십개의 구현체에서 추가 오버라이딩하여 구현해야 하므로) default 메서드를 도입한 것으로 알고 있습니다.

저는 그래서 아직까지는 인터페이스의 default 메서드를 활용해야겠다는 순간은 못느낀 것 같아요! 혁수님은 어떤 장점이 있을 것으로 판단하여 default 메서드를 적극 활용하시는지 궁금해요! 😊

Copy link
Author

Choose a reason for hiding this comment

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

사실 이 부분은 default 메서드가 리팩토링 시점이거나, 구현 끝자락 즈음에 작성된 코드였던 것으로 기억하는데요.
default 부분을 작성한 이유는 모든 CoinGenerator가 코인을 선택하는 방법만 다르지, 결국 코인을 생성하는 전체 로직은 공통될 것이라고 생각했기 때문이에요.

그리고 이렇게 작성을 하는 와중에 "CoinGenerator는 인터페이스가 아니라 추상클래스로 제공되는 것이 맞았겠다" 라는 생각을 하게 되었습니다.
default 메서드로 구현하기 보단 그냥 부모 객체가 제공하는 공통 메서드 인 편이 더 자연스럽겠다고 생각했기 때문이에요.

저 역시 아직 default 메서드를 활용하는 기준은 세우지 못했을 뿐더러, 인터페이스에서의 default 메서드를 사용하는 부분은 많이 부족하다고 생각해요.
혹시 이 부분에서 인사이트를 얻고 싶으시다면 제 의도는 "추상 클래스"의 사용이었으나 시간의 한계로 인터페이스를 추상클래스로 변경하지 못했다는 점을 알아주셨으면 좋겠습니다!!

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);
}
}
Loading