책 소개: 소프트웨어 설계의 철학
존 오스터하우트 (John Ousterhout) 의 소프트웨어 설계의 철학 - A Philosophy of Software Design 은 소프트웨어 복잡성을 줄이고 유지 관리가 용이한 코드를 작성하는 방법에 대한 심도 있는 통찰력을 제공합니다. 21개 챕터에 걸쳐 저자는 복잡성의 본질을 분석하고, 효과적인 모듈 설계를 위한 원칙을 제시하며, 코드의 가독성과 명확성을 높이는 구체적인 기법들을 소개합니다.
아래 내용은 Gemini 의 연구 결과에 약간의 퇴고를 거친 것입니다.
서론: 복잡성과의 전쟁#
존 오스터하우트의 저서 “소프트웨어 설계의 철학(A Philosophy of Software Design)“은 소프트웨어 개발의 가장 근본적인 한계가 성능이나 기능이 아니라, 우리가 만든 시스템을 우리 스스로 이해하는 능력에 있다는 주장으로 시작합니다.[1, 2] 이 책의 전체 철학은 단 하나의 적, 즉 복잡성(complexity) 에 맞서기 위한 일련의 전략들로 구성되어 있습니다.[3, 4, 5, 6]
저자는 복잡성과의 싸움에서 두 가지 핵심적인 접근법을 제시하며, 이는 책 전체를 관통하는 프레임워크 역할을 합니다.
- 코드를 더 단순하고 명확하게 만들어 복잡성을 제거하는 방법.[1, 7]
- 복잡성을 모듈 안에 캡슐화하여, 개발자가 시스템의 모든 복잡성에 한 번에 노출되지 않고도 작업할 수 있게 하는 방법.[1, 8, 9]
이 책은 코드 라인 수준의 세부 사항에 초점을 맞춘 클린 코드(Clean Code) 와 시스템 전체의 아키텍처를 다루는 클린 아키텍처(Clean Architecture) 사이의 실용적인 지침서로 자리매김합니다.[10] 모든 설계 결정의 유일한 척도로서 ‘복잡성’이라는 개념에 거의 전적으로 집중한다는 점에서 독특한 관점을 제공합니다.[11, 12]
제 1부: 철학적 기반 (1-3장)#
1.1 적의 정의: 복잡성의 본질 (1-2장)#
복잡성이란 무엇인가?#
오스터하우트는 복잡성을 “소프트웨어 시스템의 구조와 관련된 것으로, 시스템을 이해하고 수정하기 어렵게 만드는 모든 것"으로 정의합니다.[4, 13, 14] 이 복잡성은 코드를 작성하는 사람보다 읽는 사람에게 더 명확하게 드러납니다. 만약 다른 사람이 당신의 코드를 복잡하다고 느낀다면, 그 코드는 복잡한 것입니다.[13, 15]
이 책에서 강조하는 중요한 통찰 중 하나는 복잡성이 한 번에 나타나지 않는다는 점입니다. 복잡성은 수천 개의 작고 사소해 보이는 결정들이 시간이 지남에 따라 축적된 결과물이며, 이로 인해 나중에 제거하기가 매우 어렵습니다.[3, 4, 16, 17] 따라서 새로운 복잡성을 추가하는 것에 대해 “무관용(zero tolerance)” 철학을 채택해야 합니다.[4]
복잡성의 증상 (무엇이 문제인가)#
복잡성은 시스템에서 세 가지 주요 증상으로 나타납니다.
- 변경 증폭 (Change Amplification): 간단해 보이는 논리적 변경 하나가 코드베이스의 여러 곳, 심지어 서로 관련 없어 보이는 부분까지 수정을 요구하는 현상입니다.[1, 3, 15, 18]
- 높은 인지 부하 (High Cognitive Load): 개발자가 안전하게 코드를 변경하기 위해 머릿속에 엄청난 양의 정보를 담고 있어야 하는 상태를 의미합니다.[1, 3, 12, 15, 18] 때로는 “똑똑하고” 간결한 코드보다 장황하더라도 명시적인 코드가 인지 부하를 줄여주기 때문에 더 단순할 수 있습니다.[1]
- 미지의 불확실성 (Unknown Unknowns): 가장 위험한 증상으로, 작업을 완료하기 위해 어떤 코드를 수정해야 하는지, 또는 어떤 정보가 필요한지조차 명확하지 않은 상태입니다. 문제는 변경이 이루어지고 버그가 발생한 후에야 발견됩니다.[1, 3, 13, 15, 19]
복잡성의 원인 (왜 문제인가)#
이러한 증상들은 두 가지 근본적인 원인에서 비롯됩니다.
- 의존성 (Dependencies): 의존성은 특정 코드를 격리된 상태에서 이해하고 수정할 수 없을 때 존재합니다.[1, 13, 14, 20] 의존성은 소프트웨어에서 피할 수 없는 부분이지만, 좋은 설계의 목표는 (1) 의존성의 수를 줄이고, (2) 남아있는 의존성을 단순하고 명확하게 만드는 것입니다.[1, 8]
- 모호함 (Obscurity): 중요한 정보가 명확하게 드러나지 않을 때 발생합니다.[1, 8, 13, 14, 19] 이는 종종 의존성과 연관되어, 의존성의 존재 자체가 모호한 경우를 포함합니다. 방대한 양의 문서는 오히려 설계 자체가 명확하지 않다는 위험 신호(red flag)일 수 있습니다.[1]
이 원인과 증상 사이의 관계는 개발자들이 일상적으로 겪는 고통을 설명하는 직접적인 인과 사슬을 형성합니다. 개발자가 숨겨진 의존성을 만들거나 모호한 코드를 작성하면(원인), 이것들이 점진적으로 쌓여 시스템의 여러 부분을 얽히게 만듭니다. 미래의 개발자는 새로운 기능을 추가하기 위해 이 얽힌 거미줄을 먼저 이해해야 하므로 높은 인지 부하를 겪게 됩니다. 의존성이 숨겨져 있기 때문에 필요한 모든 것을 알 수 없어 미지의 불확실성에 직면하게 되고, 한 곳의 변경이 예기치 못한 파급 효과를 일으켜 다른 곳의 수정을 강요하는 변경 증폭으로 이어집니다. 이러한 관점은 “나쁜 코드"를 개인의 도덕적 실패가 아닌, 예측 가능하고 고통스러운 증상을 가진 구조적 문제로 재정의합니다. 따라서 설계의 목표는 추상적인 의미의 “깨끗함"이 아니라, 근본 원인을 공격하여 이러한 특정 증상들을 직접적으로 완화하는 것이 됩니다.
1.2 투자로서의 사고방식: 전략적 프로그래밍 vs 전술적 프로그래밍 (3장)#
전술적 프로그래밍 (Tactical Programming)#
- 정의: 많은 개발자들의 기본 사고방식으로, 기능 구현이나 버그 수정을 가능한 한 빨리 완료하는 데에만 초점을 맞춥니다.[1, 5, 6, 14] 이는 장기적인 시스템의 건강보다 즉각적인 결과를 우선시하는 근시안적인 접근법입니다.[1]
- 전술적 토네이도 (Tactical Tornado): 오스터하우트가 제시하는 핵심적인 인물 유형입니다. 이들은 엄청난 속도로 코드를 생산하지만, 순전히 전술적인 방식으로만 작업하는 개발자입니다. 경영진에게는 영웅처럼 보일 수 있지만, 그들이 지나간 자리에는 다른 개발자들이 청소해야 할 복잡성과 기술 부채의 폐허만 남게 됩니다.[1, 5, 7]
전략적 프로그래밍 (Strategic Programming)#
- 정의: 전술적 프로그래밍에 대한 대안 철학입니다. 여기서는 ‘작동하는 코드’만으로는 충분하지 않습니다. 가장 중요한 목표는 훌륭한 설계를 만들어내는 것이며, 그 설계는 우연히도 잘 작동해야 합니다.[1, 5, 18] 이를 위해서는 투자로서의 사고방식(investment mindset) 이 필요합니다.[1, 8]
- 10-20% 규칙: 저자는 구체적인 권장 사항을 제시합니다. 개발자는 전체 개발 시간의 약 10-20%를 작고 지속적인 설계 개선에 투자해야 합니다.[1, 9, 21] 이는 거창하고 선행적인 폭포수 모델의 설계를 의미하는 것이 아니라, 지속적이고 점진적인 리팩토링과 개선을 의미합니다.[1, 2, 16]
- 보상: 처음에는 더 느리게 보일 수 있지만, 이러한 투자는 복잡성의 축적을 막아 미래의 변경 작업을 더 빠르고 저렴하게 만들어주므로 빠르게 그 가치를 회수하게 됩니다.[1, 15, 21]
“전략적 vs 전술적"이라는 이분법은 이 책의 핵심적인 윤리적 프레임워크를 제공합니다. 이는 개발자가 책의 다른 모든 원칙들을 적용하는 데 시간을 할애해야 하는 정당성을 부여합니다. 예를 들어, 왜 개발자는 ‘깊은 모듈’(4장)을 만들거나 ‘에러의 존재를 없애는’(10장) 데 추가 시간을 써야 할까요? 속도를 최적화하는 전술적 프로그래머는 이 추가 작업을 거부할 것입니다. 하지만 전략적 프로그래머는 이를 ‘투자’로 봅니다. 지금 인터페이스를 단순화하는 데 들이는 추가 시간은, 미래에 그 코드를 다룰 모든 개발자들의 인지 부하와 변경 증폭을 줄여주기 때문에 정당화됩니다. 이는 “그냥 빨리 출시하라"는 비즈니스 압박에 정면으로 맞서는 논리입니다. 오스터하우트는 전술적 프로그래밍이 잘못된 경제학이라고 주장합니다. 전략적 프로그래밍의 “더 빨리 가기 위해 잠시 속도를 늦추는” 접근법은 사치가 아니라, 지속 가능한 개발을 위한 필수적인 훈련으로 제시됩니다. 이는 기술 부채를 나중에 갚아야 할 빚이 아니라, 지속적인 소규모 투자를 통해 적극적으로 예방해야 할 대상으로 재구성합니다.[8]
제 2부: 모듈식 설계의 원칙 (4-11장)#
2.1 깊은 모듈: 추상화의 초석 (4-5장)#
4장: 모듈은 깊어야 한다 (Modules Should Be Deep)#
- 핵심 개념: 이 책에서 가장 유명한 아이디어일 것입니다. 최고의 모듈은 깊습니다(deep). 즉, 방대하고 복잡한 기능을 단순한 인터페이스 뒤에 숨깁니다.[3, 6, 8, 10] 목표는 인터페이스의 복잡성 대비 기능성의 비율을 극대화하는 것입니다.[3]
- 인터페이스 vs 구현: 모듈은 두 부분으로 구성됩니다. 사용자가 모듈을 사용하기 위해 알아야 할 모든 것인 인터페이스와, 모듈이 작동하게 만드는 코드인 구현입니다.[8, 9] 인터페이스에는 메서드 시그니처와 같은 형식적인 측면과, 고수준의 동작, 사용 제약, 성능 특성 등 비공식적인 측면이 모두 포함됩니다.[3, 7, 9]
- 위험 신호 - 얕은 모듈 (Shallow Modules): 깊은 모듈의 반대 개념입니다. 제공하는 기능에 비해 인터페이스가 복잡한 모듈을 말합니다.[3, 14] 이들은 숨기는 복잡성보다 더 많은 복잡성을 추가합니다. 오스터하우트는 클래스와 메서드는 항상 작아야 한다는 통념을 명시적으로 비판하는데, 이는 종종 수많은 얕은 모듈의 확산(“클래스병, classitis”)으로 이어지기 때문입니다.[7, 10, 14] 전달 메서드(pass-through method)가 그 전형적인 예입니다.[7]
5장: 정보 은닉 (그리고 누출) (Information Hiding (and Leakage))#
- 정보 은닉 (Information Hiding): 깊은 모듈을 만드는 주요 기술입니다. 핵심적인 설계 결정과 지식을 모듈 내부에 캡슐화하여 외부 세계에 보이지 않도록 하는 것입니다.[3, 6, 8] 더 많은 정보가 숨겨질수록 인터페이스는 더 단순해지고 모듈은 더 깊어집니다.
- 위험 신호 - 정보 누출 (Information Leakage): 정보 은닉의 반대 개념입니다. 구현 세부 사항이 인터페이스를 통해 “누출"되어 모듈 사용자가 이를 인지하도록 강요할 때 발생합니다.[7, 16] 이는 모듈 간의 의존성을 만듭니다.
- 시간적 분해 (Temporal Decomposition): 정보 누출의 흔한 원인입니다. 이는 코드의 구조가 작업 순서를 그대로 반영하여, 여러 모듈이 공유된 저수준 데이터 구조를 모두 인지하게 되는 경우입니다.[6, 7, 20] 해결책은 종종 이 여러 단계의 작업을 모두 캡슐화하는 약간 더 큰 클래스를 만드는 것입니다.[7, 20]
“깊은 모듈"은 “전략적 프로그래밍” 철학이 구조적으로 발현된 형태입니다. 이는 복잡성을 관리하려는 목표를 구현하는 방법입니다. 인지 부하와 의존성을 최소화하는 것이 목표일 때(2장), 복잡한 인터페이스를 가진 얕은 모듈은 호출자의 인지 부하를 증가시킵니다. 구현 세부 사항을 노출하는 누출이 있는 모듈은 호출자와의 강한 의존성을 만듭니다. 따라서 공격적인 정보 은닉을 통해 깊은 모듈을 만드는 것은 이 책의 철학적 목표를 달성하기 위한 주요 구조적 기법입니다. 이는 복잡성의 근본 원인을 직접적으로 공격합니다. 이는 설계 리뷰에 강력하고 구체적인 척도를 제공합니다. “이 코드는 깨끗한가?“라는 모호한 질문 대신, 팀은 “이 모듈은 깊은가? 인터페이스에서 노출하는 복잡성에 비해 얼마나 많은 복잡성을 숨기고 있는가?“라고 물을 수 있습니다. 이는 대화를 주관적인 스타일에서 객관적인 구조적 이점으로 전환시킵니다.
2.2 우수한 추상화 만들기 (6-8장)#
6장: 범용 모듈이 더 깊다 (General-Purpose Modules are Deeper)#
- “어느 정도 범용적인” 최적점: 오스터하우트는 지나치게 구체적이거나 지나치게 일반적인 설계를 모두 피하라고 조언합니다. 이상적인 것은 “어느 정도 범용적인(somewhat general-purpose)” 모듈을 만드는 것입니다. 구현은 현재의 특정 문제를 해결해야 하지만, 인터페이스는 과도하게 설계되지 않으면서도 미래의 다양한 사용 사례를 수용할 수 있도록 약간 더 일반적인 관점에서 설계되어야 합니다.[4, 7, 9, 22]
- 자문해야 할 질문들: 이 균형을 찾기 위해 다음과 같이 자문해야 합니다: “현재의 모든 요구를 충족하는 가장 간단한 인터페이스는 무엇인가?”, “이 메서드는 얼마나 많은 상황에서 사용될 것인가?”, “이 API는 현재 내 필요에 사용하기 쉬운가?”.[4, 6] 만약 현재 필요에 API를 사용하기 어렵다면, 그것은 아마도 지나치게 범용적인 것입니다.
7장: 다른 계층, 다른 추상화 (Different Layer, Different Abstraction)#
- 핵심 원칙: 소프트웨어 스택에서 인접한 계층들은 서로 다른 추상화를 제공해야 합니다. 만약 두 계층이 매우 유사한 인터페이스를 가지고 있다면, 이는 클래스 분해가 잘못되었고 불필요한 복잡성을 추가하고 있다는 신호입니다.[4, 7, 19]
- 위험 신호 - 전달 메서드/변수 (Pass-through Methods/Variables): 이는 잘못된 계층화의 강력한 지표입니다. 전달 메서드는 거의 동일한 시그니처를 가진 다른 메서드를 호출하기만 할 뿐, 실질적인 기능 추가 없이 인터페이스 복잡성만 증가시킵니다.[7, 9] 전달 변수는 여러 계층을 거쳐 아래로 전달되는 변수로, 중간 계층에 필요 없는 지식을 부담시킵니다.[7, 9]
8장: 복잡성을 아래로 끌어내려라 (Pull Complexity Downwards)#
- 황금률: 모듈은 단순한 구현보다 단순한 인터페이스를 갖는 것이 더 중요합니다.[4, 6, 9]
- 고통 감수하기: 이는 모듈 개발자가 “사용자들의 고통을 줄이기 위해 스스로 약간의 추가적인 고통을 감수해야 한다"는 것을 의미합니다.[9, 18] 모듈의 많은 사용자들이 고생하지 않도록 복잡성을 모듈 내부에서 처리해야 합니다.
- 예시 - 설정: 수많은 설정 매개변수를 노출하여 사용자가 이를 파악하도록 강요하는 대신, 모듈은 내부적으로 합리적인 기본값을 계산하여 그 복잡성을 숨겨야 합니다.[6, 9]
6, 7, 8장은 4장에서 제시된 “깊은 모듈"이라는 이상을 달성하기 위한 실용적인 규칙들의 집합체입니다. 즉, “어떻게"에 대한 가이드입니다. 4장은 “모듈을 깊게 만들어라"라고 말합니다. 8장은 “어떻게? 복잡성을 인터페이스에서 구현으로 끌어내림으로써"라고 답합니다. 7장은 “실패의 징후는 무엇인가? 인접한 계층이 동일한 추상화를 가질 때(예: 전달 메서드)“라고 경고합니다. 이는 복잡성을 성공적으로 아래로 끌어내려 새롭고 더 간단한 추상화를 만들지 못했다는 의미입니다. 6장은 “이 작업을 하면서, 모듈의 가치를 극대화하기 위해 범용 인터페이스를 목표로 하되, 과용하지는 말라"는 뉘앙스를 더합니다. 이 장들은 높은 수준의 철학을 구체적이고 실행 가능한 질문으로 전환시켜 줍니다. 개발자는 클래스를 수정하면서 “이 복잡성을 아래로 끌어내릴 수 있는가? 나는 그저 전달 메서드를 만들고 있는 것은 아닌가? 이 인터페이스는 부풀려지지 않으면서도 충분히 범용적인가?“라고 자문할 수 있습니다.
2.3 로직 구조화와 에러 길들이기 (9-11장)#
9장: 함께하는 것이 나은가, 떨어지는 것이 나은가? (Better Together Or Better Apart?)#
- 핵심 트레이드오프: 이 장은 기능(클래스 또는 메서드)을 합칠 것인지 분리할 것인지에 대한 근본적인 설계 결정을 다룹니다.[6, 7] 목표는 항상 전체 시스템의 복잡성을 최소화하는 것입니다.[9]
- 합쳐야 할 경우: 코드 조각들이 정보를 공유하거나, 합치는 것이 전체 인터페이스를 단순화하거나, 중복을 제거할 수 있다면 함께 묶어야 합니다.[6, 7, 9]
- 분리해야 할 경우: 범용 코드는 특수 목적 코드와 분리되어야 합니다.[7, 10, 19] 이 둘이 섞여 있는 것은 위험 신호입니다.
- 메서드 길이에 관하여: 오스터하우트는 개발자들이 메서드를 너무 많이 분리하는 경향이 있다고 주장합니다. 긴 메서드라도 응집도가 높고 시그니처가 단순하다면 본질적으로 나쁘지 않습니다. 목표는 메서드가 “한 가지 일을, 그리고 그것을 완벽하게” 수행하는 것입니다.[5, 9, 14, 19] 분리는 임의의 라인 수 규칙을 맞추기 위해서가 아니라, 전체 복잡성을 줄일 경우에만 이루어져야 합니다.
10장: 에러의 존재 자체를 없애라 (Define Errors Out Of Existence)#
- 예외는 복잡성을 더한다: 예외 처리는 복잡성의 최악의 원천 중 하나입니다. 테스트하기 어려운 추가적인 코드 경로를 만들고 인터페이스를 복잡하게 만듭니다.[6, 7, 20, 22] 예외를 던지는 것은 종종 “문제를 호출자에게 떠넘기는” 행위입니다.[20, 22]
- 전략: 에러를 처리하는 가장 좋은 방법은 해당 조건이 더 이상 에러가 아니도록 API를 재정의하는 것입니다.[6, 7, 22, 23]
- 예시: 파일을 삭제하는 메서드는 파일이 존재하지 않을 때 에러를 던져서는 안 됩니다. 원하는 상태(파일이 존재하지 않음)가 달성되었으므로 성공으로 간주해야 합니다.[23, 24]
- 기타 기법: 에러를 정의에서 없앨 수 없다면, 마스킹(저수준에서 처리하여 호출자가 알지 못하게 함), 집계(많은 에러 유형을 하나의 일반적인 핸들러로 처리), 또는 경우에 따라 애플리케이션을 그냥 종료(crash)시키는 방법이 있습니다.[6, 19, 22, 25]
11장: 두 번 설계하라 (Design it Twice)#
- 핵심 원칙: 첫 번째 아이디어가 최선인 경우는 거의 없습니다. 오스터하우트는 중요한 시스템이나 모듈을 최소한 두 번 설계할 것을 명시적으로 권장합니다.[7, 10, 21, 26]
- 이점: 근본적으로 다른 여러 접근법을 탐색하면 문제를 더 깊이 이해하게 되고 각 설계의 장단점을 파악할 수 있습니다.[13, 15] 이는 간단하고 잘 정의된 연습 문제가 아닌 복잡한 문제에 특히 중요합니다.[16] 똑똑한 사람들이 자신의 첫 번째 ‘충분히 좋은’ 아이디어에 집착하는 경향에 대한 직접적인 대응책이기도 합니다.[16]
이 세 장은 코드가 확정되기 전, 설계 단계에서 복잡성을 사전에 관리하는 방법에 관한 것입니다. 복잡성과의 전쟁에서 이는 공격적인 기동 전략에 해당합니다. 9장은 “이 로직을 어떻게 그룹화해야 하는가?“라는 근본적인 구조적 질문을 던집니다. 10장은 “호출자에게 제공할 수 있는 가장 간단하고 견고한 계약은 무엇인가?"(종종 예외를 제거하는 것을 포함)라는 근본적인 인터페이스 질문을 던집니다. 그리고 11장은 이러한 질문들에 효과적으로 답하기 위한 메타 프로세스, 즉 “첫 번째 답에 안주하지 말고 설계 공간을 탐험하라"를 제공합니다. 이 세 장은 코딩 전 설계 의식을 형성합니다. 전략적 프로그래머는 구현에 착수하기 전에 구조(9장), 에러 처리 철학(10장), 그리고 최소한 하나의 대안 설계(11장)를 고려해야 합니다. 이는 3장에서 제시된 “투자로서의 사고방식"을 구체적으로 구현하는 방법입니다.
제 3부: 명확성과 실용적 원칙 (12-21장)#
3.1 문서화의 재정의: 주석과 이름 (12-15장)#
이 책의 후반부는 코드 자체의 명확성을 높이는 구체적인 기술에 초점을 맞춥니다. 오스터하우트는 많은 개발자들이 간과하는 주석과 이름의 중요성을 강조하며, 이를 복잡성을 줄이는 핵심 도구로 제시합니다.
12-13장: 왜, 그리고 무엇을 주석으로 달아야 하는가?#
- “좋은 코드는 스스로를 문서화한다"는 신화: 저자는 이 말을 “맛있는 신화(delicious myth)“라고 부르며 정면으로 반박합니다. 코드는 ‘어떻게’ 작동하는지는 보여줄 수 있지만, 그 뒤에 숨겨진 ‘왜’와 ‘무엇을’은 설명할 수 없습니다. 좋은 주석은 코드에 없는 정보를 제공하여 독자가 설계자의 머릿속에 있던 지식을 최대한 많이 얻도록 돕습니다.
- 주석의 역할:
- 정밀성 추가 (Add Precision): 코드만으로는 모호할 수 있는 세부 사항을 명확히 합니다. 예를 들어,
authors
라는 문자열 배열이 “성, 이름” 형식이어야 하는지, 정렬되어야 하는지 등의 규칙을 설명합니다. - 직관 제공 (Enhance Intuition): 코드의 고수준 의도와 설계 철학을 설명합니다. 이는 독자가 세부 사항에 매몰되기 전에 전체적인 그림을 이해하도록 돕습니다.
- 인터페이스와 구현 분리: 인터페이스 주석(사용자가 알아야 할 것)과 구현 주석(내부 작동 방식)을 명확히 구분해야 합니다.
- 정밀성 추가 (Add Precision): 코드만으로는 모호할 수 있는 세부 사항을 명확히 합니다. 예를 들어,
- 피해야 할 주석: 코드의 내용을 그대로 반복하는 주석은 가치가 없습니다. 주석은 코드와 다른 수준의 추상화를 제공해야 합니다.
14장: 좋은 이름 선택하기#
- 이름은 문서다: 좋은 이름은 그 자체로 강력한 문서화 도구입니다.[1, 2] 이름은 변수나 메서드의 역할을 명확하고 정확하게 전달해야 합니다.[1]
- 정확성과 일관성: 저자는 과거 Sprite 운영체제 개발 중
block
이라는 모호한 이름 때문에 발생했던 심각한 버그를 예로 듭니다. 물리적 디스크 블록과 논리적 파일 블록을 모두block
으로 지칭한 탓에 데이터 손상이 발생했습니다.fileBlock
과diskBlock
처럼 정확한 이름을 사용했다면 버그를 피할 수 있었을 것입니다. 이름은 일관성 있게 사용되어야 하며, 만약 좋은 이름을 짓기 어렵다면 해당 변수나 메서드의 설계 자체에 문제가 있다는 위험 신호일 수 있습니다.
15장: 주석을 먼저 작성하라#
- 설계 도구로서의 주석: 오스터하우트는 코드를 작성하기 전에 주석을 먼저 작성하는 파격적인 접근법을 제안합니다.[3, 4]
- 이점:
- 더 나은 설계: 주석을 먼저 쓰면 인터페이스와 추상화를 더 깊이 고민하게 됩니다. 만약 주석으로 간단히 설명하기 어렵다면, 설계가 너무 복잡하다는 신호입니다.[3, 5]
- 더 나은 주석: 코드를 다 작성한 후에 주석을 달면 귀찮은 작업으로 느껴져 소홀해지기 쉽습니다. 주석을 먼저 작성하면 설계 과정의 일부가 되어 더 충실한 내용이 담깁니다.[4]
- 개발 효율성: 주석은 개발 과정에서 해야 할 일의 목록처럼 기능하여, 구현을 더 체계적으로 진행할 수 있게 돕습니다.[5]
이 장들은 “코드는 명백해야 한다(Code Should be Obvious)“는 더 큰 원칙(18장)의 구체적인 실천 방안입니다. 오스터하우트는 주석과 이름을 단순한 스타일 문제가 아닌, 복잡성 관리의 핵심 요소로 격상시킵니다. 이는 모호함(obscurity)과 인지 부하(cognitive load)라는 복잡성의 근본 원인을 직접적으로 공격하는 방법입니다. 명확한 주석과 이름은 독자가 코드를 이해하는 데 필요한 정보를 제공하여 인지 부하를 줄이고, 숨겨진 가정이나 의도를 명시적으로 만들어 모호함을 제거합니다. 특히 ‘주석 먼저 작성하기’는 주석을 사후 작업이 아닌, 설계 과정의 중심에 놓는 혁신적인 제안입니다.
3.2 실용적 원칙과 비판적 시각 (16-21장)#
16장 & 17장: 일관성 있는 코드 수정#
- 전략적 수정: 기존 코드를 수정할 때(버그 수정, 기능 추가 등) 최소한의 변경만 가하는 전술적 접근을 피해야 합니다.[6, 7, 4] 수정 작업을 기회로 삼아 기존 설계의 문제점을 개선하는 전략적 투자를 해야 합니다.[6, 7, 4] “코드를 수정할 때 설계를 개선하지 않는다면, 아마도 악화시키고 있는 것"입니다.[8]
- 일관성의 힘: 일관성은 시스템을 예측 가능하게 만들어 인지 부하를 줄이는 강력한 도구입니다.[6, 7, 2, 9] 비슷한 작업은 비슷한 방식으로 처리하고, 일관된 명명 규칙, 코딩 스타일, 디자인 패턴을 사용해야 합니다.[2, 9]
- 위험 신호 - 불일치: 일관성을 깨는 것은 위험 신호입니다. 새로운 “더 좋은” 아이디어가 생겼더라도, 기존의 일관성을 유지하는 것이 더 가치 있는 경우가 많습니다.[2, 10]
18장: 코드는 명백해야 한다#
- 독자를 위한 설계: 이 장은 책의 여러 아이디어를 하나로 묶는 핵심 원칙을 제시합니다. 소프트웨어는 작성의 용이성이 아닌 읽기의 용이성을 위해 설계되어야 합니다. 코드는 독자가 쉽게 이해할 수 있도록 명백해야 합니다.[6, 2]
- 명백함의 척도: “명백함"은 주관적이므로, 코드 리뷰가 이를 판단하는 가장 좋은 방법입니다. 만약 다른 사람이 당신의 코드를 명백하지 않다고 느낀다면, 그것은 명백하지 않은 것입니다.
19장: 소프트웨어 동향에 대한 비판적 고찰#
이 장에서 오스터하우트는 현대 소프트웨어 개발의 여러 “성역"에 대해 복잡성이라는 렌즈를 통해 비판적인 분석을 제시합니다.
- 객체 지향 프로그래밍 (OOP): 인터페이스 상속은 추상화를 강화하므로 유용하지만, 구현 상속은 부모와 자식 클래스 간의 강한 의존성을 만들어 정보 은닉을 해치므로 주의해야 한다고 주장합니다.[2, 10]
- 애자일 개발과 TDD: 애자일 방법론은 단기적인 기능 구현에 치중하여 전술적 프로그래밍을 조장할 수 있다고 경고합니다. 특히 테스트 주도 개발(TDD) 은 “최고의 설계를 찾는 것보다 특정 기능이 작동하도록 만드는 데 초점을 맞춘다"며 전략적 프로그래밍의 위험 신호로 지목합니다. TDD는 점진적인 개선에만 유용하며, 근본적인 설계 문제를 해결하기는 어렵습니다.[11]
- 디자인 패턴: 맹목적으로 디자인 패턴을 적용하는 것은 오히려 복잡성을 증가시킬 수 있다고 경고합니다. 항상 복잡성 감소라는 관점에서 패턴의 적용 여부를 판단해야 합니다.
20장: 성능을 위한 설계#
- 측정, 그리고 또 측정: 성능 최적화는 추측이 아닌, 실제 측정을 기반으로 이루어져야 합니다.[2, 10]
- 핵심 경로(Critical Path) 집중: 모든 코드를 최적화하려는 시도는 비효율적이며 복잡성만 높입니다. 시스템 성능에 가장 큰 영향을 미치는 핵심 경로에 설계 노력을 집중해야 합니다.[2, 10]
- 성능 vs 설계: 좋은 설계를 희생하면서까지 성능을 추구해서는 안 됩니다. 목표는 깔끔한 설계를 유지하면서 높은 성능을 달성하는 것입니다.[6, 7]
21장: 무엇이 중요한지 결정하라#
- 핵심 원칙: 소프트웨어 설계의 본질은 중요한 것과 중요하지 않은 것을 구분하는 능력입니다.[6, 7]
- 모든 원칙의 근간: 이 원칙은 책에서 다룬 모든 개념의 기초가 됩니다. 추상화는 사용자가 신경 써야 할 중요한 정보만 인터페이스에 남기는 것이고, 이름은 변수의 가장 중요한 측면을 전달해야 하며, 주석은 코드에 드러나지 않는 중요한 정보를 설명하는 것입니다.[6, 7]
이 시리즈의 마지막 파트에서는 “소프트웨어 설계의 철학” 전체를 관통하는 핵심 원칙과, 개발자가 경계해야 할 ‘위험 신호(Red Flags)‘들을 실용적인 체크리스트 형태로 요약합니다. 이는 복잡성과의 전쟁에서 매일 사용할 수 있는 강력한 무기가 될 것입니다.
1. 반드시 추구해야 할 10가지 핵심 설계 원칙#
-
복잡성은 유일한 적이다 (Complexity is the Enemy): 소프트웨어 설계의 모든 결정은 단 하나의 질문으로 귀결되어야 합니다: “이것이 전체 시스템의 복잡성을 줄이는가, 아니면 증가시키는가?” [1, 2, 3, 4, 5] 새로운 복잡성을 추가하는 것에 대해 ‘무관용’ 원칙을 적용해야 합니다.[3]
-
전략적으로 프로그래밍하라 (Program Strategically): 단순히 기능이 동작하게 만드는 ‘전술적 프로그래밍’을 넘어서야 합니다.[1, 4, 5, 6] 개발 시간의 10-20%를 지속적인 설계 개선에 투자하는 ‘전략적 프로그래밍’은 장기적으로 더 빠른 개발 속도와 높은 품질로 보상받습니다.[1, 7, 8, 9]
-
모듈은 깊어야 한다 (Modules Should Be Deep): 최고의 모듈은 단순한 인터페이스 뒤에 강력하고 복잡한 기능을 숨깁니다.[2, 5, 10, 11] 인터페이스의 복잡성 대비 제공하는 기능의 비율을 극대화하는 것이 목표입니다.[2]
-
정보를 적극적으로 숨겨라 (Hide Information Aggressively): 깊은 모듈을 만드는 핵심 기술은 정보 은닉입니다.[2, 5, 10] 모듈의 구현 세부사항과 핵심 설계 결정을 외부에 노출하지 말고, 인터페이스를 통해 필요한 최소한의 정보만 제공해야 합니다.[2, 5, 10]
-
복잡성을 아래로 끌어내려라 (Pull Complexity Downwards): 모듈의 구현이 복잡해지더라도 인터페이스를 단순하게 유지하는 것이 항상 더 중요합니다.[3, 5, 7] 모듈의 사용자는 다수이고 개발자는 소수이므로, 개발자가 약간의 고통을 감수하여 수많은 사용자의 고통을 덜어주어야 합니다.[7, 12]
-
주석을 먼저 작성하라 (Write Comments First): 코드를 작성하기 전에 인터페이스에 대한 주석을 먼저 작성하는 것은 강력한 설계 도구입니다.[2, 13] 만약 어떤 기능을 주석으로 명확하고 간결하게 설명하기 어렵다면, 그 설계는 아마도 너무 복잡한 것입니다.[2, 14]
-
두 번 설계하라 (Design It Twice): 중요한 설계 문제에 대해서는 첫 번째 아이디어에 안주하지 마십시오.[15, 11, 9, 16] 최소 두 가지 이상의 근본적으로 다른 대안을 탐색하고 비교하는 과정에서 문제에 대한 더 깊은 통찰과 더 나은 설계를 얻게 됩니다.[17, 8]
-
코드는 명백해야 한다 (Code Should Be Obvious): 코드는 작성하는 사람보다 읽는 사람을 위해 최적화되어야 합니다.[10, 12] 명확한 이름, 일관성, 그리고 적절한 주석을 통해 코드의 의도가 독자에게 명백하게 드러나도록 만들어야 합니다.[10, 12]
-
일관성을 유지하라 (Maintain Consistency): 일관성은 시스템의 예측 가능성을 높여 인지 부하를 극적으로 줄여줍니다.[10, 11, 12, 18] 새로운 ‘더 나은’ 아이디어가 기존의 일관성을 해친다면, 대부분의 경우 일관성을 유지하는 것이 더 가치 있는 선택입니다.[12, 16]
-
무엇이 중요한지 결정하라 (Decide What Matters): 뛰어난 설계의 본질은 중요한 것과 중요하지 않은 것을 구분하는 능력입니다.[10, 11] 추상화는 중요한 정보만 남기는 것이고, 이름은 가장 중요한 속성을 전달해야 하며, 주석은 코드에 드러나지 않는 중요한 맥락을 설명하는 것입니다.[10, 11]
2. 반드시 피해야 할 8가지 위험 신호 (Red Flags)#
-
얕은 모듈 (Shallow Modules): 제공하는 기능에 비해 인터페이스가 복잡한 모듈입니다.[2, 6] 이는 복잡성을 숨기기보다 더 많은 복잡성을 만들어냅니다. 특히 메서드나 클래스는 무조건 작아야 한다는 생각(“클래스병, classitis”)은 얕은 모듈의 확산을 유발하는 주범입니다.[15, 11, 6]
-
정보 누출 (Information Leakage): 하나의 설계 결정(예: 특정 파일 형식)이 여러 모듈에 걸쳐 나타나 서로를 의존하게 만드는 현상입니다.[15, 19] 이는 모듈의 독립성을 해치고 변경을 어렵게 만듭니다.
-
시간적 분해 (Temporal Decomposition): 코드의 구조가 작업이 일어나는 시간 순서를 그대로 반영하는 경우입니다.[5, 15, 20] 이는 종종 여러 모듈이 동일한 저수준 데이터 구조를 공유하게 만들어 심각한 정보 누출을 야기합니다.[5, 15, 20]
-
전달 메서드/변수 (Pass-through Methods/Variables): 실질적인 기능 추가 없이 단순히 다른 메서드를 호출하거나, 여러 계층을 통해 변수를 그대로 전달하는 것은 잘못된 계층화의 명백한 신호입니다.[15, 7] 이는 불필요한 인터페이스 복잡성만 증가시킵니다.[15, 7]
-
모호한 이름 (Vague Names): 변수나 메서드의 역할을 정확하게 설명하지 못하는 이름은 심각한 버그의 원인이 될 수 있습니다.[7] 좋은 이름을 짓기 어렵다면, 해당 대상의 설계 자체가 명확하지 않다는 신호일 수 있습니다.[7]
-
너무 많은 예외 (Too Many Exceptions): 예외 처리는 시스템 복잡성의 주된 원인 중 하나입니다.[5, 15, 20, 21] 가능한 한 API 설계를 변경하여 특정 오류 조건이 더 이상 예외가 아니도록 “에러의 존재를 없애는” 방법을 모색해야 합니다.[5, 15, 21, 18]
-
불일치 (Inconsistency): 기존 코드베이스의 관례를 따르지 않는 코드는 독자의 예측을 깨뜨려 인지 부하를 높입니다.[12, 16]
-
TDD에 대한 맹신 (Blind Faith in TDD): 저자는 TDD가 개별 기능 구현이라는 ‘전술적’ 목표에 치중하게 만들어, 전체적인 설계를 고려하는 ‘전략적’ 사고를 방해할 수 있다고 경고합니다.[3, 22] TDD는 좋은 설계를 보장하는 만병통치약이 아닙니다.
결론#
“소프트웨어 설계의 철학"은 단순히 따라야 할 규칙의 목록이 아니라, 개발 과정 전반에 걸쳐 가져야 할 사고방식을 제안합니다. 위에 요약된 원칙들을 지향하고 위험 신호들을 적극적으로 피하려는 노력을 통해, 우리는 복잡성의 수렁에서 벗어나 더 견고하고, 유지보수하기 쉬우며, 궁극적으로는 개발하기 즐거운 소프트웨어를 만들 수 있을 것입니다.
- 일관성과 명백함은 코드의 가독성을 높여 미래의 개발자가 겪을 인지 부하를 줄여주는 투자입니다.
- 소프트웨어 동향에 대한 비판적 시각은 유행을 맹목적으로 따르지 않고, 모든 도구와 방법론을 ‘복잡성 감소’라는 단일 척도로 평가하는 주체적인 태도를 요구합니다.
- 성능과 중요도에 대한 통찰은 한정된 자원을 가장 효과적인 곳에 집중하는 전략적 사고의 정수입니다.
궁극적으로 오스터하우트가 제시하는 철학은, 좋은 설계가 단기적인 속도보다 장기적인 지속 가능성에 기여하며, 이것이 결국 더 빠르고 즐거운 개발로 이어진다는 믿음에 기반합니다. 훌륭한 디자이너는 버그로 가득한 복잡한 코드와 씨름하는 대신, 우아한 설계를 창조하는 즐거움을 누리게 될 것입니다.