오늘 우테코에서의 첫 주차 마지막 수업이 끝났다. 이제 겨우 4일 다녔는데 벌써 많은 일이 있었던 한 주였다. 각 날짜별 일기를 짧게 쓰고 이번주 과제였던 자동차 경주 코드 회고를 작성해보려한다. 일기는 당일마다 쓴 게 아니라 그냥 지금(14일 금요일) 지난 날들을 돌아보며 몰아쓰는 것이다 ㅋㅋㅋ
2/11 화요일
OT 날이다. 이 날 등교할 때 정말 두근두근했는데 ㅎㅎ 기대돼서 설레기도하고 걱정돼서 불안하기도 한 마음으로 등교했다. 한 편으로 심심하게 점심 혼자 먹을까봐도 걱정했었는데, 다행히 바로 연극 조원이 편성돼서 그럴 일은 없었다🥹 "연극"이 뭐냐고요?
. . . . ㅎ
OT 날엔 출석과 우테코 생활규칙 안내부터 코치와 크루원들의 자기소개, 앞으로의 커리큘럼 계획에 대해 말씀해주셨다. 이제 우테코 안에서는 닉네임("님" 자 금지. 코치에게조차...! 유교걸은 지금 적는데도 님을 붙이고싶ㄷㅏ)
2/12 수요일
연극 조원들과 연극 주제를 정하고 첫 페어프로그래밍을 했던 날이다. 냅다 연극이라니, 너무 막막하고 주제가 생각나지도 않을 것 같았는데 조원들이 아이디어를 많이 내줘서 제출할 수 있었다. 포비의 말벌아저씨 레퍼런스 탓인지 불타오르는 개그 욕심😂
페어와는 처음부터 공통점이 너무 많아서 신기했다! 코드 스타일도 비슷하고 그 이외 살아왔던 흔적(?)에서 공통점이 많아서 금방 즐겁게 같이 코딩했다.(같이 즐거웠던 거 맞나?)
우리는 TDD 개발을 시도해보았는데, 프라이빗 필드에서 getter와 setter를 만들지 않고 테스트 코드를 작성하려고 하니 여러 고민점이 생겨 머리에 쥐가 나는 줄 알았다.
점점 하면 할 수록 그냥 console 찍고 싶은 마음만 가득해져갔다 . . . ㅋㅋㅋㅋ 그래도 내일의 우리를 믿고 퇴근ㅋ
우테코에서의 첫 수업은 혼자 고민해볼 수 있는 관점과 시간을 많이 주는 것이 신선하고 좋았다. 그리고 수업 중 자유롭게 스레드에 사담하거나 질문할 수 있고, 수업 중간중간 계속 올라오는 코치들의 재치있는 농담들까지 뭐랄까 신선한 충격이다. 완전 자유롭고...짱 재밌다. 분명 아침 수업인데 시간가는 줄 모르고 재밌게 듣게되는 매직✨
2/13 목요일
아침부터 6시까지 페어프로그래밍을 마무리하다가 6시부턴 또 연극 회의를 해서 결국 연극조원들이랑 10시 넘어서 선릉캠 마감까지하게된 날이다 ㅋㅋㅋ
페어 프로그래밍은 우테코 합격 전, 최종 코테를 준비하면서 스터디원과 원격으로 해본 경험이 있었지만 역시 그때나 이번이나 참 좋은 스터디 방식인 것 같다. 서로의 생각을 이해시키고 설득하고 설득당하면서 코딩을 해야하기 때문에 개발 속도 자체는 당연히 더 느리지만, 이 과정에서 얻어가는 것들이 너무 많아 재밌기 때문이다.
페어와 나는 코드 스타일도 성향도 비슷한 편인 거 같아서 개발 중에 갈등이나 서로 이해안되는 지점은 없었다. 그런만큼 고민에 갇히게 되는 지점들도 비슷해서 같이 프라이빗 필드와 테스트에 대해 고민이 많았는데, 아직도 명확한 답은 모르겠다.
이 과정에서 프라이빗 필드를 참조하는 메서드를 mocking하고 spy 붙이는 코드가 어려워서 헤매고 있었는데 캉O이 도와줬다! 비슷한 문제를 겪었다며 강력한 힌트를 주셔서 덕분에 jest의 모킹과 스파이 개념도 제대로 복습이 되었다 ㅎㅎ
2/14 금요일
오늘의 데일리마스터는 나였다. 컨텐츠로 일단 약간의 스트레칭을 함께하는 명상을 준비해갔는데, 고개 스트레칭을 시작하자마자 나지막히 여기저기서 들리는 뚜두둑 소리가 웃펐다😂
오늘은 12시 반까진 오전 수업이 있었다. 자동차 경주 과제를 공원과 함께 풀어보았는데, 역시나 생각해볼 지점들이 많은 수업이었다!
도메인과 UI의 분리부터, validate는 차에서 검증을 해야할까 util일까, 랜덤값 생성같은 기능에 테스트가 꼭 필요한가?/단위 테스트에 꼭 mocking이나 spy가 필요할까 같이 말이다.
랜덤값 생성의 경우엔 테스트 커버리지에 대해 생각해보면 우리가 원하는 범위 내에서 움직이는지에 대한 검증만 확실히 보증하면 되지, 반환값이 꼭 랜덤인가를 테스트로 검증해야하는지는 다시 생각해볼만 하다고 하셨다.
수업이 끝나고 나선 연극 조원들과 리뷰어에게 받은 피드백도 공유하고 각자의 생각도 나눴다. 우리 조는 두 번 입력하면 프로그램이 에러 종료되는 이슈가 있었는데 왜 그러는지 이유를 모르겠어서 연극 조원들에게 말해봤더니 O건이 해결해줬다! 내가 이해를 빨리 잘 못했는데 그림으로 차근차근 알려주셔서 이해할 수 있었다 ㅠ 엄청 중요한 개념을 알아서 좋았다. 밑에 정리해서 적어야지 ㅎㅎ 감사합니다 O건!👍
그리고 다른 조원들이 받은 다양한 리뷰들을 들으니 많이 고민해볼 지점과 깨닫는 지점들이 많아서 좋았다. 코드 리뷰의 강력한 순기능 . . ..
그나저나 모킹하느라 좀 시간이 걸렸었는데, 오늘 수업에서 예고하길, 다음 주차 로또 과제 때는 모킹을 쓰지 말라고 한다. 왜냐하면 모킹을 써야하는 테스트가 과연 단위 테스트일지 생각해보면, 그렇지 않기 때문이다. 프리코스에서 봤던 테스트 코드에 모킹 함수가 있었던 것은 기능 테스트를 목적으로 E2E 테스트 코드를 짠 것이기 때문인데, 우리의 과제는 먼저 단위 테스트를 해보는 것이 목표이기 때문이다.
단위 테스트... 이것도 아직 경계가 모호하게 와닿아서 더 고민하고 공부해봐야 할 것 같다.
https://github.com/woowacourse/javascript-racingcar/pull/338
코드 리뷰
const winnerNames = [];
let maxPosition = 0;
this.#carList.forEach(car => {
if (car.position > maxPosition) {
maxPosition = car.position;
}
});
this.#carList.forEach(car => {
if (car.position === maxPosition) {
winnerNames.push(car.name);
}
});
return winnerNames;
maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용하면 코드가 더 간결해질 수 있습니다!
static async inputTryNumber() {
try {
const inputTryNumber = await InputView.readLineAsync(MESSAGE.INPUT.TRY_NUMBER);
Validator.isEmpty(inputTryNumber);
console.log(inputTryNumber);
const parsedNumber = Number(inputTryNumber.trim());
Validator.isNumber(parsedNumber);
Validator.isRangeOver(parsedNumber, DEFINITION.MIN_GAME, DEFINITION.MAX_GAME);
Validator.isDecimal(parsedNumber);
return inputTryNumber;
} catch (error) {
console.log(error.message);
await this.inputTryNumber();
}
}
'꼭 Validator와 input 함수가 에러로 소통해야 할까?'라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요?
Q & A
TDD로 개발을 할 때 모든 객체와 기능에 대해 테스트 코드를 명세서처럼 작성해두고 기능 개발을 하는 게 나을지, 하나의 기능or모델에 대한 테스트코드를 작성하고 그 기능에 대한 개발을 하고 이렇게 순차적으로 가는 게 나을지?
-> 좋은 질문입니다~!
문서화의 관점에선 다양하게,
버그 예방의 관점에선 '중요한 것부터 꼼꼼하게'인 것 같습니다!
기능 개발 단계에서 테스트 코드 작성 vs 기능 개발에선 console 찍고 나중에 품질 검증 목적으로 테스트 코드 작성
리뷰어님의 생각은?! ...저는 그냥 콘솔이 빠른 거 같아요 ㅠㅋㅋㅋㅋ 테스트 코드 어려워요오...
-> 좋은 질문입니다! 저도 고민될 것 같아요.
좋은 상황은 아니겠지만 정말 너무 급하다면 기능 구현을 우선 해야할 수도 있죠.
하지만 엣지케이스를 정리하는 것 자체는 구현이 들어가기 전에 하는 것이 큰 도움이 된다고 생각해요.
코드로 남겨지면 제일 좋겠지만, 여유가 없다면 테스트해볼만한 것들을 메모나 문서로 미리 정리하는 것도 추천합니다~!
프라이빗 필드를 변경하고 내용을 참조해 메세지를 던지는 메서드를 테스트할 때, 사실 정확한 검증을 위해서는 해당 프라이빗 필드 값을 보면 좋은데 getter를 지양하면 그럴 수 없으니...그래서 해당 필드를 이용한 기능이 정상적으로 동작하는지 간접적으로 테스트하는 걸로 충분할 수 있는지? 쵸파님 생각이 궁금합니다!
-> 요건 깊게 생각해본 적이 없네요.. 현재로는 간접적 테스트로 충분할 것 같습니다!
프론트엔드에도 MVC 패턴 적용이 좋을까요? 실제 프론트엔드 개발자가 자주 보게될 디자인 패턴도 궁금합니다...!
-> MVC 패턴 자체보다는, 데이터를 다루는 코드와 UI를 다루는 코드를 분리한다는 관점은 자주 사용하고 있습니다.
Flux 아키텍처라는 키워드가 있습니다! 이후 미션 진행하시면서 자연스럽게 알아가실 것 같습니다~!
다음 스텝도 파이팅입니다~! 💪 💪
혼자서 경주가 가능
0회면 경주 x
어떤 기조로 코드를 작성했는지, 구조는 어떻게 설계했는지, 어떤 부분이 어려웠고 더 신경써서 작업했는지를 조금 더 던져주셨다면 좋았을 것 같아요.
리뷰이의 정보가 부족하면 리뷰어는 순수 100% 코드만으로 의도까지 유추해야 한답니다.
앞으로 미션은 많으니 차차 개선해보시죠 ㅎㅎ
(참고로 회사에서는 동료에게 PR을 남기게 될텐데, 동료는 항상 절대적으로 시간이 부족하답니다... 😅 )
index.js 파일에서 바로 Game 인스턴스 생성 및 시작을 해도 되지 않을까요?
index도 class로 만드신 이유가 궁금해요 ㅎㅎ
오타가 있어요 winnerMess'a'ge !
요 익스텐션을 강추 드립니다! 앵간한 오타를 다 잡아줘용

class가 static 메서드만 가지고 있네요. 그렇다면 꼭 class가 아니라 객체였어도 될것 같아요
moveForward < 4 상수화
보통 is~ prefix는 boolean 값을 return하는 변수들에 붙입니다.
boolean을 return하는 함수들로 분리해보면 어떨까요?
(분리하고 나면 겸사겸사 Validator의 함수들 이름도 바꿔야 할텐데, 한번 고민해보세요 ㅎㅎ)
maxPosition을 찾는 과정과
maxPosition만큼 이동한 자동차를 찾는 과정을 각각 다른 메서드로 분리해보면 좋겠어요.
21 ~ 26, 28 ~ 32 line를 기준으로 생각해보셔요!
배열에 직접 값을 집어넣기 보다는
map 메서드를 활용해 Car 인스턴스로 구성된 배열을 한번에 반환해 할당할 수 있을 것 같아요. 이렇게 하면 불변성을 유지하면서 데이터(여기선 carList 필드)를 변경할 수 있답니다~! 불변성을 왜 유지해야하는지는 여유있을 때 추가적으로 학습해보시면 좋겠네요~!
(천천히 체화될 내용이라 여유를 가지셔요~ ㅎㅎ)
-> 상태값을 어디 저장했다가 한 번에 넣는게, 바로바로 push하는 거보다 메모리 비용이 적나...? -> 내가 생각했던 것과 달리 둘다 성능면에서는 큰 차이가 없고, 불변성을 지키고 추적성을 용이하게 하게 하기위해 map()으로 한 번에 넣는 게 좋구나!
start()메서드 하나에 많은 동작이 포함되어 있어요.
인풋을 받고
자동차 리스트를 만들고
경주를 하고 중간 결과값을 출력하고
최종 승자를 계산해서 출력합니다.
1, 2번 - 경주준비 / 3, 4번 - 경주시작 요렇게 2개의 맥락으로 나누어 분리하는걸 목표로 두시면 좋을 것 같아요.
너무 많이 생각하진 않으셨으면 해요😄
const DEFINITION = {
MAX_CAR_RACERS: 40,
MAX_NAME_LENGTH: 5,
MIN_GAME: 0,
MAX_GAME: 100,
};
console.log(inputTryNumber);
class InputController {
static async inputName() {
try {
const inputName = await InputView.readLineAsync(MESSAGE.INPUT.NAME);
Validator.isEmpty(inputName);
const splitedName = Parser.splitName(inputName);
Validator.isArrayLengthOver(splitedName, DEFINITION.MAX_NAME_LENGTH);
Validator.isDuplicate(splitedName);
splitedName.forEach(name => {
Validator.isStringLengthOver(name, DEFINITION.MAX_NAME_LENGTH );
});
return splitedName;
} catch (error) {
console.log(error.message);
await this.inputName();
}
}
console.log(error.message);
return this.inputName();
✅ 캐치 블록 안의 재귀함수를 return으로 바꿔줘야한다. 이 InputController를 호출하는 Game.js 에서는
const inputName = await InputController.inputName();
✅ 이렇게 작성되어있는데, 저 캐치가 작동될 때 그냥 return 없이 냅다 inputName();를 해버리면 inputName()이 재실행되든 말든 이미 inputName에 undefined 가 저장되는 문제가 있다. 이건 내가 트라이 캐치문을 잘 못 이해했던 것이었는데, 캐치 막줄에 재귀함수로 호출한다해서 inputName에 값이 저장되지 않고 다시 try를 무한정 시도한 뒤 반환값이 있을 때 저장되는 줄 알았는데, 그게 아니었따. 그래서 수정! 차근차근 알려준 O건에게 압도적 감사를 , , ., ., .ㅎㅎ
-> 🔴 그런데 try...catch 로 재귀를 하는 것에 대해 비판적인 생각을 해보았다. catch에서 반환되는 재귀함수를 결국 상위 호출에 저장해서 다시 실행하는 것이라면 오히려 불필요한 스택을 쌓아서 메모리 효율이 더 나빠지는 게 아닐까?
-> 🔴 이렇게 생각하다보니 try catch의 사용법에 대해 직관적으로도 "유효한 값을 입력할 때까지 이 입력함수를 시도하라"는 명령이 catch에 들어가는 게 맞는지 모르겠다는 생각이 들었음
-> 🔴 catch에는 예외를 기록하거나 해결하는 로직을 써야하지 않을까?
-> 🔴 예를 들어 while문으로 재귀함수를 실행하던 중에 예기치 못한 오류가 발생되거나 메모리 누수가 일어나 메모리를 다 써버려서 프로그램이 다운될뻔 한다거나 할 때 catch를 실행하도록 catch를 약간 최후의 수단처럼 이용하는게 더 합리적이지 않을까 하는 생각이 들었음.
-> ✅ 그래서 유효한 값을 입력할 때까지 입력받을 시도를 계속 하라는 명령을 시행할 inputHandler를 util 함수로 뺐고, 이 함수는 try catch가 아닌 while로 명령을 해당 조건까지 무한 반복하도록 리팩토링 했음!
✅ is...prefix 검증이 boolean 을 return 하지 않으면서 is 네이밍한 것에 대해 로직을 변경할지 네이밍을 변경할지 -> boolean 값을 반환하는 validationUtils 객체 메서로 변경
✅ 입력 유효성 문제가 한 개가 아니라 여러 개일 때 모든 문제를 다 표시하게하려면?
const isDuplicated = (arr) => new Set(arr).size !== arr.length;
DEFINITION = {
MIN: {
GAME_TRY: 1,
GAME_ ...
}
}
이렇게 MIN 정의와 MAX 정의로 나누면 코드 작성할 때 편하다는 다른 크루원의 아이디어가 있었음. 좋은 듯!
인풋에 대한 검증을 ui에서도 하고 domain 에서도 해볼까?
상수 분리 놓친 거 마저 상수 분리
/view, /domain 으로 디렉터리 수정하는건가?
✅ index라던가 뭐 쓸데없이 class로 만들어버린 거 리팩토링하자
✅ 각 사용자 입력마다 validator 호출 순서가 다르니까 각 입력을 위한 validate 함수를 만들자 -> 공통적으로 자주 쓰일 validation 판단 로직들은 validator 위에 객체메서드로 모아놓음
✅ inputController에 대한 비판적 생각....
✅ '꼭 Validator와 input 함수가 에러로 소통해야 할까?'라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요? 에 대한 해결책
✅ 자동차 "경주" 이므로 혼자 경주하는 것은 의미가 없다 판단, 2명 이상만 참여 가능하게 함
✅ 메모리 과부하를 방지하기 위해 참여인원수 최대 40명 제한
✅ Validator
maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용
❌ history 상태관리 나도 만들어볼까 -> 시간 없음...ㅎㅎ
모델의 역할에 대해 더 생각해보자...
결합도...불필요한 임포트문이 너무 많진 않은지, 의존성 주입도 적절하게 섞어보자
2차 리뷰 요청 보낼 때, 메세지 잘 적기(어떤 기조로 코드를 작성했는지, 구조는 어떻게 설계했는지, 어떤 부분이 어려웠고 더 신경써서 작업했는지, 어떤 것을 리팩토링 했는지...)
test 코드 jest.fn()을 사용하지 않는다. (마찬가지로, mock/spy 도 사용하지 않는다)
여기부터는 pr 보낼 때 쓸 메세지이므로 존댓말로 작성합니당
리팩토링 PR 링크
https://github.com/woowacourse/javascript-racingcar/pull/410
📂 리팩토링 전
/src
├── constants/
├── models/ <-- Car 모델이 여기에 위치
├── utils/
├── views/
├── controllers/
📂 리팩토링 후
/src
├── constants/
├── domain/ <-- 도메인 로직을 한 곳에서 관리
│ ├── models/ <-- Car, WinnerManager 등 도메인 관련 클래스들
├── utils/
├── views/
'꼭 Validator와 input 함수가 에러로 소통해야 할까?'라는 생각이 들었습니다!
잘못된 입력을 알려주는 것도 View 영역이라면, 판단은 Validator가 하고, 어떤 문자열을 보내줄지는 input 함수에서 책임지는 방법은 어떤가요?
💭 이 말씀에 대해서도 많은 고민을 해보았습니다. 확실히 에러 "메세지"를 결정하고 던지는 것은 view의 역할인 것 같고, validator가 에러 메세지까지 정해서 보내는 방식은 같은 검증 로직이지만 다른 메세지를 출력해야할 때 재활용성도 떨어질 것이었습니다.
const ValidationUtils = {
isEmpty: string => string.trim().length === 0,
isDuplicated: array => new Set(array).size !== array.length,
isArrayLengthOver: (array, max) => array.length > max,
isStringLengthOver: (string, max) => [...string].length > max,
isArrayLengthRangeOver: (array, min, max) => array.length > max || array.length < min,
isRangeOver: (number, min, max) => number < min || number > max,
isDecimal: number => number % 1 !== 0,
isNotNumber: number => number === null || typeof number !== 'number' || Number.isNaN(number),
isNotString: string => typeof string !== 'string',
};
✅ 그래서 우선 Validator.js 에 단순 if문(검증로직)을 boolean 형태로 반환하는 ValidationUtils 객체를 만들었고, 이 객체를 활용하여 각 입력값마다 유효한 검증 로직 순서에 맞게 검사하는 메서드를 가진 Validator로 수정하였습니다. 이렇게 하는 편이 isEmpty와 같은 is네이밍에도 잘 맞고 재활용성 및 유지보수성에도 도움이 되는 방향이라고 생각하였습니다.
💭 그리고 그렇다면 에러메세지 관리 및 처리를 어떻게 해야할까도 고민하였는데, 처음엔 validator가 에러코드를 반환하면 그 에러코드들을 배열에 담아 input이 받아 각 입력필드에 맞게 에러메세지를 출력하는 식을 고민했습니다.
💭 이렇게 한다면 에러코드로 통신하는 서버 API 환경과 통신하는 연습도 되지 않을까 싶었지만, 각 입력필드마다 유효값이 다르고 변경이 빈번할 프론트엔드 환경에는 더 좋은 방법이 있을 것 같았습니다.
const Validator = {
validateInputNames: arrayNames => {
const result = {
IS_ARRAY_LENGTH_RANGE_OVER: ValidationUtils.isArrayLengthRangeOver(arrayNames, DEFINITION.MIN_CAR_RACERS, DEFINITION.MAX_CAR_RACERS),
IS_DUPLICATED: ValidationUtils.isDuplicated(arrayNames),
IS_EMPTY: false,
IS_STRING_LENGTH_OVER: false,
IS_NOT_STRING: false,
};
arrayNames.forEach(name => {
if (ValidationUtils.isEmpty(name)) result.IS_EMPTY = true;
if (ValidationUtils.isStringLengthOver(name, DEFINITION.MAX_NAME_LENGTH)) result.IS_STRING_LENGTH_OVER = true;
if (ValidationUtils.isNotString(name)) result.IS_NOT_STRING = true;
});
return result;
},
✅ 그래서 validationUtils의 모음집으로서 Validator를 쓰고, 이 validationUtils의 값을 키값쌍으로 가진 객체를 내보냈습니다. 그리고 그 객체의 키값쌍에 따라 다른 Message를 출력하도록 Error.js 객체파일을 만들고, OutputView 파일도 수정하였습니다.
✅ 이렇게 리팩토링하면서, 한 번에 여러 가지의 유효성을 어겼을 때도 모두 안내해줄 수 있도록 하였습니다.
다만 한가지 치명적인 버그가 있는데, 한번 입력을 잘못하면 2번째 입력부터 들어가지 않네요.
💭 말씀해주신 이 부분이 try...catch 의 catch 블록에서 this.inputName() 재귀 입력 함수에 return을 하지 않은 문제였다는 것을 알게 되었습니다. return 을 하지 않으면 단순히 재귀함수를 실행만 하고 이 함수를 호출한 상위 함수의 반환값은 undefined가 되어, 2번째 실행 때 제대로 값이 반환되어도 스택이 꼬이는 문제였습니다.
💭 그래서 해당 부분에 return 을 추가하고나니 제대로 동작하였지만, 문득 이러한 동작에 try catch 를 쓰는 것이 맞는지 의문이 들었습니다.
💭 catch에서 반환되는 재귀함수를 결국 상위 호출에 저장해서 다시 실행하는 것이라면 사용자의 유효하지 않은 입력은 굉장히 빈번하게 일어날 일일텐데, 오히려 불필요한 스택을 쌓아서 메모리 효율이 더 나빠지는 게 아닐까?
💭 이런 생각이 드니 try catch의 직관적인 느낌과도 해당 로직이 맞지 않는다는 생각이 들었습니다. "유효한 값을 입력받을 때까지 이 동작을 반복하라"는 명령이 과연 예외를 기록하거나 해결하는 로직이 들어가야하는 catch에 맞는가? 하는 부분이었습니다.
💭 예를 들어, 재귀함수를 실행하던 중에 예기치 못한 오류가 발생되거나 메모리 누수가 일어나 메모리를 다 써버려서 프로그램이 다운될 뻔 한다거나 할 때 catch를 실행하도록 catch를 마치 최후의 수단처럼 이용하는 게 더 합리적이고 안전하지 않을까 하는 생각이 들었습니다.
const InputHandler = {
async getValidInput(promptMessage, parser, validator, errorCategory) {
let isNotValid = true;
let parsedInput;
while (isNotValid) {
const input = await InputView.readLineAsync(promptMessage);
parsedInput = parser ? parser(input) : input;
const validationResults = validator(parsedInput);
OutputView.printValidationResults(validationResults, errorCategory);
isNotValid = Object.values(validationResults).some(value => value);
}
return parsedInput;
},
};
✅ 그래서 이번 리팩토링에서는 input 재귀함수를 util/inputHandler로 빼서 재활용성과 직관성을 챙겼고, try catch가 아닌 while로 함수를 실행하도록 수정하였습니다.
✅ 그리고 controller란 model과 view 사이를 오가며 마치 매니저 역할을 한다고 생각하는데 input만 담당하는 inputController가 굳이 필요할까? 라는 의문에 inputView에 객체 메서드를 추가하는 방향으로 리팩토링하였습니다.
필드가 없거나 필드가 아니어도 되는 상황에서도 class를 적극 활용해주신 부분을 코멘트 드리고 싶습니다~!
✅ 단순 함수의 집합을 위해 작성하여 필드가 없는데 불필요하게 class로 작성했던 부분을 수정하였습니다.
const DEFINITION = {
MAX_CAR_RACERS: 40,
MAX_NAME_LENGTH: 5,
MIN_GAME: 0,
MAX_GAME: 100,
};
✅ MIN_GAME이 1이었어야하는데 0으로 적어서 게임 시도 0회에도 오류를 반환하지 않는 점을 수정하였습니다.
class Car {
constructor(name) {
this.validateName(name);
this.name = name;
this.position = 0;
}
validateName(name) {
const validationResults = Validator.validateCarName(name);
if (Object.values(validationResults).some(isError => isError)) {
throw new Error(ERROR.MESSAGE.INVALID_CAR_NAME);
}
}
✅ Car가 생성될 때 name이 string인지, 5글자 이하인지를 검사하는 로직을 추가하였습니다.
💭 이미 입력값을 받을 때 name의 유효성을 검사하고 인자를 보내는데 Car 클래스 내부에도 검증이 다시 필요할까? 하는 의문이 들었지만 크루원과 대화하면서 '실제 프론트와 백엔드를 생각해보면 프론트에서 막지 못한 유효하지 않은 데이터 입력이 서버로 넘어갔을 때 서버에서도, 즉 도메인 로직에서도 방어를 할 수 있어야하지 않나 하는 시각에서 둘 다에서 검사해도 좋을 것 같다.'라는 의견을 듣고, 제가 생각하지 못했던 부분이라 설득되었습니다!
✅ models/ 폴더를 domain/ 폴더 하위에 위치 시켰습니다.
maxPosition은 Math.max 함수를 활용하고, winnerNames는 map 메서드를 활용하면 코드가 더 간결해질 수 있습니다!
createCarList(names) {
this.#carList = names.map(name => new Car(name));
}
...
const maxPosition = Math.max(...this.#carList.map(car => car.position));
✅ 반영하였습니다!
class WinnerManager {
#carList;
#maxPosition;
constructor(carList) {
this.#carList = carList;
this.#maxPosition = this.#getMaxPosition();
}
#getMaxPosition() {
return Math.max(...this.#carList.map(car => car.position));
}
getWinners() {
return this.#carList.filter(car => car.position === this.#maxPosition).map(car => car.name);
}
}
export default WinnerManager;
✅ 기존에 gmae.js 에서 judgeWinner()로 우승자를 판별하는 대신 WinnerManager.js 를 만들었습니다.
💭 우승자가 누구인지는 game의 필드값에서 쉽게 뽑아낼 수 있는 값인데 winner 객체가 굳이 필요할까 싶어서 따로 만들지 않을 생각이었습니다. 그런데 고민해보니 나중에 2,3등도 뽑아야한다던지 승률을 계산해야한다던지 하는 식의 로직 확장을 생각하면 확실히 분리하는 게 나을 것 같아 분리하였습니다.
#raceRound() {
this.#carList.forEach(car => {
const randomValue = createRandom();
car.moveForward(randomValue);
OutputView.roundResult(car.name, car.position);
});
}
#raceGame(inputTryNumber) {
for (let i = 0; i < inputTryNumber; i++) {
this.#raceRound();
OutputView.break();
}
}
✅ 회차별 레이스와 전체 레이스를 관리하는 로직을 분리하였습니다.
이번 리팩토링의 목표는 가독성을 높이고, 코드의 역할을 명확히 분리하여 유지보수성과 재활용성을 향상시키는 것이었습니다. 또한 쓸데없이 무겁지 않은 프로그램을 만드는 연습을 하기 위해 메모리 비용 측면에서도 고민을 해보았습니다.
크게 네 가지 방향에서 개선을 진행했습니다.
1️⃣ 불필요한 class → 객체 메서드로 변경
필드 없이 단순히 함수만 가지고 있는 클래스들은 객체 메서드로 변경하여 불필요한 인스턴스 생성을 줄이고, 직관적인 코드로 리팩토링했습니다.
예: Validator, InputHandler 등
2️⃣ 입력 검증 및 예외 처리 방식 개선
기존 try...catch 문을 이용한 재귀 호출을 제거하고, while 문을 사용하여 메모리 효율성을 높였습니다.
예외 처리는 정확한 예외 상황에서만 동작하도록 개선하여, 불필요한 호출과 메모리 낭비를 방지했습니다.
입력값 검증은 Validator에서 판단하고, 실제 에러 메시지를 출력하는 역할은 View가 담당하도록 책임을 분리하였습니다.
3️⃣ 우승자 판별 로직을 WinnerManager로 분리
기존 Game 클래스 내부에서 처리하던 우승자 판별 로직을 별도의 WinnerManager 클래스로 분리하여 확장성을 고려했습니다.
4️⃣ 메서드 단위 책임 분리 및 가독성 향상
start() 메서드가 너무 많은 역할을 하고 있었기 때문에 경주 준비(createCarList), 경기 진행(raceGame), 결과 출력(judgeWinner)을 분리하여 코드 가독성을 향상시켰습니다.
또한, Math.max()를 활용하여 최대값을 구하고, map()을 이용해 우승자를 추출하는 등 더 간결한 코드로 리팩토링을 진행했습니다.
✅ 리팩토링 후 기대 효과
역할이 명확한 구조: 도메인(domain/)과 뷰(views/)를 분리하여 코드의 책임이 명확해짐
유지보수성 향상: 향후 기능 추가 시 변경 영향도를 최소화
메모리 효율 개선: 불필요한 try...catch 재귀 호출 제거
가독성 증가: 더 직관적인 코드 작성 (ex. Math.max() 활용, map()으로 데이터 변환)
이제 다음 미션에서도 보다 명확한 구조와 효율적인 코드 작성을 이어나갈 수 있도록 이번 리팩토링의 경험을 기억하고 더 좋은 코드를 고민하겠습니다! 🚀✨
긴 글 읽어주셔서 감사합니다. 좋은 하루되세요!😃
안녕하세요 카멜!
데일리 미팅 같은 조인 써밋입니다🙃
바쁘신 와중에도 1주차 회고까지 작성해주시고 저도 더 분발 해야겠네요..🔥🔥🔥
한 주 동안 고생 많으셨고 앞으로도 화이팅입니다~!