[Clean Code] Ⅻ. 창발성(創發性)

Clean Code 3판을 읽고 정리한 글입니다

Ⅻ. 창발성(創發性)

  • 비로소 창, 쏠 발, 성품 성
  • 불시에 솟아 나는 특성, emergent property, emergence
  • 단순한 결합이 복잡한 결과를 나타내는 것을 의미
    • 예) 인간의 뇌 : 하나의 뉴런은 인식 능력이 없으나 수십억개가 결합하면 자기 인식 발생
  • 명령을 내리는 조정자 없이 각 부분의 의사소통으로 자기 조직화를 이루게 되고
    이러한 밑으로 부터의 힘은 예기치 못한 기능을 발현하는 힘 Ex) 집단 지성
  • 창발적 설계 : 어떤 규칙과 원칙에 따라 설계를 하게 되면, 그것들이 모여 아주 좋은 거시적 설계가 된다는 원리?

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

단순한 설계 규칙 4가지(4 Rules of Simple Design)

  • 켄트백이 90년에 “Extreme Programming Explained”에서 설명
  • 착실하게 따르기만 하면
    • 우수한 설계가 나오며
    • 코드 구조와 설계 파악이 쉬워지고
    • 따라서 SRP, DIP같은 원칙 적용이 쉬워지며
    • 우수한 설계의 창발성을 촉진한다
  • 규칙 내용 : 이를 따르면 Design is Simple하다!(..라고 켄트백이 말했다;) 중요도 순서로 나열
    1. 모든 테스트를 실행한다
    2. 중복을 없앤다
    3. 프로그래머 의도를 표현한다
    4. 클래스와 메서드 수를 최소로 줄인다

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

First, 설계는 의도한 대로 돌아가는 시스템을 내놓아야
  • 문서로는 완벽히 설계했지만, 시스템이 의도한 대로 돌아가는지 검증할 간단한 방법이 없다면?
  • 문서 작성을 위해 투자한 노력에 대한 가치는 인정받기 힘들다.
테스트가 불가능한 시스템은 검증도 불가능
  • 테스트를 철저히 거쳐 모든 테스트 케이스를 항상 통과하는 시스템은 ‘테스트가 가능한 시스템’
  • 테스트가 불가능한 시스템은 검증도 불가능하다
  • 논란의 여지는 있지만, 검증이 불가능한 시스템은 절대 출시하면 안 된다
철저한 테스트가 가능한 시스템 지향 → 더 좋은 설계가 결과로
  • 다행스럽게도, 테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다
    • 크기가 작고 목적 하나만 수행하는 클래스가 나온다
    • SRP를 준수하는 클래스는 테스트가 훨씬 더 쉽다
    • 테스트를 더 많이 작성하면 할수록 프로그래머가 더 테스트하기 간단하게 코드를 작성할 수 있게 도와준다
  • 따라서 철저한 테스트가 가능한 시스템을 만들면 더 나은 설계가 얻어진다
  • 자동으로 결합도도 낮아짐
    • 결합도가 높으면 테스트 케이스를 작성하기 어렵다. 따라서 지금까지와 마찬가지로
    • 테스트 케이스를 많이 작성할수록 개발자는 DIP와 같은 원칙을 적용하고 의존성 주입(Dependency Injection),인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춘다
    • 따라서 설계 품질은 더욱 높아진다
  • 결론
    • 놀랍게도 “테스트 케이스를 만들고 계속 돌려라”라는 간단하고 단순한 규칙을 따르면
    • 시스템은 낮은 결합도와 높은 응집력이라는, 객체 지향 방법론이 지향하는 목표를 저절로 달성한다
    • 즉, 테스트 케이스를 작성하면 설계 품질이 높아진다.

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

걱정없이 리팩터링~
  • 테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리해도 괜찮다
  • 구체적으로는 코드를 점진적으로 리팩터링 해나간다
    • 코드 몇 줄을 추가할 때마다 잠시 멈추고 설계를 조감한다
  • 새로 추가하는 코드가 설계 품질을 낮춘다면??
    • 깔끔히 정리한 후 테스트를 돌려 기존 기능을 깨뜨리지 않았다는 사실을 확인
  • 코드를 정리하면서 시스템이 깨질까 걱정할 필요가 없다. 테스트 케이스가 있으니까!
sw 설계 품질 기법 적용
  • 리팩터링 단계에서는 소프트웨어 설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮다
  • 응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하는 등 다양한 기법을 동원한다
  • 또한 이 단계는 단순한 설계 규칙 중 나머지 3개를 적용해서
    • 중복 제거
    • 프로그래머 의도 표현
    • 클래스 메서드를 최소한으로 줄인다

중복을 없애라

  • 우수한 설계에서 중복은 커다란 적이다
  • 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻하기 때문
  • 중복은 여러 가지 형태로 표출된다
    • 똑같은 코드 → 당연히 중복
    • 비슷한 코드 → 더 비슷하게 고쳐주면 리팩터링이 쉬워진다
    • 구현 중복도 중복의 한 형태
      집합 클래스에 다음 메서드가 있을 때
      1
      2
      int size() {}
      boolean isEmpty{}
      구현
  • 각 메서드를 따로 구현하는 방법도 있다
  • 하지만 size()가 개수를 반환하는 로직
  • isEmpty가 이를 이용하면 중복 구현할 필요 없음
    1
    2
    3
    boolean isEmpty() {
    return 0 == size();
    }

깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다
다음 코드를 살펴보자.

scaleTOOneDimension 메서드와 rotate 메서드를 살펴보면 일부 코드가 동일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void scaleToOneDimension(float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float)(Math.floor(scalingFactor * 100) * 0.01f);

RenderedOp newImage = ImageUtilities.getScaledImage(image, scalingFactor, scalingFactor);
image.dispose();
System.gc();
image = newImage;
}

public synchronized void rotate(int degrees) {
RenderedOp newImage = ImageUtilities.getRotatedImage(image, degrees);
image.dispose();
System.gc();
image = newImage;
}
중복제거 : 적은 양이지만 공통 코드를 새 메서드로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void scaleToOneDimension(float desiredDimension, float imageDimension) {
if (Math.abs(desiredDimension - imageDimension) < errorThreshold)
return;
float scalingFactor = desiredDimension / imageDimension;
scalingFactor = (float) Math.floor(scalingFactor * 10) * 0.01f);
replaceImage(ImageUtilities.getScaledImage(image, scalingFactor, scalingFactor));
}

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

private void replaceImage(RenderedOpnewImage) {
image.dispose();
System.gc();
image = newImage;
}

리팩터링

  • 아주 적은 양이지만 공통적인 코드를 새 메서드로
  • 뽑고 보니 클래스가 SRP를 위반
  • 그러므로 새로 만든 replaceImage 메서드를 다른 클래스로 옮겨도 좋겠다고 한다
  • 그러면 새 메서드의 가시성이 높아진다
  • 따라서 다른 팀원이 새 메서드를 좀 더 추상화해 다른 맥락에서 재사용할 기회를 포착할지도 모른다
  • 이런 ‘소규모 재사용’은 시스템 복잡도를 극적으로 줄여준다
  • 소규모 재사용을 제대로 익혀야 대규모 재사용이 가능하다

템플릿 메서드 패턴 : 고차원 중복을 제거할 목적으로 자주 사용하는 기법

중간의 법정일수 계산을 제외하면 두 메서드는 거의 동일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class VacationPolicy {
public void accrueUSDDivisionVacation() {
// 지금까지 근무한 시간을 바탕으로 휴가 일수를 계산하는 코드
// ...
// 휴가 일수가 미국 최소 법정 일수를 만족하는지 확인하는 코드
// ...
// 휴가 일수를 급여 대장에 적용하는 코드
// ...
}

public void accrueEUDivisionVacation() {
// 지금까지 근무한 시간을 바탕으로 휴가 일수를 계산하는 코드
// ...
// 휴가 일수가 유럽연합 최소 법정 일수를 만족하는지 확인하는 코드
// ...
// 휴가 일수를 급여 대장에 적용하는 코드
// ...
}
}
템플릿 메서드 패턴 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract public class VacationPolicy {
public void accrueVacation() {
caculateBseVacationHours();
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() {
// 유럽연합 최소 법정 일수를 사용한다.
}
}

하위 클래스는 중복되지 않는 정보만 제공해 accrueVacation 알고리즘에서 빠진 ‘구멍’을 메운다.

표현하라

자신만 이해하는 코드의 문제

  • 아마 우리 대다수는 엉망인 코드를 접한 경험이 있다
  • 아마 우리 대다수는 스스로 엉망인 코드를 내놓은 경험도 있다
  • 자신이 이해하는 코드를 짜기는 쉽다
    • 코드를 짜는 동안에는 문제에 푹 빠져 코드를 구석구석 이해하니까
  • 하지만 나중에 코드를 유지보수할 사람이 그만큼 문제를 깊이 이해할 가능성은 희박

코드는 개발자의 의도를 분명히 표현해야 한다

  • 소프트웨어 프로젝트 비용 중 대다수는 장기적인 SM에 들어간다
  • 코드를 변경하면서 버그의 싹을 심지 않으려면 SM 개발자가 시스템을 제대로 이해해야 한다
  • 하지만 시스템이 점차 복잡
    • SM 개발자가 시스템 이해하느라 보내는 시간은 점점 늘어남
    • 동시에 코드를 오해할 가능성도 점점 커짐
  • 그러므로 코드는 개발자의 의도를 분명히 표현해야 한다
    • 개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다
    • 그래야 결함이 줄어들고 유지보수 비용이 적게 든다.

How?

  1. 좋은 이름을 선택
  • 이름과 기능이 완전히 딴판인 클래스나 함수로 개발자를 놀라게 해서는 안 된다
  1. 함수와 클래스 크기를 가능한 한 줄인다
  • 작은 클래스와 작은 함수는 이름 짓기도 쉽고, 구현하기도 쉽고, 이해하기도 쉽다
  1. 표준 명칭을 사용
  • 예를 들어, 디자인 패턴은 의사소통과 표현력 강화가 주요 목적
  • class가 COMMAND나 VISITOR등의 표준 패턴을 사용하는 경우 클래스 이름에 패턴 이름을 넣어준다
  • 그러면 다른 개발자가 클래스 설계 의도를 이해하기 쉬워진다.
  1. 단위 테스트 케이스를 꼼꼼히 작성
  • 테스트 케이스는 소위 ‘예제로 보여주는 문서’다
  • 다시 말해, 잘 만든 테스트 케이스를 읽어보면 클래스 기능이 한눈에 들어온다
  1. 노력과 주의 : 표현력을 높이는 가장 중요한 방법
  • 흔히 코드만 돌린 후 다음 문제로 직행하는 사례가 너무도 흔하다
  • 나중에 읽을 사람을 고려해 조금이라도 읽기 쉽게 만드려는 충분한 고민은 거의 찾기 어렵다
  • 하지만 나중에 코드를 읽을 사람은 바로 자신일 가능성이 높다는 사실을 명심하자
  • 그러므로 자신의 작품을 조금 더 자랑하자
    • 함수와 클래스에 조금 더 시간을 투자하자
    • 더 나은 이름을 선택
    • 큰 함수를 작은 함수 여럿으로 나누고
    • 자신의 작품에 조금만 더 주의를 기울이자
    • 주의는 대단한 재능이다.

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

(가능한)최소의 의미

  • 중복을 제거하고, 의도를 표현하고, SRP를 준수한다는 기본적인 개념도 극단으로 치달으면 득보다 실이 많아진다
  • 클래스와 메서드 크기를 줄이자고 조그만 클래스와 메서드를 수없이 만드는 사례도 없지 않다
  • 그래서 이 규칙은 함수와 클래스 수를 가능한 한 줄이라고 제안한다

독단을 멀리하고 실용성 선택

  • 때로는 무의미하고 독단적인 정책 탓에 클래스 수와 메서드 수가 늘어나기도 한다
  • 클래스마다 무조건 인터페이스를 생성하라고 요구하는 구현 표준이 좋은 예
  • 자료 클래스와 동작 클래스는 무조건 분리해야 한다고 주장하는 개발자도 좋은 예
  • 가능한 독단적인 견해는 멀리하고 실용적인 방식을 택해야 한다

해당 규칙의 목표와 우선순위에 대하여

  • 목표는 함수와 클래스 크기를 작게 유지하면서 동시에 시스템 크기도 작게 유지하는 데 있다
  • 하지만 이 규칙은 간단한 설계 규칙 네 개 중 우선순위가 가장 낮다
  • 다시 말해, 클래스와 함수 수를 줄이는 작업도 중요하지만, 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 더 중요하다는 뜻

결론

  • 경험을 대신할 단순한 개발 기법은 없음
  • 이 책에서 소개하는 기법은 저자들이 수십 년 동안 쌓은 경험의 정수
  • 단순한 설계 규칙을 따른다면 (오랜 경험 후에야 익힐) 우수한 기법과 원칙을 단번에 활용할 수 있다

Related POST

공유하기