진짜 테스트하기 쉬울까? 함수형 프로그래밍의 사실과 오해

테스트하기 쉽다?!

함수형 프로그래밍의 장점에 대해 검색하면 다양한 것들이 있지만 자주 눈에 띄는 것 중 하나는 테스트(검증)하기 쉽다는 점입니다. 그럼 왜 테스트하기 쉽다고 말할까요? 이는 함수형 프로그래밍에서 중심적으로 다루는 부수 효과(side effect)가 없는 순수 함수의 특성 때문입니다. 순수 함수의 연산 결과는 다른 상태에 의존하지 않고 오직 주어진 인자(parameter)에 의해 결정됩니다. 따라서 테스트 코드를 작성할 때 인자를 넣고 함수를 실행한 후 실제 결과와 예상 결과를 비교하면 될 뿐 이 함수를 둘러싼 환경을 조성할 필요가 없습니다.

그럼 반대로 함수의 부수 효과 때문에 테스트가 어려운 상황은 뭘까요? 예를 들어 서버 개발자가 테스트 코드를 작성한다고 합니다. 내가 테스트 할 함수는 DB에 접속해서 데이터를 가져오고 다른 서드파티 API를 사용해서 데이터를 또 가져온 후 그 결과를 콘솔에 출력합니다. 이 함수를 테스트하기 위해서는 입력으로 넣어주는 인자 외에도 DB, 서드파티 API 그리고 콘솔을 직접 연결하거나 최소한 그것을 모킹하는 인스턴스가 있어야 합니다. 개발자들이라면 흔하게 겪는 테스트 상황입니다. 그럼 이 시점에서 함수형 프로그래밍에 대한 기대가 생깁니다. 함수형 프로그래밍은 순수 함수로만 코드를 짤테니 테스트할 때 외부 환경들을 모킹하는 인스턴스를 전혀 만들 필요가 없겠구나! 하고요. 정말 그럴까요?

순수 함수로 만든 프로그램

Haskell, Scala, Clojure, OCaml 같은 함수형 프로그래밍 언어로 짜여진 프로그램은 부수효과가 전혀 없을까요? 정답은 No! 입니다. 예를 들어 콘솔에 “Hello World!”를 프린트하는 간단한 프로그램을 생각해봅시다. 콘솔에 출력하는 행위가 부수 효과인데 부수 효과가 없는 함수들을 조합해서 부수 효과가 있는 프로그램이 나올 수는 없습니다. 그럼 일반적인 Java, Python, C로 코딩하는 것과 어떤 차이가 있을까요? 간단한 JavaScript 코드로 예를 들어 설명해보겠습니다.

두 코드를 비교하면 순수 함수를 주도적으로 사용하는 프로그램이 무엇이 다른지 느껴지실 겁니다. 순수 함수형 프로그래밍에서는 프로그램의 거의 모든 부분을 순수함수로 작성합니다. 그리고 이 순수 함수들을 합성하여 거대한 합성 함수를 만듭니다. 프로그램의 가장 마지막에 이 거대한 합성 함수를 “실행”할 때 모든 부수 효과가 발생합니다.

💡 이렇게 코딩하면 어떤 장점이 있을까요? 간단히 생각해보면 마지막 부수 효과가 발생하는 부분 외에 모든 부분이 순수 함수이기 때문에 일반적으로 이야기하는 함수형 프로그래밍의 장점을 모두 갖습니다. 이 이야기도 재미있을 것 같지만 주제에서 너무 벗어날 것 같아 다음에 다른 글로 써보겠습니다.

테스트 준비하기

다시 테스트 이야기로 돌아가보겠습니다. 함수형 프로그래밍 언어(Haskell, Scala, …)는 위에 작성한 순수 함수형 프로그래밍 코드에 약간의 변형을 가합니다. 이를 두 단계로 써보겠습니다.

먼저 각 함수의 결과를 함수로 바꿉니다. 이는 함수에 인자를 넣는 즉시 실행되지 않고 한 번 더 ()를 붙여서 명시적으로 실행해야만 부수효과가 일어나도록 바꾼 것입니다. 이러한 지연 연산(Lazy Evaluation)을 하는 이유는 다음 단계와 관련이 있습니다.

다음 단계는 IO 타입을 만들어 각 함수에 타입을 붙이기만 했습니다. IO 타입은 파라미터를 받지 않고 T 타입을 리턴하는 함수입니다. 결론적으로 부수효과가 있는 함수들의 데이터 타입을 모두 IO로 표시하기 위해 1단계에서 함수의 결과를 모두 함수로 바꾼 것입니다. 이를 통해 리턴 타입에 IO가 붙어있는 함수를 실행하면 부수효과가 발생한다는 것을 짐작할 수 있습니다.

이제 모든 부수 효과가 있는 함수들을 인자를 받아서 IO 타입의 값을 리턴하는 순수 함수로 바꿨습니다. IO는 사실 함수이지만 함수형 프로그래밍에서 함수는 곧 일급 객체(first-class citizen)이므로 불변(immutable)한 값으로 취급할 수 있습니다.

💡 위 코드에서는 함수를 합성할 때 h(g(f(x)())())()를 사용합니다. 보기만해도 복잡한데요. 실제 함수형 프로그래밍에서는 이런 복잡한 합성을 도와주는 다양한 고차함수(higher-order function)를 제공합니다. 우리가 익히 아는 map, flatMap (언어에 따라 bind), reduce(or fold), filter 등의 다양한 조합 함수(combinator)들이 여기 속합니다.

이제 테스트 할 수 있을까?

그럼 이제 드디어 테스트할 수 있겠네요. 부수 효과가 있는 함수마저 순수 함수로 바꾸었으니 글의 앞쪽에서 언급한대로 DB, 서드파티 API, 콘솔을 모킹할 필요 없이 함수에 인자를 넣고 결과로 나오는 IO 타입의 값이 내가 예상한 값과 같은지 비교하면 될 것 같습니다.

하지만 여기서 문제가 발생합니다. IO 타입의 값은 결국 함수인데 두 함수를 비교할 때 이들을 실제로 실행하기 전까지 같은지 알 수 없습니다. 이 한계는 함수형 프로그래밍이기 때문에 발생하는 것이 아닌 더 근본적인 곳에서 기원합니다. 튜링 완전 언어에서는 두 함수의 구현을 알더라도 완전히 같은지 판별할 수 없습니다.

그럼 어떻게 테스트할까요? 함수형 프로그래밍 언어마다 정말 다양한 방법으로 테스트합니다. 그러나 이를 뜯어보면 결국 DB, 서드파티 API, 콘솔 등의 외부 세계를 모킹하는 인스턴스를 만들어서 테스트해야 한다는 점에서 크게 다르지 않습니다. 즉, 함수형 프로그래밍도 부수효과가 있는 함수의 테스트는 여전히 까다롭습니다.

함수형 프로그래밍 할까 말까

그럼 함수형 프로그래밍을 해야할 이유가 있을까요? 함수형 프로그래밍에서도 테스트는 쉽지 않은 것으로 밝혀졌지만 여전히 다른 장점들이 존재합니다. 중심 비즈니스 요구사항에 집중할 수 있는 프로그램, 동시성, 추상화, 리팩토링 등 함수형 프로그래밍은 아직 쓸만합니다. 하지만 우리가 테스트를 실제로 어떻게 하는지 알아봤듯이 다른 장점도 구체적으로 어떤 점이 좋은지 알아야 합니다. 결국 함수형 프로그래밍은 만능 열쇠가 아닙니다. 분명 사용하는 사람의 역량에 따라 그 효용이 좌우되는 점도 있고 잘 쓰기 위해 고민하고 공부해야 합니다. 그러나 어떤 프로그래밍 언어, 패러다임에서 개발하든 끊임없이 배우고 익혀야 하는 것은 같습니다. 만약 당신이 함수형 프로그래밍에 관심이 있다면 직접 써보고 느끼는 것이 가장 빠른 배움의 길이라고 생각합니다.

참고 사이트

comments powered by Disqus

Related