1 second == 1,000 millisecond
1 millisecond == 1,000 microsecond
1 microsecond == 1,000 nanosecond
1 second == 1,000,000,000 nanosecond
더 많은 것을 기억하기 위해 기록합니다
1 second == 1,000 millisecond
1 millisecond == 1,000 microsecond
1 microsecond == 1,000 nanosecond
1 second == 1,000,000,000 nanosecond
알고리즘을 더 분명한 것으로 교체해야 할 땐
해당 메서드의 내용을 새 알고리즘으로 바꾼다
하위클래스 안의 두 메서드가 거의 비슷한 단계들을 같은 순서로 수행할 땐
그 단계들을 시그니처가 같은 두 개의 메서드로 만들어서 두 원본 메서드를 같게 만든 후,
두 메서드를 상위클래스로 옮긴다
비슷해보이는 같은 클래스에 있다면, 두 메서드가 어떤 공통 상위클래스의 하위클래스가 되게 정리해야 한다
1 | class Printer { |
를 아래의 구조로 변경한다
1 | class Printer { |
두 메서드를 잘게 분리하는데, 공통된 부분과 공통되지 않은 부분이 나오게끔 분리한다
1 | class TextPrinter extends Printer { |
이 기법의 핵심이다
하나의 클래스에 대해 메서드 상향을 실시하자
1 | abstract class Printer { |
abstract
로 선언하고, 하위클래스에서 구현하도록 한다나머지 하나의 클래스에도 동일하게 적용한다
git config 추가
1 | $ git config --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/origin/pr/*" |
fetch로 가져올 때 origin 말고 pr도 가져올 수 있게함
1 | $ git fetch origin |
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
theme
테마는 submodule로 저장해줘야하고, 테마에서 _config.yml을 수정할 것이므로 theme fork한것을 submodule에 넣어야함
travis ci app 설치 및 진행 방식
.travis.yml, _config.yml은 여기를 참조하면 됨
https://medium.com/@changjoopark/travis-ci를-이용한-github-pages-hexo-블로그-자동-배포하기-6a222a2013e6
온라인 서점
이 도메인이 된다카탈로그의 상품과 배송의 상품은 다르다
일반적인 어플리케이션의 아키텍쳐는 아래의 4계층으로 구성된다
사용자의 요청을 처리하고 사용자에게 정보를 보여준다
외부 시스템도 사용자가 될 수 있다
사용자가 요청한 기능을 실행한다
업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다
시스템이 제공할 도메인의 규칙을 구현한다
외부 시스템과의 연동을 처리한다(DB, Messaging 등)
여기서 도메인 영역에 해당하는,
도메인의 규칙을 객체지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다
e.g. 주문 상태 변경에 대한 규칙을 처리하는 Order class
1 | class Order { |
주문 상태 변경 로직을 도메인에서 직접 처리하고 있다
이처럼 핵심 규칙을 구현한 코드가 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다
개념 모델을 만들때 처음부터 완벽하게 도메인을 표현하는 모델을 만드는 시도를 할 수 있지만 실제로 이는 불가능에 가깝다
소프트웨어를 개발하는 동안 도메인을 점점 더 이해하게 되기 때문에, 시간이 지나감에 따라 모델을 수정하는 경우가 많기 때문이다
(지식이 쌓이면서 완전히 다른 의미로 해석되어지는 상황도 존재한다)
그러므로 처음에는 개요 수준의 개념 모델을 작성하여 도메인에 대한 전체 윤곽을 이해하는데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야한다
아무리 천재 개발자라도 도메인에 대한 이해없이 코딩을 시작할수는 없다
기획서, 유스 케이스, 유저 스토리 같은 요구사항과 관련자와의 대화를 통해 도메인을 이해하고, 이를 바탕으로 도메인 모델 초안을 만들어야 코드를 작성할 수 있다
도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능
을 찾는 것이다.
이는 모두 요구사항을 분석하면서 찾아낼 수 있다
제공해야 하는 기능
을 도출해낼 수 있다
특정 항목이 어떤 데이터로 구성되어야 하는지
알 수 있다
각 항목간의 관계
를 알 수 있다
특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우
도 많다
문서화
코드는 상세한 모든 내용을 다루고 있기 때문에 코드를 이용해서 전체 소프트웨어를 분석하려면 많은 시간을 투자해야한다
전반적인 기능 목록이나 모듈 구조는 상위 수준에서 정리된 문서를 참조하는 것이 소프트웨어 전반을 빠르게 이해하는데 도움이 된다코드 자체도 문서화의 대상이 될 수 있다
도메인 지식이 잘 묻어나고, 도메인을 잘 표현하도록 코드를 작성하면 코드의 가독성이 높아지면서 코드가 문서로서의 의미도 가지게 된다
모델은 크게 엔티티와 벨류로 구분할 수 있다
이 둘의 차이를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있으므로 차이를 명확하게 이해해야 한다
엔티티 식별자 생성은 도메인의 특징과 사용하는 기술에 따라 달라진다
흔히 식별자는 아래 방식으로 생성한다
자동 증가 기능
을 사용한다우편번호, 주소, 상세주소
는 묶어서 하나의 주소
로 표현될 수 있다Address
를 사용하면 주소를 위한 기능을 Address 클래스에 추가할 수 있다final private
로 만듦
- 추적성과 별칭 문제에 대해 부담이 없어짐
- 일반적으로 날짜, 금액 등의 작은 개념을 의미하므로 새로운 객체를 만들어도 오버헤드가 적고, 추적성에도 관심을 가질 필요가 없기 떄문에 굳이 동일 객체를 유지할 필요가 없다
- 기존 객체는 가비지컬렉션의 대상이 됨
엔티티의 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에, 식별자를 위한 벨류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다
1 | class Order { |
그냥 String no
라고 썼다면 이게 주문 번호인지 알아보기 힘들었을 것이다
혹은 변수명으로 나타내야 했을텐데, 벨류 타입을 사용하는 것이 좀 더 확실하다
참고로 응용 서비스 계층에서 Order를 조회하는 메서드를 만들때, 파라미터를 OrderNo 대신 String으로 받는다
응용 서비스 계층을 호출하는 프레젠테이션 계층에서 도메인 계층의 요소를 알지 못하게 하기 위함일까?
e.g.orderRepository.findById(new OrderNo(orderNo))
도메인 모델에 무조건 getter/setter를 추가하는 것은 좋지않은 버릇이다
특히 setter는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다
changeShippingInfo()
는 배송지 정보를 새로 변경한다는 의미를 가지지만, setShippingInfo()
는 단순히 배송지 값을 설정한다는 것을 뜻한다setter 는 도메인 객체를 생성할 때 완전한 상태가 아닐수도 있게끔 한다
1 | Order order = new Order(); |
DTO는 괜찮다
도메인 용어를 코드에 반영하지 않으면 개발자가 코드의 의미를 해석해야 하는 부담이 생긴다
1 | public enum OrderState { |
각각의 STEP이 어느 상태를 나타내는지 누군가에게 물어서 알아내야하고, 그 상태로 매번 변환하는(머리속에서) 과정이 추가되게 된다
그러므로 되도록 도메인 용어를 코드에 직접 사용해서 이런 불필요한 과정을 줄이고, 코드가 바로 이해될 수 있도록 해주는 것이 좋다
참고로 우리는 한국인이라 도메인 용어를 영어로 나타내는데 많은 어려움이 있다
하지만 여기 시간을 투자하지 않는다면 코드가 점점 도메인에서 멀어지게 되니, 도메인 용어에 알맞은 단어를 찾는 시간을 아까워하지 말아야 한다
이름이 같은 메서드가 여러 하위클래스에 들어 있을 땐,
그 메서드를 상위 클래스로 옮긴다
템플릿 메서드 형성
을 실시하는 방법도 있다알고리즘 전환
을 적용해서 메서드를 똑같게 만든다필드 상향
이나 필드 자체 캡슐화
를 적용한다VirtualBox에서 공인인증서 인식시킨 방식에 대해 공유하고자 함
내 VirtualBox는 윈도우 7이었으므로, 인터넷에서 찾아보고 공인인증서를 C://Users/{userId}/AppData/LocalLow/NPKI
로 위치시켰는데도 전혀 인식하지 못했다
C://Program Files/NPKI
, C://Program Files(x86)/NPKI
또한 마찬가지였다
VirtualBox라서 경로를 다르게 인식하는 것 같았다
아마도 mac 파일시스템으로 인식했겠지… ~/VirtualBox/cdrive 이런식으로…(왜 이 생각을 못했을까 ㅠㅠ)
어찌됬든 이런 문제때문에 USB로 공인인증서를 불러오게끔 해야한다
https://imitator.kr/Windows/2826 여기 따라서 VirtualBox에서 USB를 인식시키게 하면 된다
https://www.virtualbox.org/wiki/Download_Old_Builds 여기서 자기 VirtualBox 버전에 맞춰 들어간다음,
Extension Pack
을 선택해서 설치한다
위 블로그대로 수행하면 가상머신안에서도 USB 인식이 가능해지고, 공인인증화면에서 USB로 공인인증서를 찾을 수 있다
망할놈의 공인인증서 _
이제 더하기를 구현해야하는데, 아직까진 $5 + 10CHF = $10
에 대한 테스트를 작성하기가 어렵다
그래서 좀 더 작은 단위($5 + $5 = $10
)로 줄여서 시작해본다
1 | public void testSimpleAddiction() { |
어떻게 구현해야할지 명확하므로 바로 작성해본다
1 | // Money |
TDD를 하면서 이런식의 단계조절을 계속해서 배워야한다
지금처럼 구현이 명백히 떠오를때는 조금 성큼성큼 나가도 되고, 사려깊게 고민해야할때는 천천히 나가는 것이 좋다
우리는 다중 통화 사용에 대한 내용을 시스템의 나머지 코드에게 숨겨야하는데(설계상 가장 어려운 제약), 현재의 Money 객체로는 그 행위가 불가능하다
이처럼 사용하는 객체가 우리가 원하는 방식으로 동작하지 않을 경우엔, 그 객체와 외부 프로토콜이 같으면서 내부 구현은 다른 새로운 객체(imposter)를 만들 수 있다
TDD는 적절한때에 번뜩이는 통찰을 보장하지는 못한다(우리가 다 생각해야함)
그렇지만 확신을 주는 테스트와 조심스럽게 정리된 코드를 통해, 통찰에 대한 준비와 함께 통찰이 번뜩일때 그걸 적용할 준비를 할 수 있다
우리는 Money와 비슷하게 동작하지만 사실은 두 Money의 합을 나타내는 imposter를 만들것이다
imposter가 될 수 있는 후보로 생각해본것들은 아래와 같다
지갑
같은 객체. 여러 화폐들이 들어갈 수 있다.(2 + 3) x 5
같은 수식
객체.Money
가 수식의 가장 작은 단위가 되고, 수식들을 연산한 결과도 수식이 나온다2번을 택하기로 하고, 테스트를 작성해본다
1 | public void testSimpleAddiction() { |
reduced는 수식에 환율을 적용하여 나온 단일통화(Money)가 된다
reduced를 얻는 과정을 좀 더 작성하면 아래와 같다
1 | public void testSimpleAddiction() { |
덧셈의 과정으로 수식(Expression)
이 나오게되고, 여기에 환율을 적용하여 단일통화를 얻게끔 했다
사실상 현재과정에서
은행
없이수식
에서reduce
를 구현할수도 있지만 그렇게 하지 않은 이유는,
- Expression이 우리가 하려는 일의 핵심이기 때문에, 다른 부분(환율 적용)에 대해서는 최대한 모르게 하기 위함이다
- 그렇게 해야 핵심 객체가 가능한 오래 유지되고, 테스트하기 쉽고, 재활용하기 쉬운 상태로 남을 수 있게된다
- 환율적용 외에도 Expression과 관련있는 오퍼레이션이 많을 수 있기 때문이다
- 그때마다 모든 오퍼레이션을 Expression에만 추가하면다면 Expression은 무한히 커질 것이다
이제 컴파일 에러를 잡아야한다
먼저 plus
메서드가 Expression을 반환해야 한다
1 | // Money |
클래스로 만들 수 있지만 더 가벼운 인터페이스를 선택한다
1 | interface Expression { |
Bank
stub을 가볍게 작성해서 테스트를 통과시킨다
1 | class Bank { |
메타포를 선택하고 빠르게 테스트 작성하고, 그를 통과시키는 과정?
이제 bank.reduce()
에 작성한 가짜 구현을 제거해줘야하는데, 이번 경우는 어떻게 (거꾸로)작업해야 할지가 명확하지가 않다
그래서 이번에는 순방향(?)으로 작업해보기로 한다
먼저 현재 bank.reduce()
메서드는 인자로 넘기는 source와 반환하는 Money의 값이 중복이다.
source에 넘겨주는 값과 리턴하는 Money의 값이 사실상 동일한 값이기 때문이다(삼각측량을 이용해서 가짜구현을 제거하더라도 동일하다)
이 시점에서 우리가 Expression을 만들때 생각했던, 구현체인 Sum을 등장시켜보자
Money.plus()
가 Money가 아닌 Expression(Sum)을 반환하도록 변경해주도록 하자
테스트 먼저 작성해본다
1 | @Test |
이 테스트는 너무 구현 종속적이라 오래가지 못할것이다)
이제 정확한 expected/actual 형태가 나오도록 수정해야한다
Money.plus에서 Sum을 반환하도록 수정하고, Sum
클래스를 만들어야한다
1 | // Money |
좀 빠른감이 있지만 구현이 명백하게 떠오르니 바로바로 진행한다
Sum을 작성하고 나니 추가적인 테스트가 바로 떠오른다
Sum에 전달한 Money 통화가 모두 동일하고, reduce를 통해 얻어내고자 하는 통화 역시 같다면 결과는 Sum 내의 amount를 합친 값을 갖는 Money 객체여야한다
1 | public void testReduceSum() { |
테스트를 통과시킨다
1 | public Money reduce(Expression source, String to) { |
이 코드는 현재 2가지 이유로 지저분하다
reduce()
는 모든 Expression에 대해 동작해야 한다Sum
의 public
필드와 sum.augend.amount
같이 2단계에 걸친 레퍼런스2번 문제는 간단히 고칠 수 있다. 메서드 일부를 Sum 클래스 내부로 옮겨버리면 된다
1 | // Sum |
덧셈은 됐으니 환율 적용에 대해 생각해보자
그냥 Money
가 인자로 왔을 경우 환율을 적용시킨 Money를 내보내야 한다
근데 우린 지금 Money
부터 받을수가 없어서, 이를 먼저 통과시켜야 한다
테스트를 바로 작성해보자
1 | @Test |
1 | // Bank |
코드가 너무 지저분해졌다
다른 환율에 대한 테스트를 작성하기 전에, 지저분한 코드들부터 정리하고 가는것이 좋겠다
이런식으로 클래스를 명시적으로 검사하는 코드가 있을떄는 항상 다형성
을 적용해주는 것이 좋다
Money에도 reduce()
를 구현해준다
1 | @Override |
이제 Expression
을 구현하는 Money
, Sum
에 reduce()
메서드가 있으니 인터페이스에도 선언할 수 있다
1 | public interface Expression { |
이로써 불필요한 캐스팅 코드를 모두 제거할 수 있다
1 | public Money reduce(Expression source, String to) { |
이제 다른 통화간 환율을 적용하는 테스트를 작성해본다
1 | @Test |
Money에서 직접 환율을 관장할수도 있지만, 별로 좋은 방식이 아니다
환율에 관한건 Bank
가 처리하게 해야한다
reduce()
하기전에 Bank
에 환율 관련된 부분을 물어보게끔 처리하면 될 것 같다
Bank
를 인자로 전달하게끔 파라미터를 변경하자
1 | public interface Expression { |
환율을 물어볼 메서드를 작성한다
1 | // Bank |
Money에서 rate()
에 환율을 물어본다
1 | @Override |
보다시피 아직 좋은 방법이 아니다. 게다가 addRate()
로 환율 추가하는 메서드까지 만들어놓고 전혀 활용하지 않고 있다.
addRate()
로 해시테이블 같은 곳에 환율을 추가하고(환율표), 필요할 때 매번 찾아보게 하면 될 것 같다
해시테이블에서 바로 찾기 위해 환율의 from과 to를 위한 객체를 따로 만든다
그리고 이 Pair
클래스는 키
로 사용될 것이므로 equals
와 hashCode
를 구현해준다
(현재는 리팩토링 과정중이므로 따로 테스트를 작성하지 않는다. 리팩토링이 끝난 후 모든 테스트가 잘 통과한다면 리팩토링이 잘 되었다고 판단할 수 있기 때문이다.)
1 | class Pair { |
0은 최악의 해시코드지만, 지금은 빠르게 달려야하니까 그냥 저렇게 작성한다
나중에 많은 통화를 다루게 될 경우 추가적으로 수정한다
이제 이 환율표를 사용하도록 Bank를 수정한다
1 | // Bank |
잘 동작할 줄 알았는데 테스트가 실패한다!
살펴보니 같은 통화일떄가 문제였다. 이렇게 뜻밖지 못하게 발견한 일의 경우 테스트를 추가해서 다른 사람들이 알게끔 해줘야 한다
1 | @Test |
이렇게 리팩토링하다가 실수한 경우 이 문제를 분리하기 위해 또 다른 테스트를 작성하고, 전진해나간다
이제 rate()
를 수정하자
1 | public int rate(String from, String to) { |
드디어 5$ + 10CHF = 10$
를 테스트 해볼 떄가 왔다
아래가 우리가 최종적으로 원하는 테스트의 모습이다
1 | @Test |
하지만 안타깝게도 컴파일 에러가 난다
좀 더 천천히 진행해보기로 하고(모든 에러를 컴파일러가 잡아줄것이라는 기대?), 한 단계만 뒤로 물러나보자
먼저 testMixedAddition()
상단의 Expression
을 Money
로 바꿔서 컴파일 에러를 제거하고, 테스트를 돌려보자
1 | @Test |
테스트가 실패한다. 10$ 대신 15$가 나오는 것이 축약을 하지 않는 것 처럼 보인다.
1 | @Override |
테스트가 통과했으니, 처음 컴파일 오류에서 봤던 내용을 다시 생각해보자
사실상 모든 Money는 Expression이어야 한다. 이제 이를 조금씩 없애도록 하자.
파급효과를 피하기 위해 가장자리부터 작업해 나가기 시작해서 테스트 케이스까지 거슬러 올라가도록 한다
먼저 Sum 부터 고친다
1 | public class Sum implements Expression { |
인스턴스 변수 타입을 고치고, 파라미터 타입도 바꾼다
이제Sum
을 사용하는 곳에서는Expression
을 받을 수 있다
Money.plus()
의 파라미터를 Expression으로 바꾼다.
바꾸는 김에 times()
의 반환 타입도 바꾼다
1 | public Expression plus(Expression addend) { |
이제 다시 testMixedAddition()
의 참조변수들을 바꾼다
1 | @Test |
컴파일러가 Expression
에 plus()
를 구현해야 한다고 알려주고 있다
컴파일러의 지시대로 따라가자
1 | // Expression |
Expression.plus()
를 끝마치려면 Sum.plus
를 구현해야 한다
테스트를 작성한다
1 | @Test |
테스트가 통과하게끔 작성한다
1 | @Override |
Money와 형태가 똑같아져서, 추상클래스로 분리할 수 있을 것 같다
이제 Expression.times
를 작성해야 한다
Sum.times
를 작성한다면 Expression.times
를 선언하는 일은 어렵지 않을 것 같다
Sum.times
에 대한 테스트를 작성한다
1 | @Test |
Expression에 times 메서드를 선언하고, Sum에도 times를 작성한다
1 | interface Expression { |
난 사실 이 장이 잘 이해되지 않는다…