본문 바로가기

개발자 강화/개발 독서

[Clean Code 2판] 1-10: One Thing

와!! 10장!!!


Part1. Code

Chapter10. One Thing

“Clean code does one thing well.”

클린 코드는 한 가지를 잘한다.

근데, 그 한 가지의 의미가 무엇일까?

 

저자는 과거 3천줄짜리 C 함수를 담당했다.

누군가 그 함수가 무얼 하느냐고 묻는다면 '그래픽 해석 함수에요!'라고 당당히 대답했을거라고 했다.

 

하지만, 그 3천줄짜리를 '그래픽 해석'으로 퉁쳐도 될까?

실제로 쪼개보면 더 많은 일을 하고 있을 것이다.

'그래픽 해석'은 꽤나 추상적인 개념이고, 이건 더 작은 것들로 분해될 수 있다.

 

이렇듯, '한 가지''합리적으로 정의'하는 것 또한 중요하다.

 

Extract Method Refactoring

IDE에는 Refactor라는 기능이 있고, 그 중 하나가 Extract Method이다.

 

[원래 함수]
function processOrder() {
  // 코드 블록 A (선택)
  // 코드 블록 B
  // 코드 블록 C
}

    ↓ Extract Method 실행

[리팩터링 후]
function processOrder() {
  extractedFunction();  // 코드 블록 A가 여기로 이동
  // 코드 블록 B
  // 코드 블록 C
}

function extractedFunction() {
  // 코드 블록 A
}

 

이런 식으로 선택한 함수를 새 함수로 분리할 수 있다.

 

그럼... 이렇게 분리할 수 있었다는 뜻은? 원본 함수가 일을 한 가지 이상 하고 있었다는 뜻이다.

 

A function does one thing if you cannot meaningfully extract another function from it.

 

반대로, 한 함수에서 의미있는 다른 함수를 추출할 수 없다면, 그 함수가 한 가지 일을 한다는 것을 방증하는 것이다.

 

그렇다면 "의미있는"은 구체적으로 무엇인가?

 

Stepdown Rule

의미있는 추상화 = 함수 이름이 구현보다 더 추상적이어야 함

(어라 이거 이전 장에서 나왔던 거 아닌가)

 

claude와 함께 예시로 확인해보자

1. 만약 너무 작게 추출한 경우

// Before
function saveUser(user: User) {
  localStorage.setItem('user', JSON.stringify(user));
}

// After - 의미없는 추출
function saveUser(user: User) {
  doSave(user);  // 그냥 넘기기만 함
}

function doSave(user: User) {
  localStorage.setItem('user', JSON.stringify(user));
}

saveUser가 아무 일도 안하고 doSave를 호출함

추상화를 해서 얻는 이득이 없다

 

2. 너무 많이 추출한 경우

// Before
function resetForm() {
  name = '';
  email = '';
}

// After - 의미없는 추출
function clearName() { name = ''; }
function clearEmail() { email = ''; }

함수 이름만 봐도 구현이 너무 투명하게 보임

추상화가 전혀 되지 않음.

 

3. 적절한 추상화

function resetForm() {   // What: 폼을 초기화한다
  name = '';             // How: 구체적인 필드들을 비운다
  email = '';
}

resetForm이라는 이름은 의도를 담고 있고, 본문은 구현 세부사항을 담고 있다.

Stepdown rule을 만족한다.


Extract Till You Drop!

모든 함수가 한 가지를 하도록 보장하려면,

'더 이상 의미있게 추출할 수 없을 때까지 계속 추출하라'

 

추출을 계속하면 함수가 일반적으로 3~6줄이 될 것이라고 한다. (진짜...?)

뭐, switch문이나 formating문은 예외라고 한다.

if (shouldDeleteRecord(r))
  deleteRecord(r);

if/else문과 for/while 루프문의 본문을 추출하고, 조건문도 추출해서

일종의 산문처럼 된다고 한다 (산문을정말좋아하는군...)


This Shouldn’t Be Controversial

저자의 이런 'extract till you drop'에 항상 반론하는 사람들이 있었고, 

그 주요 반론 5가지를 나열하고 반박하는 시간...

 

1. Drowning

오! 암 드라아아아우니이잉

 

반박: 작은 함수가 너무 많아지면, 작은 함수들의 바다에 빠져 drowning(익사)할 것이다

반박을 반박: 잘 이름 붙여진 계층 구조가 있으면 익사하지 않는다. 함수 이름과 위치가 이정표 역할을 한다.

 

저자는 과거에 gi 함수를 쓸 때... 이런 식으로 기억했다.

x 축 스케일링은 큰 주석 블록 후 세 번째 들여쓰기에 있었지... (지리적으로 외움...)

 

만약 후임자가 나타난다면, 이런 맥락까지 전부 인수인계 하진 못할거고

3천줄짜리 코드 늪에서 헤매다가 운 좋게 관련 함수를 발견했을 것이라고 말했다.

(우리 레거시는 8천줄이었는데 3천줄은 머...)

 

만약 setScalingOnX라는 함수가 있었다면, 금방 찾을 수 있었겠지.

이처럼 작게 잘 쪼개진 함수는 이름이 곧 좋은 이정표가 된다.

 

2. Small Functions Don't Obscure Intent

작은 함수는 의도를 가리지 않는다.

 

반박: 함수가 흩어져서 한 곳에서 모두 보이지 않아 가려질까 걱정된다.

반반박: 잘 설계되고 잘 이름 붙여진 namespace, class, function 구조는 의도를 이름에 드러낸다.

 

뭐 ctrl+f 잘되는 ide 쓰는 시대에 2번은 걱정할 문제는 아닐 듯

이름만 잘 짓는다면

 

3. Performance

반박: 작은 함수를 모두 호출하는 성능 패널티가 어플리케이션을 느리게 만들까 걱정된다.

반반박: Premature optimization is the root of all evil

성능 걱정은 측정 후에 해라. 미리 최적화하지 마라.

 

만약 당신이 극초음속 미사일 유도 시스템을 작성한다면 나노세컨드가 꽤나 중요할지도?

하지만 당신이 여행 웹사이트를 만든다면, 나노세컨드는 당신이 그렇게까지 고민할 부분은 아니다.

 

옛날옛날에는 함수를 호출할 때 몇 마이크로세컨드가 걸렸다. (그냥 진짜 성능이 구려서)

그래서 함수 호출 횟수를 걱정하고 고려했었다.

하지만, 지금은 세상이 좋아져서 함수 호출은 몇 나노세컨드 밖에 안걸린다.

그리고 성능 좋은 컴파일러가 알아서 인라이닝(함수 호출을 함수 본문 코드로 직접 대체) 해줄거다.

 

그리고 만약 진짜 측정했는데 실제로 느려졌다?

그러면 그때 가서 해결하면 된다. 미리 걱정할 문제가 아니다.

 

4. Bouncing Around

반박: 무언가를 이해하려면 이 함수에서 저 함수로 계속 이동해야할 것 같다.

반반박: 그건 작성자가 코드를 잘못 배치했을 경우임! 호출 순서대로 정렬하면 해결된다.

 

높은 수준(추상적) -> 낮은 수준(구체적)

이 순서대로(stepdown rule대로) 잘 정리한다면 문제될 일 없음

 

5. Entanglement

반박: 결과 함수들이 너무 얽혀서 독립적으로 이해하기 어려울 것 같다.

절반 정도 반박?: 판단의 문제이다. 현명한 선택을 하라.

 

낮은 수준 함수가 높은 수준 함수의 작동에 의존적일 수 있다.

낮은 수준의 함수를 이해하기 위해 높은 수준의 함수를 잘 이해하고 있어야할 수 있다.

그런데, 이 정도가 너무 심하다면? 차라리 추출하지 않는 것이 나을 수 있다.

 

하지만, 그정도로 얽힘 정도가 심하지 않다면 추출해라.

추출된 자식 함수가 잘 정의된 이름을 가지고 부모 함수 뒤에 바로 나타나면 된다.

 

결국 개발자 본인의 판단 문제이다!

 

이건 7장의 존 오스터하우트의 반론 섹션에 있는 내용과 같다

https://developer-dreamer.tistory.com/204

 


What Are Large Functions Anyway?

대형 함수란 무엇인가?

 

저자의 코드를 예시로 시작한다.

gi(graphic interpreter의 약자인듯)라는 3천줄짜리 java 함수가 있다.

 

gi를 쭉쭉 스크롤하다 if문을 만났다!

if문의 본문을 새 메서드로 추출하기로 결정했다.

if (...) {
  i++;
  j++;
 }

 

java는 함수에서 값을 하나만 반환할 수 있기 때문에

두 개의 지역 변수를 수정하는 코드는 추출할 수 없다!

 

그럼 지역 변수를 인스턴스 변수로 추출하면 추출가능해진다

public class GraphicInterpreter {
  private int i;
  private int j;
  
  public void gi() {
    if (...) {
      f();
    }
  }
  
  private void f() {
    i++;
    j++;
  }
}

 

지역변수에서 -> 인스턴스 변수로 승격시키면서

스코프가 넓어졌다.

 

이게 캡슐화의 핵심 원칙인 '변수의 스코프를 최소화하라'는 것을 위배한다고 볼 수 있다

 

하지만 이렇게 i와 j를 인스턴스로 뽑아냄으로서

'이 변수들을 조작하는 더 많은 if와 while문을 추출해낼 수 있다'

 

그리고 이런 인스턴스 변수와 메서드의 묶음은

변수 집합과 그걸 조작하는 메서드 묶음인 '클래스'라고 부를 수 있다.

 

지역 변수를 전역 변수로 끌어올려서

캡슐화 구조를 일시적으로 위배하더라도,

결과적으로 대형 함수에 숨어있는 클래스 구조를 발굴할 수 있었다!

 

3천줄짜리 거대 함수보다 잘 구조화된 클래스가 훨씬 낫다고 저자는 말한다.

 


Video Store 예제

 

본문에서 리팩토링 예제를 소개한다.

Refactoring(저자: Martin Fowler) 초판에서 나온 Video Store라는 유명한 예제라고 한다

(그런게 있다는 걸 방금 처음알았다... 그렇군)

 

코드 전체는 책 원문에서 확인!!

리팩토링 과정만 정리해보겠다

 

1. 테스트 개선

문제

- 테스트가 모두 ui 문자열을 통해 진행된다 (기획이나 디자인이 변경되면 테스트가 깨지기 쉽다)

- ui 문자열은 테스트의 의도를 정확히 설명해주지 않는다

 

개선

function makeStatement() {
  clearTotals();
  return makeHeader() + makeDetails() + makeFooter();
}

- 고수준의 의도가 바로 보인다: 합계 초기화 -> 헤더+상세+푸터

- 저수준 세부사항은 각 함수 안에 숨겨진다

 

추출하면 '코드가 의도를 숨기게 된다'는게 흔한 걱정 포인트인데,

장황한 테스트 코드를 추상화해서 오히려 한 눈에 파악하기 쉽게 만들었다는 뜻이다

 

2. 비즈니스 규칙 분리

- 기존 테스트 코드에는 determineAmount, determinePoints가 Customer 클래스에 있었다.

- determineAmount는 대여요금계산, determinePoints는 포인트 계산이다

- Customer 클래스에 있지만 Customer 데이터를 안쓰고, Rental 쪽 데이터만 쓰기 때문에 Rental 클래스로 분리한다

 

- 1번에서 고쳐놓은 테스트는 이렇게 근본 설계가 변경되어도 문제없이 작동한다!

- 테스트가 변경을 견디는 설계를 가지고 있다는 뜻 (잘 짠 테스트)

 

- 또, 기존에 이 video store 코드는 이런 구조를 가지고 있었음

Customer(고객) -> Rental(대여) -> Movie(영화)

- 그런데 determineAmount와 determinePoints를 분리함으로서 대여 로직이 Customer 클래스에서 분리됨

- 이제 Customer에는 대여명세서 로직만 남았음! 이제 클래스 이름을 RentalStatement로 바꿀 수 있음

- 고객이라는 추상적인 이름보다는 대여명세서라는 이름이 더 역할이 명확함!

 

3. 다형성으로 switch 제거

switch문을 내버려두면, 시간이 지날수록 비슷한 다른 기능에 같은 구조를 사용하는 식으로 불어날 것!

그럼 변경이 필요할 때, 그 불어난 모든 곳을 찾아 고쳐야 함

- 그렇다면 타입별 클래스로 분리해서 다형성 객체를 적용한다

 

1-7장 Clean Functions에서 나오는 내용이다!

ctrl+f해서 '다형성' 검색하면 나옴

https://developer-dreamer.tistory.com/204

 

[Clean Code 2판] 1-7: Clean Functions

어느새 7장!Part1. CodeChapter7. Clean Functions 옛날 옛적... 함수 호출이 당연하지 않았던 시절이 있었어요 복-붙 시대(1940)- 같은 작업이 필요할 때마다 노트에서 코드를 직접 베껴 씀 호출의 등장(1950)-

developer-dreamer.tistory.com

 

4. 결과

1) 비즈니스 규칙 vs 포맷팅 규칙 분리

리팩토링 후 책임이 분리됨

- RentalStatement(구 Customer): 명세서 포맷팅(헤더, 푸터, 출력 형식)

- Rental: 대여 정보 관리

- RentalType: 비즈니스 규칙 (요금 계산, 포인트 계산)

 

2) 아키텍처적 경계

- 변경이 많은 하위 수준 - 변경이 적은 상위 수준을 나누는 아키텍처적 경계가 생김!

- 하위 수준에서 변경이 생겨도, 상위 수준에 영향이 없음

 

3) OCP (개방-폐쇄 원칙)

- 확장에는 열려있고, 수정에는 닫혀있다

- 새로운 영화 타입을 추가 시, 새 클래스만 추가하면 됨

- 기존 코드는 건드리지 않고, 확장만 함!

 

4) 대형 함수 -> 여러 클래스

- 모든 대형 함수는 클래스를 숨기고 있다

- 수백줄의 함수 하나를 여러 클래스로 쪼갠다

 

Extract Till You Drop을 실천해서,

함수 크기를 줄일 뿐 아니라 '좋은 아키텍처가 드러나게'할 수 있다!

 


Conclusion

Extract Till You Drop을 언제까지 연습해야할까?

 

저자의 스타일은 3-6줄이었지만, 팀과 개인의 선호에 따라 다를 수 있음

'절대적 정답은 없다!'

 

그러나, 함수를 크게 유지한다면

예제에서 함께 살펴본 '설계 문제를 인식할 계기'를 놓칠 수 있다.

추출해봐야 설계 문제를 관찰할 수 있다.

 

Customer 클래스에서 Rental 클래스로 비즈니스 로직을 이동시킨 후,

클래스 이름을 RentalStatement로 바꿨던 것처럼.

 


1단계: 일단 추출해본다

2단계: 이 함수가 여기 있는게 맞는지 판단해본다

3단계: 필요하면 전략적으로 인라인

 

추출->판단->인라인 순서를 시도해볼 것!

 

처음부터 추출해보지 않고 '이건 추출 안해도 돼'라고 하면 설계 문제를 인지하게 어렵다.

 

또한, 함수를 인라인 하면 (추상화하지 않고, 코드 그대로 풀어서 써놓으면)

이 역할을 설명할 수 있는 이름이 사라지기 때문에 신중히 결정해야 한다!

저자는 좀 더 극단적으로 '함수를 인라인하면 그 이름을 파괴하는 것이다'라고 말했다.

// 인라인 전: 이름이 의도를 설명함
if (shouldDeleteRecord(r)) {
  deleteRecord(r);
}

// 인라인 후: 의도가 사라짐
if (r.status === 'expired' && r.refCount === 0) {
  db.remove(r.id);
}

 


 

2026.01.25. 16:41 작성 마침.

작성 시작한 건 2주 전이었던 것 같음!

스터디가 1주일 쉬고, 또 1주일에 1장 읽는 걸로 바뀌면서 조금 늘어졌다

개인적으로 2장씩 꾸준히 읽으려고 마음은 먹었지만, 또 막상 스터디 진도를 따라가게 되는 군...

하고 싶은 게 너무 많아서 그런가 ㅋㅋㅋ

728x90