기록이 힘이다.

[클린 코드] 냄새와 휴리스틱 본문

IT서적

[클린 코드] 냄새와 휴리스틱

dev22 2023. 6. 16. 06:13
728x90

휴리스틱이란 '시간이나 정보가 불충분하여 합리적인 판단을 할 수 없거나, 굳이 체계적이고 합리적인 판단을 할 필요가 없는 상황에서 신속하게 사용하는 어림짐작의 기술’

주석

 

C1: 부적절한 정보

다른 시스템에 (예를 들어, 소스 코드 관리 시스템, 버그 추적 시스템, 이슈 추적 시스템, 기타 기록 관리 시스템에) 저장할 정보는 주석으로 적절하지 못하다. 일반적으로 작성자, 최종 수정일, SPR(Software Problem Report) 번호 등과 같은 메타 정보만 주석으로 넣는다. 주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다.

 

C2: 쓸모 없는 주석

오래된 주석, 엉뚱한 주석, 잘못된 주석은 더 이상 쓸모가 없다. 주석은 빨리 낡는다.

 

C3: 중복된 주석

주석은 코드만으로 다하지 못하는 설명을 부연한다.

 

C4: 성의 없는 주석

주석을 달 참이라면 시간을 들여 최대한 멋지게 작성한다.

 

C5: 주석 처리된 코드

주석으로 처리된 코드는 흉물 그 자체다.

주석으로 처리된 코드를 발견하면 즉각 지워버려라! 소스 코드 관리 시스템이 기억하니까!

 

환경

 

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

한 명령으로 전체를 체크아웃해서 한 명령을 빌드할 수 있어야 한다.

 

svn get mySystem

cd mySystem

ant all

 

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

모든 단위 테스트는 한 명령으로 돌려야 한다. 모든 테스트를 한 번에 실행하는 능력은 아주 근본적이고 아주 중요하다. 따라서 그 방법이 빠르고, 쉽고, 명백해야 한다.

 

함수

 

F1: 너무 많은 인수

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

 

F2: 출력 인수

출력 인수는 직관을 정면으로 위배한다. 일반적으로 독자는 인수를 (출력이 아니라) 입력으로 간주한다. 함수에서 뭔가의 상태를 변경해야 한다면 ( 출력 인수를 쓰지 말고) 함수가 속한 객체의 상태를 변경한다.

 

F3: 플래그 인수(boolean)

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

 

F4: 죽은 함수

아무도 호출하지 않는 함수는 삭제한다.

 

일반

 

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

좋게 말하자면 혼란스럽고, 나쁘게 말하자면 조잡하다

이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.

 

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

최소 놀람의 원칙에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.

 

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

부지런함을 대신할 지름길은 없다. 모든 경계 조건, 모든 구석진 곳, 모든 기벽, 모든 예외는 우아하고 직관적인 알고리즘을 좌초시킬 암초다. 스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성하라.

 

G4: 안전 절차 무시

안전 절차를 무시하면 위험하다. serialVersionUID를 직접 제어할 필요가 있을지도 모르지만 그래도 직접 제어는 언제나 위험하다. 컴파일 경고 일부를 (혹은 전부를!) 꺼버리면 빌드가 쉬워질지 모르지만 자칫하면 끝없는 디버깅에 시달린다. 실패하는 테스트 케이스를 일단 제껴두고 나중으로 미루는 태도는 신용카드가 공짜돈이라는 생각만큼 위험하다.

 

G5: 중복

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. 이렇듯 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어난다. 다른 프로그래머들이 그만큼 어휘를 사용하기 쉬워진다. 추상화 수준을 높였으므로 구현이 빨라지고 오류가 적어진다.

 

미묘한 유형은 알고리즘이 유사하나 코드가 서로 다른 중복이다. 중복은 중복이므로 TEMPLATE METHOD 패턴이나

STRATEGY 패턴으로 중복을 제거한다.

 

사실 최근 15년 동안 나온 디자인 패턴은 대다수가 중복을 제거하는 잘 알려진 방법에 불과하다. BCNF 역시 데이터베이스 스키마에서 중복을 제거하는 전략이다 OO 역시 모듈을 정리하고 중복을 제거하는 전략이다. 짐작하겠지만, 구조적 프로그래밍도 마찬가지다.

 

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

추상화는 저차원 상세 개념에서 고차원 일반 개념을 분리한다. 때로 우리는 (고차원 개념을 표현하는) 추상 클래스와 (저차원 개념을 표현하는) 파생 클래스를 생성해 추상화를 수행한다. 추상화로 개념을 분리할 때는 철저해야 한다. 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.

우수한 소프트웨어 설계자는 개념을 다양한 차원으로 분리해 다른 컨테이너에 넣는다. 때로는 기초클래스와 파생 클래스로 분리하고, 때로는 소스 파일과 모듈과 컴포넌트로 분리한다. 어느 경우든 철저히 분리해야 한다.

 

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

기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다. 일반적으로 기초 클래스는 파생클래스를 아예 몰라야 마땅하다.

 

물론 예외는 있다. 간혹 파생클래스 개수가 확실히 고정되었다면 기초 클래스에 파생 클래스를 선택하는 코드가 들어간다. FSM 구현에서 많이 본 사례다.

 

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

 

G8: 과도한 정보

잘 정의된 모듈은 인터페이스가 아주 작다. 하지만 작은 인터페이스로도 많은 동작이 가능하다. 부실하게 정의된 모듈은 인터페이스가 구질구질하다. 그래서 간단한 동작 하나에도 온갖 인터페이스가 필요하다. 잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다.

 

우수한 소프트웨어 개발자는 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다. 클래스가 제공하는 메서드 수는 작을수록 좋다. 함수가 아는 변수 수도 작을수록 좋다. 클래스에 들어있는 인스턴스 변수도 작을수록 좋다.

 

G9: 죽은 코드

죽은 코드란 실행되지 않는 코드를 가리킨다. 시스템에서 제거하라.

 

G10: 수직 분리

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

비공개 함수는 처음으로 호출한 직후에 정의한다. 비공개 함수는 전체 클래스 범위에 속하지만 그래도 정의하는 위치와 호출하는 위치를 가깝게 유지한다.

 

G11: 일관성 부족

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

앞서 언급한 ‘최소 놀람의 원칙’에도 부합한다. 표기법은 신중하게 선택하며, 일단 선택한 표기법은 신중하게 따른다.

 

G12: 잡동사니

소스 파일은 언제나 깔끔하게 정리하라! 잡동사니를 없애라!

 

G13: 인위적 결합

일반적으로 인위적인 결합은 직접적인 상호작용이 없는 두 모듈 사이에서 일어난다. 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한 위치에 (물론 잘못된 위치에) 넣어버린 결과다. 게으르고 부주의한 행동이다.

함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치에 고민한다. 그저 당장 편한 곳에 선언하고 내버려두면 안된다.

 

G14: 기능 욕심

마틴 파울러가 말하는 코드 냄새 중 하나다. 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안된다. 메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.

 

G15: 선택자 인수

함수 호출 끝에 달리는 false 인수만큼이나 밉살스런 코드도 없다. 선택자 인수는 목적을 기억하기 어려울 뿐 아니라 각 선택자 인수가 여러 함수를 하나로 조합한다.

public int calculateWeeklyPay(boolean overtime){
	int tenthRate = getTenthRate();
	int tenthsWorked = getTenthsWorked();
	int straightTime = Math.min(400, tenthWorked);
	int overTime = Math.max(0, tenthsWorked - straightTime);
	int straightPay = straightTime * tenthRate;
  double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
	int overtimePay = (int)Math.round(overTime*overtimeRate);
	return straightPay + overtimePay;
}
public int straightPay(){
	return getTenthsWorked() * getTenthRate();
}

public int overTimePay(){
	int overTimeTenths = Math.max(0, getTenthWored() -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 함수

HourlyPayCalculator.calculatePay(employee,overtimeRate);

언뜻 보면 static 함수로 여겨도 적당하다. 특정 객체와 관련이 없으면서 모든 정보를 인수에서 가져오니까. 하지만 함수를 재정의할 가능성이 존재한다. 수당을 계산하는 알고리즘이 여러 개일지도 모른다.

일반적으로 static 함수보다 인스턴스 함수가 더 좋다. 조금이라도 의심스럽다면 인스턴스 함수로 정의한다. 반드시 static 함수로 정의해야겠다면 재정의할 가능성은 없는지 꼼꼼히 따져본다.

 

G19: 서술적 변수

프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 중간 값으로 서술적인 변수 이름을 사용하는 방법이다.

Matcher match = headerPattern.matcher(line);
if(match.find())
{
	String key = match.group(1);
	String value = match.group(2);
	headers.put(key.toLowerCase(), value);
}

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

 

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

Date newDate = date.add(5);

코드만 봐서는 알 수가 없다.

date 인스턴스에 5일을 더해 date 인스턴스를 변경하는 함수라면 addDaysTo 혹은 increaseByDays라는 이름이 좋다. 반면 date 인스턴스는 변경하지 않으면서 5일 뒤인 새 날짜를 반환한다면 daysLater나 daysSince라는 이름이 좋다.

 

G21: 알고리즘을 이해하라

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

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

 

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

한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다. 논리적인 의존성만으로는 부족하다. 의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.

 

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

  1. 대다수 개발자가 switch 문을 사용하는 이유는 그 상황에서 가장 올바른 선택이기보다는 당장 손쉬운 선택이기 때문이다. 그러므로 switch를 선택하기 전에 다형성을 먼저 고려하라는 의미다.
  2. 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다. 그러므로 모든 switch 문을 의심해야 한다.

G24: 표준 표기법을 따르라

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

 

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

일반적으로 코드에서 숫자를 사용하지 말라는 규칙이다. 숫자는 명명된 상수 뒤로 숨기라는 의미다.

assertEquals(7777, Employee.find("John Doe").employeeNumber());
수정
assertEquals(HOURLY_EMPLOYEE_NAME).employeeNumber());

어떤 상수는 이해하기 쉬우므로, 코드 자체가 자명하다면, 상수 뒤로 숨길 필요가 없다.

int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;

 

G26: 정확하라

검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다. 부동소수점으로 통화를 표현하는 행동은 거의 범죄에 가깝다. 마찬가지로 갱신할 가능성이 희박하다고 잠금과 트랜잭션 관리를 건너뛰는 행동은 아무리 잘 봐줘도 게으름이다. List로 선언한 변수를 ArrayList로 선언하는 행동은 지나친 제약이다. 모든 변수를 protected로 선언한 코드는 무절제하다.

 

코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충 결정해서는 안 된다. 호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다. 조회 결과가 하나분이라 짐작한다면 하나인지 확실히 확인한다. 통화를 다뤄야 한다면 정수를 사용하고 반올림을 올바로 처리한다. 병행 특성으로 인해 동시에 갱신할 가능성이 있다면 적절한 잠금 매커니즘을 구현한다.

 

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

 

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

설계 결정을 강제할 때는 규칙보다 관례를 사용한다. 명명 관례도 좋지만 구조 자체로 강제하면 더 좋다.

 

G28: 조건을 캡슐화하라

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

if(shouldBeDeleted(timer)) 

라는 코드는 다음 코드보다 좋다. 

if(timer.hasExpired() && !timer.isRecurrent())

 

G29: 부정 조건을 피하라

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

if(buffer.shouldCompact()) 

라는 코드가 아래 코드보다 좋다. 

if(!buffer.shoudNotCompact())

 

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

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

세 가지 임무를 수행한다. 직원 목록 루프를 돌며, 각 직원의 월급일을 확인하고, 해당 직원에게 월급을 지급한다. 위 함수는 다음 함수 셋으로 나누는 편이 좋다.

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.calculatePay();
	e.deliverPay(pay);
}

위에서 각 함수는 한 가지 임무만 수행한다.

 

G31: 숨겨진 시간적인 결합

때로는 시간적인 결합이 필요하다. 하지만 시간적인 결합을 숨겨서는 안 된다. 함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다.

 

G32: 일관성을 유지하라

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

 

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

경계 조건은 빼먹거나 놓치기 십상이다. 경계 조건은 한 곳에서 별도로 처리한다. 코드 여기저기에서 처리하지 않는다. 다시 말해, 코드 여기저기에 +1이나 -1을 흩어놓지 않는다.

if(level +1 < tag.length){
	parts = new Parse(body, tags, level + 1, offset + endTag);
	body = null;
}

경계 조건은 변수로 캡슐화하는 편이 좋다. 

int nextLevel = level + 1;
if(nextLevel < tags.length){
	parts = new Parse(body, tags, nextLevel, offset + endTag);
  body = null;
}

 

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

함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다. 이 장에서 설명하는 휴리스틱 중 가장 이해하기 어렵고 따르기도 어려운 항목이라 짐작한다. 개념은 아주 간단하지만 인간은 추상화 수준을 뒤섞는 능력이 너무나도 뛰어나다.

 

함수에서 추상화 수준을 분리하면 앞서 드러나지 않았던 새로운 추상화 수준이 드러나는 경우가 빈번하다.

 

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

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

 

G36: 추이적 탐색을 피하라

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다. 좀 더 구체적으로, A가 B를 사용하고 B가 C를 사용한다 하더라도 A가 C를 알아야 할 필요는 없다는 뜻이다.

이를 디미터의 법칙이라 부른다. 무엇이라 부르든 요지는 자신이 직접 사용하는 모듈만 알아야 한다는 뜻이다. 내가 아는 모듈이 연이어 자신이 아는 모듈을 따라가며 시스템 전체를 휘저을 필요가 없다는 의미다.

a.getB().getC().doSomething() 은 바람직하지 않다. 

내가 사용하는 모듈이 내게 필요한 서비스를 모두 제공해야 한다. 원하는 메서드를 찾느라 객체 그래프를 따라 시스템을 탐색할 필요가 없어야 한다. 다시 말해, 다음과 같은 간단한 코드로 충분해야 한다.

myCollaboartor.doSomething();

자바

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

패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져오라.

import package.*;

긴 import 목록은 읽기에 부담스럽다.

 

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

 

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

어떤 프로그래머는 상수를 인터페이스에 넣은 다음 그 인터페이스를 상속해 해당 상수를 사용한다. 언어의 범위 규칙을 속이는 행위다. 대신 static import를 사용하라.

 

J3: 상수 대 Enum

자바 5는 enum을 제공한다. 마음껏 활용하라 ! public static final int라는 옛날 기교를 더 이상 사용할 필요가 없다. int는 코드에서 의미를 잃어버리기도 한다. 반면 enum은 그렇지 않다. enum은 이름이 부여된 열거체에 속하기 때문이다.

enum 문법을 자세히 살펴보기 바란다. 메서드와 필드도 사용할 수 있다. int보다 훨씬 더 유연하고 서술적인 강력한 도구다.

public class hourlyEmployee extends Employee{
	private int tenthWorked;
	HourlyPayGrade grade;

	public Money calculatePay(){
		int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
		int overTime = tenthsWorked = straightTime;
		return new Monkey(
			grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime)
		);
	}
...
}

public enum HourlyPayGrade{
	APPRENTICE {
		public double rate(){
			return 1.0;
		}
	},
	LIEUTENANT_JOURNEYMANP{
		public double rate(){
			RETURN 1.2;
		}
	},
	JOURNEYMAN{
		public double rate(){
			RETURN 1.5;
		}
	},
	MASTER{
		public double rate(){
			RETURN 2.0;
		}
	};

	public abstract double rate();
}

 

이름

 

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

이름은 성급하게 정하지 않는다. 서술적인 이름을 신중하게 고른다. 소프트웨어가 진화하면 의미도 변하므로 선택한 이름이 적합한지 자주 되돌아본다.

단순히 ‘듣기 좋은’ 충고가 아니다. 소프트웨어 가독성의 90%는 이름이 결정한다. 그러므로 시간을 들여 현명한 이름을 선택하고 유효한 상태로 유지한다.

 

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

구현을 드러내는 이름은 피하라. 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라.

 

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

기존 명명법을 사용하는 이름은 이해하기 더 쉽다. 예를 들어, DECORATOR 패턴을 활용한다면 장식하는 클래스 이름에 Decorator라는 단어를 사용해야 한다. 예를들어, AutoHangupModemDecorator는 세션 끝 무렵에 자동으로 연결을 끊는 기능으로 Modem을 장식하는 클래스 이름에 적합하다.

 

N4: 명확한 이름

함수나 변수의 목적을 명확히 밝히는 이름을 선택한다. 다음은 FitNess에서 가져온 코드다

private String doRename() throws Exception
{
	if(refactorReferences)
		renameReferences();
	renamePage();

	pathToRename.removeNameFromEnd();
	pathToRename.addNameToEnd(newName);
	return PathParser.render(pathToRename);
}

이름만 봐서는 함수가 하는 일이 분명하지 않다. 아주 광범위하며 모호하다.

renamePageAndOptionallyAllReferences라는 이름이 더 좋다. 아주 길지만 모듈에서 한 번만 호출된다. 길다는 단점을 서술성이 충분히 메꾼다.

 

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

이름 길이는 범위 길이에 비례해야 한다. 범위가 작으면 아주짦은 이름을 사용해도 괜찮다. 하지만 범위가 길어지면 긴 이름을 사용한다.

 

N6: 인코딩을 피하라

이름에 유형 정보나 범위 정보를 넣어서는 안 된다.

 

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

public ObjectOutputStream getOos() throws IOException{
	if(m_oos == null){
		m_oos = new ObjectOutputStream (m_socket.getOutputStream()); 
	}
	return m_oos;
}

위 함수는 단순히 “oos”만 가져오지 않는다. 기존에 “oos”가 없으면 생성한다. 그러므로 createOrReturnOos라는 이름이 더 좋다.

 

테스트

 

T1: 불충분한 테스트

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

 

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

커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다. 커버리지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기가 쉬워진다. 대다수 IDE는 테스트 커버리지를 시각적으로 표현한다. (예를 들어, 테스트되는 행은 녹색으로, 테스트되지 않는 행은 붉은 색으로 표시한다.) 그러므로 전혀 실행되지 않는 if 혹은 case 문 블록이 금방 드러난다.

 

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

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

 

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

불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 @Ignore를 붙여 표현한다. 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달려있다.

 

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

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

 

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

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

 

T7: 실패 패턴을 살펴라

때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 테스트 케이스를 최대한 꼼꼼히 짜라는 이유도 여기에 있다. 합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.

 

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

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

 

T9: 테스트는 빨라야 한다

느린 테스트 케이스는 실행하지 않게 된다. 일정이 촉박하면 느린 테스트 케이스를 제일 먼저 건너뛴다. 그러므로 테스트 케이스가 빨리 돌아가게 최대한 노력한다.

 

 

결론

사실상 가치 체계는 이 책의 주제이자 목표다. 일군의 규칙만 따른다고 깨끗한 코드가 얻어지지 않는다. 전문가 정신과 장인 정신은 가치에서 나온다. 그 가치에 기반한 규율과 절제가 필요하다.