Clean Code 17. Code smell & heuristic


주석

C1: 부적절한 정보

다른 시스템(소스코드 관리, 이슈트래커, 버그 추적 시스템 등)에 저장할 정보, 변경 이력 등 코드와 상관없는 정보

C2: 쓸모 없는 주석

오래된 주석, 엉뚱한 주석, 잘못된 주석

C3: 중복된 주석

코드를 설명하는 주석

C4: 성의 없는 주석

주석을 공들여 달것이 아니면 달지마라.

C5: 주석 처리된 코드

지워라. 소스 코드 관리 시스템을 믿어라.

환경

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

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

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

모든 단위 테스트는 한 명령으로 돌려야 한다. IDE에서 버튼 하나로 돌린다면 가장 이상적이다.

함수

F1: 너무 많은 인수

인수는 작을수록 좋다. 아에 없으면 더 좋다. 넷 이상은 그 함수 자체를 잘못 만들었을 수 있다.

F2: 출력 인수

일반적으로 독자는 인수를 입력으로 간주한다. 함수가 뭔가의 상태를 변경해야 한다면 함수가 속한 객체의 사애를 변경한다.

F3: 플래그 인수

대놓고 함수가 여러 기능을 수행한다는 증거다.

F4: 죽은 함수

아무도 안쓰는 함수는 삭제한다.

일반

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

JSP 파일은 HTML, Java, JavaScript등 여러 언어가 가능하다. 각별히 노력하야 소스 파일에 언어 수와 범위를 최대한 줄여라.

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

최소 놀람 원칙(The Principle of Least Surprises)에 의거해 함수나 클래스는 당연하게 여길 만한 동작과 기능을 제공해야 한다. 그 기능이 없으면 저자를 신뢰하지 못하므로 코드를 일일이 살펴야 한다.

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

모든 경계 조건, 모든 구석진 곳, 모든 예외는 우아하고 직관적인 알고리즘을 좌초시킬 암초이다. 스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아내고 테스트해라.

G4: 안전 절차 무시

컴파일러의 경고를 무시. 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 행동은 신용카드가 공짜라는 생각과 다를바 없다.

G5: 중복

DRY(Don’t Repeat Yourself) - 데이비드 토머스, 앤디 헌트

한 번, 단 한 번 (Once and only once) - 켄트 벡

중복 된 코드를 발견할 때마다 추상화의 기회로 간주하라. 하위 루틴이나 다른 클래스로 분리하라.
여러 모듈에서 일련의 switch/case나 if/else가 나오면 다형성(polymophism)으로 댗[해야 한다.
알고리즘이 유사하나 코드가 서로 다른 것도 중복이다. Template Method Pattern이나 Strategy Pattern을 사용해라.

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

추상화로 개념을 분리할 때는 철저해야 한다. 모든 저차원 개념음 파생 클래스로, 고차원 개념은 기초 클래스에 넣는다.

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

기초 클래스는 파생 클래스의 존재 자체를 몰라야 한다.

G8: 과도한 정보

잘 정의된 interface는 많은 함수를 제공하지 않는다. 그래서 결합도(coupling)가 낮다. 부실하게 정의된 interface는 구질구질하다. 반드시 호출해야 하는 온갖 함수를제공한다. 그래서 결합도가 높다.

자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라. 메서드나 인스턴스 변수가 넘쳐나는 클래스를 피하라. protected 변수나 함수를 마구 생성하지 마라. 정보를 제한해 결합도를 낮춰라.

G9: 죽은 코드

실행되지 않는 코드는 제거하라. 불가능한 조건의 if/switch문, throw없는 try문에서의 catch절.

G10: 수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다. 비공개 함수는 처음 호출되는 위치를 찾은 후 조금만 아래로 내려가면 쉽게 눈에 띄어야 한다.

G11: 일관성 부족

최소 놀람 원칙(The Principle of Least Surprises)을 기억해라. 표기법은 신중하게 선택하고 따른다. 변수이름, 구현 방법에 일관성을 유지해라.

G12: 잡동사니

Empty Constructor는 지워라. 사용하지 않는 변수, 함수. 쓸데없는 주석.

G13: 인위적 결합

enum은 클래스에 속할 이유가 없다. public static 함수도 특정 클래스에 속할 이유가 없다. 뚜렷한 목적없이 당장 편한 위치에 (당연히 잘못된 위치애) 넣어버린 결과다. 시간을 들여서라도 올바른 위치를 고민하라.

G14: 기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스에 관심을 가져서는 안된다. 어쩔수 없는 경우에는 그 범위를 최소화 해라.

G15: 선택자 인수

큰 함수를 작은 함수 여럿으로 쪼개지 않으려는 게으름의 소산이다. bool뿐만 아니라 enum, int 등으로 함수 동작을 제어하려하는 것은 바람직하지 않다.

G16: 모호한 의도

짧고 빽빽한 코드는 의도를 파악하기 힘들다. 코드를 짤 때는 의도를 최대한 분명히 밝힌다.

G17: 잘못 지운 책임

코드 배치 위치를 결정하는 것은 굉장히 중요하다. 때로는 개발자가 독자에게 직관적인 위치가 아닌 개발자에게 편한 위치에 배치한다. get/set 중 어디에 계산하는 로직을 넣어야 할까 ? 어디에 넣을지 해깔리면 함수 이름에 계산한다는 사실을 표현해라.

G18: 부적절한 static 함수

모든 정보를 인수에서 가져온다면 static으로 선언한다. 하지만 함수를 재정의할 가능성이 존재한다면 인스턴스 메서드로 선언해라. 조금 의심스럽다면 인스턴스 함수로 정의해라. 그래도 static으로 하고 싶으면 제정의할 가능성이 없는지 꼼곰히 따져봐라.

G19: 서술적 변수

서술적인 변수 이름은 많이 써도 괜찮다. 많은수록 좋다. 계산 중간값에 좋은 이름만 붙여도 해석이 쉬워진다.

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

Date newData = date.add(5)

5일을 더하나 ? 5주 ? 5시간인가 ?
date 인스턴스를 변경하나 ? 아님 그대로 두고 새로운 Date를 반환하나 ?
코드만으로는 알 수가 없다.

더 좋은 이름으로 바꾸던가, 더 좋은 이름을 붙이기 쉽게 기능을 정리해라.

G21: 알고리즘을 이해하라

실제 알고리즘을 고민하지 않으면 여기저기 if문과 flag를 넣게된다. 돌아가는 것, 테스트 케이스를 통과했다는 사실만으로는 부족하다. 알고리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.

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

논리적인 의존성만으로는 부족하다. 물리적으로 의존해라. HolydayReporter안에 PAGE_SIZE라는 const 변수가 있는것은 논리적 의존이다. 다른 클래스에 해당 변수를 넣어두고 getMaxPageSize() 함수로 호출하는 것이 더 적절하다.

G23: if/else, switch/case 보다는 다형성(polymophism)을 사용하라

switch문 하나 규칙을 지켜라. 선택 유형 하나에는 switch문을 한번만 사용한다. 같은 선택을 수행하는 다른 코드가 필요하면 다형성 객체를 생성해라.

  1. switch문 사용은 올바른 결정이 아니라 가장 쉬운 결정이다.
  2. 하지만, 다형성은 코드 변경이 쉬운데, switch문은 더 복잡하다.

G24: 표준 표기법을 따르라

팀은 업계 표준을 따라야 한다. 그러면 표준을 설명하는 문서를 만들 필요가 없다. 팀이 정한 표준은 팀원 모두가 따라야 한다.

G35: Magic Number는 const로 교체하라

G26: 정확하라

부동소수점으로 통화를 표현하는 행동은 거의 범죄에 가깝다. 갱신한 가능성이 희박하다고 lock/transaction 관리를 건너뛰는건 게으름이다. List로 선언할 변수를 Array로 선언하는 행동은 지나친 제약이다.
코드에서 뭔가를 결정할 때는 정확히 결정한다. 대충 결정해서는 안된다. null을 봔한할지 모르겠다며 체크하고, 결과가 1개라 짐작된다면 확실히 확인하라.

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

명명관례보다는 구조적인 강제가 더 좋다. enum + switch/case 문보다는 추상 메서드가 있는 기초 클래스가 더 좋다.

G28: 조건을 캡슐화하라

논리식(if/while의 괄호절 안의 내용)은 이해하기 어렵다. 조건의 의도를 분명히 밝히는 함수로 표현하라.

G29: 부정 조건은 피하라

부정은 긍정보다 이해하기 어렵다.

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

함수를 짜다보면 일련의 작업을 수행하고픈 유혹에 빠진다. 한 가지만 수행하는 좀 더 작은 함수 여럿으로 나눠야 마땅하다.

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

위 함수는 3가지 일을 한다.

  1. 직원 목록을 루프로 돌려,
  2. 각 직원의 월급일을 확인하고,
  3. 해당 직원에게 월급을 지급한다.
public void pay() {
    for (Employee e: employees) {
        payIfNecessary(e)
    }
}

private void payIfNecessary(Employee e) {
    if (e.isPayDay()) {
        calculateAndDeliverPay(e);
    }
}

private void calculateAndDeliverPay(Employee e) {
    Money pay = e.calcuatePay();
    e.deliverPay(pay);
}

G31: 숨겨진 시간적인 결합

시간적인 결합을 숨겨서는 안 된다. 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다. 선행되어야 하는 함수에서 뒤에 실행할 함수에 필요한 인수를 반환하도록 코드를 짜라.

G32: 일관성을 유지하라

코드 구조를 잡을 때는 이유를 고민하고, 그 이유를 코드 구조로 명백히 표현하라. 구조에 일관성이 없어 보이면 남들이 마음대로 바꿔도 괜찮다고 생각한다. 일관성이 보인다면 남들도 그걸 따르고 보존한다.

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

경계 조건은 빼먹거나 놓치기 쉽다. 한 곳에서 별도로 처리해서 코드 여기저기서 처리하지 않아도 되게하라.

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

함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.

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

추상화 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안 된다. 최상위 단계에 뒤야 변경하기도 쉽다.

G36: 추이적 탐색을 피하라

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다. A가 B를 사용하고 B가 C를 사용할 경우 A는 C를 알 필요가 없다. 이를 디미터의 법칙(Law of Demeter) 또는 부끄럼 타는 코드(Writing Shy Code)라고도 한다. a.getB().getC().getValue() 같은 코드가 있다면 C에 Q를 끼워넣을 때 해당 코드들을 모두 찾아서 고쳐야 한다.

Java

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

패키지에서 클래스 둘 이상을 사용한다면 와일드카드(*)를 사용하라.

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

상수가 어디서 왔는지 찾는다고 interface나 부모 클래스를 찾아보는건 끔찍하다. 대신 static import를 사용하라.

J3: 상수 vs Enum

enum은 이름이 부여된 열거체라서 훨씬 더 유연하고 강력하다.

이름

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

소프트웨어 가독성의 90%는 이름이 결정한다.

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

구현을 드러내는 이름을 피하라. 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택해라. 인간은 추상화 수준을 뒤섞는 능력이 너무나도 뛰어나다.

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

패턴이름, 각 언어의 toString, 프로텍트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

N4: 명확한 이름

목적을 명확히 밝히는 이름을 선택한다.

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

범위가 작으면 아주 짧은 이름을 사용해도 괜찮다. 범위가 5줄 안팎이라면 i, j와 같은 이름도 괜찮다.

N6: 인코딩을 피하라

m_, f와 같은 접두어는 필요없다. 헝가리언 표기법의 오염에서 이름을 보호하라.

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

getOos()에서는 ObjectOutputStream을 가져오기만 해야할뿐 새로 생성해서는 안된다. 그럴려면 createOrReturnOos()라는 이름이 더 좋다.

테스트

T1: 불충분한 테스트

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

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

테스트가 불충분한 곳을 찾기 쉬워진다. 전혀 실행되지 않는 if 혹은 case가 금방 드러난다.

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

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

불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 @Ignore를 붙여 표현한다.

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

여기에서 실수하는 경우가 흔하다.

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

버그는 서로 모이는 경향이 있다. 버그를 발견했다면 해당 함수를 철저히 테스트해라. 다른 버그도 발견하리라.

T7: 실패 패턴을 살펴라

합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다. 간단한 예로 5자를 넘기는 모든 테스트가 실패한다면 ? 인수로 음수가 들어온 경우 테스트 케이스가 실패한다면 ?

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

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

T9: 테스트는 빨라야 한다

일정이 촉박할 경우 테스트가 느리면 건너뛰기 쉽다.

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)