Skip to content

[1단계 - 콘솔 기반 로또 게임] 에프이(박철민) 미션 제출합니다.#293

Merged
liswktjs merged 60 commits intowoowacourse:chysisfrom
chysis:step1
Feb 25, 2024
Merged

[1단계 - 콘솔 기반 로또 게임] 에프이(박철민) 미션 제출합니다.#293
liswktjs merged 60 commits intowoowacourse:chysisfrom
chysis:step1

Conversation

@chysis
Copy link
Member

@chysis chysis commented Feb 22, 2024

안녕하세요! 에프이입니다 :)

앱 실행 방법

  • 이전 미션과 다르게 앱 시작 방법이 약간 수정되어 먼저 알려드려요!
  • 앱의 진입점이 되는 파일은 src/step1-index.js입니다.
  • npm run start-step1 커맨드로 앱을 실행할 수 있습니다.

구현 사항

설계 방법

  • TDD를 이용해서 작은 단위 테스트부터 작성하고, 해당 테스트를 통과하도록 production 코드를 작성한 뒤 리팩토링하는 것을 반복했습니다.
  • TDD로 미션을 수행하니 초반에 구조 설계를 상당 부분 진행한 뒤에 코드를 작성해야 한다는 점이 어려웠습니다. 하지만 테스트를 위해 production 코드를 수정하는 일이 줄어들고, 구조의 안정성이 높아진 것 같습니다.
  • 기존에는 프로그램의 메인 흐름을 먼저 설계한 뒤 작은 부분을 구현해 나갔다면, 이번에는 작은 단위를 먼저 구현한 뒤에 전체 흐름을 연결지었다는 점이 가장 큰 개발 방식의 차이입니다. top-down approach에서 bottom-up approach로 바뀐 느낌입니다.
  • 익숙하지 않은 개발 방법이라 많은 시행착오를 거쳤고, 그만큼 리팩토링에 많은 시간을 투자하지 못한 점이 아쉽습니다.😢 피드백 이후 프로그램 구조에 관한 고민과 수정을 계속 이어가려 해요!

프로그램 구조

  • LotteryMachine

    • 사용자가 입력한 구매 금액만큼 로또 번호 배열 생성
    • 만들어진 로또 번호 배열을 이용해서 Lotto 객체 배열을 생성
  • Lotto

    • 사용자가 구매한 로또 1장의 번호 6개를 배열로 받아서 오름차순 정렬
    • 사용자가 입력한 당첨 번호와 보너스 번호를 인자로 받아서 당첨 번호에서 몇 개 일치하는지, 보너스 번호와 일치 여부를 객체로 반환
  • index

    • 프로그램의 메인 진입점이고, 구매 금액을 입력받음
    • LottoController 인스턴스를 생성하고 구매 금액을 인자로 넘김
    • LottoController 이후 재시작 여부를 입력받아서 처리
  • LottoController

    • 로또 생성부터 비교, 당첨 결과 출력까지의 흐름을 담당
  • LottoService

    • Lotto로부터 반환받은 당첨 번호, 보너스 번호 비교 객체로 등수 계산
    • 사용자가 구매한 각 로또마다 계산한 등수를 순회하면서 등수 별 당첨된 로또 개수를 카운팅해서 객체로 반환
    • 등수 별 당첨된 로또 개수를 저장한 객체를 순회하면서 총 당첨금을 계산하고, 수익률을 계산
  • 프로그램 종료 이후 재시작 여부를 입력받을 때, 대소문자 모두 인식하도록 toLowerCase()를 사용했습니다.

고민했던 점

  • 수익률(profit)의 타입을 string으로 결정한 이유
  1. 수익률을 구한 뒤 단순히 출력만 하면 되기 때문
  2. 수익률 출력에 대한 요구 사항(출력해야 하는 소수점 자릿수)이 바꼈을 때 더 유연한 대처가 가능

예를 들어, 0으로 끝나는 소수 부분을 표현하기에 float보다 string이 적합하다고 생각했습니다. (parseFloat의 경우 62.500 -> 62.5를 반환하기 때문)

  • 객체가 스스로 일하게 만들어야 한다고 생각했지만, 얼마나 많은 일을 해야 하는지를 많이 고민했습니다. 그리고 그 결과 적절한 지점에서 타협했습니다. 예를 들어, 사용자가 구매한 로또 번호(Lotto 객체)가 당첨 번호와 몇 개 일치하는지 구하기 위해서 Lotto 객체의 getter를 이용해 서비스 로직에서 이를 수행할 수도 있겠지만, Lotto 객체 내부의 메서드는 당첨 번호와 보너스 번호를 인자로 받으면 몇 개 일치하는지를 객체로 반환해줍니다. 이렇듯 entity가 단순히 값을 저장하고 반환하는 일만 하는 것이 아닌 스스로 일을 할 수 있도록 설계했습니다. 현재 Lotto가 적절한 수준의 일에 관여하고 있는 것이 맞는지 리뷰어님의 생각이 궁금해요 :)
  • 당첨 번호와 보너스 번호는 서로 연관도가 매우 높습니다. 유효성 검사를 할 때 보너스 번호는 당첨 번호를 알아야 하고, 로또 별로 등수를 판단하는 데도 당첨 번호와 보너스 번호 모두가 필요합니다. 그래서 이 두 종류의 번호를 어떻게 관리할지 페어와 긴 시간 의논했습니다.
  • 이전 미션에서는 view에서는 입력만 받아서 entity 인스턴스를 생성하고, entity의 생성자에서 유효성 검사를 하도록 했습니다. 하지만 이번 미션에서는 view에서 유효성 검사까지 거쳤는데요, 이 부분은 사람마다 견해가 정말 다른 것 같습니다. 프로그램 구조를 먼저 설계하고 어떤 방식이 더 효율적일지 판단하는 것이 옳은가요? 리뷰어님의 견해가 궁금합니다 :)
  • 프로그램의 시작점(index)에서 바로 로직을 실행시키는 것과, mainController를 두고 index에서는 mainController만 호출, mainController에서 로직을 실행시키는 것 중에 어떤 것이 더 좋을지 의논해보았습니다. 현재 코드는 index에서 바로 시작하는데(index -> controller 또는 서비스 로직), 프로그램이 동작하는 데 있어 파일의 depth가 줄어든다는 이점이 있습니다. 그리고 controller를 따로 두고 이곳에서 시작하면 depth는 늘어나지만(index -> mainController -> 다른 controller 또는 서비스 로직) index가 단순해지고 프로그램의 각 요소가 구조적으로 확실히 분리되어 보인다는 점이 좋았습니다. 프로그램의 시작점을 어디에 두느냐의 차이가 단순히 스타일의 차이인지, 혹은 일반적으로 따르는 컨벤션이 있는지 궁금합니다!

이번 미션은 스스로도 부족한 부분이 많다고 생각합니다. 감사합니다😊

Copy link

@liswktjs liswktjs left a comment

Choose a reason for hiding this comment

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

안녕하세요 에프이
이번에 리뷰를 맡게 된 샐리라고 합니다 👋

커밋 로그를 살펴보았을 때 테스트코드와 기능을 같이 커밋 해주셔서 이해가 빨랐습니다 👍

tdd를 진행하면서 많은 고민을 해주신 것 같아요
고민을 많이 해주신 만큼 읽기 좋은 구조가 만들어 졌다고 생각합니다
이와 관련해서 red-green-refactor를 생각해주시면 좋을 것 같아요
(아마 수업시간에 배우신 것 같긴 하지만 혹시 몰라 남겨봅니다!)

그럼 남은 미션도 파이팅입니다 💪

Comment on lines +29 to +39
async readPurchaseAmount() {
try {
const purchaseAmountInput = await Private.readPurchaseAmount();
const purchaseAmount = purchaseAmountInput.trim();
purchaseAmountValidator.validate(purchaseAmount);
return purchaseAmount;
} catch (error) {
OutputView.print(error.message);
return this.readPurchaseAmount();
}
},

Choose a reason for hiding this comment

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

input에 대한 유효성 검사를 이곳에서 해주신 것 관련해서 질문을 주셨는데요
input에서 유효성 검사를 해서 넘겨주는 지금 방식이 현재 작성해준 코드 구조상 더 깔끔하다고 느껴집니다!

지금은 TDD개발 방법론을 하고 있다 보니 우선 요구사항에 맞는 함수를 개발한 뒤에 이걸 어떻게 넣을지 구조를 고민하는 단 계를 리팩토링 단계에 넣는 방식으로 진행하면 좋을 것 같아요

추가로 경험치가 쌓일 수록 구조를 파악하거나 짜는 능력이 늘어난다고 생각해요
조급하게 생각하실 필요 없이 천천히 경험치를 쌓으면 좋을 것 같습니다 😊
TDD관련 도움이 되는 글이 하나 있어서 공유드립니다!

Copy link
Member Author

@chysis chysis Feb 23, 2024

Choose a reason for hiding this comment

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

저도 개발 과정에서 모든 input들을 단일 객체로 만들고 있지 않기 때문에 InputView에서 유효성 검사를 하는 편이 조금 더 합리적이라고 생각이 들었어요. input이 저마다 단일 객체가 되었던 지난 미션에서는 해당 객체의 생성자에서 유효성 검사를 하는 것이 더 합리적이라 생각했었어요. 결국 이것도 코드 구조마다 다를 것 같은데요, 구조에 더 적합한 유효성 검사의 위치를 판단하는 저만의 기준을 갖는 방향으로 공부를 하는 것이 좋을까요? 샐리의 의견이 궁금합니다!

좋은 글 감사합니다😊

Choose a reason for hiding this comment

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

구조를 정해서 생각하는 것 보다 어떤 요구 조건, 어떤 기능을 구현하는지에 따라 달라질 것 같아요!
기준을 지금 당장 정하기 보다는 우테코 생활을 하면서 좀 더 경험을 쌓으면 좋을 것 같아요 👍

async readWinningNumbers() {
try {
const winningNumbersInput = await Private.readWinningNumbers();
const winningNumbers = winningNumbersInput.split(CONFIG.SEPARATOR).map(number => parseInt(number.trim(), 10));

Choose a reason for hiding this comment

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

이 한 라인에 연산이 여러번 일어나고 있는데요
10라인을 넘어가면 안 된다는 컨벤션에 따라서 한 번 함수로 분리해주는 것도 좋을 것 같습니다

Copy link
Member Author

@chysis chysis Feb 23, 2024

Choose a reason for hiding this comment

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

async readWinningNumbers() {
    try {
      const winningNumbersInput = await Private.readWinningNumbers();
      const winningNumbers = winningNumbersInput.split(CONFIG.SEPARATOR).map(number => parseInt(number.trim(), 10));
      winningNumbersValidator.validate(winningNumbers);
      return winningNumbers;
    } catch (error) {
      OutputView.print(error.message);
      return this.readWinningNumbers();
    }
  },

해당 메서드 전체입니다. 궁금한 부분이 있습니다.

  1. 메서드의 길이를 따질 때 저는 가장 바깥 중괄호를 제외한 내부의 body 라인만 세는 것이 맞다고 생각해요. 그래서 44번 줄이 두 줄로 분리가 되어도 총 10줄로 컨벤션을 만족하고 있다고 생각했는데요, 어떻게 생각하시는지 궁금해요.

  2. 연산이 여러 번 일어나고 있는 것이 dot notation을 2번 사용해서 그런 것인지, 혹은 split과 map 이외에도 콜백 함수로 parseInt, trim이 포함되어서 그렇게 보신 건지 궁금합니다! 개인적으로 2번의 메서드 체이닝 정도는 괜찮다고 생각하는데요, 이 부분에서도 혹시 샐리는 어떻게 생각하시는지 궁금해요.

유효성 검사와 데이터를 가공하는 순서에 오류가 있어서 이를 고치면서 자연스럽게 라인이 2개로 나뉘어졌습니다. 😊

Choose a reason for hiding this comment

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

  1. 기존에 구현해주신 코드들은 10줄이긴 맞지만 로직들 사이에 빈 라인이 없어서 가독성이 떨어진다고 생각했습니다!
      const winningNumbers = winningNumbersInput.split(CONFIG.SEPARATOR).map(number => parseInt(number.trim(), 10));
  • 경우에 따라 다르겠지만 현재 winningNumbers의 경우에는 분리가 필요하다고 생각합니다
  • 왜냐하면 기획에 따라 winningNumbers를 만드는 기준이 바뀔 수 있는데 그럴때 대응하기가 힘들어집니다

Comment on lines +16 to +21
const lotto = [];
while (lotto.length < CONFIG.LOTTO_RANK_LENGTH) {
const randomNumber = Math.ceil(Math.random() * CONFIG.MAX_LOTTO_NUMBER);
if (lotto.includes(randomNumber)) continue;
lotto.push(randomNumber);
}

Choose a reason for hiding this comment

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

while문을 사용하면 자칫하면 무한 루프에 빠질 수 있기 때문에 프로덕트 코드에 사용하기에는 위험한 측면이 있습니다
지금 부터 while보다는 array method를 사용해서 개발하는 습관을 들이면 좋을 것 같아요!

Copy link
Member Author

@chysis chysis Feb 23, 2024

Choose a reason for hiding this comment

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

array method라는 힌트를 주셔서 프리코스에서 사용했던 util 함수를 다시 살펴볼 수 있었어요! random 값을 이용해서 1~45가 들어있는 배열을 무작위 정렬한 뒤 처음 6개의 값을 slice해서 중복되지 않는 6개의 숫자를 뽑을 수 있을 것 같습니다. 다만 매개변수 개수 요구 사항 때문에 숫자 범위의 시작과 끝, 그리고 뽑을 숫자의 개수를 모두 인자로 받을 수 없어서 util 함수로 분리하지는 못했습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

while문이 충분히 개발자의 실수로, 혹은 개발 과정에서 불완전한 코드로 인해 무한 루프가 발생할 수 있을 것 같네요. 현업에서는 while문을 최대한 사용하지 않는 방향으로 개발하는지 궁금합니다!

Choose a reason for hiding this comment

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

현업에서 while문을 최대한 사용하지 않습니다!
왜냐하면 프론트가 사용하는 값은 서버에 따라서도 변동적이고 유저가 입력하는 값에 따라서도 달라지기 때문에 무한루프에 빠질 위험도 있습니다
추가로, 함수형프로그래밍에 대해서 더 공부해보시면 좋을 것 같습니다

import LotteryMachine from '../src/domain/services/LotteryMachine';

describe('로또 발행 테스트', () => {
test('구입 금액에 해당하는 만큼 로또를 발행한다.', () => {

Choose a reason for hiding this comment

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

기능 명세를 작성한다는 접근으로
테스트 명세를 더 자세히 적어주시면 좋을 것 같아요
1000원 단위로 나누어 떨어지면서 로또를 발행해주는 것이 핵심 로직이니 이 설명이 들어가면 좋을 것 같습니다

@chysis
Copy link
Member Author

chysis commented Feb 24, 2024

안녕하세요 샐리! 피드백 내용을 바탕으로 리팩토링 및 버그 수정 과정을 거쳤습니다 :)

수정한 부분

LottoController의 run 함수 분리

  • 이전에 가독성이 떨어지는 문제를 해결할 수 있도록 (값 계산 + 값 출력)을 하나의 함수로 묶어서 분리했습니다.
  • controller에서 순차적으로 어떤 일을 수행하는지 알기 쉬워졌습니다.

while문 대체

  • 무한루프의 위험성이 있는 while문을 대체해서 array method로 중복되지 않는 로또 번호 6개를 뽑는 함수를 구현했습니다. (LotteryMachine.js)

상수의 관심사 별 분리

  • 하나의 객체에 프로그램에서 사용하는 모든 상수가 들어있어 확인하기 힘들다는 문제를 해결했습니다.
  • 크게 로또 자체가 갖는 특성, 당첨 결과와 관련된 상수, 재시작 결과와 관련된 상수, 프로그램 형식과 관련된 상수로 분리했습니다.

Message를 더 상세하게

  • 이전에는 구현 사항에 예시로 나온 것과 똑같이 메시지를 구성했는데요, 더 나은 사용자 경험을 위해 값을 입력할 때 제한 사항을 메시지에 추가했습니다.

InputView 리팩토링

  • Private 객체는 지금은 사용하지 않는데요, 불필요한 부분은 제거했습니다.
  • string를 인자로 받아서 해당 메시지를 띄우고 값을 입력받는, 재사용 가능한 함수를 만들었습니다.

test 문구 보완

  • 테스트 결과만으로 기능을 파악할 수 있도록 테스트 명세를 더 자세히 기술했습니다.

유효성 검사 보완

  • for loop를 이용해 당첨 번호 각각을 검사하던 로직을 forEach를 사용해서 구현했습니다.
  • 기존에는 입력받은 당첨 번호를 split하고 parseInt()까지 적용한 뒤에 유효성 검사를 했는데, 이런 경우 숫자가 아닌 문자 6개를 입력하면 parseInt에 의해 NaN 6개가 저장됩니다. Set을 생성하면 결국 NaN이 한 개만 저장되기 때문에 중복되는 숫자를 입력한다고 판단하게 됩니다. 이것은 적절한 로직이 아니기 때문에 먼저 split()만 적용한 뒤에 유효성 검사를 하고, 유효하다면 parseInt()를 적용하는 것으로 순서를 바꾸었습니다.

앞서 코멘트 주신 부분에 추가적으로 궁금한 부분을 댓글로 달았습니다. 확인 부탁드려요!
감사합니다 :)

Copy link

@liswktjs liswktjs left a comment

Choose a reason for hiding this comment

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

안녕하세요! 에프이
좋은 주말보내셨나요?
pr 코멘트 남겨 주신거 확인했습니다!
추가적으로 코멘트 남겼으니 2단계 반영하면서 같이 반영해주세요 👍

로또 미션 2단계도 파이팅입니다 🎱

Comment on lines +18 to +19
this.#processLottoResult(matchedResultList);
this.#processProfit(matchedResultList);

Choose a reason for hiding this comment

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

process라는 네이밍으로는 함수가 어떤 역할을 하는지 잘 와닿지 않는 것 같아요!
한번 수정해보시면 어떨까요?

Copy link
Member Author

@chysis chysis Mar 11, 2024

Choose a reason for hiding this comment

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

process 함수에는 각각 값을 구하고, 출력하는 로직이 들어가 있습니다. 그 처리하는 과정에 초점을 맞추다 보니 process라는 이름을 앞에 붙이게 되었습니다. showPurchaseResult 함수처럼 일관성 있게 showLottoResult, showProfit으로 설정하는 것도 괜찮을 것 같아서 show로 수정했습니다.

p.s. show는 hide와 반대되는 이름으로 많이 쓰인다고 하는데, 콘솔 기반의 step1에서는 이를 고려하지 않아도 될 것 같습니다. 어떻게 생각하시나요?

Choose a reason for hiding this comment

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

현재 1단계가 콘솔 기반의 구현이긴 하지만
process~관련 함수들이 현재 OutputView 로직들이 구현되어 있기 때문에 show도 적절하다고 생각합니다 👍

#pickUniqueLottoNumbers() {
const numberList = Array.from({ length: CONFIG_LOTTO.MAX_LOTTO_NUMBER }, (_, i) => i + 1);
numberList.sort(() => Math.random() - 0.5);
return numberList.slice(0, CONFIG_LOTTO.LOTTO_LENGTH);
Copy link

@liswktjs liswktjs Feb 25, 2024

Choose a reason for hiding this comment

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

상수로 한번 로직을 분리해서 진행해보면 어떨까요?

Copy link
Member Author

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.

아하 sort 쪽을 한 번 const 변수로 할당해주고 진행하면 좋을 것 같다는 의견이였습니다!
현재 numberList에 접근해서 매번 바꿔주고 있는데 이를 한 번 변수 할당을 통해 끊어주고 진행하면 좋을 것 같습니다

Comment on lines +30 to 33
let winningNumbers = winningNumbersInput.split(CONFIG_FORMAT.SEPARATOR);
winningNumbersValidator.validate(winningNumbers);
winningNumbers = winningNumbers.map(number => parseInt(number.trim(), 10));
return winningNumbers;

Choose a reason for hiding this comment

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

배열을 let으로 선언할 필요가 있을까요?
재할당 할 필요 없이 바로 return해도 무방할 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

배열을 const로 선언하고, map()를 적용한 결과를 바로 return하는 것이 더 깔끔할 것 같네요!
이 부분은 수정하겠습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants