ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Test Driven Design (TDD) :: 켄트 백
    정리필요1 2008. 9. 5. 22:30

    테스트 주도 개발(Test-Driven Development)

    테스트 주도 개발(Test-Driven Development, 이하 TDD)은 최근 학계와 업계에서 주목받는 프로그래밍 방법으로, 여러 연구 논문과 실례를 통해 개발자의 생산성과 역량을 증폭시킨다는 사실이 널리 받아들여지고 있다.

    TDD는 테스트가 개발을 주도하는 방법이다. 즉, 테스트가 코딩의 방향을 이끌어 간다는 말이다. 테스트를 실패하는 코드가 없으면 코딩을 하지 않고, 코드 상에 중복이 있으면 제거하는 간단한 규칙을 지킬 때 자연스레 아름다운 코드가 천변만변 펼쳐진다. 덤으로 회귀 테스트도 생기고, 개발 과정 자체가 즐거워진다.



    1. 테스트 주도 개발의 목표
      - 발생할 버그에 대해 걱정하지 않고, 작업이 언제 마무리 될지 알 수 있다.
    2. 테스트 주도 개발 방식
      - 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
      - 코드의 중복을 제거한다.
      # DRY 원칙을 지키자. - by 실용주의 프로그래밍
    3. 테스트 주도 개발의 행동패턴
      - 결정사항에 대해 피드백을 제공하는 실행 가능한 코드를 기반으로 하는 유기적인 설계를 해야한다.
      - 개발 환경은 작은 변화에도 빠르게 반응할 수 있어야 한다.
      - 테스트를 쉽게 하기 위해선, 응집도는 높고 결합도는 낮은 컴포넌트들로 구성되게 설계해야 한다.
    4. 테스트 주도 개발의 프로그래밍 순서
      - 빨강 : 실패하는 작은 테스트를 작성한다.
      - 초록 : 빨리 테스트가 작동하여, 통과하게 만든다.
      # Copy and Paste 또는 특정 결과 값을 무조건 반환하도록 구현하는 방법 등으로 최소한의 시간으로 테스트를 통과시킨다.
      - 리팩토링 : 테스트를 통과하게 만드는 중에 생긴 모든 중복을 제거한다.
    5.  테스트 주도 개발의 사회적 함의
      - 결함 밀도를 충분히 감소 시킬 수 있다면, 품질보증(QA)를 능동적인 작업으로 전환할 수 있다.
      # 결함 밀도, 원문에는 어떻게 쓰였을지 모르나, 결합 밀도의 오타가 아닐까 생각. 만약 결함이라면 버그의 밀도를 의미하는 듯 하다.
      - 고약한 예외 상황의 숫자를 충분히 낮출 수 있다면, 프로젝트 매니저가 정확히 추정할 수 있어 고객을 매일의 개발 과정에 참여시킬 수 있다.
      # 자동화된 테스트에서 예외적으로 발생하는 버그를 낮춘다는 의미로 쓰여진 듯
      # 고객을 매일의 개발 과정에 참여시킬 수 있다는 것은 예정된 계획에 맞춰 개발 일정이 진행 되므로(예상치 못한 버그 발생으로 인해 시간 지연이 없다), 고객의 요구사항을 정확히 이행하고 있는지를 바로 판단하거나 또는 고객에게 결과물에 대한 피드백을 즉시 받을 수 있다.

    높은 응집도, 낮은 결합도     

    ‘높은 응집도, 낮은 결합도(High Cohesion, Loose Coupling)’의 원리는 1970년대 Larry Constantine과 Edward Yourdon이 정의했던 아주 고전적인 원리이다. 이것은 현재 모든 소프트웨어 시스템 고유의 유지보수성과 적응성을 측정하는 가장 좋은 방법으로 사용되고 있다. 소프트웨어 디자인뿐만 아니라 아키텍처 평가에도 이 원리가 기준이 되는데, 그 이유는 이 원리의 적용 효과가 아주 명백하기 때문이다.

    이 원리의 예외는 거의 찾아보기 힘들만큼 보편성을 가지고 있어서 마치 물리학의 엔트로피 법칙처럼 절대적인 기반원리를 제시한다. 낮은 응집도를 갖는 구조는 변경이나, 확장 단계에서 많은 비용을 지불해야 하며 높은 결합도의 경우도 마찬가지이다.

    응집도는 ‘하나의 클래스가 하나의 기능(책임)을 온전히 순도 높게 담당하고 있는 정도’를 의미하며 이들은 서로 조화될수록 그 구조는 단순해진다. 응집도가 높은 동네에서 내부 개체가 변했을 때 다른 개체에 충격을 주는 것은 오히려 당연한 징후이다. 이들은 하나의 책임아래 서로 유기적인 관계를 갖고 있기 때문에 내부 개체가 변했을 때 다른 개체의 변경 확률이 높아진다. 마치 예쁜 부츠를 사면 부츠에 어울리는 치마를 입어야 하듯이… 응집도의 종류는 다양한데 다음은 권장할 만한 순기능적 응집 관계들이다.

    * 기능적 응집(Functional Cohesion)
    일관된 기능들이 집합된 경우를 말하며 <그림 3>의 데이터 맵퍼는 DB 처리라는 기능 항목의 높은 응집성을 갖는다.
    * 순차적 응집(Sequential Cohesion)
    한 클래스 내에 등장하는 하나의 소작업(메쏘드)의 결과가 다음 소작업(메쏘드)의 입력으로 사용되는 관계(파이프라인 방식의 처리 체인 관계).
    * 교환적 응집(Communicational Cohesion)
    동일한 입력과 출력 자료를 제공하는 메쏘드들의 집합을 말하며, 팩토리 클래스는 전형적인 교환적 응집도가 높은 인터페이스를 갖는다.
    * 절차적 응집(Procedural Cohesion)
    순서적으로 처리되어야 하는 소작업(메쏘드)들이 그 순서에 의해 정렬되는 응집관계
    * 시간적 응집(Temporal Cohesion)
    시간의 흐름에 따라 작업 순서가 정렬되는 응집관계
    * 논리적 응집(Logical Cohesion)
    유사한 성격의 개체들이 모여 있을 경우를 말하며 java.io 클래스들의 경우가 대표적인 예이다.

    이와 반해 결합도는 ‘클래스간의 서로 다른 책임들이 얽혀 있어서 상호의존도가 높은 정도’를 의미하며 이들이 조합될수록 코드를 보기가 괴로워진다. 이유는 서로 다른 책임이 산만하고 복잡하게 얽혀있기 때문에 가독성이 떨어지고 유지보수가 곤란해지기 때문이다. 이유는 필요 없는 의존성에 있다. 마치 키보드의 자판 하나가 고장나도 키보드 전체를 바꿔야 하는 것처럼. 하나의 변경이 엄청난 민폐를 야기하는 관계이다. 다음은 수용할 수 있는 수준의 결합 관계들이다.

    * 자료 결합(Data Coupling)
    두 개 이상의 클래스가 매개변수에 의해서 결합 관계를 가지므로 낮은 수준의 결합도로 연관되는 경우
    * 스탬프 결합(Stamp Coupling)
    자료 결합의 경우에서 매개변수 일부만을 사용하는 경우
    * 제어 결합 (Control Coupling)
    두 클래스간의 제어 이동이 매개변수를 이용하여 사용되는 경우로 커맨드 패턴이 대표적인 사례이다.


    의문사항 정리

    1. 테스트 주도 개발 방식에서 새로운 코드는 테스트 코드를 의미하는가?
      -
    2. 테스트 주도 개발의 사회적 함의에서 능동적인 작업이란 자동화된 테스트에 용의하다는 것을 의미하는가?
      -
    3. 테스트의 단위를 어떻게 잡아야 할까?
      -
    4. 만약 함수 단위로 테스트를 한다면 자동화된 테스트는 모든 함수에 대해 테스트한다는 것을 의미하는가?
  • 코드 표준화를 적용하여 재 수정 했습니다.
    - 헝가리안 표기법을 사용하다 낙타 표기법으로 변경하여 처음엔 조금 어색했지만 금방 익숙해져 식별하는데 불편함이 없었습니다.
    - 클래스와 멤버함수, 멤버변수의 식별을 헝가리안 표기법 없이도 낙타표기법으로 Class, getFunction, variable로 식별할 수 있지만
    동일한 이름을 가진 time 변수와 time() 함수가 있을 경우 식별이 어려운 것 같습니다.
    - 전체 코드를 표준에 맞게 작성하였는데, 이클립스의 코드 템플릿이 자동으로 변경해 주는 기능을 알게 되어 막강한(?) 리눅스의 장점을 배웠습니다.
  • 코드 문서화를 적용하여 주석을 추가 했습니다.
    - Doxygen 문법은 이전부터 써왔기에 적응하는데는 별 어려움은 없었습니다. 이전에 쓸 때는 틀에 맞춘 주석이 아니라서
    문서로 뽑아서 봐도 깔끔하지 않았는데. 주어진 양식에 맞춰 주석을 다니, 코드도 깔끔하고, 문서로 뽑았을 때도 깔끔할 것 같습니다.

  • 클래스를 파일 분할하고 테스트 코드와 라이브러리 코드를 분리했습니다.
    - 예전에는 함수에 대한 테스트를 거의 하지 않았는데, TDD의 예제를 따라하며 각각의 함수를 테스트하여, 빠르게 버그를 찾아 낼 수 있었습니다.
  • 리팩터링을 거쳐 중복을 제거하고, 예외 처리 부분을 추가했습니다.
    - 불필요한 중복된 코드 뿐 만 아니라, 다른 클래스의 함수 들을 상위 클래스로 묶어 중복을 제거 하거나, 로직을 더욱 깔끔하게 바꾸는 부분을 느꼈습니다. 기존에는 코드를 한번 짜놓으면 버그가 생기기 전엔 수정하지 않았지만, TDD는 처음부터 리팩토링을 계속 하게되어 한번 더 생각하게 되고 더 깔끔해지는 것 같습니다.

  • 화폐 예제 마무리

    - TDD의 기본 개념을 습득하였습니다.
    - 테스트의 중요성과, 테스트 주도 개발의 장점을 예제를 학습하며, 자연스레 이해할 수 있었습니다.
    - 테스트 주도 개발을 사용하면서 리팩토링을 계속 생각하게 되었습니다.
    - 실용주의 프로그래머에서 강조하는 DRY 원칙을 이 책에서도 역시 중요하게 다루고 있었습니다.

  • 의문사항 해결
    - 테스트 주도 개발 방식에서 새로운 코드는 테스트 코드를 의미하는가?
    >> 테스트가 실패 했을 경우 테스트를 정상적으로 돌리기 위해 로직을 수정하고, 수정된 로직에 대한 다른 테스트를 추가한다.
    - 테스트 주도 개발의 사회적 함의에서 능동적인 작업이란 자동화된 테스트에 용의하다는 것을 의미하는가?
    >> 문제가 발생했을 때 해당 문제를 수정하는 수동적인 작업 보다 테스트를 통해 미리 문제 발생을 예방하고, 정상적인 작동을 검증할 수 있다. 차후 프로젝트가 커질 수록 처음에 작업했던 부분에 대한 검증이 자동으로 이루어진다.
    - 테스트의 단위를 어떻게 잡아야 할까?
    >> 모든 기능에 대해 최소한의 단위에서 부터 여러가지 방법으로 테스트를 해야한다.
    - 만약 함수 단위로 테스트를 한다면 자동화된 테스트는 모든 함수에 대해 테스트한다는 것을 의미하는가?
    >> 함수의 호출로 인해 발생하는 결과를 예측하고, 원하는 값이 나오는지 확인하고, 잘못된 값을 주어서 실패하는 것을 테스트하여, 하나의 함수라도 예외적으로 발생하는 문제들을 미리 해결할 수 있게 테스트해야 한다.


    • 테스트 주도 개발 패턴
      - 어떻게 테스트할 것인가?
      자동화된 테스트를 만든다.
      - 테스트를 실행하는 것이 서로 어떤 식으로 영향을 미쳐야 좋은가?
      아무 영향이 없어야 한다.
      - 무엇을 테스트할 것인가?
      구현해야 할 것들을 테스트 목록화 한다. 즉 테스트를 통해 구현을 이끌어 간다.
      - 언제 테스트를 작성할 것인가?
      테스트 대상이 되는 코드를 작성하기 직전에 작성한다.
      - 테스트를 작성할 때 단언(assert)은 언제 할까?
      단언을 제일 먼저 쓰고 시작한다.
      - 테스트할 때 어떤 데이터를 사용해야 하는가?
      테스트를 읽을 때 쉽고 따라가기 좋을 만한 데이터를 사용한다.
      - 데이터의 의도를 어떻게 표현할 것인가?
      테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력한다.
    • 빨간 막대 패턴
      - 다음 테스트를 고를 때 무엇을 기준으로 할 것인가?
      새로운 무엇인가를 가르쳐 줄 수 있으며, 구현할 수 있다는 확신이 드는 테스트를 고른다.
      - 어떤 테스트부터 시작하는게 좋은가?
      오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트한다.
      - 자동화된 테스트가 더 널리 쓰이게 하려면 어떻게 할 것인가?
      테스트를 통해 설명을 요청하고 테스트를 통해 설명한다.
      - 외부에서 만든 소프트웨어에 대한 테스트를 작성해야 하는가?
      패키지의 새로운 기능을 처음으로 사용해보기 전에 작성할 수 있다.
      - 주제에서 벗어나지 않고 기술적인 논의를 계속할 수 있을까?
      주제와 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아간다.
      - 시스템 장애가 보고될 때 무엇을 제일 먼저 해야 하는가?
      그 장애로 인하여 실패하는 테스트, 그리고 통과할 경우엔 장애가 수정되었다고 볼 수 있는 테스트를 간단하게 작성한다.
      - 지치고 고난에 빠졌을 땐 무엇을 해야 하는가?
      휴식...
    • 테스팅 패턴
      - 큰 테스트 케이스를 어떻게 돌아가도록 할 수 있을까?
      테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고, 그 작은 테스트가 실행되도록 한다.
      그 후에 원래의 큰 테스트를 추가한다.
      - 비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야 할까?
      상수를 반환하게끔 만든 속임수 버전의 리소스를 만들면 된다.(모의 객체)
      - 한 객체가 다른 객체와 올바르게 대화하는지 테스트 하려면 어떻게 할까?
      테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 만든다.
      - 메시지의 호출 순서가 올바른지를 검사하려면 어떻게 해야 할까?
      로그 문자열을 가지고 있다가 메시지가 호출될 때마다 그 문자열에 추가하도록 한다.
      - 호출되지 않을 것 같은 에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트할 것인가?
      실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 이를 호출한다.
      - 팀 프로그래밍을 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까?
      모든 테스트가 성공한 상태로 끝마치는 것이 좋다.
    • 초록 막대 패턴
      - 실패하는 테스트를 만든 후 첫 번째 구현은 어떻게 하는 게 좋을까?
      상수를 반환하게 한다. 테스트가 통과하면 단계적으로 상수를 변수를 사용하는 수식으로 변형한다.
      - 추상화 과정을 테스트로 주도할 때 어떻게 최대한 보수적으로 할 수 있을까?
      오직 예가 두 개 이상힐 때에만 추상화를 한다.
      - 단순한 연산들을 어떻게 구현하는가?
      그냥 구현한다...
      - 객체 컬렉션을 다루는 연산은 어떻게 구현 할까?
      컬렉션 없이 구현하고 그 다음에 컬렉션을 사용하게 한다.
    • 디자인 패턴
      - 간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요하다면 어떻게 해야 할까?
      계산 작업에 대한 객체를 생성하여 이를 호출한다.
      - 공유해야 하지만 동일성은 중요하지 않을 때 객체를 어떤식으로 설계할 수 있을까?
      객체가 생성될 때 객체의 상태를 설정한 수 이 상태가 절대 변할 수 없도록 한다.
      그리고 이 객체에 대해 수행되는 연산은 언제나 새로운 객체를 반환하게 만든다.
      - 객체의 특별한 상황을 표현하고자 할 때 어떻게 해야 할까?
      특별한 상황을 표현하는 새로운 객체를 만들면 된다. 그리고 이 객체에 다른 정상적인 상황을 나타내는 객체와 동일한 프로토콜을 제공한다.
      - 작업 순서는 변하지 않지만 각 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶은 경우 이를 어떻게 표현할 것인가?
      다른 메서드들을 호출하는 내용으로만 이루어진 메서드를 만든다.
      - 변이를 어떻게 표현할 것인가?
      명시적인 조건문을 사용한다.
      - 인스턴스별로 서로 다른 메서드가 동적으로 호출되게 하려면 어떻게 해야할까?
      메서드의 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출한다.
      - 새 객체를 만들 때 유연성을 원하는 경우 객체를 어떻게 생성하는가?
      생성자를 쓰는 대신 일반 메서드에 객체를 생성한다.(팩토리 메서드)
      - 기존의 코드에 새로운 변이를 도입하려면 어떻게 할까?
      기존의 객체와 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다.
      - 하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행동하게 만들려면 어떻게 해야 할까?
      객체 집합을 나타내는 객체를 단일 객체에 대한 임포스터로 구현한다.
      - 여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집하려면 어떻게 해야 할까?
      결과가 수집될 객체를 각 오퍼레이션의 매개 변수로 추가한다.
      - 전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 어떻게 해야 할까?
      사용하지 마라...
    • 리팩토링
      - 비슷해 보이는 두 코드 조각을 합치려면 어떻게 해야 할까?
      두 코드가 단계적으로 닮아가게끔 수정한다. 둘이 완전ㅇ 동일해지면 둘을 합친다.
      - 객체나 메서드의 일부만 바꾸려면 어떻게 해야 할까?
      바꿔야 할 부분을 격리한다.
      - 표현 양식을 변경하려면 어떻게 해야 할까 ?
      일시적으로 데이터를 중복시킨다.
      - 길고 복잡한 메서드를 읽기 쉽게 만들려면 어떻게 할까?
      긴 메서드의 일부분을 별도의 메서드로 분ㅇ리해내고 이를 호출하게 한다.
      - 너무 꼬여있거나 산재한 제어 흐름을 단순화하려면 어떻게 할까?
      메서드를 호출하는 부분을 호출될 메서드의 본문으로 교체한다.
      - 메서드를 원래 있어야 할 장소로 옮기려면 어떻게 해야 할까?
      어울리는 클래스에 메서드를 추가해주고, 그것을 호출하게 한다.
      - 여러 개의 매개 변수와 지역 변수를 갖는 복잡한 메서드를 어떻게 표현할까?
      메서드를 꺼내서 객체로 만든다.