야근에 시달리다 드디어...!
요약
-
복잡한 코드와 도메인 유의성이 갖는 코드는 해당 테스트의 회귀 방지가 뛰어나기 때문에 단위 테스트에 가장 이롭다.
- 코드 복잡도 : 의사 결정 지점 수에 따라 명시적(코드), 암시적(코드가 사용하는 라이브러리)으로 정의
- 도메인 유의성 : 프로젝트의 문제 도메인에 대해 코드가 얼마나 중요한지를 보여줌.
-
모든 제품의 코드는 복잡도 또는 도메인 유의성과 협력자 수에 따라 네 가지 유형의 코드로 분류 가능
- 도메인 모델 및 알고리즘
- 간단한 코드
- 컨트롤러
- 지나치게 복잡한 코드
- 코드가 중요하거나 복잡할수록 협력자가 적어야 한다.
-
험블 객체 패턴 : 해당 코드에서 비즈니스 로직을 별도로 클래스로 추출해 복잡한 코드를 테스트할 수 있는데 도움이 된다.
- 비즈니스 코드를 둘러싼 나머지 코드 = 험블래퍼 = 컨트롤러
- 육각형 아키텍처와 함수형 아키텍처는 험블 객체 패턴을 구현함
- 비즈니스 로직과 그 외의 것들을 분리함.
- 코드는 깊을 수도 있고(복잡하거나 중요함) 넓을 수도 있지만(협력자가 많음) 둘 다는 아니다. -> 비즈니스 로직과 오케스트레이션 책임 분리
- 도메인 유의성이 있는 전제 조건이라면 테스트하고, 그 외의 경우에는 테스트하지 않는 다.
-
비즈니스 로직과 오케스트레이션을 분리할 때 중요한 특성 -> 동시에 2가지 특성만 챙길 수 있다.
- 도메인 모델 테스트 유의성 : 도메인 클래스 내 협력자 수와 유형에 대한 함수
- 컨트롤러 단순성 : 컨트롤러 의사 결정 지점이 있는지에 따라 다름
- 성능 : 프로세스 외부 의존성에 대한 호출 수로 정의
-
의사 결정 프로세스 단계 세분화 : 성능과 도메인 모델 테스트 유의성을 지키지만 컨트롤러의 단순함을 포기한다. -> 컨트롤러의 복잡함은 CanExecute/ Execute 패턴과 도메인 이벤트 패턴을 사용해 복잡함을 완화시킬 수 있음.
- CanExecute/ Execute 패턴 : Do() 메서드에 대해 CanDo()를 두고 CanDo()가 성공적으로 실행되는 것을 Do()의 전제 조건으로 한다. -> 컨트롤러의 의사결정을 근본적으로 제거
- 도메인 이벤트 : 도메인 모델의 중요한 변경 사항을 추적하고 해당 변경 사항을 외부 의존성에 대한 호출로 변환 -> 컨트롤러에서 추적에 대한 책임이 없어짐.
리팩터링할 코드 식별
-
4가지 코드 유형중 지나치게 복잡한 코드를 리팩터링 해야 한다.
- 코드 유형 분류의 기준
- 코드의 복잡도 : 코드 내 의사 결정(분기) 지점 수로 결정
-
도메인 유의성 : 코드가 프로젝트의 문제 도메인에 대해 얼마나 의미 있는지
- 도메인 코드는 복잡할 필요가 없으며, 복잡한 코드는 도메인 유의성이 나타나지 않아도 테스트할 수 있다,
-
협력자의 수
- 협력자가 많은 코드는 테스트 비용이 많이 든다,
- 도메인 모델이라면 프로세스 외부 협력자를 사용하면 안 된다. -> 도메인 외부 계층의 클래스에 위임
- 4가지 코드 유형
- 도메인 모델과 알고리즘
- 간단한 코드
- 컨트롤러
-
지나치게 복잡한 코드
- 협력자가 많으며 복잡하거나 중요함.
- 덩치가 큰 컨트롤러(작업을 위임하지 않고 모든 것을 스스로 하는 컨트롤러 = God Object)
- 테스트하기 어렵기 때문에 리팩터링을 해야함 -> 기반 코드를 리팩터링하지 않고서는 테스트 스위트를 크게 개선할 수 없다.
-
지나치게 복잡한 코드 리팩터링하기
- 알고리즘과 컨트롤러로 나눠서 리팩터링
- 코드가 더 중요해지거나 복잡해질수록 협력자는 더 적어야 한다.
- 험블객체 패턴
- 테스트하기 어려운 코드, 테스트하기 쉬운 코드를 분리
- 아키텍처의 경계마다 험블 객체 패턴을 발견할 수 있다.
- MVP, MVC 패턴에서 프레젠터와 컨트롤러의 구성요소는 험블객체
-
비즈니스 로직과 오케스트레이션 역할 분리
- 비즈니스 로직 : 컴퓨터 프로그램에서 데이터를 생성, 표시, 저장, 변경하는 부분
- 오케스트레이션 : 비즈니스 로직을 수행하면서 협력하는 객체들과 커뮤니케이션하는 역할을 하는 코드 (비즈니스 로직과 어떤 객체 사이에서 중간다리 역할을 하는??)
- 비즈니스 로직에서 의존객체들과 커뮤니케이션하는 코드를 완전히 분리해서 별도의 객체가 수행하도록 하고 비즈니스 로직을 담은 객체는 순수함수가 되게 한다.
리팩터링 하기
-
프로그램을 구성하고 있는 코드의 유형 파악하기
- 코드의 복잡도와 협력자의 수로 코드의 유형을 파악한다.
- 지나치게 복잡한 코드 유형에 해당할 경우 리팩터링을 진행
-
암시적 의존성을 명식적으로 만들기
- 테스트 용이성을 개선하는 일반적인 방법
- 구체적인 외부 협력자에 대한 인터페이스를 만들고 해당 인터페이스를 지나치게 복잡합 코드를 가진 클래스로 주입한다. (의존성 주입)
- 도메인 모델은 직접적으로든 간접적으로든(인터페이스를 통해) 프로세스 외부 협력자에게 의존하지 않는 것이 훨씬 더 깔끔하다.
-
애플리케이션 서비스 계층 도입
- 도메인 모델이 외부 시스템과 직접 통신하는 문제를 극복하려면 다른 클래스인 험블 컨트롤러로 책임을 옮겨야 한다.
- 험블 컨트롤러 : 육각형 아키텍처 분류상 애플리케이션 서비스
- 일반적으로 도메인 클래스는 다른 도메인 클래스나 단순 값과 같은 프로세스 내부 의존성에만 의존해야 한다.
- UserController 클래스가 담당하는 것은 모든 협력자를 한데 모으는 것
-
애플리케이션 서비스 복잡도 낮추기
- 프로세스 외부 의존성 직접 인스턴스화 -> 주입해서 사용
- 데이터베이스에서 받은 원시데이터로 도메인 객체를 재구성
- 복잡한 로직에 해당하므로 애플리케이션 서비스에 로직이 그대로 드러나면 안된다.
- ORM을 사용하거나 사용할 수 없다면 도메인 객체를 생성하는 팩토리 클래스를 만들 것
- 해당 애플리케이션 서비스와 상관이 없는 로직 -> 다른 클래스로 책임이동
-
새 클래스 도입
- 어색한 기능이 포함 -> 책임에 맞는 클래스를 찾아서 해당 클래스로 이동
테스트할 코드 찾기
- 비즈니스 로직과 오케스트레이션을 완전히 분리하면 코드베이스의 어느 부분을 테스트 단위로 할지 쉽게 결정할 수 있다.
-
좌측 상단의 코드의 테스트 메서드는 비용 편의 측면에서 최상의 결과를 가져다준다.
- 코드의 복잡도나 도메인 유의성이 높은면 회귀 방지가 뛰어난고 협력자가 거의 없어 유지비도 가장 낮음
-
전제 조건 테스트
- 도메인 유의성이 있는 모든 전제 조건을 테스트할 것
- 도메인 유의성이 없는 전제 조건을 테스트하는 것은 별 가치가 없다.
컨트롤러에서 조건부 로직 처리
-
비즈니스 로직과 오케스트레이션의 분리는 다음과 같이 비즈니스 연산이 세 단계로 있을 때 가장 효과적 -> 프로세스 외부 의존성에 대한 모든 참조가 비즈니스 연산의 가장자리로 밀려났을 때
- 저장소에서 데이터 검색
- 비즈니스 로직 실행
- 데이터를 다시 저장소에 저장
-
위의 그림처럼 단계가 명확하지 않은 경우 있음 -> 그럴 경우 다음 세 가지 방법으로 해결할 수 있음
- 어쩄든 외부 의존성을 가장 자리로 밀어내기 (성능 저하)
- 도메인 모델에 프로세스 외부 의존성 주입 (테스트 유의성 저하)
- 의사 결정 프로세스 세분화 (컨트롤러 단순성 저하)
- 위의 세 가지 방법은 아래 세 가지 특성 중 항상 두 가지 특성만 만족함
- 도메인 모델 테스트 유의성 : 도메인 클래스의 협력자 수와 유형에 따른
- 컨트롤러 단순성 : 의사 결정(분기) 지점이 있는지에 따라 다름
- 성능 : 프로세스 외부 의존성에 대한 호출 수로 정의
- 1,2번 해결방법은 선택할 수 없다.
- 성능을 버릴 수 없고 (1번)
- 2번 해결방법은 대부분 코드를 지나치게 복잡한 사분면에 넣는다.
-
3번 방법의 문제 해결하기 (컨트롤러 단순성 저하)
- 컨트롤러의 복잡도가 커지는 것을 완화하는 방법
-
CanExecute/Execute -> 비즈니스 로직이 도메인 모델에서 컨트롤러로 유출되는 것을 방지하는 것
- CanExecute 메서드를 사용해 Execute 메서드의 전제 조건을 도메인 모델에서 확인한다 -> 컨트롤러로 여부를 확인하는 로직이 누출되지 않음.
- 도메인 이벤트