본문으로 건너뛰기

The Pragmatic Programmer

David ThomasISBN 9788966263363

목차327개 항목

The Pragmatic Programmer 서문

“It is your life. You own it. You run it. You create it.” — David Thomas & Andy Hunt


서문 (Preface): Pragmatic Programmer란 무엇인가

이 책의 탄생 배경

  • 저자들이 1990년대에 다양한 프로젝트 컨설팅을 진행하며 반복적으로 발견한 문제들을 메모로 정리한 것이 이 책의 시작
  • “배포 전에 테스트했나요?”, “왜 Mary의 컴퓨터에서만 빌드가 되나요?”, “왜 아무도 사용자에게 묻지 않았나요?” 같은 질문들이 반복됨
  • 20주년 개정판에서는 낡은 기술 레퍼런스(CORBA, CASE 도구 등)를 제거하고, 함수형 프로그래밍 패러다임, 프라이버시/보안, 애자일 등 새로운 내용을 추가

Pragmatic(실용주의)의 의미

  • 라틴어 pragmaticus(사업에 능숙한), 그리스어 πραγματικός(사용에 적합한)에서 유래
  • 특정 기술, 언어, 운영체제에 종속되지 않는 넓은 배경과 경험을 바탕으로 상황에 맞는 최선의 솔루션을 선택하는 능력
  • “Best Solution”은 존재하지 않는다. 오직 특정 상황에 더 적합한 시스템이 있을 뿐

Pragmatic Programmer의 특성

특성 설명
얼리어답터 / 빠른 적응자 새로운 기술을 빠르게 파악하고 기존 지식과 통합함
호기심 많은 사람 끊임없이 질문하고, 작은 사실들을 수집함
비판적 사고자 주어진 사실을 그냥 받아들이지 않고 근거를 확인함
현실주의자 문제의 근본적인 특성을 이해하고, 난이도와 소요 시간을 정확히 파악함
팔방미인 다양한 기술과 환경에 익숙하고, 새로운 영역으로 이동할 수 있음

두 가지 가장 기본적인 Tip:

Tip 1 — Care About Your Craft 잘 만드는 것에 관심이 없다면 소프트웨어를 개발할 이유가 없다.

Tip 2 — Think! About Your Work 하고 있는 일에 대해 생각하라. 자동 조종 모드로 실행하지 말고, 매 결정마다 비판적으로 사고하라.

장인정신과 팀

  • 소프트웨어 공학이라는 구조 안에서도 개인의 장인정신은 필수적
  • 중세 대성당을 지은 석공들처럼: 공학적 요구사항을 해석하고, 그것을 초월하는 무언가를 만들어낸 것은 개인들의 장인정신
  • “We who cut mere stones must always be envisioning cathedrals.”

지속적인 개선 (Kaizen)

  • 일본어 카이젠(改善): 지속적으로 작은 개선을 쌓아가는 개념
  • 위대한 프로그래머는 매일 조금씩 스킬을 다듬고 새로운 도구를 추가함
  • Eton 대학 정원사의 비유: “500년 동안 매일 이슬을 쓸고, 이틀에 한 번 깎고, 매주 굴리면 완벽한 잔디밭이 된다”

Chapter 1. A Pragmatic Philosophy

챕터 1은 Pragmatic Programmer의 철학과 태도를 다룬다. 기술보다 먼저 사람으로서의 자세와 사고방식을 확립하는 것이 목표다.


Topic 1. It’s Your Life

핵심 주장

  • 개발자는 자신의 커리어, 삶, 선택에 완전한 주도권을 가지고 있다
  • 소프트웨어 개발은 가장 통제권이 높은 직업 중 하나: 수요 있는 스킬, 지리적 자유, 원격 근무, 높은 급여

문제: 왜 개발자들은 변화를 거부하는가

  • 현재 상황에 불만족하면서도 그냥 버티는 경향이 있음
  • 회사가 훈련을 안 시켜준다고 불평하지만, 스스로 학습하지 않음
  • 기술이 자신을 앞서가고 있다고 느끼면서도 새로운 것을 배우지 않음

해결책

Tip 3 — You Have Agency 직장 환경이 마음에 안 드는가? 고쳐라. 하지만 영원히 시도하지는 말라. Martin Fowler의 말처럼 “you can change your organization or change your organization” (조직을 바꾸거나, 조직을 바꾸거나)

  • 기술이 뒤처진다고 느끼면: 개인 시간을 투자해서 공부하라. 자기 자신에 대한 투자다
  • 원격 근무를 원하면: 요청하라. No라고 하면, Yes라고 하는 곳을 찾아라
  • 수동적으로 기다리지 말고 능동적으로 기회를 잡아라

Topic 2. The Cat Ate My Source Code

핵심 주장: 책임 (Responsibility)

  • Pragmatic Programmer의 핵심 철학은 자신의 행동, 커리어, 학습, 프로젝트에 대한 책임을 지는 것
  • 책임은 능동적으로 동의하는 것 (actively agree to): 직접 통제하지 못하는 영역이 있어도, 그 결과에 대해 책임진다

팀에서의 신뢰 (Team Trust)

  • 팀에서 신뢰는 절대적으로 필수적: 창의성과 협업의 기반
  • 건강한 신뢰 기반 환경에서는 안전하게 의견을 말하고, 팀원에게 의지할 수 있음
  • 예시: 잠입 작전 팀에서 “레이저 장비를 집에 두고 왔어요”라고 말하는 것 — 그런 신뢰 위반은 복구하기 어렵다

잘못됐을 때의 올바른 태도

  • 실수를 정직하게 인정하고, 해결책(options)을 제시하라
  • 다른 사람이나 상황을 탓하지 말라: 벤더, 언어, 경영진, 동료 탓은 하지 말 것
  • 백업 없이 소스코드가 날아갔다? 그건 당신 책임이다

Tip 4 — Provide Options, Don’t Make Lame Excuses 누군가에게 “왜 안 되는지” 말하기 전에, 먼저 자신에게 그 변명이 합리적으로 들리는지 물어보라.

실전 적용

  • 핑계를 말하기 전에 고무 오리(rubber duck)에게 먼저 말해보라 — 그 변명이 어리석게 들리면, 상대방도 그렇게 들을 것
  • “모른다”고 말할 때는 반드시 “— 하지만 알아보겠다”라고 덧붙여라
  • 변명 대신 옵션을 제시하라:
// 나쁜 예: "그건 불가능해요"
// 좋은 예: "현재 방식으로는 어렵지만, 이런 접근이 가능합니다"

function reportIssue(problem: string): Options {
  // 문제를 설명하는 동시에
  // 1. 무엇을 할 수 있는지
  // 2. 리스크를 줄이기 위해 무엇이 필요한지
  // 3. 더 나은 결과를 위해 무엇을 배워야 하는지
  // 항상 옵션을 함께 제시하라
  return { canDo: [...], needs: [...], alternatives: [...] };
}

Topic 3. Software Entropy

핵심 개념: 깨진 창문 이론 (Broken Window Theory)

  • 엔트로피(Entropy): 물리학 용어로 시스템 내의 “무질서”의 양. 소프트웨어도 마찬가지로 방치하면 무질서가 증가함
  • 소프트웨어 엔트로피의 심화를 “소프트웨어 부패(Software Rot)” 또는 “기술 부채(Technical Debt)“라고도 부름

깨진 창문 이론의 원리

  • 심리학 연구 결과: 하나의 깨진 창문을 방치하면, 건물 전체가 버려진 느낌을 줌
  • 그 후 또 다른 창문이 깨지고, 낙서가 생기고, 구조적 손상으로 이어짐
  • 절망감은 전염된다: 팀에서 한 명이 나쁜 코드를 방치하면, “어차피 다 엉망이니까” 라는 심리가 퍼짐
graph TD
    A[깨진 창문 하나 방치] --> B[팀의 절망감 증가]
    B --> C[더 많은 나쁜 코드 추가]
    C --> D[코드베이스 전체 부패]
    D --> E[프로젝트 실패]

Tip 5 — Don’t Live with Broken Windows “깨진 창문”(나쁜 설계, 잘못된 결정, 나쁜 코드)을 발견하면 즉시 수리하라. 시간이 없으면 “임시 판자라도 대라(board it up)”.

임시 처리 방법

  • 수정할 시간이 없을 때는 최소한:
    • 문제 코드를 주석 처리
    • // TODO: 이 결정이 잘못됨, 리팩토링 필요 메모
    • "Not Implemented" 메시지 표시
    • 더미 데이터로 대체
  • 핵심은 “우리는 이 문제를 인식하고 있고, 방치하지 않겠다”는 신호를 보내는 것

First, Do No Harm (해를 끼치지 말 것)

  • 부자의 집 화재: 소방관들이 불이 타는 와중에도 카펫 위에 매트를 깔고 들어온 이야기
  • 위기 상황에서도 불필요한 추가 피해를 입히지 말아야 한다
  • 깨끗한 코드베이스를 발견했다면 — 그것을 망가뜨리는 첫 번째 사람이 되지 말라

Topic 4. Stone Soup and Boiled Frogs

이 토픽은 서로 반대되는 두 가지 관점에서 “점진적 변화”를 다룬다.

스톤 수프 (Stone Soup): 변화의 촉매가 되어라

이야기 요약:

  • 전쟁에서 돌아온 굶주린 병사 셋이 마을에 도착. 마을 사람들은 식량을 숨김
  • 병사들이 냄비에 물을 끓이며 돌 세 개를 넣음 → 주민들이 호기심에 모여듦
  • “돌 수프인데, 당근이 있으면 더 맛있어요…” → 한 사람이 당근을 가져옴
  • 계속해서 감자, 소고기, 파 등을 추가하게 유도 → 결국 온 마을이 함께 먹는 훌륭한 수프 완성

교훈:

  • 병사들은 마을 사람들의 호기심을 이용해 촉매 역할을 했다
  • 혼자서는 불가능했던 것이 협력으로 가능해졌다 (시너지 효과)

실무 적용:

  • 전체 시스템 개편을 요청하면 위원회가 열리고, 예산 승인을 기다리며, 모든 것이 복잡해짐 (“start-up fatigue”)
  • 대신: 합리적으로 요청할 수 있는 작은 것부터 시작하고, 잘 만들고, 보여주고, “이렇게 되면 더 좋아질 것 같지 않나요?” 라고 말하라
  • 사람들은 진행 중인 성공에 합류하기가 훨씬 쉽다

Tip 6 — Be a Catalyst for Change

삶은 개구리 (Boiled Frog): 큰 그림을 놓치지 말라

개구리 비유:

  • 끓는 물에 넣으면 즉시 뛰쳐나옴
  • 차가운 물에 넣고 서서히 가열하면 온도 변화를 느끼지 못하고 익어버림

소프트웨어 프로젝트에서의 현실:

  • 대부분의 소프트웨어 재앙은 처음에는 너무 작아서 눈치채지 못함
  • 대부분의 프로젝트 초과는 하루하루 누적되어 발생
  • 기능이 하나씩 추가되고, 패치가 겹겹이 쌓이다가 결국 원형을 알아볼 수 없게 됨

Tip 7 — Remember the Big Picture 내가 하는 일만 보지 말고, 주변에서 일어나는 것을 지속적으로 검토하라.

깨진 창문 이론 vs 삶은 개구리 비교

구분 깨진 창문 이론 삶은 개구리
원인 누군가가 신경쓰지 않는다는 인식 점진적인 변화를 인지하지 못함
대응 창문을 고치면 의식 회복 항상 큰 그림을 의식적으로 확인해야 함
핵심 심리적 전염 인지적 맹점

Topic 5. Good-Enough Software

핵심 주장

  • 완벽한 소프트웨어는 존재하지 않는다: 시간, 기술, 인간의 기질이 항상 방해함
  • “Good Enough” = 슬렁슬렁 만든 코드가 아님. 사용자의 필요에 맞게 충분히 좋은 코드

사용자를 트레이드오프에 참여시켜라

  • 소프트웨어의 품질 수준을 요구사항의 일부로 다루어야 함
  • 사용자는 종종 1년 뒤의 완벽한 소프트웨어보다 지금 당장의 약간 거친 소프트웨어를 원한다
  • 페이스메이커, 자동조종장치 같은 안전 필수 시스템은 예외: 요구사항이 더 엄격함

Tip 8 — Make Quality a Requirements Issue 소프트웨어 품질의 범위와 수준은 해당 시스템의 요구사항으로 논의되어야 한다.

언제 멈출지 알아야 한다 (Know When to Stop)

  • 프로그래밍은 그림 그리기와 같다: 언제 붓을 놓을지 모르면 걸작을 망친다
  • 과도한 다듬기(over-embellishment)가 좋은 프로그램을 망친다
  • 완벽하지 않아도 괜찮다. 코드가 지금의 모습으로 잠시 서 있게 두어라
// 안티패턴: 멈추지 못하는 개발자
function processData(data: unknown) {
  // v1: 기본 동작
  // v2: 엣지 케이스 처리 추가
  // v3: 성능 최적화
  // v4: 더 나은 에러 메시지
  // v5: 리팩토링
  // v6: 또 리팩토링... (출시는 언제?)
  //
  // 핵심: 사용자 요구사항을 충족하는 시점에서 멈추어라
}

Topic 6. Your Knowledge Portfolio

핵심 은유: 지식을 금융 포트폴리오처럼 관리하라

  • 지식과 경험은 만기가 있는 자산(Expiring Asset): 새로운 기술, 언어, 환경이 개발되면서 가치가 하락
  • 지식이 낡으면 당신의 가치도 낡는다
  • 새로운 것을 배우는 능력이 가장 중요한 전략적 자산

금융 포트폴리오 관리 원칙을 지식에 적용

금융 원칙 지식 포트폴리오 적용
정기적 투자 (습관으로) 작은 양이라도 꾸준히 배워라
분산 투자 다양한 기술과 비기술적 스킬 모두 보유
리스크 관리 안정적인 기술 + 고위험 고수익 신기술 균형
저가 매수 뜨기 전에 신기술을 배워라 (Java 초기처럼)
주기적 검토/재조정 어제의 핫 기술이 오늘은 식었을 수 있음

Tip 9 — Invest Regularly in Your Knowledge Portfolio

구체적인 실천 목표

  • 매년 최소 하나의 새로운 언어를 배워라: 다른 접근 방식이 사고를 넓혀줌
  • 매달 기술 서적 한 권을 읽어라: 웹의 짧은 글보다 깊은 이해를 위해 긴 형식의 책이 필요
  • 비기술 서적도 읽어라: 컴퓨터는 사람이 사용한다. 소프트 스킬(사실은 매우 어렵다)을 개발하라
  • 강의를 들어라: 온/오프라인 대학, 컨퍼런스 활용
  • 지역 유저 그룹과 밋업에 참가하라: 고립은 커리어에 치명적
  • 다양한 환경을 실험해보라: Windows만 써봤으면 Linux도 써보라, IDE만 써봤으면 터미널도 써보라
  • 최신 동향을 파악하라: 현재 프로젝트와 다른 기술의 뉴스도 읽어라

학습 기회를 최대화하는 방법

  • 모르는 것을 발견했을 때 그냥 넘어가지 마라: 개인적 도전으로 삼아라
  • 검색하고, 물어보고, 인맥을 통해 답을 찾아라
  • 자투리 시간 활용: 병원 대기, 교통 시간 등 — 항상 읽을 것을 준비해두어라

비판적 사고 (Critical Thinking)

Tip 10 — Critically Analyze What You Read and Hear

  • 벤더 과장 광고나 미디어 과대 포장에 휘둘리지 말라
  • 검색 엔진 상위 노출 = 좋은 내용이 아님: 광고비로 순위를 살 수 있다
  • 어떤 정보든 다음 질문으로 비판적으로 분석하라:
분석 질문 목적
“5 Whys” (5번 왜 묻기) 근본 원인에 접근하기
누가 이익을 얻는가? 편향된 정보 걸러내기
맥락은 무엇인가? “one size fits all”은 없다
언제/어디서 작동하는가? 2차적 결과(second-order thinking)까지 생각하기
왜 이것이 문제인가? 기반 모델을 이해하기

Topic 7. Communicate

핵심 주장

  • 아무리 훌륭한 아이디어라도 효과적으로 전달되지 않으면 무의미하다
  • 영어(또는 모국어)를 또 하나의 프로그래밍 언어처럼 다루어라: DRY 원칙, 자동화 등을 적용

Tip 11 — English is Just Another Programming Language

효과적인 커뮤니케이션의 7가지 원칙

1. 청중을 파악하라 (Know Your Audience)

  • 같은 내용도 청중에 따라 완전히 다르게 전달해야 함
  • 예: 메시지 브로커 도입을 발표할 때
    • 최종 사용자: “다른 서비스와 연동됩니다”
    • 마케팅: “판매 차별점이 생겼습니다”
    • 운영: “그 파트의 유지보수를 외부로 넘겼습니다”
    • 개발자: “새로운 API 경험과 재활용 기회가 생겼습니다”
  • 커뮤니케이션의 의미는 상대방의 반응이다

2. 하고 싶은 말을 파악하라 (Know What You Want to Say)

  • 아웃라인을 먼저 작성하라
  • “이 내용이 청중에게 내가 원하는 방식으로 전달되는가?” 를 스스로 물어라
  • 중요한 미팅 전에 전달할 아이디어를 메모하고 전략을 계획하라

3. 적절한 타이밍을 선택하라 (Choose Your Moment)

  • 금요일 저녁 6시, 상사의 아이가 입원 중인 상황에서 노트북 업그레이드를 요청하는 것은 최악의 타이밍
  • “지금 이 이야기를 나누기 좋은 시간인가요?” 라고 먼저 물어라

4. 스타일을 선택하라 (Choose a Style)

  • 어떤 사람은 “팩트만” 을, 어떤 사람은 넓은 맥락의 대화를 원함
  • 상대방의 전문성 수준에 맞게 조정하라 (전문가 vs 초보자)
  • 확실하지 않으면 물어보라

5. 보기 좋게 만들어라 (Make It Look Good)

  • 아이디어는 좋은 포장을 받을 자격이 있다
  • 문서, 프레젠테이션의 시각적 품질에도 신경 써라
  • 맞춤법 검사: 자동 검사 후, 반드시 직접 읽어서 확인하라 (자동 교정이 놓치는 오류가 있다)

6. 청중을 참여시켜라 (Involve Your Audience)

  • 문서를 만드는 과정 자체가 종종 최종 결과물보다 중요하다
  • 초안 단계에서 독자를 참여시켜 피드백을 받아라

7. 듣는 사람이 되어라 (Be a Listener)

  • 사람들이 당신의 말을 듣게 하려면: 먼저 그들의 말을 들어라
  • 질문을 하도록 유도하고, 대화를 다이얼로그(dialogue)로 만들어라

Tip 12 — It’s Both What You Say and the Way You Say It

문서화 (Documentation)

Tip 13 — Build Documentation In, Don’t Bolt It On

  • Pragmatic Programmer는 문서화를 개발 프로세스의 필수 요소로 본다
  • 주석은 “왜(why)“에 집중하라: 코드는 이미 “어떻게(how)“를 보여줌
    • 잘못된 주석: 코드를 그대로 자연어로 반복 (DRY 위반)
    • 좋은 주석: 엔지니어링 트레이드오프, 결정의 이유, 폐기된 대안 설명
// 나쁜 주석: 코드가 이미 말하고 있는 것을 반복
// i를 1씩 증가시킨다
i++;

// 좋은 주석: "왜"를 설명
// 오프셋을 0이 아닌 1부터 시작하는 이유:
// 레거시 API가 1-indexed를 기대하며, 현재 마이그레이션이 완료되지 않았음
const offset = index + 1;
  • 모든 함수에 주석을 다는 것은 오히려 유지보수를 어렵게 만든다 (변경 시 두 곳을 수정해야 함)
  • API 주석 (exported functions, modules)은 다른 개발자를 위해 작성하라

온라인 커뮤니케이션 원칙

  • 이메일, SNS, 블로그도 동일한 원칙 적용
  • 발송 전 반드시 교정(proofreading)하라
  • 형식은 간결하고 명확하게
  • 과도한 인용 자제: 100줄짜리 이메일에 “동의합니다”만 달린 답장은 최악
  • 온라인 기록은 영구적이다: 직접 말하지 못할 것은 쓰지도 마라

커뮤니케이션 체크리스트

  • 하고 싶은 말을 알고 있는가?
  • 청중을 파악했는가?
  • 적절한 타이밍인가?
  • 적절한 스타일인가?
  • 보기 좋게 만들었는가?
  • 청중을 참여시켰는가?
  • 경청하고 있는가?
  • 답장에 응했는가?
  • 코드와 문서를 함께 관리하고 있는가?

Chapter 2: A Pragmatic Approach

챕터 2는 소프트웨어 개발의 모든 레벨에 적용되는 보편적인 원칙과 접근법을 다룬다. 좋은 설계란 무엇인지, 지식의 중복을 어떻게 피할지, 변화에 어떻게 유연하게 대응할지를 실용적인 관점에서 설명한다.

Topic 8: The Essence of Good Design (좋은 설계의 본질)

ETC 원칙 (Easier to Change)

Tip 14: Good Design Is Easier to Change Than Bad Design

  • 좋은 설계란 단 하나의 기준으로 판단할 수 있다: 변경하기 쉬운가(ETC, Easier to Change)?
  • 모든 설계 원칙은 결국 ETC의 특수한 케이스다:
    • 왜 디커플링이 좋은가? 관심사를 분리하면 각각을 변경하기 쉬워지기 때문 (ETC)
    • 왜 단일 책임 원칙(SRP)이 유용한가? 요구사항 변경이 하나의 모듈 변경으로 반영되기 때문 (ETC)
    • 왜 네이밍이 중요한가? 좋은 이름은 코드를 읽기 쉽게 하고, 변경하려면 먼저 읽어야 하기 때문 (ETC)

ETC는 규칙이 아닌 가치(Value)다

  • ETC는 결정을 내릴 때 “이 선택이 전체 시스템을 변경하기 쉽게 만드는가 아니면 어렵게 만드는가?“라는 질문을 통해 방향을 안내하는 가이드다
  • 의식적인 훈련이 필요하다: 파일을 저장할 때, 테스트를 작성할 때, 버그를 고칠 때마다 스스로에게 물어라
  • 미래의 변경 방향을 모를 때는 두 가지를 하라:
    • 대체 가능하게(replaceable) 코드를 작성하라 - 결국 디커플링과 응집도를 유지하는 것
    • 상황을 엔지니어링 일지에 기록해두고, 나중에 코드가 실제로 변경될 때 피드백을 얻어라

Topic 9: DRY — The Evils of Duplication (중복의 해악)

Tip 15: DRY — Don’t Repeat Yourself

모든 지식은 시스템 안에서 단 하나의, 명확하고, 권위 있는 표현을 가져야 한다.

DRY는 코드 복사-붙여넣기 이상의 개념

  • DRY는 코드 중복만을 말하는 것이 아니다. 지식(knowledge)과 의도(intent)의 중복을 말한다
  • 핵심 테스트: 어떤 하나의 사실이 변경될 때 여러 곳을 수정해야 한다면, 그 코드는 DRY하지 않다
  • 유지보수는 앱이 출시된 이후에 시작되는 것이 아니다. 개발 전 과정에서 계속되는 루틴이다

1. 코드 내 중복 (Duplication in Code)

아래처럼 음수 처리 로직이 여러 곳에 반복되는 경우:

function printBalance(account: Account) {
  console.log(`Debits:  ${account.debits.toFixed(2)}`);
  console.log(`Credits: ${account.credits.toFixed(2)}`);
  if (account.fees < 0) {
    console.log(`Fees:    ${(-account.fees).toFixed(2)}-`);
  } else {
    console.log(`Fees:    ${account.fees.toFixed(2)}`);
  }
  // ...동일한 패턴 반복
}

중복 로직을 추출해 DRY하게 만든다:

function formatAmount(value: number): string {
  const result = Math.abs(value).toFixed(2).padStart(10);
  return value < 0 ? `${result}-` : `${result} `;
}
function reportLine(label: string, amount: number): void {
  console.log(`${label.padEnd(9)}${formatAmount(amount)}`);
}
function printBalance(account: Account) {
  reportLine("Debits:", account.debits);
  reportLine("Credits:", account.credits);
  reportLine("Fees:", account.fees);
  console.log(`         ----------`);
  reportLine("Balance:", account.balance);
}
  • 필드 너비를 바꾸고 싶으면 formatAmount 하나만 수정
  • 레이블 포맷을 바꾸고 싶으면 reportLine 하나만 수정

주의: 코드가 같다고 해서 항상 DRY 위반이 아니다. 아래 두 함수는 코드가 동일하지만 각각 다른 독립적인 지식을 표현하므로 DRY 위반이 아니다:

function validateAge(value: number): void {
  validateType(value, "integer");
  validateMinInteger(value, 0);
}
function validateQuantity(value: number): void {
  validateType(value, "integer");
  validateMinInteger(value, 0);
}

2. 문서 내 중복 (Duplication in Documentation)

모든 함수에 주석을 달아야 한다는 신화가 있다. 하지만 주석이 코드와 동일한 내용을 두 번 표현하면 DRY 위반이다:

// BAD: 주석과 코드가 동일한 의도를 중복 표현
// 수수료를 계산한다.
// * 반환된 수표 1장당 $20
// * 3일 초과 overdraft 시 하루 $10
// * 평균 잔액 $2000 초과 시 50% 감면
function fees(account: Account): number { ... }
// GOOD: 이름이 의도를 설명하고, 로직이 세부 내용을 표현
function calculateAccountFees(account: Account): number {
  let fees = 20 * account.returnedCheckCount;
  if (account.overdraftDays > 3) fees += 10 * account.overdraftDays;
  if (account.averageBalance > 2000) fees /= 2;
  return fees;
}

3. 데이터 내 중복 (DRY Violations in Data)

파생될 수 있는 값을 필드로 저장하는 것은 DRY 위반이다:

// BAD: length는 start와 end로부터 파생 가능한 중복 데이터
class Line {
  start: Point;
  end: Point;
  length: number; // 중복!
}
// GOOD: 계산 가능한 값은 메서드로 표현
class Line {
  start: Point;
  end: Point;
  get length(): number {
    return this.start.distanceTo(this.end);
  }
}
  • 성능상 이유로 캐싱이 필요한 경우 중복이 허용되지만, 이를 클래스 내부에 캡슐화해 외부에 노출하지 않아야 한다
  • Meyer의 Uniform Access 원칙: 모듈이 제공하는 모든 서비스는 저장으로 구현되든 계산으로 구현되든 동일한 표기법으로 접근 가능해야 한다

4. 표현 간 중복 (Representational Duplication)

  • 외부 API, 서비스, 데이터 소스와 통신할 때 코드가 해당 표현 방식(API 스펙, 스키마, 에러 코드)을 알아야 하므로 어느 정도의 중복은 불가피하다
  • 완화 전략:
    • 내부 API: 중립적 형식(예: OpenAPI)으로 API를 명세하고, 그로부터 문서/목 API/테스트/클라이언트를 자동 생성
    • 외부 API: OpenAPI 같은 공식 스펙을 임포트해 통합
    • 데이터 소스: 스키마로부터 컨테이너 코드를 자동 생성하거나, 고정 구조 대신 key/value 데이터 구조에 저장 후 테이블 기반 유효성 검사를 추가

5. 개발자 간 중복 (Interdev eloper Duplication)

  • 팀 내 서로 다른 개발자가 동일한 기능을 독립적으로 구현하는 문제 - 수년간 발견되지 않을 수 있다
  • 해결 방법:
    • 활발하고 잦은 커뮤니케이션 (Daily standup, Slack 채널)
    • 팀 내 “프로젝트 사서” 지정 — 공통 유틸리티/스크립트의 교환을 촉진
    • 소스 트리에 유틸리티 루틴을 공유할 수 있는 중앙 공간 마련
    • 다른 사람의 소스 코드와 문서를 정기적으로 읽기

Tip 16: Make It Easy to Reuse

새로 작성하는 것보다 기존 것을 찾아 재사용하는 것이 더 쉬운 환경을 만들어라. 쉽지 않으면 사람들은 재사용하지 않는다.

Topic 10: Orthogonality (직교성)

직교성이란?

  • 기하학에서 차용한 개념: 두 선이 직각으로 만날 때 직교한다고 하며, 서로 독립적이다
  • 소프트웨어에서는 독립성(independence) 또는 디커플링(decoupling) 을 의미한다
  • 직교적인 시스템: DB 코드는 UI에 직교적 — UI를 바꿔도 DB에 영향 없고, DB를 바꿔도 UI에 영향 없음
  • 비직교적인 시스템의 예시: 헬리콥터 조종 — 하나의 입력이 모든 제어에 2차적 효과를 일으켜 조종이 극도로 복잡해짐

Tip 17: Eliminate Effects Between Unrelated Things

직교적 시스템의 두 가지 이점

이점 설명
생산성 향상 변경이 국소화되어 개발/테스트 시간 감소. 작고 자기 완결적인 컴포넌트 작성이 쉽고, 직교적 컴포넌트 M × N 조합으로 더 많은 기능을 적은 노력으로 구현
리스크 감소 병든 코드가 격리됨. 시스템이 덜 취약함. 테스트하기 쉬워짐. 특정 벤더/플랫폼에 덜 종속됨

설계 (Design)

  • 직교성 테스트: “특정 기능의 요구사항이 극적으로 바뀐다면 몇 개의 모듈이 영향을 받는가?” — 직교적인 시스템이라면 답은 하나여야 한다
  • GUI 패널의 버튼 이동이 DB 스키마 변경을 요구해서는 안 된다
  • 전화번호, 우편번호, 이메일 주소 등 외부 식별자를 키로 사용하지 마라 — 언제든 변경될 수 있다
  • 레이어 다이어그램은 직교성을 표현하는 좋은 방법이다:
graph TD
    UI[UI Layer]
    BL[Business Logic Layer]
    DA[Data Access Layer]
    DB[(Database)]

    UI --> BL
    BL --> DA
    DA --> DB

툴킷과 라이브러리 (Toolkits and Libraries)

  • 서드파티 라이브러리를 도입할 때 해당 라이브러리가 코드베이스에 불필요한 변경을 강제하지 않는지 확인하라
  • 객체 지속성 스킴이 투명하다면 직교적이고, 특수한 방식으로 객체를 생성/접근하도록 강제한다면 직교적이지 않다
  • Decorator 패턴: 기존 코드를 변경하지 않고 기능을 추가하는 방식 — 직교성을 유지하는 좋은 예시

코딩 (Coding)

직교성을 유지하는 세 가지 기법:

  1. 코드를 디커플링 상태로 유지 (Keep your code decoupled)
    • shy code 작성: 다른 모듈에 불필요한 것을 노출하지 않고, 다른 모듈의 구현에 의존하지 않음
    • 객체의 상태를 변경해야 할 때 직접 수정하지 말고 해당 객체에게 시키라 (Law of Demeter)
  2. 전역 데이터 피하기 (Avoid global data)
    • 코드가 전역 데이터를 참조하면 해당 데이터를 공유하는 다른 컴포넌트와 결합됨
    • 필요한 컨텍스트는 명시적으로 모듈에 전달하라 (생성자 파라미터, 함수 인자)
    • 싱글톤 패턴 사용에 주의 — 전역 변수처럼 사용되면 불필요한 결합을 야기
  3. 유사 함수 피하기 (Avoid similar functions)
    • 시작과 끝은 같지만 중간 알고리즘만 다른 함수들이 있다면 중복 코드의 징후
    • Strategy 패턴 적용을 고려하라

테스팅 (Testing)

  • 직교적인 시스템은 단위 테스트만으로도 충분히 테스트 가능 (통합 테스트 의존도 감소)
  • 단위 테스트를 빌드하기 위해 시스템의 많은 부분을 임포트해야 한다면, 해당 모듈은 디커플링이 잘 되어 있지 않은 것
  • 버그 수정 시 직교성을 평가하라: 수정이 하나의 모듈에만 국한되는가, 아니면 변경이 시스템 전체에 흩어지는가?

문서화 (Documentation)

  • 직교성은 문서에도 적용됨: 콘텐츠(내용)와 프레젠테이션(표현 형식)은 분리되어야 한다
  • Markdown처럼 콘텐츠에만 집중하고 렌더링 도구에 프레젠테이션을 맡기는 방식이 이상적

Topic 11: Reversibility (가역성)

돌이킬 수 없는 결정의 위험성

  • 프로젝트가 진행될수록 중요한 결정들이 쌓이면서 선택지가 점점 좁아진다
  • 한 번 특정 벤더의 DB, 특정 아키텍처 패턴, 특정 배포 모델을 선택하면 막대한 비용 없이는 되돌릴 수 없다
  • 요구사항, 사용자, 하드웨어는 소프트웨어 개발 속도보다 빠르게 변한다

Tip 18: There Are No Final Decisions

결정은 돌에 새기는 것이 아니라 해변의 모래 위에 쓰는 것으로 생각하라. 큰 파도가 언제든 지워버릴 수 있다.

가역성을 확보하는 방법

  • DRY 원칙, 디커플링, 외부 설정을 사용하면 불가역적인 결정의 수를 줄일 수 있다
  • DB를 단순히 “영속성 서비스”로 추상화하면 중간에 DB를 교체할 수 있다
  • 브라우저 기반 앱이 모바일 앱으로 전환되어도 서버 측에서는 HTML 렌더링을 API로 교체하는 것으로 충분해야 한다

유연한 아키텍처 (Flexible Architecture)

  • 2019년 기준 서버 사이드 아키텍처의 변화 역사 (빅 아이언 → 클러스터 → VM → 컨테이너 → 서버리스 → 다시 빅 아이언)를 보면 미래를 예측하기 불가능하다는 것을 알 수 있다
  • 대신 변경을 쉽게 만들어라:
    • 서드파티 API를 자체 추상화 레이어 뒤에 숨겨라
    • 코드를 컴포넌트로 분리하라 (단일 서버에 배포하더라도)
    • 모놀리식 앱을 나중에 분리하는 것은 처음부터 컴포넌트화하는 것보다 훨씬 어렵다

Tip 19: Forgo Following Fads

유행을 쫓지 마라. 코드가 상황이 좋을 때는 앞으로 나아가고(rock on), 변화가 닥치면 유연하게 대응할 수 있도록(roll with the punches) 만들어라.

Topic 12: Tracer Bullets (예광탄 개발)

개념

  • 군용 예광탄(Tracer Bullets): 실제 총알 사이에 섞인 인광 탄환으로, 궤적을 시각화해 실시간 피드백을 제공하며 조준을 수정하는 데 사용
  • 소프트웨어에서도 동일한 원칙 적용: 실제 환경에서, 실제 제약 조건 하에 빠른 피드백을 얻는 것

Tip 20: Use Tracer Bullets to Find the Target

예광탄 개발이 필요한 상황

  • 사용자가 이런 시스템을 본 적 없어 요구사항이 막연할 때
  • 익숙하지 않은 알고리즘/기술/라이브러리/언어를 사용할 때
  • 프로젝트 기간 동안 환경이 변할 것이 확실할 때

예광탄 코드의 특성

  • 버려지는 코드가 아니다 — 에러 체크, 구조화, 문서화, 자기 검증 코드가 포함된 실제 프로덕션 코드
  • 단지 아직 완전한 기능을 갖추지 않았을 뿐이다
  • 일단 end-to-end 연결이 이루어지면, 목표에 얼마나 가까운지 확인하고 조정할 수 있다

예광탄 개발의 5가지 장점

  1. 사용자가 일찍 작동하는 것을 볼 수 있다 — 기능이 부족해도 실제 진행 상황을 보며 기여할 수 있다
  2. 개발자에게 작업 구조를 제공한다 — 빈 종이 대신 end-to-end 상호작용이 구현된 뼈대 위에서 작업
  3. 통합 플랫폼을 제공한다 — 매일 통합, 새 코드의 영향이 명확히 보임
  4. 데모할 것이 항상 있다 — 언제든 스폰서/경영진에게 보여줄 수 있는 것이 존재
  5. 진행 상황을 더 잘 파악할 수 있다 — 유즈케이스 단위로 진행, “95% 완성”이 몇 주간 계속되는 문제 방지

예광탄 코드 vs 프로토타입

예광탄 코드 프로토타입
목적 전체 시스템이 어떻게 연결되는지 탐색 특정 측면(알고리즘, UI, 아키텍처)만 탐색
코드 수명 버리지 않는다 - 최종 시스템의 뼈대 폐기 예정 - 학습 후 버림
완성도 기능은 적지만 프로덕션 품질 품질보다 빠른 탐색이 목적
비유 실제 병력 이동 전 정찰 후 쏘는 첫 발 정찰/정보 수집 단계 자체

프로토타이핑은 예광탄을 발사하기 전에 이루어지는 정찰 및 정보 수집이다.

Topic 13: Prototypes and Post-it Notes (프로토타입)

프로토타입이란?

  • 자동차 제조사가 공기역학, 구조, 스타일링 등 특정 측면을 검증하기 위해 클레이 모델을 만들듯, 소프트웨어도 리스크를 분석하고 수정 기회를 저비용으로 얻기 위해 프로토타입을 만든다
  • 코드 기반일 필요는 없다: Post-it 노트(워크플로, 앱 로직), 화이트보드(UI), 인터페이스 빌더(비기능적 목업) 모두 프로토타입이다

Tip 21: Prototype to Learn

프로토타입의 가치는 만들어진 코드가 아니라 배운 교훈에 있다.

프로토타입으로 탐색할 것들

  • 아키텍처
  • 기존 시스템의 새로운 기능
  • 외부 데이터의 구조나 내용
  • 서드파티 툴이나 컴포넌트
  • 성능 이슈
  • UI 디자인

프로토타입에서 무시해도 되는 것들

  • 정확성(Correctness): 적절한 곳에 더미 데이터 사용 가능
  • 완성도(Completeness): 매우 제한된 입력/메뉴 항목만으로 동작해도 됨
  • 견고성(Robustness): 에러 처리가 없거나 불완전해도 됨
  • 스타일(Style): 주석이나 문서화가 거의 없어도 됨

아키텍처 프로토타이핑 시 확인할 것들

  • 주요 영역의 책임이 잘 정의되어 있고 적절한가?
  • 주요 컴포넌트 간 협력이 잘 정의되어 있는가?
  • 커플링이 최소화되어 있는가?
  • 잠재적인 중복 소지가 있는가?
  • 인터페이스 정의와 제약이 수용 가능한가?
  • 각 모듈이 실행 중 필요한 데이터에 필요한 시점에 접근할 수 있는가?

프로토타입 사용 시 주의사항

  • 반드시 폐기 예정 코드임을 명확히 밝혀라 — 그렇지 않으면 스폰서나 관리자가 프로토타입을 그대로 배포하려 할 수 있다
  • 발사나무와 덕테이프로 만든 자동차 모델이 매력적으로 보일 수 있지만, 실제 도로에서 운전하면 안 된다
  • 프로토타입 코드가 오해받을 가능성이 크다면 예광탄 접근법이 더 적합하다

Topic 14: Domain Languages (도메인 언어)

핵심 개념

  • 프로그래밍 언어는 문제를 생각하는 방식에 영향을 미친다
  • C++으로 설계한 솔루션과 Haskell로 설계한 솔루션은 다른 결과를 낳는다
  • 더 중요한 것: 문제 도메인의 언어도 프로그래밍 솔루션을 제안할 수 있다

Tip 22: Program Close to the Problem Domain

도메인의 어휘, 구문, 의미를 사용해 실제로 프로그래밍하라.

내부 도메인 언어 (Internal Domain Language)

호스트 언어 안에서 구현되며, 메타프로그래밍이나 매크로를 활용해 확장된 어휘처럼 동작한다.

RSpec (Ruby):

describe BowlingScore do
  it "totals 12 if you score 3 four times" do
    score = BowlingScore.new
    4.times { score.add_pins(3) }
    expect(score.total).to eq(12)
  end
end

Phoenix Router (Elixir):

scope "/", HelloPhoenix do
  pipe_through :browser
  get "/", PageController, :index
  resources "/users", UserController
end

외부 도메인 언어 (External Domain Language)

독립적인 언어로 파서가 필요하며, 코드가 이를 읽어 데이터 구조나 실행 가능한 형태로 변환한다.

Cucumber (테스트 명세):

Feature: Scoring
Background:
  Given an empty scorecard

Scenario: bowling a lot of 3s
  Given I throw a 3
  And I throw a 3
  Then the score should be 12

Ansible (서버 설정, YAML):

- name: install nginx
  apt: name=nginx state=latest

- name: ensure nginx is running
  service: name=nginx state=started enabled=yes

내부 vs 외부 언어 트레이드오프

내부 도메인 언어 외부 도메인 언어
장점 호스트 언어의 모든 기능 활용 가능, 별도 파서 불필요 구문에 제약 없음, 사용자가 직접 작성 가능
단점 호스트 언어 구문에 묶임, 표현의 한계 존재 파서 작성 필요, 새 라이브러리/도구 추가 필요
적합한 경우 개발자가 작성하는 경우 앱 사용자가 직접 작성하는 경우

저비용 내부 도메인 언어 팁

메타프로그래밍이 필요 없다 — 그냥 함수를 작성하면 된다:

// describe, it, expect, toBe 모두 그냥 함수다
function describe(name: string, fn: () => void) { fn(); }
function it(name: string, fn: () => void) { fn(); }
function expect<T>(value: T) {
  return { toBe: (expected: T) => assert(value === expected) };
}

describe("BowlingScore", () => {
  it("totals 12 if you score 3 four times", () => {
    const score = new BowlingScore();
    for (let i = 0; i < 4; i++) score.addPins(3);
    expect(score.total).toBe(12);
  });
});
  • 가능하면 YAML, JSON, CSV 같은 기성 외부 언어를 활용하라
  • 그게 안 되면 내부 언어를 검토하라
  • 외부 언어를 직접 만드는 것은 절약되는 노력보다 들어가는 노력이 더 많지 않을 때만 정당화된다

Topic 15: Estimating (추정)

Tip 23: Estimate to Avoid Surprises

추정하는 법을 익히고 사물의 크기를 직관적으로 느낄 수 있게 되면, 어떤 것이 실현 가능한지 마술처럼 판단할 수 있는 능력이 생긴다.

추정의 정확도: 단위가 의미를 결정한다

같은 기간도 단위에 따라 다른 정밀도를 내포한다:

기간 추정 단위
1 ~ 15일 일(Days)
3 ~ 6주 주(Weeks)
8 ~ 20주 월(Months)
20주 이상 신중하게 다시 생각하라
  • “130 근무일”은 정밀한 수치처럼 들리지만, “약 6개월”은 5~7개월 사이 어느 때로 이해된다
  • 의도한 정확도를 반영하는 단위를 선택하라

추정값은 어디서 나오는가?

모든 추정은 문제의 모델에 기반한다. 실용적인 추정 절차:

  1. 이미 해본 사람에게 물어라: 정확히 같은 상황이 없어도 다른 사람의 경험을 참고할 수 있다
  2. 무엇을 요청받는지 이해하라: 범위와 정확도 요구 수준을 파악하라
  3. 시스템의 모델을 만들어라: 러프한 멘탈 모델을 구축하라 (서버/트래픽 등)
  4. 모델을 컴포넌트로 분해하라: 각 컴포넌트가 결과에 어떻게 기여하는지 파악
  5. 각 파라미터에 값을 부여하라: 곱하거나 나누는 파라미터가 더하는 파라미터보다 결과에 더 큰 영향을 미친다
  6. 답을 계산하라: 핵심 파라미터를 변화시키며 여러 시나리오로 계산
  7. 추정 기록을 유지하라: 실제 결과와 비교하고 다음 추정을 개선하라

프로젝트 일정 추정: 두 가지 기법

기법 1: 페인팅 더 미사일 (PERT 방식)

  • 모든 태스크에 낙관적/가장 가능성 있는/비관적 세 가지 추정치를 부여
  • 태스크를 의존성 네트워크로 구성하고 통계로 전체 일정의 최적/최악 시나리오 도출
  • 단점: 팀이 공식을 사용했다는 이유만으로 정확하다고 착각하는 경향이 있음

기법 2: 코끼리를 먹는 법 (반복적 개발)

  • 종종 일정을 파악하는 유일한 방법은 동일한 프로젝트를 직접 경험하는 것
  • 아래 단계를 매우 얇은 기능 슬라이스로 반복:
    1. 요구사항 확인
    2. 리스크 분석 (리스크가 높은 항목을 먼저)
    3. 설계, 구현, 통합
    4. 사용자와 검증

Tip 24: Iterate the Schedule with the Code

코드와 함께 일정을 반복 개선하라. 초기 반복이 끝나면 그 경험을 기반으로 다음 반복을 더 정확하게 추정할 수 있다.

추정을 요청받았을 때 가장 좋은 답변

“다시 알려드릴게요(I’ll get back to you).”

커피 머신 옆에서 즉흥적으로 던진 추정은 (커피처럼) 나중에 반드시 부메랑이 된다. 위에서 설명한 단계를 천천히 밟는 것이 항상 더 나은 결과를 낳는다.

Chapter 3: The Basic Tools (기본 도구들)

도구는 재능을 증폭시킨다. 도구가 좋을수록, 그리고 도구를 잘 사용할수록, 더 생산적이 될 수 있다.

목수가 기본 도구 세트를 갖추고 시작하듯, 프로그래머도 기본 도구 세트를 갖춰야 한다. 처음에는 범용 도구로 시작하고, 경험이 쌓이면서 필요에 따라 추가해 나간다. IDE 하나에만 의존하는 것은 실수다. IDE가 제공하는 편안함의 한계를 넘어 자유롭게 작업할 수 있어야 한다.

Topic 16: The Power of Plain Text (플레인 텍스트의 힘)

Tip 25: Keep Knowledge in Plain Text

플레인 텍스트란

  • 프래그머틱 프로그래머의 원재료는 목재나 철이 아니라 지식(knowledge)
  • 지식을 영속적으로 저장하는 최선의 포맷 = 플레인 텍스트
  • 단순한 무구조 텍스트가 아님. HTML, JSON, YAML, 소스 코드 모두 플레인 텍스트
  • 핵심은 사람이 읽고 이해할 수 있어야 한다는 것
# 나쁜 예: 의미를 알 수 없음
AC27123456789B11P

# 좋은 예: 자기 서술적
<SOCIAL-SECURITY-NO>123-45-6789</SOCIAL-SECURITY-NO>

바이너리 포맷의 문제점

  • 데이터를 이해하기 위한 컨텍스트가 데이터 자체와 분리되어 있음
  • 애플리케이션 로직 없이는 데이터가 암호화된 것과 다름없음
  • 포맷 전체를 알아야만 파싱 가능

플레인 텍스트의 3가지 장점

장점 설명
진부화 방지 (Insurance Against Obsolescence) 사람이 읽을 수 있는 자기 서술적 데이터는 그것을 만든 애플리케이션보다 오래 살아남는다
기존 도구 활용 (Leverage) VCS, 에디터, grep, diff, awk, sed 등 유니버스의 거의 모든 도구가 플레인 텍스트를 처리할 수 있다
테스트 용이성 (Easier Testing) 합성 테스트 데이터를 별도 도구 없이 간단히 추가/수정/삭제할 수 있다

Unix 철학과의 연결

  • Unix는 “각각 하나의 일을 잘 수행하는 작고 날카로운 도구들” 철학 위에 설계됨
  • 이 철학은 줄 단위 플레인 텍스트 포맷이라는 공통 기반으로 구현됨
  • 시스템 관리용 DB(사용자, 비밀번호, 네트워크 설정)도 모두 플레인 텍스트 파일

최소 공분모

  • 이종(heterogeneous) 환경에서 모든 당사자가 통신할 수 있는 공통 표준이 필요
  • 플레인 텍스트가 바로 그 표준

Topic 17: Shell Games (셸 활용)

Tip 26: Use the Power of Command Shells

셸은 프로그래머의 작업대

  • 목수에게 작업대가 있듯, 텍스트 파일을 다루는 프로그래머에게 작업대는 커맨드 셸
  • 셸 프롬프트에서 도구들을 호출하고, 파이프로 연결해 개발자가 상상하지 못한 방식으로 조합 가능
  • 애플리케이션, 디버거, 브라우저, 에디터, 유틸리티를 실행하고 시스템 상태를 쿼리하며 복잡한 매크로 명령을 구축할 수 있음

GUI의 한계

  • GUI의 장점: WYSIWYG (What You See Is What You Get)
  • GUI의 단점: WYSIAYG (What You See Is All You Get)
  • GUI 환경은 설계자가 의도한 기능으로 제한됨
  • 설계자가 제공한 모델을 벗어나야 할 때 GUI로는 불가능한 경우가 많음

GUI로 할 수 없는 것들

  • 반복 작업 자동화
  • 도구들을 조합하여 커스텀 매크로 도구 생성
  • IDE에 DBC(Design by Contract) 전처리기 같은 외부 기능을 훅(hook)으로 삽입

셸 커스터마이징 (나만의 셸 만들기)

  • 색상 테마 설정: 취향에 맞는 색상 테마 적용
  • 프롬프트 구성: 현재 디렉토리, VCS 상태, 시간 등 유용한 정보를 간결하게 표시
  • Alias와 셸 함수: 자주 쓰는 명령을 단축어로 만들기
# 업데이트/업그레이드 순서를 헷갈릴 때
alias apt-up='sudo apt-get update && sudo apt-get upgrade'

# rm 실행 전 항상 확인 요청
alias rm='rm -iv'
  • 커맨드 자동완성: 명령어 타입별로 문맥에 맞는 자동완성 설정

Topic 18: Power Editing (강력한 편집기 활용)

Tip 27: Achieve Editor Fluency

에디터 유창성(Fluency)이 중요한 이유

  • 에디터는 다른 소프트웨어 도구보다도 “손의 연장선”에 더 가깝다
  • 유창해지면 편집의 기계적 동작을 의식하지 않게 됨
  • 생각과 에디터 버퍼에 나타나는 것 사이의 거리가 극도로 줄어듦
  • 실용적 효과: 주당 20시간 편집 기준, 4% 효율 향상만으로 연간 1주일 이상 절약

유창성의 기준 체크리스트

  • 문자, 단어, 줄, 단락 단위로 이동 및 선택
  • 코드에서 구분자, 함수, 모듈 등 구문 단위로 이동
  • 변경 후 코드 재들여쓰기(re-indent)
  • 단일 명령으로 코드 블록 주석 처리/해제
  • Undo/Redo
  • 에디터 창을 여러 패널로 분할하고 패널 간 이동
  • 특정 줄 번호로 이동
  • 선택된 줄 정렬
  • 문자열과 정규표현식 검색, 이전 검색 반복
  • 선택이나 패턴 매칭을 기반으로 다중 커서 생성 및 병렬 편집
  • 현재 프로젝트 컴파일 에러 표시
  • 현재 프로젝트 테스트 실행

마우스/트랙패드 없이 위 모든 것을 할 수 있는가? 그렇지 않다면 전환을 고려할 때다.

유창성을 향상시키는 방법

  1. 편집 중 스스로를 관찰하라
  2. 반복적인 동작을 발견할 때마다 “더 나은 방법이 있을 것”이라고 생각하라
  3. 더 나은 방법을 찾아라
  4. 새로운 기능을 **근육 기억(muscle memory)**에 설치하라 - 의식하지 않고 사용할 수 있을 때까지 반복

에디터 성장시키기

  • 강력한 에디터는 대부분 확장(extension) 방식으로 구성됨
  • 에디터의 한계에 부딪히면 확장을 검색하라
  • 에디터의 확장 언어를 파헤치고 반복 작업을 자동화하라
  • 필요한 확장이 없으면 직접 만들고 공개하라

Topic 19: Version Control (버전 관리)

Tip 28: Always Use Version Control

VCS는 프로젝트 전체 Undo 키

  • 단순한 Undo 키를 넘어서, 프로젝트 전체 타임머신
  • 지난주 코드가 잘 돌아갔던 시점으로 언제든 돌아갈 수 있음

공유 디렉토리는 버전 관리가 아니다

  • 네트워크나 클라우드 스토리지로 소스를 공유하는 것은 위험함
  • 동기화 없는 공유 데이터로 동시성 코드를 작성하는 것과 같음
  • VCS 소프트웨어가 사용하는 파일/디렉토리 구조를 두 인스턴스가 동시에 변경하면 전체 상태가 손상될 수 있음

VCS가 추적할 수 있는 것들

  • 이 코드 줄을 누가 변경했는가?
  • 현재 버전과 지난주 버전의 차이는?
  • 이번 릴리즈에서 몇 줄이 변경되었는가?
  • 가장 자주 변경되는 파일은?

무조건 사용해야 하는 상황

  • 1인 팀, 1주짜리 프로젝트도 예외 없음
  • 폐기될 프로토타입도 예외 없음
  • 소스 코드가 아니어도 예외 없음 (문서, 전화번호 목록, 메모, Makefile, 셸 스크립트 등 모두)

브랜칭(Branching)의 힘

  • VCS의 가장 강력한 기능 중 하나
  • 개발의 고립된 섬(island)을 만들어 작업 간 간섭을 방지
  • Feature A와 Feature B를 각각의 브랜치에서 동시에 개발 가능
  • 브랜치는 팀의 프로젝트 워크플로우의 핵심

VCS를 프로젝트 허브로 활용

좋은 VCS 호스팅 플랫폼이 제공해야 할 기능들:

  • 강력한 보안 및 접근 제어
  • 직관적인 UI
  • 커맨드라인 접근 가능 (자동화를 위해)
  • 자동화된 빌드 및 테스트
  • 브랜치 머지 지원 (Pull Request)
  • 커밋/머지와 연동된 이슈 관리
  • 칸반 보드 형태의 리포팅
  • 팀 커뮤니케이션 (알림, 위키 등)

실제 사례: 노트북 복구 실험

노트북에 차를 쏟았다. 새 컴퓨터를 샀다. 얼마나 빨리 이전과 같은 상태로 복구할 수 있는가?

VCS에 다음을 저장했다면 당일 오후에 복구 가능:

  • 사용자 환경설정 및 dotfiles
  • 에디터 설정
  • Homebrew 설치 소프트웨어 목록
  • 앱 구성을 위한 Ansible 스크립트
  • 모든 현재 프로젝트

Topic 20: Debugging (디버깅)

Tip 29: Fix the Problem, Not the Blame
Tip 30: Dont Panic (당황하지 마라)

디버깅의 심리학

  • 디버깅을 감정적 주제로 대하는 개발자가 많음 (부정, 책임 전가, 변명, 무관심)
  • 핵심: **디버깅은 그냥 문제 해결(problem solving)**이다
  • 버그가 내 잘못이든 남의 잘못이든 상관없이, 그것은 나의 문제

올바른 디버깅 마인드셋

  • 자아 방어 기제를 내려놓아라
  • 프로젝트 압박을 차단하라
  • “그럴 리가 없어”라는 생각을 즉시 차단하라 - 명백히 일어난 일이다
  • 근시안적 시각 주의: 눈에 보이는 증상만 고치려 하지 마라. 실제 결함은 관찰된 것에서 몇 단계 떨어진 곳에 있을 수 있다
  • 항상 근본 원인(root cause) 을 찾아라

시작하기 전

  • 경고(warning) 없이 깨끗하게 빌드된 코드에서 시작하라
  • 컴파일러 경고 수준을 최대한 높게 설정하라
  • 컴퓨터가 찾을 수 있는 문제에 시간을 낭비하지 마라

버그 재현 (Reproducing Bugs)

Tip 31: Failing Test Before Fixing Code

  • 재현할 수 없으면 고쳐졌는지 알 수 없다
  • 목표: 길고 복잡한 단계가 아닌 단 하나의 명령으로 버그를 재현
  • 테스트를 작성하도록 강제함으로써 종종 수정 방법에 대한 통찰을 얻게 됨

디버깅 전략들

에러 메시지 읽기

Tip 32: Read the Damn Error Message

  • 예외가 발생하면 즉시 코드로 탭을 전환하는 것이 습관인 개발자들이 많다
  • 에러 메시지를 먼저 읽어라. 그것으로 충분할 때가 많다.

나쁜 결과값 추적

  • 실패하는 테스트로 디버거를 트리거하라
  • 디버거에서 올바른 위치에서 잘못된 값을 보고 있는지 먼저 확인하라
  • 콜 스택을 위아래로 이동하며 로컬 스택 환경을 검사하라
  • 단서를 추적하다가 원래 위치를 잃지 않도록 메모를 남겨라

바이너리 초핑 (Binary Chop)

  • 이진 탐색 원리를 디버깅에 적용
  • 선형 탐색은 O(n), 이진 탐색은 O(log n)
flowchart TD
    A[스택 트레이스 64 프레임] --> B[32번째 프레임 확인]
    B --> C{에러 발생?}
    C -->|예| D[1~32번 프레임에 집중]
    C -->|아니오| E[33~64번 프레임에 집중]
    D --> F[16번째 프레임 확인]
    E --> G[48번째 프레임 확인]
    F --> H[최대 6번으로 원인 특정]
    G --> H

적용 범위:

  • 거대한 스택 트레이스 탐색
  • 특정 데이터셋에서만 발생하는 버그 (데이터를 절반씩 분리)
  • 릴리즈 간 회귀 버그 (절반 지점 릴리즈를 선택해 테스트)

로깅 및 트레이싱

  • 디버거는 현재 프로그램 상태에 집중
  • 트레이싱은 시간에 따른 상태 변화를 추적
  • 이벤트 기반 시스템, 동시성, 실시간 시스템에서 특히 유용
  • 트레이스 메시지는 정규적이고 일관된 포맷으로 작성 (자동 파싱을 위해)

고무 오리 디버깅 (Rubber Ducking)

  • 다른 사람(혹은 고무 오리)에게 코드가 해야 할 일을 단계별로 설명
  • 상대방은 아무 말도 하지 않아도 됨
  • 설명하는 과정에서 당연하게 여겼던 가정들을 명시적으로 언급하게 됨
  • 이 과정에서 문제가 스스로 드러나는 경우가 많음

배제 과정 (Process of Elimination)

Tip 33: “select” Isn’t Broken

  • 버그가 OS, 컴파일러, 서드파티 라이브러리에 있다는 생각이 드는가? 그것은 거의 항상 틀렸다
  • 발굽 소리가 나면 얼룩말이 아닌 말을 먼저 생각하라
  • 단 하나를 바꿨을 때 시스템이 멈췄다면, 그것이 원인일 가능성이 높다
  • 서드파티 라이브러리라도, 버그 리포트를 제출하기 전에 반드시 내 코드를 먼저 배제하라

놀라움의 요소 (The Element of Surprise)

Tip 34: Don’t Assume It - Prove It

  • 버그에 놀랐다면, 하나 이상의 가정이 틀렸다는 뜻
  • “이 코드는 작동한다는 걸 알아”라며 코드를 건너뛰지 마라
  • 이 컨텍스트에서, 이 데이터로, 이 경계 조건에서 작동한다는 것을 증명하라

놀라운 버그를 발견했을 때 해야 할 것들:

  • 이 실패가 왜 더 일찍 잡히지 않았는지 파악
  • 단위 테스트를 보완해 다음엔 잡을 수 있게 만들기
  • 시스템 내 유사한 취약점이 있는 곳을 찾아 함께 수정
  • 버그가 팀원 한 명의 잘못된 가정 때문이라면, 팀 전체와 공유

디버깅 체크리스트

  • 보고된 문제가 근본 버그의 직접적 결과인가, 아니면 단순한 증상인가?
  • 버그가 정말로 프레임워크/OS에 있는가, 내 코드에 있는가?
  • 이 문제를 동료에게 자세히 설명한다면 뭐라고 말할 것인가?
  • 의심 코드가 단위 테스트를 통과한다면, 테스트가 충분히 완전한가?
  • 시스템의 다른 곳에도 이 버그를 유발한 조건이 존재하는가?

Topic 21: Text Manipulation (텍스트 조작)

Tip 35: Learn a Text Manipulation Language

범용 텍스트 조작 도구의 필요성

  • 셸, 에디터, 디버거는 특정 작업에 특화된 “끌, 톱, 대패”와 같음
  • 때로는 이 기본 도구 세트로 처리하기 어려운 변환 작업이 필요
  • 텍스트 조작 언어는 목공의 라우터(router)처럼 강력하고 범용적

왜 습득해야 하는가

  • 일반 언어보다 5~10배 빠르게 유틸리티를 만들고 아이디어를 프로토타이핑할 수 있음
  • 30분짜리 실험이 5시간짜리 작업보다 훨씬 낫다
  • Kernighan과 Pike의 실험: 같은 프로그램을 5개 언어로 구현했을 때 Perl 버전은 17줄, C 버전은 150줄

도구 선택

환경 도구
Unix/macOS awk, sed, 셸 조합
구조적 작업 Python, Ruby

실제 활용 사례 (이 책 자체가 텍스트 조작으로 빌드됨)

  • 빌드 시스템: Pragmatic Bookshelf의 빌드 시스템 전체가 Ruby로 작성됨
  • 코드 포함 및 하이라이팅: Ruby 스크립트가 소스 파일에서 코드 세그먼트를 추출하고 구문 하이라이팅 후 조판 언어로 변환
  • 웹사이트 업데이트: 목차를 추출해 웹사이트에 자동 업로드
  • 수식 포함: LaTeX 수학 마크업을 포맷된 텍스트로 변환하는 Python 스크립트
  • 인덱스 생성: 텍스트에 마크업된 인덱스 항목을 Ruby 스크립트가 정리

Topic 22: Engineering Daybooks (엔지니어링 일지)

일지란 무엇인가

  • 전통적인 엔지니어들이 사용하는 종이 노트북
  • 작업한 것, 배운 것, 아이디어 스케치, 계측 수치 등 업무 관련 모든 것을 기록
  • 노트북이 가득 차면 등에 날짜 범위를 적고 선반에 꽂아둠

일지의 3가지 주요 장점

  1. 기억보다 신뢰할 수 있음: “지난주에 연락한 그 회사 이름이 뭐였지?“라는 질문에 즉시 답할 수 있음
  2. 관련 없는 아이디어를 저장하는 공간: 현재 작업에 집중하면서도 떠오른 좋은 아이디어를 잃지 않음
  3. 고무 오리 역할: 무언가를 쓰기 위해 멈추는 순간 뇌가 기어를 전환. 방금 한 행동이 완전히 잘못되었다는 것을 깨닫기도 함

실천 방법

  • 파일이나 위키가 아닌 종이를 사용하라 - 타이핑과는 다른 특별함이 있음
  • 한 달 동안 시도해보고 효과를 판단하라
  • 디버깅 중 변수 값 기록, 미팅 노트, 물건 위치 메모, 거친 아이디어 기록 등에 활용

Chapter 4: Pragmatic Paranoia (실용주의적 편집증)

Tip 36: You Can’t Write Perfect Software

완벽한 소프트웨어는 존재하지 않는다. 이것을 삶의 공리로 받아들여라. 짧은 컴퓨팅 역사에서 완벽한 소프트웨어를 작성한 사람은 아무도 없다. 이 우울한 현실을 어떻게 강점으로 전환하는가가 이 챕터의 주제다.

프래그머틱 프로그래머는 자기 자신도 믿지 않는다. 자신을 포함해 아무도 완벽한 코드를 작성하지 못한다는 것을 알기에, 자신의 실수에 대한 방어막을 코드 안에 구축한다.

Topic 23: Design by Contract (계약에 의한 설계)

Tip 37: Design with Contracts

DBC란 무엇인가

  • Bertrand Meyer가 Eiffel 언어를 위해 개발한 기법
  • 소프트웨어 모듈의 권리와 책임에 대해 문서화하고 동의함으로써 프로그램 정확성을 보장
  • 올바른 프로그램이란: 주장하는 것 이상도, 이하도 하지 않는 것

계약의 3가지 요소

요소 설명 책임
사전 조건 (Preconditions) 루틴이 호출되기 위해 참이어야 하는 것 (루틴의 요구사항) **호출자(Caller)**의 책임. 루틴은 사전 조건이 위반된 상태로 호출되어선 안 됨
사후 조건 (Postconditions) 루틴이 완료될 때 보장하는 것. 사후 조건이 있다는 것은 루틴이 반드시 종료됨을 의미 (무한 루프 불가) **루틴(Callee)**의 책임
클래스 불변조건 (Class Invariants) 클래스가 호출자 관점에서 항상 참임을 보장하는 조건. 내부 처리 중에는 위반될 수 있지만, 루틴이 종료될 때는 반드시 참이어야 함 클래스의 책임

계약 요약:

“호출자가 모든 사전 조건을 충족시킨다면, 루틴은 완료 시 모든 사후 조건과 불변조건이 참임을 보장한다. 어느 한쪽이 계약을 어기면 이미 합의된 방법(예외 발생 또는 프로그램 종료)이 발동된다.”

TypeScript로 보는 DBC 구현

런타임 assertion을 이용한 유사 구현:

function acceptDeposit(accountId: string, amount: number): string {
  // 사전 조건 (Preconditions) - 호출자의 책임 검증
  if (amount <= 0) {
    throw new Error(`Precondition violated: amount must be > 0, got ${amount}`);
  }
  if (!isAccountOpen(accountId)) {
    throw new Error(`Precondition violated: account ${accountId} must be open`);
  }

  // 실제 처리
  const transactionId = createTransaction(accountId, "deposit", amount);

  // 사후 조건 (Postconditions) - 루틴이 약속한 것을 이행했는지 검증
  const transaction = getTransaction(transactionId);
  if (!transaction || transaction.accountId !== accountId) {
    throw new Error(`Postcondition violated: transaction not found for account ${accountId}`);
  }

  return transactionId;
}

게으른 코드 작성 원칙

받아들이는 것에는 엄격하고, 돌려주는 것에는 최대한 적게 약속하라

  • 무엇이든 받고 세상 모든 것을 돌려준다고 약속하면, 작성해야 할 코드가 엄청나게 많아짐
  • 사전 조건을 좁히면 sqrt에 음수를 전달하는 문제처럼, 올바른 데이터를 전달할 책임이 호출자에게 명확히 귀속됨

DBC와 조기 종료(Crashing Early)

  • DBC는 조기 종료(Topic 24) 개념과 잘 맞음
  • assert 또는 DBC 메커니즘을 이용해 조기 종료 가능
  • Java/C에서 sqrt(-1)NaN을 반환하는 것보다 즉시 에러를 발생시키는 것이 훨씬 나음

시맨틱 불변조건 (Semantic Invariants)

  • 불변의 법칙, 즉 변경될 수 없는 철학적 계약을 표현
  • 예시: 직불카드 시스템 - “같은 트랜잭션이 두 번 처리되어서는 절대 안 된다”
    • 이것을 “소비자에게 유리한 방향으로 에러를 내라”는 단 하나의 규칙으로 표현
  • 단순히 바뀔 수 있는 비즈니스 정책(dynamic business rules)과 구분할 것
  • 시스템의 의미의 핵심이어야 하며, 정책 변경의 여지가 없어야 함
  • 문서화된 요구사항, 공용 화이트보드 등에 명확하게 표시하라

동적 계약과 에이전트 (Dynamic Contracts)

  • 자율적 에이전트 환경에서 계약은 고정불변일 필요가 없음
  • 에이전트는 원치 않는 요청을 거부하고, 계약을 재협상할 수 있음
  • 충분한 컴포넌트와 에이전트가 서로 계약을 협상할 수 있다면, 소프트웨어 생산성 위기를 소프트웨어 자체가 해결할 수 있을지도 모름
  • 손으로 계약을 작성할 수 없다면, 자동으로도 사용할 수 없다

DBC vs TDD

구분 DBC TDD
설정/목킹 필요 불필요 필요
커버리지 모든 케이스의 성공/실패 파라미터를 정의 한 번에 하나의 특정 케이스만 타겟
활성 시점 설계, 개발, 배포, 유지보수 항상 빌드 사이클의 테스트 시점
내부 불변조건 코드 내부 불변조건 검증에 집중 공개 인터페이스를 검증하는 블랙박스 방식
중복 방지 모두가 데이터를 검증해야 하는 방어적 프로그래밍보다 DRY 방어적 프로그래밍과 혼용 시 중복 발생

둘은 상호 배타적이지 않으며 상호 보완적이다.

Topic 24: Dead Programs Tell No Lies (죽은 프로그램은 거짓말을 하지 않는다)

Tip 38: Crash Early

모든 에러는 정보다

  • 라이브러리나 프레임워크 루틴이 먼저 문제를 감지하는 경우가 많음
  • nil 값 전달, 빈 리스트, 누락된 키, 네트워크/파일시스템 에러 등
  • “그럴 리 없어” 멘탈리티에 빠지기 쉬움
  • 모든 에러는 정보다. 에러를 무시하면 훨씬 더 나중에, 훨씬 더 이상한 곳에서 문제가 터진다

조기에 충돌하라 (Crash Early)

  • 문제를 최대한 빨리 감지하는 것의 이점: 더 일찍 충돌할 수 있음
  • 충돌(crash)이 종종 최선의 선택
  • 대안: 계속 실행하면서 중요한 DB에 손상된 데이터를 쓰거나, 세탁기를 스무 번 연속으로 돌리는 상황

Erlang/Elixir 철학

  • Joe Armstrong (Erlang 창시자): “방어적 프로그래밍은 시간 낭비. 그냥 충돌하게 하라!”
  • 프로그램이 실패하도록 설계하되, 그 실패를 **슈퍼바이저(supervisor)**가 관리
  • 슈퍼바이저가 실패하면, 그 슈퍼바이저의 슈퍼바이저가 관리 → 슈퍼바이저 트리
  • 고가용성, 내결함성 시스템에 매우 효과적

죽은 프로그램이 덜 해롭다

  • 불가능한 일이 발생했을 때, 그 시점 이후 프로그램의 모든 동작은 신뢰할 수 없음
  • 죽은 프로그램은 보통 절름발이 프로그램보다 훨씬 적은 해를 끼친다

Topic 25: Assertive Programming (적극적 프로그래밍)

Assertion = 실행 시간에 가정을 검증하는 코드

Assertion의 올바른 사용

// 정렬 함수 후 정렬 여부를 assert로 검증하는 예시
function sortedArray(arr: number[]): number[] {
  const sorted = [...arr].sort((a, b) => a - b);
  
  // 사후 조건: 결과가 실제로 정렬되어 있는지 검증
  for (let i = 0; i < sorted.length - 1; i++) {
    if (sorted[i] > sorted[i + 1]) {
      throw new Error(
        `Assertion failed: array not sorted at index ${i}: ${sorted[i]} > ${sorted[i + 1]}`
      );
    }
  }
  
  return sorted;
}

Assertion 사용 시 주의사항

  • Heisenbug 주의: assertion에 부작용이 있어서는 안 됨
// 나쁜 예: nextElement() 호출이 이터레이터를 이동시키는 부작용 발생
while (iter.hasMoreElements()) {
  assert(iter.nextElement() !== null); // 이터레이터가 advance되어버림!
  const obj = iter.nextElement();      // 다음 요소를 건너뜀
}

// 좋은 예
while (iter.hasMoreElements()) {
  const obj = iter.nextElement();
  assert(obj !== null);
}

Assertion을 끄지 마라

흔한 오해: “테스트가 끝나면 assertion을 꺼서 성능을 높여야 한다”

이 주장의 두 가지 잘못된 가정:

  1. 테스트가 모든 버그를 찾는다고 가정 - 현실에서는 코드가 겪을 무수한 순열의 극히 일부만 테스트됨
  2. 프로덕션 환경이 안전하다고 가정 - 테스트 중에는 쥐가 통신 케이블을 갉아먹거나, 게임이 메모리를 소진하거나, 로그 파일이 스토리지를 가득 채우는 일이 없음

테스트에서 줄타기를 성공했다고 실전에서 안전망 없이 줄을 타는 것은 어리석다.

프로덕션에서 Assertion을 켜면 큰 이익을 얻을 수 있다

  • Andy의 이웃 스타트업 사례: 프로덕션 릴리즈에 assertion을 남겨두고, 실패 시 관련 데이터를 UI로 노출
  • 실제 사용자의 실제 조건에서의 피드백으로 재현하기 어려운 버그들을 수정
  • 결과: 매우 안정적인 소프트웨어 → 수억 달러에 인수

Topic 26: How to Balance Resources (리소스 균형 잡기)

Tip 40: Finish What You Start

핵심 원칙

  • 리소스를 할당한 함수 또는 객체가 그것을 해제할 책임을 진다
  • 대부분의 리소스 사용은 예측 가능한 패턴: 할당 → 사용 → 해제

나쁜 패턴 vs 좋은 패턴

// 나쁜 예: readCustomer와 writeCustomer가 파일을 열고,
// 전역 변수처럼 다른 함수가 닫는 구조
let customerFile: FileHandle | null = null;

function readCustomer(name: string): Customer {
  customerFile = openFile(`${name}.rec`, 'r');
  return parseCustomer(customerFile.read());
}

function writeCustomer(customer: Customer): void {
  customerFile!.write(serialize(customer));
  customerFile!.close(); // readCustomer가 연 파일을 다른 함수가 닫음
  customerFile = null;
}

// 좋은 예: 열고 닫는 책임이 동일한 함수에 있음
function updateCustomer(name: string, newBalance: number): void {
  const file = openFile(`${name}.rec`, 'r+');
  try {
    const customer = parseCustomer(file.read());
    customer.balance = newBalance;
    file.write(serialize(customer));
  } finally {
    file.close(); // 열었던 함수가 닫음
  }
}

중첩 할당 (Nested Allocations)

여러 리소스가 필요할 때의 규칙:

  1. 해제는 할당의 역순으로: 리소스 B가 리소스 A를 참조하는 경우, A를 먼저 해제하면 B가 댕글링 레퍼런스를 갖게 됨
  2. 동일한 리소스 세트를 다른 곳에서 할당할 때는 항상 같은 순서로: 데드락 방지 (프로세스 A가 resource1 → resource2 순으로, 프로세스 B가 resource2 → resource1 순으로 요청하면 영원히 대기)
flowchart LR
    A[resource1 할당] --> B[resource2 할당]
    B --> C[사용]
    C --> D[resource2 해제]
    D --> E[resource1 해제]
    style D fill:#f9f,stroke:#333
    style E fill:#f9f,stroke:#333
// 예외 안티패턴 - 잘못된 예
async function processFile(path: string): Promise<void> {
  const resource = allocateResource(); // 할당
  try {
    await process(resource);
  } finally {
    deallocate(resource); // 만약 allocateResource()에서 예외가 났다면?
    // resource가 할당되지 않은 채로 해제를 시도함!
  }
}

// 올바른 패턴
async function processFile(path: string): Promise<void> {
  const resource = allocateResource(); // 먼저 할당
  // 그 다음 try 블록 시작
  try {
    await process(resource);
  } finally {
    deallocate(resource); // 항상 실행됨
  }
}

TypeScript/Node.js에서는 using 키워드(Symbol.dispose)나 클래스 패턴으로 리소스를 캡슐화하는 것이 권장됨.

객체와 리소스 캡슐화

  • 리소스를 클래스로 감싸는 것이 좋은 접근
  • 생성자에서 할당, 소멸자에서 해제
  • GC가 있는 언어에서는 언어가 지원하는 스코프 기반 정리 메커니즘을 활용

균형을 잡을 수 없는 경우 (Dynamic Data Structures)

동적 데이터 구조에서는 누가 무엇을 해제할지 결정하는 시맨틱 불변조건이 필요:

  • 옵션 1: 최상위 구조가 포함된 모든 서브 구조를 재귀적으로 해제
  • 옵션 2: 최상위 구조만 해제 (서브 구조는 고아가 됨 → 메모리 누수 위험)
  • 옵션 3: 서브 구조가 있으면 최상위 구조 해제 거부

선택은 상황에 따라 다르지만 명시적으로 결정하고 일관되게 구현해야 한다.

균형 확인하기

  • 리소스 유형별 래퍼(wrapper) 를 만들어 모든 할당/해제를 추적
  • 메인 처리 루프의 최상단에서 리소스 사용량이 증가하지 않았는지 확인
  • 메모리 누수 검사 도구를 활용

Topic 27: Don’t Outrun Your Headlights (헤드라이트를 앞지르지 마라)

Tip 42: Take Small Steps - Always Tip 43: Avoid Fortune-Telling

헤드라이트 비유

  • 밤에 산길을 달리던 차가 급커브를 놓치고 추락 → “헤드라이트를 앞질렀다”
  • 헤드라이트의 조사 거리(throw distance) 를 넘어서는 속도로 달리면 제동/조향 불가
  • 저빔 헤드라이트의 평균 조사 거리: 약 160피트 (48m)
  • 40mph에서 정지 거리: 189피트 → 이미 헤드라이트를 앞질러 있음

소프트웨어에서의 “헤드라이트”

  • 미래를 내다보는 능력의 한계
  • 먼 미래를 예측할수록 틀릴 가능성이 높아짐
  • 대신: 코드가 교체 가능하도록(replaceable) 설계하라
  • 교체 가능한 코드는 응집도, 커플링, DRY 면에서도 자연스럽게 더 나은 설계로 이어짐

항상 작은 단계로 (Take Small Steps - Always)

작은 단계의 실용적 적용:

  • 항상 피드백을 확인하며 다음 단계를 조정
  • “너무 앞서가는 것”에서 벗어나기
  • 지금 명확하게 볼 수 있는 범위까지만 설계

하지 말아야 할 예측들:

  • 미래에 어떤 데이터베이스가 사용될지 추측
  • 미래에 어떤 배포 모델이 최선인지 추측
  • 미래에 어떤 기술이 사용 가능할지 추측

Black Swans (블랙 스완)

  • Nassim Nicholas Taleb의 개념: 예측 불가능하고 드물지만 불균형적으로 큰 영향을 미치는 사건들
  • 1판 Pragmatic Programmer 출판 당시 개발자들의 논쟁: “Motif vs OpenLook, GUI 전쟁에서 누가 이길까?” → 둘 다 패배, 브라우저 중심 웹이 지배
  • 미래에 자신감을 갖더라도, 모퉁이 너머에는 언제나 블랙 스완이 있을 수 있다

핵심 메시지

flowchart TD
    A[불확실한 미래] -->|예측 시도| B[높은 리스크]
    A -->|작은 단계 + 교체 가능한 설계| C[낮은 리스크]
    C --> D[피드백 기반 조정]
    D --> E[더 나은 결과]
    B --> F[잘못된 예측으로 낭비]
  • 미래를 예측하는 데 에너지를 낭비하는 대신, 코드를 교체하기 쉽게 만드는 데 집중
  • 교체 가능성은 응집도(cohesion), 느슨한 결합(decoupling), DRY로 자연스럽게 이어짐
  • 오늘 내릴 수 있는 결정만 내리고, 더 많은 정보를 가진 미래에 나머지를 결정하라

Chapter 5: Bend, or Break

변화의 속도를 따라가려면 코드를 최대한 유연하게(loose) 작성해야 한다. 이 챕터는 돌이킬 수 없는 결정 대신 변경 가능한 결정을 내리는 방법을 다룬다. 핵심 주제는 커플링 제거, 이벤트 처리, 함수 파이프라인, 상속 문제, 그리고 외부 설정이다.

Topic 28: Decoupling (디커플링)

커플링이란 무엇인가

  • 커플링은 변경의 적: 서로 연결된 것들은 반드시 동시에 변경되어야 하므로 변경 비용이 급증한다
  • 커플링은 전이(transitive)된다: A가 B, C에 의존하고, B가 M, N에 의존하면 A는 실질적으로 B, C, M, N 모두에 의존하는 것이다
  • 커플링의 증상:
    • 무관한 모듈 사이의 이상한 의존성
    • 한 모듈의 “간단한” 변경이 다른 모듈을 연쇄적으로 깨뜨림
    • 개발자들이 변경의 영향 범위를 몰라 코드 수정을 두려워함
    • 한 변경의 영향을 아무도 몰라 회의에 모두가 참석해야 하는 상황

Tip 44: Decoupled Code Is Easier to Change

Train Wreck (메서드 체인 지뢰)

문제 코드 — 5단계 추상화를 가로질러 내부 구조를 모두 노출함:

// BAD: 너무 많은 내부 구조 노출
function applyDiscount(customer: Customer, orderId: string, discount: number) {
  const totals = customer.orders.find(orderId).getTotals();
  totals.grandTotal = totals.grandTotal - discount;
  totals.discount = discount;
}

Tell, Don’t Ask 원칙 적용 — 단계적으로 개선:

// Step 1: 할인 계산 책임을 totals 객체에 위임
function applyDiscount(customer: Customer, orderId: string, discount: number) {
  customer.orders.find(orderId).getTotals().applyDiscount(discount);
}

// Step 2: order 검색 책임을 customer에게 위임
function applyDiscount(customer: Customer, orderId: string, discount: number) {
  customer.findOrder(orderId).getTotals().applyDiscount(discount);
}

// Step 3: totals 노출 제거 (최종 형태)
function applyDiscount(customer: Customer, orderId: string, discount: number) {
  customer.findOrder(orderId).applyDiscount(discount);
}

Tip 45: Tell, Don’t Ask
객체의 내부 상태를 꺼내와서 결정하고 다시 업데이트하지 말 것. 해당 결정과 행동은 객체 스스로 하게 하라.

Tip 46: Don’t Chain Method Calls
무언가에 접근할 때 .(dot)을 1개 이상 사용하지 말 것. 중간 변수를 사용해도 마찬가지로 체인이다. 예외: 언어 표준 라이브러리처럼 변경될 가능성이 거의 없는 것은 허용.

  • 파이프라인과 Train Wreck의 차이: 파이프라인은 숨겨진 구현 세부사항에 의존하지 않고 데이터를 변환하며 전달한다. 커플링이 존재하지만 Train Wreck보다 훨씬 관리하기 쉽다.

전역 데이터의 악(The Evils of Globalization)

  • 전역 데이터는 암묵적인 추가 파라미터: 모든 메서드에 전역 데이터가 숨겨진 파라미터로 존재하는 것과 같다
  • 싱글톤도 전역 데이터다: 싱글톤 안에 export된 인스턴스 변수를 두면 그것도 전역 데이터다. 이름만 길어진 것
  • 메서드 뒤에 숨겨도 여전히 전역: Config.getLogLevel() 형태로 감싸도, 데이터는 여전히 하나의 전역 집합이다
  • 외부 리소스도 전역 데이터: DB, 파일시스템, 외부 서비스 API — 모두 변경 가능한 전역 자원이다

Tip 47: Avoid Global Data

Tip 48: If It’s Important Enough to Be Global, Wrap It in an API
중요한 전역 자원이라면 반드시 직접 제어하는 코드(API) 뒤에 감싸라.

Topic 29: Juggling the Real World (이벤트 다루기)

이벤트란 무엇인가

  • 이벤트(Event): 정보의 가용성을 나타낸다. 버튼 클릭, 주가 업데이트, 계산 완료 등 무엇이든 될 수 있다
  • 이벤트에 반응하는 애플리케이션은 더 인터랙티브하고 자원을 효율적으로 활용한다
  • 전략 없이 이벤트를 다루면 타이트하게 커플링된 혼란스러운 코드가 된다

이벤트를 처리하는 4가지 전략:

  1. Finite State Machines (FSM)
  2. Observer Pattern
  3. Publish/Subscribe (Pub/Sub)
  4. Reactive Programming & Streams

1. Finite State Machines (유한 상태 기계)

  • FSM의 구조: 상태(State)의 집합 + 현재 상태 + 각 상태별 이벤트 → 다음 상태 매핑
  • 복잡한 이벤트 흐름을 순수한 데이터(테이블)로 표현할 수 있다는 것이 핵심
  • 개발자들이 FSM을 기피하지만, 실제로는 몇 줄로 구현 가능하고 매우 강력하다

TypeScript로 구현한 메시지 파서 FSM 예시:

type State = "initial" | "reading" | "done" | "error";
type MsgType = "header" | "data" | "trailer";

const TRANSITIONS: Record<State, Partial<Record<MsgType, State>>> = {
  initial: { header: "reading" },
  reading: { data: "reading", trailer: "done" },
  done: {},
  error: {},
};

function runFSM(messages: Array<{ msgType: MsgType }>) {
  let state: State = "initial";

  for (const msg of messages) {
    if (state === "done" || state === "error") break;
    state = TRANSITIONS[state][msg.msgType] ?? "error";
  }

  return state;
}

액션이 포함된 FSM — 상태 전환 시 실행할 액션도 함께 정의:

type Action =
  | "ignore"
  | "startNewString"
  | "addCurrentToString"
  | "finishCurrentString";

type Transition = [nextState: string, action: Action];

const STRING_TRANSITIONS: Record<string, Record<string, Transition>> = {
  lookForString: {
    '"': ["inString", "startNewString"],
    default: ["lookForString", "ignore"],
  },
  inString: {
    '"': ["lookForString", "finishCurrentString"],
    "\\": ["copyNextChar", "addCurrentToString"],
    default: ["inString", "addCurrentToString"],
  },
  copyNextChar: {
    default: ["inString", "addCurrentToString"],
  },
};
  • FSM의 상태는 외부 스토리지(DB 등)에 저장할 수도 있다 — 사용자 회원가입처럼 여러 단계를 거치는 워크플로우에 탁월
  • FSM은 이벤트 스트림 파서로서 훌륭하지만, 이벤트의 조합을 처리하거나 시간 차원이 필요하면 다른 전략이 필요

2. Observer Pattern (옵저버 패턴)

  • 구조: 이벤트 소스(Observable)와 구독자 목록(Observers)
  • Observer가 Observable에 콜백 함수 참조를 등록하고, 이벤트 발생 시 Observable이 순차적으로 호출
  • 라이브러리 없이도 구현 가능 — 함수 참조를 리스트에 push하고 이벤트 발생 시 호출하면 끝
class EventEmitter<T> {
  private callbacks: Array<(event: T) => void> = [];

  register(callback: (event: T) => void) {
    this.callbacks.push(callback);
  }

  emit(event: T) {
    this.callbacks.forEach((cb) => cb(event));
  }
}
  • 단점 1: Observer가 Observable에 직접 등록해야 하므로 커플링 발생
  • 단점 2: 콜백이 Observable 내부에서 동기적으로 처리되므로 성능 병목 가능

3. Publish/Subscribe (발행-구독)

  • Pub/Sub은 Observer Pattern의 일반화: 커플링 문제와 성능 문제를 동시에 해결
  • 구조: Publisher와 Subscriber는 서로를 모르고, 이름이 있는 채널(Channel)을 통해서만 통신
  • 채널 구현은 코드 외부(라이브러리, 프로세스, 분산 인프라)에 있어 코드와 분리
  • 통신이 비동기로 처리될 수 있어 성능 이슈 없음
  • 실행 중인 애플리케이션의 코드를 변경 없이 구독자를 추가/교체 가능
특성 Observer Pattern Pub/Sub
커플링 Observable에 직접 등록 필요 (커플링 있음) 채널을 통해 완전 분리
처리 방식 동기 (성능 병목 가능) 비동기 처리 가능
가시성 누가 구독하는지 명확 어떤 구독자가 있는지 파악 어려움
구현 복잡도 라이브러리 없이 간단히 구현 가능 외부 라이브러리/인프라 필요
  • 단점: Pub/Sub을 많이 사용하는 시스템에서 특정 메시지에 어떤 구독자가 반응하는지 파악하기 어렵다

4. Reactive Programming & Streams (리액티브 프로그래밍)

  • 스프레드시트 비유: 하나의 셀 값이 바뀌면 그 값을 참조하는 셀이 자동으로 업데이트되는 것이 리액티브
  • 스트림(Streams): 이벤트를 마치 컬렉션처럼 처리. 이벤트가 발생할 때마다 리스트가 늘어나는 것과 같은 개념
  • 스트림으로 이벤트를 조작(map, filter, zip, merge)하고, 여러 스트림을 결합하며, 동기/비동기 처리를 통합 API로 처리
  • 스트림 기반 코드 예시 (RxJS 스타일):
import { Observable, interval, zip, of } from "rxjs";

const animals = of("ant", "bee", "cat", "dog", "elk");
const ticker = interval(500);

// 동물 이름과 타이머를 zip하여 500ms마다 하나씩 출력
const combined = zip(animals, ticker);
combined.subscribe(([animal, tick]) => console.log(tick, animal));
  • 여러 HTTP 요청을 병렬로 실행하고 결과를 하나의 스트림으로 수집하는 것도 자연스럽게 표현됨
  • 이벤트 스트림은 시간을 명시적으로 관리하지 않아도 되게 해준다: 동기와 비동기를 동일한 API로 통합

이벤트는 어디에나 있다. 이벤트 중심으로 설계된 코드는 더 반응적이고, 더 잘 디커플링되어 있다.

Topic 30: Transforming Programming (변환 프로그래밍)

핵심 관점의 전환

  • 모든 프로그램은 데이터를 변환(Transform)한다: 입력을 받아 출력을 만든다
  • 클래스, 모듈, 자료구조에 집중하는 대신 “입력에서 출력으로의 변환 흐름” 으로 생각하면 구조가 명확해지고 커플링이 줄어든다
  • Unix 철학의 파이프(find . | xargs wc -l | sort -n | tail -5)가 이 사고방식의 완벽한 예시
디렉터리 이름
  → 파일 목록
  → 라인 수가 붙은 파일 목록
  → 정렬된 목록
  → 상위 5개

Tip 49: Programming Is About Code, But Programs Are About Data

변환 찾기 (Finding Transformations)

  • 탑다운 접근법: 요구사항의 전체 입력과 출력을 먼저 정의하고, 그 사이를 채우는 변환 단계를 찾는다
  • 예: 애너그램 찾기 ("lyvin" → 길이별 단어 목록)
"ylvin"
  → 3글자 이상 모든 조합 (Step 1)
  → 각 조합의 서명(sorted chars) (Step 2)
  → 서명이 일치하는 사전 단어 목록 (Step 3)
  → 길이별 그룹화 (Step 4)
  • 각 단계 자체도 더 작은 변환으로 분해할 수 있다

파이프라인 연산자와 TypeScript 적용

많은 함수형 언어는 파이프라인 연산자(|>)를 지원한다. TypeScript에서는 아직 없지만, 같은 사고방식을 적용할 수 있다:

// 파이프라인 연산자 없이 변환 표현 (명시적 할당)
const content = readFile(fileName);
const lines = findMatchingLines(content, pattern);
const result = truncateLines(lines);
// 유틸리티 pipe 함수를 직접 만들어 파이프라인처럼 표현
function pipe<T>(...fns: Array<(val: any) => any>) {
  return (value: T) => fns.reduce((acc, fn) => fn(acc), value);
}

const processFile = pipe(readFile, (content) =>
  findMatchingLines(content, pattern), truncateLines);

Tip 50: Don’t Hoard State; Pass It Around OOP는 데이터를 객체 안에 숨기고 객체들이 서로 상태를 변경한다 → 커플링의 원인. 변환 모델에서는 데이터가 코드와 동등한 수준의 흐름이다: 코드 → 데이터 → 코드 → 데이터.

에러 처리 전략

파이프라인에서 에러는 값을 래핑(wrapping)하는 방식으로 처리한다. 절대 원시 값을 그대로 전달하지 않는다.

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function ok<T>(value: T): Result<T> {
  return { ok: true, value };
}
function err<T>(error: string): Result<T> {
  return { ok: false, error };
}

// 각 변환 함수는 Result를 받아 Result를 반환
function findMatchingLines(
  result: Result<string>,
  pattern: RegExp
): Result<string[]> {
  if (!result.ok) return result;
  const lines = result.value
    .split("\n")
    .filter((line) => pattern.test(line));
  return lines.length > 0 ? ok(lines) : err("nothing found");
}

function truncateLines(result: Result<string[]>): Result<string[]> {
  if (!result.ok) return result;
  return ok(result.value.map((line) => line.slice(0, 20)));
}
  • 방법 1: 각 변환 내부에서 처리 — 각 함수가 Result 타입을 검사해서 에러면 그대로 전달
  • 방법 2: 파이프라인에서 처리 (bind/andThen 패턴)andThen 함수가 Result를 언래핑하고, 에러면 다음 단계를 건너뜀 (Monad 패턴)
function andThen<T, U>(
  result: Result<T>,
  fn: (value: T) => Result<U>
): Result<U> {
  if (!result.ok) return result;
  return fn(result.value);
}

Topic 31: Inheritance Tax (상속세)

상속의 역사와 두 가지 동기

  • Simula-67 (1969): 타입 결합 목적 — link 접두 클래스로 Car와 Bicycle을 같은 리스트에 넣음. “is-a” 관계
  • Smalltalk: 동적 행동 공유 목적 — “이것은 저것과 같은데 다른 점은…” 이라는 차이 프로그래밍(differential programming)

오늘날 개발자들이 상속을 쓰는 두 가지 이유:

  1. 타이핑을 줄이려고: 기본 클래스의 공통 기능을 자식 클래스에서 재사용
  2. 타입 계층을 표현하려고: `Car is-a-kind-of Vehicle

두 가지 모두 문제가 있다.

코드 공유를 위한 상속의 문제

  • 자식 클래스는 부모, 조부모 등 모든 조상과 커플링 된다
  • 자식 클래스를 사용하는 코드도 모든 조상과 커플링 된다
class Vehicle {
  protected speed = 0;
  stop() { this.speed = 0; }
  moveAt(speed: number) { this.speed = speed; }
}

class Car extends Vehicle {
  info() { return `I'm a car driving at ${this.speed}`; }
}

// 클라이언트 코드
const myCar = new Car();
myCar.moveAt(30); // Vehicle의 메서드를 호출
  • VehiclemoveAtsetVelocity로 바꾸면 클라이언트 코드(myCar.moveAt)가 깨진다
  • 클라이언트는 Car를 쓴다고 생각했는데, Vehicle 내부 구현이 바뀌어 조용히 깨진다

타입 계층으로서 상속의 문제

  • 클래스 계층도가 벽면을 덮을 만큼 거대해진다
  • 작은 차이를 표현하기 위해 계층이 깊어지고, 변경이 위아래로 물결치며 퍼진다 (brittle)
  • CarVehicle이지만 동시에 Asset, InsuredItem, LoanCollateral이기도 하다 — 다중 상속 문제
  • C++이 다중 상속을 악명 높게 만들었고, 많은 언어가 아예 지원하지 않는다

Tip 51: Don’t Pay Inheritance Tax

더 나은 대안 3가지

1. 인터페이스와 프로토콜 (Interfaces & Protocols)

interface Drivable {
  getSpeed(): number;
  stop(): void;
}

interface Locatable {
  getLocation(): Coordinate;
  locationIsValid(): boolean;
}

class Car implements Drivable, Locatable {
  getSpeed() { return this.speed; }
  stop() { this.speed = 0; }
  getLocation() { return this.position; }
  locationIsValid() { return true; }
  private speed = 0;
  private position: Coordinate = { lat: 0, lng: 0 };
}

// 다형성: Car와 Phone이 모두 Locatable을 구현하면 같은 배열에 담을 수 있다
const locatableItems: Locatable[] = [new Car(), new Phone()];

Tip 52: Prefer Interfaces to Express Polymorphism
인터페이스/프로토콜은 상속 없이 다형성을 제공한다.

2. 위임 (Delegation)

  • 상속하면 부모의 메서드 20개가 자식에 노출되어 인터페이스 통제력을 잃는다
  • 위임을 사용하면 필요한 메서드만 공개하고, 프레임워크 API를 우회할 수 없다
// BAD: 상속으로 프레임워크 API 전체가 Account에 노출됨
class Account extends PersistenceBaseClass {}

// GOOD: 위임으로 필요한 것만 공개
class Account {
  private repo: Persister;

  constructor() {
    this.repo = Persister.for(this);
  }

  save() {
    this.repo.save();
  }
  // PersistenceBaseClass의 다른 메서드는 외부에 노출되지 않음
}

Tip 53: Delegate to Services: Has-A Trumps Is-A
심지어 AccountRecord를 분리해서 Account는 비즈니스 로직만, AccountRecord는 영속성만 담당하게 할 수도 있다.

3. 믹스인과 트레이트 (Mixins & Traits)

  • 목적: 상속 없이 클래스에 새로운 기능을 추가
  • 기능의 집합에 이름을 붙여 기존 클래스와 합성(mixin)
// 믹스인 예시 (TypeScript의 패턴)
type Constructor<T = {}> = new (...args: any[]) => T;

function CommonFinders<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    find(id: string) { /* ... */ }
    findAll() { /* ... */ }
  };
}

function Validatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    validate() { /* ... */ }
  };
}

class AccountRecord extends CommonFinders(Validatable(BasicRecord)) {}
class OrderRecord extends CommonFinders(BasicRecord) {}
  • 상황별 검증 믹스인: AccountForCustomer는 고객용 검증 포함, AccountForAdmin은 관리자용 검증 포함 — 상황에 따른 클래스를 생성

Tip 54: Use Mixins to Share Functionality

세 가지 대안 비교:

기법 목적 언제 사용
인터페이스/프로토콜 타입 정보 공유, 다형성 공통 타입 계약이 필요할 때
위임 기능 재사용, API 제어 프레임워크를 래핑하거나 Has-A 관계일 때
믹스인/트레이트 메서드 공유, 기능 합성 여러 클래스에 동일한 기능을 추가할 때

Topic 32: Configuration (외부 설정)

설정을 외부화해야 하는 이유

  • 애플리케이션이 라이브 상태에서 변경될 수 있는 값, 환경/고객별로 다른 값은 앱 외부에 두어야 한다
  • 코드가 “파라미터화”되어 실행되는 환경에 맞게 적응할 수 있다

Tip 55: Parameterize Your App Using External Configuration

설정으로 분리해야 할 것들:

  • 외부 서비스 자격증명 (DB, 3rd party API 키)
  • 로그 레벨 및 목적지
  • 포트, IP 주소, 머신/클러스터 이름
  • 환경별 검증 파라미터
  • 세금율 같은 외부에서 설정되는 파라미터
  • 사이트별 포맷, 라이선스 키

Static Configuration (정적 설정)

  • YAML, JSON 파일 또는 DB 테이블에 설정 저장
  • 앱 시작 시 데이터 구조로 읽어들임
  • 주의: 이 데이터 구조를 전역으로 만들지 말 것 — 대신 얇은(thin) API 뒤에 감싸서 설정 표현 방식과 코드를 분리
// BAD: 설정을 전역으로 노출
const config = JSON.parse(fs.readFileSync("config.json", "utf-8"));
// 어디서나 config.db.host로 직접 접근

// GOOD: 얇은 API 뒤에 감쌈
class AppConfig {
  private static data = JSON.parse(fs.readFileSync("config.json", "utf-8"));

  static getDatabaseHost(): string {
    return this.data.db.host;
  }

  static getLogLevel(): string {
    return this.data.logging.level;
  }
}

Configuration-as-a-Service (서비스형 설정)

저자들이 더 선호하는 방식. 설정 데이터를 서비스 API 뒤에 두는 것:

  • 여러 애플리케이션이 인증/접근제어와 함께 설정을 공유 가능
  • 설정 변경을 전역적으로 즉시 적용 가능
  • 특화된 UI로 설정 관리 가능
  • 가장 중요: 설정이 동적으로 변함 — 앱을 재시작하지 않아도 설정 변경 반영 가능
  • 컴포넌트가 설정 변경 이벤트를 구독(subscribe)하고 실시간으로 새 값을 받을 수 있음

주의사항: 설정을 과도하게 사용하지 말 것

  • 과도한 설정의 위험: 한 클라이언트는 모든 필드를 설정 가능하게 만들었다가 설정 변수 4만 개와 함께 악몽에 빠짐
  • 게으름으로 설정을 남용하지 말 것: 기능이 이렇게 동작해야 할지 저렇게 동작해야 할지 논쟁이 있다고 해서 설정으로 미루지 말 것 — 한 가지를 시도하고 피드백을 받아라

Chapter 6: Concurrency (동시성)

핵심 정의:

  • 동시성(Concurrency): 두 개 이상의 코드가 동시에 실행되는 것처럼 보이는 것 — 소프트웨어 메커니즘 (파이버, 스레드, 프로세스로 구현)
  • 병렬성(Parallelism): 실제로 동시에 실행되는 것 — 하드웨어 관심사 (멀티 코어, 멀티 CPU)

현실 세계는 비동기다. 사용자 인터랙션, 데이터 fetch, 외부 서비스 호출이 모두 동시에 일어난다. 직렬(serial) 처리를 강제하면 시스템이 느리고 하드웨어를 낭비한다.

Topic 33: Breaking Temporal Coupling (시간적 커플링 끊기)

시간적 커플링이란

  • 시간적 커플링(Temporal Coupling): 코드가 해결해야 할 문제와 무관하게 실행 순서를 강제하는 것
  • “Method A는 반드시 Method B 전에 호출되어야 한다”, “보고서는 한 번에 하나씩만 실행된다” 등
  • 유연하지 않고 현실적이지 않다

동시성 분석: 액티비티 다이어그램

Tip 56: Analyze Workflow to Improve Concurrency

  • 액티비티 다이어그램(Activity Diagram): 어떤 작업이 동시에 일어날 수 있고, 어떤 것은 반드시 순서가 있어야 하는지 시각화
  • 동기화 바(Synchronization Bar): 두꺼운 선으로 표현 — 이 선으로 들어오는 모든 작업이 완료되어야 다음으로 진행 가능
flowchart TD
    Start([Start]) --> A[Open blender]
    Start --> B[Open piña colada mix]
    Start --> D[Measure 1/2 cup rum]
    Start --> J[Get glasses]
    Start --> K[Get pink umbrellas]

    B --> C[Put mix in blender]
    D --> E[Pour in rum]

    A --> SYNC1[===]
    C --> SYNC1
    E --> SYNC1

    SYNC1 --> F[Add 2 cups of ice]
    F --> G[Close blender]
    G --> H[Liquefy 1 minute]

    H --> SYNC2[===]
    J --> SYNC2
    K --> SYNC2

    SYNC2 --> L[Serve]
    L --> End([Done])
  • 이 분석을 통해 1, 2, 4, 10, 11번 작업은 처음에 동시에 진행 가능하다는 것을 발견
  • Liquify(1분 소요) 동안 잔 가져오기, 우산 가져오기를 병렬 처리 가능

동시성과 병렬성 기회 구분

  • 동시성 기회: 코드가 아닌 시간이 걸리는 작업 — DB 쿼리, 외부 서비스 호출, 사용자 입력 대기 등. CPU 대신 기다리는 시간 동안 다른 작업 수행
  • 병렬성 기회: 서로 독립적인 작업들 — 큰 작업을 독립 청크로 나누고, 각각 병렬 처리 후 결과 합산. (예: Elixir 컴파일러는 모듈을 병렬로 컴파일)

Topic 34: Shared State Is Incorrect State (공유 상태는 잘못된 상태)

문제의 본질: 비원자적 업데이트

웨이터 1: 파이 남은 개수 확인 → 1개 있음 → 손님에게 약속
웨이터 2: 파이 남은 개수 확인 → 1개 있음 → 손님에게 약속  ← 동시 실행!
웨이터 1: 파이 가져감
웨이터 2: 파이 가져가려 했지만 없음 → 에러
  • 문제는 두 프로세스가 같은 메모리에 쓴다는 것이 아님
  • 메모리 뷰의 일관성을 보장할 수 없다는 것이 진짜 문제
  • “확인”과 “업데이트”가 원자적(atomic) 연산이 아니기 때문

Tip 57: Shared State Is Incorrect State

해결책 1: 세마포어(Semaphore)

class Semaphore {
  private locked = false;
  private queue: Array<() => void> = [];

  async lock(): Promise<void> {
    if (!this.locked) {
      this.locked = true;
      return;
    }
    return new Promise((resolve) => this.queue.push(resolve));
  }

  unlock(): void {
    const next = this.queue.shift();
    if (next) {
      next();
    } else {
      this.locked = false;
    }
  }
}

const caseSemaphore = new Semaphore();

async function getPieIfAvailable(): Promise<string | null> {
  await caseSemaphore.lock();
  try {
    if (pieCount > 0) {
      pieCount--;
      return "apple pie";
    }
    return null;
  } finally {
    caseSemaphore.unlock(); // 예외 발생해도 반드시 해제
  }
}
  • 세마포어의 문제: 모든 개발자가 세마포어 사용 규약을 지켜야 한다 — 한 명이라도 빠뜨리면 다시 혼란

해결책 2: 리소스를 트랜잭셔널하게 만들기

  • 확인과 취득을 단일 호출로 합치는 것 이 핵심
class DisplayCase {
  private slices: string[] = ["apple", "peach", "cherry"];
  private semaphore = new Semaphore();

  async getPieIfAvailable(): Promise<string | null> {
    await this.semaphore.lock();
    try {
      if (this.slices.length > 0) {
        this.updateSalesData("pie");
        return this.slices.shift()!;
      }
      return null;
    } finally {
      this.semaphore.unlock();
    }
  }
}

// 클라이언트는 이제 단순히:
const slice = await displayCase.getPieIfAvailable();
if (slice) giveToCustomer(slice);

Tip 58: Random Failures Are Often Concurrency Issues
재현하기 어렵고 무작위로 발생하는 실패는 동시성 문제일 가능성이 높다.

근본적 결론

  • 세마포어와 뮤텍스(mutex)를 직접 관리하는 것은 근본적으로 에러 발생 가능성이 높다
  • Rust: 언어 레벨에서 데이터 소유권(ownership)을 강제해 하나의 가변 참조만 허용
  • 함수형 언어: 불변(immutable) 데이터로 동시성을 단순화

“Doctor, it hurts when I do this.” “Then don’t do that.”
공유 자원 환경에서 동시성을 직접 관리하는 것은 어렵다. 다음 토픽들은 그 고통 없이 동시성의 이점을 얻는 방법을 다룬다.

Topic 35: Actors and Processes (액터와 프로세스)

액터 모델 정의

  • 액터(Actor): 자체 로컬 상태(private)를 가진 독립적인 가상 프로세서. 메일박스를 가짐
  • 메일박스에 메시지가 도착하면 깨어나 처리 → 완료 후 다음 메시지 처리 or 다시 수면
  • 메시지 처리 중 액터는: 다른 액터를 생성하거나, 알고 있는 다른 액터에게 메시지를 보내거나, 새로운 상태를 정의 가능
  • 프로세스(Process): OS가 구현하는 더 범용적인 가상 프로세서. 액터처럼 동작하도록 제약 가능

액터의 핵심 특성

  • 중앙 제어 없음: 무엇이 다음에 실행될지 관리하거나 조율하는 단일 주체가 없다
  • 상태 격리: 시스템의 모든 상태는 메시지와 각 액터의 로컬 상태에만 존재. 외부에서 접근 불가
  • 단방향 메시지: 응답 개념이 없다. 응답을 원하면 자신의 메일박스 주소를 메시지에 포함시켜야 한다
  • 메시지를 하나씩 처리: 한 번에 하나의 메시지만 완전히 처리

결과: 액터는 동시적으로, 비동기적으로 실행되며, 아무것도 공유하지 않는다.

Tip 59: Use Actors For Concurrency Without Shared State

액터 구현 예시 (JavaScript/Nact 기반)

세 액터: 고객(Customer), 웨이터(Waiter), 파이 케이스(PieCase)

// 고객 액터
const customerActor = {
  "hungry for pie": (msg: any, ctx: any, state: any) => {
    return dispatch(state.waiter, {
      type: "order",
      customer: ctx.self,
      wants: "pie",
    });
  },
  "put on table": (msg: any, ctx: any) => {
    console.log(`${ctx.self.name} sees "${msg.food}" appear on the table`);
  },
  "no pie left": (msg: any, ctx: any) => {
    console.log(`${ctx.self.name} sulks...`);
  },
};

// 웨이터 액터
const waiterActor = {
  order: (msg: any, ctx: any, state: any) => {
    if (msg.wants === "pie") {
      dispatch(state.pieCase, {
        type: "get slice",
        customer: msg.customer,
        waiter: ctx.self,
      });
    }
  },
  "add to order": (msg: any) => {
    console.log(`Waiter adds ${msg.food} to ${msg.customer.name}'s order`);
  },
  error: (msg: any) => {
    dispatch(msg.customer, { type: "no pie left", msg: msg.msg });
    console.log(`Waiter apologizes to ${msg.customer.name}: ${msg.msg}`);
  },
};

// 파이 케이스 액터
const pieCaseActor = {
  "get slice": (msg: any, ctx: any, state: any) => {
    if (state.slices.length === 0) {
      dispatch(msg.waiter, {
        type: "error",
        msg: "no pie left",
        customer: msg.customer,
      });
      return state;
    } else {
      const slice = state.slices.shift() + " pie slice";
      dispatch(msg.customer, { type: "put on table", food: slice });
      dispatch(msg.waiter, {
        type: "add to order",
        food: slice,
        customer: msg.customer,
      });
      return state;
    }
  },
};

실행 흐름 다이어그램:

sequenceDiagram
    participant External as 외부(God)
    participant C1 as Customer 1
    participant W as Waiter
    participant PC as PieCase

    External->>C1: hungry for pie
    C1->>W: order {wants: pie}
    W->>PC: get slice {customer: C1}
    PC->>C1: put on table {food: apple pie}
    PC->>W: add to order {food: apple pie}

액터 모델의 이점

  • 명시적 동시성 코드 불필요: 공유 상태가 없으므로 락이나 세마포어 코드가 없다
  • 명시적 엔드투엔드 로직 불필요: 액터들이 메시지에 기반해 스스로 작동
  • 아키텍처 독립적: 단일 CPU, 멀티 코어, 멀티 머신에서 동일한 코드로 동작

Erlang과 액터

  • Erlang은 원래 Actors 논문을 읽지 않고 독자적으로 액터 모델을 구현한 대표 사례
  • Erlang은 액터를 프로세스라 부르며, OS 프로세스가 아닌 경량(lightweight) 가상 프로세스 — 단일 머신에서 수백만 개 실행 가능
  • 슈퍼비전 시스템: 프로세스 수명을 관리하고 실패 시 재시작
  • 핫 코드 로딩: 실행 중인 시스템의 코드를 교체 가능
  • Erlang의 프로그래니 Elixir도 이 모델을 따름

Topic 36: Blackboards (블랙보드)

블랙보드 개념

  • 탐정 비유: 형사들이 칠판에 증거, 목격자 진술, 법의학 증거, 연결 고리를 붙여가며 협력해 사건을 해결
  • 각 형사는 다른 형사의 존재를 알 필요 없음. 칠판에 새 정보가 생기면 반응
  • 다른 분야, 다른 교육 배경, 다른 근무 시간대의 형사들이 협력 가능

블랙보드 시스템의 핵심 특성:

  • 어떤 에이전트도 다른 에이전트의 존재를 알 필요 없다
  • 에이전트들이 서로 다른 전문성을 가질 수 있다
  • 에이전트가 실행 도중 추가/교체 가능하다
  • 칠판에 올라오는 데이터 형식에 제한이 없다

실제 적용: 대출 신청 처리 시스템

복잡한 규제 환경에서의 문제:

  • 응답이 어떤 순서로든 도착 가능 (신용 조회는 오래 걸리지만 이름/주소는 즉시 가능)
  • 데이터 수집이 여러 지역, 다른 사무소, 다른 시간대에 걸쳐 진행
  • 어떤 데이터는 다른 데이터에 의존 (소유권 증명 없이 자동차 명의 조회 불가)
  • 새 데이터가 새로운 요건을 촉발 (신용 점수 낮으면 추가 서류 5개 필요)

블랙보드 + 규칙 엔진의 장점:

  • 데이터 도착 순서가 무관함
  • 새 사실이 게시되면 관련 규칙이 자동으로 트리거됨
  • 규제 변경 시 규칙 엔진만 업데이트하면 됨

Tip 60: Use Blackboards to Coordinate Workflow

현대의 메시징 시스템과 블랙보드

  • Kafka, NATS 같은 메시징 시스템은 블랙보드와 유사한 특성을 가짐
    • 영속성: 이벤트 로그 형태로 메시지 저장
    • 패턴 매칭: 특정 패턴으로 메시지 검색 가능
  • 메시징 시스템 위에서 액터 모델도 구현 가능

액터/블랙보드/마이크로서비스의 트레이드오프

장점:

  • 전체 동시성 문제 클래스를 제거

단점:

  • 동작이 간접적이라 추론하기 어려움
  • 메시지 형식/API 중앙 저장소 필요
  • 메시지와 사실이 시스템을 통과하는 것을 추적할 수 있는 좋은 툴링 필요
    • 유용한 기법: 비즈니스 기능이 시작될 때 고유 추적 ID를 붙이고 모든 관련 액터에 전파. 로그에서 전체 흐름 재구성 가능
  • 더 많은 동적 파트 → 배포와 관리가 더 복잡
    • 그러나 개별 액터만 교체 가능하다는 점이 이를 일부 상쇄

Chapter 7 — While You Are Coding

코딩은 기계적인 작업이 아니다. 매 순간 신중한 판단이 필요한 의사결정의 연속이며, 이 챕터는 코딩 중에 실천해야 할 핵심 사고방식과 기술을 다룬다.

Topic 37 — Listen to Your Lizard Brain

본능이란 무엇인가

  • 본능(instinct) = 비의식적 뇌(nonconscious brain)에 축적된 패턴 반응
  • 프로그래머로 경험이 쌓일수록, 뇌에는 암묵적 지식(tacit knowledge)이 레이어처럼 쌓인다
    • “이건 작동한다”, “이건 작동하지 않는다”, “이 오류의 원인은 무엇이다”
  • 본능은 언어가 없다 — 생각이 아니라 감정으로 온다
    • 불안함, 메스꺼움, “이건 너무 복잡하다”는 느낌 등으로 신호가 온다

두 가지 상황: 본능이 말하는 때

1. 빈 화면의 공포 (Fear of the Blank Page)

  • 새 프로젝트 또는 새 모듈을 시작할 때 느끼는 막막함
  • 원인 1: 아직 표면에 드러나지 않은 의심(doubt)이 있다 — 그 의심은 중요한 신호다
  • 원인 2: 실수에 대한 두려움 — 개발자는 자신의 코드에 자신을 많이 투영한다

2. 자신과의 싸움 (Fighting Yourself)

  • 어떤 날은 코드가 흘러나오지만, 어떤 날은 진흙 속을 걷는 것처럼 힘들다
  • “전문가니까 버텨야지”라는 생각은 오히려 역효과
  • 코드 자체가 “이건 필요 이상으로 어렵다”고 말하고 있는 것이다

본능과 대화하는 방법

Tip 61 — Listen to Your Inner Lizard

  • Step 1: 멈춰라. 코드에서 잠시 떠나 마음을 비운다 — 산책, 점심, 수다, 수면
  • Step 2: 외부화하라. 동료에게 설명하거나, 러버덕(rubber duck)에게 설명한다
    • 설명하는 도중 “아, 그렇구나!” 하는 순간이 온다
  • Step 3: 프로토타이핑으로 뇌를 속여라
    1. 포스트잇에 “I’m prototyping”이라 쓰고 모니터 옆에 붙인다
    2. 프로토타입은 실패해도 되고, 버려도 된다는 것을 상기한다
    3. 빈 에디터에 “무엇을 배우거나 확인하고 싶은지” 한 문장 주석으로 적는다
    4. 코딩을 시작한다
    • 도중에 막연한 의심이 구체적인 우려로 결정화(crystallize)되면, 그 시점에 대응한다
    • 프로토타입이 끝나면 모두 삭제하고 진짜 코드를 새로 작성한다

다른 사람의 코드에서도 적용

  • 낯선 방식으로 작성된 코드를 발견하면 메모해 두고 패턴을 찾아라
  • 작성자의 암묵적 패턴을 이해하면 코드 해독이 훨씬 쉬워진다

코드 너머에서도 적용

  • 설계나 요구사항이 “뭔가 이상하다”는 느낌이 들면 무시하지 말고 탐색하라
  • 지지적인 환경이라면 그 느낌을 소리 내어 표현하라 — 어두운 문간에 무언가 숨어있을 수 있다

Topic 38 — Programming by Coincidence

우연에 의한 프로그래밍이란

  • 코드가 작동하는 이유를 모르면서 “일단 돌아가니 됐다”고 생각하는 것
  • 지뢰밭을 총검으로 찌르며 지나가는 병사의 비유: 지뢰가 없는 것이 아니라 운 좋게 안 터진 것뿐이다

우연 프로그래밍의 유형

구현 우연 (Accidents of Implementation)

  • 현재 코드가 그렇게 작성되어 있어서 우연히 작동하는 것에 의존
  • 문서화되지 않은 에러/경계 조건에 의존
  • 예: GUI 렌더링 프레임워크에서 설계되지 않은 방식으로 함수들을 연속 호출
// 이렇게 설계되지 않은 순서로 호출하는 것은 우연적 작동 paint(); invalidate(); validate(); revalidate(); repaint(); paintImmediately();
  • 우연에 의존하는 코드의 문제:
    • 실제로 작동하는 게 아닐 수 있다 — 작동하는 것처럼 보일 뿐
    • 경계 조건이 환경(화면 해상도, CPU 코어 수)에 따라 달라질 수 있다
    • 라이브러리 다음 버전에서 문서화되지 않은 동작이 바뀔 수 있다
    • 불필요한 호출은 코드를 느리게 하고 새 버그를 도입한다

근사치로 충분 (Close Enough Isn’t)

  • 시간대 처리 오류 사례: 결과가 항상 1 차이가 났고, 개발자들은 그냥 +1/-1을 추가했다
  • 근본적인 시간 처리 모델의 부재로 전체 코드베이스가 수정 불가능한 상태가 되었다

환상적 패턴 (Phantom Patterns)

  • 인간은 패턴과 인과관계를 보도록 설계되어 있다 — 통계적으로 독립된 사건에서도
  • 로그에서 1,000 요청마다 나타나는 에러: 레이스 컨디션일 수도, 단순 버그일 수도
  • “가정하지 말고 증명하라”

컨텍스트 우연 (Accidents of Context)

  • 현재 GUI 환경을 위해 코딩 중이라고 해서 모듈이 GUI에 의존해야 하는 건 아니다
  • 영어 사용자, 리터러시 사용자, 특정 환경 변수, 쓰기 가능한 디렉토리, 네트워크 가용성 등을 당연시하는 것
  • 인터넷 첫 번째 답변을 복붙했을 때: 그 답변의 컨텍스트와 내 컨텍스트가 같은가?

암묵적 가정 (Implicit Assumptions)

  • 모든 레벨에서 사람들은 가정을 갖고 작업하지만, 이 가정들은 문서화되지 않고 개발자 간에 충돌한다
  • 잘 확립된 사실에 기반하지 않은 가정은 모든 프로젝트의 골칫거리

Tip 62 — Don’t Program by Coincidence

의도적으로 프로그래밍하는 법

  • 항상 자신이 무엇을 하고 있는지 인지한다 — 주니어에게 코드를 세부적으로 설명할 수 없다면 우연에 의존하고 있는 것
  • 어둠 속에서 코딩하지 않는다 — 완전히 이해하지 못한 기술을 사용하면 우연에 물린다
  • 계획에서 진행한다 — 머릿속에만 있더라도
  • 신뢰할 수 있는 것에만 의존한다 — 불확실하면 최악을 가정한다
  • 가정을 문서화한다 — Design by Contract는 가정을 명확히 하는 데 도움이 된다
  • 코드만 테스트하지 말고 가정도 테스트한다 — 단언(assertion)으로 가정을 검증하고, 틀렸을 때 운이 좋다고 생각하라
  • 노력의 우선순위를 정한다 — 중요하고 어려운 부분에 시간을 쓴다
  • 역사의 노예가 되지 않는다 — 기존 코드가 미래 코드를 지배하도록 두지 않는다. 리팩터링을 두려워하지 않는다

Topic 39 — Algorithm Speed

Big-O 표기법이란

  • 알고리즘이 사용하는 자원(시간, 메모리 등)이 입력 크기에 따라 어떻게 변하는지 수학적으로 표현
  • O(n)O(n)O(n)은 “n의 크기 정도”를 의미 — 상수 배수와 저차항은 무시
  • Big-O는 상한(upper bound)을 표현한다 — 실제 수치가 아닌 변화 방식을 말해준다
  • 주의: O(n2)O(n^2)O(n2) 알고리즘 A가 다른 O(n2)O(n^2)O(n2) 알고리즘 B보다 1,000배 빠를 수 있지만 Big-O로는 알 수 없다

알고리즘 복잡도 종류

표기법 이름 예시
$O(1)$ 상수(Constant) 배열 원소 접근, 단순 구문
$O(\log n)$ 로그(Logarithmic) 이진 탐색
$O(n)$ 선형(Linear) 순차 탐색
$O(n \log n)$ 선형-로그 퀵소트/힙소트 평균
$O(n^2)$ 제곱(Square) 선택 정렬, 삽입 정렬
$O(n^3)$ 세제곱(Cubic) $n \times n$ 행렬 곱셈
$O(2^n)$ 지수(Exponential) 외판원 문제, 집합 분할

100개 레코드를 1초에 처리하는 코드가 1,000개 레코드를 처리하면?

복잡도 소요 시간
$O(1)$ 1초
$O(\log n)$ 약 3초
$O(n)$ 10초
$O(n \log n)$ 약 33초
$O(n^2)$ 100초
$O(2^n)$ 우주가 끝날 때까지 기다려야 함

상식적 추정 방법

  • 단순 루프: O(n)O(n) — 순차 탐색, 최댓값 찾기, 체크섬 생성
  • 중첩 루프: O(n2) — 버블 정렬처럼 외부 루프가 각 원소를, 내부 루프가 배치를 결정
  • 이진 분할(Binary Chop): O(log⁡n) — 정렬된 리스트의 이진 탐색, 이진 트리 순회
  • 분할 정복(Divide and Conquer): O(nlog⁡n) — 퀵소트(단, 정렬된 입력에서는 성능 저하)
  • 조합(Combinatoric): O(n!)이상 — 순열이 관련된 알고리즘, 외판원 문제

Tip 63 — Estimate the Order of Your Algorithms

Tip 64 — Test Your Estimates

실전에서의 알고리즘 속도

  • 루프를 작성할 때마다 잠재적 복잡도를 자동으로 확인하는 습관을 들여라
  • 불확실하면 실제로 실행해보고 입력 크기를 변화시키며 결과를 플롯하라
  • 이론적 추정과 실제 환경(메모리 스래싱, 정렬된 입력 등)의 차이를 모두 확인하라
  • 가장 빠른 것이 항상 최선은 아니다: 작은 입력셋에는 단순한 삽입 정렬이 퀵소트만큼 빠르고 구현도 쉽다
  • 조기 최적화(premature optimization)를 경계하라: 실제 병목인지 확인한 후에 개선하라

Topic 40 — Refactoring

소프트웨어는 정원이다

  • 소프트웨어 개발의 주된 메타포는 “건축”이지만, 이는 잘못된 비유
  • 실제로는 정원 가꾸기에 더 가깝다 — 유기적이고, 살아있고, 지속적인 관리가 필요하다
    • 처음 계획대로 자라는 것도 있고, 퇴비가 되어야 할 것도 있다
    • 과성장한 식물은 분리하거나 가지치기하고, 어울리지 않는 것은 옮긴다

리팩터링의 정의 (Martin Fowler)

“기존 코드 구조를 변경하되, 외부 동작은 변경하지 않는 규율 있는 기술”

핵심 조건:

  1. 규율 있는 활동 — 아무렇게나 재작성하는 것이 아님
  2. 외부 동작은 변하지 않는다 — 기능 추가와 리팩터링을 동시에 하지 않는다

언제 리팩터링해야 하는가

다음 상황 중 하나라도 해당되면 리팩터링한다:

  • 중복(Duplication): DRY 원칙 위반을 발견했을 때
  • 비직교적 설계(Nonorthogonal Design): 더 직교적으로 만들 수 있는 부분을 발견했을 때
  • 오래된 지식(Outdated Knowledge): 요구사항이 바뀌었거나, 문제에 대한 이해가 깊어졌을 때
  • 사용 패턴 변화(Usage): 실제 사용자가 사용하면서 중요도가 달라졌을 때
  • 성능(Performance): 기능을 다른 위치로 옮겨야 성능이 개선될 때
  • 테스트가 통과했을 때: 새 테스트가 통과된 직후가 정리의 황금 기회

리팩터링을 미루면 생기는 일

  • “지금 시간이 없어서”는 리팩터링을 안 하는 변명으로 통하지 않는다
  • 지금 피하면 나중에 더 많은 의존성과 함께 더 비싼 비용을 지불한다
  • 의료 비유: 지금 잘라내면 작은 수술이지만, 방치하면 더 크고 위험한 수술이 필요하다

Tip 65 — Refactor Early, Refactor Often

리팩터링 방법

Martin Fowler의 안전한 리팩터링 원칙:

  1. 리팩터링과 기능 추가를 동시에 하지 않는다
  2. 리팩터링 전에 좋은 테스트를 먼저 확보한다 — 테스트를 자주 실행하여 변경으로 무언가 깨졌는지 빠르게 확인
  3. 작고 신중한 단계로 진행한다 — 필드 이동, 메서드 분리, 변수 이름 변경 등
    • 작은 단계 + 각 단계 후 테스트 = 긴 디버깅 없이 진행 가능

자동화된 리팩터링(Automatic Refactoring):

  • 현대 IDE는 변수/메서드 이름 변경, 긴 함수 분리, 코드 이동 등을 자동으로 지원한다
  • 외부 동작이나 인터페이스를 변경해야 한다면 의도적으로 빌드를 깨뜨려라 — 수정이 필요한 클라이언트 코드가 컴파일 에러로 드러난다

Topic 41 — Test to Code

테스트의 진짜 목적

Tip 66 — Testing Is Not About Finding Bugs

  • 테스트의 주요 이점은 테스트를 실행할 때가 아니라 생각하고 작성할 때 발생한다
  • 테스트는 코드에 대한 피드백을 얻는 행위 — 설계, API, 결합도 등을 드러낸다

테스트가 코딩을 이끈다

테스트 먼저 생각하면 API 설계가 자연스럽게 개선된다는 예시:

// 처음: 전역 DB를 사용하는 함수
function returnAvidUsers() {
  // 테스트하기 어렵다 — DB를 교체할 수 없다
}

// 테스트를 생각하며 개선: DB를 주입받는다
function returnAvidUsers(db: Database) {
  // 테스트 시 목(mock) DB를 주입할 수 있다
}

// 더 개선: 어떤 필드를 기준으로 볼지도 파라미터화
function returnAvidUsers(db: Database, qualifyingFieldName: string) {
  // 유연성이 높아졌다
}
  • 코드 한 줄 작성 전에 이미 두 가지 개선을 이끌어냈다

Tip 67 — A Test Is the First User of Your Code

  • 테스트는 코드의 첫 번째 사용자다 — 외부 사용자 관점에서 API를 바라보게 된다
  • 다른 코드와 강하게 결합된 함수는 테스트하기 어렵다 → 테스트하기 쉽게 만들면 결합도가 줄어든다
  • 테스트를 먼저 생각하면 경계 조건과 에러 처리를 미리 구조화하게 된다

TDD (Test-Driven Development)

TDD의 기본 사이클:

  1. 추가할 작은 기능을 결정한다
  2. 그 기능이 구현되면 통과할 테스트를 작성한다
  3. 모든 테스트를 실행 — 방금 작성한 테스트만 실패하는지 확인한다
  4. 테스트를 통과하는 데 필요한 최소한의 코드를 작성한다
  5. 코드를 리팩터링한다 — 테스트가 여전히 통과하는지 확인

TDD의 함정:

  • 100% 테스트 커버리지 집착 → 중요하지 않은 곳에 과도한 시간 낭비
  • 불필요한 중복 테스트 발생
  • 전체 그림보다 세부사항에 집착하게 됨

론 제프리스 vs 피터 노르빅의 스도쿠 풀기 비교:

  • 제프리스: TDD로 보드 표현 방식을 여러 번 리팩터링했지만 결국 포기
  • 노르빅: 제약 전파(constraint propagation) 기반으로 알고리즘을 먼저 이해하고 단 12줄의 코드로 해결
  • 교훈: 목적지 없이 달리면 원을 그리게 된다

Tip 68 — Build End-to-End, Not Top-Down or Bottom Up

  • 소프트웨어는 작은 단위의 엔드-투-엔드 기능을 점진적으로 쌓아야 한다
  • 탑다운(전체 요구사항을 처음부터 표현할 수 없음)도 바텀업(최종 목적지를 모르는 상태에서 추상화 레이어를 쌓을 수 없음)도 단독으로는 작동하지 않는다

단위 테스트와 계약 테스트

  • 단위 테스트 = 모듈을 격리된 환경에서 테스트
  • 계약에 맞게 테스트(Testing Against Contract):
    • 주어진 단위가 계약(contract)을 준수하는지 확인
    • 계약이 우리가 생각하는 의미인지도 확인
// 제곱근 함수의 계약 테스트 예시
// 사전 조건: argument >= 0
// 사후 조건: |result * result - argument| <= epsilon * argument

assertWithinEpsilon(mySqrt(0), 0);
assertWithinEpsilon(mySqrt(2.0), 1.4142135624);
assertWithinEpsilon(mySqrt(64.0), 8.0);
assertRaisesException(() => mySqrt(-4.0));
  • 모듈 A가 DataFeed와 LinearRegression에 의존한다면:
    1. DataFeed의 계약을 먼저 완전히 테스트
    2. LinearRegression의 계약을 완전히 테스트
    3. A의 계약을 테스트 (서브컴포넌트 계약에 의존하되 직접 노출하지 않음)

Tip 69 — Design to Test

애드혹 테스트와 테스트 창구

  • 애드혹 테스트: console.log(), REPL 입력 등 — 디버깅 세션이 끝난 후 반드시 공식 단위 테스트로 등록하라
  • 테스트 창구(Test Window): 프로덕션 환경에서 모듈 내부 상태를 볼 수 있는 방법
    • 일정한 형식의 로그 메시지 (자동 파싱 가능하게)
    • 특정 URL이나 키 조합으로 나타나는 진단 창
    • 특정 사용자 클래스에 대해 추가 진단을 활성화하는 기능 스위치

테스트 문화

테스트 시점에 따른 선택:

방식 설명
Test First TDD 포함, 가장 좋은 선택 — 테스트가 반드시 이루어짐을 보장
Test During 코드 작성 후 곧바로 테스트 작성 — 괜찮은 차선책
Test Never “나중에 테스트”라고 말하는 것 — 사실상 절대 안 함
  • “항상 실패하는” 테스트를 무시하는 습관은 모든 테스트를 무시하는 악순환으로 이어진다
  • 테스트 코드도 프로덕션 코드와 동일한 수준으로 관리하라 — 결합도를 낮추고, 깨끗하고, 견고하게
  • GUI 위젯의 절대 위치, 서버 로그의 정확한 타임스탬프 같은 불안정한 것에 의존하는 테스트는 취약한 테스트가 된다

Tip 70 — Test Your Software, or Your Users Will

Topic 42 — Property-Based Testing

단위 테스트의 맹점

  • 코드를 작성한 사람이 테스트도 작성하면, 잘못된 가정이 코드와 테스트 모두에 반영될 수 있다
  • 해결책: 개발자의 선입견을 공유하지 않는 컴퓨터가 테스트를 일부 담당하게 한다

계약, 불변식, 그리고 속성

  • 계약(Contract): 입력 조건을 충족하면 출력에 대한 특정 보장을 한다
  • 불변식(Invariant): 함수를 통과해도 항상 참인 상태 속성
    • 예: 리스트를 정렬해도 원소 수는 같다
  • 계약과 불변식을 묶어 속성(Properties) 이라고 부른다

Tip 71 — Use Property-Based Tests to Validate Your Assumptions

속성 기반 테스트 예시

Python의 Hypothesis 라이브러리를 활용한 정렬 테스트 (원리는 언어에 관계없이 동일):

from hypothesis import given
import hypothesis.strategies as some

# 속성 1: 정렬 전후 리스트 크기는 같다
@given(some.lists(some.integers()))
def test_list_size_is_invariant_across_sorting(a_list):
    original_length = len(a_list)
    a_list.sort()
    assert len(a_list) == original_length

# 속성 2: 정렬된 결과는 순서가 맞아야 한다
@given(some.lists(some.text()))
def test_sorted_result_is_ordered(a_list):
    a_list.sort()
    for i in range(len(a_list) - 1):
        assert a_list[i] <= a_list[i + 1]
  • Hypothesis는 각 테스트를 100번 실행하며 매번 다른 입력을 사용한다 — 사실상 200개의 개별 테스트

실전: 잘못된 가정 발견

창고(Warehouse) 시스템 예시: 기존 단위 테스트는 모두 통과했지만 속성 기반 테스트가 버그를 발견했다.

# 발견된 버그: in_stock이 최소 1개만 있으면 주문 가능하다고 판단
# 속성 테스트 실행 중 모자(hats) 2개 보유, 3개 주문 시도 → "Oversold hats" 예외 발생

# 버그가 있는 코드
def in_stock(self, item_name):
    return (item_name in self.stock) and (self.stock[item_name] > 0)  # 수량 확인 없음

# 수정된 코드
def in_stock(self, item_name, quantity):
    return (item_name in self.stock) and (self.stock[item_name] >= quantity)
  • 속성 테스트는 재고 조정 로직이 아니라 in_stock 함수의 버그를 발견했다 — 예상치 못한 곳에서 버그를 찾아낸다

속성 기반 테스트가 실패했을 때 대처 방법

  1. 실패를 유발한 파라미터를 확인한다
  2. 그 값을 사용하는 별도의 일반 단위 테스트를 작성한다
    • 이유 1: 속성 테스트 프레임워크의 추가 호출 없이 문제에 집중할 수 있다
    • 이유 2: 회귀 테스트 역할 — 속성 테스트는 매번 랜덤 값을 쓰므로 같은 값이 보장되지 않는다

속성 기반 테스트와 설계

  • 단위 테스트가 “테스트는 API의 첫 번째 사용자”라면, 속성 기반 테스트는 코드를 불변식과 계약의 관점에서 생각하게 만든다
  • “무엇이 변하지 않아야 하는가”, “무엇이 항상 참이어야 하는가”를 생각하는 것이 코드의 엣지 케이스를 제거하고 데이터를 불일치 상태로 남기는 함수를 찾아내는 효과가 있다
  • 단위 테스트와 속성 기반 테스트는 서로 다른 관심사를 다루는 보완적 도구

Topic 43 — Stay Safe Out There

개발자의 부주의가 가장 큰 위협

  • 대부분의 대규모 보안 침해는 공격자가 특별히 영리해서가 아니라, 개발자가 부주의했기 때문이다
  • “내 코드는 중요하지 않아, 아무도 이 서버를 모를 거야”는 위험한 착각 — 패치되지 않은 시스템의 오픈넷 생존 시간은 몇 분이다

보안 기본 원칙 5가지

1. 공격 표면 최소화 (Minimize Attack Surface Area)

공격 표면 = 공격자가 데이터를 입력/추출하거나 서비스를 실행할 수 있는 모든 접근 지점의 합

공격 벡터 대응 방법
코드 복잡성 단순하고 작은 코드를 유지 — 복잡할수록 취약점 증가
입력 데이터 외부 엔티티의 데이터를 절대 신뢰하지 말고 항상 살균(sanitize)한다
비인증 서비스 DDoS 공격의 즉각적인 벡터가 됨
인증된 서비스 허가된 사용자/서비스의 수를 최소화하고, 오래된 계정은 제거한다
출력 데이터 권한에 맞는 정보만 반환 — 민감 정보(주민번호 등)는 잘라내거나 난독화
디버깅 정보 스택 트레이스, 테스트 창 등은 외부 노출 차단

입력 살균 예시 (문자열 보간 시 인젝션 주의):

// 위험한 코드: 외부 입력을 그대로 쿼리에 삽입
const query = `SELECT * FROM users WHERE name = '${userInput}'`;

// 안전한 코드: 파라미터화된 쿼리 사용
const query = `SELECT * FROM users WHERE name = $1`;
db.query(query, [userInput]);

Tip 72 — Keep It Simple and Minimize Attack Surfaces

2. 최소 권한 원칙 (Principle of Least Privilege)

  • 필요한 최소 권한만, 필요한 최소 시간 동안만 사용한다
  • 높은 권한이 필요하면: 가져가고 → 최소한의 작업을 하고 → 즉시 반납한다
  • Unix login 프로그램 예시: 처음에는 root로 실행되지만, 사용자 인증 후 즉시 해당 사용자 권한으로 낮춘다
  • 애플리케이션에서도 블러트한 “관리자 vs 사용자” 이분법 대신 세분화된 권한 카테고리를 고려하라

3. 보안 기본값 (Secure Defaults)

  • 앱이나 사이트의 기본 설정은 가장 안전한 값이어야 한다
  • 편의성과 보안의 트레이드오프는 각 사용자가 직접 선택하게 한다
  • 예: 비밀번호 입력 기본값은 문자 숨기기 (공개 장소 고려)

4. 민감 데이터 암호화 (Encrypt Sensitive Data)

  • 개인 식별 정보, 금융 데이터, 비밀번호, 자격증명을 평문으로 저장하지 않는다
  • 버전 관리에 시크릿을 절대 커밋하지 않는다 — API 키, SSH 키, 암호화 비밀번호 등
  • 키와 시크릿은 설정 파일이나 환경 변수로 빌드/배포 과정에서 별도 관리한다

비밀번호 반패턴 (NIST 권고 기준):

  • 비밀번호 길이를 64자 미만으로 제한하지 않는다 (NIST 권장 최대 256자)
  • 비밀번호를 임의로 자르지 않는다
  • 특수 문자를 제한하지 않는다
  • 비인증 사용자에게 비밀번호 힌트를 제공하지 않는다
  • 브라우저의 붙여넣기 기능을 비활성화하지 않는다 (비활성화하면 짧은 비밀번호 유도)
  • 대소문자, 숫자, 특수문자 혼합 같은 구성 규칙을 강제하지 않는다
  • 일정 기간 후 비밀번호를 변경하도록 강제하지 않는다 (침해 발생 시에만)

5. 보안 업데이트 유지 (Maintain Security Updates)

Tip 73 — Apply Security Patches Quickly

  • 업데이트가 애플리케이션 일부를 깨트린다고 미루는 것은 매우 나쁜 생각 — 알려진 취약점에 노출된다
  • 역사상 가장 큰 데이터 침해 사고들은 업데이트가 뒤처진 시스템에서 발생했다
  • 모든 연결된 기기에 해당: 폰, 노트북, 빌드 서버, 프로덕션 서버, 클라우드 이미지

직접 암호화를 구현하지 마라

  • 암호화에서는 상식이 틀릴 수 있다 — 아주 작은 실수 하나로 모든 것이 무너진다
  • 직접 만든 암호화 알고리즘은 전문가가 몇 분 만에 해독할 수 있다
  • 검증된 오픈소스 라이브러리와 프레임워크만 사용하라
  • 직접 로그인 시스템을 구현하려면: 해시와 솔트, 레인보우 테이블, MD5/SHA1 취약성 등 매우 많은 것을 알아야 한다 → 그냥 서드파티 인증 제공자를 사용하라

Topic 44 — Naming Things

이름이 중요한 이유

  • 이름은 코드에서 의도와 믿음을 드러낸다
  • 뇌는 단어를 매우 빠르게 읽고 처리한다 — 단어에는 우선권이 있다
  • 스트룹 효과(Stroop Effect): 색상 이름이 실제 색상과 다를 때 혼란이 생기는 현상
    • 코드의 이름이 실제 동작과 불일치하면 같은 인지적 마찰이 발생한다

이름을 잘 짓기 위한 핵심 질문

“이것을 만들려는 나의 동기(motivation)는 무엇인가?”

  • 즉각적인 문제 해결 모드에서 벗어나 더 큰 그림을 바라보게 한다
  • 적절한 이름을 찾지 못하면 설계 자체가 잘못되었다는 신호일 수 있다

나쁜 이름 vs 좋은 이름 예시

변수 이름:

// 나쁜 이름: 아무 의미가 없다
const user = authenticate(credentials);

// 좋은 이름: 이 사람이 무엇을 하려는지 계속 상기시켜준다
const customer = authenticate(credentials);
const buyer = authenticate(credentials);

함수/파라미터 이름:

// 나쁜 이름: "무엇을 하는지"만 표현하고 "왜 하는지"가 없음
// amount가 절대값인지 퍼센트인지 알 수 없다
public deductPercent(amount: number): void

// 좋은 이름: 의도가 명확하고, 타입이 사용법을 문서화한다
public applyDiscount(discount: Percentage): void

모듈 내 함수 이름:

// 모듈 이름과 함께 읽힐 때를 고려한다
// Fib.fib(n) → 어색하다

// 더 자연스러운 이름
Fib.of(0)    // => 0
Fib.nth(20)  // => 4181

문화를 존중하라 (Honor the Culture)

  • 단일 문자 변수(i, j, k)는 항상 나쁜 게 아니다 — 언어/환경의 문화에 달려있다
    • C에서 i는 전통적인 루프 변수다 (60년 전 FORTRAN의 정수 변수 I-N에서 유래)
    • 다른 환경에서 기대되지 않는 관습을 사용하는 것도 마찬가지로 잘못이다
  • camelCase vs snake_case: 언어 커뮤니티의 관습을 따른다
  • Unicode 문자를 이름에 사용하는 것은 커뮤니티 기대를 파악한 후 결정한다

일관성 (Consistency)

  • 모든 프로젝트는 팀 내에서 특별한 의미를 가지는 용어(jargon)가 있다
    • “Order”는 온라인 스토어 팀과 종교 계보 앱 팀에서 전혀 다른 의미일 수 있다
  • 팀 전체가 같은 용어를 같은 의미로 사용하는 것이 중요하다
  • 방법 1: 페어 프로그래밍으로 용어가 자연스럽게 퍼지게 한다
  • 방법 2: 프로젝트 용어집(glossary)을 만든다 — 위키나 인덱스 카드로 관리

이름 바꾸기는 더 어렵다 (Renaming Is Even Harder)

  • 코드가 리팩터링되고 사용 패턴이 바뀌면서 이름이 의미를 잃거나 오도하게 된다
  • getData라는 함수가 실제로 데이터를 파일에 쓰는 경우 — 혼란의 근원
  • 문제를 발견하는 즉시 고쳐라 — 좋은 회귀 테스트가 있다면 빠뜨린 곳을 찾아준다

Tip 74 — Name Well; Rename When Needed

  • 이름을 변경하기 어렵다면 그것은 더 큰 문제(ETC 위반)의 증상이다 — 그 문제를 먼저 해결하라
  • 이름 변경을 쉽게 만들고 자주 하라 — 그렇지 않으면 신입에게 “getData가 사실은 파일에 씁니다”라고 설명해야 한다

Chapter 8: Before the Project (프로젝트 시작 전)

프로젝트 초반에 요구사항, 문제 해결, 협업, 애자일의 본질을 올바르게 이해하는 것이 성공의 전제 조건이다.

Topic 45: The Requirements Pit (요구사항의 함정)

요구사항 수집의 신화

  • "요구사항 수집" 이라는 단어 자체가 오해를 낳는다 — 요구사항은 이미 존재하며 개발자가 그냥 모아오면 된다는 잘못된 가정
  • 현실에서 요구사항은 가정, 오해, 정치 속에 깊이 파묻혀 있다
  • 때로는 요구사항이 아예 존재하지 않기도 한다

Tip 75: No One Knows Exactly What They Want
— 클라이언트는 자신이 정확히 무엇을 원하는지 모른다

Tip 76: Programmers Help People Understand What They Want
— 프로그래머의 핵심 역할은 클라이언트가 자신이 원하는 것을 이해하도록 돕는 것이다

요구사항은 탐험의 초대장이다

  • 클라이언트의 초기 요청은 “절대적 요구사항”이 아닌 탐험의 초대
  • 예시: "$50 이상 주문 시 배송 무료" 라는 요건을 받았을 때 던져야 할 질문들:
    • $50에 세금이 포함되는가?
    • $50에 현재 배송비가 포함되는가?
    • 종이책만 해당되는가, 전자책도 포함되는가?
    • 국제 주문은 어떻게 처리하는가?
  • 개발자의 역할: 요건의 결과(consequence)를 클라이언트에게 피드백하고, 함께 탐색하는 것

요구사항은 피드백 루프 속에서 학습된다

Tip 77: Requirements Are Learned in a Feedback Loop

  • 클라이언트의 말을 해석하고, 그 결과를 피드백으로 돌려주어 사고를 정제하게 한다
  • “이런 의미인가요?” 방식의 피드백 — 목업(mockup), 프로토타입을 만들어 직접 사용해보게 한다
  • 짧은 이터레이션, 직접적인 클라이언트 피드백 → 잘못된 방향으로 낭비하는 시간 최소화

클라이언트의 입장에서 생각하라

Tip 78: Work with a User to Think Like a User

  • 헬프데스크 시스템을 개발 중이라면, 며칠간 직접 전화 상담을 경험해보라
  • 재고 관리 시스템이라면, 창고에서 며칠 일해보라
  • 이런 경험은 시스템이 실제로 어떻게 사용될지에 대한 통찰을 제공한다

요구사항 vs. 정책

  • 나쁜 예: "직원의 상사와 인사 부서만 해당 직원의 기록을 볼 수 있다" → 정책이 하드코딩됨
  • 좋은 예: "권한이 있는 사용자만 직원 기록에 접근할 수 있다" → 메타데이터로 권한 관리

Tip 79: Policy Is Metadata
— 정책은 코드에 하드코딩하지 말고, 메타데이터로 분리하여 시스템이 지원해야 할 한 예시로 취급하라

  • 정책이 바뀌면 메타데이터만 업데이트하면 된다 → 변경에 유연한 구조
// 나쁜 방식 — 정책이 하드코딩됨
function canViewRecord(user: User, record: EmployeeRecord): boolean {
  return user.role === "supervisor" || user.role === "hr";
}

// 좋은 방식 — 정책이 메타데이터로 분리됨
function canViewRecord(user: User, record: EmployeeRecord, policy: AccessPolicy): boolean {
  return policy.hasPermission(user, "view", record);
}

요구사항 문서화

  • 최고의 요구사항 문서는 작동하는 코드다
  • 그러나 개발자 간 공유를 위해 이해한 내용을 기록하는 것은 필요하다
  • 요구사항 문서는 클라이언트에게 제출하는 납품물이 아니라, 구현 과정을 안내하는 이정표

요구사항 문서는 클라이언트를 위한 것이 아니다

  • 200페이지 분량의 상세 요구사항 문서를 만드는 것은 실수:
    • 클라이언트는 정확히 무엇을 원하는지 모른다 → 불안정한 기반 위의 복잡한 성
    • 클라이언트는 그 문서를 읽지 않는다
  • 개발자는 세부사항과 뉘앙스에 관심을 가지지만, 클라이언트는 고수준의 문제를 해결하는 데 관심이 있다

요구사항 문서는 계획을 위한 것이다

  • 인덱스 카드 한 장 크기의 유저 스토리(User Story) 를 선호한다
  • 짧게 유지함으로써 개발자가 질문을 유도하고, 피드백 프로세스를 강화한다
  • 보드에 붙여 상태와 우선순위를 시각적으로 관리할 수 있다

과다 명세(Overspecification)

  • 좋은 요구사항은 추상적이다
  • 요구사항은 아키텍처가 아니고, 디자인이 아니고, UI가 아니다 → 필요(Need) 그 자체다
  • 비즈니스 요구를 정확히 반영하는 가장 단순한 표현이 최선

요구사항 범위 증가(Creep) 방지

  • 기능 비대화(Feature Bloat), 요구사항 크리프는 삶은 개구리 증후군과 같다
  • 해결책: 피드백
    • 클라이언트가 이터레이션을 직접 경험하면 “기능 하나 추가”의 실제 영향을 체감하게 된다

프로젝트 용어집 유지

Tip 80: Use a Project Glossary

  • 프로젝트에서 사용하는 모든 특정 용어와 어휘를 정의하는 단일 용어집을 만들고 유지하라
  • 최종 사용자부터 지원 직원까지 모든 참여자가 사용해야 한다
  • 온라인 문서로 제공하여 접근성을 높인다
  • 사용자와 개발자가 같은 것을 다른 이름으로 부르거나, 다른 것을 같은 이름으로 부르면 프로젝트는 실패한다

Topic 46: Solving Impossible Puzzles (불가능한 퍼즐 풀기)

진짜 제약 vs. 가상의 제약

  • 불가능해 보이는 문제들도 대부분 진짜로 불가능한 것이 아니다
  • 풀 수 없는 방식으로 계속 시도하는 사람들 — 명백히 안 되는데도 계속 같은 방법을 반복한다
  • 핵심: 진짜(절대적) 제약선입견으로 만들어진 가짜 제약을 구별하라

Tip 81: Don’t Think Outside the Box — Find the Box
— “박스 밖에서 생각하라”가 아니라, 박스(진짜 제약의 경계)를 찾는 것이 핵심이다

자유도(Degrees of Freedom)를 인식하라

  • 퍼즐 해결의 핵심:
    1. 나에게 주어진 제약을 인식한다
    2. 내가 가진 자유도를 인식한다
    3. 자유도 안에서 해결책을 찾는다
  • 실천 방법:
    • 가능한 모든 경로를 나열한다 (아무리 어리석어 보여도)
    • 각 경로가 왜 안 되는지 설명한다 — 정말 확신하는가? 증명할 수 있는가?
    • 제약을 범주화하고 우선순위를 매긴다 (가장 제한적인 제약을 먼저 식별)

막혔을 때 — 의식적인 뇌에서 벗어나라

  • 문제가 너무 어렵게 느껴질 때, 잠시 다른 일을 하라
    • 다른 작업을 한다
    • 산책을 한다
    • 잠을 자며 “수면 학습”을 한다
  • 의식적 뇌는 비교적 단순하다 — 의식 아래에 있는 강력한 연상 신경망에 공간을 주어라
  • 심리학 연구: 산만해진 사람들이 복잡한 문제에서 의식적으로 집중한 사람보다 더 좋은 결과를 냈다
  • 혼자 해결이 안 될 때: 다른 사람에게 설명해보라 — 러버 덕 디버깅(Rubber Ducking)의 원리

설명할 때 도움이 되는 질문들

  • 왜 이 문제를 풀고 있는가?
  • 이 문제를 해결하면 어떤 이점이 있는가?
  • 겪고 있는 문제가 엣지 케이스와 관련 있는가? 제거할 수 있는가?
  • 더 단순하고 관련된 문제를 풀 수 있는가?

준비된 마음이 행운을 만든다

“관찰의 세계에서 행운은 준비된 마음에만 찾아온다.” — 루이 파스퇴르

  • 유레카! 순간을 갖기 위해서는 무의식적 뇌에 **충분한 원재료(경험과 학습)**가 필요하다
  • 엔지니어링 일지(Engineering Daybook) 를 활용해 무엇이 효과가 있고 없는지 기록하며 뇌를 훈련시켜라
  • 그리고 항상: 패닉하지 마라 (DON’T PANIC)

Topic 47: Working Together (함께 일하기)

협업의 진짜 의미

  • “함께 일한다”는 것 = 대규모 요구사항 문서를 공유하거나, 이메일을 주고받거나, 끝없는 회의를 하는 것이 아니다
  • 코딩하면서 함께 문제를 푸는 것 — 이것이 진짜 협업

콘웨이의 법칙 (Conway’s Law)

“시스템을 설계하는 조직은 그 조직의 커뮤니케이션 구조를 복사한 설계를 만들게 된다.” — Melvin Conway, 1967

  • 서로 소통하지 않는 팀 → 사일로(silo) 구조의 시스템
  • 두 팀으로 나뉜 조직 → 클라이언트/서버 또는 프론트엔드/백엔드 분리
  • 역원칙: 원하는 코드 구조가 있다면, 팀을 그 구조에 맞게 의도적으로 설계할 수 있다
  • 사용자를 포함한 팀 → 사용자 참여가 명확히 반영된 소프트웨어

페어 프로그래밍 (Pair Programming)

  • 한 명이 키보드를 조작, 다른 한 명은 조작하지 않지만 함께 문제를 해결
  • 필요에 따라 역할을 전환할 수 있다
  • 장점:
    • 서로 다른 배경, 경험, 문제 해결 방식이 더 많은 두뇌 파워를 가져온다
    • 타이핑하는 개발자는 저수준 세부사항(문법, 코딩 스타일)에 집중, 다른 개발자는 고수준 이슈와 범위 파악
    • 동료의 시선이 있을 때 foo와 같은 변수명 사용, 지름길 선택을 억제 → 코드 품질 향상

몹 프로그래밍 (Mob Programming)

  • 여러 명(12명 이상도 가능)이 한 문제에 동시에 집중하고, 한 명이 타이핑
  • 개발팀 외 사용자, 프로젝트 스폰서, 테스터도 참여 가능
  • 타이트한 협업 + 라이브 코딩의 조합

Tip 82: Don’t Go into the Code Alone

협업을 위한 실용적 팁

  • 코드를 짜라, 자존심을 세우지 마라 — 누가 더 뛰어난지가 아니다
  • 작게 시작하라 — 4-5명의 몹, 또는 짧은 페어 세션
  • 코드를 비판하라, 사람을 비판하지 마라 — “이 블록을 살펴봅시다”가 “당신이 틀렸어요”보다 낫다
  • 타인의 관점을 듣고 이해하려 하라 — 다르다는 것이 틀린 것이 아니다
  • 주기적인 회고(retrospective) 를 통해 지속적으로 개선하라

Topic 48: The Essence of Agility (애자일의 본질)

애자일은 명사가 아니다

Tip 83: Agile Is Not a Noun; Agile Is How You Do Things

  • “애자일하다”는 것은 방법론의 이름이 아니라 일하는 방식을 형용하는 것
  • 애자일 매니페스토의 4가지 핵심 가치:
    1. 프로세스와 도구보다 개인과 상호작용
    2. 포괄적인 문서화보다 작동하는 소프트웨어
    3. 계약 협상보다 고객 협업
    4. 계획 따르기보다 변화에 대응하기

애자일 프로세스는 존재할 수 없다

  • “이것을 하면 애자일이다”라고 말하는 사람은 틀렸다 — 정의상으로
  • 달리는 가젤은 직선으로 달리지 않는다 — 체조선수는 환경 변화에 초당 수백 번 보정한다
  • 소프트웨어 개발도 마찬가지 — 고정적이고 정적인 계획은 불확실성을 이길 수 없다
  • 가치(values)는 무엇을 해야 하는지 말해주지 않는다 — 무엇을 볼 것인지를 말해준다

애자일하게 일하는 방법 — 피드백 루프

1. 현재 위치를 파악한다
2. 목표를 향한 가장 작고 의미 있는 한 걸음을 내딛는다
3. 도달한 위치를 평가하고, 깨진 것을 수정한다
반복 (모든 레벨에서 재귀적으로)

피드백 루프의 실제 적용 — 변수 네이밍 예시

// 처음: 막연한 이름
const user = accountOwner(accountId);

// 피드백 적용: 더 명확하게
const owner = accountOwner(accountId);

// 다시 생각: 실제로 뭘 하려는 건가? → 이메일 전송
// 계정 소유자 전체 객체가 필요한가?
const email = emailOfAccountOwner(accountId);
// 이 피드백 루프가 커플링까지 줄였다
  • 피드백 루프는 변수 네이밍 같은 아주 낮은 수준에서도, 프로젝트 전체 방향 설정에서도 동일하게 작동한다
  • 어떤 프로젝트에서는 한 걸음 내딛고 나서 소프트웨어가 전혀 필요하지 않다는 것을 발견하기도 한다

피드백 루프가 설계를 이끈다

  • Topic 8에서 좋은 설계의 척도는 얼마나 쉽게 변경할 수 있느냐라고 했다
  • 이것이 바로 애자일과 연결된다:
    • 변경 후 마음에 들지 않을 때 → 깨진 것을 고칠 수 있어야 한다
    • 고치는 것이 고통스러우면 → 그냥 두고 싶은 유혹이 생긴다 (소프트웨어 엔트로피)
    • 피드백 루프를 효율적으로 만들기 위해 좋은 설계가 필수
  • 좋은 설계 = 변경이 쉬운 설계 → 이것이 진정한 애자일

Chapter 9: Pragmatic Projects (실용주의 프로젝트)

팀 수준에서의 실용주의 기법 — 팀 구성, 방법론 선택, 인프라, 사용자 만족, 장인 정신을 다룬다.

Topic 49: Pragmatic Teams (실용주의 팀)

팀의 정의

  • 팀은 소규모의 안정적인 독립 개체
  • 50명은 팀이 아니라 군중(horde)
  • 구성원이 계속 바뀌고 서로를 모르는 것은 팀이 아니라 빗속 버스 정류장의 낯선 사람들

Tip 84: Maintain Small, Stable Teams
— 10~12명 이하, 구성원이 자주 바뀌지 않고, 서로를 잘 알고 신뢰하며 의존하는 팀

팀의 깨진 창문 없애기

  • 품질은 팀의 문제다 — “품질 담당자”를 두는 것은 말이 안 된다
  • 팀 전체가 깨진 창문(고쳐지지 않는 작은 결함)을 허용하지 않아야 한다
  • 품질은 각 구성원의 기여에서 나온다 — 나중에 덧붙이는 것이 아니라 처음부터 내재된다

삶은 개구리 경계하기

  • 팀 전체는 개인보다 더 쉽게 삶겨진다
    • “누군가 처리하겠지”라는 가정
    • 팀 리더가 이미 승인했겠지라는 가정
  • 대응 방법:
    • 모든 구성원이 환경 변화를 능동적으로 모니터링하도록 독려
    • 범위 확대, 일정 단축, 추가 기능, 새로운 환경 등 변화를 항상 인식하라
    • 변화를 거부하는 것이 아니라 변화가 일어나고 있음을 인식하는 것이 목표

지식 포트폴리오 스케줄링

  • 개선과 혁신을 진지하게 원한다면, 스케줄에 넣어야 한다
  • “여유 시간이 생기면 하자”는 영원히 안 한다는 뜻

Tip 85: Schedule It to Make It Happen

팀 백로그에 포함해야 할 항목들:

항목 설명
레거시 시스템 유지보수 새 시스템이 재밌어도, 레거시 유지 업무를 구석에 숨기지 마라
프로세스 회고 및 개선 무엇이 효과가 있고 없는지 정기적으로 살펴보고 개선하라
신기술 실험 유행이라서가 아니라, 의도적으로 프로토타입으로 검증하라
학습 및 기술 향상 개인 학습도 좋지만, 팀 전체로 확산시켜야 더 효과적이다

팀 존재감 커뮤니케이션

  • 최악의 팀: 외부에서 보기에 침울하고 말이 없는 팀 — 구조 없는 회의, 일관성 없는 문서
  • 훌륭한 팀: 뚜렷한 개성을 가진다 — 잘 준비된 발표, 명확하고 일관된 문서, 하나의 목소리

팀 브랜딩 전략:

  • 프로젝트 이름(이왕이면 기발한 것)을 정하라
  • 30분 동안 로고를 만들어라
  • 대화에서 팀 이름을 자주 사용하라
  • 우스워 보여도, 팀에 정체성을 부여하고 외부 세계가 기억할 수 있는 무언가를 만든다

팀에서의 DRY

  • 팀 내 중복 작업은 사일로 시스템과 유지보수 악몽을 낳는다
  • 즉각적이고 마찰 없는(frictionless) 커뮤니케이션이 핵심
    • 동일 공간: 칸막이 너머로 질문
    • 원격: 메시지 앱 등 전자 수단
  • 마찰 없음 = 질문하기 쉽고, 진행 상황 공유가 쉽고, 팀원이 뭘 하는지 인식하기 쉬운 것

팀 트레이서 불릿

  • 팀이 다양한 기술 영역(요구사항, 아키텍처, 프론트엔드/서버 코딩, 테스팅)을 처리해야 할 때
  • 이 활동들을 별도로 분리하는 것은 잘못된 통념
  • 전문화된 역할 분리 → 게이트(gate)와 핸드오프(handoff) 발생 → 린(Lean)에서는 이를 낭비(waste)라고 부름

Tip 86: Organize Fully Functional Teams
— 엔드투엔드로 코드를 빌드할 수 있도록 팀을 구성하라 (프론트엔드, UI/UX, 서버, DBA, QA 모두 포함)

  • 트레이서 불릿 방식: 작은 기능을 엔드투엔드로 빠르게 구현 → 팀 커뮤니케이션과 전달력을 즉시 피드백받는다

자동화

  • 팀이 하는 모든 것을 자동화하라
  • 코드 포맷팅 표준 → 에디터/IDE가 자동 처리
  • 수동 테스트 → CI 빌드가 자동 실행
  • 수동 배포 → 자동화 스크립트가 매번 동일하게 반복 가능하게

페인트를 언제 멈출지 알아라

  • 팀은 개인들로 이루어진다 — 각 구성원이 자신만의 방식으로 빛날 수 있게 하라
  • 구성원을 지원하고 프로젝트가 가치를 제공할 수 있는 충분한 구조만 제공하라
  • 그 이상으로 페인트를 더 칠하고 싶은 유혹을 이겨내라

Topic 50: Coconuts Don’t Cut It (코코넛은 통하지 않는다)

카고 컬트 (Cargo Cult) 비유

  • 2차대전 당시 멜라네시아 원주민들: 비행기와 군사 물자를 가져다 준 활주로, 관제탑을 코코넛 껍데기와 나뭇가지로 재현 → 하지만 비행기는 오지 않았다
  • 형태는 모방했지만 내용(본질)은 모방하지 못한 것 — 이를 카고 컬트라고 한다

우리도 종종 이 원주민들과 같다

표면적 아티팩트에 투자하는 함정

  • 예시: “스크럼을 쓴다”고 주장하지만 실상:
    • 데일리 스탠드업을 주 1회 진행
    • 4주 스프린트가 6~8주로 늘어남
    • 그러나 “스탠드업”, “이터레이션” 같은 단어를 쓰면 마치 주문처럼 효과가 있을 것이라 믿음
  • 결과: 실제 효과 없음

Tip 87: Do What Works, Not What’s Fashionable
— 유행하는 것이 아니라 실제로 효과 있는 것을 하라

컨텍스트가 중요하다

  • Spotify, Netflix, Stripe, GitLab의 방식을 따라하고 싶은 유혹
  • 자문해야 할 것들:
    • 같은 시장에 있는가?
    • 같은 제약과 기회를 가지고 있는가?
    • 비슷한 전문성과 조직 규모인가?
    • 유사한 경영진, 문화, 사용자 기반인가?
  • 다르다면, 그 방식이 우리에게는 맞지 않을 수 있다

진짜 검증 방법: 직접 해보기

  • 소규모 팀에서 파일럿(pilot)으로 시도하라
  • 효과가 있는 것은 유지, 낭비나 오버헤드는 버려라
  • Spotify나 Netflix조차 지금의 프로세스로 성장한 것이 아니다 — 성장하면서 계속 바꿔왔다
  • 그것이 그들의 진짜 성공 비결이다

하나의 방법론으로는 모두를 만족시킬 수 없다

  • 인증 프로그램의 함정: 규칙을 암기하고 따르는 능력을 테스트 → 기존 규칙을 넘어서는 통찰력을 키워주지 않는다
  • 원하는 것: **“스크럼/린/칸반/XP가 이렇게 하잖아요”**가 아니라, 가능성을 보고 활용하는 마인드셋
  • 어떤 방법론의 가장 좋은 부분들을 선택하고 적용하라 — 현재의 방법론들은 완성형이 아니다

진짜 목표

  • “스크럼 하기”, “애자일 하기”, “린 하기”가 목표가 아니다
  • 목표: 지금 당장 사용자에게 새로운 역량을 제공하는 작동하는 소프트웨어를 전달할 수 있는 위치에 있는 것

Tip 88: Deliver When Users Need It

graph LR
    A[연 단위 배포] --> B[월 단위 배포]
    B --> C[주 단위 배포]
    C --> D[4주 스프린트]
    D --> E[2주 스프린트]
    E --> F[1주 스프린트]
    F --> G[일일 배포]
    G --> H[온디맨드 배포]

  • 온디맨드 배포 = 매분 배포해야 한다는 뜻이 아님 — 사용자가 필요할 때, 비즈니스적으로 합리적일 때 배포한다는 뜻
  • 이를 위해서는 견고한 인프라(버전 컨트롤, 자동화 테스트, 자동화)가 필요하다
  • 개발은 메인 브랜치(트렁크)에서 진행하고, **피처 스위치(feature switch)**를 활용해 테스트 기능을 선택적으로 출시

Topic 51: Pragmatic Starter Kit (실용주의 스타터 킷)

모든 팀이 방법론, 언어, 기술 스택에 무관하게 필요로 하는 세 가지 핵심 요소:

graph TD
    A[Pragmatic Starter Kit] --> B[버전 컨트롤<br/>Version Control]
    A --> C[회귀 테스팅<br/>Regression Testing]
    A --> D[완전 자동화<br/>Full Automation]

버전 컨트롤로 드라이브하라

Tip 89: Use Version Control to Drive Builds, Tests, and Releases

  • 프로젝트 빌드에 필요한 모든 것을 버전 컨트롤 아래 두어라
  • 빌드 머신을 **일회용(ephemeral)**으로 만든다 — 클라우드에서 온디맨드로 생성
  • 배포 구성도 버전 컨트롤 아래 → 프로덕션 릴리스 자동화 가능
  • 커밋/푸시 → 빌드, 테스트, 배포 자동 트리거
  • 릴리스 = 버전 컨트롤 시스템의 **태그(tag)**로 지정 → 특정 빌드 머신이나 개발자 머신에 의존하지 않는 진정한 지속적 배포

끝없는(Ruthless) 지속적 테스팅

Tip 90: Test Early, Test Often, Test Automatically
Tip 91: Coding Ain’t Done ’Til All the Tests Run

  • 프라그매틱 프로그래머는 버그를 지금 찾는다 — 나중에 다른 사람이 찾는 수치를 피하기 위해
  • 좋은 프로젝트는 프로덕션 코드보다 테스트 코드가 더 많을 수 있다
  • 테스트 환경은 프로덕션 환경을 최대한 유사하게 반영해야 한다 — 차이가 있는 곳에서 버그가 번식한다

테스트 유형

테스트 유형 목적
유닛 테스트 개별 모듈이 독립적으로 잘 동작하는지 검증
통합 테스트 주요 서브시스템들이 서로 잘 작동하는지 검증 — 계약(contracts) 기반
검증 및 확인 사용자가 원하는 것을 실제로 구현했는지 확인 — “맞는 것을 만들고 있는가?”
성능 테스트 실제 조건 하에서 성능 요구사항을 충족하는지 확인 — 확장성 포함

테스트 자체를 테스트하기

Tip 92: Use Saboteurs to Test Your Testing

  • 버그를 탐지하기 위한 테스트를 작성한 후 → 의도적으로 버그를 만들어 테스트가 잡는지 확인
  • 넷플릭스의 **카오스 몽키(Chaos Monkey)**처럼 서비스를 의도적으로 중단시켜 복원력을 테스트

상태 커버리지 vs. 코드 커버리지

Tip 93: Test State Coverage, Not Code Coverage

function test(a: number, b: number): number {
  return a / (a + b);
}
// 3줄짜리 함수지만 a가 0~999, b가 0~999일 경우 논리적 상태는 1,000,000가지
// 그 중 a+b === 0 일 때 1가지만 실패 → 코드 줄을 실행했다는 것 ≠ 모든 상태를 테스트했다는 것
  • 코드 커버리지 100%라도 가능한 모든 상태를 테스트한 것은 아니다
  • **속성 기반 테스팅(Property-Based Testing)**으로 코드의 계약(contract)과 불변성(invariant)에 따라 테스트 데이터를 컴퓨터가 생성하게 하라

버그는 한 번만 찾아라

Tip 94: Find Bugs Once

  • 버그가 자동화 테스트망을 빠져나간다면, 새 테스트를 추가해서 다음 번에 잡게 하라
  • 사람 테스터가 버그를 발견하면, 그것이 그 버그를 사람이 발견하는 마지막이 되어야 한다
  • “다시는 그런 일 없을 거예요”라는 개발자의 말을 믿지 마라 — 그 버그는 반드시 다시 나타난다

완전 자동화

Tip 95: Don’t Use Manual Procedures

  • 현대 개발은 스크립트 기반, 자동화된 절차에 의존한다
  • 수동 절차 = 일관성은 운에 맡기는 것 + 반복 가능성 보장 안 됨
  • 예시: 모든 개발자가 다른 방식으로 IDE를 설치 → 미묘한 동작 차이 → 한 머신에서는 버그, 다른 머신에서는 없음
  • 셸 스크립트나 Ansible, Puppet, Chef, Salt 같은 도구 활용
  • 스크립트 자체도 버전 컨트롤 → 빌드/릴리스 절차의 변경 이력 추적 가능
  • 수동 단계 하나라도 도입하면 → 커다란 창문 하나를 깨는 것

Topic 52: Delight Your Users (사용자를 기쁘게 하라)

개발자의 진짜 목표

  • 목표는 코드를 납품하는 것이 아니라 사용자를 기쁘게 하는 것
  • 사용자는 코드에 관심이 없다 — 그들은 예산 안에서 비즈니스 문제를 해결하는 데 관심이 있다
  • 제때 작동하는 소프트웨어를 납품하는 것만으로는 충분하지 않다

진짜 기대(Expectation)를 발굴하는 질문

“이 프로젝트가 끝난 후 한 달(혹은 1년) 뒤에, 우리 모두가 성공했다는 것을 어떻게 알 수 있을까요?”

  • 이 질문의 답변은 종종 예상 밖이다:
    • 상품 추천 개선 프로젝트 → 실제 판단 기준은 고객 유지율
    • 두 데이터베이스 통합 프로젝트 → 실제 판단 기준은 데이터 품질 또는 비용 절감
  • 이런 비즈니스 가치에 대한 기대가 진짜 중요한 것 — 소프트웨어 프로젝트 자체가 아니라

기대를 파악한 후 해야 할 일

  • 팀의 모든 구성원이 이 기대를 완전히 명확하게 이해하게 한다
  • 결정을 내릴 때 “이 방향이 그 기대에 더 가까운가?“를 생각한다
  • 기대를 바탕으로 사용자 요구사항을 비판적으로 분석한다
    • 많은 경우 “요구사항”이 사실은 기술로 할 수 있는 것에 대한 추측이거나 요구사항 문서로 위장한 아마추어 구현 계획이다
    • 요구사항 자체를 변경하는 제안도 두려워하지 마라 — 목표에 더 가까워진다면

Tip 96: Delight Users, Don’t Just Deliver Code

  • 직함은 “소프트웨어 개발자”일지라도, 진짜 직함은 **“문제 해결사(Problem Solver)”**여야 한다
  • 클라이언트와 관계를 형성해 그들의 문제를 능동적으로 해결하는 파트너가 되어라

Topic 53: Pride and Prejudice (자부심과 편견)

책임을 기꺼이 받아들여라

  • 프라그매틱 프로그래머는 책임을 회피하지 않는다 — 오히려 도전을 받아들이고 전문성을 드러내는 것을 즐긴다
  • 디자인이나 코드를 책임진다면, 자랑스러운 결과물을 만들어라

Tip 97: Sign Your Work
— 예전 장인들이 자신의 작품에 서명했듯이, 당신도 서명하라

코드 소유권의 균형

  • 코드 소유 의식 → 협력 문제를 일으킬 수 있는 위험:
    • 영역 다툼
    • 공통 기반 요소 작업 기피
    • 동료 코드에 대한 편견
  • 올바른 자세:
    • 코드를 질투하듯 방어하지 마라
    • 다른 사람의 코드를 존중하라
    • 황금률: “다른 사람에게 대접받고 싶은 대로 대접하라”
  • 익명성의 위험: 대형 프로젝트에서의 익명성은 태만, 실수, 나쁜 코드의 온상이 된다
  • 코드는 반드시 소유되어야 한다 — 반드시 개인이 소유해야 하는 것은 아니다 (공동 소유도 가능하지만, 페어 프로그래밍 등의 보완 장치 필요)

프라그매틱 프로그래머의 서명

“내가 이것을 만들었고, 내 작업의 뒤에는 내가 있다”

  • 당신의 서명이 품질의 지표가 되어야 한다
  • 누군가 코드에서 당신의 이름을 보면 다음을 기대해야 한다:
    • 견고하고 잘 작성된 코드
    • 테스트됨
    • 문서화됨
    • 진정으로 전문적인 결과물

Chapter 10: Postface (후기)

소프트웨어는 세상을 지배하게 되었다. 그 힘만큼 우리에게는 막대한 책임이 따른다.

소프트웨어가 세상을 바꾸었다

  • 프레드 브룩스: “프로그래머는 시인처럼, 순수한 생각의 재료에서 아주 조금만 벗어난 곳에서 일한다. 그는 공기 속에서 공중 성을 짓는다.”
  • 우리가 만드는 것들:
    • 트위터: 혁명 계획에 사용됨
    • 자동차 프로세서: 미끄럼 방지를 위해 작동
    • 스마트폰: 일상의 세세한 기억을 대신함
  • 우리의 상상력이 곳곳에 있다 — 그리고 그것은 세상을 바꿀 수 있다

도덕적 나침반 (The Moral Compass)

개발자의 책임

  • 소프트웨어는 더 이상 취미 프로그램이나 고립된 배치 프로세스가 아니다
  • 현대 일상의 구조를 직접 짜고 있다
  • 생명 유지 시스템(발전소, 자동차, 의료 장비)을 제어하는 임베디드 기기 → 잘못 설계되면 사람이 죽는다
  • 소셜 미디어 → 평화로운 혁명을 촉진하거나, 혐오를 조장하거나
  • 빅 데이터 → 쇼핑을 편리하게 하거나, 모든 프라이버시를 파괴하거나
  • 뱅킹 시스템 → 사람의 삶을 바꾸는 대출 결정을 내린다

코드를 납품할 때마다 던져야 할 두 가지 질문

  1. “이 사용자를 보호했는가?”
    • 보안 패치를 지속적으로 적용할 수 있게 했는가?
    • 자동 온도 조절기가 실패해도 사용자가 수동으로 제어할 수 있는가?
    • 필요한 데이터만 저장하고 개인정보를 암호화했는가?
    • 완벽할 수는 없다 — 하지만 모든 결과를 나열하고 사용자를 보호하려 진지하게 노력했는지를 자문할 수 있어야 한다

Tip 98: First, Do No Harm (먼저 해를 끼치지 마라)

  1. “내가 이 소프트웨어의 사용자가 되어도 괜찮은가?”
    • 내 세부 정보가 공유되어도 괜찮은가?
    • 내 이동 경로가 소매점에 제공되어도 괜찮은가?
    • 이 자율주행차에 타는 것이 편한가?

Tip 99: Don’t Enable Scumbags (나쁜 자들을 가능하게 하지 마라)

  • 윤리적 한계를 넘는 프로젝트에 참여한다면, 스폰서로부터 아무리 많은 분리 단계가 있더라도 당신도 그 책임에서 자유롭지 않다

원하는 미래를 상상하라

  • 미래는 당신에게 달려 있다 — 당신의 상상력, 희망, 관심사가 다음 20년을 만든다
  • 자신과 후손을 위해 미래를 짓고 있다 — 우리 모두가 살고 싶은 미래로 만들 의무가 있다
  • 이상에 어긋나는 일을 하고 있다는 것을 알아챌 때, “아니오“라고 말할 용기를 가져라
  • 더 나은 미래를 상상하고, 그것을 만들 용기를 가져라

Tip 100: It’s Your Life. Share it. Celebrate it. Build it. AND HAVE FUN!