[Clean Code] XVII. 냄새와 휴리스틱

XVII. 냄새와 휴리스틱

냄새

  • 마틴파울러가 말한 나쁜 코드의 조짐
  • 리팩토링 책에서는 켄트백이 당시 애기 키우면서 기저귀 냄새때문에 그렇게 표현했다고 읽은것 같음

전체 리스트

주석 - Comment

C1(부적절한 정보)

  • 주석은 코드,설계에 기술적인 설명만 부연
  • 변경이력, 기록등은 주석말고 형상관리에 적어라.

C2(쓸모 없는 주석)

  • 오래된 주석, 엉뚱한 주석, 잘못된 주석
  • 최대한 빨리 삭제 하자

C3(중복된 주석)

  • 코드 설명 충분한데 구구절절 설명하는 주석
    중복된 주석 예시
    1
    i++; //i 증가
  • 서명만 있는 Javadoc도 마찬가지

C4(성의없는 주석)

  • 작성할 가치가 있는 주석은 잘 작성할 가치가 있도록 정성껏 신중하게 선택한 단어로 작성
  • 주절대지 않고 당연한소리 반복없이 간결, 명료하게 올바른 문법으로 작성하도록 한다

C5(주석 처리된 코드)

  • 얼마나 오래된 코드인지, 중요한 코드인지 알 수가 없음
  • 또한 삭제도 함부로 할 수 없게 됨(다른 사람 히스토리를 모르므로)
  • 낡아간다.나중에 다른 코드들이 변경 된후에 주석을 풀면 에러덩어리
  • 필요하면 이전 버전을 가져오면 되므로 즉각 삭제하라!

환경 - Environment

E1(여러 단계로 빌드)

  • 빌드는 간단히 한단계의 명령어로 전체를 체크하웃해서 빌드 할 수 있도록

E2(여러 단계로 테스트)

  • 모든 단위 테스트는 한 명령으로 돌려야 한다.
  • IDE의 버튼 하나, 쉘의 명령 하나 등

함수 - Function

F1(너무 많은 인수)

  • 함수에서 인수개수는 작을수록 좋다 . 없으면 더 좋다
  • 넷 이상은 가치가 의심스러우므로 최대한 회피

F2(출력인수)

  • 일반적으로 읽는 이는 인수를 입력으로 간주한다.
  • 따라서 출력인수는 일반적인 직관을 정면 위배
  • 상태 변경이 필요하다면 출력 인수 사용 대신 함수가 속한 객체의 상태를 변경하라

F3(플래그 인수)

  • Flag Boolean은 함수가 여러 기능을 수행하는 명백한 증거
  • 다른 함수로 쪼개고 네이밍 제대로

F4(죽은 함수)

  • 아무도 호출하지 않는 함수
  • 낭비. 삭제!

일반 - General

G1(한 소스파일에 여러 언어)

  • 어떤 jsp : HTML, JAVA, tag Library, English comment, Javadoc, XML, JavaScript
  • 이상적으로는 파일 하나에 언어 하나!
  • 현실적으로 불가피하지만 최대한 줄이도록 노력 필요!

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

  • 최소 놀람의 원칙(The Principle of Least Surprise)
  • 당연하게 여길만한 동작과 기능이 올바르게 제공되어야 한다.
  • 그러지 않으면 신뢰할 수 없게 되고 직관적으로 예상할 수 없음

G3(올바로 처리되지 않은 경계)

  • 코드는 올바로 작동 해야 →올바른 동작은 아주 복잡하다
  • 모든 경계조건을 테스트하는 테스트 케이스를 작성하라

G4(안전절차 무시)

  • UUID 직접 제어, 컴파일러 경고 끄기, 실패 테스트 나중에 미루는 태도 등
  • 위험하다

G5 중복

  • 이 책의 가장 중요한 규칙중 하나
  • 다른 이들의 언급
    • DRY - 데이비드 토머스와 앤디 헌트 - 실용주의 프로그래머
      • Don’t Repeat Yourself(반복하지 마라)
      • 계방 폐쇄 원칙은 DRY 원칙이 적용되어야만 적용되는 원칙
      • 단일 책임 원칙도 DRY에 의존한다
    • ONCE, AND ONLY ONCE
      • 매 행동 각기 모두 한번만, 딱 한번만 소스에 나타나야 한다
      • 검색에 따라선 DRY 원칙의 subset이라고 표현된 곳도 있다.
      • KENT BACK : XP 핵심 규칙중 하나다!
      • 론 제프리스(XP 창시자중 하나, 최근 The Nature of Software Development을 쓴) : “모든 테스트를 통과한다”는 규칙 다음으로 중요하다
  • 중복을 발견할 떄마다 추사화할 기회로 간주!
    • 추상화로 중복 정리 → 설계 언어 어휘 증가 → 다른이들도 어휘 사용이 쉬워짐 → 높은 추상화 → 구현이 빨라지고 오류가 적어짐
  • 패턴
    • 뻔한 패턴 : 똑같은 코드(copy&paste처럼) → 간단한 함수로 교체
    • 미묘한 패턴 : 여러 모듈에서 switch,if/else로 같은 조건 거듭 확인하는 중복 → 다형성으로 대체
    • 더더욱 미묘한 패턴 : 유사 알고리즘인데 코드가 다른 중복 → 중복은 중복 →템플릿 메서드 패턴, 전략 패턴 사용으로 중복 제거
  • 최근 15년의 디자인 패턴 대다수는 중복을 잘 제거하는 방법에 불과
    • BCNF: DB 스키마에서 중복 제거 전략
    • : 역시 모듈 정리, 중복 제거 전략
    • 구조적 프로그래밍도 마찬가지
  • 결론 : 무조건 중복을 발견하면 없애라!

G6(올바르지 못한 추상화 수준)

  • 추상화 : 저차원 상세 개념에서 고차원 일반 개념을 분리한다
  • 추상화 수행 : (고차원 일반 개념 표현의)추상 클래스와 (저차원 상세 개념)을 표현하는 파생 클래스 생성
  • 추상화 분리는 철저히 : 모든 저차원 개념은 파생 클래스, 모든 고차원 개념은 추상 클래스
    • ex) 세부구현 관련 상수, 변수, 유틸리티 : 추상은 구현 정보에 무지해야 하므로 파생에 넣어야 한다
  • 소스파일, 컴포넌트 모듈도 마찬가지
    • 좋은 SA : 개념을 다양한 차원으로 분리 → 다른 컨테이너에 넣는다
    • 떄로는 기초 클래스 + 파생 클래스 분리, 때로는 소스파일, 모듈, 컴포넌트로 분리
  • 잘못된 추상화는 꼼수나 임시로 해결이 불가 : 가장 개발자에게 어려운 작업 중 하나가 추상화

G7(기초 클래스가 파생 클래스에 의존)

  • 앞 G6에서 봤던 부분 다시 생각해보자.
  • 개념을 기초+파생으로 나누는 이유
    • 고차원 기초 클래스 개념을 파생 클래스 개념으로부터 분리
    • 독립성을 보장하기 위해서
  • 따라서 기초가 파생에 의존하면 문제가 있음
  • 예외
    • 파생 클래스 개수가 확실히 고정되어있다면 기초 클래스에서 파생 클래스를 선택하는 코드가 들어간다
    • 책의 FSM 구현 : 기초 클래스+파생 클래스 굉장히 밀접하며 언제나 같은 JAR 파일로 배포
    • 일반적으로는 기초와 파생클래스를 다른 JAR로 배포하는 것이 좋다
      • 이렇게 배포시 (기초에서 파생 JAR를 전혀 모른다면) 독립적인 개별 컴포넌트 단위로 시스템 배치가 가능
      • 컴포넌트 변경시 해당 컴포넌트만 다시 배치 가능
      • 변경 영향이 작아서 현장에서 시스템 유지보수가 수월함

G8(과도한 정보)

  • 높은 응집도, 낮은 결합도
    • 잘 정의된 모듈은 인터페이스가 아주 작고 함수가 적으며 많은 동작이 가능
    • 부실한 모듈은 간단한 동작에 온갖 인터페이스와 꼭 호출해야하는 온갖 함수 → 높은 결합도 유발
  • 잘하는 개발
    • 클래스/모듈 인터페이스에 노출할 함수 제한
    • 클래스가 제공 메서드 수는 작을 수록
    • 함수가 아닌 변수 수도 작을수록
    • 클래스의 인스턴스 변수 수도 작을수록 좋다.
  • 이렇게 하여라
    • 숨겨라: data, 유틸리티 함수, 상수, 임시 변수
    • 피하라 : 메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라
    • 마구 만들지 마라 : 하위에서 필요하다고 protected 변수 함수를 마구 생성하지 말아라
    • 인터페이스를 매우 작게! 깐깐하게 만들고! 정보를 제한해서 결합도를 낮춰라

G9(죽은 코드)

  • 죽은 코드 : 실행되지 않는 코드

  • 죽은 코드의 예

    • 불가능한 조건을 확인하는 if문
    • throw문이 없는 try문의 catch 블럭
    • 아무도 호출하지 않는 유틸리티 함수
    • switch/case에서 불가능한 case 조건
  • 죽은 코드를 제거하라

    • 시간이 지날수록 악취가 심해진다(설계가 변해도 제대로 수정이 안되므로)

G10(수직 분리된 경우)

변수 함수는 사용되는 위치에 최대한 수직으로 분리되지 않고 가깝게 정의

  • 지역변수: 처음으로 사용하기 직전에 선언, 수직으로 가까운 곳에 위치
  • 비공개 함수 : 처음으로 호출한 직후 정의(클래스 scope여도 그래도 호출 부위와 가깝도록 유지)

G11(일관성 부족)

  • 어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다.
  • 최소놀람의 원칙에도 부합
  • 신중하게 선택한 표기법을 신중하게 따른다
  • 예를들어 response변수에 HttpServletResponse 인스턴스를 저장했으면
    HttpServletResponse를 사용하는 다른 함수에서도 일관성 있도록 동일한 변수 사용
  • 착실히 적용하면 이리 간단한 일관성 만으로 코드가 읽고 수정이 쉬워진다.

G12(잡동사니)

  • 비어있는 기본 생성자
  • 아무도 사용하지 않는 변수
  • 아무도 호출하지 않는 함수
  • 정보를 제공하지 못하는 주석

G13(인위적 결합)

  • 서로 무관한 개념을 인위적으로 결합하지 않는다.
  • 예) 일반적 enum은 특정 클래스에 속할 이유가 없다
    • enum이 A클래스에 속한다면? enum을 사용하는 B 클래스가 A클래스를 알아야 한다
    • 범용 static함수도 마찬가지로 특정 클래스에 속할 이유가 없다
  • 인위적 결합 : 직접 상호작용이 없는 두 모듈 사이에서 일어난다
    • 뚜렷한 목적없이 변수/상수/함수를 당장 편한 위치에 넣어버린 결과
  • 변수/상수/함수를 선언할때는 시간을 들여 올바른 위치를 고민한다

G14(기능 욕심)

  • 마틴 파울러가 말하는 코드 냄새중 하나
  • 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 가져서는 안된다.
  • 메서드가 다른 객체의 메서드를 이용해 객체 내용 조작? 메서드가 그 객체 클래스의 범위를 욕심 내는 탓
  • 자신이 그 클래스에 속해 그 클래스의 변수를 직접 조작 원한다는 뜻
  • 기능 욕심은 한 클래스의 속사정을 다른 클래스에 노출하므로 별다른 문제가 없으면 제거가 좋다
  • 어쩔 수 없는 경우 예시
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class HoulyEmployeeReport{
    private HourlyEmployee employee;
    public HoulyEmployeeReport(HourlyEmployee employee){
    this.employee=employee;
    }
    String reportHours(){
    return String.format(
    "NAME: %s\tHours:%d.%1d\n",
    employee.getName(),
    employee.getTenthsWorked()/10,
    employee.getTenthsWorked()%10
    );
    }
    }
    • HoulyEmployeeReport클래스의 reportHours()메서드는 HourlyEmployee클래스를 욕심낸다
    • 그렇다고 HourlyEmployee클래스가 보고서 형식을 알 필요는 없다.
    • 만약 예외를 두지 않고 함수를 HourlyEmployee로 옮기면 여러 원칙 위반(특히 SRP, OCP, CCP위반)
      • HourlyEmployee가 보고서 형식과 결합 → 보고서 형식이 바뀌면 클래스가 바뀌게 되어버림

G15(선택자 인수)

  • 인수에 따라 다른 동작 - F3(flag 인수)과도 유사한 부분이 있음
  • selector 인수 : 목적 기억도 어렵, 각 선택자 인수가 여러 함수를 하나로 조합
  • 큰 함수를 쪼개지 않으려는 게으름 소산
  • 함수 동작을 제어하려는 인수는 바람직하지 않다. 새로운 함수를 만들자.

G16(모호한 의도)

  • 행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 넘버 : 작성자 의도를 흐린게한다
  • 의도를 최대한 분명히 밝힌다(시간을 투자해서라도)

G17(잘못 지운 책임)

  • 개발자가 내리는 가장 중요한 결정 : 코드 배치 위치
  • 예) PI 상수는? 삼각함수를 선언한 클래스에 선언된다 → 독자가 자연스럽게 기대할 위치
  • 여기서도 최소 놀람의 원칙이 적용되는 것을 알 수 있다.
    다른 예시
  • 직원이 근무한 총 시간을 보고서로 출력하는 함수의 위치는 둘중 어디가 맞을까?
    1. 보고서를 출력하는 함수에서 총계를 계산
    2. 근무 시간을 입력받는 코드에서 총계를 계산
  • 결정을 내리는 방법
    1. 이 함수이름을 살펴본다 : 예로 getTotalHours()
    2. 이름만 보았을때 어디에서 총계를 계산하는게 나은가?
  • 답은 1번일 것

성능 때문에 2번답이 좋다고 판단된다면?

  • 이런 사실을 반영해서 네이밍 제대로 해야함
  • 위의 경우에는 2번의 모듈에 computeRunningTotalOfHours()의 이름

G18(부적절한 Static 함수)

좋은 static 예 : Math.max(double a, double b)

  • 특정 인스턴스 관련 기능이 아니다
  • max 메서드가 사용하는 정보는 2개의 인자가 전부.
  • 메서드 소유한 객체에서 가져오는 정보가 거의 없음
  • (결정적) override할 가능성이 전혀 없음
static으로 정의하면 안되는 함수
1
HourlyPayCalculator.calculatePay(employee, overtimeRate);
  • 함수를 재정의할 가능성 존재 : 수당 계산 알고리즘이 여러개 일 수도
  • 따라서 static으로 하면 안되고 Employee클래스에 속하는 인스턴스 함수여야 한다
  • 일반적으로 static함수보다 인스턴스 함수가 더 좋다. 조금만 의심스럽다면 인스턴스 함수로 정의
  • 재정의 가능성을 꼭 곰곰히 따져보고 static 정의 고려할 것

G19(서술적 변수)

  • kent beck이 다음의 훌륭한 책에서 지적
  • 프로그램 가독성을 가장 높이는 효과적인 방법 : 계산을 여러 단계로 나누고 중간값으로 서술적 변수 이름 사용
    FitNesse 예시
    1
    2
    3
    4
    5
    6
    Matcher match = headerPattern.matcher(line);
    if(match.find()){
    String key = match.group(1);
    String value = match.group(2);
    headers.put(key.toLowerCase(), value);
    }
  • 서술적 이름 사용 때문에 첫번째 그룹이 key, 2번째 그룹이 value라는 사실이 들어난다
  • 서술적이름은 많이 써도 괜찮다. 일반적으로는 많을 수록 더 좋다
  • 계산을 몇 단계로 나누고 중간값에 좋은 변수 이름만 부텽도 해독하기 어렵던 모듈이
    순식간에 읽기 좋은 모듈로 바뀐다

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

  • date.add(5); → 5일? 5주? 5시간? date인스턴스 변경인가, 아니면 새로운 Date를 리턴하는 함수인가?
  • 코드를 봐서 알 수가 없음
  • 기존 인스턴스 변경 : addDaysTo, increaseByDays 등의 이름이 좋다
  • 새로운 객체 반환 : daysLater, daysSince등의 이름이 좋다
  • 이름으로 분명치 않다면? 좋은 이름으로 바꾸거나, 좋은 이름을 붙이기 쉽게 기능을 정리하거나

G21(알고리즘을 이해하라)

  • 알고리즘의 충분한 이해없이 여기저기 if와 flag를 삽입하며 돌려서 결과를 만드는데 성공한 코드들
  • 테스트를 모두 통과한다는 것만으로 부족 → 알고리즘이 올바르다는 사실을 알아야 한다
  • 알고리즘의 올바름을 확인/이해하려면? 기능이 뻔히 보일정도로 깔끔하고 명확하게 함수를 재구성 하는 것이 최고의 방법

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

  • 모듈이 다른 모듈에 의존한다면 논리적 의존성으로는 부족하며 물리적인 의존성도 있어야 한다
    책의 예시
  • HourlyReporter 클래스의 generateReport()PAGE_SIZE라는 상수를 이용해 돌아간다
  • 논리적 의존성 : 55의 값으로 선언된 PAGE_SIZE라는 상수 →
  • 페이지 크기는 HourlyReporter가 아닌 HourlyReportFormatter가 책임질 정보
  • 앞서 설명한 [G17]에 해당한 실수
  • 논리적 의존성 : HourlyReporter클래스는 HourlyReportFormatter가 55 페이지 크기를 처리할 줄 안다는 사실에 의존 → 이 페이지 크기를 알거라고 가정한 자체가 논리적 의존성
  • 가정이 틀리면 오류가 되어버림
  • 해결책 : 논리적 의존성을 물리적 의존성으로
    • HourlyReportFormattergetMaxPageSize()메서드를 추가
    • HourlyReporter클래스는 PAGE_SIZE 상수 대신 getMaxPageSize()호출

G23 if/else, Switch/Case 대신 다형성 사용

  • 3장에서 밥 아저씨 : “새 유형보다 새 함수를 추가할 확율이 높은 코드는 switch가 더 적합하다”
    1. 대다수 개발자 swit문 선택 이유 : 올바른 선택이 아니라 손쉬운 선택이기 때문
      그러므로 Switch 선택 전에 다형성 먼저 고려하라는 의
    2. 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다 → 그러므로 모든 switch문을 의심하라
  • 밥아저씨가 따르는 ‘one switch” 규칙
    • 선택 유형 하나에는 switch문을 한 번만 사용
    • 같은 선택을 수행하는 다른 코드에는 다형성 객체를 생성해 switch문을 대신한다

G24(표준 표기법)

  • 팀은 업계 표준에 기반한 구현 표준을 따라야 한다
  • 팀이 정한 표준은 팀원들 모두가 따라야 한다
  • 밥아저씨가 따르는 표기법 : p512 목록 B-7 ~ B-14

G25(매직 숫자는 명명된 상수로 교체)

  • SW개발 에서 가장 오래된 규칙 : 일반적으로 코드에서 숫자를 사용하지 말라는 규칙
  • 상수가 너무 미해가 쉽고 자명하다면 숨길 필요는 없음: 하루는 24시간, 1분은 60초 등
  • 원주율: 자명하지만 숫자가 길고 근사값 사용시 오류 발생 : 다행히 Math.PI를 사용하면 된다
  • 매직숫자는 단순히 숫자만 의미하느넥 아니라 의미가 분명하지 않은 토큰 모두를 말한다

G26(정확하라)

정확하지 않은 행동

  • 검색결과 중 첫 번째 결과만 유일한 결과로 간주 → 순진
  • 부동 소수점으로 통화 표현 → 범죄
  • 갱신할 가능성이 희박하다고 잠금/트랜잭션 관리를 건더뛰는 것 → 게으름
  • List로 선언할 변수를 ArrayList로 선언 → 지나친 제약
  • 모든 변수를 protected로 선언 → 무절제
  • 코드에서 무언가를 결정할때는 정확하게 결정한다
    • 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충하면 안된다.
    • 호출하는 함수가 null 반환할지 모른다 → 반드시 null 점검
    • 검색 결과가 하나뿐이라고 짐작 → 하나인지 확실히 확인
    • 통화를 다룬다면 정수를 사용하고 반올림을 올바로 처리한다(아니면 Money클래스 사용)
    • concurrent 특성으로 동시 갱신 가능성 존재? → 적절한 잠금 매커니즘

G27(관례보다 구조 사용)

  • 설계 결정 강제시 규칙보다 관례를 사용
    예시
  • Enum변수가 멋진 Switch/case <<<< 추상 메서드가 있는 기초 클래스
  • Switch/case를 매번 똑같이 구현하게 강제하기는 어렵다
  • 파생 클래스는 추상 메서드를 모두 구현하지 않으면 안되므로

G28(조건을 캡슐화)

  • 부울논리는 (if,while에 넣어 생각하지 않아도) 이해가 오래걸림
  • 조건의 의도를 분명히 밝히는 함수
  • 예를 들어 if(timer.hasExpired() && !timer.isRecurrent())보다는
    if (shouldBeDeleted(timer))라는 코드가 좋다

G29(부정조건은 피하라)

  • 부정 조건은 긍정보다 이해를 어렵게 하므로 가능한 긍정 조건으로 표현

G30(함수는 한 가지만)

  • 함수는 단일 입무만.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //밑의 함수는 한 가지만 수행하는 함수가 아니다.
    public void pay() {
    for (Employee e : employees) {
    if (e.isPayday()){
    Money pay = e. calculatePay();
    e.deliverPay(pay);
    }
    }
    }
  • 여러 단락을 하는 함수는 작은 함수 여럿으로 나눠야 마땅하다

    이제 각 함수는 한가지 임무만 수행
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void pay() {
    for (Employee e : employees) {
    payIfNecessary(e);
    }
    }

    private void payIfNecessary(Employee e) {
    if (e.isPayday())
    calculateAndDeliveryPay(e);
    }

    private calculateAndDeliveryPay(Employee e) {
    Money pay = e. calculatePay();
    e.deliverPay(pay);
    }

G31(숨겨진 시간적 결합)

  • 때론 시간적 결합이 필요하지만 이를 숨겨서는 안된다
  • 함수를 짤 때는 함수 인수를 적절히 배치해 함수 호출 순서를 명백히 드러내야 한다
    시간적 결합이 필요한 함수
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MoogDiver {
    Gradient gradient;
    List<Spline> splines;

    public void dive(String reason) {
    //밑의 3가지 함수는 실행되는 순서가 중요하다. 하지만 시간적 결합을 강제하지 않는다.
    saturateGradient();
    reticulateSplines();
    diveForMoog(reason);
    }
    }
더 좋은 코드
1
2
3
4
5
6
7
8
9
10
public class MoogDiver {
Gradient gradient;
List<Spline> splines;

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

설명

  1. 강제로 인자를 연결 소자로 만들어 시간적 결합을 노출
  • 각 함수의 결과는 다음 함수에 필요하므로 순서를 바꿔서 호출할 수가 없다
  1. 함수가 복잡해졌다
  • 의도적으로 추가한 구문적인 복잡성이 원래 있던 시간의 복잡성을 드러낸 것이다.
  1. 인스턴스 변수를 그대로 두었다는 사실에 주목
  • 해당 클래스의 private메서드에 필요한 변수일지 모른다
  • 그렇다 하더라도 제자리를 찾은 변수들이 시간적 결합을 좀 더 명백히 드러나게 해줄 것이다

G32(일관성을 유지하라)

  • 코드 구조를 잡을 떄는 이유를 고민하고 이유를 코드 구조를 명백히 표현하라
  • 일관성 없는 구조 → 남들이 마음대로 바꿔도 괜찮다고 생각한다.
  • 시스템 전반에 걸쳐 일관성 있는 구조 → 남들도 따르고 보존

G33(경계 조건을 캡슐화)

  • 경계 조건은 빼먹거나 놓치기 쉬우므로 여기저기에서 처리하지 않고 한 곳에서 별도로 처리한다
  • +1, -1을 흩어 놓지 말자
    FIT에서 가져온 예제
    1
    2
    3
    4
    if(level + 1 < tags.length>) {
    parts = new Parse(body, tags, level + 1, offset + endTag);
    body = null;
    }
  • level + 1이 2번 나온다. 이런 경계 조건은 변수로 처리하자
    경계조건 캡슐화
    1
    2
    3
    4
    5
    int nextLevel = level + 1;
    if(nextLevel < tags.length>) {
    parts = new Parse(body, tags, nextLevel, offset + endTag);
    body = null;
    }

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

  • 함수 내 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다
  • 함수 내 모든 문장은 추상화 수준이 동일해야 한다
  • 이번 장에서 가장 이해하기 어렵고 따르기도 어려운 항목
  • 개념은 간단하지만 인간은 추상화 수준을 뒤섞는 능력이 너무 뛰어나다
    FitNess의 모듈 HruleWidget에서 가져온 Code
    1
    2
    3
    4
    5
    6
    7
    public String render() throws Exception {
    StringBuffer html = StringBuffer("<hr");
    if (size > 0)
    html.append(" size=\"").append(size+1).append("\"");
    html.append(">");
    return html.toString();
    }
    내용
  • 페이지를 가로지르는 수평자 html 태그 생성, 높이는 size변수로 지정하고 있다.
  • ----처럼 4개 이상의 연이은 대시를 감지해 태그로 변환하는 코드임
  • 추상화 수준이 최소한 2개가 섞여 있다.
    1. 수평선에 크기가 있다는 개념
    2. HR 태그 자체의 문법
  • 수정 내용은 밑과 같다 → size 변수 네이밍 적용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public String render() throws Exception {
    HtmlTag hr = new HtmlTag("hr");
    if (extraDashes > 0)
    hr.addAttribute("size", hrSize(extraDashes));
    return hr.html();
    }

    private String hrSize(int height) {
    int hrSize= height + 1;
    return String.format("%d", hrSize);
    }
  • render 함수는 HR 태그만 생성한다. 태그 문법은 상관하지 않음
  • html 문법은 HtmlTag 모듈이 알아서 처리
  • HtmlTag 모듈은 xhtml 표준을 준수해서 <hr/>출력 - 미묘한 버그 캐치
  • 추상화 수준 분리는 리팩터링 수행의 가장 중요한 이유중 하나이며 제대로 하기 가장 어려운 작업

G35(설정 정보는 최상위 단계)

  • 추상화 최상위에 있어야할 기본값 상수, 설정 관련 상수를 저차원 함수에 숨겨선 안된다.
  • 대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.

G36(추이적 탐색을 피하라)

디미터의 법칙 :모듈은 주변 모듈을 모를 수록 좋다. 자신이 직접 사용하는 모듈만 알아야 한다

  • A→B→C 여도 A가 C를 알 필요는 없다
  • 따라서 a.getB().getC().doSomething();은 바람직하지 않다.
    • 여러 모듈에서 위와 같은 코드의 형태를 사용한다면?
    • 설계와 아키텍처를 바꿔서 B와 C사이에 Q를 넣기 쉽지 않다
    • a.getB().getC()를 전부 찾아 a.getB().getQ().getC()로 바꿔야 하기 때문
    • 너무 많은 모듈이 아키텍처를 너무 많이 알게 됨 → 아키텍처가 굳어진다
  • 디미터의 법칙은 결합도와 관련된 것이며, 객체의 내부 구조가 외부로 노출되는것에 대한 것이다
  • 따라서 아래는 예외
    • Stream API등의 여러 도트는 해당사항이 없다(동일한 스트림 변환이고 캡슐화 그대로 유지)
    • DTO, 컬렉션 객체등 자료 구조 : 당연히 내부를 노출해야 함
  • 결론 : 내가 사용하는 모듈이 내게 필요한 모든 서비스를 제공해야 하며, 객체 그래프로 탐색할 필요가 없어야 한다

자바 - JAVA

J1(긴 import 대신 wild 카드 사용)

의존성 문제

  • 명시적으로 클래스를 import하면 그 클래스가 반드시 존재해야 한다
  • 와일드 카드로 패키지 지정시 특정 클래스 존재 필요 없음
  • import는 단순히 검색 경로에 추가하므로 진정한 의존성이 생기지 않음 → 모듈간의 결합성이 낮아진다.

와일드 카드 사용 단점 : 때때로 이름 충돌, 모호성 초래

  • 이름이 같거나, 패키지가 다른 클래스는 명시적 import 사용하자
  • 번거롭지만 자주 발생하지 않음 - 여전히 와일드카드 import >>명시적 import

J2(상수는 상속하지 않는다)

  • 하위에서 발견하면 계속 위로 거슬려서 찾아봐야한다. → 언어의 범우 규칙을 속이는 행위
  • 대신 static import를 사용하라

상수 대 Enum

  • public static final int말고 ENUM을 사용하라!
  • enum은 메서드와 필드도 사용할 수 있는 강력한 도구다!

이름 - Name

N1(서술적인 이름 사용)

  • 이름은 성급하지 않고 신중하게!
  • SW 가독성의 90%는 이름이 결정 → 시간을 들여 현명한 이름을 선택하고 적합성 유지

N2(적절한 추상화 수준에서 이름 선택)

  • 구현을 드러내는 이름은 피하고 해당 클래스/함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라
  • 쉽지 않겠지만 안정적인 코드를 만들려면 지속적인 개선/노력이 필요하다
    예시
    추상화 수준을 살펴보자
    1
    2
    3
    4
    5
    6
    7
    public interface Modem {
    boolean dial(String phoneNumber);
    boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedPhoneNumber();
    }
  • 얼핏 보면? 적절해 보이며, 대다수 애플리케이션엔 문제가 없음
  • 현대의 모뎀은 전용선, 케이블, USB등을 사용하기도 → 전화번호 개념은 추상화 수준이 틀렸다
    더 좋은 이름 전략
    1
    2
    3
    public interface Modem {
    boolean connect(String connectionLocator);
    }

N3(가능하다면 표준 명명법)

기존 명명법 사용 이름은 이해가 쉽다

  • Decorator 패턴 활용한다면 장식 클래스 이름에 Decorator라는 단어를 사용해야 한다.
  • AutoHangupModemDecorator: 세션 끝 무렵 자동으로 연결을 끊는 기능으로 Modem을 장식하는 클래스 이름에 적합
  • 자바에서 객체를 문자열로 변환하는 함쉬: toString() 많이 씀, 가급적 관례를 따르는 편이 좋다.
  • 팀이 특정 프로젝트에 적용할 표준 - 유비쿼터스 언어(라고 에릭 에반스는 부른다)
    • 코드에는 이 언어에 속하는 용어를 열심히 사용하자
    • 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할 수록 독자가 코드 이해가 쉽다

N4(명확한 이름)

  • 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다
    역시 FitNess 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    private String doRename() throws Exception {
    if (refactorReferences)
    renameReferences();
    renamePage();

    pathToRename.removeNameFromEnd();
    pathToRename.addNameToEnd(newName);
    return PathParser.render(pathToRename);
    }
    네이밍으로 함수의 목적 파악이 분명하지 않고 광범위, 모호
  • doRename()안에 renamePage()함수가 들어있는데.. 이름만으로 차이점을 전혀 알 수가 없다
  • 좋은 이름 : renamePageAndOptionallyAllReferences
    • 아주 길지만 모듈에서 한번만 호출된다
    • 길다는 단점을 서술성이 충분히 메꿔준다

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

  • 이름 길이는 범위 길이에 비례해야 한다 : 범위가 작으면 짧은 이름, 범위가 길면 긴 이름
  • 5줄 안팎이라면 i,j등의 변수 이름도 괜찮다
    볼링 예시
    1
    2
    3
    4
    private void rollMany(int n, int pins) {
    for (int i = 0; i < n ; i++)
    g.roll(pins);
    }
  • 깔끔, 오히려 i를 rollCount로 쓰면 더 햇갈릴 수 있다.
  • 범위가 길수록 이름을 정확하고 길게 짓는다

N6(인코딩을 피하라)

  • 헝가리안 표기법 절대 쓰지마라
  • 현대 개발 환경에서는 m_, f등의 접두어 불필요
  • IDE의 발전으로 이름 조작없이 모든 정보 파악 가능

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

  • 함수/변수/클래스가 하는 일을 모두 기술하는 이름을 사용
  • 이름에 부수효과를 숨기지 않는다.여러 작업을 하면 해당 작업 모두를 반영하는 네이밍이 필요
    TestNG의 코드
    1
    2
    3
    4
    5
    6
    public ObjectOutputStream getOos() throws IOException {
    if (m_oos == null) {
    m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
    }
  • 위 함수는 단순히 “oos”만 가져오지 않고, 기존에 “oos”가 없으면 생성한다.
  • 그러므로 createOrReturnOos()라는 이름이 더 좋다

테스트 - Test

T1(불충분한 테스트)

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

T2(커버리지 도구 사용)

  • 테스트가 불충분한 모듈 찾기가 쉬어진다. 붉은 색상으로 드러나는 IDE가 그러함

T3(사소한 테스트를 건더 뛰지 마라)

  • 사소한 테스트는 짜기 쉽다
  • 사소한 테스트가 제공하는 문서적 가치 >>>>>>>>>> 구현에 드는 비용

T4(무시한 테스트는 모호함을 뜻함)

  • 불분명한 요구사항은 테스트케이스를 @Ignore처리(컴파일 불가능한 상태라면 주석 처리)

T5(경계 조건 테스트하라)

  • 경계조건은 각별히 신경써서 테스트
  • 알고리즘 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다

T6(버그 주변은 철저히 테스트)

  • 버그는 서로 모이는 경향
  • 버그를 발견했다면 그 함수를 철저히 테스트하라 →
  • 십중 팔구 다른 버그도 발견한다

T7(실패 패턴을 살펴라)

  • 때로는 테스트 케이스가 실패하는 패턴으로 문제 진단
  • 테스트 케이스를 최대한 꼼꼼히 짜라는 이유도 여기에 있음
  • 예) 입력이 5자를 넘기는 케이스가 모두 실패, 특정인자로 음수를 넘어가는 케이스가 모두 실패~
  • 테스트 리포트 빨간색/녹색만 보고도 꺠달음이 온다

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

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

T9(테스트는 빨라야 한다)

  • 느린 테스트는 실행하지 않게 된다

결론

  • 이 리스트가 완전한 것은 아니다. 가치 체계를 피력할 뿐
  • 가치 체계 자체가 클린 코드 책의 주제이자 목표
  • 특정 규칙만 따른다고, 리스트를 익힌다고 클린코드가 얻어지거나 소프트웨어 장인이 되지 않는다
  • 전문가, 장인 정신은 가치에서 나오며 그 가치에 기반한 규율과 절제가 필요

Related POST

공유하기