기록은 기억의 연장선

더 많은 것을 기억하기 위해 기록합니다


  • Home

  • Tags

  • Categories

  • Archives

  • Search

[tdd] TDD로 화폐 개발하기(2)

Posted on 2019-04-13 | Edited on 2020-11-02 | In tdd | Comments:

이번 예제는 아래의 것들을 설명해주고 있다

  • 큰 단위의 테스트를 작게 접근하는 방법
  • 테스트를 기반으로 수월하게 리팩토링 하는 방법
  • 중복되는 두 클래스의 형태를 동일하게 바꾼 뒤 상위 클래스로 올리기(push up)
  • 해결법이 보이지 않을때 다시 되돌아가서 시도하는 방법

큰 단위의 작업에 접근하기

이제 처음에 복잡해보여서 시도하지 못했던 $5 + 10CHF = $10(환율이 2:1일 경우) 을 작성해보자
알다시피 이 요구사항의 경우 테스트 단위가 크다
우리는 이렇게 큰 테스트를 공략할 수 없으므로 진전을 나타낼 수 있는 작은 테스트를 만들어야 한다

일단 뭐 잘 모르겠고, 다시 보니 Dollar와 비슷하게 Franc이 필요해보이니 추가해보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// FrancTest
@Test
public void testMultiplication() {
Franc five = new Franc(5);

assertThat(five.times(2)).isEqaulTo(new Franc(10));
assertThat(five.times(3)).isEqaulTo(new Franc(15));
}

@Test
public void testEquality() {
assertThat(new Franc(5)).isEqualTo(new Franc(5));
assertThat(new Franc(5)).isNotEqualTo(new Franc(6));
}

알다시피 이 테스트를 빨리 초록막대에 도달하게 해야한다
가장 빠른 방법은 Dollar를 복사히여 Franc을 만드는 것이다(뭐 테스트도 복사한 마당에…)

복사 붙여넣기라니, 굉장히 이상해보일 수 있다
하지만 알다시피 5단계(중복제거)에서 다 수정하면 된다
4단계까지는 설계보다 속도가 더 중요하고, 그것을 위해선 어떤 죄악도 저지를 수 있다

물론 5단계에서 이 죄악을 수습하지 않고는 다음 단계로 나아갈 수 없다
여기서 적절한 설계를 하고, 돌아가게 만들고, 올바르게 만들어야 한다

죄를 수습하자

중복을 제거하기 위한 방법은 뭐가 있을까? 두 클래스의 공통된 상위 클래스를 추출해보는건 어떨까?

먼저 Dollar 부터 적용시켜보자

  1. 먼저 Dollar와 Franc의 공통 클래스로 Money라는 애를 만들고, 이를 상속받게 한다

    1
    2
    3
    4
    5
    6
    class Money{}

    class Dollar extends Money {
    private int amount;
    // ...
    }
  2. amount 변수를 Money로 올리고(protected), Dollar에서 amount 변수를 제거할 수 있다

    1
    2
    3
    4
    5
    6
    7
    class Money{
    protected int amount;
    }

    class Dollar extends Money {
    // ...
    }
  3. 이제 equals 부분을 Money로 올려볼 수 있다

    • 우선 equals 부분을 Money에 맞춰 변경하고

      1
      2
      3
      4
      public boolean equals(Object object) {
      Money money = (Money) object;
      return amount == money.amount;
      }
    • Money 클래스로 올린다

    • Dollar에서 equals는 삭제한다

각 과정을 진행할때마다 계속 테스트를 돌리면서 진행했고, 수월하게 리팩토링 할 수 있었다
이제 Franc도 똑같이 진행할 것이다
아까 DollerTest를 복사해서 FranTest를 만들어줬기 때문에 Franc 또한 Dollar 처럼 수월하게 리팩토링이 가능하다

만약 Franc에 대한 테스트를 추가하지 않았다면 라팩토링 전에 추가해주는 것이 좋다
(이 외에도 있어야 할 것 같은 테스트가 있다면 작성해줘야 한다)

그렇게 하지 않으면 리팩토링 하다가 결국 뭔가 꺠트릴 것이고,
리팩토링에 대해 안좋은 느낌을 갖게 되고,
리팩토링을 덜 하게 되고,
코드의 질이 떨어지게 되고,
해고당한다(!!!)

또 한번 찾아온 불길한 느낌

우리는 앞 단계에서 배웠다
부작용에 대한 혐오감이 생기는 경우, 즉시 테스트를 추가해서 결과를 확인해봐야 한다

1
2
3
public void testEquality() {
assertThat(new Dollar(5)).isNotEqualTo(new Franc(5));
}

테스트가 실패한다!
그래도 불안감을 눈으로 확인해보니 마음이 한결 편해졌다
이 부분은 객체의 클래스를 비교함으로써 간단하게 구현가능하다

1
2
3
4
5
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
&& getClass() == money.getClass();
}

간단하게 테스트를 통과시켰다
클래스 타입 비교로 통화를 비교한다는게 조금 그렇긴하지만 일단은 그냥 넘어간다

더 많은 동기가 있기 전에는 더 많은 설계를 하지 않는것이 좋다

하위클래스를 없애기 위한 시도 - 직접 참조 제거

상위 클래스 추출이 성공했으니, 남아있는 times() 메서드의 리턴타입도 상위 클래스로 변경해도 괜찮겠다

1
2
3
4
5
6
7
8
9
10
11
class Dollar {
Money times(int multiplier) {
return new Dollar(amount * multiplier);
}
}

class Franc {
Money times(int multiplier) {
return new Franc(amount * multiplier);
}
}

이쯤되니 두 하위 클래스의 형태가 많이 비슷해졌다
바로 제거하는 테크를 타고 싶긴하지만, 그렇게 한번에 큰 step을 밟는것은 좀 위험하고 TDD에도 맞지 않으니 단계적으로 진행하도록 한다

첫번째 단계는 하위클래스에 대한 직접적인 참조들을 제거하는 것이다
new Dollar(5) 와 같은 강력한 직접 참조들을 먼저 제거해보자
직접 참조를 제거하는 방법으로 팩토리 메서드를 써보면 좋을 것 같다

1
2
3
4
5
6
7
8
9
10
11
12
// DollarTest
@Test
public void testEquality() {
assertThat(Money.dollar(5)).isEqualTo(new Dollar(5));
assertThat(Money.dollar(5)).isNotEqualTo(new Dollar(6));
}

public void testMultiplication() {
Dollar dollar = Money.dollar(5);
assertThat(Money.dollar(10)).isEqualTo(dollar.times(2));
assertThat(Money.dollar(15)).isEqualTo(dollar.times(3));
}

팩토리 메서드는 아래와 같이 만든다

1
2
3
static Dollar dollar(int amount) {
return new Dollar(amount);
}

times()의 직접 참조도 제거하고,

1
2
3
public Money times(Integer multiplier) {
return Money.dollar(amount * multiplier);
}

Dollar에 대한 참조까지 제거해주자

1
2
3
4
public void testMultiplication() {
Money money = Money.dollar(5);
// ...
}

Money에 times가 없기 때문에 오류가 발생한다
이를 위해 추상 클래스를 하나 추가해준다(더 먼저해야 했을수도 있었다)

1
2
3
abstract class Money {
abstract Money times(int multiplier);
}

이제 완벽하게 new Dollar()를 팩토리 메서드로 대체할 수 있다

하위 클래스의 존재를 테스트 메서드에서 분리(decoupling)헀으므로, 이제 테스트에 영향을 주지 않고 상속구조를 마음껏 변경할 수 있게 된다
테스트 메서드뿐 아니라 모든 클라이언트 코드에서도 이런식으로 결합도를 낮추면 변화에 유연해진다

이제 Franc에 대해서도 똑같이 작업해준다

똑같이 변경해놓고 보니, DollarTest와 FrancTest의 테스트들의 형태가 매우 중복되어 보인다
나는 이 시점에서 두 테스트를 합쳐서 MoneyTest로 만들었다

이런식으로 하위클래스가 분리되다보면 몇몇 테스트가 불필요한 여분의 것이 된다

하위클래스를 없애기 위한 시도 - 추상화

times()의 모양을 같게 하려면 Money.dollar와 Money.franc을 같게 만들어야한다
그러므로 둘을 추상화 할수있는 뭔가가 필요하다
통화라는 개념을 도입하면 뭔가 하위 클래스 제거에 좀 더 가까워질 것 같다

먼저 통화 개념에 대한 테스트를 작성해본다

1
2
3
4
public void testCurrency() {
assertThat(Money.dollar(1).currency()).isEqualTo("USD");
assertThat(Money.franc(1).currency()).isEqualTo("CHF");
}

통화를 인스턴스 변수에 저장하고 메서드에서 그걸 반환해주면 될 것 같다
(좀 더 step by step으로 갈수도 있긴 하지만 바로 떠오르니깐 뭐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Dollar
private Strnig currency;

public Dollar(int amount) {
this.amount = amount;
currency = "USD";
}

public String currency() {
return currency;
}

// Franc
private String currency;

public Franc(int amount) {
this.amount = amount;
currecny = "CHF";
}

public String currency() {
return currency;
}

변수 선언과 currency() 메서드가 동일하므로, 이를 위로 올릴(push up) 수 있다(야호!)

1
2
3
4
5
6
// Money
protected String currency;

String currency() {
return currency;
}

Dollar와 Franc의 생성자에서 currency 세팅하는 부분을 파라미터로 받게하면
두 생성자의 형태가 똑같아질수 있을 것 같다

1
2
3
4
5
6
// Dollar
public Dollar(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// Franc도 동일

이렇게 바꿨더니 아래의 메서드가 깨진다(ㅠㅠ)

1
2
3
4
5
6
7
public static Dollar dollar(Integer amount) {
return new Dollar(amount);
}

public static Franc franc(Integer amount) {
return new Franc(amount);
}

우린 하위클래스의 형태를 맞추고 push up을 진행하는 중이었는데, 구조상의 변경으로 인해 이런식의 상황이 종종 발생할떄가 있다
이럴때는, 이걸 지금 고쳐야할까, 아니면 나중에 고쳐야할까?
교리상은 하던일을 중단하지 않고 다 끝낸 다음에 고치는것이 맞지만, 이정도의 짧은 중단은 그냥 받아들이고 가도 괜찮다

중요한것은 하던 일을 중단하고 다른 일을 하는 상태에서 또 그 일을 중단하면 안된다는 것이다(Jim Coplien)

1
2
3
4
5
// Money
public static Money dollar(int amount) {
return new Dollar(amount, "USD");
}
// Franc도 동일

컴파일 에러를 해결했으니 위로 올리자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Money
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}

// Dollar
public Dollar(int amount, String currency) {
super(amount, currency);
}
// Franc
public Franc(int amount, String currency) {
super(amount, currency);
}

생성자를 바로 제거하고 싶었으나 아직 Money가 추상 클래스라 불가능하다
times를 먼저 손봐야한다

이런식으로 단계적으로 밟아가는 과정이 답답할수도 있다
중요한 것은 이런식으로 일해야 한다는 것이 아니라, 이런식으로 일할수도 있어야 한다는 것이다
종종걸음 step이 답답하면 조금 보폭을 늘려도 되고, 성큼성큼 걷는것이 불안하면 조금 보폭을 줄이면 된다
TDD란 조종해나가는 과정이다. 올바른 보폭이란 존재하지 않는다

다시 돌아가서 생각을…

times() 메서드를 바로 위로 올려보려고 했는데, 생김새가 좀 막막하다

1
2
3
4
5
6
7
8
9
// Dollar
public Money times(Integer multiplier) {
return Money.dollar(amount * multiplier);
}

// Franc
public Money times(Integer multiplier) {
return Money.franc(amount * multiplier);
}

잘 모르겠으니 팩토리 메서드를 다시 생성자로 돌려보자
(이렇게 방법이 없을땐 잠시 후퇴해서 보는것도 방법이다)
(+ 여기서 currency는 클래스에 있는 값을 바로 쓰도록 한다. 굳이 파라미터로 또 전달할 필요 없다)

1
2
3
4
5
6
7
8
9
// Dollar
public Money times(Integer multiplier) {
return new Dollar(amount * multiplier, currency);
}

// Franc
public Money times(Integer multiplier) {
return new Franc(amount * multiplier, currency);
}

이렇게 돌려놓으니 좀 보이는것 같다
Dollar나 Franc으로 생성하느냐는 별로 중요한 것 같지가 않다. 어쩌피 currency가 있는데 굳이 뭐하러.
저걸 Money로 바꿔보자.

1
2
3
4
5
// Dollar
public Money times(Integer multiplier) {
return new Money(amount * multiplier, "CHF");
}
// Franc

Money를 구현 클래스로 만들어야 하므로, 추상 메서드를 제거한다

1
2
3
4
// Money
public Money times(Integer multiplier) {
return null;
}

이렇게 하고 테스트를 돌렸더니, Money 클래스가 Dollar 클래스가 아니라는둥, Money 클래스가 Franc 클래스가 아니라는 둥의 결과가 출력된다
이 말인 즉, 문제는 equals에 있었던 것이다. 비교해야 될 것은 클래스가 아니라 currency이다

이를 위해 추가적인 테스트가 작성되어야하는데, 현재 빨간막대 상태이다
빨간 막대 상태일때는 테스트를 추가하지 않는 것이 좋다
좀 보수적이긴 하지만, 다시 Dollar와 Franc의 new Money를 new Dollar, new Franc으로 돌린다

그리고 테스트를 작성한다

1
2
3
4
@Test
public void testDifference() {
assertThat(new Money(5, "USD")).isEqualTo(new Dollar(5, "USD"));
}

이 테스트를 통과시키기 위해 equals를 변경한다

1
2
3
4
5
public boolean equals(Object obj) {
Money money = (Money) obj;
return amount.equals(money.amount)
&& currency().equals(money.currency());
}

테스트가 통과하니, 다시 times의 new Dollar, new Franc을 new Money로 바꾼다
이 테스트 또한 잘 통과하고, 이제 times의 형태가 같아졌으니 push up 할 수 있다!!

뒤로 돌아가지 않았다면 이처럼 팩토리 메서드 사용을 제거할 수 있다는 사실을 알기 힘들었을 것이다

하위클래스를 없애기 위한 시도 - 나머지 부분 제거

이제 두 클래스에 남은건 생성자밖에 없다
단지 생성자 때문에 하위클래스를 남겨놓을수는 없으니, 제거하는 것이 좋다
유일하게 남아있는 하위 클래스 직접 참조인 정적 팩토리 메서드를 수정하자

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

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

이제 완벽하게 제거하…려고 하는데, 생각해보니 아직 직접 참조가 한군데 더 남아있다

1
2
3
4
@Test
public void testDifference() {
assertThat(new Money(5, "USD")).isEqualTo(new Dollar(5, "USD"));
}

보니까 이 테스트는 이미 다른 테스트에서 수행하고 있는 작업들이다. 제거하자.
이로 인해 최종적으로 하위 클래스를 전부 제거할 수 있게된다!!

Read more »

[tdd] TDD로 화폐 개발하기(1)

Posted on 2019-04-06 | Edited on 2020-11-02 | In tdd | Comments:

이번 예제는 아래의 것들을 설명해주고 있다

  • 처음 테스트를 작성하고, 주기를 완성하는 방법
  • 불길한 예감을 추가해서 테스트를 단단하게 하는 방법
  • 프로덕션 구현을 어떻게 해야할지 모르겠을 때 삼각측량을 이용하는 방법
  • 코드나 테스트가 리팩토링되면서 감춰도 되는 변수를 private 으로 감추는 방법

TDD 주기 만들기

요구사항(작업해야 할 목록)을 나열한다

  • 통화가 다른 두 금액을 더한 금액(주어진 환율에 맞게)을 결과로 얻을 수 있어야한다
  • 어떤 금액을 어떤 수에 곱한 금액을 결과로 얻을 수 있어야한다

요구사항을 보고 할일 목록을 작성한다

  • $5 + 10CHF = $10(환율이 2:1일 경우)
  • $5 x 2 = $10
  • Dollar 부작용(side effect?)
  • 등등등

이중 간단한 것 부터 시작한다

  • 복잡한 것은 작게 나눠서 시작하던지, 아예 손을 대지 않는것이 좋다
  • 여기서는 $5 x 2 = $10 부터 시작한다

필요할 테스트를 생각해보고, 작성한다

  • 이때 테스트할 메서드의 완벽한 인터페이스(형태)에 대해 상상해보는 것이 좋다
  • 가능한 최선의 API에서 시작해서 거꾸로 작업하는 것이 애초부터 일을 복잡하고 보기 흉하며 현실적이게 하는 것보다 낫다
1
2
3
4
5
6
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertThat(five.amount).isEqaulTo(10);
}

Dollar 클래스, 내부 메서드들이 구현되지 않았기 때문에 컴파일부터 실패하므로, 이를 해결한다

  • Dollar 클래스 생성
  • 생성자 생성
  • times(int) 생성
  • amount 필드 생성
    1
    int amount;

컴파일만 될 수 있게 최소한의 구현만 해서, expected/actual 을 반환하는 형태가 되도록 빨리 만든다

현재 상태에서는 exptected : 10, actual : 0 을 반환하며 테스트가 실패하지만, 이것도 진척이다(정확히 만족시켜야 할 상황을 알게되기 때문이다)
이제 스텁구현(끔찍한 죄악!)을 통해 테스트를 만족시킨다

1
int amount = 10;

테스트가 통과하니, 이제 리팩토링(중복 제거)해야한다
10이라는 숫자는 사실 초기값과 곱하고자 하는 수가 같이 들어가있는, 중복 데이터이다
이를 분리한다

1
int amount = 5 * 2;

그리고 뭐… 이렇게 저렇게해서 아래와 같이 진행한다

1
2
3
4
5
6
7
8
// constructor
Dollar(int amount) {
this.amount = amount;
}

void times(int multiplier) {
amount *= multiplier;
}

단계들이 굉장히 작다고 느껴질 수 있는데,
TDD의 핵심은 작은 단계를 밟아야 한다는 것이 아니라, 이런 작은 단계를 밟을 능력을 갖추어야 한다는 것이다
작은 단계로 작업하는 방법을 배우면, 저절로 적절한 크기의 단계로 작업할 수 있게 된다
하지만 큰 단계로만 작업했다면, 더 작은 단계가 적절한 경우에 대해 결코 알지 못하게 된다

위 과정에서 볼 수 있는 일반적인 TDD의 주기는 아래와 같다

  1. 테스트를 작성한다
    • 메서드 형태가 어떤식으로 나타나길 원하는지 생각해본다
  2. 실행 가능하게 만든다
    • 다른 무엇보다도 중요한 것은 빨리 초록 막대를 보는 것이다
    • 여기서 어떠한 죄악을 저질러도 상관없다
      • 만약 깔끔하고 단순한 해법이 명백히 보인다면 그것을 입력한다
      • 굳이 돌아갈 필요는 없다
  3. 올바르게 만든다
    • 이제 시스템이 작동하므로(초록 막대!) 그 전에 저질렀던 죄악을 수습해야 한다
    • 죄악을 수습하는 과정은 대부분 중복 제거이다

우리의 목적은 동작하는 깔끔한 코드를 얻는 것이다
하지만 이는 최고의 프로그래머들도 도달하기 힘든 목표이고, 우리같은 일반적인 사람들은 거의 불가능한 일이다
그래서 분할하여 정복(Divide and Conquer)를 사용하는 것이다
일단 작동하는 부분을 먼저 해결하고, 깔끔한 코드 부분을 해결하는 것이다

아키텍쳐 주도 개발과 정 반대다

뭔가 이상한 것 같은데?

뭔가 기존 코드에 사이드 이펙트가 있는 것 같으니, 빠르게 테스트를 추가해서 확인해보자!!

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

테스트가 실패한다. 알다시피 times 수행때마다 내부 amount가 변경되기 떄문이다
times() 메서드에서 매번 새로운 객체를 반환하게 하면 이 문제를 해결가능할 것 같다

근데 이렇게 변경하려니, 프로덕션 코드와 테스트 코드가 둘 다 변경되어야 한다
뭔가 죄를 저지른듯한 기분이지만, 괜찮다
어떤 구현이 올바른지에 대한 우리 추측이 완벽하지 못한 것과 마찬가지로, 올바른 인터페이스에 대한 추측 역시 절대 완벽하지 못하기 때문이다. 괜찮다.

테스트를 변경하자

1
2
3
4
5
6
7
8
9
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);

Dollar product = five.times(2);
assertThat(product.amount).isEqaulTo(10);
product = five.times(3);
assertThat(product.amount).isEqaulTo(15);
}

그리고 올바르다고 생각되는 코드를 넣고, 테스트를 돌린다
(지금은 운이 좋아 명백히 올바른 코드가 떠올랐지만, 바로 떠오르지 않을떄는 스텁으로 구현한다)

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

이처럼 느낌(부작용에 대한 혐오감)을 테스트로 변환하는 것은 TDD의 일반적인 주제이다
이런 작업을 오래 할수록 느낌(미적 판단)을 테스트로 담아내는 것에 점점 익숙해지게 된다

삼각측량을 이용한 VO 구현

구현해놓고 보니, new Dollar(5)와 new Dollar(5)가 같아야 할 것 같다
테스트를 작성한다

1
2
3
4
@Test
public void testEquality() {
assertThat(new Dollar(5)).isEqualTo(new Dollar(5));
}

빠르게 통과시켜보자

1
2
3
4
@Override
public boolean equals(Object obj) {
return true;
}

알다시피 이는 끔찍한 죄악이다
얼른 수정해야하는데, 어떻게 구현해야할지 잘 모르겠다(사실은 여기는 너무 간단해서 잘 알지만, 뭔가 복잡한 코드라고 생각해보자)
여기서 삼각측량을 이용해서 답을 도출해낼 수 있다

삼각측량은 어떤 한 점의 좌표와 거리를 삼각형의 성질을 이용하여 알아내는 방법이다
간단하게 말해 2개를 통해 나머지 1개를 알아내는 것 이다

테스트에 삼각측량을 도입해본다

1
2
3
4
5
@Test
public void testEquality() {
assertThat(new Dollar(5)).isEqualTo(new Dollar(5));
assertThat(new Dollar(5)).isNotEqualTo(new Dollar(6));
}

amount 5를 가진 Dollar와 amount 6을 가진 Dollar는 같아서는 안된다
amount를 대상으로 비교하면 될 것 같다

1
2
3
4
5
6
7
@Override
public boolean equals(Object obj) {
Dollar dollar = (Dollar) obj;
return amount == dollar.amount;
}
// null이나 다른 객체에 대한 검증도 필요하지만
// 당장은 필요하지 않으므로, 간단히 어디에 메모만 해두고 넘어간다

보다시피 2개의 테스트를 이용해 올바른 프로덕션 코드를 알아냈다

사실상 위의 equals 처럼 시작부터 일반적인 해법이 보일 경우 그냥 그 방법대로 구현하면 된다
이 방법은 설계를 어떻게 할지 떠오르지 않을떄 사용해보면 조금 다른 방향에서 생각해볼 기회를 제공해주는 역할을 한다

테스트 리팩토링

Dollar의 동치성을 구현하고 기존 테스트를 보니, 테스트가 그것을 정확히 얘기해주고 있지 않는것 같다
바꿔주자

1
2
3
4
5
6
7
8
9
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);

Dollar product = five.times(2);
assertThat(product).isEqaulTo(new Dollar(10));
product = five.times(3);
assertThat(product).isEqaulTo(new Dollar(15));
}

불필요한 임시변수(product)를 인라인 시키는 리팩토링을 실시한다

1
2
3
4
5
6
7
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);

assertThat(five.times(2)).isEqaulTo(new Dollar(10));
assertThat(five.times(3)).isEqaulTo(new Dollar(15));
}

이제 amount 변수는 Dollar 에서 밖에 사용하지 않으니 private으로 선언한다

1
private int amount;

위 과정에서 우리는 위험한 상황을 만들었다라는 점을 인지해야 한다
Dollar에 대한 동치성 테스트(testEquality)가 실패하면 곱하기 테스트(testMultiplication) 역시 실패하게 된다는 점이다(테스트간 의존)
이것은 TDD를 하면서 적극적으로 관리해야할 위험 요소이다

근데 뭐 어떻게 하라는지는 딱히 알려주고 있진 않네…

Read more »

[aws] AWS 프리티어(free-tier) 과금

Posted on 2019-04-04 | Edited on 2020-11-02 | In aws | Comments:

어제 밤에 갑자기 AWS에서 29달러 정도가 결제되어서 급하게 확인했더니, 프리티어 기간이 끝나서 과금이 되었었다… 아놔…
그래서 그 내용을 정리하고자 한다

프리티어?

AWS는 가입시 1년간 특정 리소스들을 무료로 사용할 수 있는 프리 티어 권한을 가질 수 있다
프리티어에서 1년간 무료로 사용가능한 리소스들은 아래와 같다
https://aws.amazon.com/ko/free/?awsf.Free Tier Typeasdasds=categories%2312monthsfree&awsm.page-all-free-tier=2

프리티어인지 확인하는 법

AWS 로그인 - 내 결제 대시보드 로 접속한다

  1. 청구서 탭에서 날짜 드롭박스를 보고 언제 가입했는지 알 수 있다. 여기서 1년이 아직 안 지났는지를 체크해보면 된다.
  2. 대시보드 홈에서 경고 및 알람 부분에 free tier 관련 내용이 써져있으면 아직 free tier라는 의미이다
    https://docs.aws.amazon.com/ko_kr/awsaccountbilling/latest/aboutv2/free-tier-eligibility.html

과금을 해결하는법

프리티어 기간이 지났으면(혹은 프리티어라도 일정 수준 넘으면 과금될 수 있음) 과금이 될 리소스를 정리해야함
위의 1년간 무료로 사용할 수 있는 리소스와 아래 정보들을 조합해서 과금 대상인 리소스들을 다 정리해야함
https://docs.aws.amazon.com/ko_kr/awsaccountbilling/latest/aboutv2/checklistforunwantedcharges.html

내 경우

EC2와 RDS에서 과금이 되고 있었음
RDS 들어가서 해당 데이터베이스를 삭제시켰고, EC2에서 인스턴스를 terminated 시켰음

Read more »

[tdd] 객체지향 스타일

Posted on 2019-03-30 | Edited on 2020-11-02 | In tdd | Comments:

우리는 작성하기 쉬운 코드보다는 유지보수하기 쉬운 코드를 높이 평가한다
가장 직접적인 방식으로 기능을 구현하면 시스템의 유지보수성이 떨어질 수 있고, 그러면 코드를 이해하기 어려워지고 컴포넌트간에 보이지 않는 의존성이 생기게 된다
하지만 알다시피 당면한 관심사와 장기적인 관심사간의 균형을 유지하는 일은 까다로운 문제이다

유지 보수성을 고려한 설계

  • 관심사의 분리

    • 관련된 변경사항이 전부 코드의 어느 한군데에 들어있다면, 뭔가 변경할 때 코드의 여러곳을 변경하지 않아도 된다
      • 하나의 클래스가 여러 관심사를 가지고 있다면, 다른 클래스에서도 해당 관심사들을 가지고 있을 확률이 매우 높다
      • 이는 각각 특정 관심사로 분리되어야 하고, 분리된 관심사들이 여러곳에서 재사용 되어야 한다
      • 그러면 특정 변경사항을 적용하기 위해 코드의 여러곳을 수정하는 상황을 막을 수 있다
    • 우리는 변경사항(요구사항)을 예측할 수 없으므로, 같은 관심사들을 하나의 클래스로 모으는 연습을 계속해서 해야한다
    • e.g. 인터넷 표준 프로토콜로 전달된 메시지를 푸는 코드는 그 메시지를 해석하는 코드와 똑같은 이유로 변경되어서는 안되므로, 두 개념을 각기 다른 패키지로 나워야 한다
  • 더 높은 수준의 추상화

    • 흐름을 직접 제어하기보다는 클래스들을 조합하는 식으로 프로그램을 작성하면 더 많은 일을 해낼 수 있다
    • 이는 사람들이 식당에서 음식을 주문할 때 세세한 조리법을 설명하는 것이 아니라 메뉴를 보고 음식을 주문하는 것과 같다
  • ports & adapter pattern
    http://getoutsidedoor.com/2018/09/03/ports-adapters-architecture/
    https://dzone.com/articles/hexagonal-architecture-for-java
    기술적인 부분을 인터페이스로 분리해서 변경을 용이하게 한다 정도로만 이해했고, 실제 예제를 보지 못해서 정확히 이해하지 못하겠다

캡슐화와 정보은닉

캡슐화와 정보은닉은 비슷해보이지만 설계의 품질을 나타낼때는 별개의 개념이다

  • 캡술화

    • https://javacan.tistory.com/entry/EncapsulationExcerprtFromJavaBook
    • 해당 객체의 API를 통해서만 객체의 행위를 좌우할 수 있다
    • 외부에서 객체의 상태를 직접 접근해 변경하거나, 무언가 행위를 하는것이 불가능하다는 의미이다
    • 예상치 못한 의존성이 없음을 보장해줌으로써, 한 객체에 대한 변경이 시스템의 다른 부분에 영향을 덜 주게끔 통제할 수 있다
    • 잘못 캡슐화된 코드를 활용할 경우 해당 코드를 어디서 참조하는지 찾아보고, 잠재적 영향력을 추적하는데 굉장히 많은 시간을 보내게 된다
  • 정보 은닉

    • 해당 객체의 기능을 구현하는 방법을 추상화된 API 너머로 감춘다
    • e.g. 정렬이라는 메서드가 있으면, 내부에서 어떻게 정렬을 수행하는지 외부에서는 알 필요가 없게끔 한다

하지만 대부분의 객체지향 언어에서 제공하는 별칭(aliasing)을 통해 캡슐화를 위반할 수 있다
관련이 없는 두 클래스를 묶거나, 특정 클래스를 wraping한 클래스를 만들고, 그 클래스를 통해 자식클래스에 행위들을 수행해버릴 수 있다

1
2
3
4
5
6
7
class A {
private B b;

public void doManything(B b) { // aliasing
b.doSomething(); // 캡슐화 위반
}
}

A를 통해서 B를 수정할 수 있게 되므로, 이는 캡술화를 위반한 것이다
위와 같이 doManything를 호출 한 후 b의 값이 변경되어 버리는 현상이 일어나서는 안된다

이러한 상황들을 방지하기 위해 아래와 같은 관례를 따라야 한다고 말한다

  • 변경 불가능한 값 타입을 정의하고
    • 값 타입은 내부 값이 수정 불가능한 final이고, 값에 특정한 행위를 수행해도 새로운 객체로 반환하기 때문에 위와 같은 상황에서 안전하다
  • 전역변수와 싱글턴은 자제하며
  • 컬렉션과 변경 가능한 값을 객체간에 전달할때는 그것을 복사해야한다
    • 객체를 복사한다면 위와 같은 상황에 안전할 것이다
    • 저런식으로 협력객체의 특정

이 같은 결정이 중요한 이유는 어떤 객체를 얼마나 쉽게 쓸수 있는지에 영향을 주고, 시스템 내부 품질에 기여하기 때문이다

단일 책임 원칙

  • 모든 객체는 반드시 단 한가지 명확히 규정된 책임을 지녀야 한다
    • 그리고 그러한 객체들이 재사용되어야 한다
  • 한 객체의 역할을 설명할때는 접속사(와,나 등)를 사용하지 않고도 해당 객체의 역할을 설명할 수 있어야 한다
    • 접속사로 구분되는 애들이 각각 객체로 나뉠 수 있을것이다?

객체 이웃의 유형

객체가 단일 책임을 지녔고, 명료한 API를 통해 이웃 객체와 통신한다면, 서로 무슨 얘끼를 할까?
한 객체가 지닐 수 있는 관계는 다음과 같다

Read more »

[tdd] TDD 주기의 유지

Posted on 2019-03-24 | Edited on 2020-11-02 | In tdd | Comments:

TDD 프로세스를 시작하고 나면, 그 프로세스를 매끄럽게 유지해야 한다
그 상세한 방법
시스템을 구축할 때 테스트를 작성하는 방법
테스트를 이용해 내외적인 품질 문제에 일찍 피드백을 받는 방법
테스트가 계속 변화를 뒷받침하고, 이후 개발에 걸림돌이 되지 않게 하는 방법

각 기능을 인수 테스트로 시작하라

인수 테스트는 우리가 작성하려는 기능이 아직 시스템에서 갖추지 못했다는 사실을 보여주고, 그 기능이 완성되기까지 진행 상황을 반영한다
인수테스트를 작성할때는 기반 기술(데이터베이스나 웹 서버 같은)의 용어가 아닌 응용 도메인에서 나온 용어만 이용한다.

이렇게하면 시스템에서 해야 할 일이 뭔지 이해하는데 보탬이 되고, 구현에 관한 초기 가정에 얽매이지도 않을 뿐더러 테스트가 기술적인 세부사항으로 복잡해지지도 않는다
이뿐 아니라 시스템의 기술 기반 구조가 바뀌었을 때도 인수 테스트를 보호할 수 있다

코딩을 시작하기 전에 테스트를 작성하면 달성하고자 하는 바가 명확해진다
실패하는 테스트덕에 요구사항을 충족하는데 필요한만큼의 기능을 구현하는데 집중할 수 있어 기능을 완성할 확률이 높아진다
테스트로 시작하게 되면 사용자 관점에서 시스템을 바라보게 되어 구현자 관점에서 기능을 짐작하지 않고 사용자가 필요로 하는 것을 이해하게 된다

반면 단위 테스트는 객체 집합을 격리된 상태에서 시험하므로, 그 클래스가 시스템의 나머지 부분과 조화롭게 동작할지에 대해서는 아무것도 담보하지 않는다
인수테스트는 단위 테스트를 거친 객체를 대상으로 통합 테스트를 수행할 뿐 아니라, 프로젝트를 앞으로 나아가게 한다

회귀를 포작하는 테스트와 진행상황을 측정하는 테스트를 분리하라

새 인수 테스트는 진행중인 작업을 나타내고, 기능이 준비될 때 까지는 통과하지 않을 것이다
인수 테스트를 통과하면 해당 테스트는 완료된 기능을 나타내고, 다시는 실패해서는 안된다
(실패는 이전 상태로 회귀했고, 기존 코드를 망가뜨렸음을 의미한다)

인수 테스트를 빨간색에서 녹색으로 바꾸는 활동으로 우리는 진행상태를 측정할 수 있다
정기적인 인수테스트 통과 주기는 중첩된 피드백 고리를 구동하는 엔진에 해당한다

즉 우리는 진행중인 테스트와 완료된 테스트(회귀 테스트)를 항상 분리해야 한다
만약 요구사항이 바뀐다면 거기에 영향을 받은 인수테스트를 회귀 테스트 그룹에서 빼내어서 진행중인 테스트 그룹으로 옮긴 후
새 요구사항을 반영토록 수정한 다음, 해당 테스트를 다시 통과하게끔 시스템을 변경해야 한다

테스트를 가장 간단한 성공 케이스로 시작하라

간단한을 지나치게 단순한으로 해석해서는 안된다
지나치게 단순한 케이스는 시스템에 가치를 별반 더하지 않으며, 더 중요한 점은 아이디어의 유효성에 관해 충분한 피드백을 전해주지 않는다는 것이다

그래서 가장 간단한 성공 케이스로 테스트를 시작해보는 것이 좋다
해당 테스트가 동작하면 솔루션의 실제 구조에 관해 더 좋은 생각이 떠오를테고, 그 과정에서 발견한 발생 가능한 실패를 처리하는 것과 이후의 성공 케이스 사이에서 우선순위를 가늠해볼 수 있다.

처리해야 할 실패 케이스, 리팩터링, 기타 작업들을 메모장 깉은 곳에 기록해두면 테스트 작성에 도움이 많이 된다

기능 구현을 할 때 실패 케이스에만 집중하면 의욕을 짐작하는데 좋지않다.
오류처리메나 신경쓰다 보면 아무것도 성취한 바가 없는 듯이 느껴지기 때문이다

읽고 싶어 할 테스트를 작성하라

각 테스트는 시스템이나 객체에서 수행할 행위로 가능한 한 명확하게 표현하는 것이 좋다

  1. 테스트를 작성하는 동안에는 테스트가 실행되지 않거나 컴파일 되지 않는다는 사실을 무시하고 테스트 내용에만 집중해서 작성하는 것이 좋다
  2. 테스트가 잘 읽히면, 이제 컴파일이나 런타임 오류를 해결한다
  3. 테스트가 명확한 오류메세지를 보이면서 예상대로 실패하면, 기반이 되는 코드를 충분히 구현했음을 의미한다
  4. 이제 테스트가 통과되도록 만들면 된다

테스트가 실패하는 것을 지켜보라

Read more »

[tdd] TDD 도구들

Posted on 2019-03-23 | Edited on 2020-11-02 | In tdd | Comments:

JUnit

가장 많이 쓰이는 java 테스트 프레임워크이다.
그 중에서도 JUnit4 버전이 가장 많이 사용된다.(현재는 JUnit5 버전까지 나와있다)

JUnit은 기본적으로 리플렉션을 통해 클래스 구조를 파악한 후, 해당 클래스에서 테스트를 나타내는 것을 모두 실행한다.

테스트 케이스

JUnit은 @Test라는 어노테이션이 지정된 메서드는 모두 테스트 케이스로 취급한다.
테스트 메서드는 값을 반환하거나 매개변수를 받아서는 안 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SomeTest {
private Some some = new Some();

@Test
public void testSomething1() {
// do something
}

@Test
public void testSomething2() {
// do something
}
}

하나 특별한점은, JUnit은 각 테스트를 실행할 때 마다 해당 테스트 클래스의 새 인스턴스를 생성하여 호출한다는 점이다.

위의 테스트들을 전부 실행하면 2개의 SomeTest 인스턴스가 생성되고, 각자 @Test 메서드들을 실행하게 된다.

이런식으로 매번 새 인스턴스를 생성하면 각 테스트간의 격리성을 확보할 수 있다. 테스트 객체의 필드가 각 테스트에 앞서 대체되기 때문이다.
이는 테스트에서 테스트 객체 필드의 내용을 맘껏 바꿀 수 있다는 의미이다.

testSomething1에서 some의 값을 변경해도 testSomething2에는 영향을 주지 않는다

assertion

기본적으로 테스트들은, 각 테스트를 수행하고 그 결과를 assertion(단정)하는 식으로 작성된다.
여기서 JUnit에서 제공하는 assertion 메서드들을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StoreTest {
private Store store = new Store();

@Test
public class 결제수단을_체크한다() {
assertTrue(store.canPay(PaymentMethod.Money));
assertFalse(store.canPay(PaymeneMethod.CreditCard));
}

@Test
public class 특정_상품을_찾는다() {
assertNotNull(store.find("치킨"));
assertNull(store.find("가죽자켓"));
}
}

보다시피 canPay의 결과가 true/false가 나올것이다 라고 단정(assertion) 했고,
find의 결과가 null이 아니라고 단정(assertion) 했다.

예외 예상하기

@Test 어노테이션은 선택적 매개변수로 expected 라는 것을 지원한다.
이 매개변수는 테스트 케이스에서 던져질 예외를 선언한다. 선언한 예외가 발생하면 테스트가 성공한다.

1
2
3
4
@Test(expected = IllegalArgumentException.class)
public void 검색어를_입력하지_않는다() {
store.find(null);
}

find 메서드는 검색어로 null을 받으면 IllegalArgument 예외를 리턴하게 작성되어 있다.
그러므로 위의 메서드는 성공하게 된다.

테스트 픽스쳐

테스트가 시작할 때 존재하는 고정된 상태를 의미한다.
테스트를 수행하기 전에 필요한 특정 상태들을 의미한다. 특정 협력객체나, 특정 데이터들이 있을 수 있다.

JUnit은 각 테스트마다 인트턴스를 새로 생성하므로
간단히 인스턴스 변수 선언과 동시에 초기화하거나 생성자 같은것을 써서 픽스쳐를 초기화하면 되긴하지만,
JUnit에서 제공하는 특정 어노테이션들을 사용하면 좀 더 명시적으로 픽스쳐 초기화가 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
private Store store = new Store();

@Before
public void setUp() {
store.addProduct("chicken");
store.addProduct("pizza");
store.addProduct("beer");
}

@After
public void tearDown() {
// 별로 할게 없음..
}

@Before 메서드가 모든 테스트 실행전에 실행되므로, 모든 테스트는 3가지 상품이 들어간 상태에서(동일한 상태에서) 테스트를 수행할 수 있게 된다.
이런식으로 모든 테스트 메서드에서 필요한 상태를 초기화하는데 사용하면 유용하다.
@After 메서드는 테스트 메서드가 끝난 후 수행되는데, 사실상 여기서 수행할 작업이 많지는 않다.
생성된 픽스쳐를 정리하는 작업 같은것도 전부 JVM 가비지 컬렉터에서 잘 수행해주기 때문이다.

테스트 러너

JUnit이 클래스를 대상으로 리플렉션을 수행해 테스트를 찾아 해당 테스트를 실행하는 방식은 테스트 러너(test runner) 에서 제어한다.
테스트 클래스에서 사용하는 러너는 @RunWith 어노테이션으로 설정할 수 있다.

1
2
3
4
@RunWith(SpringJUnit4ClassRunner.class)
public class SomeTest {

}

JUnit4의 현재 default runner는 BlockJUnit4ClassRunner 라고 한다.

햄크레스트 매처와 assertThat

햄크레스트는 매칭 조건을 선언적으로 작성하는 프레임워크이다.
matches라는 boolean을 반환하는 메서드를 가진 Matcher 인터페이스를 구현하는 많은 클래스들을 제공한다.
이 클래스들을 사용하면 기존의 단순한 assertion 구문들을 좀 더 다양하게 사용 가능하다.

1
2
3
Matcher<String> containsBananas = new StringContains("bananas");

assertTrue(containsBananas.matches(str));

햄크레스트는 코드의 가독성을 높이고자 Matcher를 생성하는 부분을 static factory 메서드로 제공한다.

1
2
3
4
5
6
import static org.hamcrest.CoreMatchers.*;

@Test
public void banana_match() {
assertTrue(containsString("bananas").matches(str));
}

하지만 실제로는 위와 같은 방법보단, self-describing 특성을 가진 assertThat을 주로 사용한다.

1
2
assertThat(str, containsString("bananas"));
assertThat(str, not(containsString("bananas")));

assertThat은 Matcher를 직접 인자로 받을 수 있다.
보다시피 str은 "bananas" 문자열을 포함하고 있다 의 형태로 작성됨으로써 테스트 코드가 더 잘 읽힌다.
(참고로 햄크레스트는 위에서 사용된 not Matcher 처럼 다른 매처를 조합할 수 있는 유용한 기능을 제공한다)

이러한 Matcher는 몇가지 조건만 만족하면 사용자가 쉽게 직접 정의해서 사용할 수 있다.

AssertJ

assertThat과 햄크레스트를 적절히 조합하여 테스트를 작성하는 것만해도 충분하지만, 좀 더 풍부한 구문을 제공하는 AssertJ 라는 라이브러리도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import static org.assertj.core.api.Assertions.assertThat;

public void 오브젝트() {
assertThat(object).isNotNull();

assertThat(object).isSameAs(otherObject);
assertThat(object).isEqualTo(otherObject);
}

public void 컬렉션() {
assertThat(list).isSorted();
assertThat(list).hasSize(4);
}

보다시피 좀 더 직관적이고, 풍부한 메서드들을 제공한다.
게다가 assertion과 햄크레스트를 직접 static import 하지 않아도 되는 편리함도 있으니, 사용해보는 것도 나쁘지 않다.

JMock2

JMock을 사용하면 mock 객체를 JUnit 같은 테스트 프레임워크에 붙여서 사용할 수 있도록 해준다.
JMock의 핵심 개념은 모조 객체와 목 객체, 예상 구문이다.
아래는 JMock을 이용한 행위 검증의 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(JMock.class)
public class AuctionMessageTranslatorTest {
private final Mockery context = new JUnit4Mockery(); // 1
private final AuctionEventListener listener = context.mock(AuctionEventListener.class);
private final AuctionMessageTranslator translator = new AuctionMessageTranslator(listener);

@Test public void notifiesAuctionCloseWhenCloseMessageReceived() {
Message message = new Message();
message.setBody("SOLVersion : 1.1; Event: Close;");

context.checking(new Expectations() {{
oneOf(listener).auctionClosed();
}});

translator.processMessage(UNUSED_CHAT, message);
}
}

여기서 모조 객체라는 개념이 나오는데, 현재 이해하기로는 목 객체들을 담는 그릇 정도로 이해된다… JMock runner가 읽는 대상들?

  1. JUnit이 JMock 테스트 러너를 사용하게 된다. 이 러너는 테스트가 끝나는 시점에 모든 모조 객체를 자동으로 호출해 모든 목 객체가 예상대로 호출되었는지 검사한다.
  2. 모조 객체를 생성한다. JMock 러너가 검사할 목 객체들이 담긴다.
  3. AuctionEventListener의 mock 객체를 생성한다. 자동으로 context에 담긴다.
  4. SUT에 mock 객체를 주입한다. SUT는 그 사실을 모르고, 알 필요도 없다.
  5. 모조객체에서 검사할 내용(행위 검증)을 작성한다. listener 목객체가 auctionClosed 메서드를 정확히 1번 호출할 것을 예상하고 있다.
  6. SUT가 행위를 수행한다.
  7. 테스트가 끝나면 JMock runner는 모조객체(현재는 context)에 명시된 행위들을 검증한다

개인적 느낌인데, 작성하기가 조금 어려운 부분이 있는 것 같다.

mockito

JMock보다 좀 더 강력한 기능을 제공하는 mockito 라는 라이브러리가 있다.
아래는 사용법에 대해 작성된 글이다.

https://github.com/mockito/mockito/wiki/Mockito-features-in-Korean

간단히 내가 느끼는 mockito의 장점은 아래 정도이다.

  • mockito는 JMock 처럼 모조객체, 목 객체에 대한 구분이 없다.
  • JMock에도 있는지 모르겠으나(당연히 있곘지…) stub을 매우 간단하게 지원한다.
  • 행위 검증도 매우 간단하게 지원한다.

나는 개인적으로 이 라이브러리가 좀 더 작성하기 편리하고, 명시적인 것 같다.

참고로 mockito 보다 더 강력한 기능을 제공하는 PowerMock 이라는 애도 있다.(private 메서드 테스트, static 메서드 주입 등 까지 제공한다)

Read more »

[tdd] TDD 주기의 시작

Posted on 2019-03-22 | Edited on 2020-11-02 | In tdd | Comments:

되돌아보면 우리는… 개발을 다 진행해놓고 마지막에 통합하면서 피드백을 받았던 것 같다.
이 마지막 순간에 변경되는 부분도 굉장히 많고, 서로가 잘못 이해한 부분도 많이 발생한다.
최악의 경우는 작업을 뒤집거나, 누군가가 포기하고 가야하는 상황도 발생한다.

그러므로 앞서 얘기했듯이, 빠른 피드백을 받을 수 있게끔 시스템을 구성하는 작업(중첩된 피드백 고리)은 매우 중요하다.

  • 위에서도 언급했듯이, 이런 시스템의 구성을 나중으로 미루는 것은 매우 위험하다
  • 저 정도 예시면 양반이지, 배포할 수 없어서 프로젝트가 취소되거나 마지막에 테스트를 추가했는데도 불구하고 오류율이 너무 높아 시스템이 폐기되기도 한다(저자의 경험)
  • 피드백은 아주 근본적인 도구이며, 올바른 방향으로 나아가고 있는지 최대한 미리 파악하고자 한다

그 중에서도 외부와의 피드백 고리를 형성하는 것은 중첩된 피드백 고리를 구성하는 첫번쨰 단추이고, 가장 중요하다.
이 말인 즉 자동화된 빌드・배포・테스트 주기 전체를 가장 처음부터 구현해야 함을 의미한다.
근데 사실 말이 쉽지, 이렇게 구성하기에는 할일도 매우 많고, 어렵다.
그러면 우리는 무엇부터 시작해야 할까?

우선 동작하는 골격을 대상으로 테스트하라

  • 먼저 동작하는 골격을 만들어야 한다
    • 전 구간을 대상으로 자동 빌드・배포・테스트를 할 수 있는 실제 기능을 가장 얇게 구현한 조각을 말한다
    • 첫 기능을 구현할 수 있을 정도의 자동화, 주요 컴포넌트, 통신 메커니즘이 포함될 것이다
  • 이 구조를 이용해 유의미한 첫 기능에 대한 인수 테스트를 작성한다
    • 이후로는 시스템의 나머지 부분을 대상으로 테스트 주도 개발을 진행할 수 있게 모든 것이 제자리에 놓일 것이다

동작하는 골격을 만드는 과정에 배포 단계를 포함한 이유는 아래와 같다

  • 배포 단계는 오류가 발생하기 쉬운 활동이므로 실제 환경에 배포해야 할 때 까지 스크립트를 철저히 검증해야 하기 때문이다
  • 배포 과정을 자동화 하는 것 만큼 프로세스를 이해하는데 도움이 되는 것은 없다
  • 배포 단계에서 개발팀이 조직의 다른 부문과 접촉하기도 하며, 실제로 어떻게 운영되는지도 배워야하기 때문이다

참고로 동작하는 골격을 만들때는 골격의 구조에만 집중하고, 테스트가 최대한 표현력을 갖추게끔 테스트를 정리하는 일에 대해서는 크게 신경쓰지 않는다
동작하는 골격과 그것을 보조하는 기반 구조는 테스트 주도 개발을 시작하는 방법을 도와주기 위해 존재하는 것이기 때문이다

동작하는 골격의 외형 결정

어떤 전체 구조에 관한 구상 없이는 빌드・배포・테스트 주기를 자동화 할 수 없으므로,
계획한 첫 출시를 달성하는데 필요할 주요 시스템 컴포넌트와 그러한 컴포넌트의 상호 작용 방식에 대한 대략적인 그림은 필요하다.

이는 화이트보드에 몇분만에 그릴 수 있다
공개된 장소에 이런 구조를 그려놓으면 코드 작성 시 팀이 업무 방향을 참고하는데 도움이 된다(마파 문디)

당연하지만, 이 같은 초기 구조를 설계하려면 시스템의 목적을 어느 정도 이해해야 한다
클라이언트의 요구사항(기능적 요구사항, 비기능적 요구사항)을 고수준 관점에서 바라보고 의사결정에 참고해야 한다
그렇지 않으면 위험을 무릅쓰고 하는 일들이 죄다 의미가 없어진다

테스트 주도 주기

참고로 이를 과도한 사전 설계와 혼동하지 않아야 한다
실제 피드백을 토대로 배우고 개선해나가는 과정을 시작할 수 있게(+TDD 주기를 시작할 수 있게)하는데 필요한 최소한의 의사결정만을 내리는 것이 좋다
(현재 생각하고 있는 바가 틀릴 가능성이 있으므로, 시스템이 성장해가면서 세부 사항을 파악해나간다)

피드백 소스 구축

아래의 글로 피드백의 중요성을 재고하고 있다

어플리케이션 설계에 대해 내린 의사결정이나, 어플리케이션 설계가 근거하는 가정의 옳고 그름에 대해서는 아무것도 보장할 수 없다
그저 최선을 다할 뿐이며 현재 밟고 있는 절차에 피드백을 적용해 최대한 빨리 의사결정이나 가정을 검증하는데 의지할 뿐이다

아래는 위의 테스트 주도 주기가 주는 피드백에 대해 그려놓은 그림이다

요구사항 피드백

보다시피 각 과정은 서로에게 피드백이 된다
이러한 시스템을 구축함으로써 우리의 기능이 요구사항에 얼마나 부합하는지, 우리 시스템 구현은 어떤지 빠르게 평가할 수 있고, 빠르게 반영할 수 있다.
최종적으로 우리는 안전해지고, 완전한 소프트웨어를 얻게 될 것이다!!!

또 하나의 가장 큰 헤택은 뭘 배우늗 거기에 맞춰 시스템을 변경할 수 있으리라는 점이다
뭐든 테스트를 먼저 작성한다는 것은 철저한 회귀 테스트 모음을 갖게 된다는 것을 의미하기 때문이다
물론 어떤 테스트도 완벽하지 않지만, 견고한 테스트 모음이 있으면 중대한 변경을 안전하게 할 수 있다는 사실은 확실하다


처음에는 이러한 순서대로 점진적인 개발을 하는 과정이 불안정하고, 활동량도 굉장히 많을 것이지만 자동화가 구축되고 나면 반복적인 과정으로 안정화된다

나중에 통합 절차를 수행하는 프로젝트는 침착한 분위기에서 시작하지만, 처음으로 시스템 통합을 시도하게 되는 후반부에 힘들어지곤 한다
통합을 나중에 하는 방식의 경우 어떤 일이 일어날지 예측하기 불가능한데, 팀에서 엄청나게 많은 각 부분을 제한된 시간에 짜맞춰야 하고 실패한 부분을 고치는 일도 감안해야 하기 때문이다
그래서 경험이 풍부한 이해관계자는 프로젝트 초반부에 생기는 불안정성에 고약하게 반응한다
후반후에 들어가면 상황이 더 악화되리라 예상하기 때문이다
비단 통합 뿐만이 아니다. 우리는 각종 테스트들을 먼저 작성함으로써 혼란을 미리 잡고가게 되는 효과를 가지게 된다.

테스트 먼저 vs 테스트 나중에

가장 중요한 것은 방향 감각을 확보하는 것과 가정을 테스트할 구체적인 구현을 갖춰야 한다는 것이다
동작하는 골격은 프로젝트 초기에 각종 쟁점을 드러내지만, 프로젝트 초기라면 아직까지 그러한 쟁점을 해결할 시간과 예산, 의지가 있을 때이다

Read more »

[git] commit message convention

Posted on 2019-03-19 | Edited on 2020-11-02 | In git | Comments:

커밋 메세지 컨벤션
https://meetup.toast.com/posts/106
https://doublesprogramming.tistory.com/256

  • feat : 새로운 기능 추가
  • fix : 버그 수정
  • docs : 문서 수정
  • style : 코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
  • refactor : 코드 리펙토링
  • test : 테스트 코드, 리펙토링 테스트 코드 추가
  • chore : 빌드 업무 수정, 패키지 매니저 수정
Read more »

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

Posted on 2019-03-17 | Edited on 2020-11-02 | In tdd | Comments:

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

객체망

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

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

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

객체망

이런식으로 시스템을 구축하면 방법(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. 예상되는 메서드 호출이 모두 일어났는지 확인

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

Read more »

[tdd] 인수테스트, 단위테스트, 통합테스트, 전 구간 테스트

Posted on 2019-03-17 | Edited on 2020-11-02 | In tdd | Comments:

테스트 주도 개발로 배우는 객체지향 설계와 실천 이라는 책과 Acceptance Test vs Integration Test 블로그의 글을 합쳐서 작성한 각 테스트들의 종류와 특징이다.

유닛 테스트(Unit Test)

  • 가장 작은 단위의 테스트이다
  • 보통 메서드 레벨이다
  • A라는 함수가 실행되면 B라는 결과가 나온다 정도로 테스트한다
  • 즉각적인 피드백이 나온다는 것이 훌륭한 장점이다
  • 꼭 메모리 내에서만 실행되는 테스트여야 한다는 법칙은 없다
    • 데이터베이스, 네트워크 엑세스, 파일 시스템 등을 사용하여도 단위테스트의 레벨일 수 있다
  • 테스트하기 어려운 부분은 stub을 사용하여 테스트한다
    • 비용이 크지 않다면 stub보다는 실제 객체를 사용하는 것이 좋다
    • 아무래도 정교한 목 객체가 실제 객체보다 정확하지는 않기 때문이다
    • 모든 것은 비용 관점에서 생각해야 한다
  • 하나의 메서드들이 잘 동작한다는 것은 보장할 수 있지만, 그들이 결합되었을때도 잘 작동한다는 것은 보장할 수 없다

전 구간 테스트(End-To-End Test)

  • 해당 시스템과 해당 시스템을 구축하고 배포하는 프로세스를 모두 시험하는 것을 말한다
    • 용어를 사용하는 곳마다 조금씩 차이가 있다
  • 내부 기능들까지(클래스의 메서드들까지) 테스트 할 필요는 없다
    • 이는 단위테스트의 영역이다
  • 단점은 테스트를 만들기가 힘들고, 만든 테스트를 신뢰하기도 힘들다는 것이다

통합 테스트(Integration Test)

  • 기본적으로 여러개를 통합해서 테스트 할때 사용하는데, 뭔가 정확한 용어가 정의된 느낌은 아니다
  • 책에서는 변경할 수 없는 부분(외부 라이브러리 등)까지 묶어서 같이 테스트 할 떄 사용한다고 말하고 있다
  • 사용하는 곳에서 어떻게 사용하는 가에 따라 다른 것 같다
  • 그래서 난 사용하지 않을 것이다

인수 테스트(Acceptance Test)

  • 단위 테스트, 전 구간 테스트, 통합 테스트와는 약간 scope 가 다르다
    • 위의 3가지는 초점이 기술 쪽이라면, 인수 테스트는 초점이 비즈니스 쪽이다
  • 구현하고자 하는 기능(비즈니스 레벨)에 대한 테스트이다
  • 대체적으로 전 구간 테스트를 사용하여 기능을 테스트한다
    • 그래서 이를 동일하게 얘기하는 곳도 많다
  • 하지만 결과적으론, 위 3가지 모두 인수테스트의 범위에 들어갈 수 있다
    • 보험금액을 계산하는 어플리케이션에서는 보험금액을 계산하는 메서드 단위 테스트가 인수테스트가 될 수 있다
Read more »
1…456…19

JunYoung Park

182 posts
18 categories
344 tags
RSS
© 2020 JunYoung Park
Powered by Hexo v3.6.0
|
Theme – NexT.Muse v7.1.0