ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 친구가 내준 문제를 푸는 데 써본 TDD 프로세스
    Today I Learned 2022. 9. 12. 00:17

     

    금요일 오후에 친구들을 만나는 자리에 맥북을 가지고 갔었다. 맥북을 펼치니 뜻하지 않은 코딩 자랑을 하게 되었는데, VS Code를 켜니 피보나치 수열 소스코드가 나왔다. 마침 친구 중 한 명이 중학교 수학 선생님이었는데, 내게 '특정 수가 주어질 때 해당 피보나치 수의 황금비를 구하는 소스코드를 작성할 수 있는가'를 물어보았다.

     

    순간 당황했지만 친구가 '황금비는 특정 피보나치 수를 바로 앞의 피보나치 수로 나누면 구할 수 있다'는 이야기를 해주었고, 다행이 구하는 방식은 머릿속으로 쉽게 그려졌다. 그때 든 생각, '이걸 TDD 방식으로 구해보는 연습을 해볼 수 있겠다.' 문제에 살을 좀 더 붙여보기로 했다.

     

     

    요구사항

    4보다 큰 자연수가 주어질 때, 4부터 주어진 수까지에 해당하는 피보나치 수들의 황금비의 평균값을 구하는 함수를 작성
    (3 이하의 수가 주어질 경우 0 반환)

     

    예시: fibonacciGoldenRatio(7) = ((2 / 1) + (3 / 2) + (5 / 3) + (8 / 5)) / 4 ≈ 1.691666666

     

     

    풀이 시 고려 사항

    - Red >> Green >> Refactoring의 과정을 거친다.

    - 기존에 작성했던 fibonacciNumber(), fibonacciArray()를 사용할 수 있다.

     

     

    과정

    일단 먼저 예외처리할 엣지 케이스의 테스트 코드를 fibonacci.test.js 파일에 작성했다.

    test('Fibonacci Golden Ratio Under 4', () => {
      expect(fibonacciGoldenRatio(3)).toBe(0);
    });

     

    함수가 정의되어 있지 않아 테스트가 실패했고, 즉각적으로 Green을 볼 수 있는 함수를 fibonacci.js 파일에 작성했다.

    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      return 1;
    }

     

    다음으로 조건에 부합하는 가장 작은 수인 4가 주어졌을 경우의 기대값을 테스트 코드에 추가했다.

    test('Fibonacci Golden Ratio With 4', () => {
      expect(fibonacciGoldenRatio(4)).toBe(2 / 1);
    });

     

    역시나 즉각적으로 Green을 볼 수 있도록 상수 값을 반환하게끔 함수를 수정했다.

    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      return 2 / 1;
    }

     

    숫자를 하나 키워서 5가 주어졌을 경우의 기대값도 테스트 코드에 추가했다.

    test('Fibonacci Golden Ratio With 5', () => {
      expect(fibonacciGoldenRatio(5)).toBe(((2 / 1) + (3 / 2)) / 2);
    });

     

    나는 잘 모르겠으니까 이번에도 일단 상수로 테스트부터 통과시키고 봤다.

    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      if (size === 4) {
        return 2 / 1;
      }
    
      return ((2 / 1) + (3 / 2)) / 2;
    }

     

    주어지는 숫자가 1 더 커진 6에 대해서도 마찬가지.

    test('Fibonacci Golden Ratio With 6', () => {
      expect(fibonacciGoldenRatio(6)).toBe(((2 / 1) + (3 / 2) + (5 / 3)) / 3);
    });
    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      if (size === 4) {
        return 2 / 1;
      }
    
      if (size === 5) {
        return ((2 / 1) + (3 / 2)) / 2;
      }
    
      return ((2 / 1) + (3 / 2) + (5 / 3)) / 3;
    }

     

    케이스가 어느 정도 쌓이기 시작하니 반복되는 수들이 눈에 들어오기 시작했다.

    뭐 예를 들면 fibonacciArray[6]의 반환값은 [0, 1, 1, 2, 3, 5]인데, 이 배열의 요소들에 수들을 맞춰줄 수 있을 것 같아 배열의 요소를 이용하는 방식으로 리팩터링을 시도해보았다.

    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      if (size === 4) {
        const fibonacciNumbers = fibonacciArray(4);
        // [0, 1, 1, 2]
        return fibonacciNumbers[3] / fibonacciNumbers[2];
      }
    
      if (size === 5) {
        const fibonacciNumbers = fibonacciArray(5);
        // [0, 1, 1, 2, 3]
        return (
          (fibonacciNumbers[3] / fibonacciNumbers[2])
          + (fibonacciNumbers[4] / fibonacciNumbers[3])
        ) / 2;
      }
    
      const fibonacciNumbers = fibonacciArray(6);
        // [0, 1, 1, 2, 3, 5]
      return (
        (fibonacciNumbers[3] / fibonacciNumbers[2])
        + (fibonacciNumbers[4] / fibonacciNumbers[3])
        + (fibonacciNumbers[5] / fibonacciNumbers[4])
      ) / 3;
    }

     

    테스트가 통과하는 것을 확인했다. 아직도 중복이 눈에 들어왔다. 중복을 어떻게 제거할 수 있을까?

    소스코드의 양상을 살펴볼 때, 몇 가지 특징적인 부분을 살펴볼 수 있었다.

     

    • 배열에서 인덱스가 0, 1인 요소는 사용되지 않는다.
      >> 배열의 앞 두 인덱스는 제거시킬 수 있을 것 같다.

    • 앞 두 인덱스를 제거시킨 배열에 대해 인덱스가 1씩 증가하면서 [인덱스] + [인덱스 + 1]을 계산하고, 나온 결과값들을 모두 더한다.
      >> reduce 함수를 이용해 결과를 쌓을 수 있을 것 같다.

    • 최종 결과값을, 반복 연산을 수행한 횟수로 나눈다.

     

     해당 내용들을 토대로 다시 리팩터링을 시도했다.

    export function fibonacciGoldenRatio(size) {
      if (size < 4) {
        return 0;
      }
    
      const fibonacciNumbers = fibonacciArray(size).slice(2);
    
      const sum = fibonacciNumbers.reduce((accumulator, _, index) => (
        index === fibonacciNumbers.length - 1
          ? accumulator
          : accumulator + (fibonacciNumbers[index + 1] / fibonacciNumbers[index])
      ), 0);
    
      return sum / (fibonacciNumbers.length - 1);
    }

     

    리팩터링한 함수로 테스트를 실행해도 테스트가 모두 통과하는 것을 확인할 수 있었다.

     

     

    최종적으로 몇 가지 임의의 수를 함수에 전달해 함수가 정상적으로 작동되는지 확인했다. 주어지는 수가 충분히 커질수록 황금비인 1.618에 가까운 수를 반환하는 것을 확인할 수 있었다.

    test('Fibonacci Golden Ratio With Some Cases', () => {
      console.log(fibonacciGoldenRatio(50));
      console.log(fibonacciGoldenRatio(200));
      console.log(fibonacciGoldenRatio(1000));
    });

     

     

     

    켄트 백의 저서 '테스트 주도 개발'에서 제시되는 테스트 주도 개발의 리듬은 다음과 같다.

     

    1. 재빨리 테스트를 하나 추가한다.
    2. 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
    3. 코드를 조금 바꾼다.
    4. 모든 테스트를 실행하고 전부 성공하는지 확인한다.
    5. 리팩토링을 통해 중복을 제거한다.

     

    문제에 평균값을 구하는 조건을 추가하니 계획대로 머릿속에서 한번에 정리가 이루어지지 않는 아주 약간 복잡한 문제가 되었는데, 통과하는 테스트들을 일반화시키는 중복 제거를 통해 문제가 거의 바로 해결되는 모습을 보는 경험은 꽤나 신기했다.

     

    이제는 익히 들어 귀에 익숙해지기는 했지만 여전히 귀찮다는 생각에 쉽사리 써지지 않는 테스트 주도 개발 방식, 기회가 될 때마다 사용해보면서 그 방식을 차근차근 몸에 체득시킨다면 자연스럽게 테스트 코드를 먼저 작성하는 습관을 들일 수 있지 않을까 생각한다.

     

     

     

    댓글

Designed by Tistory.