-
도메인의 컨셉, 그리고 구체적인 동작 (테스트 주도 개발 스터디 2회차 참여 소고)Today I Learned 2022. 10. 2. 21:30
테스트 주도 개발 스터디 2회차에서 배운 내용들 중 뜻깊게 느낀 내용들을 정리했다.
프로그래밍적인 요소가 아닌 도메인의 컨셉에 집중하기 위한 리팩터링
다중 통화를 지원하는 Money 객체를 구현하는 과정에서, Money 객체를 상속받는 구현체인 Dollar 객체와 Franc 객체의 중복을 제거하면서 최종적으로는 Money 객체만을 남기도록 하는 과정이 있다.
그 중 Franc(10, 'CHF')과 Money(10, 'CHF')가 서로 동등함을 나타내기를 기대하는 테스트 코드를 통과시키기 위해 equals() 메서드를 다음과 같이 수정하는 과정이 있다.
class Money { private String currency; // ... // 수정 전 @Override public boolean equals(Object other) { Money money = (Money) other; return amount == money.amount && getClass().equals(money.getClass()); } // 수정 후 @Override public boolean equals(Object other) { Money money = (Money) other; return amount == money.amount && currency().equals(money.currency()); } }
수정 이전의 equals() 메서드는 비교하는 두 인스턴스의 클래스가 Franc과 Money로 다르기 때문에 getClass()를 통해 비교하는 구문을 통과하지 못한다. 이를 해결하기 위해 클래스 타입을 비교하는 대신 두 인스턴스가 가진 'CHF'라는 currency를 비교하도록 해 동등성을 나타내도록 한다.
트레이너님께서 이 과정에는 동등성을 원하는 대로 나타낼 수 있도록 하기 위한 의도뿐만 아니라, 어떠한 로직의 핵심 동작을 나타내는 데 클래스 타입 같은 프로그래밍적인 요소를 사용하는 것에서 '통화'와 같은 도메인의 컨셉적인 요소를 사용하도록 로직을 개선하는 것이라는 말씀을 해주셨다.
단순히 원하는 기능을 동작하게만 하는 소스코드를 만드는 것이 아니라 소스코드를 읽는 것만으로도 그 동작이 도메인의 어떤 부분에 집중하는지 알 수 있도록 의미를 부여해야 할 것이라는 생각이 들었다.
기능을 만드는 것은 구체적인 동작을 짜는 것에서 시작하자
다중 통화 간의 덧셈 연산을 지원하는 Money 객체끼리의 덧셈을 구현하기 위해 작성된 테스트 코드는 다음과 같다.
// MoneyTest.java @Test void simpleAddition() { Money five = Money.dollar(5); Bank bank = new Bank(); Expression sum = five.plus(five); Money reduced = bank.reduce(sum, 'USD'); assertThat(reduced).isEqualTo(Money.dollar(10)); }
놀랍게도 이 소스코드는 가장 밑단에서부터 위로 한 줄씩 올라가면서 만들어졌다. 가장 마지막 코드에서부터 위로 올라가면서 내용을 간단히 살펴본 과정은 다음과 같다.
- (아직은 같은 통화끼리 더하는 것부터 시작하기는 했지만) 서로 다른 두 통화를 더하기 위해서는 두 통화를 연산한 어떤 결과에 대해 환율을 적용한 것이어야 할 것이다. (reduce 네이밍은 '어떤 연산 결과를 환율을 적용해 축약시킨다'는 의미로 생각해볼 수 있다.)
- 환율의 적용은 어떻게 보면 직접적인 연산(덧셈)과는 다른 관심사라고 볼 수 있다. 그 관심사의 동작을 이행해주는 새로운 객체가 필요할 것이다.
- 연산의 결과가 주어져야 할 것이다.
이 코드의 위쪽에는 어떤 주체들이 정의되어 있고, 아래쪽은 그 주체들의 행동이 기술되어 있다. 그 중에서도 가장 마지막 줄에서 검증하려는 부분이 가장 핵심적인 부분이다. 가장 핵심적인 동작을 먼저 기술하고, 그 동작에 사용되는 요소들이 어떤 과정을 통해 형성되는지를 순차적으로 쌓아 올라간다. 이러한 과정을 거쳐 동작에 필요한 주체들을 하나 둘 정의하는 순서를 거쳐 하나의 로직을 구성한다.
동작이 먼저 정의되면 그 동작을 수행하기 위해 정의해야 하는 과정이나 대상이 명확해지고, 분리된다. 즉 특정 과정을 수행해야 하는 대상의 내부 로직을 드러나지 않게 분리하는 관심사의 분리를 할 수 있게 된다.
어떠한 기능을 구현할 때, 그 기능의 가장 핵심적인 동작이 무엇인지 생각하면서 Bottom-up 방식으로 쌓아 올리는 과정을 실제로 적용해보는 시도를 해야 할 것이다.
'Today I Learned' 카테고리의 다른 글
실험의, 실험에 의한, 실험을 위한 하루 (레벨 테스트 2일차 작업 회고) (0) 2022.10.04 찍어보는데 안되고... 범위는 갑자기 넓어지고... (레벨 테스트 1일차 작업 회고) (0) 2022.10.03 React 웹 애플리케이션 Github에 배포하기 (0) 2022.10.01 고통의 트러블슈팅 (0) 2022.09.30 백엔드로부터 DTO로 배열 전달받기 (0) 2022.09.29 - (아직은 같은 통화끼리 더하는 것부터 시작하기는 했지만) 서로 다른 두 통화를 더하기 위해서는 두 통화를 연산한 어떤 결과에 대해 환율을 적용한 것이어야 할 것이다. (reduce 네이밍은 '어떤 연산 결과를 환율을 적용해 축약시킨다'는 의미로 생각해볼 수 있다.)