우아한테크코스 6기 프리코스 - 숫자 야구 미션 회고(2)(with 나만의 클린코드 원칙 적용하기)

jiny·2023년 10월 25일
0
post-thumbnail

이번 글에선 이전 글의 회고를 이어갈 예정이며 두 번째 목표인 나만의 클린코드 원칙 적용하기를 달성까지의 과정과 이를 통해 느끼고 배웠던 점들에 대해 서술해 보려고 합니다.

나만의 클린코드 원칙 적용하기

⚠️ 다양한 레퍼런스를 통해 참고한 내용이 있음을 먼저 밝힙니다.

우아한테크코스 프리코스가 추구하는 클린 코드 원칙을 대부분 수용했으며, 추가적으로 로버트 마틴 C: 클린 코드에서 제안하는 내용들과 일부 고려해볼만한 원칙들을 추가했습니다.

클린 코드라는 말이 굉장히 추상적이고, 다양한 방향성이 존재하지만 제가 생각하는 클린코드의 기준은 아래와 같습니다.

  1. 다수의 개발자가 쉽게 이해할 수 있는 코드
  2. 코드 일관성이 잘 유지되고 있는 코드
  3. 높은 재사용성과 함께 유지 보수하기 쉽고 테스트하기 쉬운 코드

이렇게 3가지 항목을 기준을 두고 구체적인 클린 코드 항목 들과 적용 사례들을 말해보려고 합니다.

다수의 개발자가 쉽게 이해할 수 있는 코드

제가 생각하는 다수의 개발자가 쉽게 이해할 수 있는 코드는 아래와 같습니다.

  1. 코드의 가독성에 초점을 맞춰 작성된 코드의 의도를 명확히 드러낼 수 있다.
  2. 부족한 부분은 문서화를 통해 보완 될 수 있어야 한다.

else 예약어를 지양하기

class Computer {
  // ... 
  #calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseballNumber, digit }) {
    let newStrike = strike;
    let newBall = ball;

    if (this.#isStrike(playerBaseballNumber, digit)) {
      newStrike += 1;
    } else {
      newStrike = strike;
    }

    if (this.#isBall(playerBaseballNumber, digit)) {
      newBall += 1;
    } else {
      newBall = ball;
    }

    return {
      strike: newStrike,
      ball: newBall,
    };
  }

  comparePlayerBaseball(playerBaseball) {
    return playerBaseball.reduce(
      ({ strike, ball }, playerBaseballNumber, digit) =>
        this.#calculateCompareResult({
          prevCompareResult: { strike, ball },
          playerBaseballNumber,
          digit,
        }),
      { strike: 0, ball: 0 },
    );
  }
}

export default Computer;

Computer 클래스의 경우 유저의 야구공을 받아 비교 결과를 계산(calculateCompareResult) 후 그 결과를 반환하고 있으며, calculateCompareResult의 경우 strikeball의 갯수를 세어 계산된 비교 결과를 반환하고 있습니다.

이때, strike이거나 ball이면 기존 값에 +1을 더하며, 아니라면 원래의 결과 값을 담고 있습니다.

이러한 if - else 문은 코드 길이가 길어지기 때문에 보는 사람으로 하여금 더 많은 해석을 요구하며, else자체가 부정을 나타내기 때문에 한번 더 생각을 하게끔 유도 합니다.

#calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseball, digit }) {
	return {
      strike: strike + (this.#isStrike(playerBaseball, digit) ? 1 : 0),
      ball: ball + (this.#isBall(playerBaseball, digit) ? 1 : 0),
    };
}

이렇게 삼항 연산자 활용하면 코드를 더 깔끔하게 표현할 수 있으며 코드의 가독성을 향상 시킬 수 있습니다.

메서드의 인자 수를 제한하기

#calculateCompareResult(prevCompareResult, playerBaseballNumber, digit) {
  const {strike, ball} = prevCompareResult
  return {
    strike: strike + (this.#isStrike(playerBaseballNumber, digit) ? 1 : 0),
    ball: ball + (this.#isBall(playerBaseballNumber, digit) ? 1 : 0),
  };
}

comparePlayerBaseball(playerBaseball) {
  return playerBaseball.reduce(
    ({ strike, ball }, playerBaseballNumber, digit) =>
      this.#calculateCompareResult({
        prevCompareResult: { strike, ball },
        playerBaseballNumber,
        digit,
      }),
    { strike: 0, ball: 0 },
  );
}

현재 comparePlayerBaseball 내에서 calculateCompareResult에 전달하려는 함수의 인자가 3개 이상인 것을 알 수 있습니다.

이는 호출 가능한 인자의 조합이 총 8가지가 됩니다.

즉, 아래와 같이 다양한 인자의 조합으로 호출할 수 있다는 얘기이며, 실제로 인자가 많아지면 호출할 때 헷갈리게 되어 인자 순서를 착각 할 수 있습니다.

인자 순서가 잘못되게 되면, 인자를 건네 받은 함수는 잘못된 순서로 실행하게 되고 디버깅 하기 어려운 버그를 초래할 수 있습니다.

// 정말 다양한 인자 순서
this.#calculateCompareResult(
  playerBaseball
  prevCompareResult,
  digit,
)
  
this.#calculateCompareResult(
  digit,
  playerBaseball
  prevCompareResult,
)
  
this.#calculateCompareResult(
  playerBaseball
  digit,
  prevCompareResult,
)
  
// ...
#calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseball, digit }) {
  return {
    strike: strike + (this.#isStrike(playerBaseball, digit) ? 1 : 0),
    ball: ball + (this.#isBall(playerBaseball, digit) ? 1 : 0),
  };
}

// 인자 순서를 다르게 해도 상관이 없다.
this.#calculateCompareResult({
  prevCompareResult: { strike, ball },
  playerBaseball,
  digit,
}),
  
this.#calculateCompareResult({
  playerBaseball,
  prevCompareResult: { strike, ball },
  digit,
}),  

하지만 객체로 묶어 표현한다면, 인자 순서를 다르게 하든, 매개 변수 순서를 다르게 하든 아무런 상관이 없어져 예측 가능한 동작을 만들어 낼 수 있으며, 보는 사람으로 하여금 가독성도 좋아지는 것도 확인 할 수 있습니다.

의도를 드러낸 네이밍 사용하기

async #start() {
  this.#requirePrintStartGame();
  while (true) {
    const playerBaseball = await this.#requirePlayerBaseball();
    const { strike, ball } =   this.#requireCompareResult(playerBaseball);
    this.#requirePrintCompareResult({ strike, ball });
    if (strike === GAME_TERMS.baseball.digit) break;
  }
  this.#requirePrintExitGame();
}

start라는 네이밍은 보는 사람으로 하여금, 어떤 일을 수행하고 있는지 모호하게 들릴 수 있으며 다소 추상적이라고 생각할 수도 있습니다.

이럴 때 저는 의도를 드러낸 네이밍을 통해 해당 함수의 역할을 명확히 드러내려고 하며, 아래와 같은 과정을 통해 해당 네이밍을 만들어내고 있습니다.

  1. 현재 메서드가 어떤 작업을 수행하고 있는지 정리한다.
  2. 어떤 역할을 수행하고 있는지 보는 사람으로 하여금 쉽게 확인할 수 있도록 네이밍 한다.

1. 현재 메서드가 어떤 작업을 수행하고 있는지 정리한다.

  1. 게임 시작 메시지를 출력한다.
  2. 플레이어로 부터 야구공을 입력 받는다.
  3. 입력 받은 야구공을 통해 컴퓨터와 비교하여 결과를 얻어낸다.
  4. 결과를 출력한다.
  5. 만약 3스트라이크라면 게임을 종료한다.
  6. 종료 메시지를 출력한다.

start가 수행하고 있는 것을 살펴보면 시작 메시지를 출력 후, 게임을 진행하다가 만약 종료 되면 종료 메시지를 출력하고 있습니다.

2. 어떤 역할을 수행하고 있는지 보는 사람으로 하여금 쉽게 확인할 수 있도록 네이밍 한다.

그러면 이 메서드를 한 줄로 요약하면 전체적인 게임 진행을 수행하는 메서드라는 것을 드러낼 수 있다는 것을 확인할 수 있습니다.

따라서, start라는 네이밍 보단 processGame이라는 네이밍을 통해 해당 메서드의 역할을 드러내어 어떤 로직을 수행하는지 내부 로직을 살펴보지 않아도 유추할 수 있습니다.

축약된 네이밍은 지양하기

export const genRandomNumberInRange = (a, b) =>
  Random.pickNumberInRange(a, b);

genRandomNumberInRange은 최소 숫자 a와 최소 숫자 b를 받아 해당 범위 내의 숫자를 반환하는 함수 입니다.

하지만, gen이라는 네이밍은 보는 사람으로 하여금 generate라는 인상을 쉽게 떠올리기 어려우며 몇 번 생각해야만 합니다.

또한, abaminNumber인지, bmaxNumber인지 쉽게 유추해내기 어렵습니다.

export const pickRandomNumberInRange = (minNumber, maxNumber) =>
  Random.pickNumberInRange(minNumber, maxNumber)

이렇게 축약 시키지 않고 정확한 의미를 전달할 수 있는 네이밍을 사용하면 함수명이 길어져도 보는 사람이 쉽게 파악할 수 있기 때문에 축약된 네이밍은 지양하는 것이 좋습니다.

jsDoc, Class Diagram, Flow Chart 등과 같은 시각화 자료 활용하기

아무리 위와 같은 사항들을 모두 충족시켰다고 해도, 코드를 처음 확인하는 분들의 경우 아키텍처를 파악하는데 시간이 소요되며, 코드의 전체 구조나 동작 원리를 파악하는데 어려움이 있을 수 있습니다.

또한, 메서드 내 어떤 타입을 받아 반환하는지는 파악하기 위해 정말 많은 시간이 소요 될 수 있습니다.

이런 문제 들은 Class Diagram, Flow Chart 등의 도구 들과 자바스크립트 한정으론 jsDoc을 활용하여 문제를 해결 할 수 있습니다.

/**
 * 주어진 범위 내에서 무작위 숫자를 선택하여 반환하는 함수
 * @param {number} minNumber - 선택할 수 있는 최소 숫자
 * @param {number} maxNumber - 선택할 수 있는 최대 숫자
 * @returns {number} 선택된 무작위 숫자
 */
export const pickRandomNumberInRange = (minNumber, maxNumber) =>
  Random.pickNumberInRange(minNumber, maxNumber)

jsDoc은 일반적인 주석과 다르게, 어떤 인자를 받아 어떤 값을 반환하는지의 타입을 표현할 수 있으며, 전체적인 동작 및 매개 변수, 반환 값 등의 역할 또한 명시적으로 작성할 수 있습니다.

이외에도 @private @typedef 등의 많은 어노테이션을 제공하니 자세한 사항 들은 jsDoc docs에서 확인하실 수 있습니다.

또한, 저의 경우 README.md 링크 추가하기클래스 다이어그램을 통해서는 전체적인 아키텍처 및 의존성 방향과 각 메서드 들을 문서화 하려 했으며, 플로우 차트를 통해선 애플리케이션의 전체적인 동작에 대해 쉽게 파악할 수 있게 표현하고자 했습니다.

코드 일관성이 잘 유지되고 있는 코드

제가 생각하는 코드 일관성이 잘 유지되고 있는 코드는 아래와 같습니다.

  1. 주어진 코드 컨벤션을 잘 사용할 수 있어야 한다.
  2. 나만의 네이밍 컨벤션을 따로 관리할 수 있어야 한다.
  3. Linting이나 포맷팅 도구를 통해 코드가 정돈될 수 있어야 한다.

자바스크립트 코드 컨벤션을 올바르게 사용하기

우아한테크코스 프리코스의 경우 Airbnb JavaScript Style Guide을 기반한 코드 컨벤션을 따르고 있습니다.

따라서, 해당 코드 컨벤션에 맞게 적용만 하더라도 프리코스가 추구하는 코드에 근접하게 사용하고 있을 것이라 예상했고 이런 방향성이 클린코드에도 적용될 것이라고 생각했습니다.

정말 많은 항목들이 존재하기 때문에, 놓칠 수도 있는 항목들 위주로 실제 미션 코드와 함께 설명드리겠습니다.

11.1 Don’t use iterators. Prefer JavaScript’s higher-order functions instead of loops like for-in or for-of

// bad
comparePlayerBaseball(playerBaseball) {
  let strike = 0;
  let ball = 0;
  for (let i = 0; i < 3; i += 1) {
    if (playerBaseball[i] === this.#baseball[i]) {
        strike += 1;
    }
  }
  for (let i = 0; i < 3; i += 1) {
    if (playerBaseball[i] !== this.#baseball[i] && this.#baseball.includes(playerBaseball[i])) {
        ball += 1;
    }
  }
  return { strike, ball };
}
  
// good
comparePlayerBaseball(playerBaseball) {
  return this.#baseball.reduce(
    ({ strike, ball }, _, digit) =>
      this.#calculateCompareResult({
        prevCompareResult: { strike, ball },
        playerBaseball,
        digit,
    }),
    { strike: 0, ball: 0 },
  );
}

자바스크립트객체지향, 함수형 패러다임을 모두 사용할 수 있는 프로그래밍 언어입니다. 이로 인해, 함수일급 객체로써 사용이 가능하며 고차함수를 사용해 반복되는 로직을 개선할 수 있습니다.

특히, for문의 경우 forEach, reduce, filter 등 콜백 함수를 인자로 받는 고차 함수를 사용하게 되면 로직의 가독성과 재사용성을 증가 시킵니다.

21.1 Yup(semicolon)

// bad
export const pickRandomNumberInRange = (minNumber, maxNumber) =>
  Random.pickNumberInRange(minNumber, maxNumber)

// good
export const pickRandomNumberInRange = (minNumber, maxNumber) =>
  Random.pickNumberInRange(minNumber, maxNumber);

JavaScript V8 엔진세미콜론이 누락된 경우 코드를 파싱하는 과정에서 세미콜론이 없을 경우 자동으로 삽입합니다.

이를 통해 코드의 가독성을 향상시키고 코드 작성을 쉽게 해주지만, 일부 상황에서는 예상치 못한 문제를 일으킬 수 있습니다.

예를 들어, 줄바꿈 후에 오는 토큰이 현재 문장의 일부로 해석될 수 없는 경우 해당 줄바꿈 직전에 세미콜론을 삽입하여 문제를 일으킬 수 있습니다.

저의 경우 prettier"semi": true 옵션을 통해 semicolon을 관리하여 문제를 최소화 하고 있습니다.

23.10 You may optionally uppercase a constant only if it (1) is exported, (2) is a const (it can not be reassigned), and (3) the programmer can trust it (and its nested properties) to never change.

// bad
export const SYMBOLS = Object.freeze({
  EMPTY_STRING: '',
  SPACE: ' ',
  COMMA: ',',
});

// good
export const SYMBOLS = Object.freeze({
  emptyString: '',
  space: ' ',
  comma: ',',
});

상수가 객체인 경우 내부 프로퍼티 까지 상수로 적용하는 것은 불필요하며, 이렇게 하는 것이 아무 의미 없기 때문에 소문자로 사용하는 것을 지향 해야 한다고 언급하고 있습니다.

그래서 저 또한 이전엔 상수가 객체인 경우 내부 프로퍼티 까지 snake_case를 적용했었지만 컨벤션을 적용하여 불 필요한 코드 작성을 줄이기 위해 노력하고 있습니다.

나만의 네이밍 컨벤션을 만들어 관리하기

이번 미션에서 MVC 패턴을 구현하며, 특히 controller의 역할인 애플리케이션 흐름 제어다른 계층들과의 상호작용을 명확히 드러낼 수 있는 네이밍에 대해 많은 고민을 했습니다.

고민 끝에 2가지 컨벤션을 만들어 관리하고자 했습니다.

  • require ~ - 다른 레이어와 상호작용을 하여 값을 만들어내고, 기능 로직을 실행하는 메서드
  • process - 하나의 큰 기능(ex - 게임 실행, 명령어 처리)을 실행하기 위한 메서드

대부분의 메서드를 processrequire 네이밍 컨벤션으로 통일하여 controller가 수행하는 역할을 명시적으로 드러내고, 보는 사람으로 하여금 어떤 작업을 진행하고 있는지 명확하게 드러내어 가독성을 향상시키고자 했습니다.

또한, 전체적으로 네이밍에 통일성이 생기다보니 코드를 바라보기도 편하다는 것을 알 수 있습니다.

📝 정적 팩토리 메서드 네이밍 컨벤션

  • of - 매개변수가 2개 이상인 경우
  • from - 매개변수가 1개인 경우
  • create - 매개변수가 없는 경우

MVC 패턴이 아니라도 또 다른 네이밍 컨벤션을 만들어볼 수 있는데요. 저의 경우 정적 팩토리 메서드를 사용할 때 네이밍 컨벤션을 만들어 관리하고자 했습니다.

일단 네이밍 컨벤션 부터 살펴보면, 보통 정적 팩토리 메서드의 경우 매개변수에 따라 자신의 인스턴스를 반환하는 메서드이기 때문에, 매개변수 갯수에 따라 관리하면 좋을거 같아 위와 같은 컨벤션을 정했습니다.

// bad
async #requirePlayerBaseball() {
  const inputPlayerBaseball = await this.#inputPlayerBaseball();
  new BaseballValidator(inputPlayerBaseball).validateBaseball();
  return inputPlayerBaseball.split(SYMBOLS.emptyString).map(Number);
}

// good
async #requirePlayerBaseball() {
  const inputPlayerBaseball = await this.#inputPlayerBaseball();
  BaseballValidator.from(inputPlayerBaseball).validateBaseball();
  return inputPlayerBaseball.split(SYMBOLS.emptyString).map(Number);
}

new 키워드를 통한 메서드 체이닝 보단, 정적 팩토리 메서드를 통해 메서드 체이닝을 하게 되면 보는 사람으로 하여금 가독성을 높일 수 있습니다.

물론 new BaseballValidator(inputPlayerBaseball)validator라는 변수를 만들어 체이닝 할 수 있지만 코드 취향의 차이라고 생각합니다!

Linting 및 코드 포맷터 도구를 활용하기

기본적으로 Airbnb JavaScript Style Guide가 제시되어 있고, 클린 코드 규칙을 여러번 정독해서 읽었지만 그 규칙들을 모두 아는 상태로 적용하기에는 쉬운 일이 아닙니다.

이런 귀찮을 수 있는 작업 들을 Linting 및 코드 포맷터 도구를 활용하면 이런 작업 들을 자동화 시킬 수 있으며, 프로젝트 전체에서 동일한 코드 스타일과 규칙을 유지시키기 때문에 프로젝트의 일관성에서도 이점을 가져갈 수 있습니다.

eslintrc.json

{
  "env": {
    "es2021": true,
    "node": true,
    "jest": true
  },
  "extends": ["airbnb-base", "prettier"],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "rules": {
    "import/prefer-default-export": ["off"],
    "no-await-in-loop": ["off"],
    "no-constant-condition": ["off"]
  }
}

.prettierrc

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParents": "always",
  "endOfLine": "auto"
}

ESLint를 통해 코드 린팅을, Prettier통해선 코드 포맷팅을 자동화 시켜 코드의 일관성을 가져갈 수 있습니다.

자세한 설정 방법은 알아두면 쓸데있는 ESLint & Prettier 설정 방법을 통해 셋팅 방법을 쉽게 확인하실 수 있습니다!

재 사용 및 유지 보수하기 쉽고 테스트하기 쉬운 코드

제가 생각하는 재 사용 및 유지 보수하기 쉽고 테스트하기 쉬운 코드는 아래와 같습니다.

  1. 모든 함수 및 메서드 들은 하나의 일을 수행할 수 있어야 한다.
  2. 코드 중복은 되도록 최소화 할 수 있어야 한다.

메서드가 한 가지 일만 담당하도록 구현하기

comparePlayerBaseball(playerBaseball) {
  return playerBaseball.reduce(
    (result, playerBaseballNumber, digit) => {
      if (playerBaseballNumber === this.#baseball[digit]) {
        result.strike += 1;
      } else if (this.#baseball.includes(playerBaseballNumber)) {
        result.ball += 1;
      }
      return result;
    },
    { strike: 0, ball: 0 }
  );
}

comparePlayerBaseball유저의 야구공과 비교하여 strike와 ball을 반환하는 메서드 입니다.

해당 메서드는 총 4가지의 일을 수행하고 있습니다.

  1. 공의 특정 자릿수가 strike 인지 확인한다.
  2. 공의 특정 자릿수가 ball 인지 확인한다.
  3. 조건에 부합하면 strike, ball을 계산한다.
  4. 계산된 결과를 반환한다.

다양한 일을 하나의 메서드에서 수행하고 있기 때문에, 코드의 복잡도도 높은 상태이며, 전체적인 로직 파악이 어려운 것을 파악할 수 있습니다.

#isStrike(playerBaseball, digit) {
  return playerBaseball[digit] === this.#baseball[digit];
}

#isBall(playerBaseball, digit) {
  return !this.#isStrike(playerBaseball, digit) && this.#baseball.includes(playerBaseball[digit]);
}

#calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseball, digit }) {
  return {
    strike: strike + (this.#isStrike(playerBaseball, digit) ? 1 : 0),
    ball: ball + (this.#isBall(playerBaseball, digit) ? 1 : 0),
  };
}

comparePlayerBaseball(playerBaseball) {
  return playerBaseball.reduce(
    ({ strike, ball }, playerBaseballNumber, digit) =>
      this.#calculateCompareResult({
        prevCompareResult: { strike, ball },
        playerBaseballNumber,
        digit,
      }),
    { strike: 0, ball: 0 },
  );
}

이렇게 일의 수행 범위에 따라 비교 결과를 계산하는 메서드, 스트라이크를 판별하는 메서드, 볼을 판별하는 메서드를 각각 분리한 것을 알 수 있습니다.

이를 통해 메서드가 어떤 역할을 하고 있는지 쉽게 파악이 가능하기 때문에 버그 수정, 디버깅 등 유지보수를 수월하게 할 수 있습니다.

또한, 지금 처럼 private method가 아닌 유틸 함수로 분리한다면 재 사용성과 테스트 코드에 필요한 함수를 만들 수 있습니다.

클래스를 작게 유지하기 위해 노력하기

class BaseballMachine {
  #minNumber;
  #maxNumber;
  #baseball;

  constructor() {}

  createBaseball() {}

  #isStrike(playerBaseball, digit) {}

  #isBall(playerBaseball, digit) {}

  #calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseball, digit }) {}

  comparePlayerBaseball(playerBaseball) {}
}

export default BaseballMachine;

BaseballMachine을 보면 총 3개의 field와 5개의 메서드로 구성된 것을 알 수 있습니다.

이 객체의 역할은 다음과 같습니다.

야구공 생성 - 야구공 비교

아까 위에서 메서드가 한 가지 일만 담당하도록 구현하기에서 언급된 문제가 발생할 수 있을거 같습니다.

또한, 클래스에서 여러 역할이 드러나게 되면 결국 변경 될 수 있는 이유도 여러가지가 되기 때문에 응집성이 떨어지는 것도 확인할 수 있습니다.

이 문제도 하나의 역할만 수행하게 함으로써 해결해볼 수 있을거 같습니다.

// 야구공 생성의 역할을 수행하는 BaseballMaker
class BaseballMaker {
  #minNumber;
  #maxNumber;
  
  createBaseball() {}
}

// 두 공을 비교하는 역할을 수행하는 Computer
class Computer {
  #baseball;
  
  #isStrike(playerBaseball, digit) {}

  #isBall(playerBaseball, digit) {}

  #calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseball, digit }) {}

  comparePlayerBaseball(playerBaseball) {}
}

하나의 역할만 수행할 수 있도록 클래스를 분리하게 되면 변경의 이유를 하나만 가질 수 있기 때문에 높은 응집성을 가질 수 있습니다.

도메인 모델을 재 사용하는 경우는 드물긴 하지만, 하나의 역할을 가지는 클래스는 만약 특정 역할을 수행 +a의 역할을 가진 새로운 클래스가 필요하다면 이를 합성하여 새로운 클래스로 만들 수 있어 재 사용도 가능합니다.

Computer.test.js

describe('Computer 테스트', () => {
  beforeAll(() => {
    BaseballMaker.prototype.createBaseball = () => [3, 4, 5];
  });

  test.each([
    {
      input: [1, 2, 6],
      output: {
        strike: 0,
        ball: 0,
      },
    },
  ])(
    '플레이어가 선택한 야구공 $input과 비교한 결과는 $output.strike스트라이크 $output.ball볼 이다.',
    ({ input, output }) => {
      // given
      const computer = new Computer();
      // when
      const { strike, ball } = computer.comparePlayerBaseball(input);
      // then
      expect(strike).toBe(output.strike);
      expect(ball).toBe(output.ball);
    },
  );
});

하나의 역할을 가지면 그 만큼 테스트 할 항목도 명확하게 나타낼 수 있게 되어 유용합니다.

빌트인 메서드를 최대한 사용하기

// bad
validateBaseball() {
  this.#commonValidator.validate();
  const { validationTypes } = BaseballValidator;
  for (const key in validationTypes) {
    if (key in validationTypes) {
      const { errorMessage, isValid } = validationTypes[key];
      if (!isValid(this.#baseball)) throw new AppError(errorMessage);
    }
  }
}
  
// good
validateBaseball() {
  this.#commonValidator.validate();
  Object.values(BaseballValidator.validationTypes).forEach(({ errorMessage, isValid }) => {
    if (!isValid(this.#baseball)) throw new AppError(errorMessage);
  });
}

객체를 사용할 땐 for문 대신 Object.valuesforEach를 사용하여 로직을 표현할 수 있습니다.

이렇게 빌트인 메서드를 사용하면 복잡한 코드를 간결하게 사용할 수 있으며, 이해하기 쉬워지기 때문에 관리하기 쉬운 코드를 만들 수 있습니다.

중복 된 코드를 최대한 중앙화 시키기

InputView.js

// bad
const InputView = {
  async readPlayerBaseball() {
    return await Console.readLineAsync(INPUT_MESSAGE.playerBaseball);
  },

  async readExitGameCommand() {
    return await Console.readLineAsync(INPUT_MESSAGE.exitGameCommand);
  },
};

// good
const InputView = {
  async read(query) {
    const inputValue = await Console.readLineAsync(query);
    return inputValue;
  },

  readPlayerBaseball() {
    return this.read(INPUT_MESSAGE.playerBaseball);
  },

  readExitGameCommand() {
    return this.read(INPUT_MESSAGE.exitGameCommand);
  },
};

bad case에서 InputViewreadPlayerBaseballreadExitGameCommandmessage를 제외한 나머지 로직이 모두 동일한 것을 알 수 있습니다.

이로 인해, 추가 할 메서드가 생긴다면 공통된 로직을 또 다시 반복해서 작성해야 하기 때문에 번거로워지며, 변경되는 로직이 발생했을 때도 모든 메서드를 수정해야 하는 문제가 발생합니다.

하지만 good case 처럼 공통되는 로직을 read 메서드와 같이 추상화하게 되면 추가해야 할 메서드가 생겼을 때 read 함수를 재 사용할 수 있기 때문에 확장성에도 뛰어나며, 변경 사항이 발생한다면 read 함수에서 변경하면 되기 때문에 유지보수에도 원활해질 수 있습니다.

magic number 및 하드 코딩 된 값 상수화 시키기

// bad
export const OUTPUT_MESSAGE_TEXT = Object.freeze({
  exitGame: '3개의 숫자를 모두 맞히셨습니다! 게임 종료',
});

// good
export const OUTPUT_MESSAGE_TEXT = Object.freeze({
  exitGame: `${GAME_TERMS.baseball.digit}개의 숫자를 모두 맞히셨습니다! 게임 종료`,
});

magic number 또한 위와 비슷한 맥락으로 정의할 수 있습니다.

exitGame 뿐만 아니라 digit과 관련된 상수 값을 사용하고 있는 모든 로직들에 대해 digit과 같이 중앙화 시켜 관리함으로써, 3이 아닌 다른 값으로 변경되어도 쉽게 변경이 가능해집니다.

레퍼런스


0개의 댓글