Search
3️⃣

프리코스 3 주차 기능 구현기

태그
프리코스
이전글
다음글

View 만들기

뷰의 경우 입력을 받는 InputView 와 결과를 출력하는 OutputView 를 만들었습니다.
ViewPromt 라는 이름을 가진 Enum 클래스를 생성하여 “구입 금액을 입력해 주세요.” 와 같은 메세지를 관리하였습니다.
WinningType 이라는 이름을 가진 Enum 클래스를 생성하여 “3개 일치 (5,000원) - ” 과 같은 메세지를 관리하였습니다.

Model 만들기

[제약사항의 등장]
Lotto 클래스를 활용하여 기능을 구현하세요.

 기능 FLOW 다시 보기

제가 생각한 기능 플로우는 아래와 같습니다.
사용자는 입력 인터페이스 뷰를 통해 구입 금액, 당첨 번호, 보너스 번호를 입력합니다.
메인 컨트롤러는 입력 데이터 모델에 사용자의 입력을 저장합니다.
메인 컨트롤러는 로또 번호 생성기를 사용하여 사용자가 구매한 로또 번호를 생성합니다.
생성된 번호들과 사용자의 당첨 번호를 당첨 검증기가 검사하여 결과를 결과 저장기에 저장합니다.
메인 컨트롤러는 결과 저장기에서 결과를 가져와 수익률 계산기를 사용하여 수익률을 계산합니다.
마지막으로 결과 출력 뷰를 통해 사용자에게 당첨 통계와 수익률을 보여 줍니다.

그렇다면 여기서 Model 은 어떤걸까?

MVC 패턴 모델의 역할

데이터와 비즈니스 로직을 관리합니다.
일반적으로 데이터베이스, 파일, 메모리 등에 저장된 데이터와 그 데이터를 처리하는 로직을 포함합니다.
모델은 어떤 정보를 표시할 것인지, 어떤 정보를 갱신해야 하는지 등의 규칙을 정의합니다.
모델은 직접적으로 사용자 인터페이스와 상호작용하지 않습니다.
   다른 패턴의 역할도 보고싶다면?

결론과 역할 정리

결론적으로 저는 아래와 같이 모델을 만들어 보았습니다. 그리고 대략적인 역할을 정리했습니다.

LottoGenerator.java

입력으로 받은 값을 이용하여 당첨 로또를 생성합니다.
입력을 받은 값을 이용하여 랜덤 로또를 생성합니다.

Lotto.java (주어진 클래스)

입력으로 받은 값을 이용하여 생성된 랜덤 로또의 값을 저장합니다.

ValueValidator.java

입력값과, 입력값으로 생성된 값들을 검증합니다.

WinningValidator.java

로또들과 당첨 로또의 값을 비교하여 번호가 몇개 당첨이 되었는지 검증합니다.

WinningResult.java

모든 로또들의 당첨 값을 저장합니다.

YieldCalculator.java

당첨 값들을 이용하여 수익률을 계산합니다.

Lotto 클래스는 왜 주었을까?

로또 클래스를 보는 순간 당황했습니다. 이걸 어디에 어떻게 쓰라는걸까? 처음에는 이를 그대로 두고 구현 했지만, 마지막 테스트 단계에서 뭔가 이상함을 감지했습니다. Lotto 테스트가 통과되지 않더군요
그래서 Lotto 클래스와 로또 테스트 코드를 다시한번 찬찬히 뜯어보았습니다.

Lotto.java

public class Lotto { private final List<Integer> numbers; public Lotto(List<Integer> numbers) { validate(numbers); validateDuplicate(numbers); this.numbers = numbers; } private void validate(List<Integer> numbers) { if (numbers.size() != 6) { throw new IllegalArgumentException(); } }
Java
복사

Lotto Test.java

class LottoTest { @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") @Test void createLottoByOverSize() { assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) .isInstanceOf(IllegalArgumentException.class); } @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") @Test void createLottoByDuplicatedNumber() { // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) .isInstanceOf(IllegalArgumentException.class); } // 아래에 추가 테스트 작성 가능 }
Java
복사
아래 createLottoByDuplicatedNumber() 에 해당하는 예외처리 메서드가 없어서 에러가 발생했던 것 이였습니다. 그래서 처음에 랜덤값을 List<Integer> 에 받았었는데, 이를 Lotto 클래스의 numbers 에 저장하는 것으로 코드를 변경하였습니다. 이렇게 하면 랜덤 로또들을 List<List<Integer>> 로 저장 하는 것에서 List<Lotto> 로 바꿀 수 있습니다. 이렇게 개선하면서 요구사항을 충족하고, 테스트도 통과 하였습니다.

수익률에 대하여

일반적으로 당첨이 되지 않았을 경우 수익률은 -100.0% 여야 합니다.
하지만 요구사항을 자세히 보니 아래와 같은 문구가 적혀있었습니다.
수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
Markdown
복사
제 생각과 다른 수익률. 수익률은 마이너스가 나오면 안된다?
일반적으로 수익률은 아래와 같이 계산합니다.
순수익 = 당첨 금액 - 투자금액
수익률(%) = (순수익 / 투자금액) * 100
따라서 이렇게 예제를 계산했을 경우 -37.5% 가 나오지만 결과는 62.5 가 나와야 합니다.
이 숫자는 어떻게 나온것인가?  생각해보니 수익률 보다는 회수율에 더 가깝다는 것을 알게 되었습니다. 이는 수익률과는 조금 다른 개념인데요, 회수율의 개념은 아래와 같습니다.
투자 금액 대비 얼마를 회수 했는지의 비율
회수율(%) = (당첨금액 / 투자금액) * 100
이렇게 회수율로 계산을 하면 정상적으로 문제가 원하는 62.5% 의 값을 얻을 수 있습니다.

Controller 만들기

의존성 주입(Dependency Injection)

컨트롤러는 모델과 뷰를 잇는 역할을 합니다. 이를 위해서 컨트롤러에 모델과 뷰에 해당하는 클래스들을 컨트롤러에 받아와야 합니다. 이는 의존성 주입이라는 개념으로 표현 됩니다. 의존성 주입이란 뭘까요?
의존성 주입은 객체가 사용하는 의존성(다른 객체) 을 외부에서 받아오는 디자인 패턴입니다.
왜 의존성 주입을 사용하나요?
클래스가 다른 클래스를 생성하지 않기 때문에 클래스 사이의 결합도가 낮아집니다.
의존성을 외부에서 주입 받기 때문에 단위 테스트 시 테스트를 용이하게 합니다.
필요한 객체를 주입받기 때문에 코드의 재사용성과 확장성이 향상 됩니다.

LottoController.java

아래는 컨트롤러의 의존성 주입 부분입니다.
public class LottoController { private final InputView inputView; // view private final OutPutView outputView; // view private final ValueValidator valueValidator; // model private final LottoGenerator lottoGenerator; // model private final WinningValidator winningValidator; // model private final WinningResult winningResult; // model private final YieldCalculator yieldCalculator; // model public LottoController() { this.inputView = new InputView(); this.outputView = new OutPutView(); this.valueValidator = new ValueValidator(); this.lottoGenerator = new LottoGenerator(); this.winningValidator = new WinningValidator(); this.winningResult = new WinningResult(); this.yieldCalculator = new YieldCalculator(); } ...
Java
복사

컨트롤러의 역할과 구조

 다시보는 컨트롤러의 역할

사용자의 입력을 처리하고 모델과 뷰 사이의 상호작용을 관리합니다.
사용자의 액션에 반응하여 모델을 업데이트하거나, 모델의 변경 사항에 따라 뷰를 갱신합니다.
컨트롤러는 사용자가 데이터를 어떻게 요청하고 조작하는지에 대한 로직을 캡슐화합니다.

컨트롤러 구조와 역할

모델과 인풋은 서로 상호작용 하지 않고, 컨트롤러를 통해 상호작용을 해야합니다.
inputPurchasedPrice()
인풋뷰로 구매 금액을 입력받습니다.
인풋뷰로 받은 금액을 모델의 검증기로 넘깁니다.
generateLottos()
인풋으로 받은 구매 금액을 이용하여 로또를 몇개 만들지 계산합니다.
계산된 값을 모델의 로또 생성기로 넘깁니다.
아웃풋뷰로 생성된 랜덤 로또를 출력합니다.
generateWinningCondition()
인풋뷰로 당첨 로또 숫자를 받습니다.
인풋뷰로 받은 숫자를 모델의 로또 생성기로 넘깁니다.
inputBonusNumber()
인풋뷰로 보너스 숫자를 받습니다.
인풋뷰로 받은 숫자를 모델의 검증기로 넘깁니다.
generateWinningResult()
생성된 로또들의 당첨 여부를 모델의 당첨 검증기로 검사합니다.
당첨 검증기로 계산된 값을 계산 결과 모델에 저장합니다.
displayFinalResult()
계산 결과 모델에 저장한 값을 회수율을 계산합니다.
계산 결과를 아웃풋뷰에 넘겨 출력합니다.

Input Exception 그리고 재실행

이전 과제에서는 게임의 시작부분, 진행부분, 마무리 부분으로 나누어 컨트롤러를 구현했습니다. 그리고 각 단계에서 예외가 발생하면 예외가 터지며 게임이 종료되어야 했죠. 하지만 이번 과제에서는 방식이 약간 달랐습니다.
요구사항을 자세히보면 아래와 같은 문구를 발견할수 있습니다.
사용자가 잘못된 값을 입력할 경우 IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
Markdown
복사
오직 로또 당첨 결과가 나온 후에만 종료를 할 수 있습니다.  보통 예외가 던져지면 메세지를 출력하고 프로그램은 종료 됩니다. 하지만 우리는 예외가 발생하면 그 부분부터 입력을 다시 받아야 합니다. 저는 이를 아래와 같이 해결하였습니다.

while 과 try-catch 문 그리고 예외처리

이 세가지는 자세히보면 연결되어 있습니다. 구입 금액을 입력받을 때를 생각해 봅시다.
구입금액이 1000 원 이하이거나 기타 등등의 이유로 IllegalArgumentException 을 발생할 수 있습니다. 이 특정 에러를 catch 하고, 메세지를 출력하기 위해 try-catch 문을 사용합니다.
try { inputPurchasedPrice = inputView.promptForPurchasedPrice(); valueValidator.validatePurchasedPrice(inputPurchasedPrice); } catch (IllegalArgumentException e){ System.out.println(e.getMessage()); }
Java
복사
하지만 이렇게 하면 이전과 같이 예외를 catch 하고 종료를 하게 될겁니다. 이를 해결하기 위해 반복문을 사용해야 하는데 조건은 어떻게 하는게 좋을까요?
저는 검증에서 boolean 형식의 flag 를 리턴하고, 이를 조건으로 걸었습니다.
boolean isValidated = false; String inputPurchasedPrice = ""; while(!isValidated) { try { inputPurchasedPrice = inputView.promptForPurchasedPrice(); isValidated = valueValidator.validatePurchasedPrice(inputPurchasedPrice); } catch (IllegalArgumentException e){ System.out.println(e.getMessage()); } }
Java
복사
이렇게 하면 검증을 통과했을 시 isValidated 값이 true 로 바뀌면서 프로그램은 종료됩니다. 만약 검증에서 에러가 발생하면 계속 isValidated false 상태이기 때문에 입력값을 다시 받게 되죠.

 번외 많은 예외를 컨트롤러에 어떻게 불러올까?

Model 에 속하는 클래스인 ValueValidator.java 에는 입력값을 검증하는 메서드들이 존재합니다. 이 메서드에서 리턴된 값을 컨트롤러의 조건으로 사용하였는데요, 예외처리를 할수 있는 부분은 많지만 이 메서드를 모두 가져온다면 컨트롤러의 메서드가 한없이 길어질 거에요. 하지만 우리는 아래 요구사항을 지켜야 합니다.
함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
Markdown
복사
그래서 저는 아래와 같이 관련된 예외를 한데 모아서 처리했습니다.
public class ValueValidator { // 구입 금액 입력값 검증 public boolean validatePurchasedPrice(String inputPurchasedPrice) { validatePurchasedPriceValue(inputPurchasedPrice); validatePurchasedPriceValueRange(inputPurchasedPrice); return true; } private void validatePurchasedPriceValue(String inputPurchasedPrice) { if (!inputPurchasedPrice.matches("\\d+")) { throw new IllegalArgumentException(ErrorMessage.ILLEGAL_NUMBER_FORMAT.getMessage()); } } private void validatePurchasedPriceValueRange(String inputPurchasedPrice) { int purchasedPrice = Integer.parseInt(inputPurchasedPrice); if (purchasedPrice < 1000) { throw new IllegalArgumentException(ErrorMessage.INSUFFICIENT_PURCHASE_AMOUNT.getMessage()); } } ...
Java
복사
이렇게 구입 금액에 관련된 검증을 하는 메서드를 pulic 메서드에 모았습니다. 그리고 예외가 처리하면 위의 catch 문에서 잡히기 때문에 return 값은 true 로 두었습니다. 검증 메서드들이 다 통과되면 문제가 없기 때문이죠. 또한 이렇게 작성하면 이전처럼 private 코드를 테스트할 필요가 없습니다. public 메서드를 통해 간접적으로 할 수 있기 때문이죠. (이 내용은 테스트 편에서 더 자세히 작성하겠습니다.)