Book Notes

[개발서적] Clean Code 12. 창발성

feel2 2025. 10. 28. 22:43

[창발적 설계로 깔끔한 코드를 구현하자]

착실하게 따르기만 하면 우수한 설계가 나오는 간단한 규칙 네가지가 있다면?

우리들 대다수는 켄트 백이 제시한 단순한 설계 규칙 네 가지가 소프트웨어 품질을 크게 높여준다고 믿는다.

  • 모든 테스트를 실행한다.
  • 중복을 없앤다.
  • 프로그래머 의도를 표현한다.
  • 클래스와 메서드 수를 최소로 줄인다.

위 목록은 중요도 순이다.

 

[단순한 설계 규칙1 : 모든 테스트를 실행하라]

무엇보다 먼저, 설계는 의도한 대로 돌아가는 시스템을 내놓아야 한다.

테스트를 철저히 거쳐 모든 테스트 케이스를 항상 통과하는 시스템은 ‘테스트가 가능한 시스템’이다.

 

다행스럽게도, 테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다.

테스트가 가능한 시스템은 다음과 같은 특징이 있다.

  • 크기가 작고 목적 하나만 수행하는 클래스가 나온다.
  • SRP를 준수하는 클래스는 테스트가 훨씬 쉽다.
  • 테스트 케이스가 많을수록 개발자는 테스트가 쉽게 코드를 작성한다.

따라서 철저한 테스트가 가능한 시스템을 만들면 더 나은 설계가 얻어진다.

 

놀랍게도 “테스트 케이스를 만들고 계속 돌려라”라는 간단하고 단순한 규칙을 따르면 시스템은 낮은 결합도높은 응집력이라는, 객체 지향 방법론이 지향하는 목표를 저절로 달성한다.

 

[단순한 설계 규칙 2~4: 리팩터링]

테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리해도 괜찮다.

구체적으로는 코드를 점진적으로 리팩터링 해나간다. 코드 몇 줄을 추가할 때마다 잠시 멈추고 설계를 조감한다.

 

코드를 정리하면서 시스템이 깨질까 걱정할 필요는 없다. 테스트 케이스가 있으니까!

리팩터링 단계에서는 소프트웨어 설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮다.

  • 응집도를 높인다.
  • 결합도를 낮춘다.
  • 관심사를 분리한다.
  • 시스템 관심사를 모듈로 나눈다.
  • 함수와 클래스의 크기를 줄인다.
  • 더 나은 이름을 선택한다.

또한 이 단계에서 단순한 설계 규칙 나머지 3개인 중복을 제거, 프로그래머 의도를 표현, 클래스의 메서드 수 최소화 등을 같이 적용해도 좋다.

 

[중복을 없애라]

우수한 설계에서 중복은 커더란 적이다. 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻하기 때문이다.

깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다.

public void scaleToOneDimension(
  float desiredDimension, float imageDimension) {
  if (Math.mabs(desiredDimension - imageDimension) < errorThreshold) return;
  float scalingFactor = desiredDimension / imageDimension;
  scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
  
  RenderedOp newImage = ImageUtilities.getScaledImage(
    image, scalingFactor, scalingFactor);
  image.disponse();
  System.gc();
  image = newImage; 
}

public synchronized void rotate(int degrees) {
  RenderedOp newImage = ImageUtilities.getRotatedImage(
    image, degrees);
  image.disponse();
  System.gc();
  image = newImage;
}

 

scaleToOneDimension 메서드와 rotate 메서드를 살펴보면 일부 코드가 동일하다.

다음과 같이 코드를 정리해 중복을 제거한다.

public void scaleToOneDimension(
  float desiredDimension, float imageDimension) {
  if (Math.mabs(desiredDimension - imageDimension) < errorThreshold) return;
  float scalingFactor = desiredDimension / imageDimension;
  scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);
  replaceImage(ImageUtilities.getScaledImage(
    image, scalingFactor, scalingFactor));
}

public synchronized void rotate(int degrees) {
  replaceImage(ImageUtilities.getRotatedImage(image, degrees);
}

public void replaceImage(RenderedOp newImage) {
  image.disponse();
  System.gc();
  image = newImage; 
}

 

아주 적은 양이지만 공통적인 코드를 새 메서드로 뽑고 보니 클래스가 SRP를 위반한다.

 

그러므로 replaceImage는 다른 클래스로 옮겨도 좋겠다. 그러면 새 메서드의 가시성이 높아진다.

TEMPLATE METHOD 패턴은 고차원 중복을 제거할 목적으로 자주 사용하는 기법이다. 예를 살펴보자.

 

고차원? 저차원?

더보기
더보기

즉, 무엇(What)을 하는지 vs 어떻게(How) 하는지 의 차이라고 볼 수 있다.

 

클린 코드(로버트 C. 마틴)에서 말하는 “고차원(high-level) 메서드”“저차원(low-level) 메서드”“추상화 수준(abstraction level)” 을 기준으로 나뉜다.

즉, 무엇(What)을 하는지 vs 어떻게(How) 하는지 의 차이라고 볼 수 있다.

예를 들어

// 고차원 메서드
public void processOrder(Order order) {
    verifyPayment(order);
    allocateInventory(order);
    shipOrder(order);
    notifyCustomer(order);
}

→ “주문을 처리한다”는 비즈니스 의도를 담고 있고,

세부 구현은 하위 단계(저차원 메서드)로 위임하고 있다.

// 저차원 메서드
private void allocateInventory(Order order) {
    for (Item item : order.getItems()) {
        Inventory inv = inventoryRepo.findBySku(item.getSku());
        inv.reserve(item.getQuantity());
    }
}

→ 구체적으로 “어떻게 재고를 할당할지” 를 기술한다.

결론적으로

  • 고차원 함수는 비즈니스 개념만 말해야 하고
  • 저차원 함수는 구현 세부만 말해야 한다.
  • 이 두 수준이 섞이면 읽는 사람은 머릿속에 추상 레벨이 계속 바뀌어서 이해가 어려워진다.

간단한 구조로 비유하면

main()                   ← 가장 고차원 (“무엇을 한다”)
  ↓
usecase/service()        ← 고차원 (비즈니스 절차)
  ↓
repository(), adapter()  ← 중간 수준 (기술 행위)
  ↓
SQL, I/O, loop           ← 저차원 (구현 세부)
public class VacationPolicy {
  public void accureUSDivisionVacation() {
    // 지금까지 근무한 시간을 바탕을 휴가 일수를 계산하는 코드
    // ...
    // 휴가 일수가 미국 최소 법정 일수를 만족하는지 확인하는 코드
    // ...
    // 휴가 일수를 급여 대장에 적용하는 코드
    // ...
  }
  
  public void accureEUDivisionVacation() {
    // 지금까지 근무한 시간을 바탕을 휴가 일수를 계산하는 코드
    // ...
    // 휴가 일수가 유럽연합 최소 법정 일수를 만족하는지 확인하는 코드
    // ...
    // 휴가 일수를 급여 대장에 적용하는 코드
    // ...
  }
}

 

최소 법정 일수를 계산하는 코드만 제외하면 두 메서드는 동일하다. 최소 법정 일수를 계산하는 알고리즘은 직원 유형에 따라 살짝 변한다.

여기에 TEMPLATE METHOD 패턴을 적용해 눈에 들어오는 중복을 제거한다.

abstract public class VacationPolicy {
  public void accureVacation() {
    calculateBaseVacationHours();
    alterForLegalMinimums();
    applyToPayroll();
  }
  
  private void calculateBaseVacationHours() { /* ... */ };
  abstract protected void alterForLegalMinimums();
  private void applyToPayroll() { /* ... */ };
}

----

public class USVacationPolicy extends VacationPolicy {
  @Override protected void alterForLegalMinimums() {
    // 미국 최소 법정 일수를 사용한다.
  }
}

----

public class EUVacationPolicy extends VacationPolicy {
  @Override protected void alterForLegalMinimums() {
    // 유럽연합 최소 법정 일수를 사용한다.
  }
}

 

[표현하라]

아마 우리 대다수는 엉망인 코드를 접한 경험이 있으리라. 스스로 그런 코드를 내놓은 경험 또한 있으리라.

소프트웨어 프로젝트 비용 중 대다수는 장기적인 유지보수에 들어간다.

그러므로 코드는 개발자의 의도를 분명히 표현해야 한다.

 

다음 얘기하는 것들을 꼭 준수하도록 노력해보자.

  • 좋은 이름을 선택하라
  • 함수와 클래스 크기를 가능한 줄이자
  • 표준 명칭을 사용하자
  • 단위 테스트 케이스는 꼼꼼히 작성하자

하지만 가장 중요한 방법은 노력이다. 나중에 코드를 읽을 사람을 위해서 더 높은 품질의 코드를 위해 노력하자.

 

[클래스와 메서드 수를 최소로 줄여라]

우리의 목표는 함수와 클래스 크기를 작게 유지하면서 동시에 시스템 크기도 작게 유지하는데 있다.

하지만 이 규칙은 우선 순위가 가장 낮다.

가장 중요한 것은 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 더 중요하다는 뜻이다.

 

[결론]

경험을 대신할 단순한 개발 기법이 있을까? 당연히 없다.

하지만 이 장, 이 책에서 소개하는 방법은 저자들이 수십 년 동안 쌓은 경험의 정수다.