박종훈 기술블로그

6장 단위 테스트 스타일 – 함수형 아키텍처의 단점 이해 + 6장 요약

단위테스트 (블라디미르 코리코프)


아래 내용에서 이어지는 글입니다.

6장 단위 테스트 스타일
6장 단위 테스트 스타일 - 스타일 비교
6장 단위 테스트 스타일 - 함수형 아키텍처
6장 단위 테스트 스타일 – 함수형 아키텍처와 출력 기반 테스트로의 전환


안타깝게도 항상 함수형 아키텍처를 이룰 수 있는 것은 아니다. 또한 함수형 아키텍처라고해도, 코드베이스가 커지고 성능에 영향을 미치면서 유지 보수성의 이점이 상쇄된다.

5.1 함수형 아키텍처 적용 가능성

감사 시스템은 결정을 내리기 전에 입력을 모두 미리 수집할 수 있으므로 함수형 아키텍처가 잘 동작했다.

그러나 종종 실행 흐름이 간단하지 않을 수 있다. 의사 결정 절차의 중간 결과에 따라 프로세스 외부 의존성에서 추가 데이터를 질의해야한다면 어떨까?

예를 들어보자.

지난 24시간 동안 방문 횟수가 임계치를 초과하면 감사 시스템이 방문자의 접근 레벨을 확인해야 한다고 해보자. 그리고 방문자의 접근 레벨이 데이터베이스에 저장돼 있다고 가정하자.

다음과 같이 IDatabase 인스턴스를 AuditManager에 전달할 수 있다 상각해보자

public FileUpdate AddRecord(
    FileContent[] files, string visitorName,
    DataTime timeOfVisit, IDatabase database
)

그러면 이 인스턴스는 AddRecord() 메서드에 숨은 입력이 생겼다. 따라서 이 메서드는 수학적 함수가 될 수 없으며 더 이상 출력 기반 테스트를 적용할 수 없다.

 이러한 상황에는 두 가지 해결책이 있다.

- 애플리케이션 서비스 전면에서 디렉터리 내용과 더불어 방문자 접근 레벨을 수집할 수 있게 한다.
- AuditManager에서 isAccessLevelCheckRequired()와 같은 새로운 메소드를 둔다. 애플리케이션 서비스에서 AddRecord() 전에 이 메서드를 호출하고, true를 반환하면 서비스는 데이터베이스에서 접근 레벨을 가져온 후 AddRecord()에 전달한다.

두 방법 모두 단점은 있다. 첫 번째 방법은 성능이 저하된다. 접근 레벨이 필요 없는 경우에도 무조건 데이터베이스를 질의해야 한다. 그러나 이 방법은 비즈니스 로직과 외부 시스템과의 통신을 완전히 계속 분리할 수 있다.

두 번째 방법은 성능 향상을 위해 분리를 다소 완화한다. 데이터베이스를 호출할지에 대한 결정은 AuditManager가 아니라 애플리케이션 서비스로 넘어간다.

도메인 모델(AuditManager)이 데이터베이스에 의존하는 것은 좋은 생각이 아니다.

이후 장들에서는 관심사 분리와 성능 간의 균형을 지키는 것에 대해 자세히 알아볼 것이다.

5.2 성능 단점

시스템 전체에 영향을 미치는 성능은 함수형 아키텍처의 흔한 논쟁이다.
문제가 되는것은 테스트의 성능이 아니다. 시스템의 성능이다.

시스템은 프로세스 외부 의존성을 더 많이 호출하게 되었고, 그 결과로 성능은 떨어진다.

예를들어 이전 버전에서는 작업 디렉터리의 모든 파일을 읽지는 않았다.
그러나 최종 버전은 읽고-결정하고-쓰기(read-decide-act) 방식을 따르도록 작업 디렉터리의 모든 파일을 읽었다.

함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성(제품 코드와 테스트 코드 모두) 같의 절충이다.
성능 영향이 그다지 눈에 띄지 않는 부분이라면 함수형 아키텍처를 사용해 유지 보수성을 향상시키는 편이 낫다. 그와 반대로 선택해야 할 수도 있다.
상황에 따라 선택해야 한다.

5.3 코드베이스 크기 증가

함수형 아키텍처는 함수형 코어와 가변 셸 사이를 명확하게 분리해야 한다. 궁극적으로 코드 복잡도가 낮아지고 유지 보수성이 향상되지만, 초기에 코딩이 더 필요하다.

모든 프로젝트가 초기 투자가 타당할 만큼 복잡도가 높은것은 아니다.
시스템의 목잡도와 중요성을 고려해 함수형 아키텍처를 전략적으로 적용해야 한다.

함수형 방식에서 순수성에 많은 비용이 든다면 순수성을 따르지 않는게 좋다.

대부분의 프로젝트에서는 모든 도메인 모델을 불변으로 할 수 없기 때문에 출력 기반 테스트에만 의존할 수 없다.

대부분의 경우 출력 기반 스타일과 상태 기반 스타일을 조합하게 되며, 통신 기반 스타일을 약간 섞어도 괜찮다.

모든 테스트를 출력 기반 스타일로 전화하는 것이 아니라 가능한 많은 테스트를 전화하는 것을 목표로 하자. 그 차이는 미미하지만 중요하다.

요약

- 출력 기반 테스트는 SUT에 입력을 주고 출력을 확인하는 테스트 스타일이다. 이 테스트 스타일은 숨은 입출력이 없다고 가정하고, SUT 작업의 결과는 반환하는 값뿐이다.

- 상태 기반 테스트는 작업이 완료된 후의 시스템 상태를 확인한다.

- 통신 기반 테스트는 목을 사용해서 테스트 대상 시스템과 협력자 간의 통신을 검증한다.

- 단위 테스트의 고전파는 통신 기반 스타일보다 상태 기반 스타일을 선호한다. 런던파는 반대를 선호한다. 두 분파 모두 출력 기반 테스트를 사용한다.

- 출력 기반 테스트가 테스트 품질이 가장 좋다. 이러한 테스트는 구현 세부 사항에 거의 결합되지 않으므로 리팩터링 내성이 있다. 또한 작고 간결하므로 유지 보수 하기도 쉽다.

- 상태 기반 테스트는 안정성을 위해 더 신중해야 한다. 단위 테스트를 하려면 비공개 상태를 노출하지 않도록 해야 한다. 상태 기반 테스트는 출력 기반 테스트보다 크기가 큰 편이므로 유지 보수가 쉽지 않다. 헬퍼 메서드와 값 객체를 사용해 유지 보수성 문제를 완화할 수도 있지만 제거할 수는 없다.

- 통신 기반 테스트도 안정성을 위해 더 신중해야 한다. 애플리케이션 경계를 넘어서 외부 환경에 사이드 이펙트가 보이는 통신만 확인해야 한다. 통신 기반 테스트의 유지 보수성은 다른 두 테스트에 비해 좋지 않다. 목은 공간을 많이 차지하는 경향이 있어 테스트 가독성이 떨어진다.

- 함수형 프로그래밍은 수학적 함수로 된 프로그래밍이다.

- 수학적 함수는 숨은 입출력이 없는 함수(또는 메서드)다. 사이드 이펙트와 예외가 숨은 출력에 해당한다. 내부 상태 또는 외부 상태에 대한 참조는 숨은 입력이다. 수학적 함수는 명시적이므로 테스트 용이성을 상당히 높인다.

- 함수형 프로그래밍의 목표는 비즈니스 로직과 사이드 이펙트를 분리하는 것이다.

- 함수형 아키텍처는 사이드 이펙트를 비즈니스 연산의 가장자리로 밀어내 분리를 이루는 데 도움이 된다. 이 방법으로, 사이드 이펙트를 다루는 코드를 최소화하면서 순수 함수 방식으로 작성된 코드의 양을 최대화 할 수 있다.

- 함수형 아키텍처는 모든 코드를 함수형 코어와 가변 셸이라는 두 가지 범주로 나눈다. 가변 셸은 입력 데이터를 함수형 코어에 공급하고, 코어가 내린 결정을 사이드 이펙트로 반환한다.

- 함수형 아키텍처와 육각형 아키텍처의 차이는 사이드 이펙트의 처리에 있다. 함수형 아키텍처는 모든 사이드 이펙트를 도메인 계층 밖으로 밀어낸다. 이와 반대로, 육각형 아키텍처는 도메인 계층에만 한정돼 있는 한은 도메인 계층에 의해 만들어진 사이드 이펙트도 괜찮다. 극단적으로 함수형 아키텍처는 육각형 아키텍처다.

- 함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성 사이의 절충이며, 함수형 아키텍처는 유지 보수성 향상을 위해 성능을 희생한다.

- 모든 코드 베이스를 함수형 아키텍처로 전환할 수는 없다. 함수형 아키텍처를 전략적으로 적용하라. 시스템의 복잡도와 중요성을 고려하라. 코드베이스가 단순하거나 그렇게 중요하지 않으면, 함수형 아키텍처에 필요한 초기 투자는 별 효과가 없다.

fin.