냄새와 휴리스틱

주석

C1: 부적절한 정보

  • 다른 시스템(소스 관리 시스템, 버그 추적 시스템, 이슈 추적 시스템 등)에 저장할 정보를 주석으로 작성하는 것은 부적절
  • 일반적으로 작성자, 최종 수정일 등의 메타 정보만 주석으로 작성

C2: 쓸모 없는 주석

  • 오래된 주석, 엉뚱안 주석, 잘못된 주석은 더 이상 쓸모가 없음
  • 주석은 빨리 낡음
  • 쓸모없는 주석은 빨리 제거함으로써 코드를 그릇된 방향으로 이끄는 것을 방지해야함

C3: 중복된 주석

  • 코드만으로 설명가능한 것을 주석으로 중복설명하는 것을 방지해야함

C4: 성의 없는 주석

  • 주석은 간결하고 명료하게 작성해야함
  • 단어를 신중하게 선택하고 문법, 구두점을 올바로 사용해야함

C5: 주석 처리된 코드

  • 주석으로 처리된 코드는 아주 거슬림
  • 얼마나 오래된 코드인지, 중요한 코드인지 알수 가 없음
  • 아무도 삭제하지도 않음
  • 소스 관리 시스템으로 이전 코드를 추적하고 주석 코드는 존재해서는 안됨

환경

E1: 여러 단계로 빌드해야 한다

  • 빌드는 간단히 한 단계로 끝나야함
  • 스크립트등을 작성해 각 요소를 따로 빌드할 필요가 없어야함
  • 한 명령으로 전체를 체크아웃해서 한 명령으로 빌드 할 수 있어야함

E2: 여러 단계로 테스트해야 한다

  • 모든 단위 테스트는 한 명령으로 돌려야함, 버튼 하나로 모든 테스트가 돌려져야함
  • 모든 테스트를 한 번에 실행하는 능력은 아주 근본적이고 아주 중요함

함수

F1: 너무 많은 인수

  • 인수는 없으면 가장 좋음
  • 넷 이상은 그 가치가 아주 의심스러움

F2: 출력 인수

  • 함수에서 먼가 상태를 변경해야하면 인수가 아닌 함수가 속한 객체의 상태를 변경해야함
  • 인수는 일반적으로 입력으로 간주

F3: 플래그 인수

  • 플래그 인수는 함수가 여러기능을 수행한다는 명백한 증거
  • 혼란을 초래

F4: 죽은 함수

  • 사용하지 않는 함수는 삭제해야함
  • 소스 코드 관리 시스템이 모두 기억하므로 과감히 삭제

일반

G1: 한 소스 파일에 여러 언어를 사용한다

  • 하나의 소스파일에 여러가지의 언어가 사용되면 혼란스럽고 조잡해짐

G2: 당연한 동작을 구현하지 않는다

  • 최소 놀람의 원칙(The principle of Least Surprise)에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야함
  • 이는 코드에 대한 직관을 제공함

G3: 경계를 올바로 처리하지 않는다

  • 부지런함을 대신할 지름길은 없음
  • 모든 경계 조건, 구석진 곳, 기볍, 예외는 우아하고 직관적인 알고리즘을 좌초시킴
  • 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성해야함

G4: 안전 절차 무시

  • 안전 절차를 무시하면 위험함
    • serialVersionUID를 직접 제어할 필요가 있을지도 모르지만 그래도 직접 제어하는것은 위험
  • 컴파일러 경고를 일부 끄면 빌드가 쉬워질지 모르지만 끝없는 디버깅에 시달릴수 있음
  • 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 신용카드가 공짜 돈이라는 생각만큼 위험

G5: 중복

  • 이 책에 나오는 가장 중요한 규칙 중 하나
  • 소프트웨어 설계를 거론하는 저자라면 거의 모두가 이 규칙을 언급
  • 데이티브 토머스와 앤디 헌트는 이를 DIY(Don’t Repeat Yourself) 원칙이라고도 함
  • 켄트백은 익스트림 프로그래밍의 핵심 규칙 중 하나로 선언, Once, and only once라고 명명
  • 론 제프리스는 모든 테스트를 통과한다는 규칙 다음으로 이 규칙을 중요하게 생각
  • 코드 중복을 발견할 때마다 추상화할 기회로 간주하라
    • 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라
    • 중복을 추상화로 정리하면 어휘가 늘어남, 이는 다른 프로그래머들이 사용하기 쉽게 만듬
  • switch/case나 if/else문으로 똑같은 조건을 거듭확인하는 중복은 다형성으로 대체해야함
  • 더더욱 미묘한 유형은 알고리즘이 유사하나 코드가 서로 다른 중복
    • template method pattern 또는 strategy pattern으로 중복을 제거
  • 15년동안 나온 디자인 패턴은 대다수가 중복들 제거하는방법

G6: 추상화 수준이 올바르지 못하다

  • 추상화는 저차원 상세 개념에서 고차원 일반 개념을 분리함
  • 때로는 (고차원 개념을 표현하는)추상화 클래스와 (저차원 개념을 표현하는) 파생 클래스로 추상화 수행
  • 저차원과 고차원을 썩어서는 안됨
public interface Stack {
  Object pop() throws EmptyException;
  void push(Object o) throws FullException;
  double percentFull();
  class EmptyException extends Exception {}
  class FullException extends Exception {}
}
  • 위 코드에서 percentFull함수는 추상화 수준이 올바르지 못함
    • 개념이 확실하지 않음
    • BoundedStack과 같은 파생 인터페이스에 넣어야 마땅함

G7: 기초 클래스가 파생 클래스에 의존한다

  • 기초 클래스가 파생클래스를 사용한다면 문제가 있다는 것
  • 기초 클래스는 파생클래스의 구현을 아예 몰라야 마땅함
  • 이런 경우 독립된 jar로 배포 가능하고 컴포넌트 변경이 필요해도 기초 컴포넌트까지 다시 배포할 필요 없음
  • 즉 변경이 시스템에 미치는 영향이 아주 작아지므로 유지보수하기 용의한 시스템을 구축가능

G8: 과도한 정보

  • 잘 정의된 모듈은 인터페이스가 아주 작음
    • 하지만 작은 인터페이스로 많은 동작이 가능
  • 부실하게 정의된 모듈인 인터페이스가 구질구질함
    • 간단한 동작하나에도 온갖 인터페이스를 필요로함
  • 우수한 개발자는 클래스나 모듈 인터페이스에 노출함 함수를 제한할 줄 알아야함
    • 이는 결합도를 낮춤
  • 클래스가 제공하는 메서드수는 작을수록 좋음
    • 함수가 아는 변수 또한 작을수록 좋음

G9: 죽은 코드

  • 죽은 코드란 실행되지 않는 코드를 의미
  • 시간이 지날수록 악취를 풍김
  • 죽은 코드를 발견하면 적절한 제거가 필요

G10: 수직 분리

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

G11: 일관성 분리

  • 어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현해야함
  • 최소 놀람의 원칙(The Principle of Least Surprise)에 부합
  • 표기법은 신중하게 선택하며, 선택했다면 신중하게 따라야함

G12: 잡동사니

  • 비어있는 생성자등, 아무도 사용하지 않는 변수, 함수 등의 정보는 제거해야함

G13: 인위적 결합

  • 서로 무관함 개념을 인위적으로 결합해서는 안됨
  • 예를들어 일반적인 enum은 클래스에 속할 필요가 없음
  • 함수, 상수, 변수를 선언할 때에는 올바른 위치를 고민해야함

G14: 기능 욕심

  • 마틴파울러가 리펙토링이 필요하다는 코드
  • 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안됨
  • 다른 객체의 참조자나 변경자를 사용한다면 그 객체에 대한 클래스의 범위에 욕심을 내는것

G15: 선택자 인수

  • 함수 끝에 달리는 false인수 만큼 밉살스런 코드가 없음
  • 선택자(selector)인수는 목적을 기억하기 어려울 뿐만 아니라 함수를 여러개로 쪼개지 않기위한 게으름의 소산

G16: 모호한 의도

  • 코드를 짤때는 의도를 최대한 분명히 밝혀야함

G17: 잘못 지운 책임

  • 개발자가 내리는 가장 중요한 결정 중 하나가 코드를 배치하는 위치를 선택하는 것
  • 여기서도 최소 놀람의 원칙(The Principle of Least Surprise)을 적용

G18: 부적절한 static 함수

  • Math.max와 같이 좋은 static 메서드가 존재하는 반면 간혹 static으로 정의하면 안되는 함수를 static으로 정의함
  • 일반적으로 static함수보다 인스턴스 함수가 좋음, 조금이라도 의심스럽다면 인스턴스 함수로 정의
  • 반드시 static함수로 정의하게싸면 재정의할 가능성에 대한 판단필요

G19: 서술적 변수

  • 프로그램 가독성을 높이는 가장 효과적인 방법중 하나가 계산을 여러단계로 나누고 중간값으로 서술적인 이름의 변수를 사용하는것
  • 서술적인 변수 이름은 많이 써도 괜찮음, 일반적으로 많을수록 좋음
    • 이는 읽기쉬운 코드로 만듬

G20: 이름과 기능이 일치하는 함수

  • 다음의 코드를 살펴보자
Date newDate = date.add(5);
  • 5일을 더하는 함수인가? 5주? 5시간?
  • 이름만으로 분명하지 않기에 구현을 살피거나 문서를 뒤적여한다면 더 좋은 이름으로 바꾸거나 더 좋은 이름을 붙이기 쉽도록 기능을 정리해야함

G21: 알고리즘을 이용해라

  • 대다수 괴상한 코드는 알고리즘을 이해하지 못하고 구현해서 발생
    • 알고리즘을 고민하는 대신 여기저기 if/else를 붙히며 코드를 돌리는 탓
  • 구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해해야함

G22: 논리적 의존성은 물리적으로 드러내라

  • 한 모듈이 다른 모듈에 의존한다면 물리적 의존성도 있어야함

G23: If/Else 혹은 Switch/Case 문보다 다형성을 사용하라

  • 새 유형을 추가할 확률이 새 함수를 추가할 확률보다 높은 경우 switch문은 사용하는 것을 권장
  • 대다수의 switch문이 적용된 상황은 올바른 선택이기 보다 손쉬운 선택임
  • 또한 유형보다 함수가 더 쉽게 변하는 경우는 극히 드뭄
  • 그러므로 모든 switch문은 의심해야함

G24: 표준 표기법을 따르라

  • 팀은 업계 표준에 기반한 구현을 따라야함
  • 구현 표준은 인스턴스 변수 이름을 선언하는 위치, 클래스/메서드/변수 이름 지정법, 괄호 넣는 위치등을 명시해야함
  • 표준을 설명하는 문서는 코드 자체로 충분히 설명가능해야하며 별도의 문서를 만들 필요는 없어야함

G25: 매직 숫자는 명명된 상수로 교체하라

  • 의미 있는 숫자는 명명된 상수뒤로 숨겨 사용해야함
  • 소프트웨어 개발에서 가장 오래된 규칙 중 하나

G26: 정확하라

  • 부정확하지 상황
    • 검사 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진한 행동
    • 부동소수점으로 통화를 표현하는 행동은 거의 범죄에 가까움
    • 변경가능성이 희박하다고 잠금이나 트랙잭션 관리를 건너뛰는 행동은 게으른것
  • 코드에서 뭔가 결정할 때에는 정확히 결정해야함
  • 결정을 내리는 이유와 예외를 처리할 방법이 분명해야함
  • 코드에서 모호성과 부정확은 의견차나 게으름의 결과, 어느쪽이든 제거해야함

G27: 관례보다 구조를 사용하라

  • 설계 결정을 강제할 때는 규칙보다 관례를 사용
  • 명명 관례도 좋지만 구조 자체로 강제하면 더 좋음

G28: 조건을 캡슐화하라

  • 부울 논리는 이해하기 어려움
  • 조건의 의도를 분명히 밝히는 함수로 표현하는 것이 좋음

G29: 부정 조건은 피하라

  • 부정 조건은 긍정 조건 보다 이해하기 어려움

G30: 함수는 한 가지만 해야 한다

  • 함수를 짜다보면 재사용과 별개로 한 함수에 여러 단락을 이어 작성하고 싶은 유혹에 빠짐
  • 이런 함수는 한 가지만 수행하는 함수로 분리해야함

G31: 숨겨진 시간적인 결합

  • 때로는 시간적 결합이 필요함
  • 하지만 시간적 결합을 숨겨서는 안됨
// 시간적 결합이 숨겨지는 코드
public void dive(String reason) {
  saturateGradient();
  reticulateSplines();
  diveForMoog(reason);
}

// 시간적 결합이 숨겨지지 않는 코드, 순서를 바꾸면안됨
public void dive(String reason) {
  Gradient gradient = saturateGradient();
  List<Spline> splines = reticulateSplines(gradient);
  diveForMoog(splines, reason);
}

G32: 일관성을 유지하라

  • 코드 구조를 잡을 때는 이유를 고민하고 그 이유를 코드 구조로 명백히 표현해야함
  • 코드 구조에 일관성이 보이면 남들도 일관성을 따르고 보존함

G33: 경계 조건을 캡슐화하라

  • 경계 조건은 빼먹거나 놓치기 쉬움

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

  • 함수 내 모든 문장은 추상화 수준이 동일해야함
  • 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야함
  • 이장의 휴리스틱 중 가장 이해하기 어렵고 따르기 어려운 항목

G35: 설정 정보는 최상위 단계에 둬라

  • 추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안됨
  • 설정 관련 상수는 최상위 단계에 줘야함, 그래야 변경하기 쉬워짐

G36: 추이적 탐색을 피하라

  • 일반적으로 한 모듈은 주변 모듈을 모를수록 좋음
  • A가B를 사용하고 B가 C를 사용하더라도 A가 C를 알필요는 없음
  • 이를 디미터의 법칙(Law of Demeter)이라고 함
  • 내가 아는 모듈이 연이어 자신이 아는 모듈을 따라가며 시스템 전체를 휘저을 필요는 없다는 의미

자바

J1: 긴 import 목록을 피하고 와일드카드를 사용하라

  • 패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져와야함
  • 긴 import 목록은 읽기 부담스러움
  • 명시적인 import문은 강한 의존성을 생성
  • 명시적인 import문이 사용되어야할 경우도 존재함
    • 레거시 코드 사용하면서 테스트용 모듈이나 스텁을 빌드할 클래스를 찾을 경우
  • 와일드카드 import문은 때로 이름 충돌이나 모호성을 초래함

J2: 상수는 상속하지 않는다

  • 인터페이스에 상수를 선언하고 해당 인터페이스를 구현해서 상수를 사용하는 경우가 존재
  • 상수를 계층 최 상위에 선언한것! 끔직한 관행

J3: 상수 대 Enum

  • public static final int와 같은 옛날 상수 정의 방법을 사용할 필요는 없음
  • enum 문법자체를 자세히 살피고 사용해야함

이름

N1: 서술적인 이름을 사용하라

  • 이름은 성급하게 결정하지 말고 서술적인 이름을 신중히 골라야함
  • 소프트웨어의 가독성 90%이 결정

N2: 적절한 추상화 수준에서 이름을 선택하라

  • 구현을 드러내는 이름은 피하라
  • 추상화 수준을 반영하는 이름을 선택하라

N3: 가능하다면 표준 명명법을 사용하라

  • 기존 명명법을 사용하는 이름은 이애하기 쉬움
  • 패턴은 한 가지 표준에 불과

N4: 명확한 이름

  • 함수나 변수의 목적을 명확히 밝히는 이름을 선택

N5: 긴 범위는 긴 이름을 사용하라

  • 이름 길이는 범위에 비례해야함
  • 범위가 짧다면 짧은 이름을 사용해도 괜찮음
  • 범위가 길다면 긴 이름을 사용

N6: 인코딩을 피하라

  • 이름에 유형 정보나 범위 정보를 넣어서는 안됨
  • 특정 접두어 사용도 불필요함

N7: 이름으로 부수 효과를 설명하라

  • 함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용해야함
  • 실제로 여러작업을 수행하는데 동사 하나만 사용하면 곤란하

테스트

T1: 불충분한 테스트

  • 잠재적으로 깨질 만한 부분은 모두 테스트해야함
  • 테스트 케이스가 확인하지 않는 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전

T2: 커버리지 도구를 사용하라

  • 커버리지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기가 쉬워짐
  • 대다수의 IDE는 테스트 커버리지를 시각적으로 표현

T3: 사소한 테스트를 건너뛰지 마라

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

T4: 무시한 테스트는 모호함을 뜻한다

  • 때로는 요구사항이 불분명해서 프로그램 동작방식을 확인하기 어렵다
  • 불분명한 요구사항의 테스트는 주석처리되거나 @Ignore이 붙는다
  • 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일 가능한지 불가능한지에 달려있다

T5: 경계 조건을 테스트하라

  • 경계조건은 각별히 신경써서 테스트한다

T6: 버그 주변은 철저히 테스트하라

  • 버그는 서로 모이는 경항이 존재
  • 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트해야함

T7: 실패 패턴을 살펴라

  • 실패하는 패턴으로 문제를 진단할 수 있음
  • 때문에 테스트케이스는 꼼꼼히 짜야함

T8: 테스트 커버리지 패턴을 살펴라

  • 통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 원인을 찾을 수 있음

T9: 테스트는 빨라야 한다

  • 일정등의 이유로 느린 케이스는 실행하지 않게 됨