함수형 프로그래밍과 Clojure
복잡성의 본질은 ‘엉킴’에 있습니다. 이 글에서는 함수형 프로그래밍과 Clojure가 어떻게 그 엉킴을 풀어내 단순함을 만들어내는지 이야기합니다.

함수가 주는 가치
함수란, 어떤 집합의 각 원소를 다른 어떤 집합의 유일한 원소에 대응시키는 규칙입니다. f(x)=x+1
이나 f(x)=sin(x)
같은 것이죠. 익숙한 개념이지만, 프로그래밍의 관점에서 그 가치를 다시 살펴보겠습니다.
첫째, 함수는 값 기반입니다. f(x)=x+1
이라는 함수에 1을 넣어 2라는 값이 나올 때, 1과 2는 독립적인 개념입니다. 1이 2가 된 것이 아니라, f(x)
라는 함수가 정의역에 있던 1을 치역에 있는 2와 연결시켜 준 것입니다. 함수는 변수(variable)가 아닌 불변값(immutable)을 규칙에 따라 연결합니다.
둘째, 함수는 순수합니다. 다시 f(x)=x+1
을 예로 들어보겠습니다. x=10
을 대입했을 때 f(10)
은 11이 됩니다. 그 과정에서 갑자기 눈앞에 도넛이 나타나거나 자동차에 시동이 걸리는 일은 일어나지 않습니다. 순수하다는 것은 연산 외의 부수효과가 없다는 뜻입니다.
종합하면, 프로그래밍 관점에서 함수는 값을 기반으로 한 정보의 순수한 처리라고 할 수 있습니다. 우리가 계산기를 두드려 결과를 얻는 것과 크게 다르지 않습니다.
함수형 프로그래밍이란?
무엇이 함수형 프로그래밍인지에 대해서는 다양한 의견이 있지만, 저는 프로그램을 계산기로 환원시키려는 움직임으로 이해합니다. 계산기를 닮은 프로그램은 놀랍도록 단순해집니다.
구체적으로, 서비스에 연동된 카드 정보를 삭제하는 상황을 살펴보겠습니다. DB에 기록된 카드 정보를 삭제하면서, 금융기관 API를 호출해 연동 정보를 끊고, 결과를 내부적으로 통보해야 합니다. 각 단계에는 조건이 얽혀 있습니다.

순수 로직과 부수효과가 뒤섞여 복잡성이 커져 있습니다.
부수효과를 최소화하고, 한데 모으며, 지연하는 일련의 과정을 통해 더 계산기처럼 될 수 있습니다. 아래처럼요!


planDeletion
은 순수 함수로, 입력이 같으면 항상 같은 결과를 돌려줍니다.

executePlan
에는 모든 부수효과가 모여 있습니다. 부수(side)는 함수의 반환값이 아니라는 뜻이고, 효과(effect)는 연산 밖의 상호작용을 뜻합니다. 따라서 아래의 모든 코드는 부수효과라 할 수 있습니다.

구체적으로 어떤 점이 좋아졌을까요?
복잡성이 줄어드는 원리
복잡성이 줄어드는 원리를 짚어보겠습니다.
첫째, 엉킴을 제거했습니다.기존 코드에서는 데이터베이스 저장, API 호출, 상태 갱신이 한 함수 안에 얽혀 있었습니다. 이런 얽힘은 시스템의 동작을 예측하기 어렵게 만듭니다. 입력을 바꿔도 결과가 항상 일정하지 않고, 외부 환경(DB, 네트워크, 슬랙 등)에 따라 달라지기 때문입니다.
순수 함수와 효과 함수를 분리하면 이 얽힘이 풀리고, 로직의 흐름을 독립된 계산 그래프처럼 다룰 수 있게 됩니다.
둘째, 시간적 복잡성이 지연됩니다. 부수효과는 언제 실행되는가가 중요한데, 기존 코드에서는 로직 중간중간에서 곧바로 실행되어 추적이 어렵습니다. planDeletion은 모든 부수효과를 해야 할 일 목록으로만 기록합니다. 실행은 나중에 한 번에 이뤄집니다. 이렇게 하면 시간 순서의 부담(“어느 시점에서 DB가 바뀌었지?”)이 줄어들고, 개발자는 순수한 상태 변환만 생각하면 됩니다.
마지막으로 인지 부하가 감소합니다. 로직은 순수 함수로, 효과는 경계에서만 실행되도록 하면 프로그래머가 동시에 추적해야 할 요소가 줄어듭니다. 즉, 문제를 작은 계산 단위로 쪼개어 기억 부담을 줄이는 효과가 생깁니다.
제품 코드를 순수함수로 채울수록, 가변 상태와 부수효과는 가장자리로 밀려나게 됩니다. **인지부하를 일으키는 요인이 한데 모이고, 최소화되며, 마지막까지 지연되는 효과가 발생하면서 복잡성을 더 효율적으로 다룰 수 있게 됩니다.

함수형 프로그래밍 언어의 특징
함수형 프로그래밍 언어는 함수형 프로그래밍을 지원하기 위한 여러 특징들이 있습니다.
- 불변성(immutability): 데이터는 바뀌지 않으며, 변경이 필요하면 새 값이 만들어집니다.
- 함수의 일급성: 함수가 값처럼 자유롭게 다뤄집니다. 1급 시민(first class citizen)이라고 표현하는데, 그렇게 함으로써 함수의 가치를 극대화 합니다.
- 지연 평가(lazy evaluation): 필요할 때만 계산합니다. 직접 사용되기 전까지 평가를 미룸으로서 연산효율을 가져갑니다.
- 고차 함수(higher-order function): 선언적 문제 표현을 가능하게 합니다.
예컨대 수학 문제는 "2x+3=11을 만족하는 모든 x를 구하라”라고 표현하지, “11에서 3을 빼고 2로 나누라”라고 하지는 않습니다. Clojure에서는 이렇게 표현할 수 있습니다.

함수형 프로그래밍의 개념은 더 이상 특정 언어만의 전유물이 아닙니다. 그럼에도 함수형 프로그래밍 언어를 택하는 이유는 코드를 작성하는 개발자의 사고를 제한하기 위해서입니다.
복잡성의 본질은 엉켜있음임을 지난 글에서 확인했습니다.
갈라진 길에서 어디든 갈 수 있는 상황이라면, 발이 닿는 대로가는 쉬운 길을 택할 확률이 높습니다. 그리고 쉬운 길은 많은 경우 복잡성을 제어하는 것과 관계가 없습니다. 함수형 프로그래밍 언어는 단순함을 추구할 수 있도록, 함수의 길을 가도록 강하게 유도합니다.
Clojure와 단순성
Clojure는 대표적인 함수형 프로그래밍 언어로, 단순성을 강력하게 추구합니다.
첫째, 코드가 간결합니다. 가령 위의 카드 삭제 코드의 예시는, Clojure로 아래처럼 표현될 수 있습니다. (아래 코드는 개념적 등가물로, 실제로는 DB 조회, API 호출 등이 들어갑니다)

Clojure의 간결성에 대해선, Clean Code의 저자, 밥 아저씨의 블로그로 나머지 설명을 대신하겠습니다.(https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure.html)
둘째, 실용적입니다. JVM과 JavaScript 생태계를 모두 활용할 수 있습니다. 간단한 인터롭(Interop) 을 통해 씬에서 가장 큰 거인의 어깨에 올라탈 수 있습니다.
셋째, 동형성(homoiconicity)을 가집니다. 코드와 데이터가 같은 형태이므로, 언어를 스스로 확장할 수 있습니다.

이 특징 덕분에 매크로를 통해 새로운 구문을 정의할 수 있습니다. 예를 들어, 디버깅용으로 값을 출력하고 그대로 반환하는 매크로는 다음처럼 만들 수 있습니다.

마지막으로, LLM 친화적입니다. Clojure와 같은 압축성있는 고맥락 프로그래밍 언어는, 사용자로 하여금 코드의 의도를 잘 드러나도록 합니다. Clojure mcp (https://github.com/bhauman/clojure-mcp) 는 Clojure의 최대 장점 중 하나인 REPL을 통해 LLM이 실시간으로 코드를 평가하며 도울 수 있도록 합니다.
마치며
복잡성의 본질은 엉킴입니다. 함수형 프로그래밍은 이 엉킴을 푸는 방식으로 단순성을 지향합니다. Clojure는 불변성, 동형성, 간결성이라는 장치를 통해 단순함을 강제하는 언어입니다.
재미란 체험의 부수효과입니다. 글을 읽으신 분들께서 호기심이 생겼다면, https://tryclojure.org/ 에서 체험해보실 수 있습니다. 복잡성을 줄이는 언어로서 Clojure가 줄 수 있는 생산성 체험을 직접 느껴보시길 바랍니다!
** 함수형 프로그래밍의 방법에 대한 훌륭한 영상으로 Solving Problems the Clojure Way - Rafal Dittwald 를 추천드립니다.