이 글은 Claude Fable 5.1 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.


1년 전에 존 오스터하우트(John Ousterhout)의 A Philosophy of Software Design 을 소개하는 글을 올렸습니다. 21개 장의 주장을 요약한 글이었고, 코드는 한 줄도 없었습니다.

이 책의 주장은 요약본으로 읽으면 대부분 “당연한 말” 처럼 들립니다. 모듈은 깊어야 한다, 정보를 숨겨라, 에러를 없애라. 그런데 막상 자기 코드에서 얕은 모듈을 찾아내라고 하면 손이 멈춥니다. 원칙과 코드 사이의 거리가 생각보다 멉니다.

이 시리즈는 그 거리를 좁히는 8부작입니다. 책의 각 장이 말하는 원칙을 Before 코드와 After 코드 로 나란히 놓습니다. Before 는 책이 “위험 신호(red flag)” 라고 부르는 형태이고, After 는 원칙을 적용한 형태입니다. 언어는 Go 입니다.

이번 편은 책의 서론에 해당하는 1~3장을 다룹니다. 복잡성이 무엇이고 어디서 오는지, 그리고 그것이 왜 “일단 돌아가게 만드는” 습관에서 자라는지를 코드로 봅니다.


1. 책과 저자#

먼저 서지 사항입니다.

항목내용
제목A Philosophy of Software Design
저자John K. Ousterhout
1판2018년 4월, Yaknyam Press (Palo Alto). ISBN 978-1-7321022-0-0
2판2021년 7월. ISBN 173210221X
분량2판 기준 22개 장 + 설계 원칙 요약 + 위험 신호 요약

2판에서 바뀐 것은 세 가지입니다. 21장 “Decide What Matters” 가 새로 들어갔고, 6장 “General-Purpose Modules are Deeper” 가 확장·재편되었으며, 로버트 마틴의 Clean Code 와 견해가 갈리는 지점(메서드 길이, 주석의 역할)을 비교하는 절이 두 곳 추가되었습니다. 이 시리즈는 2판의 장 번호를 따릅니다.

한국어 번역서는 이 글을 쓰는 시점(2026년 9월)에 확인하지 못했습니다. 알라딘에서 저자명과 예상 제목으로 검색했지만 결과가 없었습니다. 본문의 인용은 영문 원서를 직접 옮긴 것입니다.

저자는 스탠퍼드 컴퓨터과학과 교수입니다. 본인 홈페이지에 은퇴해서 정규 강의는 하지 않는다고 적어 두었습니다. 이 책이 나온 배경도 그 강의입니다. 스탠퍼드의 소프트웨어 설계 과목(CS190)에서 학생들의 코드를 리뷰하며 반복해서 지적하게 된 것들을 정리한 책입니다. 그래서 책의 예제 상당수가 “수업에서 학생들이 만든 텍스트 편집기” 에서 나옵니다.

저자의 이력을 알면 책의 어조가 이해됩니다. 버클리 시절 Tcl/Tk 를 만들었고, Sprite 분산 운영체제와 로그 구조 파일 시스템(1992) 연구를 이끌었습니다. 스탠퍼드에서는 RAMCloud 저장 시스템과 Raft 합의 알고리즘(2014, Diego Ongaro 와 공저)을 냈습니다. 수십 년간 시스템 소프트웨어를 직접 짜고 학생 코드를 읽어 온 사람의 책이라, 추상적인 원칙보다 “이 코드는 왜 나쁜가” 에 대한 구체적 판단이 많습니다.


2. 왜 Go 인가, 그리고 어디서 부딪히는가#

책의 예제는 Java 와 C++ 로 쓰여 있습니다. 클래스와 예외가 있는 언어입니다. Go 에는 둘 다 없습니다. 그래서 Go 로 옮기면 책의 주장 중 어떤 것은 더 선명해지고, 어떤 것은 번역이 필요합니다. 미리 표로 정리해 둡니다.

부딪히는 지점해당 장이 시리즈의 대응
예외가 없다. 에러가 반환값이라 if err != nil 이 인터페이스에 그대로 드러난다10장오히려 10장의 논지가 가장 잘 보이는 언어입니다. 함수 시그니처에서 error 가 사라지는 것이 “에러의 존재를 없앴다” 는 증거가 됩니다
클래스와 상속이 없다4·19장책의 “모듈” 을 패키지·타입·함수로 읽습니다. 19장의 구현 상속 비판은 struct embedding 남용으로 대응합니다
가시성이 패키지 단위다5장정보 은닉의 단위가 클래스가 아니라 패키지입니다. 인터페이스의 크기를 go doc 출력 줄 수로 셀 수 있어서 오히려 편합니다
저자가 Go 의 짧은 이름 관례에 반대한다14장14.5절 제목이 “A different opinion: Go style guide” 입니다. 숨기지 않고 양쪽 논거를 그대로 싣습니다

책이 Go 를 직접 언급하는 곳도 있습니다. 4장에서 깊은 모듈의 예로 Unix 파일 I/O 와 함께 가비지 컬렉터 를 들면서 “Go 나 Java 같은 언어의” 라고 씁니다. 인터페이스가 아예 없는 모듈이라는 설명입니다. 메모리를 해제하는 인터페이스를 없애 버렸으니 시스템 전체의 인터페이스가 오히려 줄었다는 것입니다.


3. 복잡성의 정의 (2장)#

책은 복잡성을 이렇게 정의합니다.

“Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.” (2.1절)

정의에서 눈여겨볼 곳은 “structure” 와 “understand and modify” 입니다. 기능이 많다고 복잡한 것이 아닙니다. 규모가 크다고 복잡한 것도 아닙니다. 이해하고 고치기 어려우면 복잡한 것이고, 작고 기능이 없어도 고치기 어려우면 복잡한 것입니다.

같은 절에 판정 기준도 있습니다.

“Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex.” (2.1절)

작성자의 의견은 증거로 치지 않습니다. 이 기준은 시리즈 내내 쓰입니다. Before 코드가 “나는 이해되는데” 싶으면, 그 코드를 처음 보는 사람 입장으로 다시 읽어 봐야 합니다.

책은 복잡성의 증상 세 가지와 원인 두 가지를 구분합니다. 관계는 아래와 같습니다.

flowchart LR
    D["의존성<br/>(dependencies)"]
    O["모호성<br/>(obscurity)"]
    CA["변경 증폭<br/>(change amplification)"]
    CL["인지 부하<br/>(cognitive load)"]
    UU["모름<br/>(unknown unknowns)"]
    D --> CA
    D --> CL
    O --> CL
    O --> UU
    style D fill:#87CEEB,color:#000000
    style O fill:#87CEEB,color:#000000
    style CA fill:#FFD700,color:#000000
    style CL fill:#FFD700,color:#000000
    style UU fill:#FF9999,color:#000000

의존성은 변경 증폭과 인지 부하를 만들고, 모호성은 모름과 인지 부하를 만듭니다. 셋 중 최악은 모름 입니다. 무엇을 알아야 하는지조차 알 수 없는 상태라, 고치고 나서 버그가 터질 때까지 문제가 있다는 것을 모릅니다.

아래에서 증상 세 가지를 각각 Go 코드로 봅니다. 예제의 뼈대는 책의 2장 예제(웹사이트 배너 색, C 의 메모리 해제 책임, 에러 상태 메시지 테이블)를 그대로 가져왔고, Go 로 옮기면서 실제로 컴파일되고 테스트가 도는 형태로 만들었습니다.

3.1 변경 증폭 — 같은 값이 여러 곳에#

원칙. 설계 결정 하나가 영향을 주는 코드의 양을 줄입니다.

Before. 페이지 세 개가 각자 배너 색을 박아 넣고 있습니다.

func HomePage() string {
	return fmt.Sprintf(`<div style="background:#336699">%s</div>`, "Home")
}

func AboutPage() string {
	return fmt.Sprintf(`<div style="background:#336699">%s</div>`, "About")
}

func ContactPage() string {
	// 강조 문구는 배너보다 조금 어두운 색으로.
	return fmt.Sprintf(`<div style="background:#336699"><b style="color:#224466">%s</b></div>`,
		"Contact")
}

무엇이 문제인가. 배너 색을 바꾸려면 페이지를 전부 찾아서 고쳐야 합니다. 페이지가 셋이면 귀찮은 정도지만, 책의 표현대로 수천 페이지짜리 사이트라면 사실상 불가능합니다. 그리고 세 번째 페이지에는 더 나쁜 것이 숨어 있습니다. 강조색 #224466 은 배너색을 어둡게 만든 값인데, 그 관계가 코드 어디에도 적혀 있지 않습니다. 배너색을 바꾼 사람이 강조색도 바꿔야 한다는 사실을 알 방법이 없습니다. 변경 증폭 안에 모름이 들어 있는 셈입니다.

After. 색을 한 곳에 두고, 파생되는 색은 계산합니다.

// Color 는 RGB 색상입니다.
type Color struct{ R, G, B uint8 }

// Hex 는 CSS 에 쓰는 "#rrggbb" 표기를 돌려줍니다.
func (c Color) Hex() string { return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B) }

// Darker 는 강조용으로 각 채널을 2/3 로 줄인 색을 돌려줍니다.
func (c Color) Darker() Color {
	return Color{uint8(int(c.R) * 2 / 3), uint8(int(c.G) * 2 / 3), uint8(int(c.B) * 2 / 3)}
}

// BannerBg 는 모든 페이지 배너의 배경색입니다. 여기 한 곳만 바꾸면 됩니다.
var BannerBg = Color{0x33, 0x66, 0x99}

func ContactPage() string {
	return fmt.Sprintf(`<div style="background:%s"><b style="color:%s">%s</b></div>`,
		BannerBg.Hex(), BannerBg.Darker().Hex(), "Contact")
}

무엇이 달라졌나. 배너색을 바꾸면 강조색이 따라옵니다. 테스트로 확인했습니다. BannerBg 를 #993333 으로 바꾸면 ContactPage 의 강조색이 #662222 로 나옵니다.

의존성이 사라진 것은 아닙니다. 책도 그 점을 짚습니다. 페이지들끼리 서로 같은 값을 가져야 한다는 보이지 않는 의존성 이, 모든 페이지가 BannerBg 하나를 참조한다는 보이는 의존성 으로 바뀌었습니다. 이름으로 검색하면 사용처가 전부 나오고, 이름을 바꾸면 컴파일러가 빠뜨린 곳을 알려 줍니다. 좋은 설계는 의존성을 없애는 것이 아니라 단순하고 눈에 보이게 만드는 것입니다.

여담으로, Darker 의 첫 초안은 c.R * 2 / 3 이었습니다. uint8 끼리의 곱이라 0x99 * 2 에서 wrap 이 나서 테스트가 깨졌습니다. int 로 올려서 계산하도록 고친 것이 위 코드입니다. 이 시리즈의 모든 코드는 실제로 돌려 본 것이고, 이런 수정 이력이 있으면 밝히겠습니다.

3.2 인지 부하 — 호출자가 알아야 할 것이 많다#

원칙. 작업 하나를 끝내기 위해 개발자가 알아야 하는 양을 줄입니다.

책의 예는 C 함수가 메모리를 할당해서 돌려주고, 해제는 호출자에게 맡기는 경우입니다. Go 에는 GC 가 있으니 그대로 옮길 수 없지만, 같은 구조는 파일 핸들과 버퍼 에서 그대로 나옵니다.

Before. 키와 파일 오프셋의 매핑을 텍스트 파일에 덧붙이는 작은 인덱스입니다.

// Open 은 인덱스 파일을 엽니다. 호출자는 사용이 끝나면 반드시 Close 를 불러야 하고,
// Close 전에 Flush 를 부르지 않으면 버퍼에 남은 항목이 유실됩니다.
func Open(path string) (*Index, error) {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return nil, err
	}
	return &Index{f: f, w: bufio.NewWriter(f)}, nil
}

// Add 는 항목 하나를 버퍼에 씁니다. 디스크에는 Flush 때 내려갑니다.
func (ix *Index) Add(key string, off int64) {
	fmt.Fprintf(ix.w, "%s\t%d\n", key, off)
}

// Flush 는 버퍼를 파일로 내립니다.
func (ix *Index) Flush() error { return ix.w.Flush() }

// Close 는 파일을 닫습니다. Flush 는 하지 않습니다.
func (ix *Index) Close() error { return ix.f.Close() }

무엇이 문제인가. 주석이 이미 자백하고 있습니다. 이 API 를 쓰는 사람은 세 가지를 알아야 합니다. Close 를 불러야 한다, Close 전에 Flush 를 불러야 한다, 그리고 그 순서를 틀리면 에러 없이 조용히 데이터가 사라진다. 세 번째가 결정적입니다. 테스트로 재현하면 Flush 없이 Close 한 뒤 파일을 읽으면 0 바이트입니다. 컴파일러도, 런타임도 아무 말을 하지 않습니다.

이것은 문서화로 해결되는 문제가 아닙니다. 위 주석은 충분히 친절합니다. 그런데도 실수는 납니다. 알아야 할 것이 많은 API 는 그 자체로 버그의 원인입니다.

After. 열고, 쓰고, 내리고, 닫는 순서를 모듈이 책임집니다.

// Index 는 키 → 파일 오프셋 매핑입니다. Update 안에서만 쓸 수 있습니다.
type Index struct{ w *bufio.Writer }

// Add 는 항목 하나를 추가합니다.
func (ix *Index) Add(key string, off int64) {
	fmt.Fprintf(ix.w, "%s\t%d\n", key, off)
}

// Update 는 path 의 인덱스를 열고 fn 을 실행한 뒤, 버퍼를 내리고 파일을 닫습니다.
// fn 이 정상 반환하면 fn 안에서 추가한 항목은 모두 디스크에 있습니다.
func Update(path string, fn func(*Index)) (err error) {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); err == nil {
			err = cerr
		}
	}()
	w := bufio.NewWriter(f)
	fn(&Index{w: w})
	return w.Flush()
}

호출하는 쪽은 이렇게 됩니다.

err := index.Update(path, func(ix *index.Index) {
	ix.Add("alice", 0)
	ix.Add("bob", 128)
})

무엇이 달라졌나. 호출자가 알아야 할 것이 하나로 줄었습니다. “Update 가 에러 없이 돌아오면 다 저장된 것입니다.” Flush 와 Close 는 인터페이스에서 사라졌습니다. 호출 순서를 틀릴 방법 자체가 없습니다.

Go 에서 이 형태는 낯설지 않습니다. filepath.WalkDir, sync.Once.Do, t.Run 이 모두 “자원의 수명을 함수 하나가 감싸는” 모양입니다. 호출자에게 핸들을 넘겨주고 정리를 맡기는 것보다, 콜백을 받아서 정리까지 해 주는 쪽이 인지 부하가 낮습니다.

3.3 모름 — 고쳐야 할 곳이 어딘지 알 수 없다#

원칙. 작업에 필요한 정보가 코드 안에서 드러나게 합니다.

책의 예는 이렇습니다. 시스템에 새 에러 상태를 추가하면 다른 곳의 메시지 테이블에도 항목을 넣어야 하는데, 상태 선언부만 보는 사람은 그 테이블이 있다는 것을 모릅니다.

Before. 상태 코드와 메시지가 다른 파일에 있습니다.

// status.go
type Status int

const (
	OK Status = iota
	NotFound
	Timeout
	RateLimited // 새로 추가
)
// messages.go (다른 파일)
var messages = map[Status]string{
	OK:       "ok",
	NotFound: "not found",
	Timeout:  "timed out",
}

// Message 는 사용자에게 보여 줄 문구를 돌려줍니다.
func (s Status) Message() string { return messages[s] }

무엇이 문제인가. RateLimited 를 추가한 사람은 status.go 만 봤습니다. 컴파일은 됩니다. RateLimited.Message() 는 빈 문자열을 돌려줍니다. map 에 없는 키는 zero value 를 주는 것이 Go 의 정상 동작이라 패닉도 없습니다. 사용자 화면에 빈 에러 문구가 뜰 때까지 아무도 모릅니다. 테스트로 확인하면 정확히 "" 가 나옵니다.

이것이 “모름” 입니다. 고쳐야 할 곳이 하나 더 있는데, 그것을 알 방법이 선언부에 없습니다.

After. 코드와 메시지를 떨어뜨릴 수 없게 만듭니다.

// Status 는 요청 처리 결과입니다. 코드와 사용자 문구를 항상 함께 정의합니다.
type Status struct {
	code int
	msg  string
}

var (
	OK          = Status{0, "ok"}
	NotFound    = Status{1, "not found"}
	Timeout     = Status{2, "timed out"}
	RateLimited = Status{3, "rate limited"} // 문구 없이는 만들 수 없다
)

// Code 는 숫자 코드를 돌려줍니다.
func (s Status) Code() int { return s.code }

// Message 는 사용자에게 보여 줄 문구를 돌려줍니다.
func (s Status) Message() string { return s.msg }

무엇이 달라졌나. 새 상태를 추가하는 사람은 메시지를 안 쓸 수가 없습니다. 선언 자리가 곧 메시지 자리입니다. 메시지 테이블이라는 두 번째 장소가 없어졌으니 “그 장소를 모른다” 는 문제도 없어졌습니다.

Go 에서 iota 열거형은 편하지만, 열거값에 딸린 데이터 가 생기는 순간 위와 같은 병렬 테이블이 자라기 시작합니다. 메시지, HTTP 상태 코드, 재시도 가능 여부 같은 것들입니다. 그럴 때는 int 대신 작은 struct 로 바꾸는 편이 “모름” 을 줄입니다. switch 로 전수 검사를 하는 방법도 있지만, 컴파일러가 빠진 case 를 잡아 주지 않는 Go 에서는 struct 쪽이 확실합니다.


4. 전술적 프로그래밍과 전략적 프로그래밍 (3장)#

2장이 복잡성의 해부라면 3장은 그것이 자라는 습관 에 대한 이야기입니다.

책은 두 가지 태도를 구분합니다. 전술적 프로그래밍(tactical programming) 은 지금 맡은 기능이나 버그 수정을 최대한 빨리 끝내는 데 집중합니다. 그 과정에서 “약간의 복잡성” 이나 “작은 땜질 한두 개” 를 허용합니다. 전략적 프로그래밍(strategic programming) 은 돌아가는 코드가 목표가 아니라는 데서 출발합니다.

“Working code isn’t enough.” (3.2절)

전략적 프로그래밍의 핵심은 투자입니다. 책은 개발 시간의 10~20% 를 설계 개선에 계속 쓰라고 권합니다. 처음에는 그만큼 느리지만, 복잡성이 덜 쌓이므로 몇 달 안에 오히려 빨라진다는 것입니다. 다만 저자는 이 곡선(책의 Figure 3.1)이 정성적인 그림 이며 정확한 형태를 실측한 데이터를 알지 못한다고 스스로 밝혀 둡니다. 경험에서 나온 주장이지 측정된 사실은 아닙니다.

두 태도의 차이는 같은 버그 하나를 어떻게 고치는지에서 가장 잘 드러납니다.

4.1 전술적 수정 — 호출처마다 땜질#

Before. 태그 문자열을 파싱하는 함수와 그것을 쓰는 세 곳입니다.

// ParseTags 는 "go,design,book" 같은 문자열을 태그 목록으로 바꿉니다.
func ParseTags(csv string) []string {
	return strings.Split(csv, ",")
}

// 호출처 1: 검색 필터
func Matches(csv, want string) bool {
	tags := ParseTags(csv)
	if len(tags) == 1 && tags[0] == "" { // 빈 입력 땜질
		return false
	}
	for _, t := range tags {
		if t == want {
			return true
		}
	}
	return false
}

// 호출처 2: 화면 표시
func Render(csv string) string {
	tags := ParseTags(csv)
	if len(tags) == 1 && tags[0] == "" { // 같은 땜질 복사
		return "(no tags)"
	}
	return "#" + strings.Join(tags, " #")
}

// 호출처 3: 통계 — 아직 땜질하지 않은 곳
func Count(csv string) int {
	return len(ParseTags(csv))
}

무엇이 문제인가. 원인은 Go 의 strings.Split("", ",") 이 빈 슬라이스가 아니라 [""] 를 돌려준다는 데 있습니다. 태그가 없는 글에서 빈 태그가 하나 잡힌다는 버그 리포트가 들어왔고, 검색 필터에서 땜질했습니다. 화면 표시에서 또 들어와서 같은 땜질을 복사했습니다. 통계 쪽은 아직 아무도 신고하지 않았을 뿐입니다. 테스트로 확인하면 Count("") 는 1 을 돌려줍니다.

땜질 하나하나는 합리적으로 보입니다. 두 줄이고, 그 자리에서 문제가 사라집니다. 그러나 이제 이 코드베이스에는 “태그 파싱 결과는 빈 문자열 하나일 수 있다” 는 지식이 호출처마다 흩어져 있습니다. 새 호출처를 추가하는 사람은 그 지식을 알아야 하고(인지 부하), 알 방법이 없습니다(모름). 파싱 규칙을 바꾸면 땜질을 전부 찾아 고쳐야 합니다(변경 증폭). 2장의 세 증상이 한 자리에 모였습니다.

책은 이런 사람을 “tactical tornado” 라고 부릅니다. 누구보다 빨리 기능을 내놓지만, 뒤에 남는 것은 다른 사람이 치워야 할 잔해입니다.

4.2 전략적 수정 — 원인이 있는 곳에서 고친다#

After. 문제가 있는 곳은 파싱 함수이므로 거기서 고칩니다.

// ParseTags 는 "go, design ,book" 같은 문자열을 태그 목록으로 바꿉니다.
// 빈 입력이나 공백만 있는 입력은 빈 목록이 됩니다. 각 태그의 앞뒤 공백은 지웁니다.
func ParseTags(csv string) []string {
	var tags []string
	for _, t := range strings.Split(csv, ",") {
		if t = strings.TrimSpace(t); t != "" {
			tags = append(tags, t)
		}
	}
	return tags
}

func Matches(csv, want string) bool {
	for _, t := range ParseTags(csv) {
		if t == want {
			return true
		}
	}
	return false
}

func Render(csv string) string {
	tags := ParseTags(csv)
	if len(tags) == 0 {
		return "(no tags)"
	}
	return "#" + strings.Join(tags, " #")
}

func Count(csv string) int { return len(ParseTags(csv)) }

무엇이 달라졌나. 호출처 세 곳에서 땜질이 사라졌습니다. Render 에 남은 len(tags) == 0 은 땜질이 아니라 “태그가 없을 때 뭘 보여 줄지” 라는 화면의 고유한 결정입니다. Count("") 는 0 입니다. 그리고 고치는 김에 앞뒤 공백도 처리했습니다. " , " 같은 입력도 빈 목록이 됩니다. 원래 버그 리포트에는 없던 요구지만, 파싱 함수의 계약을 “빈 태그는 결과에 없다” 로 정하고 나면 자연스럽게 따라오는 것입니다.

이것이 책이 말하는 반응적 투자(reactive investment) 입니다. 설계 문제를 발견했을 때 그 자리에서 우회하지 않고, 약간의 시간을 더 써서 문제 자체를 고치는 것입니다. 이번 수정은 전술적 땜질보다 몇 분 더 걸렸습니다. 대신 네 번째 호출처를 만드는 사람은 아무것도 알 필요가 없습니다.

한 가지 짚어 둘 것이 있습니다. Before 와 After 의 차이는 코드 줄 수가 아닙니다. After 의 ParseTags 가 더 깁니다. 책은 2장에서 “줄 수로 복잡성을 재는” 습관을 경계합니다. 줄이 늘어도 호출자가 알아야 할 것이 줄면 그쪽이 단순한 것입니다.


5. 이 시리즈의 구성#

Part책의 장Before → After 로 보여줄 것
11~3장복잡성의 세 증상과 두 원인. 전술적 땜질 → 전략적 수정 (이 글)
24~5장얕은 wrapper → 깊은 함수. 노출된 필드 → 은닉. 시간적 분해 → 캡슐화
36~8장특수 목적 API → 범용. pass-through 메서드 → 계층 재정렬. 설정 파라미터 → 기본값
49·11장잘게 쪼갠 함수 → 응집된 메서드. 범용/특수 분리. 설계 A 와 B 를 둘 다 써 보고 판정
510장if err != nil 홍수. 없는 파일 삭제. 마스킹·집계·크래시의 Go 판
612~15장코드를 반복하는 주석 → 정밀성·직관. block 이라는 이름의 버그. 주석 먼저 쓰기
716~18장최소 변경 패치 → 설계 개선. 에러 패턴 통일. 범용 컨테이너 → 이름 있는 struct
819~21장struct embedding 남용 → 인터페이스. TDD 비판과 반론. 측정 후 핵심 경로. 총정리

2편부터 5편까지는 예제 일부가 같은 도메인을 공유합니다. 파일 저장소를 가진 작은 노트 서비스입니다. 원칙마다 같은 코드를 다른 각도에서 고치는 편이 “이 원칙은 앞의 원칙과 무엇이 다른가” 를 보여 주기 좋습니다. 다만 억지로 끼워 맞추지는 않고, 독립 예제가 나은 곳은 독립 예제를 씁니다.


References#

1차 자료

  • Ousterhout, J. K. A Philosophy of Software Design, 1st ed. Yaknyam Press, 2018. ISBN 978-1-7321022-0-0. 본문 인용(2.1, 2.2, 2.3, 3.1~3.3, 4.4절)은 1판 원문을 직접 옮긴 것입니다.
  • Ousterhout, J. K. A Philosophy of Software Design, 2nd ed. 2021. ISBN 173210221X. 장 번호는 2판을 따릅니다.
  • 저자의 책 소개 페이지 — 2판 출간 시점(2021년 7월)과 2판의 변경 사항(21장 신설, 6장 재편, Clean Code 비교 절 추가)의 출처입니다.
  • 저자 홈페이지 — 이력(Tcl/Tk, Sprite, 로그 구조 파일 시스템, RAMCloud, Raft, 은퇴 상태)의 출처입니다.
  • Stanford CS 190: Software Design Studio — 책의 배경이 된 과목입니다.

본문의 코드

  • 모든 Go 코드는 go version go1.26.0 darwin/arm64 에서 go vet 과 테스트를 통과했습니다. Before 코드의 결함(Flush 누락 시 0 바이트, RateLimited.Message() 가 "", Count("") 가 1)은 각각 테스트로 재현한 결과입니다.
  • 한국어 번역서의 존재는 2026-09-16 알라딘 검색으로 확인을 시도했으나 찾지 못했습니다.

시리즈