리팩터링할 코드 식별하기

코드의 네 가지 유형

  • 모든 프로덕션 코드는 2차원으로 분류할 수 있다.
    • 복잡도 또는 도메인 유의성
      • 복잡도: 코드 내 의사 결정 지점 수로 정의한다.
      • 도메인 유의성: 코드가 프로젝트의 문제 도메인에 대해 얼마나 의미 있는지를 나타낸다.
      • 복잡한 콤드와 도메인 유의성을 갖는 코드가 단위 테스트에서 가장 이롭다.
    • 협력자 수: 가변 의존성이거나 프로젝트 외부 의존성이다.
      • 도메인 모델이라면 프로세스 외부 협력자를 사용하면 안 된다.
      • 테스트에서 목 체계가 복잡하기 때문에 유지비가 더 든다.
  • 네 가지 코드 유형
    • 도메인 모델과 알고리즘
      • 도메인 모델 또는 복잡한 알고리즘
      • 단위 테스트 하면 가장 이롭다. 단위 테스트가 매우 가치 있고 저렴하다.
    • 간단한 코드
      • 매개변수가 없는 생성자나 한 줄 속성 등
      • 테스트할 필요가 전혀 없다. 테스트 가치가 0에 가깝다.
    • 컨트롤러
      • 도메인 크래스와 외부 애플리케이션 같은 구성 요소의 작업을 조정
      • 포괄적인 통합 테스트의 일부로서 간단히 테스트해야 한다.
    • 지나치게 복잡한 코드
      • 덩치가 큰 컨트롤러
      • 단위 테스트가 어렵지만, 테스트 커버리지 없이 내버려두는 것은 위험하다.
      • 알고리즘과 컨트롤러라는 두 부분으로 나눠서 리팩토링하는 것이 일반적이다.
      • 코드가 더 중요하거나 복잡해질수록 협력자는 더 적어야 한다.

험블 객체 패턴을 사용해 지나치게 복잡한 코드 분할하기

  • 지나치게 복잡한 코드를 쪼개려면, 험블 객체 패턴을 써야 한다.
  • 지나치게 복잡한 코드에서 테스트 가능한 부분을 추출해 이를 둘러싼 험블 객체를 만든다.
  • 험블 객체의 예시
    • 육각형 아키텍처의 애플리케이션 서비스 계층
    • 함수형 아키텍처의 가변 셸
    • MVP와MVC 패턴의 프리젠터와 컨트롤러
    • DDD에 나오는 Aggregate 패턴
  • 비즈니스 로직과 오케스트레이션을 계속 분리해야 하는 이유는 테스트 용이성이 좋아져서만이 아니다.
    • 코드 복잡도를 해결할 수 있으며, 유지 보수도 쉽게 해준다.

가치 있는 단위 테스트를 위한 리팩터링하기

  • 도메인 유의성이 높은 코드에서 프로세스 외부 협력자는 사용하면 안 된다.
  • 도메인 모델을 험블 컨트롤러에서 직접 재구성하면, 이는 복잡한 로직이므로 테스트하기 힘들어진다.
  • ORM을 사용하지 않거나 사용할 수 없으면, 원시 데이터베이스 데이터로 도메인 클래스를 인스턴스화 하는 팩토리 클래스를 작성하라.
  • 도메인 모델들의 모든 사이드 이렉트는 메모리에 남아있다는 사실로 인해 테스트 용이성이 크게 향상된다.

최적의 단위 테스트 커버리지 분석

  • 각 사분면 테스트 방법
    • 도메인모델 및 알고리즘: 단위 테스트의 비용 편익 측면에서 최상의 결과를 가져다준다.
    • 간단한 코드: 테스트는 회귀 방지가 떨어질 것이므로 테스트할 필요가 없다.
    • 지나치게 복잡한 코드: 리팩터링으로 제거한다.
    • 컨트롤러: 다음 장에서 확인
  • 메소드를 호출할 때 처음에 검증하는 전제 조건을 테스트해야 하는가?
    • 일반적으로 권장하는 지침은 도메인 유의성이 있으면 모든 전제 조건을 테스트한다.
    • 도메인 유의성이 없는 전제 조건은 테스트하는 데 시간을 들이지 말라.

컨트롤러에서 조건부 로직 처리

  • 컨트롤러 내에 비즈니스 로직과 오케스트레이션의 분리는 다음과 같이 비즈니스 연산이 세 단계로 있을 떄 가장 효과적이다.
    • 저장소에서 데이터 검색
    • 비즈니스 로직 실행
    • 데이터를 다시 저장소에 저장
  • 하지만 컨트롤러 내에는 위와 같이 명확하게 나누어지지 않는다. 이러한 상황에서는 다음과 같이 세 가지 방법이 있다.
    • 외부에 대한 모든 읽기와 쓰기를 비즈니스 연산 가장자리로 밀어내기: 컨트롤러는 계속 단순하게 하고 프로세스 외부 의존성과 도메인 모델을 분리하지만, 성능이 저하 된다.
      • 대부분의 소프트웨어 프로젝트에서는 성능이 매우 중요하므로 첫 번째 방법은 고려할 필요가 없다.
    • 도메인 모델에 프로세스 외부 의존성 주입하기: 성능을 유지하면서 컨트롤러를 단순화 하지만, 도메인 모델의 테스트 유의성이 떨어진다.
      • 대부분 코드를 지나치게 복잡한 사분면에 넣게 된다.
    • 의사 결정 프로세스 단계를 더 세분화하기: 성능과 도메인 모델 테스트 유의성에 도움을 주지만, 컨트롤러가 단순하지 않다. 이러한 세부 단계를 관리하려면 컨트롤러에 의사 결정 지점이 있어야 한다.
      • 이 방식을 쓰면 컨트롤러를 지나치게 복잡한 사분면에 더 가까워지게 된다. 하지만 이 문제를 완화할 수 있는 방법이 있다.

CanExecute/Execute 패턴 사용

  • CanExecute/Execute 패턴을 사용해, 성능 저하를 감수하지 않으면서 비즈니스 로직을 컨트롤러로 유출되는 것을 방지할 수 있다.
public string CanChangeEmail()
{
	if(IsEmailConfirmed)
		return "Can't change a confirmed email";
	return null;
}

public void ChangeEmail(string newEmail, Company company)
{
	Precondition.Requires(CanChangeEmail() == null);
	// 메서드의 나머지 부분
}

도메인 이벤트를 사용해 도메인 모델 변경 사항 추적

  • 도메인 이벤트는 컨트롤러에서 의사 결정 책임을 제거하고 해당 책임을 도메인 모델에 적용함으로써 외부 시스템과의 통신에 대한 단위 테스트를 간결하게 한다.
  • 컨트롤러를 검증하고 프로세스 외부 의존성을 목으로 대체하는 대신, 단위 테스트에서 직접 도메인 이벤트 생성을 테스트할 수 있다.

결론

  • 도메인 이벤트와 CanExecute/Execute 패턴을 사용해 도메인 모델에 모든 의사결정을 잘 담을 수 있었지만, 항상 그렇게 할 수는 없다.
    • 컨트롤러에 비즈니스 로직이 있는 것을 피할 수 없는 상황이 있다.
  • 도메인 클래스에 모든 협력자를 제거할 수 있는 경우는 거의 없다.
    • 그러나 협력자와의 상호 작용을 검증하려고 목을 사용하지는 말라.
    • 같은 연산내 인전 도메인 클래스에 대해 수행하는 후속 호출은 모두 구현 세부 사항이다.
  • 식별할 수 있는 동작의 기준은 두 가지 기준 중 하나를 충족해야 한다.
    • 클라이언트 목표 중 하나에 직접적인 연관이 있음
    • 외부 애플리케이션에서 볼 수 있는 프로세스 외부 의존성에서 사이드 이펙트가 발생함