Skip to content

Conversation

@ketru44
Copy link

@ketru44 ketru44 commented Oct 27, 2025

기능 목록

  • 입력
    • 자동차 명 입력 받기
    • , 기준으로 자동차명을 구분
    • 진행 랩 수(몇번 진행할 것인지) 입력 받기
  • 입력값 검증(예외)
    • 사용자가 잘못된 값을 입력할 경우 [ERROR]로 시작하는 문구와 함께 에러를 발생시키고 프로그램을 종료시킨다.
    • 각 자동차명은 공백을 포함하지 않은 5자 이하의 문자열이다.
    • 각 자동차명은 중복을 허용하지 않는다.
    • 랩 수는 양의 정수이다.
  • 랜덤 넘버 데이터 생성
    • 자동차 수 * 랩 수 만큼의 랜덤 숫자(0~9사이 정수) 생성
  • 레이스 진행(전진 or 정지)
    • 랜덤 숫자가 4 이상(4,5,6,7,8,9)이면 전진, 4미만(0,1,2,3)이면 정지
  • 우승자 결정
    • 가장 많이 전진한 자동차가 우승자로 결정된다.
    • 공동 우승도 인정된다.
  • 레이스 히스토리 출력
    • 각 랩 순으로 레이스 기록을 출력한다.
  • 우승자명 출력
    • 동률의 경우 ,로 구분하여 출력한다.

문제 해결 과정

설계 단계

OOP + FP(함수형 프로그래밍)

  • 해결해야하는 문제: 프리코스 기간 동안 직접 함수형 패러다임을 공부하고 적용을 시도해보고 있습니다. 함수형 패러다임을 OOP에 융합 시키기 위한 구조 설계에 어려움을 겪고 있습니다.
  • 어떻게 해결했는지: 큰 틀에서 보면 App 클래스부수효과(입출력, API)를 모두 담당하고 레이스를 진행하는 부분은 순수함수로 작성하려고 합니다. 이를 바탕으로 함수명 구조를 짜보았는데, 아마 구현을 진행하면서 많은 수정이 있을 것 같습니다.

Car 클래스 분리

  • 해결해야하는 문제: Car을 객체로 만들어서 관리한다는 아이디어가 자연스레 떠올랐습니다. 의미적으로 확실하게 분리가 되니까 관리와 가독성에 장점이 있다고 생각합니다. 하지만 자동차가 복잡도 높은 기능을 수행하는 것도 아니기 때문에 고민이 되었습니다.
  • 어떻게 해결했는지: 클래스보다는 객체와 같은 구조로 관리를 하여 함수형 중심적으로 설계를 했습니다.

랜덤 넘버 생성

  • 해결해야하는 문제: 함수형 패러다임의 적용과 연결되는 부분입니다. API를 통해서 생성하는 난수들을 어떻게 생성하고 사용할 지에 대한 문제입니다. 처음에는 별 생각없이 매 라운드마다 난수를 만들어서 판별하도록 하면 되겠다고 생각했지만 설계를 하다보니 레이스마다 매번 난수를 생성하면 순수성을 유지하지 못하고, 테스트에도 어려움이 있을 것이라 판단했습니다.
  • 어떻게 해결했는지: App 단에서 필요한 개수의 난수를 모두 생성하여 넘겨주는 방식으로 문제를 해결해보려 합니다. 마찬가지로 매 랩읙 결과를 모두 끝이 나고 한 번에 순서대로 출력하는 방식으로 진행하려고 합니다.

구현 단계

데이터 구조

  • 해결해야하는 문제: 난수 테이프 부분에서 필요한 모든 난수를 미리 만들어서 레이스를 진행하는 흐름을 처음 구현하다보니 데이터 구조 결정에 문제있었습니다. 기능 중심으로만 설계하다 보니, 각 함수가 어떤 데이터를 주고받을지에 대한 구조적 고려가 부족했던 것이 원인이었습니다.
  • 어떻게 해결했는지: 처음에는 단순히 HashMap을 사용하여 <자동차명-히스토리 배열>을 이루면 쉽게 구현애 가능하겠구나 생각했습니다. 여러 데이터를 파라미터로 넘겨주다 보니 시간복잡도와 메모리 비용이 너무 크게 발생함을 깨닫고, 2차원 배열로 히스토리를 저장하는 방식으로 문제를 해결했습니다.

FIFO vs LIFO

  • 해결해야하는 문제: 모든 테스트 코드 중 제공된 테스트만 통과하지 못하는 문제가 있었습니다.
  • 어떻게 해결했는지: 난수를 생성해서 배열에 push함수를 사용해서 난수테이프를 만들었기에, 자연스레 pop()을 사용했던 것이 문제였습니다. 먼저 push된 것을 먼저 사용할 수 있도록 shift()를 사용하여 문제를 해결했습니다.

강성준 added 17 commits October 26, 2025 23:20
요구사항을 바탕으로 구조화한 설계와 기능 목록을 정리하고, 설계 단계에서 고민한 내용들을 정리했습니다.
기능 목록의 검증 기능을 따로 두어 세분화하였습니다.
woowacourse/mission-utils의 Console을 사용하여 사용자로 부터 입력을 받는 함수를 구현하였습니다. 해당 함수를 사용해 받는 입력은 쉼표로 구분된 자동차명과 진행될 랩 수입니다.
입력받은 자동차를 쉼표를 기준으로 파싱하기 위해서 parseByComma 함수를 구현하였습니다.
missionutil의Random을사용해 무작위 수를 생성하는 함수를 추가하였습니다. 그리고 해당 함수를 호출하여 필요로 하는 개수(차량의 수*랩 수)의 난수 배열을 반환하는 함수를 구현했습니다.
자동차명, 랩수, 난수를 받아 경주를 진행하는 도메인 함수를 구현하였습니다. 반환값은 경주 히스토리와 최종 결과를 모두 사용할 수 있도록 랩을 기준으로 분리된 2차원 배열로 결정했습니다.
최종 점수를 가지고 가장 높은 점수를 뽑아 우승자를 결정하는 함수를 구현하였습니다. 동률일 경우 공동 우승으로 처리됩니다.
utils/parsing에서 변환한 히스토리 로그를 app에서 mission util api로 출력하는 함수를 구현했습니다.
우승자를 문자열로 파싱한 뒤 misson util로 우승자를 출력하는 함수를 구현하였습니다. 우승자가 여러 명일 경우 쉼표로 구분되어 출력됩니다.
처음에 주어진 테스트 케이스를 통과하지 못하는 이유가 생성한 난수 배열을 stack의 pop()을 통해 값을 하나씩 가져오고 있기 때문이었습니다. 그래서 FIFO로 읽어 올 수 있도록 shift()를 사용하도록 수정하였습니다.
Copy link

@nomiupsight nomiupsight left a comment

Choose a reason for hiding this comment

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

_

describe("레이스 진행 도메인 테스트", () => {
test.each([
[["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 0], [2, 0, 1]]],
[["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 0, 1], [2, 1, 2], [3, 2, 3]]]
Copy link

Choose a reason for hiding this comment

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

테스트 코드에 작성해주신 숫자들이 너무나도 불규칙적이어서 다소 무슨 테스트를 하는지 헷갈리는것 같습니다
다른 분들중에서
const MOVE = 4 const STOP = 3
과 같은 방식으로 상수화 해서 테스트코드를 깔끔하게 하신분도 있더라고요 추천드립니다!

Copy link
Author

Choose a reason for hiding this comment

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

생각도 못해 본 부분이네요! 각 테스트의 규칙적이고 필요한 이유에 대해서 생각해보겠습니다. 새로운 방식 추천도 감사드립니다 ㅎㅎ

class App {
async run() {
// 입력(자동차명, 횟수)
const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
Copy link

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.

제가 봐도 조금 과한 부분인 것 같습니다. 타인이나 나중에 제가 코드를 다시 본다면 이해할 수 있을까를 고민하다가, 최대한 디테일하게 이름을 지어보자는 의도였습니다. getInputWoowaMissionApireadAsyncWoowaAPI 정도도 충분할 것 같습니다. 이것도 과할까요 ㅋㅋㅋ ;;

validateCarNameRule(arrayOfCarNamesUserRequest);
const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?");
const countOfLapUserRequest = Number(stringOfLapUserRequest);
validateLapNumberRule(countOfLapUserRequest);
Copy link

Choose a reason for hiding this comment

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

흐름 가장 이상적이네요 ..
입력 - 검증 입력 - 검증
저도 이렇게 했어야했는데 좋은 코드 배웠습니다

const countOfLapUserRequest = Number(stringOfLapUserRequest);
validateLapNumberRule(countOfLapUserRequest);
// 레이스 진행
const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest);
Copy link

Choose a reason for hiding this comment

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

미리 개수만큼 랜덤을 뽑는건가요 ?
미리 하신 이유가 있을까요 ?

race 의 책임 경계에서 고민을 많이하셨을거 같습니다

Copy link
Author

Choose a reason for hiding this comment

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

레이스를 진행하는 부분을 순수함수로 구현하고자 함이었습니다. 그렇다 보니 입력, 랜덤과 같은 부분을 분리하려고 했습니다. 메모리에 대한 트레이드오프가 있지만 테스트할 때 매우 유용했습니다.

@@ -0,0 +1,8 @@
export const findMaxValue = (values) => Math.max(...values); // 명확성과 재사용성이 높아보여 분리
Copy link

Choose a reason for hiding this comment

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

코멘트 남기려고 보니 주석을 남기셨네요
저도 동의합니다!!

Copy link

@manNomi manNomi left a comment

Choose a reason for hiding this comment

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

전체적으로 너무 잘 작성되고 가독성 좋은코드라 보기 좋았습니다
특히 라이브러리 테스트 코드를 작성하신게 인상적이네요
좋은코드 많이배웠습니다!

이메일을 잘못남겨서 알림이 안오더라고요 .. 동일한 내용으로 리뷰남겼습니다 죄송해요 ㅠ

describe("레이스 진행 도메인 테스트", () => {
test.each([
[["a", "b", "c"], 2, [4, 1, 2, 9, 0, 4], [[1, 0, 0], [2, 0, 1]]],
[["red", "blue", "black"], 3, [5, 2, 4, 9, 5, 9, 9, 4, 6], [[1, 0, 1], [2, 1, 2], [3, 2, 3]]]
Copy link

Choose a reason for hiding this comment

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

테스트 코드에 작성해주신 숫자들이 너무나도 불규칙적이어서 다소 무슨 테스트를 하는지 헷갈리는것 같습니다
다른 분들중에서
const MOVE = 4 const STOP = 3
과 같은 방식으로 상수화 해서 테스트코드를 깔끔하게 하신분도 있더라고요 추천드립니다!

class App {
async run() {
// 입력(자동차명, 횟수)
const stringOfCarNamesUserRequest = await this.readInputAsyncUsingWoowaMissionApi("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
Copy link

Choose a reason for hiding this comment

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

함수명을 조금은 줄여도 되지 않을까 싶습니다...
의미를 그대로 살리기 위해서 함수명을 줄이지 않는것이 중요하지만
다소 길거나 불필요한것 같기도 한 부분이 존재하는것 같아요

validateCarNameRule(arrayOfCarNamesUserRequest);
const stringOfLapUserRequest = await this.readInputAsyncUsingWoowaMissionApi("시도할 횟수는 몇 회인가요?");
const countOfLapUserRequest = Number(stringOfLapUserRequest);
validateLapNumberRule(countOfLapUserRequest);
Copy link

Choose a reason for hiding this comment

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

흐름 가장 이상적이네요 ..
입력 - 검증 입력 - 검증
저도 이렇게 했어야했는데 좋은 코드 배웠습니다

const countOfLapUserRequest = Number(stringOfLapUserRequest);
validateLapNumberRule(countOfLapUserRequest);
// 레이스 진행
const randomNumberTape = this.makeRandomNumbersTape(arrayOfCarNamesUserRequest, countOfLapUserRequest);
Copy link

Choose a reason for hiding this comment

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

미리 개수만큼 랜덤을 뽑는건가요 ?
미리 하신 이유가 있을까요 ?

race 의 책임 경계에서 고민을 많이하셨을거 같습니다

@@ -0,0 +1,8 @@
export const findMaxValue = (values) => Math.max(...values); // 명확성과 재사용성이 높아보여 분리
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.

당연하다고 생각한 의존성까지 테스트 하신 부분이 인상깊습니다 :)

Comment on lines +23 to +27
[2.1, "[ERROR] : 횟수는 양의 정수이어야 합니다."],
[-1, "[ERROR] : 횟수는 양의 정수이어야 합니다."],
[NaN, "[ERROR] : 횟수는 양의 정수이어야 합니다."],
[null, "[ERROR] : 횟수는 양의 정수이어야 합니다."],
[undefined, "[ERROR] : 횟수는 양의 정수이어야 합니다."],

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.

3주차 미션 때 상수화 부분을 적용해보려고 합니다. 감사합니다!!

Comment on lines +1 to +6
export function validateCarNameRule(names) {
if(new Set(names).size != names.length) throw new Error("[ERROR] : 중복된 이름을 사용할 수 없습니다.")
names.forEach(n => {
if(n.length > 5 || n.length === 0) throw new Error("[ERROR] : 자동차 명은 5자 이하여야 합니다.");
});
}

Choose a reason for hiding this comment

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

자동차 이름이 없어도 "[ERROR] : 자동차 명은 5자 이하여야 합니다." 라는 에러가 뜹니다!

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 +8 to +11
export function validateLapNumberRule(lap) {
if(lap <= 0 || !Number.isInteger(lap))
throw new Error("[ERROR] : 횟수는 양의 정수이어야 합니다.")
} No newline at end of file

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.

분리 처리 후 다른 에러 메세지를 준다면 사용자 입장에서 더 편할 것 같네요! 감사합니다~

Comment on lines +5 to +13
export function parseToHistoryFormat(oneRoundHistory, name) { // [1,1,1] ["a","b","c"]
return oneRoundHistory
.map((result, idx) => `${name[idx]} : ${"-".repeat(result)}`)
.join("\n");
}

export function parserToWinnerFormat(winner) {
return `최종 우승자 : ${winner.join(", ")}`;
} No newline at end of file

Choose a reason for hiding this comment

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

이 부분들을 파서로 빼신게 인상깊습니다. 어디까지가 레이스와 아웃풋의 책임인지 생각할 여지를 주는 것 같아요

this.printWinnerOfRace(namesOfWinner);
}

async readInputAsyncUsingWoowaMissionApi(questionStr) {

Choose a reason for hiding this comment

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

명칭을 구체화하는 과정에서 오히려 가독성이 떨어진다고 느껴집니다!

Comment on lines +28 to +35
makeRandomNumbersTape(cars, laps) { // 부수효과를 없애기 위해 필요한 만큼의 난수를 만들고 레이스 진행
let numberTape = [];
const countOfNeededNumber = cars.length * laps;
for(let cnt = 0; cnt < countOfNeededNumber; cnt++) {
numberTape.push(this.pickRandomNumberInRangeUsingWoowaMissionApi(0, 9))
}
return numberTape;
}

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.

레이스를 진행하는 부분을 순수함수로 구현하고자 함이었습니다. 그렇다 보니 입력, 랜덤과 같은 부분을 분리하려고 했습니다. 메모리에 대한 트레이드오프가 있지만 테스트할 때 매우 유용했습니다.

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.

4 participants