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을 쓴) : “모든 테스트를 통과한다”는 규칙 다음으로 중요하다
- DRY - 데이비드 토머스와 앤디 헌트 - 실용주의 프로그래머
- 중복을 발견할 떄마다 추사화할 기회로 간주!
- 추상화로 중복 정리 → 설계 언어 어휘 증가 → 다른이들도 어휘 사용이 쉬워짐 → 높은 추상화 → 구현이 빨라지고 오류가 적어짐
- 패턴
- 뻔한 패턴 : 똑같은 코드(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
14public 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 상수는? 삼각함수를 선언한 클래스에 선언된다 → 독자가 자연스럽게 기대할 위치
- 여기서도 최소 놀람의 원칙이 적용되는 것을 알 수 있다.
다른 예시 - 직원이 근무한 총 시간을 보고서로 출력하는 함수의 위치는 둘중 어디가 맞을까?
- 보고서를 출력하는 함수에서 총계를 계산
- 근무 시간을 입력받는 코드에서 총계를 계산
- 결정을 내리는 방법
- 이 함수이름을 살펴본다 : 예로
getTotalHours()
- 이름만 보았을때 어디에서 총계를 계산하는게 나은가?
- 이 함수이름을 살펴본다 : 예로
- 답은 1번일 것
성능 때문에 2번답이 좋다고 판단된다면?
- 이런 사실을 반영해서 네이밍 제대로 해야함
- 위의 경우에는 2번의 모듈에
computeRunningTotalOfHours()
의 이름
G18(부적절한 Static 함수)
좋은 static 예 : Math.max(double a, double b)
- 특정 인스턴스 관련 기능이 아니다
- max 메서드가 사용하는 정보는 2개의 인자가 전부.
- 메서드 소유한 객체에서 가져오는 정보가 거의 없음
- (결정적) override할 가능성이 전혀 없음
1 | HourlyPayCalculator.calculatePay(employee, overtimeRate); |
- 함수를 재정의할 가능성 존재 : 수당 계산 알고리즘이 여러개 일 수도
- 따라서 static으로 하면 안되고 Employee클래스에 속하는 인스턴스 함수여야 한다
- 일반적으로 static함수보다 인스턴스 함수가 더 좋다. 조금만 의심스럽다면 인스턴스 함수로 정의
- 재정의 가능성을 꼭 곰곰히 따져보고 static 정의 고려할 것
G19(서술적 변수)
- kent beck이 다음의 훌륭한 책에서 지적
- 프로그램 가독성을 가장 높이는 효과적인 방법 : 계산을 여러 단계로 나누고 중간값으로 서술적 변수 이름 사용
FitNesse 예시 1
2
3
4
5
6Matcher 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 페이지 크기를 처리할 줄 안다는 사실에 의존 → 이 페이지 크기를 알거라고 가정한 자체가 논리적 의존성 - 가정이 틀리면 오류가 되어버림
- 해결책 : 논리적 의존성을 물리적 의존성으로
HourlyReportFormatter
에getMaxPageSize()
메서드를 추가HourlyReporter
클래스는PAGE_SIZE
상수 대신getMaxPageSize()
호출
G23 if/else, Switch/Case 대신 다형성 사용
- 3장에서 밥 아저씨 : “새 유형보다 새 함수를 추가할 확율이 높은 코드는 switch가 더 적합하다”
- 대다수 개발자 swit문 선택 이유 : 올바른 선택이 아니라 손쉬운 선택이기 때문
그러므로 Switch 선택 전에 다형성 먼저 고려하라는 의 - 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다 → 그러므로 모든 switch문을 의심하라
- 대다수 개발자 swit문 선택 이유 : 올바른 선택이 아니라 손쉬운 선택이기 때문
- 밥아저씨가 따르는 ‘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
15public 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
11public class MoogDiver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
//밑의 3가지 함수는 실행되는 순서가 중요하다. 하지만 시간적 결합을 강제하지 않는다.
saturateGradient();
reticulateSplines();
diveForMoog(reason);
}
}
1 | public class MoogDiver { |
설명
- 강제로 인자를 연결 소자로 만들어 시간적 결합을 노출
- 각 함수의 결과는 다음 함수에 필요하므로 순서를 바꿔서 호출할 수가 없다
- 함수가 복잡해졌다
- 의도적으로 추가한 구문적인 복잡성이 원래 있던 시간의 복잡성을 드러낸 것이다.
- 인스턴스 변수를 그대로 두었다는 사실에 주목
- 해당 클래스의 private메서드에 필요한 변수일지 모른다
- 그렇다 하더라도 제자리를 찾은 변수들이 시간적 결합을 좀 더 명백히 드러나게 해줄 것이다
G32(일관성을 유지하라)
- 코드 구조를 잡을 떄는 이유를 고민하고 이유를 코드 구조를 명백히 표현하라
- 일관성 없는 구조 → 남들이 마음대로 바꿔도 괜찮다고 생각한다.
- 시스템 전반에 걸쳐 일관성 있는 구조 → 남들도 따르고 보존
G33(경계 조건을 캡슐화)
- 경계 조건은 빼먹거나 놓치기 쉬우므로 여기저기에서 처리하지 않고 한 곳에서 별도로 처리한다
- +1, -1을 흩어 놓지 말자
FIT에서 가져온 예제 1
2
3
4if(level + 1 < tags.length>) {
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
} - level + 1이 2번 나온다. 이런 경계 조건은 변수로 처리하자
경계조건 캡슐화 1
2
3
4
5int 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
7public 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개가 섞여 있다.
- 수평선에 크기가 있다는 개념
- HR 태그 자체의 문법
- 수정 내용은 밑과 같다 → size 변수 네이밍 적용
1
2
3
4
5
6
7
8
9
10
11public 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
7public interface Modem {
boolean dial(String phoneNumber);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedPhoneNumber();
} - 얼핏 보면? 적절해 보이며, 대다수 애플리케이션엔 문제가 없음
- 현대의 모뎀은 전용선, 케이블, USB등을 사용하기도 → 전화번호 개념은 추상화 수준이 틀렸다
더 좋은 이름 전략 1
2
3public interface Modem {
boolean connect(String connectionLocator);
}
N3(가능하다면 표준 명명법)
기존 명명법 사용 이름은 이해가 쉽다
- Decorator 패턴 활용한다면 장식 클래스 이름에 Decorator라는 단어를 사용해야 한다.
AutoHangupModemDecorator
: 세션 끝 무렵 자동으로 연결을 끊는 기능으로 Modem을 장식하는 클래스 이름에 적합- 자바에서 객체를 문자열로 변환하는 함쉬:
toString()
많이 씀, 가급적 관례를 따르는 편이 좋다. - 팀이 특정 프로젝트에 적용할 표준 - 유비쿼터스 언어(라고 에릭 에반스는 부른다)
- 코드에는 이 언어에 속하는 용어를 열심히 사용하자
- 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할 수록 독자가 코드 이해가 쉽다
N4(명확한 이름)
- 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다네이밍으로 함수의 목적 파악이 분명하지 않고 광범위, 모호
역시 FitNess 코드 1
2
3
4
5
6
7
8
9private 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
4private 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
6public 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
- [Clean Code] Ⅰ. 깨끗한 코드
- [Clean Code] Ⅱ.의미 있는 이름
- [Clean Code] Ⅲ. 함수
- [Clean Code] Ⅳ. 주석
- [Clean Code] Ⅴ. 형식 맞추기
- [Clean Code] Ⅵ. 객체와 자료구조
- [Clean Code] Ⅶ. 오류 처리
- [Clean Code] Ⅷ. 경계
- [Clean Code] Ⅸ. 단위 테스트
- [Clean Code] Ⅹ. 클래스
- [Clean Code] Ⅺ. 시스템
- [Clean Code] Ⅻ. 창발성(創發性)
- [Clean Code] XIII. 동시성
- [Clean Code] XIV. 점진적 개선(SUCCESSIVE REFINEMENT)
- [Clean Code] XV. JUnit 들여다보기
- [Clean Code] XVI. SerialDate 리팩터링
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] 다 읽었다~