
1장. 깨끗한 코드(Clean Code)
깨끗한 코드가 중요한 이유
- 나쁜 코드가 축적되면 결국 개발 조직을 무너뜨릴 수 있다 : 실행은 가능하지만 유지.보수가 어려운 상태가 되며, 기능을 추가하거나 수정하기가 점점 어려워진다.
- 생산성 저하 :코드를 작성하는 시간보다 읽고 이해하는 시간이 훨씬 많다. 지저분한 코드가 많아질수록 생산성은 거의 0에 가까워진다.
- 느끼는 부담감이 누적되면 결국 프로그램을 조사하고 리팩토링해야 하는 상황으로 이어지고, 반복되면 팀이 프로젝트 진행을 멈출 수도 있다.
깨끗한 코드란 무엇인가?
- 가독성 (Readability) : 코드는 읽기 편해야 한다. 마치 언어가 문제 해결을 위해 만들어진 것처럼 자연스러워야 한다.
- 단순성 (Simplicity) : 복잡함은 유지보수성을 저해한다. 코드 한 줄이라도 의도가 명확해야 한다.
- 표현력 (Expressiveness) : 변수나 메소드 이름만 보고도 의도가 드러나야 한다.
- 불필요한 요소 제거 (Decisive) : 꼭 필요한 코드만 남기고 나머지는 제거
Boy Scout Rule (보이 스카우트 규칙)
“Leave the campground cleaner than you found it.”
- 코드 수정 시, 작더라도 항상 한걸음 더 개선하고 나간다는 원칙입니다.
작은 변화라도 쌓이면 장기적으로 코드베이스 전반의 품질이 향상됩니다. 단 팀 내 커뮤니케이션은 필수입니다. - 무작정 리팩토링하다가 문제를 띄우면 오히려 역효과가 날 수 있으니까요.
2장. 의미 있는 이름(Meaniningful Names)
이름에 담겨야 할 주요 원칙들
- 의도가 드러나는 이름(Use Intention-revealing Names)
변수, 함수, 클래스의 이름만 보고 그 목적과 의미를 파악할 수 있어야 합니다. 만약 주석이 필요하다면 이름이 부족하다는 증거입니다. 예 : int elapsedTimeInDays
- 오해를 피하는 구문 (Avoid Disinformation / Make Meaningful Distinctions)
accountList라고 이름 붙였는데 실제로 자바 List가 아니라면 오히려 혼란을 초래할 수 있습니다. 대신 accounts, accountGroup 등으로 명확하게 구분해야 합니다.
- 발음하기 쉬운 이름 (Use Pronounceable Names)
코드 리뷰나 회의 중 발음하기 어려운 이름은 소통에 방해가 됩니다. 사람이 말할 수 있는 이름을 선택하세요
- 검색에 용이한 이름 (Use Searchable Names)
짧은 글자나 숫자, 애매한 약어보다는 검색 가능한 구체적인 이름이 유지보수에 유리합니다. 예 : studentCount vs s
- 한 개념에 하나의 단어 사용 (Pick One Word per Concept)
get, fetch, retrieve를 혼용하지 말고, 팀에서 한 단어를 선택해 일관되게 사용하세요
- 도메인 용어 사용 (Use Problem/Domain-Specific Names)
시스템의 문제 영역(Domain)에 맞는 용어를 사용하면 의미 전달과 코드 가독성이 향상됩니다.
클래스 & 메소드 명명 규칙
- 클래스 이름 -> 명사 또는 명사구 사용 (
Customer, AddressParser 등)
- 메소드 이름 -> 동사 또는 동사구 사용 (
saveOrder, postPayment, deletePage 등), 접근자(accessor)는 get, set 접두어 사용 권고
3장. 함수(Functions)
작고 단일한 목적 (Small & Do One Thing)
- 함수는 작아야 합니다. 이상적으로는 최대 20줄, 블록 내 들여쓰기 수준은 12레벨 이하가 좋습니다. 조건문이나 반복문 안은 함수 호출 한 줄이면 충분합니다.
- 하나의 함수가 한 가지 일만 수행해야 하며, 잘 수행해야 합니다. 다른 역할이 섞였다면 추가 함수로 분리하세요.
한 수준의 추상화 (Step-down Rule)
- 함수 안의 모든 코드 문장은 같은 수준의 추상화를 유지해야 합니다.
- 최상위 함수는 추상적, 그 아래는 세부 구현 함수가 계층을 따라 내려가도록 설계하세요. 읽는 이는 단계별로 코드 흐름을 쉽게 이해할 수 있습니다.
이름은 서술적이어야 한다 (Descriptive Names)
- 함수 이름은 길더라도,
doIt() 보다 calculateMonthlySalary() 처럼 의도를 드러내는 이름이 더 낫습니다.
- 명명에 충분히 시간을 투자해야 하며, 일관된 네이밍 규칙에 따르세요.
매개변수는 최소화하라 (Minimize Arguments)
- 인수 개수는 0개(niladic)가 이상적, 그 다음 1개(monadic), 2개(dyadic) 순으로 바람직합니다.
- 3개 이상은 피하고, 부득이할 경우 관련 인수를 하나의 객체로 묶어 전달하세요.
- Flag 인자 (플래그)는 함수 하나로 두 가지 행동을 수행하게 하므로 분리된 함수를 만드는 것이 좋습니다.
명령과 질의를 분리하라 (Command-Query Separation)
- 함수는 명령(상태 변경) 또는 질의(정보 반환) 중 하나만 수행해야 합니다.
- 두 가지 역할을 동시에 하게 되면 혼동을 유발합니다.
예외 처리를 사용하라 (Use Exceptions over Error Codes)
- 에러 상황에서는 반환 코드보다 예외(exception) 처리 방식을 사용하는 것이 더 명확하며, 코드 분리와 읽기 쉬움을 돕습니다.
부작용을 피하라 (No Side Effects)
- 함수 내부에서 예기치 않은 전역 상태나 외부 상태를 변경하지 마세요.
- 상태 변경이 필요하다면 그 역할이 명확하게 함수 이름에 드러나야 합니다.
중복은 악 (DoNotRepeat Yourself)
- 동일한 로직이 여러 곳에 있다면 함수로 추출해 한 번만 유지보수할 수 있도록 합니다.
- "주석은, 잘 작성된 코드가 자신을 설명하지 못했음을 보여주는 실패"라는 인식이 핵심 메시지입니다. Martin은 "주석을 썼다는 건, 코드로 충분히 표현하지 못했다는 증거"라고 강조합니다.
- 가능하면 코드로 자신을 설명하고, 주석은 최소화해야 합니다.
- 오래된 주석은 실제 코드와 동떨어진 잘못된 정보를 전달할 위험이 있습니다.
- 실제 동작은 오직 코드에서 확인 가능합니다.
- Legal Comments : 저작권, 라이선스 등 법적 요구 사항
- Informative Comments : 정규식처럼 코드만으로 설명하기 어려운 부분
- Explanation of Intent : 언뜻 이상해 보이는 코드 뒤에 숨은 이유를 설명
- Clarification : 모호한 값이나 인수 의미를 명확히
- Warning of Consequences : 특정 코드 실행의 부작용 경고
- TODO : 미래에 개선하거나 완료해야 할 항목 표시
- Public API Documentation : 외부에 공개되는 함수 또는 클래스에 대한 설명
“The best comment is the one you don’t have to write!”
포맷팅의 중요성
- 포맷팅은 단순한 미적 요소가 아닙니다. 코드의 커뮤니케이션 수단이며, 팀 전체가 함께 읽고 이해할 수 있도록 돕습니다. 일관성 있는 스타일은 장기적으로 생산성과 협업에 큰 영향이 있습니다.
- 파일 크기 제약 : 하나의 소스 파일은 보통 200줄을 권장하며 100줄이 이상적입니다. 작고 집중된 파일은 이해와 관리에 유리합니다.
- 신문 기사 흐름 : 상단에는 추상적이고 중요한 내용(API, 클래스 선언)을, 하단으로 갈수록 세부 구현으로 내려가는 구조가 좋습니다.
- 개념 간 공백 줄(Vertical Openness) : 논리적으로 구분되는 코드 블록은 한 줄 이상의 빈 줄로 분리하세요
- 관련 코드 밀집(Vertical Density) : 서로 관계 있는 변수나 메서드는 붙여서 배치해 가독성을 높이세요
- 선언 위치(Vertical Distance) :
- 지역 변수 : 사용 직전 또는 함수 상단 가장 가까운 곳에 선언
- 인스턴스 변수 : 클래스 상단에 통일하여 배치
- 호출 관계가 있는 함수는 호출자 위에 두어 흐름을 명확히
- 줄 길이 제한 : 한 줄에 100~120자 이내로 유지하고, 스크롤 필요 없도록 작성하세요.
- 적절한 공백 사용 :
- 연산자 주변, 쉼표 등 구분이 필요한 부분에는 공백을 넣어 가독성 향상
- 함수 정의 앞이나 블록 시작 등 관련 없는 부분은 붙여 쓰세요.
- 수평 정렬 지양 : 열 맞추기를 위한 정렬은 유지보수를 어렵게 할 수 있으니 피하세요.
- 들여쓰기 : 언어별 스타일(예: K&R, 2~4칸 등)을 팀 내에서 정하고 일관되게 적용하세요.
팀 차원의 스타일 합의
- 팀 합의된 규칙 : 모든 팀원이 일관된 스타일을 따르도록 명확한 규칙을 정하고, 자동 포맷터(예: eslint, prettier, clang-format)를 활용해 자동화하세요.
- 원활한 입문과 코드 인식 : 새로운 팀원도 즉시 코드를 읽고 이해할 수 있는 일관된 스타일 유지가 중요합니다.
6장. 객체와 자료 구조 (Objects & Data Structures)
데이터 추상화 (Data Abstraction)
- 내부 구현 세부사항은 감추고, 추상적인 인터페이스로만 데이터를 조작해야 합니다.
- 예: 내부 연료 수준을 직접 제공하는 대신 퍼센트로 계산된
getFuelLevelPercentage()처럼 데이터를 추상화해 노출하는 방식이 권장됩니다.
객체와 자료 구조의 반대 대칭 (Data/Object Anti-Symmetry)
- 객체(Object) : 데이터를 숨기고(private), 데이터를 조작하는 행동(public 메서드)을 제공합니다.
- 자료 구조(Data Structure) : 데이터를 노출(public 변수 등)하며, 의미 있는 로직이 없는 순수한 구조입니다. (예: DTO)
객체 vs 자료구조 언제 사용?
- 새로운 데이터 타입을 자주 추가할 경우 -> 객체(Object)
- 새로운 연산(기능)을 추가할 경우 -> 자료 구조(Data Structure)
Law of Demeter (최소 지식의 원칙)
- "친구에게만 말하고, 낯선 객체에선 묻지 마라"
- 메서드는 다음 대상만 호출해야 합니다.
- 자기 자신
- 직접 생성한 객체
- 인자로 받은 객체
- 인스턴스 변수로 보유한 객체
- 긴 체이닝 호출(예:
a.getB().getC().doSomething())은 Train-wreck 패턴으로, 이는 Demeter 법칙 위반입니다.
Tell, Don`t Ask
- 데이터 구조의 내부를 "달라고 묻는 것(Ask)"이 아니라, 행동을 "시켜"야 합니다.
- 예:
order.calculateTotal()과 같이 호출자의 내부 구현을 몰라도 되도록 메시지를 보내야 한다는 의미입니다.
하이브리드 구조 경계
- 객체와 자료 구조가 섞인 하이브리드는 새로운 타입과 기능 모두 확장 어려움이라는 최악의 상황을 초래합니다. 따라서 엄격히 구분하고, 혼합을 피하는 것이 중요합니다.
7장. 에러 처리 (Error Handling)
오류 처리는 로직을 가릴 수 있다.
- 에러 처리 코드가 기본 로직을 가리면 안 됩니다. 처리 루틴은 행위 로직과 분리되어야 합니다.
“If error handling obscures logic, you’re doing it wrong!”
함수는 에러 처리만 담당하도록 구성
- 함수는 Try-Catch-Finally 구조를 먼저 짜고, 각 블록 내에서 명확한 단일 동작만 수행해야 합니다.
- Catch 블록은 필요한 로깅/정리만 수행하고, 예외 흐름을 감추지 말아야 합니다.
void delete(Page page) {
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.error("Got an error while deleting page: " + page, e);
throw e;
}
}
에러 코드보다 예외를 사용하라
- 반환 코드 방식은 호출자에 의한 누락 가능성, 가독성 저하, 책임 혼란을 초래합니다. 예외(Exception) 처리 방식이 월등히 낫습니다. (Java 등 언어의 기본 관행).
- NULL 반환/전달 금지 :
- 함수가 NULL을 반환하면 호출자마다 검사해야 하는 부담 발생
- NULL 인자는
NullPointerException 같은 예끼치 않은 결과를 유발할 수 있습니다. -> 대신 예외 Throw하거나 Special Case Pattern을 사용하세요.
특별 사례 패턴 (Special Case Pattern)
- 특정 예외 상황에도 호출자는 단순히 메시지를 보내는 것만 고려하고, 특수 사례 객체가 내부 로직을 담당하도록 캡슐화합니다.
-> 에러 분기 로직이 호출자에서 사라지고 구조가 깔끔해집니다.
예외에는 충분한 컨텍스트 제공
- 오류 메시지는 어디서 발생했는지, 무슨 상황인지, 어떤 파라미터인지 등의 정보가 포함되어야 디버깅이 용이합니다.
throw new MyCustomException("Failed to delete page "+page.getId(), e);
``` :contentReference[oaicite:19]{index=19}
8장. 경계 (Boundaries)
핵심 개요
- 외부 라이브러리나 API 같은 "경계(boundary)"는 우리 시스템의 핵심 비즈니스 로직과는 분리되어야 합니다.
- 직접 참조를 최소화하고, 필요한 부분만 감싸는 래퍼(wrapper) 또는 인터페이스(interface) 구현이 권장됩니다.
학습 테스트 (Learning Tests)
- 새 라이브러리를 사용할 때는 실험 코드를 별도 테스트로 작성해 API의 동작을 탐색하세요.
- 테스트는 문서이자 안전망 역할을 하며, 버전 업데이트 시 회귀 여부 확인에도 유용합니다.
인터페이스 & 어댑터 설계
- 외부 API와의 통신은 Port(인터페이스)를 먼저 정의한 후, 이를 구현하는 어댑터를 통해 연결하는 방식으로 구성합니다.
- 실제 구현이 없는 상황에서도 코드 작성과 테스트가 가능합니다.
- 외부 API가 준비되면 쉽게 실제 어댑터로 교체할 수 있습니다.
클린 경계의 장점
- 외부 변경 영향 최소화 : API가 변경되더라도 래퍼/어댑터만 수정하면 됩니다.
- 테스트 용이성 향상 : 내부 모듈은 Fake 어댑터로 격리하여 안정적인 단위 테스트가 가능합니다.
- 코드 복잡도 감소 : 외부 코드의 복잡성을 경계 내부로 은닉, 핵심 로직은 단순하고 가독성 유지
9장. 유닛 테스트 (Unit Tests)
TDD의 세 가지 법칙 (Three Laws of TDD)
- 실패하는 테스트 먼저 작성 : 실제 코드를 작성하기 전에 실패하는 단위 테스트를 반드시 먼저 작성해야 합니다.
- 최소한의 테스트 코드 작성 : 컴파일에 실패하지 않으면서, 실제로 실패하는 수준의 테스트만 작성해야 합니다.
- 테스트 합격 분량만 코드 작성 : 현재 실패하는 테스트를 통과하도록 충분한 생산 코드만 구현하며, 과도한 코드는 피합니다.
-> 이를 통해 Red-Green-Refactor 사이클을 반복하면서 점진적이고 안전하게 기능을 개발할 수 있습니다.
깨끗한 테스트 유지의 중요성
- 테스트 코드도 프로덕션 코드만큼 신중하게 설계되어야 합니다. 더러워진 테스트는 유지보수의 부담이 되어 오히려 코드베이스를 망가뜨릴 수 있습니다.
- 테스트는 유연성, 유지보수성, 재사용성을 보장해 개발 부담을 줄입니다.
깨끗한 테스트의 구조와 규칙
- BUILD-OPERATE-CHECK 패턴:
- BUILD - 테스트 데이터를 생성
- OPERATE - 대상 코드 실행
- CHECK - 결과 검증
- 테스트당 하나의 assert만 사용하거나, 적어도 한 개념만 검증하도록 합니다. 여러 개의 assert는 테스트 파악을 어렵게 만듭니다.
F.I.R.S.T 원칙
- Fast - 테스트는 빨라야 합니다.
- Independent - 테스트는 서로 의존하면 안 됩니다.
- Repeatable - 어떤 환경에서도 동일하게 실행되어야 합니다.
- Self-Validating - 테스트 결과는 명확한 Boolean (통과/실패)로 나와야 합니다.
- Timely - 해당 기능 구현 직전에 작성되어야 합니다.
테스트 코드의 특수성
- 테스트는 가독성, 단순성, 표현력이 중요하며, 퍼포먼스 최적화보다 명확한 의미 전달에 집중해야 합니다.
- 도메인 특화 언어(DSL)를 테스트에 도입해 가독성을 높이고, 테스트 의미를 직관적으로 표현할 수 있습니다.
10장. 클래스 (Classes)
클래스란 무엇인가?
- 클래스는 함수들의 집합(bag of methods)이며, 조작하는 데이터를 숨기고(private) 메서드를 통해 공개합니다. 추상 클래스는 개념을 표현하며, 구체 클래스는 구현을 포함합니다.
클래스 조직 구조
Java 기준 권장 구조 :
1. Public static constants
2. Private static 변수
3. Private 인스턴스 변수
4. Public 메서드
5. Public 메서드에서 호출되는 private 유틸리티 메서드 순으로 배치
작고 작게, 더 작게
- 작아야 하고, 더 작아야 하며, 크기는 '책임의 개수'로 측정합니다.
- 이름은 클래스 책임을 25단어 내외로 설명할 수 있어야 합니다. 'Processor', 'Manager'등은 책임이 다중임을 암시할 수 있어 피하세요.
단일 책임 원칙 (SRP)
- 클래스는 오직 하나의 이유로 변경되어야 하며, 하나의 책임만 가져야 합니다.
응집도 (Cohesion)
- 응집도는 클래스 내 메서드가 얼마나 인스턴스 변수를 함께 사용하는지에 따라 측정됩니다. 메서드 내 사용되는 인스턴스 변수 수가 많을수록 응집도가 높습니다.
- 응집도가 떨어지는 경우, 클래스를 분리해 응집도를 높이는 것이 권장됩니다.
응집도 유지 -> 작은 클래스 증가
- 큰 클래스를 기능 단위로 분해하면서 자연스럽게 작은 클래스들이 분리됩니다. 이는 코드 구조를 더 명확하게 하고, 유지보수를 쉽게 합니다.
OCP와 DIP
- 개방-폐쇄 원칙(Open-Closed Principle) : 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.
- 의존성 역전 원칙(Dependency Inversion Principle) : 구체 클래스가 아닌 추상화된 인터페이스에 의존해야 변경에 유연해집니다.