Post

우아한테크코스 자바스크립트 온보딩 3번 리팩토링

온보딩 3번 문제 풀이는 이전글에서 확인하실 수 있습니다.

리팩토링

이번 문제를 리팩토링 하면서 모듈화객체지향을 중심으로 생각해보았습니다.

함수와 매개변수

자바스크립트에서 코드를 읽어내려갈 때 함수의 동작을 파악하기 위해 가장 먼저 보게되는 것은 함수의 이름매개변수 이다. 우리는 이 두 가지를 통해 함수의 동작을 유추하고 이를 토대로 함수의 역할을 파악한다. 따라서 명확한 함수의 이름과 매개변수를 설정하는 것은 전체적인 코드의 가독성을 높이는데 도움을 준다.

함수

이전 풀이에서는 game 이라는 점에 주목해 함수의 이름을 결정하였다. 하지만 문제의 gameclap game이라는 보다 구체적인 개념에 속하며 다른 game이 추가되는 경우 이와 같은 함수명은 문제가 될 수 있다. 따라서 보다 명확히 함수의 동작이 드러날 수 있게 clap game에 맞춰 함수 이름을 변경하였다.

매개변수

기존에는 모든 매개변수에 number를 사용하였다. 하지만 문제에서는 주로 한 자리 숫자를 다루고 있기 때문에 이를 표현하는 digit을 사용하는 것이 number 보다 구체적으로 매개변수의 역할을 드러낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Before
const GAME = Object.freeze({ NUMBERS: [3, 6, 9] });

function isGameNumber(number) {
  return GAME.NUMBERS.includes(number);
}

function splitNumber(number) {
  return number.toString().split("");
}

function calculateGameNumber(number) {
  return splitNumber(number).reduce(
    (result, splitedNumber) =>
      isGameNumber(Number(splitedNumber)) ? result + 1 : result,
    0
  );
}

function indexArray(number) {
  return Array.from(Array(number), (_, index) => index);
}

리팩토링 과정에서 몇몇 유틸리티 함수의 이름을 변경하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// After
const CLAP_GAME = Object.freeze({ DIGITS: [3, 6, 9] });

function isClapGameDigit(digit) {
  return CLAP_GAME.DIGITS.includes(digit);
}

function splitDigits(digit) {
  return digit.toString().split("");
}

function countClapDigits(number) {
  return splitDigits(number).reduce(
    (result, splitedDigit) =>
      isClapGameDigit(Number(splitedDigit)) ? result + 1 : result,
    0
  );
}

function generateRange(number) {
  return Array.from({ length: number }, (_, index) => index);
}

모듈화

문제를 하나의 파일에서 풀이하면서 여러 함수들이 하나의 파일에 모이게 되었다. 지금 처럼 간단한 문제를 풀이한다면 크게 문제가 되지 않지만 문제가 복잡해질 수록 규모가 커지면서 각각의 함수를 관리하기 어려워질 수 있다. 이러한 문제를 해결하기 위해 각각의 함수를 역할과 책임에 따라 분류하고 모듈화를 진행함으로써 문제를 해결할 수 있다.

이번 문제를 역할과 책임에 따라 다음과 같이 모듈화를 진행하였다.

  • clapGame.js clapGame과 직접적으로 관련된 함수를 모아 놓은 코드
  • constants.js 상수(constant) 를 모아 놓은 코드
  • utilities.js 재사용 가능한 유틸리티 함수를 모아 놓은 코드
  • validator.js 검증을 위한 함수를 모아 놓은 코드
  • index.js clapGame을 직접 실행하는 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// clapGame.js
const { CLAP_GAME } = require("./constants");
const { splitDigits, generateRange } = require("./utilities");
const { validateNumber } = require("./validator");

function isClapGameDigit(digit) {
  return CLAP_GAME.DIGITS.includes(digit);
}

function countClapDigits(number) {
  return splitDigits(number).reduce(
    (result, splitedNumber) =>
      isClapGameDigit(Number(splitedNumber)) ? result + 1 : result,
    0
  );
}

function countTotalClaps(number) {
  return generateRange(number).reduce(
    (result, current) => result + countClapDigits(current + 1),
    0
  );
}

function clapGame(number) {
  validateNumber(number);

  return countTotalClaps(number);
}

module.exports = clapGame;

위에서 clapGame 외에 함수는 export 하지 않았는데, 이를 통해 모듈의 내부 구현 세부 사항을 숨길 수 있다. 이러한 모듈을 통한 추상화 및 캡슐화는 사용자가 내부 작동 방식을 이해할 필요 없이 쉽게 사용할 수 있게 만들 수 있다. 또한 이러한 설계는 내부 함수가 외부 인터페이스에 영향을 주지 않기 때문에 쉽게 리팩토링하고 최적화를 진행할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// constants.js
const CLAP_GAME = Object.freeze({ DIGITS: [3, 6, 9] });

const ERROR_MESSAGE = Object.freeze({
  ERROR_INVALIDE_TYPE: "입력된 숫자의 타입이 올바르지 않습니다.",
  ERROR_NUBER_IS_NOT_INTEGER: "입력된 숫자가 정수가 아닙니다.",
  ERROR_NUMBER_NOT_IN_RANGE: "입력된 숫자가 유효 범위 안에 존재하지 않습니다.",
});

module.exports = {
  CLAP_GAME,
  ERROR_MESSAGE,
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// utilities.js
function generateRange(number) {
  return Array.from({ length: number }, (_, index) => index);
}

function splitDigits(digit) {
  return digit.toString().split("");
}

module.exports = {
  generateRange,
  splitDigits,
};

유틸리티 함수는 재사용성을 고려한 만큼 특정 로직에 종속되지 않은 일반적인 이름을 사용했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// validator.js
const { ERROR_MESSAGE } = require("./constants");

function validateNumber(number) {
  if (typeof number !== "number") {
    throw new Error(ERROR_MESSAGE.ERROR_INVALIDE_TYPE);
  }
  if (!Number.isInteger(number)) {
    throw new Error(ERROR_MESSAGE.ERROR_NUBER_IS_NOT_INTEGER);
  }
  if (!(0 < number && number <= 10000)) {
    throw new Error(ERROR_MESSAGE.ERROR_NUMBER_NOT_IN_RANGE);
  }
}

module.exports = {
  validateNumber,
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js
const clapGame = require("./clapGame");

function problem3(number) {
  try {
    return clapGame(number);
  } catch (error) {
    return error.message;
  }
}

console.log(problem3(33));

module.exports = problem3;

이렇게 코드를 모듈화 함으로써 각 모듈을 독립적으로 관리하고 수정할 수 있어 전체적인 개발 및 유지 관리 과정을 효율적으로 진행할 수 있다.

객체지향

모듈화를 통해 clapGame이라는 하나의 모듈을 만들어보았다. 이것을 객체지향적으로 생각한다면 어떻게 풀이할 수 있을까? ClapGame 이라는 Game 객체를 생각하고 객체의 속성과 행위(메서드)를 다음과 같이 정의하였다.

constants, utilities, validator 모듈은 위와 동일하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ClapGame.js
class ClapGame {
  constructor(utils) {
    this.utils = utils;
  }

  _digits = [3, 6, 9];

  _isClapGameDigit(digit) {
    return this._digits.includes(digit);
  }

  _countClapDigits(number) {
    return this.utils
      .splitDigits(number)
      .reduce(
        (result, splitedNumber) =>
          this._isClapGameDigit(Number(splitedNumber)) ? result + 1 : result,
        0
      );
  }

  _countTotalClaps(number) {
    return this.utils
      .generateRange(number)
      .reduce(
        (result, current) => result + this._countClapDigits(current + 1),
        0
      );
  }

  play(number) {
    return this._countTotalClaps(number);
  }
}

module.exports = ClapGame;

utils 모듈과 강하게 결합되는 문제를 해결하기 위해 모듈을 의존성 주입할 수 있는 방식으로 구현하였다.

모듈화 방식과 비교해보았을 때 객체지향을 이용한 코드는 객체를 중심으로 객체의 데이터와 메서드를 하나의 클래스에 캡슐화해 사용한다. 이러한 코드 구성을 통해 객체와 객체의 관계를 정의하고 그들의 상호작용을 통해 코드를 구조화하며 유지보수 하기 쉽게 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.js
const ClapGame = require("./ClapGame");
const utils = require("./utilities");
const { validateNumber } = require("./validator");

function clapGame(number) {
  validateNumber(number);

  const clapGame = new ClapGame(utils);

  return clapGame.play(number);
}

function problem3(number) {
  try {
    return clapGame(number);
  } catch (error) {
    return error.message;
  }
}

console.log(problem3(33));

모듈화와 객체지향 그리고 함수형 프로그래밍

모듈화와 객체지향을 비교하게 되었지만 사실 상반된 비교대상은 아니다.

모듈화는 코드의 구조와 관리를 쉽게 하기 위한 방법이며, 객체지향은 데이터를 효과적으로 다루고 프로그램의 복잡성을 줄이기 위한 패러다임이기 때문이다. 실제로 객체지향을 이용한 풀이에서 모듈화와 객체지향의 개념을 함께 이용하였다.

모듈화를 이용한 풀이에서 주목할만한 점은 여기에 조금이나마 함수형 프로그래밍의 개념을 적용하려 했다는 것이다. 순수 함수를 만들고 이것을 조합하여 모듈화 수준을 높이려고 노력하였다.

함수형 프로그래밍은 어플리케이션, 함수의 구성요소, 더 나아가서 언어 자체를 함수처럼 여기도록 만들고, 이러한 함수 개념을 가장 우선순위에 놓는다.”

라는 마이클 포거스의 말에 따라 함수(동작)을 위주로 생각하고 데이터 세트를 구성하는 함수형 프로그래밍 방식을 따랐다기에는 분명 아쉬운 부분이 존재하지만, 객체 지향과 함수형 패러다임을 비교하고 두 패러다임의 사고 방식을 따라 고민해보면서 두 패러다임의 차이를 조금이나마 이해할 수 있게되었다.

전체코드는 해당 PR을 통해 확인해볼 수 있습니다.

This post is licensed under CC BY 4.0 by the author.