메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

혁신적인 아키텍처와 창발적 설계: 테스트 주도 설계, Part 1

테스트를 통한 설계 진행 및 개선

Neal Ford, 소프트웨어 아키텍트, IBM
Neal Ford사진
Neal Ford는 글로벌 IT 컨설팅 업체인 ThoughtWorks의 소프트웨어 아키텍트이자 Meme Wrangler이다. 애플리케이션, 교육용 자료, 매거진 기사 및 비디오/DVD 프리젠테이션을 설계 및 개발하며 다양한 기술과 관련된 서적의 저자 또는 편집자이기도 하다. 최근에 출판된 책으로는 The Productive Programmer가 있다. 대규모 엔터프라이즈 애플리케이션의 설계 및 빌드에 많은 관심을 가지고 있는 그는 전세계의 개발자 컨퍼런스에서 국제적으로 인정 받고 있는 연사로도 활동하고 있다. 그의 웹 사이트를 살펴보자.

요약:  대부분의 개발자들은 테스트 주도 개발(TDD)을 사용할 때 가장 유익한 부분이 테스트라고 생각한다. 하지만, TDD를 올바로 완료하면 전체적인 코드 설계가 개선된다. 혁신적인 아키텍처와 창발적 설계 시리즈 중 이번 기사에서는 어떻게 테스트를 통해 부각되는 관심사에서 창발적 설계를 할 수 있는지 보여주는 확장된 예제를 살펴보자. 테스트는 TDD의 부수적 효과일 뿐이며, 중요한 점은 TDD가 어떻게 코드를 보다 나은 쪽으로 변화시키느냐 하는 것이다.

이 연재 자세히 보기

기사 게재일:  2011 년 10 월 11 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (146KB | 14 pages)Get Adobe® Reader®
페이지뷰:  1503 회
의견:  


TDD는 일반적인 민첩한 개발 방법 중 하나이다. TDD는 요구사항 단계 중 마지막 단계의 이해에 도움이 되도록 테스트를 사용하는 소프트웨어를 작성하는 하나의 스타일이다. 즉, 코드를 작성하기 전에 테스트를 먼저 작성하여 해당 코드가 수행해야 할 사항을 더욱 확실히 이해하는 것이다.

대부분의 개발자는 TDD로부터 파생되는 주요 이점이 결과적으로 얻게 되는 포괄적인 단위 테스트 세트라고 생각한다. 하지만, TDD를 올바로 완료하면 마지막으로 책임 있는 의사결정을 내릴 수 있을 때까지 의사결정을 미룰 수 있기 때문에 전체적인 코드 설계를 더 나은 쪽으로 바꿀 수 있다. 미리부터 설계와 관련된 의사결정을 하지 않으므로, 더 나은 설계 선택사항이나 더 나은 설계를 위한 리팩토링 과정을 선택할 가능성을 열어둘 수 있다. 이 기사에서는 단위 테스트를 바탕으로 내리게 되는 의사결정에서 창발적으로 설계할 수 있다는 장점이 가진 진정한 힘을 보여주는 예제를 살펴본다.

이 시리즈의 정보

시리즈의 목적은 소프트웨어 아키텍처 및 설계와 관련하여 자주 논의되지만 정의를 명확하게 내리기 어려운 개념에 대한 신선한 관점을 제공하는 것이다. Neal Ford는 구체적인 예제를 통해 실제 애자일 개발 환경에서 경험하게 되는 혁신적 아키텍처창발적 설계에 대한 견고한 기초 지식을 제공한다. 중요한 아키텍처 및 설계 결정을 최후의 결정 순간까지 미룸으로써 소프트웨어 프로젝트에 해가 되는 불필요한 복잡성을 방지할 수 있다.

TDD 워크플로우

테스트 주도 개발이라는 용어에서 중요한 단어는 테스트가 개발 프로세스를 주도한다는 점을 나타내는 주도란 단어이다. 그림 1은 TDD 워크플로우를 나타낸 것이다.


그림 1. TDD 워크플로우
TDD 워크플로우

그림 1에 표시된 워크플로우를 다시 한 번 눈여겨보자.

  1. 실패한 테스트를 쓴다.
  2. 실패한 테스트가 합격할 수 있도록 해줄 코드를 쓴다.
  3. 1단계와 2단계를 반복한다.
  4. 그 과정에서 적극적으로 리팩토링한다.
  5. 더 이상의 테스트를 생각할 수 없으면 워크플로우가 완료된 것이다.

TDD와 TAD

테스트 주도 개발에서는 테스트를 먼저 수행해야 한다. 테스트를 작성하고 실패한 후에만 테스트 대상 코드를 작성한다. 많은 개발자가 TAD(Test-After Development)라고 부르는 유형의 테스트 기법을 사용하는데, 코드를 작성한 다음 단위 테스트를 작성하는 방법이다. 이 경우, 여전히 테스트는 하지만 TDD의 창발적 설계라는 열매를 맺지는 못한다. 개발자가 너무나 끔찍한 수준의 코드를 작성한 후, 이를 어떻게 테스트할지 머리를 긁적이며 고민에 빠지는 것을 막을 길은 없다. 코드를 먼저 작성한다는 것은 코드 작동 방식에 대한 개발자의 선입견을 코드에 임베드한 후 나중에 이를 테스트하겠다는 것과 다름없다. TDD에서는 이 순서를 반대로 선택해야 한다. 즉, 테스트를 먼저 작성하고 실행하여 테스트에 통과할 수 있는 코드의 작성 방법을 파악하는 것이다. 이 중요한 차이를 예증하기 위해 확장된 예제를 제시하겠다.


완전수

TDD의 설계상 이점을 보여주기 위해 해결할 문제점이 있어야 하는데, Kent Beck은 Test Driven Development(참고자료 참조)라는 저서에서 통화를 예로 든다(TDD의 이점을 훌륭하게 보여주면서도 매우 간단함). 실질적인 과제는 문제 영역에서 길을 잃을 정도로 복잡하지는 않지만 실제 값을 보여줄 수 있을 정도는 복잡한 예를 찾는 일이다.

그 때문에 필자는 완전수를 선택했다. 수학적 상식에 약한 사람들을 위해 설명하자면, 완전수라는 개념은 (완전수를 이끌어낸 초창기 증명 중 하나를 해낸) 유클리드 이전 시대로 거슬러 올라간다. 완전수는 그 인수를 합하면 최대 그 수까지 되는 수이다. 예를 들어, 6은 자신을 제외한 인수가 1, 2, 3이고 이들을 합하면 6이 되기 때문에 완전수이다. 완전수에 대해 더욱 알고리즘적인 정의는 (자신을 제외한) 인수의 합이 자신과 같은 수이다. 이 예에서는 1 + 2 + 3 + 6 - 6 = 6으로 계산된다.

그 점이 바로 해결해야 할 문제 영역이다. 완전수 찾기 프로그램을 작성해보자. 이 솔루션을 두 가지 다른 방식으로 구현해보겠다. 우선, TDD를 수행하고 싶다는 생각을 지우고 문제 해결을 위한 코드를 작성하는 데만 집중한 다음, 이를 위한 테스트를 작성한다. 그런 다음, 두 가지 접근 방식을 비교하고 대조할 수 있도록 TDD 버전의 솔루션을 작성한다.

이 예제의 경우 Java 언어(테스트에서 어노테이션을 사용할 것이므로 버전 5 이상), JUnit 4.x(최신 버전), Google 코드의 Hamcrest matcher(참고자료 참조)로 완전수 찾기 프로그램을 구현한다. Hamcrest matcher에서는 표준 JUnit matcher 위에 휴먼 인터페이스 설탕 구조(syntactic sugar)를 제공한다. 예를 들어, assertEquals(expected, actual) 대신 좀 더 실제 문장처럼 읽을 수 있도록 assertEquals(actual, is(expected))라고 쓸 수 있다. Hamcrest matcher는 JUnit 4.x에서 제공되고(단지 정적 가져오기임), 아직 JUnit 3.x를 사용 중인 경우에는 호환 가능 버전을 다운로드할 수 있다.

사후 테스트

목록 1은 PerfectNumberFinder의 첫 번째 버전을 나타낸 것이다.


목록 1. 사후 테스트 PerfectNumberFinder

public class PerfectNumberFinder1 {
    public static boolean isPerfect(int number) {
        // get factors
        List<Integer> factors = new ArrayList<Integer>();
        factors.add(1);
        factors.add(number);
        for (int i = 2; i < number; i++)
            if (number % i == 0)
                factors.add(i);

        // sum factors
        int sum = 0;
        for (int n : factors)
            sum += n;

        // decide if it's perfect
        return sum - number == number;
    }
}

특별히 훌륭한 코드는 아니지만, 이 작업을 완료할 수 있는 코드이다. 모든 인수의 목록을 동적 목록(ArrayList)으로 작성하는 것부터 시작한다. 목록에 1과 대상 숫자를 추가한다. (위에서 제시한 수식을 고수하고 있고, 모든 인수 목록에 1과 대상 숫자 자신이 포함된다.) 그런 다음, 각각의 숫자가 인수인지 차례로 검사하면서 대상 숫자 자신까지 가능한 인수를 반복 계산한다. 어떤 숫자가 인수라면 그 숫자를 목록에 추가한다. 그 다음, 모든 인수를 더하고 마지막으로 위에 표시된 수식의 Java 버전을 작성하여 완전성을 판단한다.

이제는 사후 테스트를 위한 단위 테스트를 실시하여 코드가 올바로 작동하는지 판단해야 한다. 최소한, 완전수가 올바로 표시되는지 여부와 false positives 결과로 출력되지 않는지 여부를 검사하는 두 가지 테스트가 필요하다. 목록 2에 단위 테스트가 나와 있다.


목록 2. PerfectNumberFinder에 대한 단위 테스트

public class PerfectNumberFinderTest {
    private static Integer[] PERFECT_NUMS = {6, 28, 496, 8128, 33550336};

    @Test public void test_perfection() {
        for (int i : PERFECT_NUMS)
            assertTrue(PerfectNumberFinder1.isPerfect(i));
    }

    @Test public void test_non_perfection() {
        List<Integer>expected = new ArrayList<Integer>(
                Arrays.asList(PERFECT_NUMS));
        for (int i = 2; i < 100000; i++) {
            if (expected.contains(i))
                assertTrue(PerfectNumberFinder1.isPerfect(i));
            else
                assertFalse(PerfectNumberFinder1.isPerfect(i));
        }
    }

    @Test public void test_perfection_for_2nd_version() {
        for (int i : PERFECT_NUMS)
            assertTrue(PerfectNumberFinder2.isPerfect(i));
    }

    @Test public void test_non_perfection_for_2nd_version() {
        List<Integer> expected = new ArrayList<Integer>(Arrays.asList(PERFECT_NUMS));
        for (int i = 2; i < 100000; i++) {
            if (expected.contains(i))
                assertTrue(PerfectNumberFinder2.isPerfect(i));
            else
                assertFalse(PerfectNumberFinder2.isPerfect(i));
        }
        assertTrue(PerfectNumberFinder2.isPerfect(PERFECT_NUMS[4]));
    }
}

테스트 이름에 "_" 포함

단위 테스트를 작성할 때 메소드 이름에 밑줄을 포함하는 것은 필자의 코딩 버릇 중 하나이다. 물론, Java 표준에서는 캐멀 케이스(이름 중간에 대문자를 쓰는 방식)가 메소드 이름을 쓰는 올바른 방법이다. 그러나 필자는 테스터 메소드 이름이 일반 메소드 이름과 다르게 보이도록 하려고 이렇게 하는 것이다. 테스트 메소드 이름은 메소드가 무엇을 테스트하는지 나타내야 하므로, 해당 메소드를 통해 원하는 것이 무엇인지 정확한 설명이 포함된 긴 이름이 된다. 하지만, 긴 캐멀 케이스 이름을 읽기가 어렵고, 특히 수많은 테스트 이름이 비슷한 값으로 시작하고 거의 끝 부분에 가서야 분기되기 때문에 수십 또는 수백 개의 테스트가 나타나는 단위 테스트 실행 프로그램에서는 더욱 곤욕스러운 일이다. 그래서 내가 작업하는 모든 프로젝트에서는 코드의 가독성 향상을 위해 밑줄(테스트 이름에서만) 사용을 강력히 지지하는 입장이다.

이 코드를 실행하면 완전수가 올바로 출력되지만, 무척 많은 수를 검사하기 때문에 음수 테스트에서는 실행 속도가 매우 느리다. 단위 테스트에서는 성능 문제가 불거질 수 있으며, 이럴 때는 어떤 사항을 개선할 수 있을지 알아보기 위해 다시 코드를 검토해야 한다. 현재, 필자는 인수를 얻기 위해 숫자 자신에 도달할 때까지 계속 루프를 돌리고 있다. 그러나 그렇게까지 할 필요가 있을까? 인수들을 쌍으로 얻을 수 있다면 그럴 필요가 없을 것이다. 모든 인수는 짝을 이루기 때문이다(예: 대상 숫자가 28인 경우 2라는 인수를 찾으면 14도 자연스럽게 인수임을 알 수 있음). 이처럼 인수를 쌍으로 얻을 수 있다면 목표 숫자의 제곱근까지만 찾아보면 된다. 그래서 원래 알고리즘을 개선하여 목록 3과 같이 코드를 리팩토링한다.


목록 3. 알고리즘을 개선한 버전

public class PerfectNumberFinder2 {
    public static boolean isPerfect(int number) {
        // get factors
        List<Integer> factors = new ArrayList<Integer>();
        factors.add(1);
        factors.add(number);
        for (int i = 2; i <= sqrt(number); i++)
            if (number % i == 0) {
                factors.add(i);
                factors.add(number / i);
            }

        // sum factors
        int sum = 0;
        for (int n : factors)
            sum += n;

        // decide if it's perfect
        return sum - number == number;
    }
}

이 코드의 실행 시간은 괜찮은 수준이지만, 두 가지 테스트 어설션이 실패한다. 이는 인수를 쌍으로 얻는 과정에서 정수 제곱근에 이를 때 인수를 두 번 얻기 때문으로 드러난다. 예를 들어, 숫자 16의 제곱근인 4가 목록에 두 번 추가된다. 목록 4에 나타낸 것처럼, 이런 경우 감시 조건을 작성하면 문제를 쉽게 수정할 수 있다.


목록 4. 개선 후 문제점을 수정한 알고리즘

for (int i = 2; i <= sqrt(number); i++)
    if (number % i == 0) {
        factors.add(i);
        if (number / i !=  i)
            factors.add(number / i);
    }

완전수 찾기 프로그램의 사후 테스트 버전이 완성되었다. 이 버전은 당연히 잘 작동하지만, 몇 가지 설계상의 문제점도 나타난다. 우선, 필자는 코드의 섹션을 서술하기 위해 주석을 사용했다. 이렇게 주석을 사용하면 항상 코드라는 느낌이 날 수밖에 없다. 이는 곧 스스로의 방법으로 리팩토링해달라고 강력히 도움을 요청하는 셈이다. 필자가 방금 새로 추가한 내용에는 아마 작은 감시 조건의 역할을 설명하는 주석이 필요하겠지만, 지금 당장은 그대로 두겠다. 가장 큰 문제점은 그 길이에 있다. Java 프로젝트에서 내가 얻은 경험칙에 따르면, 메소드 코드의 길이는 10행 이하로 해야 한다는 점이다. 메소드 코드의 길이가 10행을 초과하면 거의 확실히 두 가지 이상의 역할을 하게 되므로 초과하지 않도록 주의해야 한다. 이 메소드는 명백히 이런 경험적 추론에 따른 규칙을 위반하므로, 이번에는 TDD를 사용하는 다른 방법을 시도해볼 것이다.


TDD를 통한 창발적 설계

TDD 코딩을 위한 주문은 "테스트를 작성할 수 있는 가장 간단한 것은?"이라는 질문이다. 이 경우, "완전수인가, 아닌가?"라는 질문이 적합할까? 아니다. 그에 대한 대답의 범위가 너무 넓기 때문이다. 문제점을 분석하고 "완전수"의 의미에 대해 생각해봐야 한다. 완전수를 발견하는 데 필요한 여러 가지 단계를 다음과 같이 손쉽게 제시할 수 있다.

  • 문제가 되는 숫자의 인수가 필요하다.
  • 어떤 숫자가 대상 숫자의 인수인지 판단해야 한다.
  • 인수의 합을 구해야 한다.

가장 간단한 것이라는 착상을 바탕으로, 이 목록의 항목 중 어떤 것이 가장 간단해 보이는가? 나는 어떤 숫자가 다른 숫자의 인수인지 판단하는 것이라는 생각이므로, 목록 5에 나타낸 것처럼 그걸 첫 번째 테스트로 정했다.


목록 5. "이 숫자가 인수인가?"를 판단하는 테스트

public class Classifier1Test {

    @Test public void is_1_a_factor_of_10() {
        assertTrue(Classifier1.isFactor(1, 10));
    }
}

이런 간단한 테스트는 우둔할 정도로 하찮아 보이지만, 그게 바로 내가 원하는 바이다. 이 테스트를 컴파일하려면 Classifier1로 이름 지정된 클래스와 함께 isFactor() 메소드가 있어야 한다. 그래서 이 클래스의 기본 폼 구조를 먼저 작성해야 실패에 이를 수 있다. 이렇듯 매우 간단한 단위 테스트를 작성하면 문제 영역에 대해 진지하게 생각하기 시작해야 하는 시점이 오기 전에 구조를 정할 수 있다. 필자는 한 번에 한 가지에 대해서만 생각하고 싶고, 이를 통해 해결하려는 문제점의 뉘앙스를 걱정하지 않고 기본 폼 구조에 집중할 수 있다. 목록 6에 나타낸 것처럼, 이 테스트를 컴파일하고 테스트 실패 기준을 제시하면 코드를 작성할 준비가 된 것이다.


목록 6. 인수 메소드에서의 첫 번째 통과

public class Classifier1 {
    public static boolean isFactor(int factor, int number) {
        return number % factor == 0;
    }
}

정상적이고 간단하게 작동하고, 원하는 작업을 수행하는 것을 확인했다. 이제 다음으로 간단한 태스크, 즉 숫자의 인수 목록을 구하는 태스크로 넘어갈 수 있다. 이 테스트는 목록 7에서 보여준다.


목록 7. 다음 테스트: 한 숫자의 인수

@Test public void factors_for() {
    int[] expected = new int[] {1};
    assertThat(Classifier1.factorsFor(1), is(expected));
}

목록 7은 필자가 인수를 얻기 위해 생각할 수 있는 테스트 중에서 가장 간단한 테스트를 나타낸 것이며, 이 테스트에 통과할 수 있는 가장 간단한 코드를 작성할 수 있다(나중에 리팩토링을 통해 더 정교하게 만들 수 있음). 목록 8에 다음 메소드가 나와 있다.


목록 8. 간단한 factorsFor() 메소드

public static int[] factorsFor(int number) {
    return new int[] {number};
}

이 메소드는 작동하긴 하지만, 도중에 중지된다. isFactor() 메소드는 단순히 입력 데이터를 바탕으로 어떤 출력을 리턴하므로, 이 메소드를 정적 메소드로 만드는 것이 좋을 것 같았다. 하지만, factorsFor() 메소드도 정적으로 만든 메소드였다. 즉, 두 메소드에 모두 number라는 매개변수를 전달해야 한다는 뜻이다. 그러면 이 코드의 프로시저적 성격이 매우 강해지는데, 이는 메소드의 정적 특성이 너무 강해서 생기는 부작용이다. 이런 점을 수정하기 위해, 이미 있는 두 개의 메소드를 리팩토링하겠다. 지금까지 작성한 코드가 별로 길지 않기 때문에 이는 손쉬운 작업이다. 목록 9에 리팩토링한 Classifier 클래스를 표시한다.


목록 9. 개선된 Classifier 클래스

public class Classifier2 {
    private int _number;

    public Classifier2(int number) {
        _number = number;
    }

    public boolean isFactor(int factor) {
        return _number % factor == 0;
    }
}

Classifier2 클래스 내에서 숫자를 멤버 변수로 만들었다. 이렇게 하면 숫자를 매개변수로서 정적 메소드 모음에 전달하지 못하게 할 수 있기 때문이다.

분해 작업 목록에서 다음으로 해야 할 일은 어떤 숫자의 인수를 찾는 것이다. 따라서 다음 테스트에서 인수를 검사해야 한다(목록 10 참조).


목록 10. 다음 테스트: 한 숫자의 인수

@Test public void factors_for_6() {
    int[] expected = new int[] {1, 2, 3, 6};
    Classifier2 c = new Classifier2(6);
    assertThat(c.getFactors(), is(expected));
}

이제, 목록 11과 같이 주어진 매개변수에 대한 인수의 배열을 리턴하는 메소드를 구현한다.


목록 11. getFactors() 메소드에서의 첫 번째 통과

public int[] getFactors() {
    List<Integer> factors = new ArrayList<Integer>();
    factors.add(1);
    factors.add(_number);
    for (int i = 2; i < _number; i++) {
        if (isFactor(i))
            factors.add(i);
    }
    int[] intListOfFactors = new int[factors.size()];
    int i = 0;
    for (Integer f : factors)
        intListOfFactors[i++] = f.intValue();
    return intListOfFactors;
}

이 코드로 테스트에 통과할 수 있지만, 다시 살펴보면 끔찍하기 짝이 없다! 테스트를 사용하여 코드를 구현하는 방법을 조사할 때 가끔 이런 일이 생긴다. 이 코드의 어떤 점이 잘못된 것일까? 우선, 코드가 매우 길고 복잡하여 "둘 이상의" 문제점이 우리를 괴롭힌다. 나는 본능적으로 int[]를 리턴하도록 만들었지만, 사실은 코드의 복잡도가 많이 증가되어 아무런 쓸모가 없어진다. 이 메소드를 호출할지도 모르는 향후의 메소드를 위해 더 편리하게 만들겠다는 생각에 얽매인 나머지, 시작하기 너무 어려운 해결책에 집착했기 때문이다. 이 시점에서 이처럼 복잡한 것을 추가할 확실한 이유가 있어야겠지만, 나는 아직 그 타당성을 찾지 못하겠다. 이 코드를 잘 살펴보면 아마도 factors가 클래스에 대한 내부 상태로도 존재해야 이 메소드의 기능을 분해할 수 있을 것이라는 점을 알 수 있다.

테스트를 통해 드러나는 유익한 특성 중 하나는 참으로 응집력 있는 메소드이다. Kent Beck은 큰 영향력을 떨친 Smalltalk Best Practice Patterns(참고자료 참조)라는 저서에서 이 점에 대해 기술했다. 그 책에서, Kent는 컴포즈드 메소드(composed method)라는 패턴을 정의했다. 컴포즈드 메소드 패턴은 다음 세 가지 주요 사항을 정의한다.

  • 식별 가능한 하나의 태스크를 수행하는 메소드로 프로그램을 나눈다.
  • 같은 추상 레벨에 있는 메소드에 모든 연산을 유지한다.
  • 그러면 자연스럽게 프로그램이 다수의 작은 메소드로 나뉘며, 각 메소드의 길이는 몇 행에 불과하다.

컴포즈드 메소드는 TDD로 촉진되는 유익한 설계 특성 중 하나인데, 필자는 목록 11getFactors() 메소드에서 이 패턴을 명백히 위반했다. 다음 단계에 따라 이 점을 수정할 수 있다.

  1. factors를 internal 상태로 승격한다.
  2. factors에 대한 초기화 코드를 생성자로 이동한다.
  3. int[] 코드로의 (gold-plated) 변환을 제거하고 이 변환을 하는 것이 유리해지는 경우 나중에 이 문제를 다룬다.
  4. addFactors()에 대해 다른 테스트를 추가한다.

네 번째 단계는 꽤 난해하지만 중요하다. 결함이 있는 이 코드 버전을 작성하면 분해 단계에서의 첫 번째 통과가 완전하지 않았음이 드러난다. 이 긴 메소드의 중간에 있는 addFactors() 코드 행은 테스트 가능한 작동이다. 이 문제점은 너무 사소한 것이라서 처음 살펴볼 때는 몰랐는데 지금은 알고 있다. 이런 문제점은 자주 발생한다. 한 가지 테스트로 이 문제점을 각각 테스트 가능한 작은 청크들로 분해할 수 있다.

getFactors()의 더 큰 문제점들은 잠시 보류하고 새로 알게 된 이 작은 문제점부터 해결하겠다. 따라서 다음 테스트는 목록 12에 나타낸 addFactors()이다.


목록 12. addFactors()에 대한 테스트

@Test public void add_factors() {
    Classifier3 c = new Classifier3(6);
    c.addFactor(2);
    c.addFactor(3);
    assertThat(c.getFactors(), is(Arrays.asList(1, 2, 3, 6)));
}

목록 13에 표시된 테스트의 코드 자체는 단순하다.


목록 13. 인수 추가를 위한 간단한 코드

public void addFactor(int factor) {
    _factors.add(factor);
}

드디어 테스트에 통과하리라는 확신에 가득 차서 단위 테스트를 실행하지만 실패한다! 어째서 이처럼 간단한 테스트가 실패할 수 있을까? 그림 2에 근본 원인이 나와 있다.


그림 2. 테스트 실패의 근본 원인
단위 테스트 실패의 근본 원인

내 예상 목록에는 1, 2, 3, 6이라는 값이 있는데, 실제 리턴된 결과는 1, 6, 2, 3이다. 그걸 보고서야 내가 생성자에서 1과 대상 숫자를 추가하도록 코드를 변경했기 때문이란 걸 알았다. 이 문제점의 한 가지 해결책은 1과 대상 숫자가 항상 먼저라고 가정하고 예상 목록을 작성하는 것이다. 그러나 그것이 올바른 해결책일까? 아니다. 문제는 훨씬 더 근본적인 데 있다. 인수가 숫자의 목록일까? 아니다. 인수는 숫자의 세트이다. 애초의 (잘못된) 가정으로 인해 인수에 대해 정수 목록을 사용했지만, 그게 잘못된 추상이다. 목록 대신 세트를 사용하도록 코드를 리팩토링함으로써 이제는 더 정확한 추상을 사용하고 있기 때문에, 이 문제점을 수정할 뿐만 아니라 전체적으로 더 나은 해결책을 찾을 수 있다.

이것이 바로 판단을 흐릴 수 있는 코드를 작성하기 전에 테스트를 작성하면 드러낼 수 있는 잘못된 생각이다. 이 간단한 테스트 덕분에 더 알맞은 추상을 발견했고, 따라서 전체적으로 더 나은 코드를 설계할 수 있는 것이다.


결론

지금까지 완전수 문제라는 컨텍스트에서 창발적 설계를 논의했다. 특히, 첫 번째 버전의 솔루션(사후 테스트 버전)에서는 데이터 유형에 대해 동일하게 잘못된 가정을 했음에 주목하자. "사후 테스트"로는 코드의 대체적인 기능을 테스트할 수 있지만, 개별 파트를 테스트할 수는 없다. TDD에서는 대체적인 기능을 구성하는 요소들을 테스트하여 그 과정에서 더 많은 정보가 노출된다.

다음 기사에서도 완전수 문제를 계속 다루면서, 테스트를 피하는 경우 나타날 수 있는 설계에 대한 몇 가지 예제를 더 설명하겠다. TDD 버전을 완성하면 두 가지 코드 베이스 간에 몇 가지 메트릭을 비교해 볼 것이다. 또한, 전용 메소드를 테스트하는 경우와 시기와 같이, TDD에 대한 다른 몇 가지 어려운 설계 관련 문제도 다룰 것이다.


참고자료

교육

  • Hamcrest matchers: 다른 프레임워크에서 사용하기 위해 "match"를 선언적으로 정의할 수 있도록 허용하는 matcher 오브젝트의 라이브러리다.

  • Test-Driven Development(Kent Beck, Addison-Wesley, 2003년): Extreme Programming의 창시자인 Beck이 통화를 바탕으로 한 예제를 사용하여 TDD를 설명한다.

  • Smalltalk Best Practice Patterns(Kent Beck, Prentice Hall, 1996년): 컴포즈드 메소드 패턴에 대해 자세히 알아보자.

  • The Productive Programmer(Neal Ford, O'Reilly Media, 2008년): Neal Ford가 최근에 내놓은 저서의 "Test Driven Development" 장에서 본 기사에서 제시한 예제의 상세 버전을 볼 수 있다.

  • "Emergent Optimization in Test Driven Design"(Michael Feathers저): 테스팅이 미성숙한 최적화를 방지하는 데 도움을 주는 방법.

  • 기술 서점에서 다양한 기술 주제와 관련된 서적을 살펴보자.

  • developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.

제품 및 기술

  • JUnit: JUnit을 다운로드할 수 있다.

토론

필자소개

Neal Ford사진

Neal Ford는 글로벌 IT 컨설팅 업체인 ThoughtWorks의 소프트웨어 아키텍트이자 Meme Wrangler이다. 애플리케이션, 교육용 자료, 매거진 기사 및 비디오/DVD 프리젠테이션을 설계 및 개발하며 다양한 기술과 관련된 서적의 저자 또는 편집자이기도 하다. 최근에 출판된 책으로는 The Productive Programmer가 있다. 대규모 엔터프라이즈 애플리케이션의 설계 및 빌드에 많은 관심을 가지고 있는 그는 전세계의 개발자 컨퍼런스에서 국제적으로 인정 받고 있는 연사로도 활동하고 있다. 그의 웹 사이트를 살펴보자.

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=자바
ArticleID=764320
ArticleTitle=혁신적인 아키텍처와 창발적 설계: 테스트 주도 설계, Part 1
publish-date=10112011

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.