함수형 프로그래밍(FP)은 세 가지 패러다임 중 가장 오래되었지만, 가장 최근에 주목받기 시작했다. 1936년 알론조 처치(Alonzo Church)의 람다 계산법에서 시작된 이 패러다임은, 불변성(Immutability)이라는 개념을 통해 현대 소프트웨어 아키텍처에 중요한 통찰을 제공한다.
람다 계산법과 함수형 프로그래밍의 기원
튜링 이전의 계산 이론
1936년, 앨런 튜링(Alan Turing)이 튜링 기계를 발표하기 전, 알론조 처치는 람다 계산법(Lambda Calculus)을 발명했다. 람다 계산법은 계산 가능성을 연구하기 위한 수학적 시스템이었다.
| |
LISP의 탄생
1958년, 존 매카시(John McCarthy)는 람다 계산법에 기반한 최초의 함수형 프로그래밍 언어 LISP를 개발했다.
| |
LISP는 현대까지 살아남아 Clojure, Racket 등의 형태로 사용되고 있다.
불변성: 함수형 프로그래밍의 핵심
함수형 언어에는 할당문이 없다
가장 엄격한 함수형 언어에서, 변수는 한 번 초기화되면 변경되지 않는다.
| |
| |
순수 함수 (Pure Function)
순수 함수는:
- 같은 입력에 항상 같은 출력
- 부수 효과(Side Effect)가 없음
| |
참조 투명성 (Referential Transparency)
순수 함수는 참조 투명하다. 함수 호출을 그 결과값으로 대체해도 프로그램의 의미가 변하지 않는다.
| |
불변성이 해결하는 문제: 동시성
가변 상태의 저주
현대 소프트웨어의 가장 큰 문제 중 하나는 동시성(Concurrency)이다. 여러 스레드가 동시에 가변 상태에 접근하면:
| |
count++는 실제로 세 단계로 이루어진다:
- count 읽기
- 1 더하기
- count 쓰기
두 스레드가 동시에 실행하면:
sequenceDiagram
participant T1 as Thread 1
participant M as count
participant T2 as Thread 2
Note over M: count = 0
T1->>M: 읽기 (0)
T2->>M: 읽기 (0)
T1->>M: 쓰기 (1)
T2->>M: 쓰기 (1)
Note over M: count = 1 (기대값: 2)
이것이 경쟁 조건(Race Condition)이다.
전통적인 해결책: 락(Lock)
| |
그러나 락은 문제를 해결하는 대신 다른 문제로 바꿔치기한다는 근본적 한계가 있다. 락을 여러 개 잘못된 순서로 획득하면 서로 상대의 락을 기다리며 영원히 멈추고, 락 경합이 심해지면 스레드들이 대기 상태에 머무는 시간이 늘어나며, 락의 범위와 순서를 관리하는 코드 자체가 버그의 원천이 된다.
- 데드락(Deadlock) 위험
- 성능 저하
- 복잡성 증가
함수형 해결책: 불변성
가변 상태가 없으면, 경쟁 조건도 없다.
| |
위 Counter는 count 필드가 final이라 생성된 이후 절대 바뀌지 않는다. increment()는 기존 객체를 수정하는 대신 새 Counter 객체를 만들어 반환하므로, 여러 스레드가 동시에 increment()를 호출해도:
- 원본 객체는 변경되지 않음
- 각 스레드는 새 객체를 받음
- 락이 필요 없음
아키텍처에서의 불변성
완전한 불변성은 가능한가?
현실적으로, 모든 상태를 불변으로 만들기는 어렵다. 프로그램은 결국:
- 파일을 쓰고
- 데이터베이스를 업데이트하고
- 네트워크로 데이터를 전송한다
가변성의 분리 (Segregation of Mutability)
마틴은 가변 컴포넌트와 불변 컴포넌트를 분리할 것을 제안한다.
flowchart TB
subgraph Immutable [불변 컴포넌트]
P[순수 함수들]
D[불변 데이터 구조]
end
subgraph Mutable [가변 컴포넌트]
DB[(데이터베이스)]
S[상태 관리]
end
Immutable --> Mutable
style Immutable fill:#9f9
style Mutable fill:#f96
- 불변 컴포넌트: 가능한 많이, 순수 함수로 구성
- 가변 컴포넌트: 최소화, 격리
판단 기준
모든 코드를 불변으로 만들려는 시도는 비현실적이다. 아래 기준으로 어디에 얼마나 불변성을 적용할지 정하는 것이 실용적이다.
| 상황 | 권장 |
|---|---|
| 계산·검증·변환 로직(비즈니스 규칙) | 순수 함수 + 불변 데이터로 작성 |
| 여러 스레드가 동시에 접근하는 공유 상태 | 불변 객체로 만들어 락 자체를 제거 |
| DB·파일·네트워크처럼 외부 세계와 맞닿는 지점 | 가변성을 인정하되, 그 범위를 이 지점으로만 한정 |
| 성능이 critical한 대량 데이터 처리 루프 | 매 단계 객체 생성 비용을 측정해보고, 필요하면 국소적으로 가변 자료구조 허용 |
트랜잭션 메모리와 동시성
가변 컴포넌트를 완전히 없앨 수는 없지만, 최소화한 뒤 그 좁은 영역에만 동시성 제어 기법을 집중시키면 관리 범위가 크게 줄어든다. 가변 상태가 필요한 곳에서는:
- 트랜잭션 메모리 사용
- 적절한 락 사용
- 원자적 연산 사용
Clojure의 예:
| |
이벤트 소싱 (Event Sourcing)
상태 대신 이벤트 저장
전통적인 방식:
- 현재 상태만 저장
- 이전 상태는 사라짐
이벤트 소싱:
- 모든 변경을 이벤트로 저장
- 현재 상태는 이벤트들의 결과
전통적인 방식은 계좌의 현재 잔액만 필드에 저장하고, 입금이 일어날 때마다 그 필드를 직접 덮어쓴다.
| |
이벤트 소싱은 잔액을 직접 저장하지 않는다. 대신 “입금이 있었다"는 사실 자체를 이벤트로 쌓아두고, 현재 잔액이 필요할 때마다 그 이벤트들을 처음부터 재생해 계산한다.
| |
이벤트 소싱의 장점
flowchart LR
subgraph Events [이벤트 로그]
E1[입금 100]
E2[출금 30]
E3[입금 50]
E4[출금 20]
end
subgraph States [상태 재구성]
S1["시점1: 100"]
S2["시점2: 70"]
S3["시점3: 120"]
S4["현재: 100"]
end
E1 --> S1
E2 --> S2
E3 --> S3
E4 --> S4
- 완전한 이력: 모든 변경 기록 보존
- 시점 복원: 어떤 시점의 상태든 재구성 가능
- 감사 추적: 누가, 언제, 무엇을 했는지 추적
- 디버깅: 버그 재현이 쉬움
저장 공간은?
“모든 이벤트를 저장하면 공간이 부족하지 않나?”
마틴의 답은 이렇다: 저장 공간은 빠르게 저렴해지고 있으니, 더 이상 1960년대처럼 공간을 아껴야 하는 시대가 아니라는 것이다(Martin, Clean Architecture, 2017). 그럼에도 이벤트 양이 정말 부담이 된다면 다음 완화책을 쓸 수 있다:
- 일정 시점까지의 스냅샷 저장
- 오래된 이벤트 아카이브
- CQRS 패턴으로 읽기/쓰기 분리
세 패러다임의 교훈
flowchart TB
subgraph Paradigms [세 패러다임]
SP["구조적 프로그래밍goto 제거"]
OOP["객체 지향함수 포인터 제어"]
FP["함수형할당 제한"]
end
subgraph Lessons [교훈]
L1[제어 흐름의 직접 전환 규제]
L2[제어 흐름의 간접 전환 규제]
L3[할당의 규제]
end
SP --> L1
OOP --> L2
FP --> L3
| 패러다임 | 제거하는 것 | 얻는 것 |
|---|---|---|
| 구조적 | goto | 증명/테스트 가능성 |
| 객체 지향 | 함수 포인터 남용 | 의존성 제어 |
| 함수형 | 할당 | 동시성 안전 |
마틴은 이렇게 요약한다: 세 패러다임 모두 우리에게서 무언가를 빼앗는다. 권한을 부여하지 않는다(Martin, Clean Architecture, 2017).
아키텍처에 주는 교훈
1. 불변성을 최대화하라
| |
2. 가변성을 격리하라
flowchart TB
subgraph Pure [순수 영역]
direction TB
BL[비즈니스 로직]
V[검증]
C[계산]
end
subgraph Impure [불순 영역]
direction TB
DB[(데이터베이스)]
API[외부 API]
UI[UI]
end
Pure --> Impure
3. 이벤트 소싱을 고려하라
이벤트 소싱은 만능 해법이 아니라 특정 상황에 강한 도구다:
- 모든 시스템에 필요하진 않음
- 감사 추적이 중요한 도메인에 유용
- 이력과 복원이 필요한 경우 강력
핵심 요약
마틴은 이렇게 요약한다: 함수형 프로그래밍은 할당문에 대해 규칙을 부과한다(Martin, Clean Architecture, 2017).
| 항목 | 내용 |
|---|---|
| 핵심 개념 | 불변성 (Immutability) |
| 제거하는 것 | 할당문 |
| 해결하는 문제 | 동시성, 경쟁 조건 |
| 아키텍처 적용 | 가변성 분리, 이벤트 소싱 |
흔한 오해
“함수형 프로그래밍은 상태를 아예 다룰 수 없다”는 흔한 오해다. 실제로는 상태를 아예 없애는 것이 아니라, 상태 변경을 새 값 생성으로 대체하고 그 범위를 격리하는 것이다. 위 Counter 예제처럼 “상태가 바뀐 것처럼 보이는” 코드도, 실제로는 매번 새 불변 객체를 만들어 반환할 뿐이다. 또한 “함수형 언어를 써야만 이 원칙을 적용할 수 있다”는 것도 오해다 — Java, Python 등 명령형 언어에서도 final/불변 클래스 설계로 이 장의 원칙 대부분을 적용할 수 있다.
학습 목표
이 장을 읽은 후 다음을 할 수 있어야 한다.
- 순수 함수·참조 투명성이 무엇인지, 그리고 이것이 동시성 문제를 어떻게 근본적으로 제거하는지 설명할 수 있다.
- “가변성의 분리” 전략을 자신의 코드베이스에 어떻게 적용할지 설명할 수 있다.
- 이벤트 소싱이 적합한 상황과 부적합한 상황을 구분할 수 있다.
참고 자료
- Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.
- McCarthy, J. (1960). “Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”. Communications of the ACM, 3(4).
다음 파트에서는 이러한 패러다임 위에 구축되는 설계 원칙(SOLID)을 다룬다.
![Featured image of post [Clean Architecture] 13. 함수형 프로그래밍](/post/clean-architecture/functional-programming-immutability/wordcloud_hu_f2d00ed7d95670e1.webp)
![[Clean Architecture] 11. 구조적 프로그래밍](/post/clean-architecture/structured-programming-goto-elimination/wordcloud_hu_1e5cd083abaa560f.webp)
![[Clean Architecture] 12. 객체 지향 프로그래밍](/post/clean-architecture/object-oriented-programming-polymorphism/wordcloud_hu_66479534e61250c6.webp)
![[Clean Architecture] 13. 함수형 프로그래밍](/post/clean-architecture/functional-programming-immutability/wordcloud_hu_98c6895f66e673eb.webp)
![[Clean Architecture] 14. SOLID 원칙 서론](/post/clean-architecture/solid-principles-introduction/wordcloud_hu_5dd3e02a522cd4eb.webp)
![[Clean Architecture] 15. SRP: 단일 책임 원칙](/post/clean-architecture/srp-single-responsibility-principle/wordcloud_hu_7c9c1878b0d9c6e6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)
![[Philosophy] 시간의 본질: 계산적 관점에서 바라본 시간과 관찰자](/post/2024-10-17-on-the-nature-of-time/wordcloud_hu_de7748521f005f14.webp)
![[SoftwareDevelopment] DDD(도메인 주도 설계) 개념과 실무 적용](/post/2024-08-08-ddd/wordcloud_hu_eaa3a13ff021cc10.webp)