기록은 기억의 연장선

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


  • Home

  • Tags

  • Categories

  • Archives

  • Search

millisecond, microsecond, nanosecond

Posted on 2019-05-02 | Edited on 2020-11-02 | In etc | Comments:

1 second == 1,000 millisecond
1 millisecond == 1,000 microsecond
1 microsecond == 1,000 nanosecond

1 second == 1,000,000,000 nanosecond

Read more »

알고리즘 전환

Posted on 2019-05-02 | Edited on 2020-11-02 | In refactoring | Comments:

알고리즘을 더 분명한 것으로 교체해야 할 땐
해당 메서드의 내용을 새 알고리즘으로 바꾼다

특징

  • 어떤 기능을 수행하기 위해 비교적 간단한 방법이 있다면, 복잡한 방법을 더 간단한 방법으로 교체해야한다
  • 기본적으로 메서드를 잘게 쪼개놔야 가능하다
    • 길고 복잡한 알고리즘은 수정하기 어렵기 때문이다

방법

  1. 테스트가 꼭 필요하다
  2. 기존 알고리즘을 간결한 알고리즘으로 바꾸면서 계속해서 테스트를 실행한다
  3. 모든 테스트 결과가 같으면 성공이다
    • 다르다면 디버깅을 실시해 비교해본다
    • 기존 알고리즘과 새로운 알고리즘의 출력값을 비교하면서 진행하는 것도 좋은 방법이다

참고 : 마틴 파울러, 『리팩토링』, 김지원 옮김, 한빛미디어(2012)

Read more »

템플릿 메서드 형성

Posted on 2019-05-02 | Edited on 2020-11-02 | In refactoring | Comments:

하위클래스 안의 두 메서드가 거의 비슷한 단계들을 같은 순서로 수행할 땐
그 단계들을 시그니처가 같은 두 개의 메서드로 만들어서 두 원본 메서드를 같게 만든 후,
두 메서드를 상위클래스로 옮긴다

동기

  • 하위 클래스에 있는 두 메서드가 비슷하다면 둘을 합쳐서 하나의 상위 클래스로 만드는것이 좋다
  • 두 메서드가 완전히 똑같지 않다면, 중복된 부분은 가능한 한 전부 없애고 차이가 있는 필수 부분만 그대로 둬야한다

방법

  1. 비슷해보이는 같은 클래스에 있다면, 두 메서드가 어떤 공통 상위클래스의 하위클래스가 되게 정리해야 한다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Printer {
    public String textPrint() {
    // ...
    }

    public String imagePrint() {
    // ...
    }
    }

    를 아래의 구조로 변경한다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Printer {
    public String textPrint() {
    return new TextPrinter().print();
    }

    public String imagePrint() {
    return new ImagePrinter().print();
    }
    }
    class TextPrinter extends Printer {
    public String print() {
    // ...
    }
    }
    class ImagePrinter extends Printer {
    public String print() {
    // ...
    }
    }
    • 리팩토링은 작게작게 진행해야하므로, 하위클래스에 위임하는 방식으로 구현했다
    • 메서드상향이 용이하도록 두 하위클래스의 메서드명을 같게했다
  2. 두 메서드를 잘게 분리하는데, 공통된 부분과 공통되지 않은 부분이 나오게끔 분리한다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class TextPrinter extends Printer {
    public String print() {
    // ...
    printBody();
    // ...
    }
    public String printBody() {
    // 다른 부분
    }
    }
    class ImagePrinter extends Printer {
    public String print() {
    // ...
    printBody();
    // ...
    }
    public String printBody() {
    // 다른 부분
    }
    }

    이 기법의 핵심이다

  3. 하나의 클래스에 대해 메서드 상향을 실시하자

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    abstract class Printer {
    public String print() {
    // ...
    printBody();
    // ...
    }
    public abstract String printBody(); // 달랐던 부분
    }
    class TextPrinter extends Printer {
    public String printBody() {
    // 다른 부분
    }
    }
    • 달랐던 부분은 abstract로 선언하고, 하위클래스에서 구현하도록 한다
    • 위임하던 코드는 삭제한다
  4. 나머지 하나의 클래스에도 동일하게 적용한다

참고 : 마틴 파울러, 『리팩토링』, 김지원 옮김, 한빛미디어(2012)

Read more »

[git] git pull request checkout

Posted on 2019-05-01 | Edited on 2020-11-02 | In git | Comments:

직접 checkout

git config 추가

1
$ git config --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/origin/pr/*"

fetch로 가져올 때 origin 말고 pr도 가져올 수 있게함

1
2
$ git fetch origin
$ git checkout origin/pr/[PR number]

intellij 에서 checkout

https://blog.jetbrains.com/idea/2018/10/intellij-idea-2018-3-eap-github-pull-requests-and-more/
shift 2번 눌러서 search everywhere 실행시킨 뒤 view pull request 클릭
생성된 pr에서 보고 싶은 pr을 선택하여
local branch 만들고 checkout

Read more »

[git] travis ci로 github blog 자동 배포

Posted on 2019-05-01 | Edited on 2020-11-02 | In git | Comments:
  1. github app에서 travis CI 설치
  2. github setting - developer setting에서 personal access token 발급
  3. .travis_ci, _config.yml 설정
  4. push하고 travis에서 빌드 잘 되는지 확인
  • theme

    테마는 submodule로 저장해줘야하고, 테마에서 _config.yml을 수정할 것이므로 theme fork한것을 submodule에 넣어야함

  • travis ci app 설치 및 진행 방식

    https://okky.kr/article/515352

  • .travis.yml, _config.yml은 여기를 참조하면 됨

    https://medium.com/@changjoopark/travis-ci를-이용한-github-pages-hexo-블로그-자동-배포하기-6a222a2013e6

Read more »

[git] git submodule

Posted on 2019-05-01 | Edited on 2020-11-02 | In git | Comments:

https://www.git-scm.com/book/ko/v1/Git-도구-서브모듈

Read more »

[ddd] 도메인 모델

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

도메인이란?

  • 온라인 서점 시스템을 구현한다고 할 때, 소프트웨어로 해결하고자 하는 문제의 영역인 온라인 서점이 도메인이 된다
  • 한 도메인은 다시 여러개의 하위 도메인으로 나뉠 수 있다
    • 온라인 서점의 하위 도메인 : 상품, 회원, 주문, 정산, 배송 등등
  • 모든 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다. 상황에 따라 달라진다
    • 대상이 기업인지, 사용자인지 등등
  • 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다
    • 외부 업체의 배송 시스템이나, 결제 시스템 같은것들을 이용한다

도메인 모델

  • 특정 도메인을 개념적으로 표현한 것이다
  • 이를 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고, 도메인 지식을 공유하는데 도움이 된다
    • 도메인을 이해하려면 도메인이 제공하는 기능과 주요 데이터 구성을 파악해야 한다
    • 보통은 기능과 데이터를 함꼐 보여주는 클래스 다이어그램이 적합하다
    • 꼭 UML만 사용해야 하는 것은 아니다. 도메인을 이해하는데 도움이 된다면 표현방식이 무엇인지는 중요하지 않다
  • 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안된다
    • 각 하위 도메인마다 별도로 모델을 만들어야 한다
    • 모델의 각 구성요소는 특정 도메인을 한정할 때 비로소 의미가 완전해지기 때문이다

      카탈로그의 상품과 배송의 상품은 다르다

도메인 모델 패턴(★★★)

일반적인 어플리케이션의 아키텍쳐는 아래의 4계층으로 구성된다

  • Presentation(표현)

    사용자의 요청을 처리하고 사용자에게 정보를 보여준다
    외부 시스템도 사용자가 될 수 있다

  • Application(응용)

    사용자가 요청한 기능을 실행한다
    업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다

  • Domain(도메인)

    시스템이 제공할 도메인의 규칙을 구현한다

  • Intrastructure(인프라스트럭쳐)

    외부 시스템과의 연동을 처리한다(DB, Messaging 등)

여기서 도메인 영역에 해당하는,
도메인의 규칙을 객체지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다

e.g. 주문 상태 변경에 대한 규칙을 처리하는 Order class

1
2
3
4
5
6
7
8
9
10
11
class Order {
// ...

public void changeShippingInfo(ShippingInfo newShippingInfo) {
if(!isShippingChangeable()) {
throw new IllegalStateException();
}

// ...
}
}

주문 상태 변경 로직을 도메인에서 직접 처리하고 있다
이처럼 핵심 규칙을 구현한 코드가 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다

모델 작성시 주의사항

개념 모델을 만들때 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 시도를 할 수 있지만 실제로 이는 불가능에 가깝다

소프트웨어를 개발하는 동안 도메인을 점점 더 이해하게 되기 때문에, 시간이 지나감에 따라 모델을 수정하는 경우가 많기 때문이다
(지식이 쌓이면서 완전히 다른 의미로 해석되어지는 상황도 존재한다)

그러므로 처음에는 개요 수준의 개념 모델을 작성하여 도메인에 대한 전체 윤곽을 이해하는데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야한다

도메인 모델 도출

아무리 천재 개발자라도 도메인에 대한 이해없이 코딩을 시작할수는 없다
기획서, 유스 케이스, 유저 스토리 같은 요구사항과 관련자와의 대화를 통해 도메인을 이해하고, 이를 바탕으로 도메인 모델 초안을 만들어야 코드를 작성할 수 있다

도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.

이는 모두 요구사항을 분석하면서 찾아낼 수 있다

  • 요구사항을 통해 제공해야 하는 기능을 도출해낼 수 있다
    • 출고 상태로 변경하기, 배송지 정보 변경하기 등
  • 요구사항을 통해 특정 항목이 어떤 데이터로 구성되어야 하는지 알 수 있다
    • e.g. 한 상품을 한개 이상 주문할 수 있다
  • 요구사항을 통해 각 항목간의 관계를 알 수 있다
    • e.g. 최소 한 종류 이상의 상품을 주문해야 한다
  • 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우도 많다
    • e.g. 출고를 하면 배송지 정보를 변경할 수 없다
  • 요구사항을 이해하다보면 설계가 바뀌는 경우가 종종 생긴다

문서화
코드는 상세한 모든 내용을 다루고 있기 때문에 코드를 이용해서 전체 소프트웨어를 분석하려면 많은 시간을 투자해야한다
전반적인 기능 목록이나 모듈 구조는 상위 수준에서 정리된 문서를 참조하는 것이 소프트웨어 전반을 빠르게 이해하는데 도움이 된다

코드 자체도 문서화의 대상이 될 수 있다
도메인 지식이 잘 묻어나고, 도메인을 잘 표현하도록 코드를 작성하면 코드의 가독성이 높아지면서 코드가 문서로서의 의미도 가지게 된다

엔티티와 벨류

모델은 크게 엔티티와 벨류로 구분할 수 있다
이 둘의 차이를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있으므로 차이를 명확하게 이해해야 한다

엔티티 타입

  • 가장 큰 특징은 식별자를 갖는다는 것이다
    • 식별자는 엔티티마다 고유해서 각 엔티티는 서로 다른 식별자를 가진다
    • 엔티티를 생성하고 엔티티의 속성을 바꾸고 엔티티를 삭제할 때 까지 식별자는 유지된다
  • 두 엔티티의 식별자가 같으면 두 엔티티는 같다고 볼 수 있다
    • equals와 hashCode를 식별자를 기준으로 구현한다
  • 항상 변화하고, 추적 가능해야 한다

식별자 생성

엔티티 식별자 생성은 도메인의 특징과 사용하는 기술에 따라 달라진다
흔히 식별자는 아래 방식으로 생성한다

  • 특정 규칙에 따라 생성
    • 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것이다
    • 여기서 주의할 점은 같은 시간에 동시에 식별자를 생성할 때 같은 식별자가 만들어지면 안된다는 것이다
  • UUID
    • 마땅한 규칙이 없을때 사용하면 좋다
    • 대부분의 개발 언어는 UUID 생성기를 제공하고 있다
  • 값 직접 입력
    • 회원 id, email 같은 것이다
    • 직접 입력하는 것이기 때문에 중복되지 않도록 사전에 방지하는 것이 중요하다
  • 일련번호 사용
    • 주로 데이터베이스에서 제공하는 자동 증가 기능을 사용한다

벨류 타입

  • 개념적으로 완전한 하나를 표현할 때 사용한다
    • 우편번호, 주소, 상세주소 는 묶어서 하나의 주소로 표현될 수 있다
  • 보다 명확하게 표현할 수 있게되고, 코드의 가독성도 올라간다
    • 벨류 타입 자체만 봐도 의미를 쉽게 이해할 수 있다
    • 벨류 타입으로 구성된 클래스 또한 쉽게 이해 가능해진다
  • 벨류 타입이 꼭 2개 이상의 데이터를 가져야하는 것은 아니다
    • 의미를 명확하게 하기 위해 int 대신 Money를 쓸 수 있다
  • 밸류 타입을 위한 기능을 추가할 수 있다는 장점도 있다
    • Address를 사용하면 주소를 위한 기능을 Address 클래스에 추가할 수 있다
  • 두 벨류 객체가 같은지 비교할때는 모든 속성이 같은지 비교해야한다

벨류 타입을 불변으로 선언하기

  • 모든 필드를 final private로 만듦
  • 값의 변경이 있을떄마다 해당 필드의 값을 변경하는 것이 아니라 변경된 값을 가진상태의 새로운 객체를 생성해서 반환
  • 추적성과 별칭 문제에 대해 부담이 없어짐
  • 일반적으로 날짜, 금액 등의 작은 개념을 의미하므로 새로운 객체를 만들어도 오버헤드가 적고, 추적성에도 관심을 가질 필요가 없기 떄문에 굳이 동일 객체를 유지할 필요가 없다
  • 기존 객체는 가비지컬렉션의 대상이 됨

엔티티 식별자에 벨류 타입 사용

엔티티의 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에, 식별자를 위한 벨류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다

1
2
3
class Order {
private OrderNo no;
}

그냥 String no 라고 썼다면 이게 주문 번호인지 알아보기 힘들었을 것이다
혹은 변수명으로 나타내야 했을텐데, 벨류 타입을 사용하는 것이 좀 더 확실하다

참고로 응용 서비스 계층에서 Order를 조회하는 메서드를 만들때, 파라미터를 OrderNo 대신 String으로 받는다
응용 서비스 계층을 호출하는 프레젠테이션 계층에서 도메인 계층의 요소를 알지 못하게 하기 위함일까?
e.g. orderRepository.findById(new OrderNo(orderNo))

set 사용하지 않기

도메인 모델에 무조건 getter/setter를 추가하는 것은 좋지않은 버릇이다

  • 특히 setter는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다

    • changeShippingInfo()는 배송지 정보를 새로 변경한다는 의미를 가지지만, setShippingInfo()는 단순히 배송지 값을 설정한다는 것을 뜻한다
    • 구현할때에도 setter에 추가적 처리를 하기가 애매해진다
      • 습관적으로 setter는 필드값만 변경하고 끝나는 경우가 많았기 때문이다
    • 이러한 특징으로 인해 도메인 지식이 코드에서 사라지게 되는것이다
  • setter 는 도메인 객체를 생성할 때 완전한 상태가 아닐수도 있게끔 한다

    1
    2
    3
    4
    5
    6
    7
    8
    Order order = new Order();
    order.setOrderLine(lines);
    order.setShippingInfo(shippingInfo);
    // ...

    // 주문자를 설정하지 않은 상태에서 주문 완료 처리가 되어버렸다
    // 그렇다고 여기다가 주문자 null 체크를 넣는것 또한 이상하다
    order.setState(OrderState.PREPARING);
    • 도메인 객체가 불완전한 상태로 사용되는것을 막으려면 생성시점에 필요한 것을 모두 전달해줘야 한다
  • DTO는 괜찮다

    • 도메인 로직을 가지고 있지 않기 때문이다
    • 프레임워크에서 setter 없이도 값을 할당하게끔 해주기도 하는데, 이럴경우 그 기능을 최대한 활용해서 DTO까지 VO의 특징을 가지게 하는것이 좋다

도메인 용어

도메인 용어를 코드에 반영하지 않으면 개발자가 코드의 의미를 해석해야 하는 부담이 생긴다

1
2
3
public enum OrderState {
STEP1, STEP2, STEP3, STEP4 ..
}

각각의 STEP이 어느 상태를 나타내는지 누군가에게 물어서 알아내야하고, 그 상태로 매번 변환하는(머리속에서) 과정이 추가되게 된다
그러므로 되도록 도메인 용어를 코드에 직접 사용해서 이런 불필요한 과정을 줄이고, 코드가 바로 이해될 수 있도록 해주는 것이 좋다

참고로 우리는 한국인이라 도메인 용어를 영어로 나타내는데 많은 어려움이 있다
하지만 여기 시간을 투자하지 않는다면 코드가 점점 도메인에서 멀어지게 되니, 도메인 용어에 알맞은 단어를 찾는 시간을 아까워하지 말아야 한다

참고 : 최범균, 『DDD Start!』, 지앤선(2016)

Read more »

메서드 상향

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

이름이 같은 메서드가 여러 하위클래스에 들어 있을 땐,
그 메서드를 상위 클래스로 옮긴다

특징

  • 다른 리팩토링 단계를 마친 후 적용하는 것이 일반적이다
  • 다른 클래스에 있는 두 개의 메서드가 매개변수로 전환되어 결국 같은 메서드가 될 수도 있다
  • 하위클래스가 상위 클래스 기능을 재정의했음에도 불구하고 기능이 같다면, 바로 실시해야 한다
  • 두 메서드가 똑같진 않고 비슷한 부분이 있다면 템플릿 메서드 형성을 실시하는 방법도 있다

방법

  1. 메서드가 서로 같은지 검사한다
    • 거의 비슷한데 똑같진 않다면 알고리즘 전환을 적용해서 메서드를 똑같게 만든다
  2. 메서드 시그니처가 서로 다르다면 모든 시그니처를 상위클래스에서 사용하고자 하는 시그니처로 수정한다
  3. 상위클래스에 새 메서드를 작성하고, 하위클래스의 메서드를 복사해서 넣은 뒤 적절히 수정하고 컴파일한다
    • 메서드가 하위클래스의 메서드를 사용한다면 상위클래스에 abstract 타입 메서드를 선언한다
    • 메서드가 하위클래스의 필드를 사용한다면 필드 상향이나 필드 자체 캡슐화를 적용한다
  4. 하위클래스 메서드를 하나 삭제하고 컴파일과 테스트를 실시한다
  5. 4번과 같은 식으로 상위클래스 메서드만 남을때까지 메서드를 계속 삭제한다
  6. 메서드 호출하는 부분을 상위클래스로 대체할 수 있는지 파악하고, 수정할 수 있으면 수정한다

참고 : 마틴 파울러, 『리팩토링』, 김지원 옮김, 한빛미디어(2012)

Read more »

VirtualBox에서 공인인증서 인식시키기

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

VirtualBox에서 공인인증서 인식시킨 방식에 대해 공유하고자 함

VirtualBox 하드디스크에 위치시키는 방법은 안된다

내 VirtualBox는 윈도우 7이었으므로, 인터넷에서 찾아보고 공인인증서를 C://Users/{userId}/AppData/LocalLow/NPKI 로 위치시켰는데도 전혀 인식하지 못했다
C://Program Files/NPKI, C://Program Files(x86)/NPKI 또한 마찬가지였다
VirtualBox라서 경로를 다르게 인식하는 것 같았다
아마도 mac 파일시스템으로 인식했겠지… ~/VirtualBox/cdrive 이런식으로…(왜 이 생각을 못했을까 ㅠㅠ)
어찌됬든 이런 문제때문에 USB로 공인인증서를 불러오게끔 해야한다

VirtualBox에서 usb 인식시키기

https://imitator.kr/Windows/2826 여기 따라서 VirtualBox에서 USB를 인식시키게 하면 된다

https://www.virtualbox.org/wiki/Download_Old_Builds 여기서 자기 VirtualBox 버전에 맞춰 들어간다음, Extension Pack을 선택해서 설치한다

위 블로그대로 수행하면 가상머신안에서도 USB 인식이 가능해지고, 공인인증화면에서 USB로 공인인증서를 찾을 수 있다
망할놈의 공인인증서 _

Read more »

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

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

다시 한번 작은 테스트로 시작

이제 더하기를 구현해야하는데, 아직까진 $5 + 10CHF = $10에 대한 테스트를 작성하기가 어렵다
그래서 좀 더 작은 단위($5 + $5 = $10)로 줄여서 시작해본다

1
2
3
4
public void testSimpleAddiction() {
Money sum = Money.dollar(5).plus(Money.dollar(5));
assertThat(sum).isEqualTo(Money.dollar(10));
}

어떻게 구현해야할지 명확하므로 바로 작성해본다

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

TDD를 하면서 이런식의 단계조절을 계속해서 배워야한다
지금처럼 구현이 명백히 떠오를때는 조금 성큼성큼 나가도 되고, 사려깊게 고민해야할때는 천천히 나가는 것이 좋다

우리는 다중 통화 사용에 대한 내용을 시스템의 나머지 코드에게 숨겨야하는데(설계상 가장 어려운 제약), 현재의 Money 객체로는 그 행위가 불가능하다
이처럼 사용하는 객체가 우리가 원하는 방식으로 동작하지 않을 경우엔, 그 객체와 외부 프로토콜이 같으면서 내부 구현은 다른 새로운 객체(imposter)를 만들 수 있다

TDD는 적절한때에 번뜩이는 통찰을 보장하지는 못한다(우리가 다 생각해야함)
그렇지만 확신을 주는 테스트와 조심스럽게 정리된 코드를 통해, 통찰에 대한 준비와 함께 통찰이 번뜩일때 그걸 적용할 준비를 할 수 있다

우리는 Money와 비슷하게 동작하지만 사실은 두 Money의 합을 나타내는 imposter를 만들것이다
imposter가 될 수 있는 후보로 생각해본것들은 아래와 같다

  1. 지갑 같은 객체. 여러 화폐들이 들어갈 수 있다.
  2. (2 + 3) x 5 같은 수식 객체.
    2$와 같은 Money가 수식의 가장 작은 단위가 되고, 수식들을 연산한 결과도 수식이 나온다
    최종적으로 수식에 환율을 적용하여 단일통화를 얻게된다
    (어떻게 이런 생각을 도출해냈는지 잘 떠오르지를 않는다…)

2번을 택하기로 하고, 테스트를 작성해본다

1
2
3
4
public void testSimpleAddiction() {
Money reduced = // ...
assertThat(reduced).isEqualTo(Money.dollar(10));
}

reduced는 수식에 환율을 적용하여 나온 단일통화(Money)가 된다
reduced를 얻는 과정을 좀 더 작성하면 아래와 같다

1
2
3
4
5
6
7
8
public void testSimpleAddiction() {
Money five = Money.dollar(5);

Expression sum = five.plus(five);
Money reduced = bank.reduce(sum, "USD");

assertThat(reduced).isEqualTo(Money.dollar(10));
}

덧셈의 과정으로 수식(Expression)이 나오게되고, 여기에 환율을 적용하여 단일통화를 얻게끔 했다

사실상 현재과정에서 은행 없이 수식에서 reduce를 구현할수도 있지만 그렇게 하지 않은 이유는,

  • Expression이 우리가 하려는 일의 핵심이기 때문에, 다른 부분(환율 적용)에 대해서는 최대한 모르게 하기 위함이다
    • 그렇게 해야 핵심 객체가 가능한 오래 유지되고, 테스트하기 쉽고, 재활용하기 쉬운 상태로 남을 수 있게된다
  • 환율적용 외에도 Expression과 관련있는 오퍼레이션이 많을 수 있기 때문이다
    • 그때마다 모든 오퍼레이션을 Expression에만 추가하면다면 Expression은 무한히 커질 것이다

이제 컴파일 에러를 잡아야한다
먼저 plus 메서드가 Expression을 반환해야 한다

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

클래스로 만들 수 있지만 더 가벼운 인터페이스를 선택한다

1
2
3
4
5
6
interface Expression {
}

class Money implements Expression {
// ...
}

Bank stub을 가볍게 작성해서 테스트를 통과시킨다

1
2
3
4
5
class Bank {
Money reduce(Expression source, String to) {
return Money.dollar(10);
}
}

메타포를 선택하고 빠르게 테스트 작성하고, 그를 통과시키는 과정?

순방향 진행

이제 bank.reduce()에 작성한 가짜 구현을 제거해줘야하는데, 이번 경우는 어떻게 (거꾸로)작업해야 할지가 명확하지가 않다
그래서 이번에는 순방향(?)으로 작업해보기로 한다

먼저 현재 bank.reduce() 메서드는 인자로 넘기는 source와 반환하는 Money의 값이 중복이다.
source에 넘겨주는 값과 리턴하는 Money의 값이 사실상 동일한 값이기 때문이다(삼각측량을 이용해서 가짜구현을 제거하더라도 동일하다)
이 시점에서 우리가 Expression을 만들때 생각했던, 구현체인 Sum을 등장시켜보자
Money.plus()가 Money가 아닌 Expression(Sum)을 반환하도록 변경해주도록 하자
테스트 먼저 작성해본다

1
2
3
4
5
6
7
8
9
@Test
public void testPlusReturnsSum() {
Money five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum) result;

assertThat(sum.augend).isEqualTo(five);
assertThat(sum.addend).isEqualTo(five);
}

이 테스트는 너무 구현 종속적이라 오래가지 못할것이다)

이제 정확한 expected/actual 형태가 나오도록 수정해야한다
Money.plus에서 Sum을 반환하도록 수정하고, Sum 클래스를 만들어야한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Money
Expression plus(Money addend) {
return new Sum(this, addend);
}

public class Sum implements Expression {
public Money augend;
public Money addend;

public Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}
}

좀 빠른감이 있지만 구현이 명백하게 떠오르니 바로바로 진행한다

Sum을 작성하고 나니 추가적인 테스트가 바로 떠오른다
Sum에 전달한 Money 통화가 모두 동일하고, reduce를 통해 얻어내고자 하는 통화 역시 같다면 결과는 Sum 내의 amount를 합친 값을 갖는 Money 객체여야한다

1
2
3
4
5
6
public void testReduceSum() {
Expression sum = new Sum(Money.dollar(3), Money.dollar(4));
Bank bank = new Bank();
Money result = bank.reduce(sum, "USD");
assertThat(result).isEqualTo(Money.dollar(7));
}

테스트를 통과시킨다

1
2
3
4
5
public Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
int amount = sum.augend.amount + sum.addend.amount;
return new Money(amount, to);
}

이 코드는 현재 2가지 이유로 지저분하다

  1. 형변환. reduce()는 모든 Expression에 대해 동작해야 한다
  2. Sum의 public 필드와 sum.augend.amount 같이 2단계에 걸친 레퍼런스

2번 문제는 간단히 고칠 수 있다. 메서드 일부를 Sum 클래스 내부로 옮겨버리면 된다

1
2
3
4
5
6
7
8
9
10
11
// Sum
public Money reduce(String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}

// Bank
public Money reduce(Expression source, String to) {
Sum sum = (Sum) source;
return sum.reduce(to);
}

덧셈은 됐으니 환율 적용에 대해 생각해보자
그냥 Money가 인자로 왔을 경우 환율을 적용시킨 Money를 내보내야 한다
근데 우린 지금 Money 부터 받을수가 없어서, 이를 먼저 통과시켜야 한다
테스트를 바로 작성해보자

1
2
3
4
5
6
@Test
public void testReduceMoney() {
Bank bank = new Bank();
Money result = bank.reduce(Money.dollar(1), "USD");
assertThat(result).isEqualTo(Money.dollar(1));
}
1
2
3
4
5
6
7
8
// Bank
public Money reduce(Expression source, String to) {
if(source instanceof Money) {
return (Money) source;
}
Sum sum = (Sum) source;
return sum.reduce(to);
}

코드가 너무 지저분해졌다
다른 환율에 대한 테스트를 작성하기 전에, 지저분한 코드들부터 정리하고 가는것이 좋겠다
이런식으로 클래스를 명시적으로 검사하는 코드가 있을떄는 항상 다형성을 적용해주는 것이 좋다

Money에도 reduce()를 구현해준다

1
2
3
4
@Override
public Money reduce(String to) {
return this;
}

이제 Expression을 구현하는 Money, Sum에 reduce() 메서드가 있으니 인터페이스에도 선언할 수 있다

1
2
3
public interface Expression {
Money reduce(String to);
}

이로써 불필요한 캐스팅 코드를 모두 제거할 수 있다

1
2
3
public Money reduce(Expression source, String to) {
return source.reduce(to);
}

환율 적용!

이제 다른 통화간 환율을 적용하는 테스트를 작성해본다

1
2
3
4
5
6
7
@Test
public void testReduceMoneyDifferentCurrency() {
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(Money.franc(2), "USD");
assertThat(result).isEqualTo(Money.dollar(1));
}

Money에서 직접 환율을 관장할수도 있지만, 별로 좋은 방식이 아니다
환율에 관한건 Bank가 처리하게 해야한다
reduce() 하기전에 Bank에 환율 관련된 부분을 물어보게끔 처리하면 될 것 같다
Bank를 인자로 전달하게끔 파라미터를 변경하자

1
2
3
4
5
public interface Expression {
Money reduce(Bank bank, String to);
}

// Money, Sum 적용

환율을 물어볼 메서드를 작성한다

1
2
3
4
5
6
7
// Bank
public int rate(String from, String to) {
if(from.equals("CHF") && to.equals("USD")) {
return 2;
}
return 1;
}

Money에서 rate()에 환율을 물어본다

1
2
3
4
5
@Override
public Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}

보다시피 아직 좋은 방법이 아니다. 게다가 addRate()로 환율 추가하는 메서드까지 만들어놓고 전혀 활용하지 않고 있다.
addRate()로 해시테이블 같은 곳에 환율을 추가하고(환율표), 필요할 때 매번 찾아보게 하면 될 것 같다

해시테이블에서 바로 찾기 위해 환율의 from과 to를 위한 객체를 따로 만든다
그리고 이 Pair 클래스는 키로 사용될 것이므로 equals와 hashCode를 구현해준다
(현재는 리팩토링 과정중이므로 따로 테스트를 작성하지 않는다. 리팩토링이 끝난 후 모든 테스트가 잘 통과한다면 리팩토링이 잘 되었다고 판단할 수 있기 때문이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Pair {
String from;
String to;

public Pair(String from, String to) {
this.from = from;
this.to = to;
}

@Override
public boolean equals(Object obj) {
Pair pair = (Pair) obj;
return from.equals(pair.from) && to.equals(pair.to);
}

@Override
public int hashCode() {
return 0;
}
}

0은 최악의 해시코드지만, 지금은 빠르게 달려야하니까 그냥 저렇게 작성한다
나중에 많은 통화를 다루게 될 경우 추가적으로 수정한다

이제 이 환율표를 사용하도록 Bank를 수정한다

1
2
3
4
5
6
7
8
9
10
// Bank
Map<Pair, Integer> rateTable = new Hashtable<>();

public void addRate(String from, String to, int rate) {
rateTable.put(new Pair(from, to), rate);
}

public int rate(String from, String to) {
return rateTable.get(new Pair(from, to));
}

잘 동작할 줄 알았는데 테스트가 실패한다!
살펴보니 같은 통화일떄가 문제였다. 이렇게 뜻밖지 못하게 발견한 일의 경우 테스트를 추가해서 다른 사람들이 알게끔 해줘야 한다

1
2
3
4
5
@Test
public void testIdentityRate() {
Bank bank = new Bank();
assertThat(bank.rate("USD", "USD")).isEqualTo(1);
}

이렇게 리팩토링하다가 실수한 경우 이 문제를 분리하기 위해 또 다른 테스트를 작성하고, 전진해나간다

이제 rate() 를 수정하자

1
2
3
4
5
6
7
public int rate(String from, String to) {
if(from.equals(to)) {
return 1;
}

return rateTable.get(new Pair(from, to));
}

다른 통화간 더하기

드디어 5$ + 10CHF = 10$ 를 테스트 해볼 떄가 왔다
아래가 우리가 최종적으로 원하는 테스트의 모습이다

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testMixedAddition() {
Expression dollar = Money.dollar(5);
Expression franc = Money.franc(10);

Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);

Money result = bank.reduce(dollar.plus(franc), "USD");
assertThat(result).isEqualTo(Money.dollar(10));
}

하지만 안타깝게도 컴파일 에러가 난다
좀 더 천천히 진행해보기로 하고(모든 에러를 컴파일러가 잡아줄것이라는 기대?), 한 단계만 뒤로 물러나보자

먼저 testMixedAddition() 상단의 Expression을 Money로 바꿔서 컴파일 에러를 제거하고, 테스트를 돌려보자

1
2
3
4
5
6
@Test
public void testMixedAddition() {
Money dollar = Money.dollar(5);
Money franc = Money.franc(10);
// ...
}

테스트가 실패한다. 10$ 대신 15$가 나오는 것이 축약을 하지 않는 것 처럼 보인다.

1
2
3
4
5
6
@Override
public Money reduce(Bank bank, String to) {
int amount = augend.reduce(bank, to).amount
+ addend.reduce(bank, to).amount;
return new Money(amount, to);
}

테스트가 통과했으니, 처음 컴파일 오류에서 봤던 내용을 다시 생각해보자
사실상 모든 Money는 Expression이어야 한다. 이제 이를 조금씩 없애도록 하자.
파급효과를 피하기 위해 가장자리부터 작업해 나가기 시작해서 테스트 케이스까지 거슬러 올라가도록 한다

먼저 Sum 부터 고친다

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Sum implements Expression {
// 1
public Expression augend;
public Expression addend;

// 2
public Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}

// ...
}

인스턴스 변수 타입을 고치고, 파라미터 타입도 바꾼다
이제 Sum을 사용하는 곳에서는 Expression을 받을 수 있다

Money.plus()의 파라미터를 Expression으로 바꾼다.
바꾸는 김에 times()의 반환 타입도 바꾼다

1
2
3
4
5
6
7
public Expression plus(Expression addend) {
return new Sum(this, addend);
}

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

이제 다시 testMixedAddition()의 참조변수들을 바꾼다

1
2
3
4
5
6
@Test
public void testMixedAddition() {
Expression dollar = Money.dollar(5);
Expression franc = Money.franc(10);
// ...
}

컴파일러가 Expression에 plus()를 구현해야 한다고 알려주고 있다
컴파일러의 지시대로 따라가자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Expression
public interface Expression {
// ...
Expression plus(Expression addend);
}

// Money
// 이미 구현되어 있음

// Sum
@Override
public Expression plus(Expression addend) {
return null; // stub
}

추상화

Expression.plus()를 끝마치려면 Sum.plus를 구현해야 한다
테스트를 작성한다

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testSumPlusMoney() {
Expression dollar = Money.dollar(5);
Expression franc = Money.franc(10);

Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);

Expression sum = new Sum(dollar, franc).plus(dollar);
Money result = bank.reduce(sum, "USD");

assertThat(result).isEqualTo(Money.dollar(15));
}

테스트가 통과하게끔 작성한다

1
2
3
4
@Override
public Expression plus(Expression addend) {
return new Sum(this, addend);
}

Money와 형태가 똑같아져서, 추상클래스로 분리할 수 있을 것 같다

이제 Expression.times를 작성해야 한다
Sum.times를 작성한다면 Expression.times를 선언하는 일은 어렵지 않을 것 같다
Sum.times에 대한 테스트를 작성한다

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testSumTimes() {
Expression dollar = Money.dollar(5);
Expression franc = Money.franc(10);

Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);

Expression sum = new Sum(dollar, franc).times(2);
Money result = bank.reduce(sum, "USD");

assertThat(result).isEqualTo(Money.dollar(20));
}

Expression에 times 메서드를 선언하고, Sum에도 times를 작성한다

1
2
3
4
5
6
7
8
9
interface Expression {
// ...
Expression times(int multiplier);
}

@Override
public Expression times(int multiplier) {
return new Sum(augend.times(multiplier), addend.times(multiplier));
}

난 사실 이 장이 잘 이해되지 않는다…

Read more »
1…345…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