////
Search
Duplicate

6. 단위 테스트 스타일

6장에서 다루는 내용
단위 테스트 스타일 비교
함수형 아키텍처와 헥사고날 아키텍처의 관계
출력 기반 테스트로 전환
이 장에서는 단위 테스트 스타일을 비교해본다.
일반적으로 출력 기반이 가장 좋고 상태 기반는 두 번째로 좋은 선택이며 통신 기반은 간헐적으로만 사용해야 한다.
출력 기반 테스트 스타일은 순수 함수 방식으로 작성된 코드에만 적용할 수 있다. 이를 위해 함수형 프로그래밍 원칙을 이용해 기반 코드가 함수형 아키텍처를 지향하게끔 재구성해야 한다.
이 장을 마쳤을 때, 함수형 프로그래밍이 출력 기반 테스트와 어떻게 관련되어 있는지 이해할 수 있어야 한다.
또한 출력 기반 스타일을 사용해 테스트를 작성하는 방법뿐만 아니라 함수형 프로그래밍과 함수형 아키텍처가 지닌 한계도 배운다.

1. 단위 테스트의 세 가지 스타일

다음 세 스타일이 존재한다.
출력 기반 테스트
상태 기반 테스트
통신 기반 테스트
1.
출력 기반 테스트 정의
테스트 대상 시스템에 입력을 넣고 생성되는 출력을 검증하는 방식이다.
이러한 단위 테스트 스타일은 전역 상태나 내부 상태를 변경하지 않는 즉 사이드 이펙트가 없고 코드에만 적용되므로 반환 값만 검증하면 된다.
이는 함수형이라고도 한다. 이 이름은 사이드 이펙트 없는 코드 선호를 강조하는 프로그래밍 방식인 함수형 프로그래밍에 뿌리를 두고 있다.
다음 코드가 그 예시다.
public class PriceCalculator { public double calculateDiscount(int price) { return price * 0.99; } } @Test void test() { // arrange int price = 1000; PriceCalculator sut = new PriceCalculator(); // action double result = sut.normalizeNameLength(price); // assert assertThat(result).isEqualTo(price * 0.99); }
Java
복사
2.
상태 기반 스타일의 정의
상태 기반 스타일은 작업이 완료된 후 시스템의 상태를 확인하는 것이다.
이 테스트 스타일에서 상태라는 용어는 SUT나 협력자 중 하나 또는 데이터베이스나 파일 시스템 등과 같은 프로세스 외부 의존성의 상태를 의미한다.
다음 코드가 그 예시다.
public class Shop { public double addProduct(Product product) { ... } } @Test void test() { // arrange Product banana = new Banana(); Shop sut = new Shop(); // action sut.addProduct(banana); // assert assertThat(sut.getProductCount()).isEqualTo(1); }
Java
복사
3.
통신 기반 스타일의 정의
이 스타일은 목을 사용해 테스트 대상 시스템과 협력자 간의 통신을 검증한다.
다음 코드가 그 예시다.
public void sendGreetingEmail() { // arrange EmailSender emailSender = Mockito.mock(EmailSender.class); UserService sut = new UserService(emailSender); // action sut.greetUser("test@test.test"); // assert Mockito.verify(emailSender, Mockito.times(1)).send(); }
Java
복사
고전파는 통신 기반 스타일보다 상태 기반 스타일을 더 선호한다. 런던파는 이와 반대로 선택한다. 두 분파는 출력 기반 테스트를 사용한다.

2. 단위 테스트 스타일 비교

출력, 상태, 통신 기반 단위 테스트 스타일은 낯설지 않다. 이제 4장에서 학습한 좋은 단위 테스트의 4대 요소로 서로 비교해보겠다.
회귀 방지, 리팩토링 내성, 빠른 피드백, 유지보수성
1.
회귀 방지, 피드백 속도 지표로 스타일 비교하기
회귀 방지
실행되는 코드의 양이 많든 적든 테스트를 작성하기엔 용이하다. 코드 복잡도, 도메인 유의성도 마찬가지다.
따라서 단위 테스트의 스타일은 회귀 방지 지표에 큰 영향을 끼치지 않는다.
피드백 속도
피드백 속도 역시 관련이 없다.
프로세스 외부 의존성과 충분히 떨어져서 테스트된다면 테스트 스타일은 거의 동일한 피드백 속도를 제공하기 때문이다.
2.
리팩토링 내성 지표로 스타일 비교하기
리팩토링 내성은 리팩토링 중에 발생하는 거짓 양성 수에 대한 척도다. 결국 식별할 수 있는 동작이 아닌 코드의 구현 세부 사항에 결합된 테스트의 결과다.
출력 기반은 거짓 양성 방지가 가장 우수하다.
테스트가 테스트 대상 메소드에만 결합되기 때문이다.
이런 테스트가 구현 세부 사항에 결합하는 경우는 테스트 대상 메소드가 구현 세부 사항일 때 뿐이다.
상태 기반은 일반적으로 거짓 양성을 유발하기 쉽다.
이러한 테스트는 테스트 대상 메소드 외에도 클래스 상태와 함께 동작한다.
테스트와 제품 코드 간의 결합도가 클수록 유출된 구현 세부 사항에 테스트가 얽매여있을 가능성이 더 높다.
상태 기반 테스트는 API 노출 영역(getter 등)에 의존하므로 구현 세부 사항과 결합할 가능성도 더 높다.
통신 기반은 거짓 양성에 가장 취약하다.
테스트 더블로 상호 작용을 확인하는 테스트는 대부분 깨지기 쉽기 때문이다.
이는 항상 스텁과 상호작용하는 경우로 이러한 상호 작용을 테스트해서는 안 된다. 스텁은 동작이 무언가를 하기위한 값을 제공만 해야한다.
이것이 괜찮은 경우는 해당 애플리케이션에 접근할 수 있는 방법이 내부 로직뿐만이 아닐때다.
3.
유지 보수성 지표로 스타일 비교하기
유지 보수성 지표는 단위 테스트 스타일과 밀접한 관련이 있다. 더욱이 지표를 개선할 방법도 별로 많지 않다.
유지 보수성은 단위 테스트의 유지비를 측정하며 다음 두 가지 특성으로 정의한다.
테스트를 이해하기에 얼마나 어려운가?
테스트가 크면 필요할 때 이해하기도 변경하기도 어려우므로 유지 보수가 쉽지 않다.
테스트를 실행하기에 얼마나 어려운가?
마찬가지로 하나 이상의 프로세스 외부 의존성과 직접 작동하는 테스트는 데이터베이스 세팅, 네트워크 연결 등으로 운영할 때 비용이 발생한다.
출력 기반의 테스트 유지 보수성
다른 두 가지 스타일에 비해 출력 기반 테스트가 가장 유지 보수하기 용이하다. 거의 짧고 간결하기 때문이다.
이러한 이점은 메소드로 입력을 공급하는 것과 해당 출력을 검증하기 때문이다. 단 몇 줄로 이 두가지를 수행할 수 있다. 기억하자 일반적으로 코드는 짧을수록 이해하기 쉽다.
출력 기반 테스트의 기반 코드는 전역 상태나 내부 상태를 변경할 리가 없으므로 프로세스 외부 의존성을 다루지 않는다. 따라서 실행하기도 쉽다.
상태 기반의 테스트 유지 보수성
상태 기반 테스트는 일반적으로 출력 기반 테스트보다 유지 보수성이 떨어진다. 이는 상태 검증이 종종 출력 검증보다 더 많은 코드 라인을 필요로 하기 때문이다.
public void addCommentToArticle() { // arange Article sut = new Article(); String text = "Comment text"; String author = "John Doe"; DateTime now = new DateTime(2019, 4, 1); // action sut.AddComment(text, author, now); // assert Assert.Equal(1, sut.Comments.Count); Assert.Equal(text, sut.Comments[0].Text); Assert.Equal(author, sut.Comments[0].Author); Assert.Equal(now, sut.Comments[0].DateCreated); }
Java
복사
public void addCommentToArticle() { // arange Article sut = new Article(); String text = "Comment text"; String author = "John Doe"; DateTime now = new DateTime(2019, 4, 1); // action sut.AddComment(text, author, now); // assert sut.ShouldContainNumberOfComments(1).WithComment(text, author, now); }
Java
복사
이처럼 헬퍼 메소드를 이용해서 문제를 완화할 수 있지만 이것도 비용이다.
상태 기반 테스트를 단축하는 또 다른 방법으로 검증 대상 클래스의 동등 멤버를 정의해 비교할 수 있다.
이는 강력한 기술이지만 본질적으로 클래스가 값에 해당하고 값 객체로 변환할 수 있을 때만 효과적이다. 그렇지 않으면 코드 오염으로 이어진다.
보다시피 이 두 가지 기법은 양날의 검이므로 가끔만 적용할 수 있다. 그리고 이를 적용하더라도 상태 기반 테스트는 출력 기반 테스트보다 공간을 많이 차지하므로 유지 보수성이 떨어진다.
통신 기반의 테스트 유지 보수성
통신 기반 테스트는 유지 보수성 지표에서 상태 기반 테스트보다도 낮다.
통신 기반 테스트은 테스트 더블과 상호 작용 검증을 설정해야 하며 이는 공간을 많이 차지한다. 더불어 목이 사슬 형태로 있을 때 테스트는 더 커지고 유지 보수가 어려워진다.
4.
스타일 비교하기: 결론
비교 결과는 다음과 같다.
일반적으로 출력 기반 테스트가 좋은 단위 테스트의 특성을 만족한다.
이 스타일은 구현 세부 사항과 거의 결합되지 않으므로 리팩토링 내성을 적절히 유지하고자 주의를 많이 기울일 필요가 없다.
이러한 테스트는 간결하고 프로세스 외부 의존성이 없기 때문에 유지 보수도 쉽다.
출력 기반
상태 기반
통신 기반
회귀 방지
-
-
-
피드백 속도
-
-
-
리팩토링 내성을 지키기 위한 노력
낮음
중간
중간
유지 비용
낮음
중간
높음
상태, 통신 기반 테스트는 두 지표 모두 좋지 않다. 공개 API면서 구현 세부 사항인 메소드에 결합할 가능성이 높고 크기도 커서 유지 비용이 많이 든다.
따라서 항상 출력 기반 테스트를 선호하라.
하지만 출력 기반 테스트를 작성하기는 어렵다. 함수형으로 작성된 코드에만 적용할 수 있고 대부분의 객체지향 프로그래밍 언어에는 해당하지 않기 때문이다.
그럼에도 테스트를 출력 기반 테스트로 변경하는 기법이 있다. 코드를 순수 함수로 만들면 상태, 통신 기반 테스트 대신 출력 기반 테스트가 가능해진다.

3. 함수형 아키텍처 이해

이 절에서는 바꾸는 기법을 이해하기 위해 함수형 프로그래밍과 함수형 아키텍처가 무엇인지 알아보고 헥사고날 아키텍처와 어떤 관련이 있는지 알아본다.
1.
함수형 프로그래밍이란?
출력 기반은 함수형이라고도 한다. 기반 제품 코드를 함수형 프로그래밍을 이용해 순수 함수 방식으로 작성해야 하기 때문이다.
함수형 프로그래밍은 수학적 함수를 사용한 프로그래밍이다. 수학적 함수란 다음과 같은 특징을 가진다.
수학적 함수는 숨은 입출력이 없는 함수다.
수학적 함수의 모든 입출력은 메소드 시그니처에 명시해야 한다.
수학적 함수는 호출 횟수에 상관없이 주어진 입력에 대해 동일한 출력을 생성하는 즉 멱등성을 유지해야 한다.
public double calculateDiscount(int price) { return price * 0.99; }
Java
복사
이 메소드는 하나의 입력과 하나의 출력이 있으며 둘 다 메소드 시그니처에 명시되어 있다. 이로써 calcuateDiscount()는 수학적 함수가 된다.
숨은 입출력이 없는 메소드는 수학에서 말하는 함수의 정의를 준수하기 때문에 수학적 함수라고 한다.
수학에서의 함수는 x 집합의 각 요소에 대해 y 집합에서 정확히 하나의 요소를 찾는 두 집합 사이의 관계다.
입출력을 명시한 수학적 함수는 이에 따르는 테스트가 간결하며 이해하고 유지 보수하기 쉬우므로 테스트하기가 매우 쉽다.
출력 기반 테스트를 적용할 수 있는 메소드 유형은 수학적 함수뿐이다.
반면에 숨은 입출력은 코드를 테스트하기 어렵게 만든다. 숨은 입출력의 유형은 다음과 같다.
사이드 이펙트 : 사이드 이펙트는 메소드 시그니처에 표시되지 않은 출력이며 따라서 숨어있다. 연산은 인스턴스의 상태를 변경하고 파일 시스템을 조작하는 등 사이드 이펙트를 발생시킨다.
예외 : 메소드가 예외를 던지면 메소드 시그니처에 명시된 리턴 값을 우회하는 경로를 타게 된다. 호출된 예외는 호출 스택의 어느 곳에서도 발생하므로 메소드 시그니처가 전달하지 않는 출력을 추가한다.
내외부 상태에 대한 참조 : DateTime.now()와 같이 static을 통해 날짜와 시간을 가져올 수도 있다. 데이터베이스에 질의할 수 있고 비공개 변경 가능 필드를 참조할 수도 있다. 모두 메소드 시그니처에 언급되지 않은 실행 흐름에 대한 입력이며 따라서 숨어있다.
메소드가 수학적 함수인지 판단하는 가장 좋은 방법은 프로그램의 동작을 변경하지 않고 해당 메소드에 대한 호출을 반환 값으로 대체할 수 있는지 확인하는 것이다.
메소드 호출을 해당 값으로 바꾸는 것을 참조 투명성이라고 한다. 다음이 그 예다.
public int increment(int x) { return x + 1; }
Java
복사
이 메소드는 수학적 함수다. 다음 두 구문이 서로 동일하기 때문이다.
int y = increment(4); int y = 5;
Java
복사
반면 다음은 수학적 함수가 아니다.
int x = 0; public int increment(int x) { x++; return x; }
Java
복사
이 예제에서 숨은 출력 즉 사이드 이펙트는 필드 x 값의 변경이다.
사이드 이펙트는 숨은 출력의 가장 일반적인 유형이다.
2.
함수형 아키텍처란?
결국 사이드 이펙트는 사용자 정보를 변경하거나 장바구니에 물건을 담는 등 모든 애플리케이션이 만들어내는 행위다.
함수형 프로그래밍의 목표는 사이드 이펙터를 완전히 제거하는 것이 아니라 비즈니스 로직을 처리하는 코드와 사이드 이펙트를 일으키는 코드를 분리하는 것이다.
이 두 가지 책임은 각각을 놓고 보더라도 충분히 분리될만큼 복잡하다. 이 둘을 동시에 구현하고자 고려하게되면 복잡도가 배가되고 장기적으로 생산성을 저하시킨다.
함수형 아키텍처는 바로 이 곳에 사용된다. 사이드 이펙트를 비즈니스 연산 끝으로 몰아서 비즈니스 로직을 사이드 이펙트로부터 분리시킨다.
함수형 아키텍처는 사이드 이펙트를 다루는 코드를 최소화하면서 순수 함수 방식으로 작성한 코드의 양을 극대화시킨다. 일단 객체가 생성되면 그 상태를 변경시킬 수 없다.
다음 두 가지 코드 유형을 구분해서 비즈니스 로직과 사이드 이펙트를 분리할 수 있다.
결정을 내리는 코드
이 코드는 사이드 이펙트가 필요 없기 때문에 수학적 함수를 사용해 작성할 수 있다.
해당 결정에 따라 작동하는 코드
이 코드는 수학적 함수에 의해 이뤄진 결과를 데이터베이스의 변경이나 메시지 버스로 전송된 메시지와 같이 가시적인 부분으로 변환한다.
결정을 내리는 코드는 종종 함수형 코어라고도 한다. 해당 결정에 따라 작용하는 코드는 가변 셸이다. 함수형 코어와 가변 셰을 다음과 같은 방식으로 동작한다.
가변 셸은 모든 입력을 수집한다.
함수형 코어는 결정을 생성한다.
셸은 결정을 사이드 이펙트로 변환한다.
이 두 계층을 지속적으로 잘 분리하려면 가변 셸이 의사결정을 수행하지 않게끔 결정을 나타내는 클래스에 정보가 충분히 있는지 확인해야 한다.
다시 말해 가변 셸은 가능한 아무 말도 하지 않아야 한다. 목표는 출력 기반 테스트로 함수형 코어를 두루 다루고 가변 셸을 훨씬 더 적은 수의 통합 테스트에 맡기는 것이다.
3.
함수형 아키텍처와 헥사고날 아키텍처 비교
둘은 비슷한 점이 많다. 둘 다 관심사 분리라는 아이디어를 기반으로 한다. 그러나 분리를 둘러싼 구체적인 내용은 다양하다.
헥사고날 아키텍처는 도메인 계층과 애플리케이션 서비스 계층을 구별한다.
도메인 계층은 비즈니스 로직에만 책임이 있는 반면, 애플리케이션 서비스 계층은 데이터베이스나 STMP와 같은 외부 애플리케이션과의 통신에 책임이 있다.
이는 결정과 실행을 분리하는 함수형 아키텍처와 매우 유사하다.
또 다른 유사점은 의존성 간의 단방향 흐름이다.
헥사고날 아키텍처에서 도메인 계층 내 클래스는 서로에게만 의존해야 한다. 애플리케이션 서비스 계층의 클래스에 의존해서는 안 된다.
마찬가지로 함수형 아키텍처의 불변 코어는 가변 셸에 의존하지 않는다. 자급할 수 있고 외부 계층과 격리되어 동작할 수 있다. 이로 인해 함수형 아키텍처가 테스트하기 쉬운 것이다.
가변 셸에서 불변 코어를 떼어내 셸이 제공하는 입력을 단순한 값으로 모방할 수 있다.
이 둘의 차이점은 사이드 이펙트에 대한 처리에 있다.
함수형 아키텍처는 모든 사이드 이펙트를 불변 코어에서 비즈니스 연산 가장자리로 밀어낸다. 이 가장자리는 가변 셸이 처리한다.
헥사고날 아키텍처는 도메인 계층에 제한하는 한, 도메인 계층으로 인한 사이드 이펙트도 문제없다. 모든 수정 사항은 도메인 계층 내에 있어야 하며 계층의 경계를 넘어서는 안 된다.
도메인 계층의 인스턴스는 데이터베이스에 직접 저장할 수 없지만 상태는 변경할 수 있다. 애플리케이션 서비스 계층의 데이터베이스가 이를 적용한다.
함수형 아키텍처는 헥사고날 아키텍처의 하위 분류다. 극단적으로는 함수형 아키텍처를 헥사고날 아키텍처로 볼 수도 있다.

4. 함수형 아키텍처와 출력 기반 테스트로의 전환

이 절에서는 애플리케이션 샘플을 함수형 아키텍처로 리팩토링한다. 두 가지 리팩토링 단계를 나눌 수 있다.
프로세스 외부 의존성에서 목으로 변경한다.
목에서 함수형 아키텍처로 변경한다.
상태, 통신 기반 테스트를 출력 기반 테스트 스타일로 리팩토링할 것이다.
1.
감사 시스템 소개
주 도메인은 조직의 모든 방문자를 추적하는 감사 시스템이다. 다음과 같은 구조로 텍스트 파일을 저장소로 사용한다.
이 시스템은 가장 최근 파일의 마지막 줄에 방문자의 이름과 방문 시간을 추가한다. 파일당 최대 항목 수에 도달하면 인덱스를 증가시켜 새 파일을 작성한다.
이 예제의 시스템 초기 버전은 다음과 같은 작업을 수행한다.
작업 디렉토리에서 전체 파일 목록을 조회한다.
인덱스별로 정렬한다.
아직 파일이 존재하지 않으면 단일 레코드로 첫 번째 파일을 생성한다.
감사 파일이 있으면 최신 파일을 가져와서 파일의 항목 수가 한계에 도달했는지에 따라 새 레코드를 추가하거나 새 파일을 생성한다.
⇒ 감이 온다.. 파일 시스템을 모킹해야할 것 같은..!
초기 버전의 AuditManager 클래스는 파일 시스템과 밀접하게 연결되어 있어 테스트하기가 매우 어렵다.
테스트 전에 파일을 세팅하고 테스트가 끝나면 해당 파일을 확인한 뒤 삭제해야 한다.
테스트에 있어서 파일 시스템은 병목 지점이 된다. 이는 테스트의 실행 흐름을 방해할 수 있는 공유 의존성이다.
초기 버전 테스트에 대한 단위 테스트 4대 요소를 정리해보면 다음과 같다.
초기 버전
회귀 방지
좋음
리팩토링 내성
좋음
빠른 피드백
나쁨
유지 보수성
나쁨
물론 이는 단위 테스트의 정의에도 맞지 않다. 단위 테스트의 두, 세 번째 특성을 준수하지 않으므로 통합 테스트 범주에 속한다.
2.
테스트를 파일 시스템에서 분리하기 위한 목 사용
테스트가 영향을 받을 수 있는 공유 의존성에 결합된 문제는 일반적으로 해당 시스템을 목으로 처리해 해결한다.
파일의 모든 연산을 별도의 클래스(IFileSystem)으로 도출하고 AuditManager에 생성자로 해당 클래스를 주입할 수 있다.
이렇게 되면 AuditManager가 파일 시스템에서 분리되므로 공유 의존성이 사라지고 테스트를 서로 독립적으로 실행할 수 있다.
초기 버전보다 개선되었음을 확인할 수 있다. 테스트는 더이상 파일 시스템에 접근하지 않으므로 빠르게 실행된다. 또한 파일 시스템을 확인하고 삭제할 필요가 없으므로 유지 비용도 절감된다.
리팩토링을 진행했음에도 회귀 방지와 리팩토링 내성은 나빠지지 않았다.
초기 버전
회귀 방지
좋음
리팩토링 내성
좋음
빠른 피드백
좋음
유지 보수성
중간
하지만 아직 개선할 점들이 보인다. 목을 사용했기에 목의 동작을 지정하는 복잡한 세팅이 사용되었다.
목 라이브러리가 열심히 목을해주고 있지만 평이한 입출력에 의존하는 테스트만큼 읽기가 쉽지 않다.
3.
함수형 아키텍처로 리팩토링하기
앞서 인터페이스 뒤로 사이드 이펙트를 숨기고 해당 인터페이스를 주입하는 대신 사이드 이펙트를 클래스 외부로 완전히 이동할 수 있다.
그러면 AuditManager는 파일에 수행할 작업을 둘러싼 결정만 책임지면 된다.
새로운 클래스인 Persister는 그 결정에 따라 파일 시스템에 업데이트를 적용한다.
여기서 Persister가 가변 셸 역할을 수행하며 AuditManager는 불변 코어에 해당한다.
이렇게 했을 때, 출력 기반 단위 테스트가 되어 좋은 단위 테스트 요소의 모든 부분이 좋아진다.

5. 함수형 아키텍처의 단점 이해하기

1.
함수형 아키텍처 적용 가능성
감사 시스템은 결정을 내리기 전에 입력을 모두 미리 수집할 수 있으므로 함수형 아키텍처에 적합했다.
의사 결정 절차의 중간 결과에 따라 프로세스 외부 의존성에서 추가 데이터를 질의가 필요할 수도 있다.
지난 24시간의 방문 횟수가 임계치를 초과했따면 감사 시스템이 방문자의 접근 레벨을 확인한다고 해보자. 이때 접근 레벨은 모두 데이터베이스에 저장되어 있다.
이런 상황에선 두 가지 해결책이 있다.
애플리케이션 서비스 전면에서 디렉토리 내용과 더불어 방문자 접근 레벨을 수집해온다.
AuditManager에서는 IsAccessLevelCheckRequired()와 같은 새로운 메소드를 둔다.
애플리케이션 서비스에서 addRecord() 이전에 이 메소드를 호출하고 true를 반환하면 서비스는 접근 레벨을 가져온 후 addRecord()에 인수로 전달한다.
두 방법 모두 단점이 있다.
첫 번째는 성능이 저하된다. 접근 레벨이 필요 없더라도 무조건 조회해야하기 때문이다. 그러나 비즈니스 로직과 외부 시스템과의 통신을 완전히 계속 분리할 수 있다.
두 번째 방법은 성능 향상을 위해 분리를 다소 완화한다. 다만 데이터베이스를 호출할지에 대한 결정을 AuditManager가 아닌 애플리케이션 서비스가 처리한다.
2.
성능 단점
함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성(프로덕션 코드, 테스트 코드 모두) 간의 절충이다.
성능 영향이 적은 일부 시스템에서는 함수형 아키텍처를 사용해 유지 보수성을 향상시키는 편이 좋다. 물론 반대로 선택해야 하는 경우도 있다. 즉, 은탄환은 없다.
3.
코드베이스 크기 증가
코드베이스의 크기도 마찬가지다. 함수형 아키텍처는 불변 코어와 가변 셸 사이를 명확하게 분리해야 한다, 따라서 궁극적으로 코드 복잡도가 낮아지고 유지 보수성이 향상되지만 그 양이 늘어난다.
모든 도메인 모델이 초기 투자 비용이 높은 것이 타당할만큼 복잡도가 높은 것은 아니다. 항상 시스템의 복잡도와 중요성을 고려해 함수형 아키텍처를 전략적으로 적용하라.
대부분의 프로젝트에서는 모든 도메인 모델을 불변으로 할 수 없기에 출력 기반 테스트만 사용할 수 없다. 대부분의 경우, 출력 기반과 상태 기반을 조합하게 되며 통신 기반을 약간 섞어도 괜찮다.
이 장의 목표는 모든 테스트를 출력 기반 스타일로 전환하는 것이 아니라 가능한 많은 테스트를 전환하는 것이다.

요약

출력 기반 테스트는 SUT에 입력을 주고 출력을 확인하는 테스트 스타일이다. 이 테스트 스타일은 숨은 입출력이 없다고 가정하고 SUT 작업의 결과는 반환하는 값뿐이다.
상태 기반 테스트는 작업이 완료된 후의 시스템 상태를 확인한다.
통신 기반 테스트는 목을 사용해서 테스트 대상 시스템과 협력자 간의 통신을 검증한다.
단위 테스트의 고전파는 통신 기반 스타일보다 상태 기반 스타일을 선호한다. 런던파는 반대를 선호한다. 두 분파 모두 출력 기반 테스트를 사용한다.
출력 기반 테스트가 테스트 품질이 가장 좋다.
이러한 테스트는 구현 세부 사항에 거의 결합되지 않으므로 리팩토링 내성이 있다. 또한 작고 간결하므로 유지 보수성도 좋다.
상태 기반 테스트는 안정성을 위해 더 신중해야 한다.
단위 테스트를 하려면 비공개 상태를 노출하지 않도록 해야 한다.
출력 기반 테스트보다 크기가 큰편이므로 유지 보수가 쉽지않다. 헬퍼 메소드와 값 객체를 사용해 유지 보수성을 향상시킬 수 있지만 완전히 제거할 수는 없다.
통신 기반 테스트도 안정성을 위해 더 신중해야 한다.
애플리케이션 경계를 넘어서 외부 환경에 사이드 이펙트가 보이는 통신만 확인해야 한다.
통신 기반 테스트의 유지 보수성은 출력 및 상태 기반 테스트와 비교할 때 좋지 않다.
목은 공간을 많이 차지하는 경향이 있어서 가독성이 떨어진다.
함수형 프로그래밍은 수학적 함수로 된 프로그래밍이다.
수학적 함수는 숨은 입출력이 없는 메소드다. 사이드 이펙트와 예외가 숨은 출력에 해당한다.
내부 상태 또는 외부 상태에 대한 참조는 숨은 입력이다.
수학적 함수는 명시적이므로 테스트 용이성을 향상시킨다.
함수형 프로그래밍의 목표는 비즈니스 로직과 사이드 이펙트를 분리시키는 것이다.
함수형 아키텍처는 사이드 이펙트를 비즈니스 연산의 가장자리로 밀어내 분리를 이루는 데 도움이 된다.
이 방법으로 사이드 이펙트를 다루는 코드를 최소화하면서 순수 함수 방식으로 작성된 코드의 양을 최대화할 수 있다.
함수형 아키텍처는 모든 코드를 불변 코어와 가변 셸이라는 두 가지 범주로 나눈다.
가변 셸은 입력 데이터를 불변 코어에 제공하고 코어가 내린 결정은 사이드 이펙트로 변환한다.
함수형 아키텍처와 헥사고날 아키텍처의 차이는 사이드 이펙트의 처리에 있다.
함수형 아키텍처는 모든 사이드 이펙트를 도메인 계층 밖으로 밀어낸다.
헥사고날 아키텍처는 도메인 계층에만 한정되어있다면 도메인 계층에 의해 만들어진 사이드 이펙트도 괜찮다.
함수형 아키텍처와 전통적인 아키텍처 사이의 선택은 성능과 코드 유지 보수성 사이의 절충이며 함수형 아키텍처는 유지 보수성 향상을 위해 성능을 희생한다.
모든 코드베이스를 함수형 아키텍처로 바꿀수는 없다.
함수형 아키텍처를 전략적으로 적용하라.
시스템의 복잡도와 중요성을 고려하라.
코드베이스가 단순하거나 그렇게 중요하지 않으면 함수형 아키텍처는 별 효과가 없다.