정글에서 온 개발자

1/11 TIL 가치 있는 단위 테스트를 위한 리팩토링 본문

TIL

1/11 TIL 가치 있는 단위 테스트를 위한 리팩토링

dev-diver 2025. 1. 12. 16:09

오늘의 팁

추상화할 것(구현체)을 테스트하기보다 추상화를 테스트하는 것이 더 쉽다.

  • 도메인 이벤트는 프로세싀 외부 의존성 호출 위의 추상화
  • 도메인 클래스의 변경은 데이터 저장소의 향후수정에 대한 추상화

컨트롤러에 비즈니스 로직이 있는 것을 완전히 피할 수 없다.
도메인 클래스에서 모든 협력자를 완전헤 제거할 수 있는 경우도 거의 없다.
    그러나 최소한 외부 프로세스는 참조하지 않게 하자.
    그리고 남은 협력자들과의 호출은 구현 세부 사항이다 -> 상호작용을 검증하지 말자

어떤 협력자와 어떻게 소통해서 결과를 내는지는 검증하지 말자.
그러나 해당 협력자의 소통 메서드는 검증할 수 있다. (더 아래 레이어로 내려가서)

세부사항인지, 식별가능인지의 구현은 클라이언트가 누구냐에 달려있다.
양파껍질처럼 계층을 바꾸면서 클라이언트와 대상 메서드를 바꾸자


테스트 코드를 개선하려면 리팩토링이 필요하다.

코드의 4가지 유형

2가지 기준

  1. 복잡도 혹은 도메인 유의성
  2. 협력자 수
    • 가변 의존성
    • 프로세스 외부 의존성
      • 관리 의존성 - ex. 해당 서비스만 접근할 수 있는 데이터베이스
      • 비관리 의존성 - 외부 API
    • 불변 의존성 - 값 , 또는 객체값 -> 협력자 수로 세지 않는다.
    • 암시적 협력자 VS 명시적 협력자

4가지 유형

  1. 도메인 모델 및 알고리즘 - 단위 테스트 가치 높음
  2. 간단한 코드 - 테스트 안해도 됨  (생성자, 한줄 코드 등)
  3. 컨트롤러 - 도메인 클래스와 외부 어플리케이션의 구성요소 작업 조정(오케스트레이션) -> 통합테스트
  4. 지나치게 복잡한 코드 - 1이나 3으로 분리가 필요함 (험블 객체 패턴 이용)

코드 유형의 4분면

리팩토링 순서 예

  1. 암시적 의존성을 명시적으로 만들기
  2. 애프리케이션 서비스 계층 도입 - 험블 컨트롤러로 통신 책임 옮김
  3. 애플리케이션 서비스 복잡도 낮추기 - 외부 의존성과의 통신을 위한 재구성 로직을 분리한다.
    • ORM이나 Factiory 클래스를 이용한다.
  4. 객체 간의 역할(책임)을 다시 리팩토링 한다. (묻지 말고 말하라)
    • 이 과정에서 책임을 나눈 협력자가 추가되어 테스트가 조금 어려워 질 수는 있다.
    • 마지막 순서까지 모든 부작용이 메모리에 남아있게 한다 -> 테스트 용이성 향상

리팩토링 후의 테스트

전제조건 테스트 : 도메인 유의성이 있는 모든 전제조건은 테스트, 도메인 유의성 없으면 테스트 안함


외부 프로세스가 중간 결과로 작용하는 경우는 어떻게 할까?

고민이 필요하다. 성능을 포기하고 모든 읽기와 쓰기를 가장자리로 밀어낼 수도 있다. (로직과 상관 없이 조회를 여러번 하게)
그렇지 않으면 도메인 모델에 프로세스 외부 의존성을 주입할 수 있는데, 이건 원하는 바가 아니다.

마지막으로 의사 결정 프로세스 단계를 세분화 하는 방법이 있다.
컨트롤러의 단순성이 떨어지지만 보완하는 방법을 사용한다.

  • CanExecute/Execute 패턴
  • 도메인 이벤트 사용

의사 결정 프로세스 단계 세분화하기가 우리의 선택

CanExecute/Execute 패턴

의사 결정하는 메서드를 도메인에 추가하고, 해당 의사결정 메서드를 컨트롤러의 의사결정 분기점에 한번 쓴다.
이 메서드는 의사결정 뒤에  추가 로직을 실행할 때 전제조건으로 한번 더 작용한다.

class Controller{
public :
    컨트롤러메소드(){

        //첫번째 데이터 로드
        object[] data1 = db.getData1();
        Model mode = ModelFactory.Create(data1);

        if(!model.의사결정()) //전제조건을 미리 확인함으로써 쓸데없는 두번째 데이터로드를 피함
            return "다음 실행 안함";

        //두번째 데이터 로드
        object[] data2 = db.getData2();

        model.식별동작(data2);
    }
}

class Model{
public :
	의사결정(){
    }
    
    식별동작(object[] data){
    	if(!의사결정()){  //여기서도 전제조건을 확인
        	return;
        }
    }

private:
	object[] modelData;
}

이렇게 하면 Controller의 의사결정은 테스트 할 필요 없다.  Model의 전제 조건만 단위 테스트 하면 충분하다.

이 부분은 책 읽을 때 반영 코드가 안 나와있어 이해가 조금 어려웠다.

도메인 이벤트 이용

모델에 모두 캡슐화를 하면서도, 컨트롤러가 모델의 중간 상황을 외부에 알리고 싶을 때 사용할 수 있다. 

  1. 도메인 이벤트 클래스(모델의 변경사항 추적)를 하나 만들고
  2. 모델에서 이벤트 클래스 컬렉션을 만든다.
  3. 컨트롤러는 해당 도메인 이벤트를 돌면서 외부 프로세스와 통신한다.

이걸 쓰지 않으면,
외부 프로세스와의 통신 여부 판단 로직이 컨트롤러로 가게 된다.
또는 외부 의존성을 모델에 안 넘기기에는 너무 복잡한 상황이 생길 수도 있다.

더욱 발전시키면 이벤트 디스패처를 작성할 수도 있다.


내 생각

테스트 코드가 있어야 리팩토링이 가능하다고만 알고 있었는데, 테스트 코드 자체를 개선하기 위해서도 리팩토링은 필요했다.

안티 패턴도 패턴의 이름이 붙는다는 걸 알았다. 유명한 안티패턴도 공부가 필요하다는 생각이 들었다.

CanExecute/Execute 패턴을 도입하기 전에 SQL단에서 조회를 한번에 하게 작성할 수 있는 경우도 있는데, 이럴 때는 어떻게 해야할지 궁금하다.

참조

발송 전 도메인 이벤트 병합


번역 수정 필요

번역본 p259.

그러나 애플리케이션이 프로세스 외부 의존성을 도메인 모델로 넘기지 않고 해당 의존성을 불필요하게 호출해서 도메인 모델을 오히려 지나치게 복잡하게 하는 것과 같이 더 어려운 상황이 될 수 있다.

원문 p176.

But you may find yourself in a more difficult situation in which it’s hard to prevent your application from making unnecessary calls to out-of-process dependencies without passing those dependencies to the domain model, thus over- complicating that domain model.

내가 생각한 번역.

하지만 도메인 모델에 외부 프로세스 의존성을 전달(즉, 도메인 모델을 복잡하게 만드는)하지 않고는, 어플리케이션의 해당 의존성의 불필요한 호출을 방지하기 어려운 상황에 처할 수 있습니다.

영어 문장 자체가 복잡해서 문장을 찢지 않고는 한국말로 번역하기 어려운 것 같다.

'TIL' 카테고리의 다른 글

1/13 TIL 목 처리  (0) 2025.01.13
1/12 TIL 통합테스트  (1) 2025.01.12
1/9 TIL 테스트, 리팩토링, 볼링게임  (0) 2025.01.09
1/8 TIL [CKA] 기본 개념  (1) 2025.01.09
1/7 TIL 실서비스 request body 캡쳐  (0) 2025.01.08