본문 바로가기
기타

TDD 스터디 #3 - 화폐 예제 (다중 통화 구현 & 중복 제거)

by 유세지 2023. 7. 21.

 

 

 

지난 포스트에서 이어집니다. 우리는 지난 시간에 화폐의 곱을 계산할 수 있도록 하는 times() 메서드를 TDD를 통하여 구현해보았습니다. 화폐의 가치를 다루는 기능을 만들어보았으니, 다음으로는 이 화폐들을 비교하는 로직을 구현해보겠습니다.

 

 

equals()

우리는 지금까지 화폐를 Dollar라고 하는 값 객체를 통해 다루었습니다. 값 객체는 동일한 값의 다른 객체를 조작했을때 기존 객체의 값이 함께 변하는 별칭 문제로부터 자유로움을 보장하지만, 값이 동일하더라도 인스턴스가 다르면 다른것으로 취급하는 프로그램의 특성 때문에 값을 기반으로 비교하는 별개의 메서드가 필요합니다. 그것이 지금부터 만들 equals() 메서드입니다.

 

우선 TDD 답게 테스트를 먼저 작성해봅니다.

 

 

example.test.js

it("equals() 테스트", () => {
  expect(new Dollar(5).equals(new Dollar(5))).toBe(true);
});

 

빨간 막대를 확인합니다

 

다음엔 빠르게 초록 막대로 전환합니다.

 

 

src/money.js

class Dollar {
  ...
  equals(object) {
    return true;
  }
}

 

구현에 신경쓰지 말고, 일단 초록 막대로 전환합니다.

 

 

이제 조건을 완전히 만족하도록 구현하는 단계만 남았는데, 우리가 앞으로 구현해야 할 내용이 복잡하고 어렵다면 테스트를 추가하여 요구사항을 확실하게 설정하는 방법이 있습니다. 저자는 이를 삼각측량이라고 표현합니다. 삼각측량이 두 개의 기지국 신호와 방향을 토대로 현재 위치를 정확히 집어내는 것처럼 구현 범위를 좁히는 방법의 일종이라고 생각하면 되겠습니다.

 

지금 우리 예시의 경우에는 복잡하지 않아 이렇게까지 할 필요는 없지만, 연습삼아 한 번 적용해보겠습니다. 요구 사항을 명확히 할 수 있는 테스트를 하나 더 추가합니다.

 

 

example.test.js

it("equals() 테스트", () => {
  expect(new Dollar(5).equals(new Dollar(5))).toBe(true);
  expect(new Dollar(5).equals(new Dollar(6))).toBe(false);
});

 

이제 이 테스트를 만족시킬 수 있도록 구현해보겠습니다.

 

 

src/money.js

equals(object) {
  return this.amount === object.amount;
}

 

성공적으로 초록 막대가 유지되었습니다.

 

 

이렇게 성공적으로 equals() 메서드를 구현하였습니다. 물론 예외처리나 더 꼼꼼히 볼 필요가 있는 부분이 있지만 여기선 넘어가도록 하겠습니다.

 

 

amount 숨기기

지난번에 구현했던 times() 메서드는 배수를 인자로 받아 기존의 amount에 인자를 곱한 amount를 갖는 새로운 인스턴스를 생성하여 반환했습니다. 그런데 우리의 테스트 코드는 만들어진 인스턴스에서 amount를 꺼내 그 값을 직접 비교하고 있었습니다.

 

 

example.test.js

it("오류 수정 예제", () => {
  const five = new Dollar(5);
  let product = five.times(2);
  expect(product.amount).toBe(10);

  product = five.times(3);
  expect(product.amount).toBe(15);
});

 

 

그렇다면 이 테스트는 times() 를 올바르게 테스트 하고 있다고 보긴 어렵겠죠. 해당 amount 값을 갖는 인스턴스를 생성한게 맞는지를 테스트하도록 코드를 변경해보겠습니다,.

 

 

example.test.js

it("오류 수정 예제", () => {
  const five = new Dollar(5);
  expect(new Dollar(10)).toStrictEqual(five.times(2));
  expect(new Dollar(15)).toStrictEqual(five.times(3));
});

 

인스턴스의 amount를 꺼내는 대신, 생성한 그대로를 놓고 비교 대상에 times() 메서드를 사용하도록 코드를 변경했습니다. jest에서 객체의 완전 일치 여부를 판단할때는 toBe() 대신 toStrictEqual()을 사용합니다.

 

훨씬 테스트 코드가 깔끔해졌습니다. 테스트 코드가 깔끔해졌다는 것은, 본 코드도 깔끔하게 만들 수 있다는 신호이기도 합니다.

 

테스트도 정상적으로 통과합니다.

 

 

Franc 추가하기

이제 Dollar와 비슷하게 동작하는, Franc이라는 단위를 만들어 볼겁니다. 드디어 우리가 처음 목표로 했던 다중 통화를 구현하는 단계입니다. 먼저 테스트 코드로 시작합니다.

 

 

example.test.js

it("Dollar 테스트", () => {
  const five = new Dollar(5);
  expect(new Dollar(10)).toStrictEqual(five.times(2));
  expect(new Dollar(15)).toStrictEqual(five.times(3));
});

it("Franc 테스트", () => {
  const five = new Franc(5);
  expect(new Franc(10)).toStrictEqual(five.times(2));
  expect(new Franc(15)).toStrictEqual(five.times(3));
});

 

Franc에 대한 테스트를 작성하고, 각각의 테스트 이름도 보기 쉽게 바꾸어주었습니다.

 

빨간 막대를 확인했습니다.

 

Dollar 클래스를 그대로 복사하여, Franc으로 이름만 바꿔주고 초록 막대를 확인해보겠습니다. import와 export 형식에 주의하여 수정합니다.

 

 

src/money.js

class Dollar {
  amount;

  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    return new Dollar(this.amount * multiplier);
  }

  equals(object) {
    return this.amount === object.amount;
  }
}

class Franc {
  amount;

  constructor(amount) {
    this.amount = amount;
  }

  times(multiplier) {
    return new Franc(this.amount * multiplier);
  }

  equals(object) {
    return this.amount === object.amount;
  }
}

export { Dollar, Franc };

 

 

example.test.js

import { Dollar, Franc } from "../src/money";

// ...

 

 

초록 막대가 보입니다.

 

 

테스트는 통과하였지만, 복사한 코드에서 중복이 굉장히 많이 보이고 있습니다. 이 중복들을 제거해봅시다. 가장 먼저 눈에 띄는 중복은 Dollar와 Franc이 동시에 가지고 있는 equals() 메서드입니다. 완전히 동일하기에, 이 메서드를 끌어올려서 상위의 클래스에서 가지게 하면 어떨까요?

 

Money라는 상위 클래스를 하나 만들어주도록 하겠습니다.

 

 

src/money.js

class Money {}

class Dollar extends Money {
  ...
}

class Franc extends Money {
  ...
}

 

책에서는 빈 클래스를 상속받는다고 해서 테스트가 깨지지는 않았습니다. 그러나 우리의 자바스크립트는 상위 생성자 함수를 호출해주지 않으면 하위 클래스에서 그대로 사용할 수 없습니다.

 

테스트가 와장창 깨져버렸습니다.

 

상위 클래스의 생성자를 호출하도록 코드를 변경해줍니다

 

src/money.js

class Money {
  amount;

  constructor(amount) {
    this.amount = amount;
  }
}

class Dollar extends Money {
  amount;

  constructor(amount) {
    super(amount);
    this.amount = amount;
  }
  
  // ...
}

class Franc extends Money {
  amount;

  constructor(amount) {
    super(amount);
    this.amount = amount;
  }
  
  // ...
}

 

이제 테스트가 깨지지 않는걸 확인할 수 있었습니다.

 

모두 통과합니다.

 

 

이제 equals() 메서드를 끌어올려 봅시다. Dollar와 Franc에 있던 equals() 메서드를 삭제하고, 새로 만든 Money 클래스에 추가해줍니다.

 

 

src/money.js

class Money {
  amount;

  constructor(amount) {
    this.amount = amount;
  }

  equals(object) {
    return this.amount === object.amount;
  }
}

 

 

 

이제 테스트를 돌려보...려고 했는데 equals() 메서드를 테스트할때 Franc은 테스트하고 있지 않다는 것을 깨달았습니다. 빠르게 추가해줍시다.

 

 

example.test.js

it("equals() 테스트", () => {
  expect(new Dollar(5).equals(new Dollar(5))).toBe(true);
  expect(new Dollar(5).equals(new Dollar(6))).toBe(false);
  expect(new Franc(5).equals(new Franc(5))).toBe(true);
  expect(new Franc(5).equals(new Franc(6))).toBe(false);
});

 

이제 테스트를 돌려보면,

 

역시 초록 막대입니다.

 

 

모든 테스트가 성공하는 모습입니다. 이렇게 중복을 제거하여, 상위 클래스로 옮겨주는 작업을 하나 마쳤습니다. 그러나 위쪽의 테스트를 보면, Dollar와 Dollar, Franc과 Franc만을 비교하고 있다는걸 알 수 있습니다. 실제로는 Dollar와 Franc도 비교해보아야 정확한 테스트라고 할 수 있겠죠. 이 부분은 다음 포스트에서 이어서 진행해보겠습니다.

 

 

 

마치며

오늘은 드디어 처음 목표였던 다중 통화를 구현하는데 성공했습니다. 아직 완전하지 않고 작업해야 할 것들이 많지만, 지금까지의 테스트가 성공적으로 동작하는 것을 확인했습니다. 또한 테스트 코드와 본 코드의 중복을 제거해보았습니다. TDD를 통해 안전하게 중복을 제거하는 과정을 살펴보았으니, 다음에는 더 빠르게 진행할 수 있어 보입니다.

 

포스팅에서 사용된 예제 코드들은 아래 레포지토리에서 확인할 수 있습니다.

읽어주셔서 감사합니다.

 

반응형

댓글