Clean Code: 냄새와 휴리스틱

jiffydev·2021년 6월 21일
0

Clean Code

목록 보기
13/13

코드에서 나는 '나쁜 냄새'들의 목록

주석

1. 부적절한 정보

주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다. 작성자, 최종 수정일, SPR(Software Problem Report) 번호 등과 같은 메타정보만 넣는다.

2. 쓸모 없는 주석

쓸모 없어질 주석은 아예 달지 않는 것이 좋고, 쓸모 없어진 주석은 빠르게 삭제한다.

3. 중복된 주석

코드만으로 다 설명하지 못하는 부분을 부연하는 것이 주석이다. 코드만으로 설명이 충분하다면 주석은 필요 없다.

4. 성의 없는 주석

주석을 달아야 한다면 시간을 들여서 잘 작성해야 한다.

5. 주석 처리된 코드

주석으로 된 코드는 얼마나 오래된 것인지, 중요한 코드인지 알 길이 없다. 즉시 삭제할 것.

환경

1. 여러 단계의 빌드

빌드는 간단히 한 단계로 끝나야 한다. 한 명령으로 전체를 체크아웃 해서 한 명령으로 빌드할 수 있어야 한다.

2. 여러 단계의 테스트

모든 단위 테스트는 한 명령으로 돌릴 수 있어야 한다.

함수

1. 너무 많은 인수

인수의 개수는 없는 것이 가장 좋고 적을수록 좋다.

2. 출력 인수

보통 인수는 출력이 아닌 입력으로 간주하므로 출력 인수는 직관적이지 않다.

3. 플래그 인수

인수가 boolean이면 두 가지 이상의 기능을 수행한다는 증거다. 함수는 하나의 기능만 수행해야 하므로 플래그 인수는 피한다.

4. 죽은 함수

아무도 호출하지 않는 함수는 삭제한다.

일반

1. 한 소스 파일에 여러 언어 사용

이상적으로는 소스 파일 하나에 언어 하나. 최대한 사용하는 언어 수를 줄인다.

2. 당연한 동작을 구현하지 않는다

함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.
놀람 최소화 원칙 (https://en.wikipedia.org/wiki/Principle_of_least_astonishment)

3. 경계를 올바로 처리하지 않는다

직관에 의존하지 말고 모든 경계를 찾아내 경계 조건을 테스트하는 케이스를 작성할 것

4. 안전 절차 무시

컴파일러 경고를 꺼버리면 빌드는 쉬워지겠지만 끝없는 디버깅에 시달리게 된다.
실패하는 테스트 케이스를 나중으로 미루면 거기에 발목을 잡힌다.

5. 중복

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라.
여러 모듈에서 일련의 switch/case, if/else문으로 똑같은 조건을 확인하는 중복은 다형성으로 대체해야 한다.
알고리즘이 유사하나 코드가 서로 다른 중복일 경우 TEMPLATE METHOD 패턴이나 STRATEGY 패턴으로 중복을 제거한다.

6. 추상화 수준이 올바르지 못하다

추상화로 개념을 분리할 때는 철저해야 한다. 고차원 개념은 기초 클래스에, 저차원 개념은 파생 클래스에 넣어야 한다.
기초 클래스는 구현 정보에 무지해야 한다.

7. 기초 클래스가 파생 클래스에 의존한다

기초 클래스와 파생 클래스를 나누는 이유는 고차원의 기초 클래스를 저차원 파생 클래스로부터 분리해 독립성을 보장하기 위한 것.
기초 클래스는 파생 클래스를 몰라야 한다.

8. 과도한 정보

잘 정의된 모듈은 인터페이스가 작으면서도 많은 동작이 가능하다.
클래스가 제공하는 메서드 수, 함수가 아는 변수 수, 클래스의 인스턴스 변수 수도 적을 수록 좋다.

9. 죽은 코드

죽은 코드란 실행되지 않는 코드.
불가능한 조건을 확인하는 if문이나 throw문이 없는 try문에서의 catch, 아무도 호출하지 않는 유틸리티 함수 등.
죽은 코드를 발견하면 즉시 삭제할 것.

10. 수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다.
지역변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치한다.
비공개 함수는 처음으로 호출한 직후에 정의한다.

11. 일관성 부족

어떤 개념을 특정 방식으로 구현했다면, 유사한 개념도 같은 방식으로 구현한다.

12. 잡동사니

아무도 사용하지 않는 변수, 아무도 호출하지 않는 함수, 정보를 제공하지 못하는 주석 등은 잡동사니다. 제거하라.

13. 인위적 결합

무관한 개념을 인위적으로 결합하지 않는다. enum이 특정 클래스에 속하는 것, 범용 static 함수가 특정 클래스에 속하는 것은 인위적 결합이다.
뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한(잘못된) 위치에 넣어서는 안된다.

14. 기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지, 다른 클래스의 변수와 함수에 관심을 가져서는 안된다.

15. 선택자 인수

선택자 인수는 목적을 기억하기 어려울 뿐 아니라 각 선택자 인수가 여러 함수를 하나로 조합한다.
boolean 인수뿐만 아니라 enum, int 등 함수 동작을 제어하려는 인수는 바람직하지 않다.

16. 모호한 의도

행을 바꾸지 않고 표현한 수식, 헝가리안 표기법, 매직 넘버 등은 모두 저자의 의도를 흐린다.
의도를 분명이 표현하도록 시간을 투자해야 한다.

17. 잘못 지운 책임

코드는 독자가 자연스럽게 기대할 위치에 배치하여 놀람을 최소화할 수 있게 한다.
코드를 배치할 위치를 결정할 때, 함수 이름을 고려할 수 있다.

18. 부적절한 static 함수

메서드가 사용하는 정보가, 메서드를 소유하는 객체에서 가져오는 정보가 아니라면, 또는 메서드를 재정의할 가능성이 없다면 static 함수를 정의할 수 있다.
이 경우가 아니라면 대부분은 인스턴스 함수로 정의하는 것이 바람직하다.

19. 서술적 변수

프로그램 가독성을 높이려면 계산을 여러 단계로 나누고, 중간 값으로 서술적인 변수 이름을 사용하는 방법을 사용할 수 있다.

20. 이름과 기능이 일치

이름만으로 분명하지 않아 구현을 살피거나 문서를 봐야 한다면, 더 좋은 이름으로 바꾸거나 더 좋은 이름을 붙이기 쉽도록 기능을 정리해야 한다.

21. 알고리즘 이해

괴상한 코드들은 알고리즘에 대한 충분한 이해 없이 코드를 구현한 탓이다.
구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 충분히 이해했는지 확인해야 한다.
알고리즘이 올바르다는 사실을 확인하고 이해하려면, 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하면 된다.

22. 논리적 의존성은 물리적으로 드러내라

의존하는 모듈이 상대 모듈에 대해 뭔가를 가정하면 안된다. 의존하는 모든 정보를 명시적으로 요청하는 것이 좋다.
메서드를 추가함으로써 논리적으로만 의존하던 것을 물리적으로도 의존하도록 보여줄 수 있다.

23. if/else 혹은 switch/case문보다 다형성을 활용

switch를 선택하기 전에 다형성을 먼저 고려한다.
유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다. 따라서 모든 switch문을 의심해야 한다.

24. 표준 표기법을 따르라

업계 표준에 기반한 구현 표준을 따른다. 표준을 설명하는 문서는 코드 자체로 충분해야 한다.
팀이 정한 표준은 팀원 모두가 따라야 한다.

25. 매직 넘버는 명명된 상수로 교체하라

숫자는 명명된 상수 뒤로 숨긴다.
매직 넘버는 숫자만 의미하지 않는다. 의미가 분명하지 않은 모든 토큰이 해당된다.

26. 정확하라

코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.
호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다.
조회 결과가 하나뿐이라고 짐작한다면 하나인지 확실히 확인한다.
통화를 다뤄야 한다면 정수를 사용하고 반올림을 올바로 처리한다.
concurrent 특성으로 인해 동시에 갱신할 가능성이 있다면 적절한 lock 매커니즘을 구현한다.

27. 관례보다 구조를 사용

설계 결정을 강제할 때는 규칙보다 관례, 관례보다 구조가 좋다.
enum 변수가 switch/case문보다 낫지만 추상 메서드가 있는 기초 클래스가 더 좋다.

28. 조건을 캡슐화

boolean은 이해하기 어렵다. 조건의 의도를 분명히 밝히는 함수로 표현하라.

// Good
if (shouldBeDeleted(timer))

// Bad
if (timer.hasExpired() && !timer.isRecurrent())

29. 부정 조건은 피하라

가능하면 긍정조건으로 표현하는 것이 이해하기 쉽다.

30. 함수는 한 가지만 해야 한다.

한 함수 안에 여러 단락을 이어 일련의 작업을 수행하는 것은 한 가지 일만 하는 것이 아니다.
한 가지만 수행하는 더 작은 함수 여럿으로 나눠야 한다.

public void pay() {
  for (Employee e : employees) {
    if (e.isPayday()) {
      Money pay = e.caculatePay();
      e.deliverPay(pay);
    }
  }
}

위 함수는 세 가지 임무를 수행한다. 직원 목록을 루프로 돌고, 직원의 월급일을 확인하고, 월급을 지급한다.
이를 임무별로 3개의 함수로 나누는 것이 좋다.

public void pay() {
  for (Employee e : employees) {
    payIfNeccessary(e)
  }
}

public void payIfNeccessary(Employee e) {
  if (e.isPayday()) {
    calculateAndDeliverPay(e);
  }
}

public void calculateAndDeliverPay(Employee e) {
  Money pay = e.calculatePay();
  e.deliverPay(pay);
}

31. 숨겨진 시간적인 결합

때로는 시간적인 결합이 필요하지만 이를 숨겨서는 안된다.
함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러내야 한다.

public class MoogDiver {
  Gradient gradient;
  List<Spline> splines;
  
  public void dive(String reason) {
    Gradient gradient = saturateGradient();
    List<Spline> splines = reticulateSplines(gradient);
    diveForMoog(splines, reason);
  }
  ...
}

위 코드에서는 일종의 연결 소자를 생성해 시간적인 결합을 노출했다.

32. 일관성 유지

코드 구조를 잡을 때는 이유를 고민한다. 그리고 그 이유를 코드 구조로 명백히 표현한다.
시스템 전반에 걸쳐 구조가 일관성이 있다면 남들도 일관성을 따르고 보존한다.

33. 경계 조건을 캡슐화

경계 조건은 한 곳에서 별도로 처리한다. 그리고 변수로 캡슐화 하는 것이 좋다.

// Bad
if (level + 1 < tags.length)
{
  parts = new Parse(body, tags, level + 1, offset + endTag);
  body = null
}

//Good
int nextLevel = level + 1;
if (nextLevel < tags.length)
{
  parts = new Parse(body, tags, nextLevel, offset + endTag);
  body = null
}

34. 함수는 추상화 수준을 한 단계만 내려가야 한다

함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.
추상화 수준 분리는 리팩토링을 수행하는 가장 중요한 이유 중 하나이면서, 가장 어려운 작업이기도 하다.

35. 설정 정보는 최상위 단계에 둔다

추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안된다.
고차원 함수에서 저차원 함수를 호출할 때 인수로 넘겨야 한다.
저차원 함수에 상수 값을 정의하면 안된다.

36.추이적 탐색을 피하라

한 모듈은 주변 모듈을 모를수록 좋다.
A가 B를 사용하고 B가 C를 사용하더라도 A가 C를 알 필요는 없다.
a.getB().getC()와 같은 형태는 설계와 아키텍처를 바꾸기가 어렵다.
내가 사용하는 모듈이 내게 필요한 모든 서비스를 제공해야 한다.

이름

1. 서술적인 이름

소프트웨어 가독성의 90%는 이름이 결정한다. 따라서 시간을 들여 현명한 이름을 선택하고 유효한 상태로 유지한다.
신중하게 선택한 이름은 독자로 하여금 모듈 내 다른 함수가 하는 일을 짐작할 수 있도록 한다.

2. 적절한 추상화 수준에서 이름을 선택

구현을 드러내는 이름은 피하고, 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택한다.

3. 가능하다면 표준명명법 사용

기존 명명법을 사용하는 이름은 이해하기 더 쉽다. DECORATOR 패턴을 활용한다면, 장식하는 클래스 이름에 decorator라는 단어를 사용해야 한다.
유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

4. 명확한 이름

함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.
이로 인해 이름이 길어지더라도 그 단점을 서술성이 메꿀 수 있다.

5. 긴 범위는 긴 이름을 사용

이름 길이는 범위 길이에 비례해야 한다.

6. 인코딩을 피하라

이름에 유형 정보나 범위 정보를 넣어서는 안된다. 오늘날 개발환경에서는 이름을 조작하지 않고도 모든 정보를 제공하기 때문이다.

7. 이름으로 부수 효과 설명

함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다. 여러 작업을 수행하는 함수에 동사 하나만 사용하면 안된다.

테스트

1. 불충분한 테스트

테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다.

2. 커버리지 도구 사용

커버리지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기 쉬워진다.

3. 사소한 테스트를 건너뛰지 마라

사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.

4. 무시한 테스트는 모호함을 뜻한다

불분명한 요구사항은 테스트를 주석으로 처리하거나 테스트 케이스에 @Ignore를 붙여 표현한다. 그 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지에 달려 있다.

5. 경계 조건을 테스트

경계 조건은 각별이 신경 써서 테스트한다. 알고리즘의 중앙 조건은 올바로 짜 놓고 경계 조건에서 실수하는 경우가 흔하다.

6. 버그 주변은 철저히 테스트

버그는 서로 모이는 경향이 있다. 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 것이 좋다.

7. 실패 패턴을 살펴라

테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 합리적 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.

8. 테스트 커버리지 패턴을 살펴라

통과하는 테스트가 실행하거나 실행하지 않는 코드를 보면 실패하는 테스트 케이스의 실패 원인이 드러난다.

9. 테스트는 빨라야 한다

느린 테스트 케이스는 실행하지 않게 된다. 테스트 케이스가 빨리 돌아가도록 노력해야 한다.

profile
잘 & 열심히 살고싶은 개발자

0개의 댓글