Junuuu 2023. 7. 8. 00:01

7장에서 다루는 내용

  • 네 가지 코드 유형 알아보기
  • 험블 객체 패턴 이해
  • 가치 있는 테스트 작성

 

리팩터링이 필요한 이유

기반 코드를 리팩터링 하지 않고서는 테스트 코드를 크게 개선할 수 없습니다.

 

코드의 네 가지 유형

  • 도메인 모델 및 알고리즘 : 노력대비 가장 이로운 테스트
  • 지나치게 복잡한 코드 : 한 가지 예로 덩치가 큰 컨트롤러로 모든 작업을 스스로 하는 컨트롤러
  • 컨트롤러
  • 간단한 코드

지나치게 복잡한 코드가 가장 문제가 되며 단위 테스트하기 어렵지만 테스트를 하지 않는 것은 너무 위험합니다.

지나치게 복잡한 코드의 경우 도메인 모델 및 알고리즘, 또는 컨트롤러로 나누어 리팩터링 해야 합니다.

가장 이상적으로는 지나치게 복잡한 코드가 존재하면 안 됩니다.

 

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

지나치게 복잡한 코드를 쪼개려면, 험블 객체 패턴을 사용해야 합니다.

험블객체란 테스트하기 어려운 코드와 쉬운 코드를 분리하는 패턴을 말합니다.

 

 

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

다음과 같은 요구사항을 가진 애플리케이션 리팩터링

  • 사용자 이메일이 회사 도메인에 속한 경우 직원으로 표시한다.
  • 회사의 직원수를 추적하며 사용자 유형이 직원 -> 고객, 고객 -> 직원으로 변경되면 이 숫자도 변한다.
  • 이메일이 변경되면 외부 시스템에 알려야 한다.

 

기존의 ChangeEmail 메서드의 책임

  • DB에 사용자의 이메일과 유형 검색
  • DB에서 조직의 도메인 이름과 직원 수 검색
  • 새 이메일의 도메인 이름에 따라 사용자 유형 설정
  • 필요한 경우 조직의 직원 수 업데이트
  • 데이터베이스에 사용자 저장
  • 메시지 버스에 알림 전송

위의 메서드는 핵심 비즈니스 로직으로 복잡도, 도메인 유의성 측면에서 점수가 높습니다.

명시적인 의존성은 userId와 newEmail 인자이며, 암시적인 의존성은 Database와 MessageBus입니다.

도메인 유의성이 높은 코드에서 외부 협력자(암시적 의존성)를 사용하면 안 됩니다.

 

1단계: 암시적 의존성을 명시적으로 만들기

데이터베이스와 메시지 버스에 대한 인터페이스를 두고, 이 인터페이스를 User에 주입한 후 테스트에서 목으로 처리한다.

하지만 도메인 모델은 직접적이든 간접적이든 외부 협력자에게 의존하지 않는 것이 더 깔끔하다.

 

2단계: 애플리케이션 서비스 계층 도입

도메인 모델이 외부 시스템과 직접 통신하는 문제를 극복하려면 Service 계층을 도입하여 도메인로직(테스트하기 쉬운 코드)과 외부 협력자(테스트하기 어려운 코드)를 분리합니다.

 

하지만 여전히 통합 테스트에서 문제가 될 수 있습니다.

하지만 도메인 클래스는 이제 테스트하기 매우 쉬워졌습니다.

 

3단계: 애플리케이션 서비스 복잡도 낮추기

User를 생성하는 부분은 팩토리 클래스로 추출하여 코드의 복잡도를 낮추고 테스트하기 쉬운 코드로 변환한다.

도메인 유의성은 없지만 유틸리티 코드의 예입니다.

하지만 분기점이 있으며, 코드에서 사용하는 기본 라이브러리에도 숨은 분기점이 존재하기 때문에 테스트해 볼 만합니다.

 

4단계: 새 Company 클래스 소개

User에서 업데이트된 직원 수를 반환하는 부분이 어색합니다.

이를 위해 회사 관련 로직을 데이터와 함께 묶는 Compnay 클래스를 도입합니다.

ChangeNumberOfEmployees(), IsEmailCorporate()라는 2가지 메서드가 존재하고 이제 캡슐화가 잘된 모습입니다.

마찬가지도 Company 클래스도 팩토리 클래스로 추출하여 생성할 수 있습니다.

 

이런 과정들을 보면서 든 생각에 객체들 관의 협력을 적절하게 잘 정의하는 것이 테스트하기 쉬운 구조로 가는 길이라는 생각이 듭니다.

 

우리가 하지 말아야 할 방법

우리가 해야 하는 방법

외부 의존성이 비즈니스 연산의 가장자리로 밀려났을때 가장 효과적

물론 이런 방법을 사용했을 때 성능 저하를 감수해야 할 수도 있다.

읽기 1에서 데이터를 검증하고 필요 없는 경우 읽기 2가 필요 없어질 수 있을 수 있습니다.

하지만 우리가 원하는 방법은 읽기 1, 읽기 2를 초기에 모두 읽어옵니다.

 

이를 방지하기 위해서는 읽기 2를 하기 전 분기문을 통해 해결할 수 있습니다.

하지만 도메인 모듈의 캡슐화가 떨어지고 비즈니스 로직과 컨트롤러가 결합됩니다.

 

이런 파편화를 방지하기 위해서는 User에 CanChangeEmail 메서드를 두어 이메일 변경의 전제조건으로 합니다.

이를 CanExecute/Execute 패턴이라 부릅니다.

 

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

도메인 이벤트는 시스템에서 발생하는 중요한 변경 사항을 외부 애플리케이션에 알리는 데 사용합니다.

도메인 이벤트는 컨트롤러에서 의사 결정 책임을 제거하고 해당 책임을 도메인 모델에 적용함으로써 외부 시스템과의 통신에 대한 단위 테스트를 간결하게 합니다.

 

 

개인적인 생각: Java Spring

Spring을 사용하다 보면 이런 험블객체패턴을 다루기가 생각보다 쉬워질 수 있습니다.

예를 들어 외부의존성들은 Spring Event를 발행하고, 기존의 Transaction이 완료되면 실행할 수 있습니다.

 

예를 들어 사용자가 회원가입을 완료하면 웰컴 이메일을 보낼 수 있습니다.

이때 회원가입에 이메일을 전송하는 외부의존성을 넣기보다 이벤트를 발행하여 EventListener에서 처리하면 의존성도 약하게 가져가며 테스트하기 쉬워지고 기존 로직에 영향을 주지 않는 코드를 만들어낼 수 있습니다.