함수형 프로그래밍 다시 보기: 상태의 추방과 합성의 복권
7 views
# 함수형 프로그래밍 다시 보기: 상태의 추방과 합성의 복권
## 1. 오래된 것이 다시 새것이 될 때
람다 대수가 종이 위에 처음 적힌 것은 1930년대였다. 알론조 처치가 계산 가능성을 정의하기 위해 고안한 이 형식 체계는 컴퓨터가 발명되기 전에 이미 컴퓨터의 한 가능태를 그려두었다. 같은 시기 앨런 튜링은 상태와 테이프를 가진 기계를 상상했고, 이후 70년간 산업의 주류가 된 것은 후자였다. 함수형은 패배했다기보다 보류되었다.
보류는 길었다. 1958년 Lisp이 등장했고 ML과 Haskell이 학계의 어휘로 자리 잡았지만, 실무의 대다수는 변수에 값을 다시 대입하는 명령형 세계에서 살았다. 1990년대를 장악한 객체지향은 상속과 캡슐화를 설계의 공용어로 만들었다. 그 사이 함수형은 학계의 우아한 사투리쯤으로 취급되었다.
전세가 흔들린 시점은 멀티코어가 일상이 된 2000년대 후반이다. 단일 CPU의 클럭 상승이 물리적 한계에 부딪히자 산업은 병렬성에 의존하기 시작했다. 그런데 병렬성은 공유 가변 상태와 본질적으로 불화한다. 자바스크립트는 React를 통해 선언형으로 기울었고, 자바는 8 버전에서 람다와 Stream을 도입했으며, Kotlin·Swift·Rust는 처음부터 함수형 어휘를 표준 문법으로 흡수했다. 보류되었던 것이 마침내 호명된 것이다.
## 2. 패러다임의 골격: 상태가 없을 때 무엇이 남는가

함수형 프로그래밍의 정의는 단순하다. **계산을 수학적 함수의 평가로 본다.** 같은 입력에 같은 출력을 약속하고, 그 과정에서 바깥을 건드리지 않는다. 이 약속을 **참조 투명성(referential transparency)** 이라 부른다.
이 단순한 약속에서 여러 결과가 파생된다.
### 불변성과 순수 함수
변수는 한 번 바인딩되면 다시 바뀌지 않는다. `x = x + 1`은 함수형의 어휘가 아니다. 대신 `x`로부터 `x'`라는 새 값을 만들어낸다. 상태의 변경이 아니라 새 상태의 생성이다. 메모리 관점에서는 낭비처럼 보이지만, 영속 자료구조(persistent data structure)는 구조 공유를 통해 이 비용을 로그 시간으로 끌어내린다. Clojure의 PersistentVector, Scala의 Vector, Haskell의 Data.Map이 같은 원리 위에 서 있다.
순수 함수는 부수 효과를 갖지 않는다. 파일을 쓰지 않고, 전역 변수를 바꾸지 않으며, 콘솔에 찍지 않는다. 이 제약은 처음에는 불편하지만, 함수의 행동을 시그니처만으로 예측 가능하게 만든다. 테스트는 입출력 쌍의 검증으로 환원되고, 디버깅은 호출 경로의 산수가 된다.
### 일급 함수와 고차 함수
함수가 값이 되면 함수를 인자로 넘기고 함수를 반환할 수 있다. `map`, `filter`, `reduce`는 이 능력의 최소 사례다. 명령형의 for-loop이 "어떻게 순회할지"를 적는다면, 고차 함수는 "무엇을 할지"를 적는다. 의도와 절차가 분리된다.
```javascript
// 명령형: 어떻게
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) result.push(users[i].name);
}

// 함수형: 무엇을
const result = users
.filter(u => u.active)
.map(u => u.name);
```
### 합성과 파이프라인
작은 함수들을 이어 붙여 큰 함수를 만든다. `f ∘ g`는 수학의 표기법이지만, 실무에서는 `pipe(g, f)`라는 모양으로 나타난다. UNIX 파이프가 작은 도구의 합성으로 거대한 텍스트 처리를 만들어냈듯, 함수형은 단위 함수의 합성으로 도메인 로직을 구축한다. 객체지향이 명사(객체)의 협력으로 세계를 그렸다면, 함수형은 동사(변환)의 연쇄로 세계를 그린다.
## 3. 부수 효과는 어디로 가는가
순수성을 추구한다고 해서 부수 효과가 사라지지는 않는다. 데이터베이스에 쓰지 않는 서버는 무용하고, 화면을 그리지 않는 UI는 존재 이유가 없다. 함수형은 부수 효과를 제거하는 것이 아니라 **격리한다.**
Haskell의 IO 모나드가 그 극단이다. 부수 효과를 가진 모든 행위는 `IO a` 타입 안에 봉인되고, 이 타입은 순수한 영역과 명확히 분리된다. 컴파일러는 어떤 함수가 부수 효과를 가지는지 타입만으로 판별한다. 부수 효과가 거짓말을 할 수 없는 구조다.
극단을 따르지 않는 언어에서도 같은 정신은 작동한다. 함수형 코어, 명령형 셸(Functional Core, Imperative Shell)이라 불리는 이 설계는 도메인 로직을 순수 함수로 두고 외부 세계와의 상호작용을 얇은 껍질로 분리한다. Gary Bernhardt가 2012년 강연에서 정리한 이 패턴은 지금도 가장 실용적인 절충안으로 인용된다.
## 4. 실무는 무엇을 가져갔는가
이론의 우아함과 실무의 채택은 다른 문제다. 산업이 함수형의 어떤 부분을 실제로 받아들였는지 살펴보면, 그 채택은 철저히 선택적이다.
### 받아들인 것
- **불변 데이터.** React의 상태 관리, Redux의 리듀서, Java의 record, Kotlin의 data class. 변경 가능한 객체보다 새 객체를 만드는 편이 추론하기 쉽다는 합의가 자리 잡았다.
- **선언형 컬렉션 처리.** Stream, LINQ, Sequence. for-loop을 직접 적는 코드는 점점 드물어졌다.
- **타입 시스템의 함수적 어휘.** Option/Maybe, Result/Either, sum type은 Null 참조의 오류를 컴파일 타임에 막는 도구로 자리 잡았다. Rust의 `Result<T, E>`는 이 흐름의 가장 대중적인 결실이다.
- **반응형 흐름.** RxJava, RxJS, Kotlin Flow. 시간에 따라 변하는 값을 함수의 연쇄로 다루는 추상화다.

### 받아들이지 않은 것
- **모나드의 전면적 도입.** 대부분의 산업 코드베이스에서 모나드는 이름 없이 사용된다. `Promise`, `Optional`, `Stream`은 모두 모나드의 사례지만, 개발자 대부분은 그 이름을 모른 채 잘 쓴다. 이는 실패가 아니라 추상화의 자연스러운 잠복이다.
- **순수성의 강제.** Haskell의 시장 점유율은 여전히 작다. 산업은 순수성을 권장 사항으로 받았을 뿐, 컴파일러로 강제하지 않았다.
- **함수형 일색의 아키텍처.** 대규모 시스템은 객체지향의 모듈 경계와 함수형의 내부 구현을 혼용한다. 이 혼종이 현재의 주류다.
## 5. 비용은 누가 치르는가
함수형은 공짜가 아니다. 진입 비용은 셋으로 나뉜다.
첫째는 **인지의 비용**이다. 명령형의 사고는 "단계별로 무엇을 한다"이고, 함수형의 사고는 "입력을 무엇으로 변환한다"이다. 후자가 더 본질에 가깝다고 주장할 수는 있지만, 대다수의 개발자는 전자로 훈련받았다. 합성과 고차 함수의 두꺼운 레이어는 처음에는 가독성을 해치고, 익숙해진 뒤에야 더 명료해진다.
둘째는 **성능의 비용**이다. 불변 자료구조는 영리한 구현으로도 가변 배열의 인덱스 접근 속도를 완전히 따라잡지 못한다. GC 압력도 늘어난다. 하드 리얼타임 시스템이나 초고성능 게임 엔진에서 함수형이 주류가 되지 못한 이유다.
셋째는 **생태계의 비용**이다. 함수형 친화적 라이브러리가 늘었지만, 여전히 많은 SDK와 프레임워크는 가변 상태와 부수 효과를 전제로 설계된다. 순수한 함수형 코드를 작성하려는 시도는 종종 외부 세계와의 마찰을 낳는다.
이 세 비용을 인식하지 못한 채 "함수형이 옳다"고 선언하는 태도는 위험하다. 모든 패러다임은 어떤 문제에 대한 어떤 답이지, 모든 문제에 대한 모든 답이 아니다.
## 6. 왜 지금 다시 호명되는가
오늘의 개발 환경은 세 가지 압력 아래 있다. 멀티코어와 분산은 여전하고, AI가 생성한 코드의 검증 가능성이 새 요구로 떠올랐으며, 시스템의 복잡성은 어느 시대보다 높다. 함수형은 이 세 압력에 부분적인 응답을 제공한다.
병렬성에 대해서는 공유 가변 상태의 부재가 답이다. 검증 가능성에 대해서는 참조 투명성이 답이다. 복잡성에 대해서는 작은 함수의 합성이 답이다. 어느 것도 완전한 해결책은 아니지만, 셋 모두에 대해 일관된 방향을 제시한다.
도구가 사람보다 빠르게 코드를 생산하는 시대에, 인간이 마지막에 책임지는 것은 **추론 가능성**이다. 어떤 코드가 무엇을 하는지 시그니처만 보고 알 수 있다면, 검토의 속도는 생산의 속도를 따라갈 수 있다. 함수형이 약속하는 것은 마법이 아니라 그 추론 가능성이다.
## 7. 닫는 말
함수형 프로그래밍은 새로운 발명이 아니라 오래된 약속의 재호명이다. 상태를 추방하고 합성을 복권한 이 사유는 학계의 사투리에서 산업의 어휘로 천천히 번역되어 왔다. 그 번역은 완결되지 않았고, 완결될 필요도 없다. 모든 코드가 순수해질 필요는 없지만, 어떤 코드는 반드시 그래야 한다. 그 경계를 가늠하는 안목이 패러다임을 다루는 자의 몫이다.
기계는 상태를 좋아한다. 인간의 머리는 그렇지 않다. 둘 사이에서 코드를 쓰는 일이 우리의 직업이다.