[도서] Clean Code(클린 코드)

Theo·2025년 7월 24일

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)

  • 동일한 로직이 여러 곳에 있다면 함수로 추출해 한 번만 유지보수할 수 있도록 합니다.

4장. 주석 (Comments)

주석은 실패의 증거 (Comments are Failures)

  • "주석은, 잘 작성된 코드가 자신을 설명하지 못했음을 보여주는 실패"라는 인식이 핵심 메시지입니다. Martin은 "주석을 썼다는 건, 코드로 충분히 표현하지 못했다는 증거"라고 강조합니다.
  • 가능하면 코드로 자신을 설명하고, 주석은 최소화해야 합니다.

주석은 썩는다 (Comments Rot)

  • 오래된 주석은 실제 코드와 동떨어진 잘못된 정보를 전달할 위험이 있습니다.
  • 실제 동작은 오직 코드에서 확인 가능합니다.

좋은 주석의 유형 (Good Comments)

  • 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!”

5장. 포맷팅 (Formatting)

포맷팅의 중요성

  • 포맷팅은 단순한 미적 요소가 아닙니다. 코드의 커뮤니케이션 수단이며, 팀 전체가 함께 읽고 이해할 수 있도록 돕습니다. 일관성 있는 스타일은 장기적으로 생산성과 협업에 큰 영향이 있습니다.

세로 구성 (Vertical Formatting)

  • 파일 크기 제약 : 하나의 소스 파일은 보통 200줄을 권장하며 100줄이 이상적입니다. 작고 집중된 파일은 이해와 관리에 유리합니다.
  • 신문 기사 흐름 : 상단에는 추상적이고 중요한 내용(API, 클래스 선언)을, 하단으로 갈수록 세부 구현으로 내려가는 구조가 좋습니다.
  • 개념 간 공백 줄(Vertical Openness) : 논리적으로 구분되는 코드 블록은 한 줄 이상의 빈 줄로 분리하세요
  • 관련 코드 밀집(Vertical Density) : 서로 관계 있는 변수나 메서드는 붙여서 배치해 가독성을 높이세요
  • 선언 위치(Vertical Distance) :
    • 지역 변수 : 사용 직전 또는 함수 상단 가장 가까운 곳에 선언
    • 인스턴스 변수 : 클래스 상단에 통일하여 배치
    • 호출 관계가 있는 함수는 호출자 위에 두어 흐름을 명확히

가로 구성 (Horizontal Formatting)

  • 줄 길이 제한 : 한 줄에 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 (최소 지식의 원칙)

  • "친구에게만 말하고, 낯선 객체에선 묻지 마라"
  • 메서드는 다음 대상만 호출해야 합니다.
    1. 자기 자신
    2. 직접 생성한 객체
    3. 인자로 받은 객체
    4. 인스턴스 변수로 보유한 객체
  • 긴 체이닝 호출(예: 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)

  1. 실패하는 테스트 먼저 작성 : 실제 코드를 작성하기 전에 실패하는 단위 테스트를 반드시 먼저 작성해야 합니다.
  2. 최소한의 테스트 코드 작성 : 컴파일에 실패하지 않으면서, 실제로 실패하는 수준의 테스트만 작성해야 합니다.
  3. 테스트 합격 분량만 코드 작성 : 현재 실패하는 테스트를 통과하도록 충분한 생산 코드만 구현하며, 과도한 코드는 피합니다.
    -> 이를 통해 Red-Green-Refactor 사이클을 반복하면서 점진적이고 안전하게 기능을 개발할 수 있습니다.

깨끗한 테스트 유지의 중요성

  • 테스트 코드도 프로덕션 코드만큼 신중하게 설계되어야 합니다. 더러워진 테스트는 유지보수의 부담이 되어 오히려 코드베이스를 망가뜨릴 수 있습니다.
  • 테스트는 유연성, 유지보수성, 재사용성을 보장해 개발 부담을 줄입니다.

깨끗한 테스트의 구조와 규칙

  • BUILD-OPERATE-CHECK 패턴:
    1. BUILD - 테스트 데이터를 생성
    2. OPERATE - 대상 코드 실행
    3. 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) : 구체 클래스가 아닌 추상화된 인터페이스에 의존해야 변경에 유연해집니다.

0개의 댓글