[테스트 주도 개발] 1장_다중 통화를 지원하는 Money 객체

테스트 주도 개발의 리듬은 다음과 같다.

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

어떤 테스트들이 있어야 보고서에 제대로 계산되도록 하는 코드가 완성됐다고 확신할 수 있을까 ?

  1. 통화가 다른 두 금액을 더해서 주어진 환율에 맞게 변환 금액을 결과로 얻을 수 있어야한다.
  2. 어떤 금액을 어떤 수에 곱한 금액을 결과로 얻을 수 있어야한다.

앞으로 어떤 일을 해야하는지 알려주고, 하는 일에 집중할 수 있게 하고, 언제 일이 끝나는지 알려 줄 수 있도록 할 일 목록을 만든다. 작업이 끝난 항목에는 줄을 긋는다.
할일 목록에서 볼 수 있듯이 곱하기를 먼저 다룬다. 작은 것부터 시작한다.

1
2
3
4
5
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
AssertionErrors.assertEquals(10, five.amount);
}

위 테스트는 아직 컴파일조차 되지 않는다. 실행은 안되더라도, 컴파일만은 되도록 만들고 싶다. 네 개의 컴파일 에러가 있다.

  1. Dollar 클래스가 없음
  2. 생성자가 없음
  3. times(int) 메서드가 없음
  4. amount 필드가 없음
1
2
3
4
5
6
7
8
9
10
11
class Dollar {
int amount;

Dollar(int amount) {

}

void times(int multiplier){

}
}

위 코드에서 times(int multiplier) 는 stub 구현이다. 이 메서드를 호출하는 코드가 컴파일 될 수 있도록 껍데기만 만들어두는 것을 의미한다.
테스트를 다시 실행해보자. 실패한다. 테스팅 프레임워크가 결과로 10이 나와야 하는데 0이 나왔다는 것을 알려준다.

Read more

[테스트 주도 개발] 2장_타락한 객체

TDD 주기는 다음과 같다.

  1. 테스트를 작성한다.
  2. 실행가능하게 만든다.
  3. 올바르게 만든다. (중복 제거)

“작동하는 깔끔한 코드” 를 얻어야한다는 전체 문제 중에서, “작동하는” 에 해당하는 부분을 먼저 해결해라.
테스트를 하나 통과했지만, Dollar 에 대한 연산을 수행한 후에 해당 Dollar 의 값이 바뀌는 것이 이상하다.

1
2
3
4
5
6
7
void testMultiplication() {
Dollar product = new Dollar(5);
product.times(2);
assertEquals(10, product.amount);
product.times(3);
assertEquals(15, product.amount);
}

times() 를 처음 호출한 이후에 product 는 더이상 5가 아니다. times() 가 새로운 객체를 반환하면 어떨까 ?

1
2
3
4
5
6
7
void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(15, product.amount);
}
1
2
3
4
5
6
7
8
9
10
11
12
class Dollar {
int amount;

Dollar(int amount) {
this.amount = amount;
}

Dollar times(int multiplier) {
amount *= multiplier;
return null;
}
}

위 코드는 컴파일되지만 실행되지 않는다. 통과를 위해서는 올바른 금액을 갖는 새 Dollar 를 반환해야한다.

1
2
3
4
5
6
7
class Dollar {
...

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

최대한 빨리 초록색을 보기 위한 방법이 있다.

Read more

[오브젝트] 부록 B 타입 계층의 구현

타입과 타입 계층을 구현할 수 있는 방법들을 정리하자.

클래스를 이용한 타입 계층 구현

타입은 객체의 퍼블릭 인터페이스이다. 클래스는 객체의 타입과 구현을 동시에 정의한 것이다.

Phone 클래스가 있다. Phone 의 인스턴스는 calculateFee 메시지를 수신할 수 있는 퍼블릭 메서드를 구현한다. 타입은 퍼블릭 인터페이스이기 때문에, Phone 클래스는 calculateFee 메세지에 응답할 수 있는 타입을 선언한 동시에 객체 구현을 정의한 것이다.

상속은, 퍼블릭 인터페이스는 유지하면서 새로운 구현을 가진 객체를 추가할 수 있는 간단한 방법이다. 하지만, 상속은 자식 클래스를 부모 클래스의 구현에 강하게 결합시킨다.

인터페이스를 이용한 타입 계층 구현

인터페이스는, 상속으로 인한 결합도 문제를 피하고 다중 상속이라는 구현 제약을 해결할 수 있는 방법이다.

추상 클래스를 이용한 타입 계층 구현

추상 클래스는, 클래스 상속을 이용해 구현을 공유하면서도 결합도로 인한 부작용을 피하는 방법이다.

추상 클래스와 인터페이스 결합하기
Read more

[오브젝트] 부록 C 동적인 협력, 정적인 코드

좋은 설계는 객체 사이의 협력과 행동을 표현하는 동적 모델을 기반으로 해야한다.

  1. 동적 모델
    프로그램 실행 구조를 표현하는 움직이는 모델

  2. 정적 모델
    코드의 구조를 담는 고정된 모델

01 동적 모델과 정적 모델

행동이 코드를 결정한다

객체가 외부에 제공하는 행동이 중요하다. 동적 모델이 정적 모델을 결정해야한다.

02 도메인 모델과 구현

도메인 모델에 관하여
  1. 도메인
    사용자가 프로그램을 사용하는 대상 영역

  2. 모델
    지식을 선택적으로 단순화하고 의식적으로 구조화한 형태

  3. 도메인 모델
    사용자가 프로그램을 사용하는 대상 영역에 대한 지식을 선택적으로 단순화하고 의식적으로 구조화한 형태

소프트웨어의 도메인에 대해 고민하고 도메인 모델을 기반으로 소프트웨어를 구축해야한다. 그러면, 개념과 소프트웨어 사이의 표현적 차이를 줄일 수 있기 때문에 이해하고 수정하기 쉬온 소프트웨어를 만들 수 있다.

중요한 것은, 도메인 모델을 작성하는 것이 목표가 아니라 출발점이다. 중요한 것은 객체들의 협력을 지원하는 코드 구조를 만드는 것이다. 코드의 구조를 주도하는 것은 행동이다.

Read more

[오브젝트] 부록 A 계약에 의한 설계

인터페이스만으로 객체의 행동에 관한 다양한 관점을 전달하기 어렵다.
명령의 부수효과를 쉽고 명확하게 표현할 수 있는 커뮤니케이션 수단이, 계약에 의한 설계이다.
여기서 중요한 것은 코드가 아니라, 개념이다.

01 협력과 계약

부수 효과를 명시적으로

일반적인 정합성 체크 로직은 코드의 구현 내부에 숨겨져있어 실제로 코드를 분석하지 않는 한 정확하게 파악하기 어렵다.

하지만, Code Contracts 와 같이 계약에 의한 설계를 지원하는 라이브러리나 언어들은 일반 로직과 구분할 수 있도록 제약 조건을 명시적으로 표현하는 것이 가능하다.

02 계약에 의한 설계

버트란드 마이어가 제시한 계약은, 사람들 사이의 계약과 유사하다. 계약은 협력에 참여하는 두 객체 사이의 의무와 이익을 문서화한 것이다.

  1. 협력에 참여하는 객체는 계약으로부터 이익을 기대하고 이익을 얻기위해 의무를 이행한다.
  2. 협력에 참여하는 객체의 이익과 의무는 객체의 인터페이스 상에 문서화된다.

의도를 드러내는 인터페이스는 오퍼레이션의 시그니처만으로 어느 정도 클라이언트와 서버가 협력을 위해 수행해야하는 제약 조건을 명시한다.

계약은 여기서 한 걸음 더 나아간다. 서버는 자신이 처리할 수 있는 범위의 값들을 클라이언트가 전달할 것으로 기대한다. 클아이언트는 자신이 원하는 값을 서버가 반환할 것이라고 예상한다. 클아이언트는 메세지 전송 전과 후의 서버 상태가 정상일 것이라고 기대한다.

Read more