본문 바로가기

개발자 강화/개발 독서

[Clean Code 2판] 1-3: First Principles

야호 3장!

 


Part1: Code

Chapter3. First Principles

 

Everything Small, Well Named, Organized, and Ordered

 

1. 모든 것을 작은 크기로 유지하라

2. 그 작은 것의 의미가 타인에게 잘 전달되도록 잘 명명하라

3. 타인이 쉽게 이해할 수 있도록 구조를 정의하고 유지하라

4. 하나의 잘 정의된 개념을 따르도록 구조 내 요소를 잘 정렬된 상태로 유지하라

 

코드를 이해 못하는 타인 입장에서 항상 생각하려고 노력해라!

 


Functions

이 파트는 전반적으로 '쏙쏙 들어오는 함수형 프로그래밍'에서 읽었던 거랑 비슷한 내용이다.

 

1. 함수는 작게 쪼개야 한다.

2. '합리적으로 이름을 붙일 수 있는 동작'을 하고 있는 코드는 반드시 함수로 추출해야 한다.

3. 함수의 이름은 함수의 동작을 설명할 수 있는 동사로 구성되어야 한다.

4. 각 함수가 오직 하나의 일만 하도록 보장해야 한다. 

 

어쨌든 단독 역할을 하도록 작게 함수를 쪼개고 이름을 잘 붙이라는 뜻

 


프로그램은 성장한다

제곧내;

 

회사가 성장함에 따라 필요한 기능도 늘어날 것이다.

처음에는 생각 못했던 부분에 부속 기능이 생길거고, 코드는 덕지덕지 붙는다.

 

엔지니어가 처음부터 이런 코드를 짜고 싶었던 건 아닐거야...

그때그때 돌아가게 코드를 짜다보니 완성된 덩어리는 꽤나 못생겨진다.

 

라는 슬픈 이야기를 저자가 하는데 남일 같진 않네요 ^^;

그래서 코드가 썩기 전에 지금! 청소하라고 하시네요~

 

그럼 어떻게 해야하는가? 아래의 방법을 따르면 된다!

 

 

1. SRP (Single Responsibility Principle)

한 함수/클래스는 하나의 이유로만 변경되어야 한다.

 

// (전) 세금, 할인, 가격이 다 섞여있음
public void rent(CatalogItem item, int days) {
  int unitPrice = switch (item) { ... };
  boolean eligibleForDiscount = switch (item) { ... };
  int thisTax = switch (item) { ... };
  // ...
}

// (후) 각각 분리
public void rent(CatalogItem item, int days) {
  int unitPrice = getUnitPrice(item);
  int price = calculatePrice(item, days, unitPrice);
  int thisTax = getTax(item, price);
  // ...
}

 

처음엔 rent라는 함수에 세금, 할인, 가격이 마구잡이로 섞여있다.

그런데, 추후 기획이 바뀌면서 '세금'의 규칙을 수정해야 했다.

 

이 rent 함수 상태 그대로 세금을 건드리면, 할인과 가격 로직도 같이 예상치 못한 side effect를 받을 수 있다.

 

그렇다면, 할인/세금/가격을 각각 분리하면 된다.

이렇게 다른 이해관계를 가진 함수들을 멀리 떨어뜨릴수록, 변경에서 코드를 안전하게 지킬 수 있다.

 

만약! SRP를 지키지 않는다면?

 

코드에 "취약성"이 생긴다.

PM이 세금 규칙을 수정해주세요 라고 시켰고 -> 엔지니어는 분명 세금만 건드렸음

그런데 갑자기 할인율 로직이 망가졌다는 voc가 들어옴 (정말 슬프다)

 

이렇게 되면 우리가 이 시스템의 통제권을 잃게 됨...

뭘 건드리면 뭐가 터질지 모르거든...

 

2. OCP (Open-Closed Principle)

새 기능을 추가할 때, 기존 코드를 수정하지 않고 확장할 수 있어야 한다.

 

// (전) NotePads 추가하면 switch문 5군데 수정
case SMALL_ROOM -> 100;
case LARGE_ROOM -> 150;
case NOTEPADS -> ???;  // 여기도, 저기도, 또 저기도...

// (후) 새 클래스만 추가하면 끝
public class NotePads implements CatalogItem {
  public int getUnitPrice() { return 20; }
  public double getTaxRate() { return 0.05; }
  // ...
}

 

 

만약, 기능 하나를 추가하는데 코드를 오만군데 건드려야 한다?? 그건 뭔가 잘못된거임.

한 곳만 수정해서 한 기능을 추가할 수 있어야 한다.

 

기능 추가하는 데 핵심 비즈니스 로직 파일을 건드리는 건 좋지 않은 패턴임!

핵심 비즈니스 로직 파일 Statement.java에서 CatalogItem을 별도의 파일로 분리한다.

그리고, 이 CatalogItem만 건드려서 새 품목인 NotePads를 추가한다.

 

3. DIP (Dependency Inversion Principle)

 

자 그럼 Statement.java에서 CatalogItem.java을 분리했으니 된건가?

댓츠 노노

 

만약 CatalogItem.java에 NotePads를 추가한다면, 재컴파일의 범위는 어디까지일까?

 

일단 CatalogItem.java는 무조건 재컴파일 됨

그리고, Statement.java도 CatalogItem.java에 의존성이 있으므로 재컴파일된다.

 

근데 Statement.java가 진짜 재컴파일 되어야 할까?

CatalogItem에 노트패드 항목 하나 추가했다고, 세금/할인/계산 로직이 재컴파일 되어야 할까?

 

이는 DIP(의존성 역전 원칙) 위반에 해당한다.

-> 세부 구현을 추가해도 핵심 로직은 그대로여야 한다.

 

(전) 핵심 → 세부

Statement (핵심 로직)
    │
    └──→ CatalogItem (enum)
              │
              ├── SMALL_ROOM 값
              ├── LARGE_ROOM 값
              └── NOTEPADS 값  ←── 이거 바뀌면 Statement도 영향

(후) 세부 → 핵심

Statement (핵심 로직)
    │
    └──→ CatalogItem (interface)  ←── 추상화된 계약
              ↑
              │ implements
              │
    ┌─────────┼─────────┐
    │         │         │
SmallRoom  LargeRoom  NotePads  ←── 이거 추가해도 위쪽은 영향 없음

 

CatalogItem을 enum에서 interface로 바꾸고, 

내부의 각 항목을 별도 클래스로 만든다.

핵심 비즈니스 로직 Statement는 CatalogItem만 바라보도록 한다.

 

이제 NotePads를 추가할 때 NotePads.java 새 파일로 추가하면 된다.

이제 새로 추가된 이 NotePads.java 파일만 재컴파일 된다.

 

이전에는 Statement -> CatalogItem -> 세부 항목 // 이렇게 화살표가 흐르는데,

이제 Statement -> CatalogItem <- 세부 항목 // 이렇게 화살표 방향이 바뀐다

 

이렇게 화살표 방향이 바뀌는게 "의존성 역전"이다.

 


YAGNI - You Aren't Going to Need It

저자는 이 리팩토링에 덧붙여서 이렇게 말한다.

 

누군가는 이걸 YAGNI(You Aren't Going to Need It)이라고 할 수 있다.

 

보통 YAGNI는 이런 식으로 흘러간다.

나중에 필요할 것 같은데 미리 만들까? -> YAGNI! 지금 필요 없으면 미리 만들지마!

 

그래서, 이 리팩토링이 파일 3개를 파일 10개로 쪼갠 과잉 설계라고 말할 수 있다고 한다.

 

하지만, 저자가 설정한 시나리오에서는 회사가 계속 확장하고, 계속 새로운 기능이 필요한 상황이었다.

새로운 상품, 새로운 세금 규칙, 할인 규정이 추가될 것이다.

그리고 이미 코드는 레거시 범벅 상태다.

 

그럼 지금 비용을 투자해서 바꿔두는게,

나중에 아주 큰 리소스를 들여서 대규모 리팩토링을 하는 것보다 낫다.

(정말 남일 같지 않군)

 

YAGNI는 '필요 없으니까 지금 하지마'가 아닌, '비용을 계산해봐, 지금 진짜 필요해?'라고!


리팩토링은 이름을 개선할 기회!

 

저자는 처음에 Statement로 시작한 파일 이름을 마지막에 Rental Order로 바꾸게 된다.

 

(전)
Statement가 ItemList를 가지고 있고,
ItemList의 getItems()를 호출하면,
Bonus들이 적용된 Item 배열이 반환됨

(후)
RentalOrder(렌탈 주문)가 RentalReceipt(영수증)를 가지고 있고,
RentalReceipt의 finalize(마무리)를 호출하면,
Bonus들이 적용된 최종 영수증이 반환됨

 

추상적인 이름이 리팩토링 과정에서 명확한 이름으로 변경됨!

 

작은 규모의 프로젝트에서는 이름에 일관성이 없어도, 머릿속에서 금방 개념을 추적할 수 있다.

하지만, 규모가 커지면 머릿속에서 개념을 추적하기 어렵고, 이름의 역할이 중요해진다!

 

그래서, 리팩토링을 하면서 처음에 지었던 이름을 다시 정정하면서 정확한 이름을 붙여줄 수 있다.


Architectural Boundary

┌──────────────────────────────────────────┐
│  고수준 (핵심 로직)                          │
│  RentalOrder, RentalReceipt,             │
│  CatalogItem(interface), Bonus(interface)│
├─────────────── 경계선 ────────────────────┤
│  저수준 (세부 구현)                         │
│  SmallRoom, LargeRoom, Coffee,          │
│  CookieBonus, NotePadBonus...           │
└─────────────────────────────────────────┘

 

아키텍쳐 경계선은 배포 단위를 구분한다!

같이 빌드하고 같이 배포하는 파일 묶음 경계를 잘 나눠두면, 일부만 바뀌었을 때 일부 배포만 해도 된다.

 

항상 저수준이 고수준을 바라보는 방향이어야 한다!

역방향 의존 금지!

흠 fsd structure가 생각나는군.

 

경계선을 잘 나누고, 화살표가 저수준->고수준을 향하도록 유지하면,

세부 구현을 바꿔도 핵심 로직이 안전하게 지켜진다!

 


결론

그래서, 여전히 '이렇게까지 해야 됨?'이라는 의문을 가질 수 있다.

저자도 trade-off가 있다는 점을 인정했다.

 

장점 단점
성장을 위한 공간 확보 파일/모듈 수 증가
변경 시 영향 범위 최소화 코드 구조가 복잡해짐
독립적인 배포 가능 인지적 부하 증가
새 기능 = 새 파일 추가 일부 성능 저하

 

구조를 추가하면, 복잡성이 증가한다.

당연함.

한 파일에 5000줄 때려넣으면 1depth고, 이를 500개의 파일로 분리하면 5depth 이상이 되겠지.

 

그런데도 왜 해야하지?

'지금 복잡성 10을 추가해서, 나중에 복잡성 100을 막는다'

 

그리고, 이런 구조 분리로 인해 성능 저하가 발생할 수 있으나, 측정하기 어려울 정도로 미미하다고 함.

 


AI의 한계

이번에도 Grok3 딸깍을 시도해본 저자, 결과는 어땠을까?

 

DIP를 지키지 않고, 고수준/저수준을 분리하려는 시도도 없었다고 한다.

 

LLM은 유용하지만, 아키텍쳐를 이해하는 인간을 대체하지는 못한다.

 

하수: 하 오늘도 인간 밥그릇 지켰다! 인간 1승! 우효 www

고수: 아키텍쳐를 이해하지 못하면 대체되겠구나.

 

그럼 밥그릇을 지키기 위해 아키텍쳐를 공부하러 떠나겠습니다^^

 


 

작성 시작: 2025.12.17. 23:16

작성 종료: 2025.12.18. 01:35

 

그럼 다음에 또 만나요~^^

728x90