#4 Pragmatic Paranoia
4장 실용주의 편집증
💻 실용주의 프로그래머: Topic 23 계약에 의한 설계 (Design by Contract, DBC)
1. 핵심 개념 — “좋은 프로그램은 신뢰의 계약으로 이루어진다”
“상식과 정직만큼 사람을 놀라게 하는 건 없다.” — 랄프 월도 에머슨
소프트웨어는 사람과 시스템의 약속이다. ‘계약에 의한 설계(Design by Contract)’ 는 그 약속을 명문화하여 코드로 보증하는 방법론이다.
이는 단순한 패러다임이 아니라, “소프트웨어 구성 요소 간의 신뢰를 수학적으로 정의”하려는 철학이다.
2. 배경 — “사람의 계약에서 시스템의 계약으로”
우리는 이미 사회적으로 수많은 계약 속에 살아간다.
근로 계약: “회사는 급여를 지급하고, 직원은 의무를 수행한다.”
임대 계약: “사용 권리와 책임이 상호 명시된다.”
이처럼 권리와 의무의 균형을 명확히 규정하는 것이 계약이다. 소프트웨어에서도 같은 개념이 적용된다. → “함수와 호출자 간의 약속을 코드 수준에서 정의하자.”
3. DBC의 기본 구조 — “선행 조건 · 후행 조건 · 불변식”
버트란드 마이어(Bertrand Meyer)는 이를 에펠(Eiffel) 언어에 처음 도입하며 이렇게 정의했다.
“루틴(메서드)은 어떤 일을 하기 전에 전제 조건을 만족해야 하고, 일을 마친 후에는 결과 조건을 보장해야 하며, 그 사이의 상태는 항상 일관되어야 한다.”
선행 조건 (Precondition)
루틴이 호출되기 전에 만족해야 하는 조건
입력 금액이 0보다 커야 함
후행 조건 (Postcondition)
루틴이 종료된 후 보장해야 하는 상태
거래가 계좌에 추가됨
클래스 불변식 (Class Invariant)
객체의 내부 상태가 항상 유지되어야 하는 규칙
계좌 잔액 ≥ 0
4. 코드 예시 — Clojure 스타일
:pre → 선행 조건: 금액이 양수이며, 계좌가 열려 있어야 함
:post → 후행 조건: 트랜잭션 목록에 거래가 포함되어야 함
💡 즉, 함수의 계약이 위반되면 예외를 던진다. → “이 함수는 거짓말을 하지 않는다.”
5. 예외 발생 — 계약 위반의 결과
금액이 0 이하라면 “선행 조건 위반” 예외가 발생한다. 이것은 단순 버그가 아니라 계약 위반의 증거다.
DBC는 “누구의 책임인가?”를 명확히 한다.
호출자가 선행 조건을 어겼다면 호출자 책임.
피호출자가 후행 조건을 어겼다면 함수 책임.
6. 다른 언어에서의 DBC 예시
✅ Elixir
→ “guard clause”로 DBC 구현 (명시적 전제 조건)
✅ Kotlin (assert 활용)
✅ TypeScript (타입 기반 계약)
7. DBC와 테스트 주도 개발(TDD)의 비교
초점
외부 동작의 검증
내부 계약의 보증
단위
테스트 케이스
코드 자체의 선언
실패 시점
실행 후
실행 중 즉시
목표
기능 검증
신뢰성 확보
둘 다 “품질 보증”을 위한 도구지만, DBC는 계약을 코드 안에 포함시킨 TDD라고 볼 수 있다.
8. 의미론적 불변식 (Semantic Invariant)
불변식은 단순한 변수 상태를 넘어서 **“의미 수준의 일관성”**을 지켜야 한다. 예를 들어, 신용카드 결제 시스템에서
“거래 승인 없이 결제 처리가 되어선 안 된다.” 는 규칙이 바로 의미론적 불변식이다.
9. 확장된 개념 — 동적 계약과 에이전트
정적 계약: 코드 컴파일 시점에 보장
동적 계약: 런타임에 조건을 검증 (테스트/모니터링 기반)
에이전트(Agent): 계약을 자동으로 체결하고 위반 시 복구하는 지능형 컴포넌트
즉, 미래의 시스템에서는 AI 기반 계약 시스템이 스스로 조건을 감시하고, 위반을 탐지·복구할 수도 있다.
10. 연습 문제 요약
연습 14
믹서 인터페이스에 선행/후행 조건, 불변식 추가하기
연습 15
수열(0, 5, 10, 15…)에 포함된 숫자 개수 계산 — 계약 기반 구현
11. 관련 항목
항목 24. 죽은 프로그램은 거짓말을 하지 않는다
항목 25. 단정적 프로그래밍
항목 42. 속성 기반 테스트
항목 45. 요구 사항의 구체화
“계약 없는 신뢰는 환상이다.” 함수와 모듈은 반드시 자신의 의무와 권리를 코드로 증명하라.
💻 실용주의 프로그래머: Topic 24
죽은 프로그램은 거짓말을 하지 않는다 (Dead Programs Tell No Lies)
1. 핵심 개념 — “실패를 숨기지 말고, 즉시 드러내라”
“거짓 없는 프로그램이란, 실패를 감추지 않는 프로그램이다.”
프로그램이 잘못된 상태로 계속 실행되면, 문제는 훨씬 커지고 더 찾기 어려워진다.
따라서 실용주의 프로그래머는 잘못된 상태에서는 프로그램을 멈추는 것이 정직한 선택이라고 말한다.
💡 즉, “죽더라도 정직하게 죽어라.”
2. 배경 — “문제를 숨기면 더 큰 재앙이 온다”
많은 경우, 오류는 “조용히” 발생한다.
비어 있는 리스트를 반환한다.
default case로 빠져버린다.
예외를 덮어쓰고 로그만 남긴다.
이런 코드는 겉보기엔 정상 동작처럼 보여도 실제론 **논리적 오류를 감추는 ‘거짓말하는 프로그램’**이다.
“없는 일이 일어날 리 없다”는 생각이 가장 위험하다. 반드시 “불가능한 상황은 언젠가 일어난다.”
3. 잘못된 예 — “예외를 삼켜버리는 코드”
이 코드는 겉보기에 친절하지만, 문제를 로그로만 처리하고 프로그램을 계속 진행시킨다. 결과적으로 데이터 불일치, 무한 루프, 손상된 상태로 이어질 수 있다.
4. 올바른 방식 — “명확하게 실패하라”
이렇게 하면 프로그램은 즉시 종료되고, 문제의 원인을 명확히 드러낸다. **“실패를 인정하는 프로그램”**이 되는 것이다.
Tip 38. 일찍 작동을 멈춰라. (Fail Fast)
5. 철학 — “일찍 멈추는 것이 덜 아프다”
문제가 생긴 상태로 계속 진행하면, 나중에는 더 많은 데이터와 시스템이 오염된다.
오류를 조기에 탐지하고 중단시키면
문제를 쉽게 복구할 수 있고
원인을 명확히 추적할 수 있으며
전체 시스템의 신뢰성이 높아진다.
6. 실제 적용 — Erlang과 Elixir의 철학
Erlang의 창시자 Joe Armstrong은 이렇게 말했다.
“방어하지 말고, 그냥 멈추게 놔둬라. 실패하도록 설계하라!”
Erlang은 Supervisor 트리 구조를 통해 프로세스가 실패하면 상위 Supervisor가 자동으로 재시작한다. 즉, **“죽을 수 있게 설계된 언어”**이다.
이 철학은 고가용성(Fault-tolerant) 시스템의 핵심이다. 프로그램이 죽어도 전체 시스템은 살아남는다.
7. 비교 — “계속 달리는 시스템 vs 멈추는 시스템”
방어적 프로그래밍
오류를 억제하고 프로그램을 계속 실행
문제 누적, 데이터 손상
Fail-fast 프로그래밍
오류 즉시 발생, 프로그램 종료
빠른 탐지, 명확한 원인 파악
“멈추는 것이 용기다. 거짓말하는 프로그램은 결국 더 큰 거짓을 만든다.”
8. 현대적 적용 — Spring, Kotlin, Node.js에서의 Fail-fast 예시
Spring Boot
@Validated 검증 실패 시 즉시 400 응답 반환
Kotlin
require(amount > 0) / checkNotNull(user)
Node.js
throw new Error("Invalid input") 즉시 종료
CI/CD
테스트 실패 시 파이프라인 중단 (Fail Fast)
9. 관련 항목
항목 23. 계약에 의한 설계 — 계약을 어기면 멈춰라
항목 25. 단정적 프로그래밍 — assert로 거짓을 막아라
항목 43. 바깥에서는 안에 주의하라 — 경계에서 실패를 포착하라
“죽은 프로그램은 거짓말하지 않는다.” 오류를 덮지 말고, 빠르게 드러내라. Fail fast, fail honestly.
💻 실용주의 프로그래머: Topic 25
단정적 프로그래밍 (Assertive Programming)
1. 핵심 개념 — “믿지 말고, 단정(assert)하라”
“자기 비난에는 사치성이 있다.” — 오스카 와일드
모든 개발자는 코드에서 일어날 수 없는 상황을 명시적으로 검증해야 한다. 즉, “그럴 리 없어”라고 믿는 대신, 단정문(assertion) 을 통해 “그럴 리 없음”을 코드로 증명하라는 것이다.
💡 믿음이 아니라 검증이다. “그럴 리 없다”는 말은 프로그램의 적이다.
2. 단정문의 역할 — “거짓을 드러내는 장치”
단정문(assert)은
“이 시점에서 반드시 이 조건이 참이어야 한다.” 는 것을 코드에 명시하는 장치다.
예:
이 조건이 거짓이면 프로그램은 즉시 중단된다. 즉, 죽은 프로그램은 거짓말하지 않는다는 원칙을 자동으로 실행하게 된다.
3. 단정의 올바른 사용법
✅ 사용
불가능해야 하는 상황 검증
assert user != null;
❌ 금지
정상적인 오류 처리 대용
assert input != null 대신 if (input == null) throw
✅ 주석 대용
의도를 명확히
assert size > 0 : "빈 리스트면 안 됨";
✅ 테스트
로직의 전제 조건 보증
assert isSorted(list);
4. 잘못된 예 — “단정으로 로직을 대체하지 말라”
이건 잘못된 코드다.
사용자 입력은 언제나 유효하지 않을 가능성이 있으므로
if 문과 예외 처리가 필요하다.
단정은 절대로 외부 입력 검증용이 아니다.
단정은 “코드 내부의 논리적 가정”만 검증해야 한다. 외부 환경은 신뢰하지 말고 방어적으로 처리하라.
5. 단정과 부작용 (Side Effect)
다음 코드는 단정문이 부작용을 일으켜 버그를 만든다.
assert 내부에서 next()를 호출해 반복자를 이동시켜버렸기 때문.
단정문은 상태를 바꾸지 않고, 오직 확인만 해야 한다.
6. 단정 기능을 켜 두어라 — “죽이지 말고 살려둬라”
테스트 중에는 단정문을 활성화(-ea 옵션 등)해야 한다.
많은 개발자들이 단정이 성능을 저하시킨다고 꺼두지만,
단정은 디버그 도구이며, 안전장치다.
“성능이 문제 되기 전까지 단정을 지우지 말라. 단정은 코드의 가드 레일이다.”
7. 실무 사례 — “실 서비스에서도 단정은 도움 된다”
작은 네트워크 장비 회사를 운영하던 한 엔지니어는 “단정문을 배포 코드에 그대로 남겨두는 것”을 결정했다.
버그가 발생하면 단정이 실패한 시점의 데이터를 모두 수집했고, 결과적으로 훨씬 안정적인 제품을 만들 수 있었다.
👉 단정은 품질 확보를 위한 자동 로그 시스템이 될 수도 있다.
8. 단정의 한계 — “모든 버그를 잡진 못한다”
단정은 논리적 오류(Internal bug)에만 유효하다. 다음과 같은 외부 문제에는 효과가 없다.
하드웨어/네트워크 오류
사용자 입력 오류
경쟁 조건(race condition)
즉, 단정은 “내가 통제할 수 있는 코드의 진실만 확인” 한다.
9. 연습 문제 요약
연습 16. “불가능한 일” 목록 중 실제 일어날 수 있는 것은?
예시 질문:
한 달이 28일보다 적은 달이 있다?
시스템 콜 오류 메시지: “디렉터리에 접근할 수 없음.”
a=2; b=3;인데a+b=5가 아님.삼각형이 60초가 아닌 180도?
👉 결론: “절대 일어나지 않는다”는 것은 없다. 단정으로라도 대비하라.
10. 관련 항목
Topic 23. 계약에 의한 설계 — 계약을 위반하면 단정이 실패한다
Topic 24. 죽은 프로그램은 거짓말을 하지 않는다 — 단정은 실패 시 죽게 만든다
Topic 26. 견고함(Resilience) — 단정 이후의 복원력을 다룬다
“단정(assert)은 코드의 양심이다.” 스스로 증명할 수 없는 믿음을 코드에 두지 말라. Assert early, assert often.
💻 실용주의 프로그래머: Topic 26
리소스 사용의 균형 (Balancing Resources)
1. 핵심 개념 — “자신이 시작한 것은 자신이 끝내라”
“촛불 하나를 켜는 건 그림자도 하나 던지는 거란 말이다.” — 어슐러 K. 르 귄, 《어스시의 마법사》
모든 프로그램은 리소스를 소비한다. 메모리, 트랜잭션, 스레드, 파일, 네트워크 연결, 락(lock), DB 커넥션, 타이머 등 한정된 자원을 빌려 쓰고 해제해야 한다.
💡 원칙: 리소스를 사용했으면, 반드시 해제하라. 사용한 주체가 해제의 책임도 져야 한다.
이것이 바로 “자신이 시작한 것은 자신이 끝내라 (Tip 40)”의 의미다.
2. 문제 인식 — “대부분의 개발자는 해제를 잊는다”
우리는 자원 할당(open, connect, beginTransaction)은 잘하지만,
그에 대응하는 해제(close, disconnect, commit/rollback)는 종종 잊는다.
이런 무책임한 사용은 누적되면 다음과 같은 문제를 만든다.
파일 핸들이 닫히지 않아 I/O 장애 발생
메모리 누수(memory leak)
DB 커넥션 풀 고갈
트랜잭션 미종료로 인한 Deadlock
즉, 리소스 사용의 균형이 무너진 것이다.
3. 잘못된 예시 — “공유된 리소스와 결합된 루틴”
겉보기엔 괜찮지만, read_customer와 write_customer가
같은 인스턴스 변수(@customer_file) 를 공유하며 결합(coupling) 되어 있다.
이로 인해 다음과 같은 위험이 생긴다:
파일이 정상적으로 닫히지 않을 수 있다.
예외 발생 시 리소스가 해제되지 않는다.
여러 스레드에서 동시에 접근하면 레이스 컨디션이 발생한다.
4. 리팩터링 — “책임을 한 함수 안으로 모아라”
이제 파일을 여는 것(open)과 닫는 것(close)이
하나의 블록 스코프 안에서 해결된다.
파일이 블록을 벗어나면 자동으로 닫힘.
공유 상태가 사라짐.
해제 책임이 명확히 한 곳에 모임.
✅ “열면 닫아라. 시작한 곳에서 끝내라.”
5. Tip 41 — “지역적으로 행동하라”
리소스의 수명을 가능한 한 짧게 두어라. 스코프를 작게 유지할수록
관리가 쉽고,
오류가 적고,
테스트가 용이하다.
예를 들어, 파일 객체를 전역 변수로 들고 다니지 말고 필요한 시점의 함수 안에서 열고 닫아라.
6. 중첩 합당 — “여러 리소스를 다룰 때 순서를 지켜라”
리소스가 여러 개일 때는 항상 반대 순서로 해제해야 한다.
💡 “마지막에 연 것은 가장 먼저 닫는다.” Stack 자료구조처럼 LIFO로 관리해야 Deadlock을 피할 수 있다.
7. 예외와 리소스 — “스코프가 닫히면 리소스도 닫혀야 한다”
리소스를 다루는 블록 안에서 예외가 발생하더라도 그 리소스는 반드시 해제되어야 한다.
언어별 예시 👇
C++ / Rust
변수의 스코프를 벗어나면 소멸자(destructor) 자동 실행
Java / Kotlin / Python
try-with-resources, use {}, with 블록
JavaScript
finally 절 활용
예:
8. 나쁜 예외 처리 방식 🚫
이건 얼핏 좋아 보이지만,
allocate_resource()에서 예외가 발생하면 thing이 정의되지 않아
해제 코드가 동작하지 않는다.
✅ 올바른 구조:
9. 리소스 계층의 균형 — “최상위 구조는 하위 구조를 해제해야 한다”
리소스 구조는 트리 형태로 관리돼야 한다.
최상위 구조는 자신이 소유한 하위 리소스를 해제한다.
하위 구조는 자신이 가진 내부 리소스만 정리한다.
💡 “누가 시작했는가?” → 그가 반드시 끝내야 한다.
10. 실용적 점검 — “리소스의 생애를 추적하라”
리소스 누수를 방지하기 위해, 프로그램의 실행 중에 리소스 점검 도구(memory profiler) 를 사용하라.
또한 서버 프로그램이라면 “요청 한 건이 끝날 때마다 리소스가 제대로 해제되었는가?”를 정기적으로 모니터링하라.
11. 관련 항목
Topic 24. 죽은 프로그램은 거짓말을 하지 않는다 → 예외는 숨기지 말고 종료하라.
Topic 25. 단정적 프로그래밍 → 불가능한 상태는 코드로 검증하라.
Topic 33. 시간적 결합 깨뜨리기 → 리소스 생명주기를 결합하지 말라.
“시작한 자가 끝내라. 열면 닫아라.” 자원은 빌려 쓰는 것이다. Manage scope, manage life.
💻 실용주의 프로그래머: Topic 27
헤드라이트를 앞서가지 말라 (Don’t Outrun Your Headlights)
1. 핵심 개념 — “예측은 어렵다, 특히 미래에 대해서는”
“예측은 힘들다. 특히 미래에 대해서는.” — 요기 베라(Yogi Berra)
야간 운전 중, 급커브를 돌다가 헤드라이트가 비추는 범위를 벗어난 속도로 달리면 아무리 브레이크를 밟아도 이미 늦는다. 빛이 닿지 않는 영역은 “보이지 않는 위험 구간”, 즉 미래다. 운전과 마찬가지로 소프트웨어 개발에서도 우리의 헤드라이트(시야) 는 제한되어 있다.
💡 “미래를 내다보려 하기보다, 지금 비추는 빛 안에서만 안전하게 움직여라.”
2. 비유의 의미 — “투사 거리(throw distance)”
헤드라이트의 투사 거리(빛이 닿는 범위)는 물리적으로 제한되어 있다. 시속 60km일 때 정지거리는 약 52m, 시속 100km일 때는 약 116m 정도다.
즉, 아무리 빠른 차라도 헤드라이트보다 멀리 볼 수는 없다. 미래를 예측하려는 시도는 결국 빛의 속도를 앞지르려는 것과 같다.
3. 소프트웨어의 교훈 — “우리의 헤드라이트도 제한되어 있다”
개발자들은 종종 “이번 프로젝트가 언제 완성될까?”, “사용자 요구가 어떻게 변할까?” 같은 질문에 답하려 한다. 하지만 너무 먼 미래를 확신하려는 순간, 통제 불가능한 변수들이 생긴다.
예측 대신, 작은 단계와 짧은 피드백 루프를 통해 앞으로 나아가라. 그게 바로 실용주의 프로그래머의 방식이다.
4. Tip 42 — “작은 단계를 밟아라, 언제나.”
작업을 작게 나누고, 각 단계를 진행하기 전에 피드백을 확인하고 조정하라.
REPL 결과
코드와 알고리즘이 의도대로 동작하는가?
단위 테스트
특정 코드에 대한 즉각적인 검증
사용자 피드백
기능이 실제 사용자의 니즈를 충족하는가?
너무 먼 단계로 “예언”하려 하지 말고, 작은 단위에서 실험과 피드백을 반복하라. 그것이 경험과 통계 기반의 **“무모한 예측이 아닌, 반복 가능한 추론”**이다.
5. 예측 대신 설계하라
“다음 달 완성일”보다 “다음 스프린트 완료 목표”를 세워라.
“향후 확장성”보다 “오늘 가능한 리팩터링”에 집중하라.
“사용자의 미래 행동”보다 “지금의 사용 패턴”을 분석하라.
“예상 기술 트렌드”보다 “현재 검증된 기술”을 활용하라.
💡 미래를 위한 가장 좋은 대비책은 현재의 구조를 유연하게 설계하는 것이다.
6. 불확실성과 블랙 스완 (Black Swan)
나심 탈레브(Nassim Taleb)는 『블랙 스완』에서 이렇게 말했다.
“모든 예측은 결국 ‘평균적 세상’을 가정한다. 하지만 진짜 변화는 예외적 사건(black swan)에서 온다.”
즉, 예측이 빗나가는 이유는 우리가 비정상적 사건을 무시하기 때문이다. 따라서 실용주의 프로그래머는 예측을 덜 하고, 변화와 예외를 견디는 구조를 만든다.
7. GUI 전쟁의 교훈 — “예측은 거의 항상 틀린다”
책의 1판이 나올 당시, 세상은 “데스크톱 GUI 전쟁”에 열광했다. 모티프(Motif) vs 오픈룩(OpenLook) — 하지만 누가 이겼는가? 둘 다 졌다.
세상을 바꾼 건 웹 브라우저였다. 즉, “이길 것 같다”는 예측은 아무 의미가 없었다.
8. Tip 43 — “예언하지 말라”
“대부분의 경우 내일은 오늘과 크게 다르지 않다. 그러나 확신하지는 말라.”
너무 멀리 예언하지 말고, 지금 눈앞에 비치는 헤드라이트의 범위 안에서, 가장 좋은 결정을 반복하라.
“예측하지 말고, 피드백으로 조정하라.” 작은 단계를 밟으며, 지금 보이는 빛 안에서 움직여라. Don’t predict. Adapt.
Last updated