2주차
2주차에는 이전 주차에 고민하고 리뷰를 받았던 부분들을 중점적으로 다루었다.
불필요한 코드는 최소화 하고 클린한 코드를 작성하도록 신경쓰며, 각 역할에 맞도록 분리하는데에 집중하였다.
2주차 미션은 자동차 경주 프로그램 구현이었다. n대의 자동차 이름을 입력받고, 원하는 이동 시도 횟수를 입력하여 우승자를 가려내는 프로그램을 만드는 것이다.
이번 주차 역시 지난 주차에 비해 난이도가 크게 상이하지 않다고 생각되어, 익숙해지고자 하는 포인트에 집중하고자 하였다.
- 역할 분리
이번 주차에는 MVC 패턴이라는 것에 집착하기 보단, 함수의 이름만으로 어떤 역할을 하는지 쉽게 파악할 수 있도록 하고, 코드가 읽히는 흐름이 명료하도록 함수를 나누며, 하나의 함수가 여러 개의 논리를 담지 않도록 하였다.
async run() {
const carNamesInput = await getCarNamesInput();
const roundInput = await getRoundInput();
printLineBreak();
const cars = handleCarCreation(carNamesInput);
printMessage('실행 결과');
const movedCars = handleMoveController(cars, roundInput);
const winnerNames = findWinner(movedCars);
printWinner(winnerNames);
}
엔트리 메서드에서 핵심이 되는 기능만을 명시적으로 보여주도록 하고자 하였다.
1주차 미션에서 코드 리뷰를 해보며 다양한 방식의 코드를 보았는데, 엔트리에서 복잡하게 얽혀있는 코드는 상대적으로 가독성이 떨어진다는 느낌을 받았다. 반대로 간략하게 작성하여도 그 의미를 알 수 없게 짜여진 코드도 직관적으로 의미가 와닿지 않기 때문에 좋지 않은 듯 하였다.
이 두가지 생각을 바탕으로 2주차 엔트리에서는 가급적이면 핵심 기능을 위치시키고 그 로직이 의미하는 역할이 무엇인지 뚜렷하게 표현할 수 있도록 하였다.
이 방식으로 하고자 하면서 입력, 실행, 출력의 3단계중에서 실행 부분을 크게 두 가지로 나누게 되었다.
carController 라는 컨트롤러를 생성하여 자동차 이름의 유효성을 검증하고 로직 내에서 사용할 수 있도록 파싱하는 기능을 수행할 수 있도록 하고, moveController 라는 컨트롤러를 생성하여 라운드마다 자동차의 포지션 상태를 변경하는 기능을 수행할 수 있도록 하였다.
export function handleMoveController(cars, roundsInput) {
const rounds = validateRounds(roundsInput);
let updatedCars = [...cars];
for (let i = 0; i < rounds; i++) {
updatedCars = updatedCars.map(moveCar);
for (let j = 0; j < updatedCars.length; j++) {
printCarPosition(updatedCars[j]);
}
printLineBreak();
}
return updatedCars;
}
이렇게 해당 컨트롤러가 어떤 역할을 하는 건지 간단히 보일 수 있다.
- 함수형 구현?
이번 구현에도 모든 로직을 함수형으로 구현하였다. 1주차에 클래스를 사용해야할 이유를 느끼지 못했고, 역할 분리에 집중했기 때문이었다.
이번 구현에서 가장 아쉬웠던 부분은 각 자동차 마다의 포지션 값에 대한 인식이었다.
각 라운드마다 이전 라운드의 자동차 포지션 상태를 이용하고 해당 상태를 변화시키기 위한 메서드가 존재한다. 이 부분을 클래스화 하여 객체로 만들었다면 더 좋은 구현을 할 수 있었을 것이라고 생각된다.
이유는 다음과 같다.
export function handleMoveController(cars, roundsInput) {
const rounds = validateRounds(roundsInput);
let updatedCars = [...cars];
for (let i = 0; i < rounds; i++) {
updatedCars = updatedCars.map(moveCar);
for (let j = 0; j < updatedCars.length; j++) {
printCarPosition(updatedCars[j]);
}
printLineBreak();
}
return updatedCars;
}
해당 컨트롤러에서 각 라운드에 대한 상태 개념이 존재하지 않기 때문에 매 반복마다 출력이 필요하다.
책임 분리에 신경을 쓰고자 했음에도 당시에는 도무지 이를 분리할 방법이 생각나지 않았다.
라운드마다의 결과를 히스토리처럼 기록한다는 생각만 하였어도 클래스에 대한 의미가 느껴졌을 것 같다.
이 뿐만이 아니라
export function moveCar(car) {
const randomValue = generateRandomNumber();
if (decideMove(randomValue)) {
return { ...car, position: car.position + '-' };
}
return car;
}
export function decideMove(randomNumber) {
return randomNumber >= 4;
}
이 두 가지 로직을 분리하고자 새로이 파일을 만들고 메서드로 나누는 것이 과연 정말로 가독성에 도움이 되는 것이 맞을까? 하는 의문이 들었다.
분명 여러 책임을 맡으면서 한 메서드 내부의 로직이 복잡해지는 것을 막아 가독성을 높이는 것이 목적인데, 이렇게까지 분리한다면 오히려 코드를 수월하게 파악하는데에 방해가 될 것 같았다.
이를 클래스화하여 적용하였다면, 충분히 하나의 객체 안에서 이루어질 수 있도록 구현가능했을 것이다.
회고
리뷰를 통해 새로운 관점을 가질 수 있었던 한 주차였다.
클래스화를 하지 않아도 충분히 가독성이 좋게 할 수 있는 것은 맞지만, 일관된 방법을 통해 로직간의 응집도를 높이고 결합도는 낮출 수 있는 방법이라고 생각된다.
특히 이번 주차처럼 자동차 데이터 , 라운드 결과 등 하나의 핵심 기능과 관련되어 있는 메서드들은 하나의 객체 안에서 관리하여 응집도를 높이고 가독성을 좋게 할 수 있고, 여전히 별도의 메서드로 관리할 수도 있다.
1주차에는 목적에 집중했다면, 2주차를 마치며 방법에 대해 생각해볼 수 있었던 것 같다.