기타

TDD 스터디 #2 - 화폐 예제 (다중 통화 지원하기)

유세지 2023. 7. 2. 17:02

 

 

지난 포스트에서는 저자와 역자의 말을 읽고, 테스트를 진행할 수 있는 환경을 구성해 보았습니다. 이제부터는 책을 읽으며 본격적으로 흐름을 따라가 보겠습니다.

 

책에서 마주하는 첫 번째 상황은 다중 통화를 지원하는 Money 객체를 구성하는 예제입니다.

 

 

통화 단위 추가하기

기존의 Money 객체는 화폐의 단위로 달러(USD)를 사용하였습니다. 그러나 사용자의 필요에 의해서 스위스 프랑(CHF)을 추가로 지원해야 했는데요, 이렇게 새로운 단위가 추가되는 상황이라면 어떤 기능들이 더 필요할까요? 먼저 필요한 기능들을 리스트업 하는 것부터 시작합니다.

 

필요한 기능

  • 다른 통화끼리의 연산이 가능해야한다.
  • 하나의 통화 사이의 연산이 가능해야한다.

 

이 기능을 테스트 할 수 있는 명제로 나타내면 다음과 같겠네요.

 

통과해야 할 테스트

  • $5 + 10CHF = $10
  • $5 * 2 = $10

 

TDD의 과정 중 가장 처음 나와야 하는 과정이 바로 "빨간 막대 보기" 입니다. 빨간 막대라는 말이 생소하게 느껴지실텐데, 자바의 테스트 프레임워크인 Junit에서는 테스트가 성공하면 초록색 막대가, 테스트가 실패하면 빨간색 막대가 표시됩니다. 즉, 빨간 막대를 보라는 말은 구현은 하지 않고, 먼저 오류가 나는 테스트를 작성하라는 의미입니다. 저자의 표현을 따라, 앞으로는 저도 실패하는 테스트를 작성할때 부연 설명없이 빨간 막대를 본다고 표현하겠습니다.

 

먼저 빨간 막대를 보기 위해 테스트를 작성해봅시다.

 

__test__/example.test.js

it("간단한 곱셈 예제", () => {
  const five = new Dollar(5);
  five.times(2);
  expect(five.amount).toBe(10);
});

 

npm run test 명령을 통해 테스트를 실행시켜보면,

 

빨간 막대(FAIL)가 보입니다.

 

jest에서도 빨간 막대로 에러를 표시해주고 있는 모습입니다. 여기서는 하나의 오류가 발생하며 테스트가 종료되었지만, 이 코드에는 총 네 가지의 잠재적인 오류가 있습니다.

 

  1. Dollar 클래스가 정의되지 않음
  2. Dollar 클래스의 생성자가 존재하지 않음
  3. Dollar 클래스에 times() 메서드가 정의되지 않음
  4. Dollar 클래스에 amount 필드가 존재하지 않음

 

우선 우리가 생각할 수 있는 가장 단순한 방법으로 이 문제를 해결해보겠습니다. 코드를 보고 '눈 가리고 아웅' 이라는 속담이 떠오른다면 아주 잘하고 계신겁니다. TDD에서는 시작부터 기능을 구현할 필요가 없습니다.

 

src/money.js

class Dollar {
  amount = 10;

  constructor(amount) {}

  times(multiplier) {}
}

export default Dollar;

 

__test__/example.test.js

import Dollar from "../src/money";

describe("TDD 연습을 위한 테스트입니다.", () => {
  beforeAll(() => {});

  it("간단한 곱셈 예제", () => {
    const five = new Dollar(5);
    five.times(2);
    expect(five.amount).toBe(10);
  });
});

 

테스트를 통과할만한 가장 간단한 Dollar 클래스를 만들었습니다. 내용은 텅텅 비어있지만, 테스트가 amount의 값이 10일것을 기대하고 있기 때문에 이 테스트는 통과할 수 있을 것입니다. 바로 확인해보겠습니다.

 

ESM 오류가 발생하였습니다.

 

import에서 에러가 발생하는 것을 보니 아무래도 지난 시간에 babel 설정을 빼먹은것 같습니다. 

 

괜찮습니다. 명령어를 통해 의존성을 설치해주고, babel 설정을 마무리하겠습니다.

 

$ npm install --save-dev babel-jest @babel/core @babel/preset-env

 

루트 디렉토리에 babel configuration 파일을 추가해줍니다.

 

babel.config.js

module.exports = {
  presets: [["@babel/preset-env", { targets: { node: "current" } }]],
};

 

이제 다시 테스트를 실행해봅시다!

 

기다리던 초록 막대(PASS)가 등장했습니다.

 

여기까지가 TDD의 2단계 과정인 초록 막대 보기입니다. 이제 중복을 제거하는 리팩토링 과정을 거쳐서 조금씩 원하는 기능을 향해 나아가는 일만 남았습니다.

 

물론 지금의 테스트는 곧바로 구현할 수 있을만큼 쉽고 간단하지만, 구현을 배우는게 아닌 과정을 배운다는 느낌으로 작은 단계부터 밟아보겠습니다.

 

먼저 amount 필드를 선언하는 부분을 이렇게 표현할 수 있을 것 같습니다. 결국 10은 5와 2의 곱셈 과정을 통해 나오는 값이기 때문에, 이렇게 나누어 줄 수 있겠네요. 

 

 

src/money.js

amount = 5 * 2;

 

여기서 테스트를 진행하여 잘 작동하는지 확인하면, 다음으로 넘어갑니다. 이 5 * 2 라는 값은 사실 필드에서 선언될 필요는 없죠. amount에 값을 넣어주는 부분을 times() 안쪽으로 옮겨도 될 것 같습니다.

 

 

src/money.js

amount;

times(multiplier) {
  this.amount = 5 * 2;
}

 

다시 테스트를 돌려보고, 또 어떤 중복을 제거할 수 있을지 살펴보면 2 라는 상수를 제거할 수 있어 보입니다. 우리가 테스트에서 인자로 넘겨주고 있는 수가 2이므로, 이 2를 받고있는 multiplier를 대신 사용할 수 있겠네요.

 

 

src/money.js

times(multiplier) {
  this.amount = 5 * multiplier;
}

 

또 테스트를 돌려 성공하는지 확인하고, 이번엔 이 5 라는 상수를 제거해볼까요? 테스트에서는 생성자에 5를 인자로 보내주고 있으니 이 값을 amount 값에 넣어주면 될 것 같습니다.

 

 

src/money.js

amount;

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

times(multiplier) {
  this.amount = this.amount * multiplier;
}

 

이제보니 times()에 this.amount가 중복해서 등장하고 있네요. 이 부분도 지워줄 수 있어 보입니다. 그 전에 테스트가 성공하는지 확인하는 것도 잊지 마세요.

 

 

src/money.js

amount;

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

times(multiplier) {
  this.amount *= multiplier;
}

 

이렇게 우리가 처음 원했던 기능을 TDD를 통해 구현해 보았습니다. 어떤가요, 너무 작은 단위로 변경하고 테스트를 진행해서 다소 답답하게 느껴지진 않으셨나요?

 

저도 처음에 느린 진행 속도 때문에 답답함을 느끼곤 했었습니다. 모든 TDD가 이렇게 진행된다면 실무에서 이걸 사용하는게 맞을까 하는 의문도 들었습니다. 다만 지금은 경험이 없으니 TDD의 과정을 느껴보자는 취지로 하나하나 예시를 보며 코딩을 진행한 것이고, 숙달되면 여기서 몇 단계 정도는 건너 뛸 수 있게 되지 않을까 싶습니다. 저자가 말하는 보폭을 넓게 가져간다는게 아마 이런 의미일 것 같아요.

 

 

오류 고치기

그럼 같은 방법으로 조금 더 해보겠습니다. 방금 작성한 Dollar는 테스트를 무사히 통과하지만 논리적으로는 약간의 오류가 있습니다. 바로 times() 를 실행할때마다 amount의 값이 바뀌게 된다는 점입니다. 이렇게 되면 여러번 times()를 사용할 경우 정상적인 값을 얻기 어려울테니 코드를 수정해주어야 합니다.

 

우선 여러번 times() 를 사용하는 테스트 케이스를 먼저 작성해보겠습니다.

 

 

example.test.js

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

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

 

이대로 테스트를 실행해보면,

 

역시 생각대로 실패했습니다.

 

일단 빨간 막대를 띄웠습니다. 그런데 막상 테스트를 하고 보니, 우리가 원하는 것은 times() 가 숫자가 아닌 화폐를 반환하도록 하는 것입니다. 이건 인터페이스가 잘못된 경우이므로, 테스트를 다시 작성해야합니다.

 

 

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);
});

 

단순 숫자가 아닌, Dollar 객체를 비교 대상에 넣고 amount 프로퍼티를 뽑아서 비교하도록 테스트를 작성했습니다. 이렇게 하면 times() 를 한다고 해서 기존의 amount가 변하는 일이 일어나지 않겠죠? 이제 변경된 테스트가 초록 막대를 볼 수 있도록 코드를 빠르게 수정해보겠습니다.

 

 

src/money.js

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

 

간단히 새 Dollar 객체를 반환하도록 변경해주면 될 것 같아 보입니다. 위쪽의 "간단한 곱셈 예제" 테스트는 삭제하고, 테스트를 돌려보겠습니다.

 

초록 막대를 보았습니다.

 

 

원래대로라면 리팩토링 과정을 거쳐야겠으나, 지금 코드에서 추가로 제거해줄만한 중복은 딱히 보이지 않네요. 이 테스트는 여기서 마무리하면 될 것 같습니다.

 

 

 

마치며

오늘은 다중 통화 예제를 통해 TDD를 직접 진행해보았습니다. 간단한 기능을 구현하는데 그쳤지만, 이 다음에 구현해야 할 기능들은 제법 복잡한 모습을 하고 있으니 하면 할수록 TDD의 효과를 느낄 수 있을 것 같네요. 긴 글 읽어주셔서 감사합니다.

 

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

 

반응형