[tdd] 객체를 활용한 테스트 주도 개발

객체의 설계에 대해 설명하고 있다

객체망

객체 지향 설계는 객체 자체보다 객체간의 의사소통에 더 집중한다

중요한 것은 메시지 전달이며, 위대하고 성장 가능한 시스템을 만들때의 핵심은 모듈간의 의사소통에 있지, 모듈의 내부 특성이나 작동 방식에 있지 않다

시스템은 객체를 생성해 서로 메시지를 주고받을 수 있게 조립하는 과정을 거쳐 만들어진다.
시스템의 행위는 객체의 조합(객체의 선택과 연결 방식)을 통해 나타나는 특성이다.

객체망

이런식으로 시스템을 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어서, 시스템에 포함된 객체의 구성을 변경해 시스템 작동 방식을 쉽게 바꿀 수 있다.

값과 객체

시스템을 설계할 때는 값(value)과 객체(object)를 구분하는 것이 중요하다.
값은 변하지 않는 양이나 크기를 나타내며, 객체는 식별자를 가지고 시간이 지남에 따라 상태가 변할수도 있는 애들을 가리킨다.
(DDD의 Value Object와 Reference Object)
대부분의 객체지향 언어에서는 이 두 개념을 모두 클래스라는 동일한 언어 구성물로 구현한다는 점에서 혼동의 여지가 있다

  • 값(value)
    • 양이 고정된 불변 인스턴스
    • 개인 식별자가 없으므로 두 값 인스턴스의 상태가 같다면 사실상 동일한 셈이다
      • 그러므로 두 값의 식별자를 비교하는 것은 적절하지 않다
      • string1 == string2 보단 string1.equals(string2) 를 쓰라고 하는 이유이다
  • 객체(object)
    • 변경 가능한 상태를 이용해 시간의 추이에 따른 객체의 행위를 나타낸다
    • 두 객체 인스턴스의 상태가 정확히 동일하더라도 별개의 식별자를 가진다
      • 즉, 식별자로 비교해야 한다

메시지를 따르라

  • 객체를 설계할 때 다른 객체와 쉽게 관계를 맺을 수 있게 객체를 설계해야 한다
  • 객체가 의사소통 패턴을 따르고, 객체간의 의존성이 명시적이어야 한다
    • 의사소통 패턴은 다른 객체와 상호 작용하는 방법을 관장하는 각종 규칙으로 구성되어 있다
    • 객체의 역할, 객체에서 전달 가능한 메세지, 전달 가능한 시점 등
  • 의사소통 구조는 처음 객체를 배울때 느끼는 정적인 분류에서 개념적으로 굉장히 발전한 단계에 해당한다
    • 객체간 메시지 주고받는 것에 더 집중한 방식이라고 생각된다

역할, 책임, 협력자
객체는 역할을 하나 이상 구현한 것이며, 책임은 어떤 과업을 수행하거나 정보를 알아야 할 의무를 말한다
협력은 객체나 역할(또는 둘 다)의 상호 작용에 해당한다

CRC 카드(또는 UML)를 이용해가며 객체를 모델링 해보는 것도 좋은 방법이다

묻지 말고 말하라(★)

서로간에 메시지를 전달하는 객체가 있다면, 서로 무슨 이야기를 할까?

객체를 호출할 땐 이웃 객체가 하는 역할 측면에서 해당 객체가 무엇을 원하는지 기술하고,
호출된 객체가 전달받은 바를 어떻게 실현할 지 결정한다

  • 디미터의 법칙(Law of Demeter), 묻지 말고 말하라(Tell, Don’t Ask)
    • 객체는 그것이 내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만 가지고 의사결정을 내려야한다
    • 객체가 다른 객체를 탐색해 뭔가를 일어나게 하면 안된다
    • 호출자는 해당 객체의 내부 구조나 또는 그 너머에 존재하는 시스템의 구조에 대해 알 필요가 없다(알아서도 안된다)
    • 이 스타일을 일관되게 따른다면 코드가 좀 더 유연해진다
      • 같은 역할을 수행하는 객체를 손쉽게 교체할 수 있기 때문이다
1
2
3
4
5
6
7
8
// 외부에서 물어서 직접 판단하지 말고
master.getModelisable()
.getDockablePanel() // 이러한 것을
.getCustomizer() // '열차 전복'이라고 한다
.getSavetem().setEnabled(Boolean.FALSE.booleanValue());

// 해당 객체가 판단하게끔 하라
master.allowSavingOfCustomisations();

이렇게 작성함으로써 추가적으로 얻는 이점은 아래와 같다

  • master를 이용하는 쪽에서 메서드를 해당 객체의 내부 구조까지 몰라도 된다
    • 기존에는 연이어 호출하는 모든 객체의 타입까지 알고 있었다
  • 만약 설계가 변경되어도, 이 코드에 미치는 영향이 작다
    • 기존처럼 사용되는 곳이 많았다고 하면, 설계 변경이 미치는 영향이 매우 컸을것이다
  • 명시적인 이름을 부여하였기 때문에 코드를 이해하기가 더 쉬워진다

그래도 가끔은 물어라

물론 모든 것만을 말하지만은 않는다
값과 컬렉션으로부터 정보를 가져오거나 팩토리를 이용해 새 객체를 생성할때(?)는 묻는다

검색이나 필터링을 할 때를 생각해보면 된다

그러나 이렇게 묻는 과정에서도 열차 전복은 피하게끔 작성해야 한다

1
2
3
4
5
6
7
8
9
// 이렇게 내부 구조를 노출해서는 안된다  
if(carriage.getSeats().getPercentReserved() < persentReservedBarrier){
request.reserveSeatsIn(carriage);
}

// 정말로 내가 원하는 답을 주게끔 질문을 해야한다
if(carriage.hasSeatsAvailableWithin(persentReservedBarrier)){
request.reserveSeatsIn(carriage);
}

이렇게 작성하면 추가적으로, 이해하기 쉽고 테스트하기 쉬워진다는 장점을 얻을 수 있다

참고로 이런식의 질의(getter 포함) 메서드는 되도록 적게 쓰려고 하는 것이 좋다

  • 질의가 객체 바깥으로 새어 나가서 시스템이 더 경직될 수 있기 때문이다
  • 호출하는 객체의 의도를 서술하는 질의를 작성하려고 애써야한다

협력 객체의 단위 테스트

위의 디미터의 법칙을 지키다보면, assertion을 하나도 쓸 곳이 없어지는 것 같은 현상을 맞이하게 된다
(각 객체가 서로 명령을 전달하고, 상태를 질의하는 수단을 노출하지 않기 때문에)

호출되었을때 주위에 하나 또는 그 이상의 이웃객체에 메시지를 보내는 객체의 메서드가 있다고 한다면,
해당 메서드가 올바르게 수행되었는지 어떻게 테스트 할 수 있을까(내부 상태를 드러내지 않고)?

협력자가 잘 호출되었는지 mock으로 검증하라?

SUT의 이웃(협력자)를 stub으로 대체하면 된다
실제로 SUT를 테스트할때는 협력자까지 같이 테스트 할 필요는 없기 때문이다.
협력자에 대한 테스트들은 협력자들이 직접 수행하게끔 하고, 우리는 stub에 예상 구문(expectation)을 작성하여 SUT의 테스트만 집중하게끔 해야한다(맞나?)

  1. 필요한 mock(stub) 객체 생성
  2. mock(stub)을 포함한 실제 객체 생성
  3. 협력객체가 어떻게 호출될것인지를 기술(행위 검증을 하라는 의미인가?)
  4. SUT에서 협력객체가 포함된 메서드를 호출
  5. 예상되는 메서드 호출이 모두 일어났는지 확인

모든 테스트 의도를 명확하게 해서 테스트를 거친 기능과 보조 역할을 담당하는 기반 구조, 객체 구조를 분리하는 것?