원문: Leo Lanese, "Making unit-test fun again with Functional Programming"
구조적 필요성
단위 테스트는 소프트웨어 개발에 있어 필수적인 부분이며 좋은 사례로 간주됩니다. 선택이 아니라 필수죠.
간략히 설명하자면 단위 테스트는 현재 우리가 하는 일을 테스트하는 데에 도움을 줄 뿐만 아니라 코드의 기본 단위를 격리시켜 신뢰할 수 있게 해줍니다. 또한 계속하여 소프트웨어를 만들고 밤에 잠도 푹 잘 수 있게 해주죠.
어떤 문제의 해결 방법이든 그 기능성을 시험하지 않는다면 완전할 수 없음에 우리 모두가 동의한 바 있지만 단위 테스트는 시간이 필요하며 항상 그리 재미있는 일은 아닙니다. 따라서 우리는 둘 중 하나의 접근 방식을 취하게 됩니다. 테스트 주도 개발(TDD: Test Driven Development) 혹은 나중에 테스트를 작성하기(WTA: Write After Test)가 우리의 최종 해결 방법이 될 것입니다.
함수형 프로그래밍과 함께 하는 테스트 지향 코드
"테스트하기 어려운 코드는 좋지 않은 코드다." - Joe Eames
코드를 작성하기 전이든 작성하고 나서든 우리는 코드가 동작하고 의도한 대로 기능하는지 확인하기 위해 단위 테스트를 작성할 것입니다. 이 부분에서 코드의 복잡도에 따라 우리는 시간을 더 쓰게 될 수도 혹은 덜 쓰게 될 수도 있습니다. 복잡도는 추상화와 부작용이 없는 순수한 코드를 통해 단순해질 수 있습니다.
간단히 말해서 함수형 프로그래밍은 함수를 이용해 프로그래밍하는 것이라는 점을 기억합시다.
여러 개의 작은 순수 함수를 만드는 것은 실제로 단위 테스트의 복잡도를 단순화할 수 있으며 우리 코드의 이식성, 재사용성, 예측 가능성을 높여줍니다. 그 결과 테스트하기가 쉬워지죠.
선언형과 명령형 패러다임의 제어 흐름
프로그래밍 언어는 일반적으로 선언형과 명령형 두 측면의 스펙트럼으로 나뉩니다.
선언형 언어는 컴퓨터에게 "무엇을 할 지"를 말하는 반면 명령형 언어는 "어떻게 할 지"를 말합니다.
명령형 패러다임 흐름: "어떻게 할 지" 부분이 테스트되어야
명령형 접근에서 우리는 제어에 따릅니다. 컴퓨터가 목표를 달성하기 위해 따라야 하는 단계 (어떻게 할 지)를 정확하고 자세하게 표현하는 코드를 개발자가 작성해야 하죠.
원하는 결과를 달성하기 위한 구체적인 단계를 표현하기 위해 몇 줄의 코드를 사용합니다.
// 명령형 패러다임
var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2);
}
array; //-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
선언형 패러다임 흐름: "무엇을 할 지" 부분이 테스트되어야
선언형 접근에서는 우리가 흐름을 제어합니다. 함수형 접근은 해결해야 하는 문제를 실행할 함수의 집합(무엇을 할 지)으로 구성하는 과정이 포함됩니다. 이러한 접근은 여러 개의 "단일 함수"를 만드는 것을 장려합니다. 오직 한 가지 작업만 수행하는 함수인 단일 함수를 만드는 것은 각각의 함수를 더욱 이해하기 쉽고 테스트하기 쉽게 해주죠.
선언형은 흐름 제어 과정을 추상화하는 대신 데이터의 흐름(무엇을 할 지)을 표현하는 데에 몇 줄의 코드를 사용합니다.
// 선언형 프로그램
// 단위 테스트가 쉬워질 겁니다
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => Math.pow(num, 2));
테스트되어야 하는 상태
명령형 프로그래밍 테스트
평가되는 함수들은 시간이 지나며 상태가 변하거나 데이터가 변환됨에 따라 따라야 하는 특정한 단계가 있는 주요 경로입니다. 변수들은 프로그램의 현재 상태를 저장하기 위해 사용됩니다. 이러한 단계들과 변수들은 원하는 순서의 실행 단계를 얻기 위해 모의(mock)로 대체될 필요가 있습니다.
함수형 프로그래밍 테스트
함수형 프로그래밍은 상태가 없습니다. 이러한 무상태성은 함수형 언어를 그저 순수한 함수의 처리 과정(즉, 입력과 출력)을 보는 것만으로 추론할 수 있게 합니다. 또한 함수는 순수하기 때문에 실행 순서 또한 그 중요도가 낮습니다.
함수의 결과는 오직 입력에만 의존하며 다른 것에는 의존하지 않습니다.
동작하는 부분
객체지향은 동작하는 부분을 캡슐화하여 코드를 이해하기 쉽게 만듭니다.
함수형은 동작하는 부분을 최소화하여 코드를 이해하기 쉽게 만듭니다.
- Michael Feathers
로직의 흐름은 가능한 한 최소화해야 합니다. 로직이 적으면 적을수록 테스트되어야 하는 새로운 로직도 적어집니다. 우리는 이러한 동작하는 부분을 상태 변화 (또는 상태 변이)로 이해해야 합니다. 함수형 프로그래밍에서는 객체지향 프로그래밍에서처럼 상태 변이를 캡슐화하는 것이 아니라 애초에 상태 변이를 다루는 것을 피하려 합니다.
순수한 코드와 "순수한 테스트"
어떻게 보자면 우리는 격리된 한 단위의 코드를 작성할 때마다 함수를 테스트하고 싶다고 할 수 있겠습니다.
즉, 함수는 주어진 하나의 입력에 대해 항상 동일하고 적절한 출력으로 응답합니다. 우리는 특정한 "단위"의 코드를 단독으로 테스트할 수 있으며 이는 다른 단위의 코드로부터 격리되어 있습니다. 어떤 테스트가 되었든 다른 테스트에 의존하면 안 되며 테스트들은 동시에 그리고 어떠한 순서로든 실행 가능해야 합니다.
예를 들어 순수 함수는 부작용이 없기 때문에 디버깅하기 쉽고 병렬적으로 실행하기 수월합니다. 이러한 특징들은 Jasmine 3 버전 이상과 Jest에 의해 강화됩니다.
테스트 작성하기: 당신이 작성하는 대부분의 Redux 코드가 함수이며 순수하기 때문에 모의로 대체하는 과정이 없어도 테스트하기 쉽습니다.
- Redux 문서: https://redux.js.org/usage/writing-tests
결론
함수형 프로그래밍은 일급 고차 함수에 의존함으로써 함수를 기본 단위로 취급하여 코드의 모듈성과 재사용성을 향상시킵니다. 선언형 패러다임, 순수 함수와 무상태성이라는 특성, 함수 지향 구조의 조합은 신뢰성 있고 빠르며 예측 가능하고 유지보수하기 쉬운 단위 테스트를 생성하는 테스트 지향 코드를 만들 수 있는 가능성을 제공하여 밤에 단잠을 잘 수 있도록 해줄 것입니다.