Language/Java

[클린 코드] Ch.16 - 냄새와 휴리스틱

비소_ 2022. 8. 29.

클릭하시면 네이버 Book으로 연결됩니다!

해당 서적을 참고하여 개인 공부용으로 정리한 글입니다.

16장 SerialDate 리팩터링은 JUnit과 마찬가지로 기존 라이브러리를 리팩터링하는 과정인데,

리팩터링 하는 기준이 해당 포스트에 나오는 것을 기준으로 한 것이기 때문에 넘어가고바로 17장을 정리하게 되었습니다.


Intro

'냄새'는 마틴 파울러의 '리팩터링' 저서에서 나온 것이다.

이번 챕터는 이 리팩터링 내용 일부에 클린 코드 저자가 추가한것이다.

또한, '냄새'에서 알 수 있듯이 원래는 위험 요소를 뜻하고 '냄새가 발견되면 피해라' 라는 형식으로 작성되어 있다.

하지만 나는 추후에 한눈에 알 수 있도록 냄새보다 휴리스틱에 포커스를 맞춰 작성했다.

코드를 짜다 의문이 들때면 이 포스트를 참고하면 좋을 것 같다.


주석

C1: 필요한 정보만

다른 시스템(VCS, 버그 추적 시스템 등)에 저장할 정보는 주석으로 적절하지 못하다.

일반적으로 작성자, 최종 수정일, SPR(Software Problem Report) 번호 등과 같은 메타 정보만 주석으로 넣는다.

C2: 쓸모 없는 주석은 삭제

오래된 주석, 엉뚱한 주석, 잘못된 주석은 쓸모없으며 재빨리 삭제한다.

C3: 주석보다 코드를 깔끔히

코드로 설명이 가능한데 구구절절 설명한 주석은 제거하고, 코드를 더 깔끔하게 만진다.

C4: 정성스럽게

주석을 작성해야 한다면 간결하고 명료하게 작성한다.

C5: 코드 주석처리 금지

코드가 주석처리되면 왜 주석처리 된지 알 수가 없다.

코드 흐름을 망가뜨릴 뿐이다.

코드는 VCS가 기억하므로 즉시 제거한다.


환경

E1: 빌드는 한 단계로

빌드는 간단히 한 단계로 끝나야 한다.

온갖 JAR, XML 등 파일을 찾느라 여기저기 뒤적일 필요가 없어야 한다.

E2: 모든 단위 테스트는 한 번에

IDE에서 버튼 하나로 모든 테스트를 한번에 돌릴 수 있다면 가장 이상적이다.

Shell에서는 명령 하나로 가능해야 한다.

테스트를 빠르고, 쉽고, 명백하게 수행해야 한다.


함수

F1: 인수는 최소로

함수에서 인수 개수는 최소화해야 한다.

많다면 멤버 변수로 승격하거나 캡슐화로 줄일 수 있다.

F2: 출력 인수 금지

사용자는 일반적으로 인수를 입력으로 간주한다.

상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경한다.

F3: 플래그 인수 금지

boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다.

플래그 인수는 혼란을 초래하므로 피해야 마땅하다.

F4: 호출되지 않는 함수 삭제


일반

G1: 소스 파일 하나에 한 언어만

한 파일에 여러 언어를 혼용해서 사용하지 않는다.

소스 파일에서 언어 수와 범위를 최대한 줄이도록 노력해야 한다.

G2: 당연하게 작동

함수나 클래스가 예상치 못하게 작동하면 안된다.

최소 놀람 원칙(the Principle of Least Surprise)에 의거해 당연하게 여길 만한 동작과 기능을 제공해야 한다.

G3: 확실한 경계

모든 경계 조건, 예외, 구석진 곳 등은 알고리즘을 좌초시킬 암초다.

직관에 의존하지 말고 해당 조건을 모두 찾아내고 테스트해야한다.

G4: 안전 절차 따름

컴파일러 경고를 꺼버리거나 테스트 케이스 실패를 무시하는 것은 안전 절차를 무시하는 행동이다.

번거롭다는 이유에서 경고를 무시하지 말자.

G5: 중복 제거

DRY(Don't Repeat Yourself)라고 잘 알려진 중복은 이 책에서 가장 중요한 개념이다.

똑같은 코드는 당연히 간단한 함수로 교체한다.

switch/case나 if/else 문으로 똑같은 조건을 거듭 확인하는 것은 다형성으로 대체한다.

알고리즘이 유사하나 코드가 다른 것은 템플릿 메서드 패턴이나 전략 패턴으로 중복을 제거한다.

디자인 패턴은 대다수가 중복을 제거하는 방법이다.

BCNF(Boyce-Codd Normal Form) 역시 스키마에서 중복을 제거하는 전략이다.

이외에도 다양한 분야에서 중복을 제거하는 방법 역시 다양하다.

반드시 제거하자.

G6: 올바른 추상화 수준

추상화로 개념을 분리할 때는 철저해야 한다.

고차원 개념과 저차원 개념을 섞어서는 안 된다.

G7:  파생 클래스 의존 금지

기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 개념을 저차원 개념으로부터 분리해 독립성을 보장하기 위해서다.

따라서 기초 클래스가 파생 클래스를 사용한다면 문제가 있다.

일반적으로 기초클래스는 파생 클래스를 아예 몰라야 마땅하다.

또한, 기초 클래스와 파생 클래스를 다른 JAR 파일로 배포하고, 기초 JAR 파일이 파생 JAR 파일을 전혀 모른다면 독립적인 개별 컴포넌트 단위로 시스템을 배치할 수 있다.

물론 예외는 있다. (FSM 처럼 파생 클래스 개수가 고정되었고 밀접하다면)

G8: 과도한 정보

잘 정의된 인터페이스는 많은 함수를 제공하지 않는다.

우수한 개발자라면 노출할 함수를 제한할 줄 알아야 한다.

함수가 아는 변수 수, 클래스 인스턴스 변수 수도 작을수록 좋다.

자료, 유틸리티 함수, 상수, 임시 변수를 숨겨라.

메서드나 인스턴스 변수가 넘쳐나는 클래스는 피한다.

인터페이스를 매우 작게, 깐깐하게 만들고, 정보를 제한해 결합도를 낮춰야한다.

G9: 죽은 코드 삭제

throw 문이 없는 try-catch 블록, 호출되지 않는 메서드 등 죽은 코드를 발견하면 제거해야한다.

G10: 수직 배치

변수와 함수는 사용되는 위치에 가깝게 정의한다.

지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위차해야 한다.

비공개 함수는 처음으로 호출한 직후에 정의한다.

비공개 함수는 전체 클래스 범위(scope)에 속하지만 그래도 정의하는 위치와 호출하는 위치를 가깝게 유지한다.

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

G11: 일관성

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

G12: 언제나 깔끔히

죽은 코드와 마찬가지로 정보를 제공하지 못하거나 필요없는 잡동사니는 모두 제거한다.

G13: 인위적 결합 금지

서로 무관한 개념을 인위적으로 결합하지 않는다.

enum이나 범용 static 함수도 특정 클래스에 속할 이유가 없다.

인위적인 결합은 직접적인 상호작용이 없는 두 모듈사이에서 일어난다.

당장 편한 위치에 선언하지 말고 올바른 위치를 고민한다.

G14: 기능을 욕심내지 말 것

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

가령 메서드가 다른 객체의 getter/setter를 사용해 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.

public class HourlyPayCalculator {
    public Money calculateWeeklyPay(HourlyEmployee e) {
        int tenthRate = e.getTenthRate().getPennies();
        int tenthsWorked = e.getTenthsWorked();
        int straightTime = Math.min(400, tenthWorked);
        int overTime = Math.max(0, tenthsWorked - straightTime);
        int straightPay = straightTime * tenthRate;
        int overtimePay = (int)Math.round(overTime * tenthRate * 1.5);
        return new Money(straightPay + overtimePay);
    }
}

calculateWeeklyPay 메서드는 HourlyEmployee 클래스의 범위를 욕심 낸다.

기능 욕심은 한 클래스의 속사정을 다른 클래스에 노출하므로 별다른 문제가 없다면 제거하는 편이 좋다.

하지만 때로는 어쩔 수 없는 경우도 생긴다.

public class HourlyEmployeeReport {
    private HourlyEmployee employee;

    public HourlyEmployeeReport(HourlyEmployee e) {
        this.employee = e;
    }

    String reportHours() { 
        "Name : %s\tHours : %d.%1d\n",
        employee.getName(),
        employee.getTenthsWorked() / 10,
        employee.getTenthsWorked() % 10);
    }
}

이 경우 HourlyEmployee 클래스로 옮기면 객체 지향 설계의 여러 원칙을 위배할 뿐더러 HourlyEmployee가 보고서 형식과 결합되므로 보고서 형식이 바뀌면 클래스도 바뀐다.

G15: 선택자 인수 금지

boolean, enum 등 함수 동작을 제어하려는 인수는 하나 같이 바람직하지 않다.

인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.

G16: 의도를 명백히

G17: 코드를 올바르게 배치

G18: static 남용 금지

반드시 static 함수로 정의해야 한다면 재정의할 가능성은 없는지 따져본다.

일반적으로 static 함수보다 인스턴스 함수가 더 좋다.

G19: 서술적 변수

계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하면 프로그램 가독성이 높아진다.

G20: 이름과 기능을 일치시키기

G21: 알고리즘 이해

구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인한다.

알고리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.

G22: 의존 정보를 명시적으로 요청

의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.

HourlyReporter 클래스가 정보를 모아 HourlyReportFormatter에 넘긴다고 가정하자.

이때 HourlyReporter 클래스에 PAGE_SIZE = 55라는 상수가 선언되어 있으면, 이는 [G17]을 위반한 것이면서, HourlyReportFormatter가 페이지 크기를 알 것이라 가정(논리적 의존성)한 것이다.

만약 HourlyReportFormatter가 페이즈 크기 55를 제대로 처리하지 못한다면 오류가 생긴다.

따라서, HourlyReportFormatter에 getMaxPageSize() 메서드를 추가하고 HourlyReporter가 이 메서드를 호출하는 형식으로 변경해야 한다.

G23: 다형성 사용

if/else나 switch 문보다 다형성을 먼저 고려해야 한다.

선택 유형 하나에는 switch 문을 한 번만 사용하고, 같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성해 대신한다.

G24: 표준 표기법 사용

업계 표준에 기반한 구현 표준을 따라야 한다.

팀이 정한 표준은 팀원들 모두 따라야 한다.

G25: 상수

숫자 혹은 특정한 문자열을 명명된 상수(static final) 뒤로 숨긴다.

G26: 결정은 정확하게

코드에서 뭔가를 결정할 때 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.

호출하는 함수가 null을 반환할지도 모른다면 반드시 null을 검증한다.

코드에서 모호성과 부정확은 의견차나 게으름의 결과다. 어느쪽이든 제거한다.

G27: 구조 사용

설계 결정을 강제할 때 관례를 사용하는 것보단 구조 자체로 강제하면 더 좋다.

예를 들어, enum 변수가 멋진 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다.

G28: 조건 캡슐화

조건의 의도를 분명히 밝히는 함수로 표현한다.

G29: 긍정 조건 사용

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

G30: 함수는 한 가지만

G31: 숨겨진 시간적 결합

함수가 시간적인 결합(순서대로 수행)이 있다면 이를 노출해야 한다.

G32: 일관성

코드 구조를 잡을 때는 이유를 고민하고, 코드 구조로 명백히 표현하라.

일관성이 있다면 남들도 일관성을 따르고 보존한다.

G33: 경계 조건 캡슐화

경계 조건은 한 곳에서 별도로 처리한다.

쉽게 말해, 코드 여기저기에 +1이나 -1을 흩어놓지 않는다.

G34: 추상화 수준은 한 단계만 내려가기

함수 내 모든 문장은 추상화 수준이 동일해야 한다.

그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.

개념은 간단하지만 인간은 추상화 수준을 뒤섞는 능력이 너무나도 뛰어나다..

G35: 설정 정보는 최상위에

기본값 상수나 설정 관련 상수는 추상화 최상위 단계에 둔다.

저차원 함수가 필요하다면 고차원 함수가 호출 시 인수로 넘긴다.

G36: 추이적 탐색 회피

한 모듈은 주변 모듈을 모를수록 좋다.

즉, A가 B를 사용하고 B가 C를 사용하더라도 A는 C를 알아야할 필요가 없다. (디미터의 법칙, Law of Demeter)

a.getB().getC()라는 형태를 사용하면 설계와 아키텍처를 바꿔 B와 C 사이에 Q를 넣기 쉽지 않다.


자바

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

와일드카드로 패키지를 지정하면 특정 클래스가 존재할 필요는 없다.

import 문은 패키지를 단순히 검색 경로에 추가하므로 진정한 의존성이 생기지 않는다.

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

상수를 인터페이스에 선언하고 상속하는 방식은 사용하지 않는다.

static import를 사용하라

J3: 상수 보다 Enum

Enum은 int보다 훨씬 더 유연하고 서술적인 강력한 도구다.


이름

N1: 서술적 이름 사용

가독성의 90%는 이름이 결정한다.

시간을 들여 현명한 이름을 선택하고 유효한 상태로 유지한다.

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

구현을 드러내는 이름은 피한다.

저수준의 이름은 모듈을 구속시킨다.

N3: 표준 명명법 사용

N4: 명확한 이름

N5: 이름 길이는 범위에 비례

N6: 인코딩을 피할 것

헝가리안 표기법처럼 이름에 유형 정보나 범위 정보를 넣어서는 안된다.

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

하는 일을 모두 기술하는 이름을 사용한다. 부수 효과를 숨기지 않는다.


테스트

T1: 불충분한 테스트

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

확인하지 않는 조건이나 검증하지 않는 계산이 있다면 불완전하다.

T2: 커버리지 도구 사용

커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다.

T3: 사소한 테스트 스킵 금지

T4: 무시한 테스트

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

선택 기준은 컴파일 가능 여부에 달려있다.

T5: 경계 조건 테스트

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

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

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

T7: 실패 패턴을 살펴라

테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다.

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

T9: 테스트는 빨라야 한다

느린 테스트는 돌리지 않는다.

그러므로 최대한 빨리 돌아가게 노력한다.

'Language > Java' 카테고리의 다른 글

[클린 코드] Ch.15 - JUnit  (0) 2022.08.25
[클린 코드] Ch.14 - 점진적인 개선  (0) 2022.08.24
[클린 코드] Ch.13 - 동시성  (0) 2022.08.23
[클린 코드] Ch.12 - 창발성  (0) 2022.08.22
[클린 코드] Ch.11 - 시스템  (0) 2022.08.19

댓글