세 원전을 한 장으로 잇는 설계 정전 3권 정독의 심화편이다. 종합본이 이 논문을 3개 절로 압축했다면, 여기서는 10개 장 전체를 따라간다. 인용부호 안의 문장은 전부 원문(2006년 2월 6일판)이다.
이 논문의 FRP는 Functional Relational Programming이다. RxSwift·Combine 계열의 Functional Reactive Programming과 완전히 다르다. 검색할 때 반드시 "Out of the Tar Pit"을 함께 넣어야 한다.
1. 문제 제기 — 무엇과 싸우는가
논문 제목의 "tar pit(타르 늪)"은 Brooks의 The Mythical Man-Month 첫 장에서 온 이미지다. 거대한 짐승들이 허우적대다 빠져나오지 못하는 늪 — 대형 소프트웨어 프로젝트의 은유다.
Moseley와 Marks의 진단은 단호하다. "Complexity is the single major difficulty in the successful development of large-scale software systems." — 복잡도는 대규모 소프트웨어 시스템 개발을 성공시키는 데 있어 단일 최대 난점이다. 그리고 복잡도가 위험한 이유는 딱 하나다 — 우리가 시스템을 이해하려는 시도를 방해하기 때문이다.
2. 우리가 시스템을 이해하는 두 가지 방법 🔥 핵심 20%
3장이 이 논문의 인식론적 토대다. 우리가 코드가 맞는지 확인하는 방법은 둘뿐이다.
"Of the two informal reasoning is the most important by far."
이유가 두 개다. 첫째, 테스트에는 본질적 한계가 있다. 둘째, 비공식적 추론은 개발 과정에 내재해 있어서 언제나 사용된다 — 코드를 읽는 순간 이미 하고 있다.
결정적인 논거
세 번째 이유가 가장 강하다. 두 방법이 서로 다른 일을 한다는 것이다.
테스트의 두 가지 근본 한계
"a test (of any kind) that uses one particular set of inputs tells you nothing at all about the behaviour of the system or component when it is given a different set of inputs."
그래서 테스트에 대한 걱정은 언제나 "올바른 테스트를 했는가?"이고, 확실한 답은 오직 부정형으로만 온다 — 시스템이 깨질 때.
Dijkstra의 유명한 문장도 인용한다 — "testing is hopelessly inadequate ... it can be used very effectively to show the presence of bugs but never to show their absence." 그리고 논문은 한 줄 덧붙인다. "We agree with Dijkstra. Rely on testing at your peril."
"It is precisely because of the limitations of all these approaches that simplicity is vital. When considered next to testing and reasoning, simplicity is more important than either. Given a stark choice between investment in testing and investment in simplicity, the latter may often be the better choice because it will facilitate all future attempts to understand the system — attempts of any kind."
"테스트가 쓸모없다"가 아니다. 논문은 "모든 접근법에 한계가 있으므로 테스트와 추론을 함께 쓰는 것이 현명할 때가 많다"고 명시한다. 주장은 우선순위에 관한 것이다 — 단순함이 둘 다를 돕는다.
3. 상태 — 가장 큰 범인 🔥 핵심 20%
4.1절은 아주 일상적인 장면으로 시작한다. 논문에서 가장 유명한 도입부다.
소프트웨어 고객센터에 전화해서 "다시 해 보세요", "문서를 다시 여세요", "프로그램을 재시작하세요", "컴퓨터를 재부팅하세요", "재설치하세요", 심지어 "OS를 재설치하고 프로그램을 다시 까세요"를 들어 본 사람은, 이미 상태가 신뢰성 있고 이해 가능한 소프트웨어를 쓰는 데 어떤 문제를 일으키는지 직접 경험한 것이다.
그리고 인과를 역순으로 짚는다 — 이 말들이 익숙한 이유는 실제로 자주 문제를 해결하기 때문이고, 자주 해결되는 이유는 많은 시스템이 상태 처리에 오류를 갖고 있기 때문이며, 그런 오류가 많은 이유는 상태의 존재가 프로그램을 이해하기 어렵게 만들기 때문이다.
"껐다 켜보세요"가 통하는 이유를 이렇게 설명한 글은 드물다. 그건 상태를 날리는 행위다.
논문은 각주로 범위를 좁힌다. "By 'state' we mean mutable state specifically" — 가변 상태만을 말하며, 논리형 언어의 단일 대입 변수 같은 불변 값은 제외한다. 즉 "변수를 쓰지 마라"가 아니라 "바뀌는 것을 줄여라"다.
상태가 테스트를 무너뜨리는 방식
"a test (of any kind) on a system or component that is in one particular state tells you nothing at all about the behaviour of that system or component when it happens to be in another state."
그러면 실무는 어떻게 하고 있는가? 논문의 묘사가 정직하다.
- 시스템을 "깨끗한" 또는 "초기" 상태(대개는 숨겨져 있다)로 띄운다.
- 테스트 입력으로 원하는 테스트를 수행한다.
- 그리고 "내부 숨은 상태와 무관하게 같은 입력이면 늘 같게 동작할 것"이라고 가정한다.
"In essence, this approach is simply sweeping the problem of state under the carpet."
그리고 왜 그러는지도 인정한다 — 첫째로 대개 그렇게 해도 넘어가지기 때문이고, 더 결정적으로는 복잡한 내부 숨은 상태를 가진 시스템을 테스트할 때 다른 대안이 없기 때문이다. 문제는 "항상 넘어가지지는 않는다"는 것.
상태가 추론을 무너뜨리는 두 가지 방식
① 지수적 폭발. 문장이 짧다. "for every single bit of state that we add we double the total number of possible states." 상태를 1비트 추가할 때마다 가능한 전체 상태 수는 두 배가 된다.
final class UploadView {
var isLoading = false // 2
var hasError = false // 4
var isEditing = false // 8
var isSelected = false // 16
var isOffline = false // 32
var didAuth = false // 64
// ... 여기까지 64가지. 30개면 10억 가지가 넘는다.
// 테스트에서 확인하는 건 그중 몇 개일까?
}불리언 여러 개를 하나의 열거형으로 바꾸면 불가능한 조합이 타입 수준에서 사라진다.
enum UploadState {
case idle
case loading
case failed(Error)
case done(URL)
}
final class UploadView {
var state: UploadState = .idle
}② 오염(contamination). 이게 더 무섭다. 논문의 논지는 이렇다 — 상태 없는 프로시저는 이해하기 쉬울 거라고 기대하지만, 대체로 그렇지 않다.
"it is our belief that the single biggest remaining cause of complexity in most contemporary large systems is state, and the more we can do to limit and manage state, the better."
4. 제어와 코드량 🔥 핵심 20%
제어 — 우리가 시키지도 않은 순서
제어(control)는 일이 일어나는 순서다. 논문의 지적은 이렇다 — 우리는 대개 순서에 관심이 없는데, 언어가 순서를 말하도록 강요한다.
a := b + 3
c := d + 2
e := f * 4프로그래머는 값들 사이의 관계만 말하고 싶었다. 그런데 임의의 제어 흐름을 골라야 했다. 논문의 표현으로는 "문제를 과잉 명세(over-specify)하도록 강요당한 것"이다.
"first an artificial ordering is being imposed, and then further work is done to remove it."
먼저 인위적 순서가 부과되고, 그다음 컴파일러가 그것을 제거하려고 일한다. 그리고 코드를 읽는 사람도 컴파일러가 한 일을 그대로 반복해야 한다 — 일단 순서가 유의미하다고 가정하고, 더 살펴본 뒤 아니라고 판단해야 한다. 이 판단에서 나오는 실수가 아주 미묘하고 찾기 어려운 버그가 된다.
논문이 명확히 한다 — "the problem is not in the text of the program above", 어차피 코드는 어떤 순서로든 적혀야 한다. 문제는 "텍스트 순서가 곧 실행 순서"라고 정의한 언어의 의미론에 있다. 같은 텍스트를 그런 순서를 정의하지 않는 언어의 프로그램으로 볼 수도 있다.
동시성 — "이보다 더 나빠질 수는 없다"
"Running a test in the presence of concurrency with a known initial state and set of inputs tells you nothing at all about what will happen the next time you run that very same test with the very same inputs and the very same starting state... and things can't really get any worse than that."
상태는 "다른 상태에서는 모른다"였고, 동시성은 "같은 상태, 같은 입력에서도 모른다"이다. 테스트가 줄 수 있는 정보가 0이 되는 지점이다.
코드량 — 이차적이지만 무시 못 할
코드가 많으면 복잡하다. 다만 논문은 이것을 대체로 이차적 효과로 본다 — "much code is simply concerned with managing state or specifying control." 그래도 따로 다루는 이유가 두 가지다.
Brooks는 복잡도가 크기에 대해 비선형적으로 증가한다고 했다. 논문은 "현재 대부분의 시스템에서는 그 말이 맞다"고 동의하되(단, "essential"이라는 단어에는 반대한다), Dijkstra를 인용해 조건을 붙인다 — "추상화 능력을 적절히 적용하면, 프로그램을 이해하는 데 드는 지적 노력이 길이에 비례하는 것 이상으로 커질 필요는 없다."
그리고 자기 입장을 밝힌다 — 상태와 제어를 효과적으로 관리하면, 복잡도가 코드량에 대해 비선형으로 는다는 것도 훨씬 불분명해진다.
5. 나머지 원인들 — 3가지 원리로 수렴한다
4.4절이 짧지만 밀도가 높다. 중복 코드, 죽은 코드, 불필요한 추상화, 놓친 추상화, 나쁜 모듈성, 부실한 문서 — 이 모든 것이 세 가지 원리로 환원된다.
| 원리 | 내용 |
|---|---|
| 복잡도가 복잡도를 낳는다 Complexity breeds complexity | 시스템을 명확히 이해할 수 없기 때문에 생기는 이차적 복잡도. 중복이 대표 사례다 — (상태·제어·코드량 때문에) 그 기능이 이미 있는지 알 수 없거나, 있는 것이 정확히 원하는 걸 하는지 판단하기에 너무 복잡하면 중복해서 만들려는 강한 경향이 생긴다. 시간 압박이 있으면 특히 그렇다. |
| 단순함은 어렵다 Simplicity is Hard | 단순함을 얻는 데는 상당한 노력이 든다. 첫 번째 해법은 대개 가장 단순한 해법이 아니다 — 기존 복잡도가 있거나 시간 압박이 있으면 더욱. 그리고 결론이 강하다 — "단순함은 인식되고, 추구되고, 소중히 여겨질 때만 얻어진다." |
| 권력은 부패한다 Power corrupts | 언어가 강제로 보장해 주지 않으면(= 언어의 힘을 제한하지 않으면) 실수와 남용은 반드시 일어난다. GC가 좋은 이유는 수동 메모리 관리라는 "권력"을 빼앗기 때문이다. 상태에도 정확히 같은 원리가 적용된다 — 그래서 상태를 "허용만" 하는 언어(ML, Scheme 등)도 경계해야 한다. 아무리 사용을 권장하지 않더라도. |
var보다let이 좋은 이유는 취향이 아니라 보장이다. 강제되지 않는 규칙은 언젠가 깨진다.struct가class보다 안전한 이유도 같다 — 공유 가변 참조라는 권력이 없다.- "우리 팀은 전역 변수 안 씁니다"는 컨벤션이고, 컨벤션은 언어의 보장이 아니다.
6. 패러다임 채점 — OO · FP · 논리형
5장은 기존 패러다임을 상태와 제어 두 잣대로만 채점한다.
객체지향
논문은 캡슐화를 부정하지 않는다. 객체 = 상태 + 그 상태를 다루는 절차라는 발상은 추상 자료형(ADT)의 계승이며, "덜 구조화된 명령형 스타일과 비교하면 OOP 접근의 주요 강점 중 하나"라고 명시한다. 접근을 메서드로 제한해 무결성 제약을 강제할 수 있기 때문이다.
문제는 그다음이다.
정체성(identity) — OO가 상태와 함께 가져오는 두 번째 부담
이 대목이 자주 놓친다. OOP에서 객체는 속성과 무관하게 고유하게 식별되는 존재다. 논문의 용어로 내포적 정체성(intensional identity)이며, 속성이 같으면 같은 것으로 보는 외연적 정체성(extensional identity)과 대비된다.
"In a sense, object identity can be considered to be a rejection of the 'relational algebra' view of the world in which two objects can only be distinguished through differing attributes."
final class Money { // 값인데 객체다
let amount: Decimal
let currency: String
}
let a = Money(amount: 100, currency: "KRW")
let b = Money(amount: 100, currency: "KRW")
a === b // false — 정체성 비교
a == b // 직접 구현해야 한다 (Value Object 기법)
// 논문의 지적: 도메인별 동등성을 직접 만들면
// 그것이 동치관계의 표준 성질(예: 추이성)을 만족한다는 보장이 사라진다.
struct Money2: Equatable { // ✅ 정체성 개념 자체가 없다
let amount: Decimal
let currency: String
}논문의 결론은 이렇다 — 정체성이라는 추가 개념은 상태 사용에서 곧바로 파생되며, 패러다임의 일부라 피할 수 없다. 그리고 두 동일성 개념 사이를 정신적으로 오가야 하므로 추론 부담이 늘고, 혼동에서 심각한 오류가 나올 수 있다.
함수형
"It is this cast iron guarantee of referential transparency that obliterates one of the two crucial weaknesses of testing."
테스트의 약점 두 개 중 "다른 상태에서는 모른다"가 통째로 사라진다. 남는 약점은 입력 문제 하나뿐이다. 그리고 결정적인 성질 — "결과에 영향을 줄 수 있는 모든 것이 언제나 실제 인자에 즉시 드러나 있다."
FP의 약점 — 논문이 솔직하게 인정하는 부분
5.2.4절은 FP 옹호론이 잘 인용하지 않는 대목이다. 상태가 특정한 종류의 모듈성을 제공한다는 반론을 논문은 "certainly true"라고 받는다.
Haskell의 모나드는 "elegant"하고 위 문제를 실제로 피하게 해 준다고 인정한다. 그러나 "매우 쉽게 남용되어 Haskell 안에 상태 있고 부수효과 있는 하위 언어를 만들 수 있다"고 경고한다 — 타입으로 표시된다는 점만 다를 뿐, 우리가 피하려던 문제를 그대로 다시 들여올 수 있다.
그리고 FP의 약점을 한 문장으로 정리한다 — "the main weakness of functional programming is the flip side of its main strength": 상태를 유지해야만 하는 시스템을 만들 때 문제가 생긴다는 것.
논리형
논리형 프로그래밍은 "제어에서 오는 복잡도를 벗어날 가능성"을 제시한다는 점에서 논문이 가장 높이 사는 패러다임이다. 순수 논리 프로그래밍은 문제와 해의 조건을 공리로 진술할 뿐이고, 실행은 각 해에 대한 형식 증명을 구성하는 것과 같다.
모든 순수 Prolog 프로그램은 두 가지로 읽힌다 — ① 순수한 논리 공리의 집합, ② 목표를 증명하려고 특정 순서로 적용되는 명령의 나열. 그리고 결정적인 문제 — "하나의 Prolog 프로그램이 첫 번째 방식으로는 옳고, 두 번째 방식으로는 (예컨대 비종료 때문에) 틀릴 수 있다."
그래서 공리를 쓰는 동안 운영적 해석을 신경 써야 한다. cut 같은 기능은 이 문제를 더 키운다 — "cut의 효과를 오해하는 것이 Prolog 프로그램 버그의 주요 원인으로 알려져 있다."
7. 본질과 우발을 다시 긋다 🔥 핵심 20%
6장에서 논문은 Brooks의 용어를 이어받되 더 엄격하게 다시 정의한다.
Essential Complexity — "is inherent in, and the essence of, the problem (as seen by the users)."
Accidental Complexity — "is all the rest — complexity with which the development team would not have to deal in the ideal world (e.g. complexity arising from performance issues and from suboptimal language and infrastructure)."
그리고 자기 정의가 통상적 용법보다 엄격하다는 걸 명시한다 — "strictly essential to the users' problem"이지, 어떤 구현된 시스템에 본질적이거나 소프트웨어 일반에 본질적인 것이 아니다.
판별 규칙 두 개
"if the user doesn't even know what something is (e.g. a thread pool or a loop counter — to pick two arbitrary examples) then it cannot possibly be essential by our definition."
"the 'have to' part of this observation is critical — if there is any possible way that the team could produce a system that the users will consider correct without having to be concerned with a given type of complexity, then that complexity is not essential."
이 잣대를 오늘 코드에 대 보면 결과가 꽤 불편하다.
| 우리 코드에 있는 것 | 분류 | 이유 |
|---|---|---|
| "대출 한도는 연소득의 3배를 넘을 수 없다" | 본질 | 사용자가 아는, 사용자의 규칙 |
| "주문은 결제 후에만 배송으로 넘어간다" | 본질 | 사용자의 업무 규칙 |
| Redis 캐시 · CDN | 우발 | 성능 때문. 사용자가 요구한 적 없다 |
| DB 인덱스 · 커넥션 풀 · 샤딩 | 우발 | 같은 이유 |
| ORM · DTO ↔ 엔티티 매핑 | 우발 | 인프라가 만든 것 |
| 스레드 · 락 · 비동기 큐 | 우발 | 규칙 ①에 정확히 해당 — 사용자는 이게 뭔지 모른다 |
| 비트 · 바이트 · 트랜지스터 · 전기 · 컴퓨터 자체 | 우발 | 논문이 직접 든 예. 사용자 문제와 무관하다 |
Brooks는 "The complexity of software is an essential property, not an accidental one"이라고 썼고, Booch 등도 동의했다. 논문의 응답은 두 단어로 시작한다 — "We disagree."
근거는 두 개다. ① 복잡도는 소프트웨어의 내재적 속성이 아니다 — 단순하면서도 여전히 소프트웨어인 것을 쓰는 게 완벽히 가능하다. ② 기존 소프트웨어에서 보이는 복잡도의 상당수는 (문제에) 본질적이지 않다.
논문도 인정한다. "현실 세계에서는 모든 가능한 방법이 실용적이지는 않으므로, 어떤 실제 개발이든 어느 정도의 우발적 복잡도를 상대해야 한다. 이 정의는 그것을 부정하려는 게 아니라 — 그것의 이차적 성격을 식별하려는 것이다."
즉 필요 없다는 게 아니라 본질과 섞이지 않게 관리하라는 것이다. 이게 뒤에 나올 FRP의 전부다.
8. 이상적 세계 — 무엇이 진짜 본질적 상태인가
7.1절은 이 논문에서 가장 자주 잘못 요약되는 부분이다. "입력 데이터가 본질적 상태"라고 흔히 쓰는데, 원문은 훨씬 엄격하다.
"사용자가 이 값을 수정할 수 있는데도 파생이라고?" 논문의 답이 정교하다. 파생 데이터의 가변성은 그것을 만든 함수에 역함수가 있을 때만 말이 된다(없다면 — 가변이므로 — 그건 지속적으로 파생된다고 볼 수 없고 사실상 입력이다). 역함수는 파생 데이터가 입력 데이터의 단순한 재구조화일 때 흔히 존재하고, 그럴 때는 파생 데이터의 수정을 원본 상태의 대응하는 수정으로 그대로 취급하면 된다.
9. 처방 — Avoid, Separate 🔥 핵심 20%
"Our recommendations for dealing with complexity (as exemplified by both state and control) can be summed up as: Avoid · Separate"
Avoid — 절대적으로, 진정으로 본질적이지 않은 상태와 제어를 피한다. Separate — 그래도 남는 것(진짜 본질이거나, 본질은 아니지만 실용적으로 유용한 것)은 나머지로부터 분리해 낸다.
"There is nothing particularly profound in these recommendations, but they are worth stating because they are emphatically not the way most software is developed today."
— 특별히 심오하지 않다. 다만 지금 대부분의 소프트웨어가 이렇게 만들어지지 않고 있기 때문에 말할 가치가 있다.
뿌리도 밝힌다. Prolog 공동 개발자 Kowalski의 1979년 논문 제목이 곧 그 아이디어다.
Algorithm = Logic + Control
무엇을 원하는지(로직)와 어떻게 찾을지(제어)를 분리하라는 것. 논문은 "이 분리가 우리가 권고하는 것의 핵심에 가깝다"고 쓴다.
10. 관계 모델 — 접근 경로 독립성
8장은 "그래서 본질적 상태를 무엇으로 표현할 것인가"에 답한다. 답은 관계(relation)이고, 이유는 성능이나 익숙함이 아니라 접근 경로 독립성(access path independence)이다.
"The idea of structuring data using relations is appealing because no subjective, up-front decisions need to be made about the access paths that will later be used to query and process the data."
논문의 예 — 직원과 부서
직원과 그들이 일하는 부서 정보를 표현한다고 하자. 계층 모델에서는 시작부터 주관적 선택을 강요당한다.
| 선택 | 쉬워지는 질문 | 어려워지는 질문 |
|---|---|---|
| 부서가 상위 부서가 직원을 "담고" 있다 | 이 부서의 직원 전부 → 경로를 따라가면 된다 | 이 직원의 부서 → 전 부서를 탐색해야 한다 |
| 직원이 상위 직원이 부서를 "담고" 있다 | 이 직원의 부서 | 이 부서의 직원 전부 |
| 관계형 | 둘 다 | 둘 다 — 경로가 애초에 없다 |
네트워크 모델은 양방향 경로를 허용해 문제를 완화했다. 그런데 논문의 지적이 결정적이다 — "미래에 필요해질 모든 접근 경로를 미리 예측하는 것은 불가능하다." 그래서 언제나 예견된 1차 검색 요구와 예견되지 않은 2차 검색 요구 사이에 격차가 생긴다.
그리고 OOP는 정확히 같은 선택을 강요한다
같은 예를 객체지향으로 풀면 선택지는 셋이다 — ① Employee가 Department 참조를 갖는다, ② Department가 Employee 참조들의 집합을 갖는다, ③ 둘 다.
③을 고르면 "잘해야 중복 참조를 유지하는 추가 작업에 노출되고, 최악의 경우 버그에 노출된다." 그리고 마무리가 세다 — "There are disturbing similarities between the data structuring approaches of OOP and XML on the one hand and the network and hierarchical models on the other." OOP와 XML의 데이터 구조화 방식은 네트워크·계층 모델과 불안할 정도로 닮았다.
final class Employee {
var department: Department? // 참조 ①
}
final class Department {
var employees: [Employee] = [] // 참조 ②
}
// 이제 이 둘을 항상 맞춰야 한다 — 한 곳만 고치면 조용히 깨진다
func move(_ e: Employee, to d: Department) {
e.department?.employees.removeAll { $0 === e }
e.department = d
d.employees.append(e) // 🐞 이 줄을 빠뜨리면?
}struct Employment: Hashable { // 본질적 상태: 관계 하나
let employeeID: EmployeeID
let departmentID: DepartmentID
}
var employment: Set<Employment> = []
// 두 방향 질문 모두 파생이다 — 저장하지 않는다
func members(of d: DepartmentID) -> [EmployeeID] {
employment.filter { $0.departmentID == d }.map(\.employeeID)
}
func department(of e: EmployeeID) -> DepartmentID? {
employment.first { $0.employeeID == e }?.departmentID
}
func move(_ e: EmployeeID, to d: DepartmentID) {
employment = employment.filter { $0.employeeID != e }
employment.insert(Employment(employeeID: e, departmentID: d))
}논문은 Chen의 ER 모델링과 비교하며 관계 모델의 또 다른 장점을 든다. 개체(entity)와 관계(relationship)를 구분하지 않는다는 것이다. 이유는 실용적이다 — "무언가가 개체인지 관계인지는 매우 주관적인 질문일 수 있기 때문에 그런 구분을 쓰는 것은 문제가 될 수 있다."
11. FRP 아키텍처
9장이 논문의 종착지다. 시스템을 네 덩어리로 강제 분리한다. 이름은 원문 그대로다.
각 칸의 규칙
| 구성 요소 | 규칙 |
|---|---|
| ① 본질적 상태 Essential State | base relvar의 이름과 타입만 명세한다. "FRP에서 모든 상태는 오직 관계로만 저장된다 — 예외는 없다." 그리고 사용자가 직접 입력한 것일 때만 본질적 상태로 다루기를 강하게 권장한다. |
| ② 본질적 로직 Essential Logic | 파생 relvar 정의(관계 대수) + 무결성 제약(항상 참이어야 하는 불리언 식) + 임의의 순수 사용자 정의 함수. 상태를 수정하려는 모든 시도는 언제나 무결성 제약의 적용을 받는다. |
| ③ 우발적 상태와 제어 Accidental State and Control | 서로를 어떤 식으로도 참조할 수 없는 고립된 성능 "힌트"들의 나열. 선언적이어야 한다. 예: "이 파생 relvar는 매번 계산하지 말고 실제로 저장하라", "이 속성 부분집합은 따로 저장하라", "인덱스를 써라", "이건 즉시 계산하고 저건 지연 계산하라". |
| ④ Other Feeders / Observers | Feeder는 입력을 관계 대입으로 변환한다. Observer는 (파생) relvar의 변화에 반응해 출력한다 — live query이자 트리거 역할. 둘 다 최소한이어야 하며, 관계로/에서의 필요한 변환만 수행한다. |
본질적 로직을 설계할 때 "absolutely NO consideration is paid to performance issues of any kind here." 그리고 이어서 — "'성능을 위한 비정규화(denormalization for performance)' 같은 개념은 이 맥락에서 전혀 말이 되지 않는다." 물리 저장이 여기서 명세하는 관계 구조를 그대로 따라간다는 가정이 깔려 있는데, 전혀 그렇지 않기 때문이다.
논문이 명시적으로 금지한다. "hybrid feeders / observers should not act as triggers which directly update the essential state" — 그렇게 하면 정의상 파생 상태, 즉 우발적 상태를 만드는 것이기 때문이다. 오늘날 "이벤트를 받아서 다른 테이블을 업데이트하는" 트리거·리스너가 정확히 이 함정이다.
12. 오늘의 코드로 번역하기
FRP 전체를 그대로 도입할 일은 거의 없다. 그런데 세 가지는 언어와 무관하게 그대로 쓸 수 있다.
① 파생 가능한 값은 저장하지 않는다
final class Cart {
private(set) var items: [Item] = []
private(set) var total: Int = 0 // ← 파생 가능한데 저장했다
private(set) var isEmpty: Bool = true // ← 이것도
private(set) var discount: Int = 0 // ← 이것도
func add(_ item: Item) {
items.append(item)
total += item.price // 세 줄을 매번 맞춰 줘야 한다
isEmpty = false
discount = computeDiscount()
}
func remove(at i: Int) {
items.remove(at: i)
total -= items[i].price // 🐞 이미 지운 뒤에 읽는다
// 🐞 isEmpty · discount 갱신을 잊었다
}
}struct Cart {
private(set) var items: [Item] = [] // ① 본질적 상태 — 사용자가 넣은 것
// ② 본질적 로직 — 저장하지 않고 그때그때 만든다
var total: Int { items.reduce(0) { $0 + $1.price } }
var isEmpty: Bool { items.isEmpty }
var discount: Int { computeDiscount(items) }
mutating func add(_ item: Item) { items.append(item) }
mutating func remove(at i: Int) { items.remove(at: i) }
// 동기화할 게 없으므로 동기화 버그도 없다
}② 무결성 제약을 한곳에 모은다
struct OrderBook {
private(set) var orders: [OrderID: Order] = [:]
// 무결성 제약 — 항상 참이어야 하는 불리언 식들
private static func invariants(_ s: [OrderID: Order]) -> Bool {
s.values.allSatisfy { $0.total >= 0 }
&& s.values.filter { $0.status == .shipped }.allSatisfy { $0.paidAt != nil }
}
// 상태를 바꾸는 유일한 통로 — 모든 변경이 제약의 적용을 받는다
mutating func apply(_ change: (inout [OrderID: Order]) -> Void) throws {
var next = orders
change(&next)
guard Self.invariants(next) else { throw OrderError.invariantViolated }
orders = next
}
}③ 성능은 "결과를 바꿀 수 없는 자리"에만 넣는다
struct Cart {
private(set) var items: [Item] = []
var total: Int { Self.computeTotal(items) }
// 정답을 만드는 순수 함수 — 캐시의 존재를 모른다
static func computeTotal(_ items: [Item]) -> Int {
items.reduce(0) { $0 + $1.price }
}
}
// ③ 우발적 상태 — 완전히 분리된 계층
final class TotalCache {
private var memo: [Cart.ID: Int] = [:]
func total(of cart: Cart) -> Int {
if let hit = memo[cart.id] { return hit }
let v = Cart.computeTotal(cart.items) // ← 항상 같은 정답 함수를 부른다
memo[cart.id] = v
return v
}
func invalidateAll() { memo.removeAll() } // 지워도 결과는 같다. 느려질 뿐.
}"이 캐시를 통째로 지우면 결과가 달라지는가?" 달라진다면 우발적 상태가 본질적 상태에 섞인 것이다 — 분리 대상이다. 달라지지 않는다면 제대로 분리된 것이다.
이 논문의 자식들
- 이 프로퍼티는 다른 값으로부터 계산할 수 있는가? → 가능하면 저장하지 말 것
- 이 불리언들이 동시에 참일 수 있는 조합이 실제로 존재하는가? → 아니면 열거형으로
- 이 캐시를 지우면 결과가 달라지는가? → 달라지면 본질에 우발이 섞인 것
- 이 순서가 정말 필요한가, 아니면 언어가 시켜서 쓴 것인가?
- 사용자가 이게 뭔지 아는가? → 모르면 우발적 복잡도다. 격리 대상.
이미 커져 버린 기존 시스템이라면
FRP는 새로 짓는 시스템을 전제한다. 그런데 논문은 그 분리를 직접 적용할 수 없는 경우를 위해 결론부에 별도의 조언을 남긴다. 지금 우리 대부분이 여기 해당한다.
"In cases (such as existing large systems) where this separation cannot be directly applied we believe the focus should be on avoiding state, avoiding explicit control where possible, and striving at all costs to get rid of code."
— 우선순위가 셋으로 정해져 있다. ① 상태를 피하고, ② 가능한 곳에서 명시적 제어를 피하고, ③ 무슨 수를 써서라도 코드를 없앤다. 아키텍처를 갈아엎으라는 말이 아니다. 오늘 PR에서 할 수 있는 일들이다.
그래서 늪에서 나가는 길은 무엇인가
제목이 던진 질문에 논문은 마지막 문단에서 직접 답한다. 그리고 그 답은 자기가 66쪽에 걸쳐 만든 FRP가 아니다.
"So, what is the way out of the tar pit? What is the silver bullet? . . . it may not be FRP, but we believe there can be no doubt that it is simplicity."
— 그래서 타르 늪에서 나가는 길은 무엇인가? 은탄환은 무엇인가? … 그것이 FRP는 아닐지 모르지만, 단순함이라는 데에는 의심의 여지가 없다고 우리는 믿는다.
Brooks의 "은탄환은 없다"에 대한 최종 응답이자, 이 논문 전체의 요약이다. FRP는 수단이고, 목적은 단순함이다. 그래서 관계 모델을 안 쓰더라도, Swift로 짜더라도, 이 논문이 요구하는 것은 그대로 남는다 — 상태를 줄이고, 제어를 줄이고, 남은 것은 격리하라.