3. 모든 원시값과 문자열을 포장하라

소트웍스 앤솔로지Object Calisthenics 내용을 TypeScript로 적용해봤습니다.

이전 두 개의 규칙도 간단했지만, 이번 규칙은 더 간단하다. 본래 이는 Java에서 byte, short, int, long, float, double, boolean, char 그리고 String을 포장하라는 의미다.

TypeScript의 경우에 Object, Array 등도 원시값으로 정의하므로 우리는 조금 이를 바꿔보자. TypeScript 작성 시 NumberString은 포장하도록 하자. 값을 포장한다는 것은 하드코딩하지 말자는 이야기다. 두 가지 예시를 통해 이해해보자.

의미있는 값은 상수화하자.

const generateRandomLottoNumber = () => {
  return Math.round(Math.random() * 45) + 1;
};

const lottoTicket: number[] = [];
for (let i = 0; i < 6; i++) {
  lottoTicket.push(generateRandomLottoNumber());
}

console.log(lottoTicket);

로또 티켓을 만드는 로직이다. 모든 NumberString을 포장하라 했으니, 이를 토대로 한 번 고쳐보자.

const LOTTO_NUMBERS_COUNT = 6;
const LOTTO_MINIMUM_NUMBER = 1;
const LOTTO_MAXIMUM_NUMBER = 45;

const generateRandomLottoNumber = () => {
  return (
    Math.round(Math.random() * LOTTO_MAXIMUM_NUMBER) + LOTTO_MINIMUM_NUMBER
  );
};

const lottoTicket: number[] = [];
for (let i = 0; i < LOTTO_NUMBERS_COUNT; i++) {
  lottoTicket.push(generateRandomLottoNumber());
}

console.log(lottoTicket);

6, 1, 45를 상수화 했는데, 그 이유는 각각의 숫자들이 특별한 의미를 가지기 때문이다. 이렇게 의미를 지닌 원시값들을 명시적으로 상수화한다면 읽는 사람으로 하여금 코드를 더 이해하기 쉽게 만들어준다.

단, 0ZERO로, 2TWO로 상수화하는 등 의미없는 상수명을 붙이는 것은 지양하자.

특정 기능이 있는 원시형은 값 객체로 만들자.

const store = {
  dietCoke: 1800,
  coke: 1600,
  snack: 1800,
  tofu: 1500,
  milk: 3800,
};

let money = 12000;

console.log('잔액: ', money);
money = money - store.dietCoke;
money = money - store.snack;
money = money - store.milk;
console.log('잔액: ', money);

우선 이전에 했던 규칙에 따라 의미있는 NumberString을 상수화해보자.

const INITIAL_MONEY_AMOUNT = 12000;
const BALANCE_PREFIX = '잔액: ';

const store = {
  dietCoke: 1800,
  coke: 1600,
  snack: 1800,
  tofu: 1500,
  milk: 3800,
};

let money = INITIAL_MONEY_AMOUNT;

console.log(BALANCE_PREFIX, money);
money = money - store.dietCoke;
money = money - store.snack;
money = money - store.milk;
console.log(BALANCE_PREFIX, money);

우리는 돈을 표현하기 위해 Number를 사용하고 있는데, 한 가지만 생각해보자. 돈은 일반 숫자와는 다르게 0인 경우에 사용할 수 없다. 그리고 초기값이 음수일 수도 없다. 특별한 의미를 가지고 있는 이 숫자를 어떻게 의미있게 포장할 수 있을까? 이 때 나오는 것이 값 객체(Value Object)다.

값 객체가 무조건 한 가지 필드를 가지고 있어야 하는 것은 아니다. 다만, 한 가지의 필드만 있더라도 그 값에 의미가 있다면 값 객체로 포장하는 습관을 들이면 좋다.

const NEGATIVE_AMOUNT_ERROR_MESSAGE = '초기값은 음수가 될 수 없습니다.';
const LOWER_LIMIT_AMOUNT = 0;

class Money {
  constructor(private _amount: number) {
    if (this._amount < 0) {
      throw Error(NEGATIVE_AMOUNT_ERROR_MESSAGE);
    }
  }

  get amount() {
    return this._amount;
  }

  add(other: Money): void {
    this._amount += other._amount;
  }

  reduce(other: Money): void {
    if (this._amount - other._amount < LOWER_LIMIT_AMOUNT) {
      throw Error(NEGATIVE_AMOUNT_ERROR_MESSAGE);
    }
    this._amount -= other._amount;
  }
}

const INITIAL_MONEY_AMOUNT = 12000;
const BALANCE_PREFIX = '잔액: ';

const store = {
  dietCoke: new Money(1800),
  coke: new Money(1600),
  snack: new Money(1800),
  tofu: new Money(1500),
  milk: new Money(3800),
};

let money = new Money(INITIAL_MONEY_AMOUNT);

console.log(BALANCE_PREFIX, money.amount);
money.reduce(store.dietCoke);
money.reduce(store.snack);
money.reduce(store.milk);
console.log(BALANCE_PREFIX, money.amount);

의미 있는 값 포장을 통해 Primitive Obsession Anti Pattern을 피할 수 있다. NumberString을 상수화하거나 값 객체로 포장하여 한 눈에 들어오는 가독성 좋은 코드를 만들자.

Last updated