[테스트 주도 개발] 10장_흥미로운 시간

두 times() 구현이 거의 비슷하다. 하지만 완전히 동일하지 않다.

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
}
1
2
3
4
5
6
7
8
9
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}
}

팩토리 메서드를 인라인 시키면 어떨까 ?

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Franc(amount * multiplier, "CHF");
}
}
1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Dollar(amount * multiplier, "USD");
}
}

바로 전 장에서 팩토리 메서드를 호출하는 것으로 바꿨었는데.. 실망스러운 일이다.
Franc 에서는 인스턴스 변수 currency 가 항상 ‘CHF’ 이므로,

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Franc(amount * multiplier, currency);
}
}
1
2
3
4
5
6
7
8
9
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}

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

Franc 를 가질지, Money 를 가질지 정말 중요한 사실인가 ? 고민하는 대신 그냥 수정하고 테스트를 돌려보자.

Read more

[테스트 주도 개발] 11장_모든 악의 근원

두 하위 클래스에는 이제 생성자밖에 없다. 제거하자.

코드의 의미를 변경하지 않으면서 하위 클래스에 대한 참조를 상위 클래스에 대한 참조로 변경할 수 있다.

1
2
3
4
5
6
7
8
//Money
static Money dollar(int amount) {
return new Money(amount, "USD");
}

static Money franc(int amount) {
return new Money(amount, "CHF");
}

이제 Dollar 에 대한 참조가 하나도 없으므로 지울 수 있다. 하지만, Franc 는 테스트 코드에서 아직 참조한다.

1
2
3
4
@Test
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}

이 테스트를 지워도 될 정도로 다른 곳에서 이미 동치성 테스트를 충분히 하고 있는가 ?

1
2
3
4
5
6
7
8
@Test
void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5))); // 1
assertFalse(Money.dollar(5).equals(Money.dollar(6))); // 2
assertTrue(Money.franc(5).equals(Money.franc(5))); // 3
assertFalse(Money.franc(5).equals(Money.franc(6))); // 4
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}

3, 4 단언은 1, 2 단언과 중복이다. 지우자.

1
2
3
4
5
6
@Test
void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}

Read more

[테스트 주도 개발] 12장_드디어 더하기

전체 더하기 기능에 대한 스토리를 어떻게 적어야할지 모르겠다. 간단한 예, “5 달러 + 5달러 = 10 달러” 에서 시작하자.

1
2
3
4
5
@Test
void testSimpleAddition() {
Money sum = Money.dollar(5).plus(Money.dollar(5));
assertEquals(Money.dollar(10), sum);
}

그냥 Money.dollar(10) 을 반환하는 식으로 가짜 구현을 할 수 있지만, 어떻게 구현할지 명확하므로 다음과 같이 하겠다.

1
2
3
4
// Money
Money plus(Money addend){
return new Money(amount + addend.amount, currency);
}

다중 통화 사용에 대한 내용을 시스템의 나머지 코드에 숨기고 싶다. ( 2달러 + 3CHF ) * 5 를 보자. Money 가 수식의 가장 작은 단위가 된다. 연산의 결과로 Expression 들이 생기는데, 그 중 하나가 sum 이다. 연산이 완료되면 환율을 이용해서 결과 expression 을 단일 통화로 축약할 수 있다. 이런식으로 테스트할 수 있다.

1
2
3
4
5
@Test
void testSimpleAddition() {
....
assertEquals(Money.dollar(10), reduced);
}

reduced 란 이름의 expression 은 expression 에 환율을 적용해서 얻어진다. 실세계에서 환율이 적용되는 곳은 어디인가 ? 은행이다.

1
2
3
4
5
6
@Test
void testSimpleAddition() {
...
Money reduced = back.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}

여기서, 단순히 “…reduce = sum.reduce(“USD”, back)” 라고 할수 있었다. 왜 Back 가 reduce() 를 수행할 책임을 맡았을 까 ?

  1. Expression 은 하려고 하는 일의 핵심이다. 핵심이 되는 객체가 다른 부분에 대해서 될 수 있는 한 모르도록 하자. 그러면, 핵심 객체가 가능한 오랫 동안 유연할 수 있다. 그리고, 테스트하기도 쉽고 재활용하거나 이해하기에 쉽다.
  2. Expression 과 관련이 있는 오퍼레이션이 많은 수 있다. 모든 오펴레이션을 Expression 에만 추가하면 Expression 는 무한히 커진다.
Read more

[빅데이터] 3장_빅데이터를 위한 데이터 모델: 사례

Apache Thrift 란 직렬화 프레임워크를 사용해 SuperWebAnalytics.com 데이터 모델을 구현한다.

3.1 어째서 직렬화 프레임워크인가

많은 개발자들이 원시 데이터를 기록하는 방법으로 JSON 등의 스키마 없는 형식을 고른다. 작업을 쉽게 착수할 수 있는 장점이 있지만, 데이터 오염이 언제든지 터질 수 있는 단점이 있다.
데이터 오염 문제는 그 문제가 어떻게 발생했는지에 대한 전후사정을 거의 손에 넣을 수 없기 때문에 디버깅이 어렵다. 예를 들어, 필수 항목이 누락되어 NPE 가 발생하면 문제의 원인이 누락된 항목이라는 것은 바로 알 수 있지만 애초에 그 데이터가 어떻게 들어왔는지에 대한 정보는 없다.
강제 가능 스키마를 만들었으면, 데이터를 기록하는 시점에 오류가 나므로 데이터가 무효화된 사정과 원인에 대한 전후 사정을 알 수 있다. 그리고, 이때 발생한 오류 덕에 프로그램은 무효 데이터를 기록하지 못하므로 마스터 데이터 집합도 오염되지 않는다.
직렬화 프레임워크를 사용하면 강제가능 스키마를 쉽게 적용 할 수 있다.

3.2 Apache Thrift

Apache Thrift 는 정적 타입의 강제가능 스키마를 정의하는데 쓰이는 도구이다. 이와 유사한 도구로, Protocol Buffers 나 Avro 등이다.
Apache Thrift 의 주요 요소는 구조체 (struct) 와 공용체 (union) 타입 정의이다. 이들은 다음 필드의 조합으로 구성된다.

  1. 기본 데이터 타입 : 문자열, 정수, long 정수, double 실수
  2. 다른 타입의 집합체 (리스트, 맵, 세트)
  3. 다른 구조체와 공용체
3.2.1 노드

SuperWebAnalytics.com 의 사용자 노드에서 개인은 사용자ID 나 브라우저 키기로 식별된다. 이 둘이 동시에 함께 식별에 쓰이지는 않는다. 이런 패턴은 노드를 나타날 때 흔히 볼 수 있는데, 공용체 데이터 타입과 일치한다. 하나의 값으로 여러 가지를 나타내는 타입이다.

1
2
3
4
5
6
7
8
union PersonID {
1: string cookie;
2: i64 user_id;
}

union PageID {
1: String url;
}
3.2.2 간선
Read more

[테스트 주도 개발] 4장_프라이버시

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

첫 번째 assertion 을 Dollar 와 Dollar 를 비교하는 것으로 재작성할 수 있다.

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

두 번째 assertion 도 마찬가지다.

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

이제 임시 변수인 product는 필요없다.

1
2
3
4
5
6
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}

테스트를 고치고 나니, Dollar 의 amount 인스턴스 변수를 사용하는 코드는 Dollar 자신 밖에 없다. 따라서, 변수를 private 으로 변경 가능하다.

1
private int amount;

동치성 테스트가, 동치성에 대한 코드가 정확히 동작하는 것을 검증하는데 실패하면다면 곱하기 테스트 역시, 곱하기에 대한 코드가 정확히 동작한다는 것을 검증하는데 실파헤가 된다. 이것은 TDD 를 하면서 적극적으로 관리해야할 위험 요소이다.

Read more