Clean Code 17장 - 냄새와 휴리스틱

boring-km·2023년 4월 15일
0

Clean Code

목록 보기
17/17
post-thumbnail

2022년 3월 17일 Commit

  • 주석
  • 환경
  • 함수
  • 일반
  • 자바
  • 이름
  • 테스트
  • 결론

  • 코드를 읽으면서 기록한 나쁜 냄새들의 목록

주석

C1: 부적절한 정보

  • 다른 시스템에 (소스 코드 관리 시스템, 버그 추적 시스템, 이슈 추적 시스템, 기타 기록 관리 시스템) 저장할 정보는 주석으로 적절하지 못하다.
  • 변경 이력은 장황한 날짜와 따분한 내용으로 소스 코드만 번잡하게 만든다.
  • 주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다.

C2: 쓸모 없는 주석

  • 오래된 주석, 엉뚱한 주석, 잘못된 주석은 더 이상 쓸모가 없다.
  • 코드와 무관하게 혼자서 따로 놀며 코드를 그릇된 방향으로 이끈다.

C3: 중복된 주석

  • 코드만으로 충분한데 구구절절 설명하는 주석이 중복된 주석이다.
  • 함수 서명(signature)만 달랑 기술하는 javadoc 예시를 보자.
/**
 * @param sellRequest
 * @return
 * @throws ManagedComponentException
 */
public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException

C4: 성의 없는 주석

  • 작성할 가치가 있는 주석은 잘 작성할 가치도 있다.
  • 단어를 신중하게 선택하고 주절대지 않으며 당연한 소리를 반복하지 않고 간결/명료하게 작성하자

C5: 주석 처리된 코드

  • 얼마나 오래된 코드인지, 중요한 코드인지 아닌지, 알 길이 없다.
  • 누군가에게 필요한 코드라 생각하기에 아무도 삭제하지 않는다.
  • 주석으로 처리된 코드를 발견하면 즉각 지워버리자!
  • 소스 코드 관리 시스템이 기억할 것이다.

환경

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

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

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

  • 모든 단위 테스트는 한 명령으로 돌려야 한다.
  • 모든 테스트를 한 번에 실행하는 능력은 아주 근본적이고 아주 중요하다.

함수

F1: 너무 많은 인수

  • 함수에서 인수 갯수는 작을수록 좋다.
  • 아예 없으면 가장 좋다.

F2: 출력 인수

  • 출력 인수는 직관성을 정면으로 위배한다.
  • 독자는 인수를 입력으로 간주하기 때문에 함수에서 뭔가의 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경하라

F3: 플래그 인수

  • boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다. 플래그 인수는 혼란을 초래하므로 피해야 마땅하다.

F4: 죽은 함수

  • 아무도 호출하지 않는 함수는 삭제한다.
  • 죽은 코드는 낭비다.

일반

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

  • 오늘날 프로그래밍 환경은 한 소스 파일 내에서 다양한 언어를 지원한다.
  • 이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.

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

  • 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.
  • 당연한 동작을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 어렵다.
  • 저자를 신뢰하지 못하므로 코드를 일일이 살피게 된다.

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

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

G4: 안전 절차 무시

  • 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 신용카드가 공짜 돈이라는 생각만큼 위험하다.

G5: 중복

  • 코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라.
  • 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라.
  • switch/case 문이나, if/else 문으로 똑같은 조건을 거듭 확인하는 중복에 대해서는 다형성으로 대체하라
  • 알고리즘이 유사하나 코드가 서로 다른 중복에 대해서는 템플릿 메서드 패턴이나 전략 패턴으로 대체하라

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

  • 추상화는 소프트웨어 개발자에게 가장 어려운 작업 중 하나다.
  • 추상화로 개념을 분리할 때는 철저해야 한다.
  • 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.
  • 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안 된다. 소스 파일, 컴포넌트, 모듈도 마찬가지다.
  • 개념을 다양한 차원으로 분리해 다른 컨테이너에 넣자.
  • 고차원 개념과 저차원 개념을 섞어서는 안 된다.

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

  • 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.
  • 파생 클래스 개념으로부터 분리해 독립성을 보장해야 한다.

G8: 과도한 정보

  • 잘 정의된 모듈은 인터페이스가 아주 작다.
  • 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다.
  • 클래스가 제공하는 메서드 수는 작을수록 좋다.
  • 함수가 아는 변수 수도 작을수록 좋다.
  • 클래스에 들어있던 인스턴스 변수 수도 작을수록 좋다.
  • 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라.
  • 정보를 제한해 결합도를 낮춰라

G9: 죽은 코드

  • 죽은 코드란 실행 되지 않는 코드를 가리킨다.
  • 옛날옛적 시스템의 모양새가 다른 시절에 짜놓은 코드다.
  • 시스템에서 제거하라.

G10: 수직 분리

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

G11: 일관성 부족

  • 최소 놀람의 원칙(The Principle of Least Surprise)
  • 어떤 개념을 특정 방식으로 구현했다면, 유사한 개념도 같은 방식으로 구현한다.
  • 명명법을 통일한다던가...

G12: 잡동사니

  • 아무도 사용하지 않는 변수, 아무도 호출하지 않는 함수, 정보를 제공하지 못하는 주석 등이 좋은 예다.
  • 모두가 코드만 복잡하게 만들 뿐이니 제거해야 마땅하다.

G13: 인위적 결합

  • 서로 무관한 개념을 인위적으로 결합하지 안흔다.
  • 함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치를 고민한다.
  • 그저 당장 편한 곳에 선언하고 내버려두면 안 된다.

G14: 기능 욕심

  • 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안 된다.
  • 다른 객체의 참조자와 변경자를 이용해 객체 내용을 조작한다면 욕심이다.

G15: 선택자 인수

  • 선택자 인수는 큰 함수를 작은 함수 여럿으로 쪼개지 않으려는 게으름의 소산이다.
class TestClass {
    public int calculateWeeklyPay(boolean overtime) {
        int tenthRate = getTenthRate();
        int tenthWorked = getTenthsWorked();
        int straightTime = Math.min(400, tenthWorked);
        int overTime = Math.max(0, tenthWorked - straightTime);
        int straightPay = straightTime * tenthRate;
        double overtimeRate = overTime ? 1.5 : 1.0 * tenthRate;
        int overtimePay = (int)Math.round(overTime * overtimeRate);
        return straightPay + overtimePay;
    }
}
  • 초과근무 수당을 1.5배로 지급하면 true고 아니면 false다.
  • 독자는 calculateWeeklyPay(false)라는 코드를 발견할 때마다 의미를 떠올려야 한다.
  • 이렇게 구현했다면?
class TestClass {
    public int straightPay() {
        return getTenthsWorked() * getTenthRate();
    }
    
    public int overTimePay() {
        int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
        int overTimePay = overTimeBonus(overTimeTenths);
        return straightPay() + overTimePay;
    }
    
    private int overTimeBonus(int overTimeTenths) {
        double bonus = 0.5 * getTenthRate() * overTimeTenths;
        return (int) Math.round(bonus);
    }
}
  • 일반적으로, 인수를 넘겨 동작을 선택하려는 대신 새로운 함수를 만드는 것이 좋다. (나도 너무 공감하고, 어서 회사 코드를 고쳐봐야겠다.)

G16: 모호한 의도

  • 코드를 짤 때는 의도를 최대한 분명히 밝힌다.
  • 행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 번호 등은 모두 저자의 의도를 흐린다.

G17: 잘못 배치한 책임

  • 코드를 배치하는 위치를 결정하는 것은 중요하다.
  • 영리하게 독자에게 직관적인 위치가 아닌 개발자에게 편한 함수에 배치한다면, 그 함수의 이름을 정확히 짓자.

G18: 부적절한 static 함수

  • 간혹 우리는 static으로 정의하면 안 되는 함수를 static으로 정의하는 실수를 범할 수 있다.
  • 일반적으로 static 함수보다 인스턴스 함수가 더 좋다.
  • 반드시 static으로 정의해야겠다면, 재정의할 가능성이 없는지 꼼꼼히 따져본다.

G19: 서술적 변수

  • 서술적인 변수 이름은 많이 써도 괜찮다. 일반적으로 많을수록 더 좋다.
  • 계산을 몇 단계로 나누고 중간값에 좋은 변수 이름만 붙여도 해독하기 어렵던 모듈이 순식간에 읽기 쉬운 모듈로 탈바꿈한다.

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

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

G21: 알고리즘을 이해하라

  • 구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라.
  • 테스트 케이스를 모두 통과한다는 사실만으로 부족하다.
  • 작성자가 알고리즘이 올바르다는 사실을 알아야 한다.

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

  • 한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다.
  • 논리적인 의존성만으로는 부족하다.
  • 의존하는 모듈이 상대 모듈에 대해 뭔가를 가정하면 안 된다. (논리적으로 의존하면 안 된다.)
  • 의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.
import java.util.List;
import java.util.ArrayList;

public class HourlyReporter {
    private HourlyReporterFormatter formatter;
    private List<LineItem> page;
    private final int PAGE_SIZE = 55;

    public HourlyReporter(HourlyReporterFormatter formatter) {
        this.formatter = formatter;
        page = new ArrayList<LineItem>();
    }
    
    public void generateReport(List<HourlyEmployee> employees) {
        for (HourlyEmployee e: employees) {
            addLineItemToPage(e);
            if (page.size() == PAGE_SIZE)
                printAndClearItemList();
        }
        if (page.size() > 0)
            printAndClearItemList();
    }
    
    private void printAndClearItemList() {
        formatter.format(page);
        page.clear();
    }
    
    private void addLineItemToPage(HourlyEmployee e) {
        LineItem item = new LineItem();
        item.name = e.getName();
        item.hours = e.getTenthsWorked() / 10;
        item.tenths = e.getTenthWorked() % 10;
        page.add(item);
    }
    
    public class LineItem {
        public String name;
        public int hours;
        public int tenths;
    }
}
  • PAGE_SIZE 상수에 대한 논리적인 의존성이 존재한다.
  • HourlyReporter 클래스는 HourlyReportFormatter 클래스가 페이지 크기를 알 거라고 가정하고 있다.
  • 페이지 크기 55를 처리할 줄 안다는 사실에 의존한다.
  • 만약 HourlyReportFormatter 구현 중 하나가 페이지 크기 55를 제대로 처리하지 못한다면 오류가 생긴다.
  • HourlyReportFormatter 클레스에 getMaxPageSize()라는 메서드를 추가해 논리적인 의존성이 물리적인 의존성으로 변하도록 사용하면 된다.

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

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

G24: 표준 표기법을 따르라

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

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

  • 의미가 분명하지 않은 토큰 값에 대해 상수로 선언하라

G26: 정확하라

  • 검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다.
  • 코드에서 뭔가를 결정할 때는 정확히 결정한다.
  • 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.

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

  • 설계 결정을 강요할 때는 규칙보다 관례를 사용한다.
  • 명명 관례도 좋지만 구조 자체로 강제하면 더 좋다.
  • 예를 들어, enum 변수가 멋진 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다.
    • 파생 클래스는 추상 메서드를 모두 구현해야 하는 강제성이 생기기 때문이다.

G28: 조건을 캡슐화하라

  • 부울 논리는 조건의 의도를 분명히 밝히는 함수로 표현하라.

G29: 부정 조건은 피하라

  • 부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건으로 표현하라.

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

  • 한 가지만 수행하는 좀 더 작은 함수 여럿으로 나눠야 마땅하다.

G31: 숨겨진 시간적인 결합

  • 순서가 정해진 코드에서 시간 결합이 감춰져 있으면 실행 순서가 드러나지 않아, 잘못된 순서로 호출하게 되면 오류를 유발한다.
  • 시간 순서로 흘러야 하는 코드에선 여러 함수라면, 함수의 결과 값이 그 다음 함수의 인수로 들어가는 방식으로 결합을 드러내보자.

G32: 일관성을 유지하라

  • 코드 구조를 잡을 때는 이유를 고민하라.
  • 구조에 일관성이 없어 보이면 남들이 마음대로 바꿔도 괜찮다고 생각하게 된다.

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

  • 경계 조건은 if문에서만 사용하지 말고 변수로 캡슐화해라

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

  • 함수 내 모든 문장은 추상화 수준이 동일해야 한다.
  • 인간은 추상화 수준을 뒤섞는 능력이 너무나도 뛰어나다. (ㅋㅋㅋㅋ)
  • 어렵겠지만, 결국 핵심은 SRP를 지켜내면서 함수 안에 담긴 추상화의 내용이 하나이면 해결될 것 같다.

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

  • 그래야 나중에 변경하기도 쉽다.
  • 저차원 함수에 상수 값을 정의하지 말자.

G36: 추이적 탐색을 피하라

  • 디미터 법칙을 지키자
  • 자신이 직접 사용할 모듈만 알도록 구현하자

myCollaborator.doSomething();

자바

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

  • 패키지에서 클래스를 둘 이상 사용한다면 와일드카드 사용하라

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

  • 언어의 범위 규칙을 속이는 행위다.
  • 대신 static import를 사용해라

J3: 상수 vs Enum

  • static final로 상수를 사용하기 보다 훨씬 더 유연하고 서술적인 enum을 사용하자
  • 메서드와 필드도 사용할 수 있다.

이름

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

  • 이름은 성급하게 정하지 않는다.
  • 소프트웨어가 진화하면 의미도 변하므로 선택한 이름이 적합한지 자주 되돌아본다.
  • 소프트웨어 가독성의 90%는 이름이 결정한다.
  • 시간을 들여 현명한 이름을 선택하고 유효한 상태로 유지한다.
  • 대충 정하기에 이름은 너무나도 중요하다.

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

  • 구현을 드러내는 이름은 피하라
  • 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라
  • 발견할 때마다 기회를 잡아 바꿔놓으며 안정적인 코드를 만들기 위한 지속적인 개선과 노력이 필요하다.

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

  • 데코레이터 패턴을 사용한다면, 클래스 이름에 Decorator 라는 단어를 사용해야 한다.
  • 자바에서 객체를 문자열로 변환하는 toString()은 관례이기 때문에 따르는 것이 좋다.
  • 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

N4: 명확한 이름

  • 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.
  • 아주 긴 이름이어도 모듈에서 한 번만 호출되고, 그 이름이 확실하다면 사용하자

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

  • 이름 길이는 범위 길이에 비례해야 한다.
  • 범위가 작으면 아주 짧은 이름을 사용해도 괜찮다.
  • 하지만 범위가 길어지면 긴 이름을 사용한다.
  • 이름 범위가 길수록 이름을 정확하고 길게 짓자

N6: 인코딩을 피하라

  • 이름에 유형 정보나 범위 정보를 넣어서는 안 된다.
  • 접두어도 필요없다.

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

  • 함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다.
import java.io.ObjectOutputStream;

public class Test {
  public ObjectOutputStream getOos() throws IOException {
    if (m_oos == null) {
      m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
  }
}
  • 위의 코드에서 메서드 명이 단순이 oos를 가져오는 것이 아니라 oos가 없으면 생성한다.
  • createOrReturnOos라는 이름이 더 좋다.

테스트

T1: 불충분한 테스트

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

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

  • 커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다.
  • 커버리지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기 쉬워진다.

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

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

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

  • 때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확신하기 어렵다.
  • 불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 @Ignore 붙이기

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

  • 알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.

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

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

T7: 실패 패턴을 살펴라

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

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

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

T9: 테스트는 빨라야 한다

  • 느린 테스트 케이스는 실행하지 않게 된다.
  • 일정이 촉박하면 건너뛰게 되고, 그러지 않기 위해 테스트 케이스가 빨리 돌아가도록 최대한 노력하자

결론

  • 일군의 규칙만 따른다고 깨끗한 코드가 얻어지지 않는다.
  • 전문가 정신과 장인 정신은 가치에서 나온다.
  • 그 가치에 기반한 규율과 절제가 필요하다.
profile
열등감에 몸부림치는 중입니다.

0개의 댓글