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

[정찬욱] 자동차 경주 미션 Step2 #11

Open
wants to merge 41 commits into
base: seeyoujeong
Choose a base branch
from

Conversation

seeyoujeong
Copy link

@seeyoujeong seeyoujeong commented Jun 30, 2024

추가 요구사항 구현 목록

  • 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 사용자가 잘못된 입력 값을 작성한 경우 에러 메시지를 보여주고, 다시 입력할 수 있게 한다.
  • 경주의 주체 목록 중 원하는 주체를 선택할 수 있다. (Car 클래스 삭제)
  • 경주의 규칙을 설정할 수 있다. (임시)

미션을 수행하면서 어려웠던 점

  • 구현 세부 사항의 범위를 정하기
    지금 코드는 main에서 RaceEntry, RaceScoreboard, Race 클래스의 인터페이스를 알아야 하는데 Race 클래스는 내부로 숨기는게 좋을지.. 잘 모르겠네요.
  • 입력 출력 관련 테스트 코드 작성
    mock으로 작성하는 방법에 익숙하지 않아 어려움을 겪고 있어요..ㅎㅎ (현재진행중)

리뷰 받고 싶은 부분

  • 경주의 규칙 설정
    현재 경주의 규칙은 Race 클래스의 공개 메서드인 start의 인수로 넘겨줘서 정할 수 있습니다. 이렇게 넘겨주는게 좋을지, 아니면 RaceEntry 클래스를 생성할때 인수로 넘겨줄지... 명확한 기준이 보이지않네요.
  • 에러 처리
    InputManager 클래스에 retryScan 메서드를 통해 에러를 받아 에러 메시지를 보여주고 다시 입력할 수 있게 재귀방식으로 구현했습니다. try...catch 문 내부에서 발생한 에러만 잡을 수 있다보니 콜백을 받는 형식이 되어버렸네요. 편의성이 없는건지.. 궁금하네요.
  • 유효성 검사
    유효성 검사 관련 클래스를 만들어봤습니다. 가독성은 괜찮을까요..? 클래스로 작성한 이유는 통일성을 위해서(?) 입니다. ㅎㅎ

궁금한 점

  • 코드를 설계할때 어떻게 하고 있으신가요? 트리 구조를 그리시나요? 아니면 머리로만?

내가 생각하는 클린코드의 원칙 한가지

하나의 일을 하자!


고민을 하다보니 시간만 흐르고 구현을 많이 못했지만 더이상 지체하면 제가 불편해서 일단 올려봅니다...

@seeyoujeong seeyoujeong requested a review from JunilHwang June 30, 2024 00:32
@seeyoujeong seeyoujeong self-assigned this Jun 30, 2024
@seeyoujeong seeyoujeong changed the title [정찬욱] 자동차 경주 미션 Step2 #1 [정찬욱] 자동차 경주 미션 Step2 Jun 30, 2024
Copy link

@suyeon1218 suyeon1218 left a comment

Choose a reason for hiding this comment

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

안녕하세요 찬욱님!! 클린코드 2주차도 고생 많으셨습니다...!! 비록 이번주 코어타임은 취소했지만 공부는 틈틈히 하고 있답니다..ㅎㅎ

리뷰 받고 싶은 부분

현재 경주의 규칙은 Race 클래스의 공개 메서드인 start의 인수로 넘겨줘서 정할 수 있습니다. 이렇게 넘겨주는게 좋을지, 아니면 RaceEntry 클래스를 생성할때 인수로 넘겨줄지... 명확한 기준이 보이지않네요.

저는 개인적으로 두 방법 다 장단점이 있다고 생각해요...! start 가 호출될 때마다 규칙을 바꾸고 싶다면 전자의 방법이 좋고, 규칙을 인스턴스마다 다르게 설정하고 싶다면 후자가 좋은 것 같습니다. 다만 지금은 RaceEntry 클래스를 생성할 때 Race 도 같이 생성하고 있어서 후자의 경우를 채택하게 되면 룰이 어디서 생성되어 관리되는지 main 파일에선 유추하기 힘들 것 같다는 생각도 들어요.

InputManager 클래스에 retryScan 메서드를 통해 에러를 받아 에러 메시지를 보여주고 다시 입력할 수 있게 재귀방식으로 구현했습니다. try...catch 문 내부에서 발생한 에러만 잡을 수 있다보니 콜백을 받는 형식이 되어버렸네요. 편의성이 없는건지.. 궁금하네요.

저는 좋다고 생각합니다! 시도 자체에서도 error 블록에서 error 를 확실히 잡아줄 수 있다는 점에서 유용하고 편리한 것 같아요!

유효성 검사
유효성 검사 관련 클래스를 만들어봤습니다. 가독성은 괜찮을까요..? 클래스로 작성한 이유는 통일성을 위해서(?) 입니다. ㅎㅎ

마찬가지로 좋았던 시도라고 생각합니다. 각각의 메서드들도 직관적이고 공통적인 부분을 잘 묶어서 처리하신 것 같아요 ㅎㅎ!! 저도 다음번엔 validator 를 따로 만들어봐야할까봐요...

궁금한 점

코드를 설계할때 어떻게 하고 있으신가요? 트리 구조를 그리시나요? 아니면 머리로만?

머리로만 하는 건 아무래도 전 불가능한 것 같아요... 저는 멘토님께서 '지금은 콘솔로 입출력을 다루고 있지만 웹에선 어떻게 동작해볼지 생각해보라' 는 조언에 따라서 React였다면 제가 어떻게 코드를 작성했을까 생각해보는 편이에요! 어떻게 관리해야 사용하는 쪽에서 편리할지를 먼저 생각하기 위해 코드로 작성한 다음 코드 구조를 트리형태로 그려보는 편입니다!

기타 리뷰

  • input 유틸 함수에서 processFn 를 넣어서 후처리를 해주는 부분이 좋아보였어요. 그런데 잘 쓰면 적절한 후처리를 할 수 있어 좋지만... 이 부분을 너무 많이 숨겨버리면 후처리에 대한 부분을 상위 컴포넌트(?)에선 알 수 없어서 왜 이런 데이터가 반환되는지 모를 수 있다는 생각이 들어요!
  • 경주하는 사람들을 선택할 수 있게 한 것도 좋은 시도였습니다...! 저는 필수 요구사항만 구현해서 도전적인 시도는 많이 못해보았네요...ㅠㅠ

@@ -0,0 +1,51 @@
class Validator {
static throwErrorWithCondition(condition, errorMessage) {
if (condition) {

Choose a reason for hiding this comment

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

condition 이 true 일때? 아니면 값이 있을때? 언제 해당 if 문에 걸리게 되나요?

Copy link
Author

Choose a reason for hiding this comment

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

아 그러네요... truthy, falsy도 고려해야겠네요! 감사합니다~

Comment on lines +16 to +18
static function(value, errorMessage) {
Validator.type(value, "function", errorMessage);
}

Choose a reason for hiding this comment

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

function 은 예약어인데 메서드 이름으로 사용하면 에러가 안 나나요...?!

Copy link
Author

Choose a reason for hiding this comment

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

넵넵! 에러 안나서 이름으로 사용하고 있지만... 다시 생각해보니 fn이나 func으로 바꾸는게 덜 헷갈리겠네요!

try {
const inputValue = await this.scan(query);

return processFn ? processFn(inputValue) : inputValue;

Choose a reason for hiding this comment

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

processFn 이 함수가 아닌 경우엔 에러를 발생시키지 않나요?

Copy link
Author

Choose a reason for hiding this comment

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

오 맞네요.. 에러가 나겠군요! 유효성 검사를 추가해야겠어요!

Comment on lines +22 to +25
return await this.retryScan(
`${error.message} 다시 입력해주세요.\n`,
processFn
);

Choose a reason for hiding this comment

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

개인적으로 try-catch 를 사용해서 재귀적으로 에러를 처리하는 부분 아이디어 넘 좋은 거 같아요!

Comment on lines +5 to +9
await raceEntry.selectEntityType();
const racers = await raceEntry.registerRacers();
const race = await raceEntry.setRaceLaps();
race.start(racers);

Choose a reason for hiding this comment

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

setRaceLaps 나 resigsterRacers 메서드는 비동기적으로 main 에서 값을 받아오는데 selectEntityTypemain 에서 아무런 값도 안 받아온다는 데서 일관성을 해칠 우려가 있다는 생각이 들어요...!

Copy link
Author

Choose a reason for hiding this comment

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

저도 약간 거슬렸던 부분이긴 했어요... 고민을 더 해봐야겠어요

import Racer from "./Racer.js";

class RaceEntry {
static #SEPARATOR = ",";

Choose a reason for hiding this comment

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

staticprivate 연산자를 같이 쓴 이유가 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

같이 쓴 의도는 메모리 낭비를 줄이고(...사실 큰 차이는 없지만) 외부에서 접근 못하게 하기위한! 입니다.

Comment on lines +25 to +32
const racers = await inputManager.retryScan(
`경주할 ${this.#entityType} 이름을 입력하세요(이름은 쉼표(${
RaceEntry.#SEPARATOR
})를 기준으로 구분).\n`,
(inputValue) => {
const racerNameList = inputValue.split(RaceEntry.#SEPARATOR);

return racerNameList.map((name) => new Racer(name.trim()));

Choose a reason for hiding this comment

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

결과적으론 racers 에 Racer 의 인스턴스가 담기게 되는 것 같은데 main 파일에서 racers 변수를 읽을 땐 인스턴스가 담기는 줄 몰랐어요...! inputManager 가 간단한 후처리를 해주는 게 아니라면 후처리를 숨기는 게 오히려 혼란스럽게 만들 수 있을 것 같아요...!

Copy link
Author

Choose a reason for hiding this comment

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

그런가요..? registerRacers 라는 메서드명이 오히려 혼랍스럽게 만드나보군요. 후처리를 main에서 하는게 좋을까요?

Choose a reason for hiding this comment

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

input 을 받는 부분 / validation 을 하는 부분 / input 가공하기

이 세가지를 inputManager.retryScan 콜백에서 한번에 해주니까 매우 간편하긴 한데, 분리하는 방향도 고려하시면 좋을 것 같습니다 !
간단한 문자열 가공이면 괜찮지만 Racer 객체를 생성하는 로직이 들어있어서 저로서는 이해가 살짝 어려웠어요 🤔
추가적으로, Racer 생성자 안에서 에러가 발생하면 어떻게 될까 ? 라는 부분도 있습니다

Choose a reason for hiding this comment

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

const racerNames = await inputManager.retryScan(.... , () => { input trim 까지만 해주기! }
const racers = racerAnswers.map(name => new Racer(name))

이런 식으로 !

Copy link
Author

Choose a reason for hiding this comment

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

retryScan 안에서 Racer를 생성해야 에러가 발생하는걸 retryScan에서 잡아줘서 에러 메시지를 보여주면서 다시 입력할 수 있게 구현돼있어요!

Choose a reason for hiding this comment

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

RaceEntry 는 input 과 output 을 관리하는 동시에 인스턴스 (model) 를 생성해서 반환하기 때문에 완전히 도메인에 속해있긴 힘든 친구인것 같아요...! 찬욱님께 도메인 로직이란 어떤 의미인가요...?!

Copy link
Author

Choose a reason for hiding this comment

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

도메인에 속한 컨트롤러라고 생각해서 도메인에 같이 넣었답니다 ㅎㅎ

Comment on lines +38 to 39
#progressRacePerLap(rule) {
let recordPerLap = [];

Choose a reason for hiding this comment

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

let 으로 선언한 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

let을 쓴 이유는 현재 배열에 속한 값이 바뀔 수 도 있다는 것을 명시적으로 나타내기위해서 입니다!


describe("RaceEntry 클래스 테스트", () => {
test("올바른 유형을 선택하지 않으면 오류가 발생한다.", async () => {
retryScanMock("5");

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.

테스트 코드가 아니었다면 유형이 늘어날 경우를 대비해서 상수로 빼거나 했을 것 같습니다.
하지만 테스트 코드이기 때문에, 가장 예외가 없고 확실한 방법인 하드 코딩을 선택했습니다!

Copy link

@chasj0326 chasj0326 left a comment

Choose a reason for hiding this comment

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

찬욱님은 항상 제가 방심하는 부분까지 꼼꼼하게 처리하셔서 놀라울 뿐입니다 ! 저에게 꼼꼼함을 0.3퍼라도 넘겨주세요 👊 (인스턴스종속/클래스종속 따져서 Static 붙인거 + 외부 사용메소드 분리해서 private 철저히 붙인거 등 등 . .. . )

경주 규칙 설정

리뷰에 남겼습니다 :)

InputManager 에 대해

inputManager 메소드의 콜백에 너무 많은 일을 넣은 느낌입니다. useQuery select 에서 setState 해주는 느낌이라 해야하나 ... 콜백에 넣은 일을 처리하자! 가 아닌 , input 관련 일만 해주고, input 관련 에러만 발생시키자 ! 라는 방향으로 가면 고민이 좀 덜어지실 것 같네요 (아마도.....)

유효성 검사

저는 잘 읽혔어요 ! validation 행위 자체를 여러 종류로 분류하고 추상화한 느낌이라, 좋은 아이디어라는 생각이 들었습니다 👍

코드를 설계할 ㄸ ㅐ ..

코드를 설계할 때, 트리구조 까지는 그리지 않고 ... 전체 로직을 어떻게 나눌 수 있고 각각 무슨 멤버변수가 있으면 좋겠는지, 메인에서 사용될 때 내가 어떻게 작성하고 싶은지 ? 저는 결과와 인터페이스를 먼저 생각하는 편이에요 !
완성된 코드의 모습을 먼저 상상하고, 그 안을 채워넣는 느낌 ..?

Comment on lines +11 to +22
async selectEntityType() {
const typeNumber = await inputManager.retryScan(
`원하시는 레이서의 유형을 선택해서 번호를 입력해주세요.\n${RaceEntry.#stringifyRacerEntityTypes()}\n`,
(inputValue) => {
RaceEntry.#validateTypeNumber(inputValue);

return inputValue;
}
);

this.#entityType = RACER_ENTITY_TYPES[typeNumber];
}

Choose a reason for hiding this comment

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

entityType 이 사실 잘 안와닿았어요 ! 다소 포괄적인 느낌이 들어요, selectRacerType은 어떤가요 ?
RACER_ENTITY_TYPE 도 바로 RACER_TYPE 이라고 안하고 entity 를 붙인 이유가 있나요 ?

Copy link
Author

Choose a reason for hiding this comment

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

경주자의 유형이라는 의미를 담고 싶어서 entity를 사용했답니다..ㅎㅎ
간단하게 racer type도 괜찮긴하군요! 왜 생각을 못했지...ㅎ

Comment on lines +7 to +8
class RaceEntry {
static #SEPARATOR = ",";

Choose a reason for hiding this comment

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

우왕 이것까지 상수로..... !! 꼼꼼함의 끝판왕이다 !!!

하지만 .... 제 개인적인 생각으로는, 'Race' 라는 도메인과 무관한 상수라서 ... 그냥 console 출력 (또는 entry.js?) 전용 상수 파일을 하나 만들어도 될 듯 합니다 !

Copy link
Author

Choose a reason for hiding this comment

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

의견 감사합니다! 처음에는 분리 문자를 설정할 수 있게 클래스 생성시에 인자로 넘겨주는 형태였는데 과한 느낌이 들어서 내부에 상수로 지정해줬습니다. 근데 애초에 분리 문자를 상수로 하는게 과한 처리였던걸지도 모르겠네요... ㅎㅎ

Comment on lines +17 to +19
start(racers, rules = Race.#DEFAULT_RULES) {
Race.#validateRacers(racers);
Race.#validateRules(rules);

Choose a reason for hiding this comment

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

PR에 고민을 남겨주셨는데 .. 저는 Race 생성 시 rule 을 넣어주는 게 좀 더 합당해 보입니다 !
DEFAULT_RULES 는 Race 클래스에 들어있고, 인스턴스 생성 시 추가하는 rule 들은 인스턴스에 들어있는 것이 더 자연스러워 보여요 !

약간 ... rule 이 반드시 게임이 시작할 때(start 메소드가 동작함과 동시에) 세팅되어야 하는 이유가 없다고 생각하기 때문이에요

Copy link
Author

Choose a reason for hiding this comment

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

현실이라고 생각했을때, 경주가 시작하기전에 보통 룰이 정해져있다고 생각해보니까 세진님 말씀이 일리가 있군요!

Comment on lines +25 to +32
const racers = await inputManager.retryScan(
`경주할 ${this.#entityType} 이름을 입력하세요(이름은 쉼표(${
RaceEntry.#SEPARATOR
})를 기준으로 구분).\n`,
(inputValue) => {
const racerNameList = inputValue.split(RaceEntry.#SEPARATOR);

return racerNameList.map((name) => new Racer(name.trim()));

Choose a reason for hiding this comment

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

input 을 받는 부분 / validation 을 하는 부분 / input 가공하기

이 세가지를 inputManager.retryScan 콜백에서 한번에 해주니까 매우 간편하긴 한데, 분리하는 방향도 고려하시면 좋을 것 같습니다 !
간단한 문자열 가공이면 괜찮지만 Racer 객체를 생성하는 로직이 들어있어서 저로서는 이해가 살짝 어려웠어요 🤔
추가적으로, Racer 생성자 안에서 에러가 발생하면 어떻게 될까 ? 라는 부분도 있습니다

Comment on lines +25 to +32
const racers = await inputManager.retryScan(
`경주할 ${this.#entityType} 이름을 입력하세요(이름은 쉼표(${
RaceEntry.#SEPARATOR
})를 기준으로 구분).\n`,
(inputValue) => {
const racerNameList = inputValue.split(RaceEntry.#SEPARATOR);

return racerNameList.map((name) => new Racer(name.trim()));

Choose a reason for hiding this comment

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

const racerNames = await inputManager.retryScan(.... , () => { input trim 까지만 해주기! }
const racers = racerAnswers.map(name => new Racer(name))

이런 식으로 !

@@ -0,0 +1,74 @@
import { RACER_ENTITY_TYPES } from "../constants/index.js";

Choose a reason for hiding this comment

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

레이서 유형의 규칙이 아주 명확하잖아요 ?! validation 하기가 아주 괜찮아 보입니다 ㅎㅎ
따라서, main 에서 RaceEntry 인스턴스를 생성할 때 어떤 레이서 타입들이 있는지 받아도 재밌을 것 같아요 !

상수는 디폴트값으로 존재하고, new RaceEntry({ racerTypes : { 1 : 마리오, 2: 와리오, 3: 키노피오 }}) 이렇게 main 에서 조작할 수도 있는거죠

Copy link
Author

Choose a reason for hiding this comment

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

오 그러네요! 사용자가 추가하고 싶을 수도 있으니깐 인수로도 받을 수 있게 하면 좋겠네요! 감사합니다~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants