[우아한 테크코스] - 자동차 경주 미션 step 1 회고

jiny·2024년 2월 15일
0
post-thumbnail

Intro

step 1 미션의 경우 정말 빠르게 지나갔던거 같은데, 우여곡절 끝에 미션을 마무리 하고 리뷰 요청 까지 제 시간에 할 수 있었다.

짧은 기간이었지만 페어 프로그래밍, 단위 테스트 등에 대해 학습하고 적용했던 것들을 회고 해보려고 한다.

미션 결과물

https://github.com/woowacourse/javascript-racingcar/pull/275

미션 진행 결과는 여기서 확인해볼 수 있다.

👯‍♀️ 페어 프로그래밍

👏 진행 방식

페어 프로그래밍에 대해 파트너인 프룬과 나 또한 처음이었기 때문에 미리 규칙 들과 진행 순서를 정해서 계획적으로 움직일 필요가 있다고 느꼈다.

우선 규칙과 진행 순서는 아래와 같다.

✨ 규칙

  • 페어 프로그래밍 시 드라이버 & 네비 게이터 방식을 사용한다.
    • 드라이버
      • 코드 작성을 주로 담당한다.
      • 현재 진행하는 것을 말로 표현하며 진행하며, 네비게이터와의 대화 역시 함께 진행한다.
    • 네비게이터
      • 코드 작성에 대한 의견 제안을 주로 담당한다.
      • 코드 작성 중에 의문이 드는 점이나 잘못 흘러가고 있는 상황을 대화로 바로 잡는다.
  • 드라이버와 네비게이터는 1시간 작업 10분 휴식 후 교대 한다.
  • 네비게이터는 제안할 때 반드시 충분한 근거를 가지고 토론을 제안 해야 하며, 드라이버는 그런 네비게이터의 의견을 최대한 존중한다.
  • 당일 해야 할 목표를 정한 후 그 목표는 최대한 완수하려고 노력한다.
  • 의견 충돌이 발생하면 반드시 대화로 해결 한다.
    페어 진행 순서는 아래와 같습니다.

📈 진행 순서

  1. 미션 레포지토리를 fork 한다.
  2. 미션 레포지토리를 양측 모두 clone 한다.
    • git clone <Repo URL>
  3. 서로의 local 레포지토리에 remote 한다.
    • git remote add pair <Repo URL for pairs fork>
  4. vs code → live share → share(드라이버) or join(네비게이터) → 링크 입력 및 전달
  5. 드라이버는 작업 후 commit - push 한다.
    • 이 때, 네비게이터의 co-author를 드라이버가 함께 추가한다.
  6. push 까지 마치고 교대 시 네비게이터는 드라이버의 레포지토리를 pull 받은 후 live share를 키고 작업을 진행한다.
    • git pull pair <branch name>
  7. 1 - 6 과정을 step1이 끝날 때 까지 반복한다.

💬 장점과 보완 할 점

페어를 2일 동안 진행하며 장점과 보완 할 점이 정말 뚜렷했다고 생각했다.

✔️ 장점

나의 부족한 점을 페어와 함께 채워나갈 수 있었던 점이 정말 좋았던거 같다.

나의 단점은 하다가 막히거나 하기 싫어질 때쯤 2% 아쉬운 코드를 수정하지 않는다는 단점이 있었고 이 단점은 스스로 인지해도 기분에 따라 나도 모르게 그래왔었기 때문에 혼자서 해결하기 어려웠다.

하지만, 페어와 함께 진행하며 정말 코드 하나 작성할 때 마다 좋은 코드인지에 대해 토론을 진행했고,하기 싫다는 마음가짐이 페어와의 토론으로 무뎌져 정말 꼼꼼하게 코드를 검토하고 작성했던거 같다.

또한, 내가 몰랐던 점을 페어의 코드를 통해 새롭게 파악하고 확인해볼 수 있어 좋았다.

구현 전에 서로의 코드 스타일을 파악하기 위해 크리스마스 미션으로 코드를 설명 하는 시간을 따로 가졌었는데, 여러 질문 들을 통해 맞춰 가는 과정에서 해당 문법을 사용한 이유, 설계 방식 등에 대해 인사이트를 얻어갈 수 있었다.

✔️ 보완 할 점

프룬과 페어를 진행할 때, live share라는 vscode extension을 활용하여 진행했는데, 교대 할 때 마다 드라이버가 share url을 보내주면 네비게이터가 그것을 수락하는 형태로 번갈아가며 진행했다.

또한, 커밋 마다 co-author를 추가하여 공동 작업임을 명시했었다.

하지만 다른 크루들에게 물어볼 땐 우리와 같은 형태가 아닌 하나의 컴퓨터에서 작업하는 형태로 진행했다는 이야기를 듣게 되었다.

굳이 코치 님들이 이렇게 해라~고 명시한게 아니라면 다른 크루 분들이 적용한 방식이 시간을 더 효율적으로 썼을거 같아 그 점이 좀 아쉬웠다.

✨ 칭찬할 점

👏 class를 정말 필요할 때만 사용해보기

// Cars.js

import CarEngine from '../CarEngine/module.js';

class Cars {
  #racingCarDetails;

  constructor(racingCarNames) {
    this.#createInitRacingCarDetails(racingCarNames);
  }

  #createInitRacingCarDetails(racingCarNames) {
    this.#racingCarDetails = racingCarNames.map((carName) => ({ carName, moveCount: 0 }));
  }

  #updateMoveCounts(randomMoveCounts) {
    const racingCarDetailsCopy = this.#racingCarDetails.map((detail) => ({ ...detail }));

    randomMoveCounts.forEach((randomMoveCount, index) => {
      racingCarDetailsCopy[index] = CarEngine.triggerMove(racingCarDetailsCopy[index], randomMoveCount);
    });

    return racingCarDetailsCopy;
  }

  moveCars(randomMoveCounts) {
    this.#racingCarDetails = this.#updateMoveCounts(randomMoveCounts);

    return this.#racingCarDetails;
  }
}

export default Cars;

이 Cars 모듈의 경우 입력 받은 자동차 이름 들로 racingCarDetails(자동차 이름, 이동 횟수가 담긴 객체 배열)을 세팅한 후 각 turn 마다 랜덤 값에 따른 이동이 완료 된 racingCarDetails를 반환하는 class다.

이 모듈만 유일하게 class를 적용했었는데, 이유는 아래와 같다.

// RacingGame.js

import Cars from '../Cars/module.js';

const RacingGame = Object.freeze({
  startRace({ racingCarNames, tryCount, randomMoveCounts }) {
    const cars = new Cars(racingCarNames);

    const racingResult = Array.from({ length: tryCount }, (_, index) => cars.moveCars(randomMoveCounts[index]));

    return racingResult;
  },
});

export default RacingGame;

RacingGame의 경우 tryCount 만큼 게임을 진행하여 나온 자동차 경주 결과 배열을 반환하는 도메인 모델이다.

Array.from을 통해 moveCars 메서드가 반환하는 각 turn의 racingCarDetails을 추가하는 형태로 startRace를 구현했다.

즉, Cars는 이전 turn의 자동차 경주 결과를 기반으로 현재 turn에서 자동차 들을 이동 시킨 racingCarDetails을 반환하는 형태다.

이전 turn의 자동차 경주 결과의 생명주기가 지속되어야 했기 때문에, 객체 리터럴을 사용하지 않고 class를 사용하게 되었다.

즉, class는 멤버 변수의 생명 주기를 오랫동안 가져가야 할 경우 사용하며, 그 이외에는 객체 리터럴을 사용하는 것으로 구현을 진행했다.

위와 같은 방식으로 구현하다 보니 불필요한 class 사용을 줄였고, 개인적으로 이전 보다 코드의 가독성이 보기 좋아졌다.

또한, 대부분 순수 함수 형태다 보니 어떤 값이 들어와 어떤 로직을 수행하고, 어떤 값을 반환하는지 예측하기 쉬워졌다.

중요 도메인 로직 내 모킹 없는 테스트 진행

이번 자동차 경주 미션에서 핵심 도메인은 자동차 이동과 우승자 결정이었다.

따라서, 관련 도메인 모델인 RacingGame, Cars, CarEngine, RacingWinnerRecorder에 대해 모든 메서드가 순수 함수를 유지하도록 하고 싶었다.

// RacingGame.js

const RacingGame = Object.freeze({
  startRace({ racingCarNames, tryCount, randomMoveCounts }) {
    const cars = new Cars(racingCarNames);

    const racingResult = Array.from({ length: tryCount }, (_, index) => cars.moveCars(randomMoveCounts[index]));

    return racingResult;
  },
});

따라서, RacingGame 내에서 tryCount와 racingCarName의 크기 만큼의 랜덤 값 배열을 주입하는 형태로 코드를 전개했으며, cars와 carEngine에도 randomMoveCounts내 존재하는 랜덤 값을 기반으로 코드를 동작하도록 했다.

// RacingGameController.js
function processRacingGame({ racingCarNames, tryCount }) {
  const randomMoveCounts = RandomMoveCountMaker.execute(tryCount, racingCarNames.length);
  const racingResult = RacingGame.startRace({ racingCarNames, tryCount, randomMoveCounts });

  const finalRacingResult = racingResult.at(-1);
  const racingWinners = RacingWinnerRecorder.createRacingWinners(finalRacingResult);

  OutputView.printRacingResult(racingResult);
  OutputView.printRacingWinners(racingWinners);
}

또한, controller 내부에서 RandomMoveCountMaker에게 randomMoveCounts를 받아 RacingGame에 넘겨주도록 했기 때문에 문제 없이 실행할 수 있었다.

이로 인해 관련 테스트 코드들에 모킹 없이 테스트를 진행하여 예측 가능한 테스트를 만들어 낼 수 있었다.

🥲 아쉬웠던 점

🥹 통합 테스트의 부재

프리코스에선 ApplicationTest.js 파일이 있었기 때문에, 편하게 통합 테스트 결과를 확인할 수 있었다.

이번 미션에는 ApplicationTest 파일이 없었음에도 불구하고, Controller 테스트를 하는 것을 깜빡하고 말았다.

각 모듈이 정상적으로 작동되더라도, Controller가 제대로 실행되지 않으면 사실상 cli 기반의 application은 정상적으로 작동하지 않을 수 있다는 위험성이 존재한다고 생각했다.

따라서, 안정적인 프로덕션 코드를 유지하기 위해 추가적으로 통합 테스트 코드를 추가해볼 생각이다.

🥹 jsDoc

TypeScript를 사용하지 못하기 때문에, TypeScript 처럼 타입 자동 완성과 문서화 기능을 제공할 수 있는 방법에 대해 고민하다 jsDoc을 사용했다.

// jsDoc.js

/**
 * @typedef {object} CommonValidationType
 * @property {string} errorMessage - 유효성 검사 실패 시의 에러 메시지
 * @property {(inputValue : string) => boolean} isValid - 유효성 검사 함수
 */

/**
 * @typedef {object} CommonValidationTypes
 * @property {CommonValidationType} emptyValues - 입력 값이 비어있는지를 검사하기 위한 객체
 * @property {CommonValidationType} existSpaces - 입력 값에 공백이 포함되어 있는지를 검사하기 위한 객체
 */

export {}

따라서, utils/jsDoc.js 내 각 타입 들에 대해 typedef를 사용하여 타입을 저장했다.

import { SYMBOLS } from '../../constants/symbols.js';
import { startValidation } from '../startValidation.js';
import { CAR_NAME_RANGE, CAR_NAME_REGEX, MIN_CAR_LENGTH } from './constant.js';

/**
 * @module CarNameValidator
 * 자동차 이름 입력에 대한 유효성 검사를 수행하는 모듈
 */
const CarNameValidator = Object.freeze({
  /**
   * @type {import('../../utils/jsDoc.js').CarNameValidationTypes}
   */
  validationTypes: Object.freeze({
    notCommaSeparated: Object.freeze({
      errorMessage: `자동차 이름은 ${SYMBOLS.comma}로만 구분 가능합니다.`,
      isValid(inputValue) {
        return CAR_NAME_REGEX.test(inputValue);
      },
    }),

    duplicateCarNames: Object.freeze({
      errorMessage: `중복된 자동차 이름이 존재합니다.`,
      isValid(inputValue) {
        const carNames = inputValue.split(SYMBOLS.comma);

        return new Set(carNames).size === carNames.length;
      },
    }),

    invalidCarLength: Object.freeze({
      errorMessage: `자동차는 ${MIN_CAR_LENGTH}대 이상 부터 가능합니다.`,
      isValid(inputValue) {
        return inputValue.split(SYMBOLS.comma).length >= MIN_CAR_LENGTH;
      },
    }),

    invalidCarNameLength: Object.freeze({
      errorMessage: `자동차 이름은 ${CAR_NAME_RANGE.min} ~ ${CAR_NAME_RANGE.max}자의 범위만 가능합니다.`,
      isValid(inputValue) {
        return inputValue
          .split(`${SYMBOLS.comma}`)
          .every((name) => name.length >= CAR_NAME_RANGE.min && name.length <= CAR_NAME_RANGE.max);
      },
    }),
  }),

  /**
   * 사용자의 입력 값에 대한 유효성 검사를 수행하고 에러를 발생시킬 수 있음
   * @param {string} inputValue - 사용자의 입력 값
   * @throws {AppError} 유효성을 만족하지 않을 경우 에러 발생
   * @returns {void}
   */
  check(inputValue) {
    startValidation(this.validationTypes, inputValue);
  },
});

export default CarNameValidator;

그 후, 각 모듈에서 그 typedef 들을 import 해서 사용하는 형태로 jsDoc을 활용했다.

jsDoc을 통해 부분적인 코드 자동 완성과 기능 문서화를 할 수 있다는 장점을 다시 한번 확인할 수 있었다.

단, 시간이 부족했던 탓에 모든 모듈에 적용하지 못했던 점이 아쉬웠다.

🙏 끝으로

아쉬웠던 점이 꽤 있었지만 무엇보다 step1을 사고 없이 마무리 할 수 있어서 정말 다행이었다. (그 만큼 시간이 부족했기 때문이다.)

잘하지 못했던 점들을 더 보완해서 step2는 성능적으로 문제 없이 더 알아보기 쉽고 직관적인 코드를 작성해보려 노력해야겠다!

0개의 댓글