Chapter 2

프로그래밍 패러다임

  • 2.1 세 가지 패러다임
  • 2.2 패러다임은 권한을 빼앗는다
  • 2.3 구조적 프로그래밍과 증명
  • 2.4 goto의 몰락
  • 2.5 기능적 분해와 테스트
  • 2.6 OO의 세 기둥
  • 2.7 다형성과 의존성 역전
  • 2.8 함수형과 불변성
  • 2.9 가변성의 분리
  • 2.10 이벤트 소싱

프로그래밍 패러다임은 딱 세 가지뿐이고, 앞으로도 새로운 패러다임은 안 나올 거야. 왜 그런지, 각각이 아키텍처와 무슨 상관인지를 파볼 거야.

저자가 주장하는 핵심부터 짚자. 구조적 프로그래밍(1968, Dijkstra), 객체 지향 프로그래밍(1966, Dahl & Nygaard), 함수형 프로그래밍(1936, Church의 람다 계산법) -- 이 세 가지가 전부야. 재밌는 건 발견된 순서가 이름 순서와 다르다는 거야. 함수형이 제일 먼저고(1936), 객체 지향이 다음(1966), 구조적이 마지막(1968)이지.

이 세 패러다임의 공통점이 뭐냐면, 모두 뭔가를 **"할 수 있게 해주는 것"이 아니라 "하지 못하게 하는 것"**이야. 구조적은 goto를 못 쓰게, 객체 지향은 함수 포인터를 못 쓰게, 함수형은 할당을 못 하게. 패러다임은 프로그래머에게 새로운 능력을 주는 게 아니라 권한을 빼앗지. 빼앗을 수 있는 권한이 세 가지뿐이니까 -- 더 이상 새로운 패러다임이 나올 이유가 없다는 게 저자의 논리야.

구조적 프로그래밍부터 보자. 다익스트라(Dijkstra)는 프로그래밍이 어렵고, 프로그래머가 잘 못한다는 걸 깨달은 사람이야. 수학자 출신답게 프로그램이 올바르다는 걸 수학적으로 증명할 수 있어야 한다고 생각했지. 프로그램을 작은 단위로 분해하고, 각 단위의 정확성을 증명한 뒤 조합해서 전체의 정확성을 증명하려 했어. 유클리드 계층구조처럼. 이 과정에서 특정 제어 흐름 구조가 증명을 불가능하게 만든다는 걸 발견했고, 그게 바로 goto문이었어.

goto를 쓰면 제어 흐름이 이리저리 뒤얽혀서 프로그램의 논리를 따라갈 수가 없거든. 다익스트라는 goto의 "좋은 사용법"(순차, 분기, 반복)만 남기면 프로그램을 증명할 수 있다는 걸 알아냈어. 이게 그 유명한 1968년 논문 "Go To Statement Considered Harmful"의 배경이지. 뵘과 야코피니가 이미 수학적으로 증명해놨어 -- 모든 프로그램은 순차(sequence), 분기(selection), 반복(iteration) 세 가지 구조만으로 표현 가능하다고. goto가 사라지면서 프로그램을 모듈로 분해하는 **기능적 분해(functional decomposition)**가 가능해졌지.

그런데 다익스트라의 원래 꿈이었던 수학적 증명은 결국 실현되지 못했어. 대신 남은 건 테스트야. 다익스트라 본인의 명언이 있지 -- "테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 증명하지는 못한다." 소프트웨어는 수학이 아니라 과학에 가까워. 반증 가능한 단위로 분해할 수 있다는 것, 모듈과 컴포넌트를 기능적으로 분해하고 각각을 테스트로 반증 가능하게 만드는 것 -- 이게 아키텍처 수준에서 구조적 프로그래밍이 하는 역할이야.

객체 지향 프로그래밍(OO) 이야기로 넘어가자. OO의 세 기둥이라 불리는 캡슐화, 상속, 다형성을 하나씩 까봐야 해. 캡슐화 -- 많은 사람이 OO의 장점으로 먼저 꼽는데, C 언어도 완벽한 캡슐화가 가능했어. 헤더 파일에 함수 선언만 공개하고 구현은 .c 파일에 숨기면 데이터 은닉이 완벽하게 돼. 오히려 C++이 나오면서 멤버 변수를 헤더에 선언해야 하는 바람에 캡슐화가 약화됐다고 볼 수도 있지. 상속도 OO 이전에 비슷한 것이 있었어. C에서 구조체 안에 다른 구조체를 첫 번째 멤버로 넣으면 업캐스팅 비슷한 트릭이 가능했으니까. 다만 OO 언어가 이걸 훨씬 편리하고 안전하게 만들어준 건 맞아.

핵심은 다형성이야. 다형성도 OO 이전에 있었어. C에서 함수 포인터를 쓰면 다형적 행위를 구현할 수 있거든. 실제로 UNIX의 모든 I/O 디바이스 드라이버가 이런 식으로 동작하지 -- open, close, read, write, seek 다섯 개의 함수 포인터로 된 구조체를 각 디바이스가 구현하는 방식. 그런데 함수 포인터를 직접 다루는 건 위험해. OO 언어가 해준 건 이 위험한 함수 포인터를 안전하고 편리한 다형성으로 대체해준 거야. 그리고 이게 아키텍처에 미친 영향은 거대해.

다형성 이전에는 소스 코드의 의존성 방향이 제어 흐름의 방향을 따라갈 수밖에 없었어. 다형성이 생기면서 의존성의 방향을 마음대로 뒤집을 수 있게 됐지. 인터페이스를 끼워 넣으면, 소스 코드 의존성이 제어 흐름과 반대 방향으로 갈 수 있어. 이게 바로 **의존성 역전(Dependency Inversion)**이고, 저자가 OO의 진짜 핵심이라고 보는 거야. OO란 뭐냐 -- "다형성을 이용하여 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 얻는 것"이야. 이 힘이 있으면 아키텍트가 플러그인 아키텍처를 만들 수 있지.

마지막으로 함수형 프로그래밍. 핵심은 간단해 -- 변수의 값은 변하지 않아. 한번 할당되면 끝이야. 그게 아키텍처랑 뭔 상관이냐고? 경합 조건(race condition), 교착 상태(deadlock), 동시 업데이트 문제 -- 이 모든 동시성 문제는 가변 변수 때문에 생기는 거거든. 변수가 안 변하면 잠금도 필요 없고, 동시성 문제 자체가 사라져.

물론 완전한 불변성은 비실용적이야. 실용적인 접근은 애플리케이션 내부에서 가변 컴포넌트와 불변 컴포넌트를 분리하는 거지. 불변 컴포넌트는 순수 함수형으로 동작하고, 가변 컴포넌트는 트랜잭션 메모리 같은 실천법으로 보호해. 아키텍트의 전략은 가능한 한 많은 처리를 불변 컴포넌트로 밀어넣고, 가변 컴포넌트에서는 최소한의 코드만 남기는 거야.

**이벤트 소싱(Event Sourcing)**이라는 흥미로운 개념도 있어. 은행 계좌의 잔고를 저장하는 대신 모든 거래 내역을 저장하는 거지. 잔고가 필요하면 처음부터 모든 거래를 합산해. "상태"를 변경하는 게 아니라 이벤트를 추가만 하는 거야. CRUD에서 CR만 남는 셈이지. 소스 코드 버전 관리 시스템이 바로 이렇게 동작하잖아.

세 패러다임은 아키텍처와 직결돼. 다형성으로 아키텍처 경계를 넘나들고, 함수형으로 데이터 배치와 접근을 제어하고, 구조적으로 모듈의 알고리즘을 구성하지. 그리고 이 규율들은 1958년 이후로 변하지 않았어. 하드웨어는 엄청나게 바뀌었지만, 소프트웨어의 본질은 그대로야 -- 순차, 분기, 반복, 간접 참조.


정리

2장 읽고 기억할 거 세 가지:

  1. 패러다임은 능력을 주는 게 아니라 권한을 빼앗아. 구조적은 goto를, 객체 지향은 함수 포인터를, 함수형은 할당을 제한하지. 빼앗을 게 세 가지뿐이니 새로운 패러다임은 더 나올 이유가 없어.
  2. OO의 진짜 핵심은 다형성을 통한 의존성 역전이야. 캡슐화나 상속이 아니라, 소스 코드 의존성의 방향을 마음대로 뒤집을 수 있다는 것 -- 이게 플러그인 아키텍처의 근간이지.
  3. 불변성은 동시성 문제를 근본적으로 해결해. 완전한 불변성은 비실용적이지만, 가변/불변 컴포넌트를 분리하고 가변 영역을 최소화하는 전략이 아키텍처의 핵심 무기야.