❓휴리스틱?
휴리스틱은 정보가 불충분하거나 시간이 부족할 때, 경험과 직관에 의존하여 신속하게 결론을 내리거나 문제를 해결하는 방법을 말한다.
이는 '발견법' 또는 '어림짐작'이라고도 불리며, 정확한 답보다는 현실적인 만족을 목표로 하는 경우가 많다.
아래 목록은 다양한 프로그램을 검토하고 리팩터링하면서 만들었다.
프로그램을 수정할 때마다 나는 “왜?”라고 자문한 다음 그 답을 기록했다.
코드를 읽으면서 나쁜 냄새를 정리하다보니 목록이 상당히 길어졌다.
[주석]
C1: 부적절할 정보
다른 시스템(예를 들어 소스 코드 관리 시스템, 버그 추적 시스템, 이슈 시스템 등)에 저장할 정보는 주석으로 적절하지 못하다.
일반적으로 작성자, 최종 수정일 ,SRP(Software Problem Report) 번호 등과 같은 메타 정보만 넣는다.
C2: 쓸모 없는 주석
오래된 주석, 엉뚱한 주석, 잘못된 주석은 더 이상 쓸모가 없다. 주석은 빨리 낡는다.
쓸모 없어진 주석은 재빨리 삭제하는 편이 좋다.
C3: 중복된 주석
코드만으로 충분한데 구구절절 설명하는 주석이 중복된 주석이다.
i++ // i 중가
혹은
/**
* @param sellRequest
* @return
* @throws ManagedComponentException
*/
public SellResponse beginSellItem(SellRequest sellRequest)
throws ManagedComponentException {
...
}
이렇게 함수 서명만 달랑 기술하는 Javadoc도 마찬가지다.
C4: 성의 없는 주석
작성할 가치가 있는 주석은 잘 작성할 가치도 있다.
주절대지 않는다. 그리고 당연한 소리를 반복하지 않는다. 간결하고 명료하게 작성하자.
C5: 주석 처리된 코드
코드를 읽다가 주석으로 처리된 코드를 보면 신경이 아주 거슬린다.
주석으로 처리된 코드를 발견하면 즉시 지워라!
[환경]
E1: 여러 단계로 빌드해야 한다.
빌드는 간단히 한 단계로 끝나야 한다.
한 명령으로 전체를 체크아웃해서 한 명령으로 빌드할 수 있어야 한다.
svn get mySystem
cd mySystem
ant all
E2: 여러 단계로 테스트해야 한다.
모든 단위 테스트는 한 명령으로 돌려야 한다. IDE에서 버튼 하나로 모든 테스트를 돌리는게 가장 이상적이다.
아무리 열악한 환경이라도 쉘에서 명령 하나로 가능해야 한다.
[함수]
F1: 너무 많은 인수
함수에서 인수 개수는 작을수록 좋다. 아예 없는 것이 가장 좋다.
넷 이상은 그 가치가 아주 의심스러우므로 최대한 피하자.
F2: 출력 인수
출력 인수는 직관을 정면으로 위배한다. 일반적으로 독자는 인수를 입력으로 간주한다.
만약 함수에서 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경하자.
F3: 플래그 인수
boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다!
플래그 인수는 혼란을 초래하므로 피해야 마땅하다.
F4: 죽은 함수
아무도 호출하지 않는 함수는 삭제한다. 죽은 코드는 낭비다.
[일반]
G1: 한 소스 파일에 여러 언어를 사용한다.
오늘날 프로그래밍 환경은 한 소스 파일 내에서 다양한 언어를 지원한다.
이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.
G2: 당연한 동작을 구현하지 않는다.
최소 놀람의 원칙에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.
당연한 동작을 구현하지 않으면 코드를 읽거나 사용하는 사람이 더 이상 함수 이름만으로 함수 기능을 직관적으로 예상하기 힘들다.
G3: 경계를 올바로 처리하지 않는다.
코드는 올바로 동작해야 한다.
스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 케이스를 작성하라.
G4: 안전 절차 무시
체르노빌 원전 사고는 책임자가 안전 절차를 차례로 무시하는 바람에 일어났다.
안전 절차를 무시하면 위험하다. serialVersionUID를 직접 제어할 필요가 있을지 모르지만, 직접 제어는 위험하다.
G5: 중복
이 책에 나오는 가장 중요한 규칙 중 하나이므로 심각하게 숙고하기 바란다.
데이비드 토머스와 앤디 헌트는 이를 DRY(Don’t Repeat Yourself) 원칙이라 부른다.
코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라.
중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. 이렇듯 추상화로 중복을 정리하면 설계 언어의 어휘가 늘어난다.
좀 더 미묘한 유형의 중복은 여러 모듈에서 swtich/case 나 if/else 문으로 똑같은 조건을 확인하는 중복이다.
이런 중복은 다형성으로 대체해야 한다.
더더욱 미묘한 유형은 알고리즘이 유사하거나 코드가 서로 다른 중복이다.
중복은 중복이므로 TEMPLATE METHOD 패턴이나 STRATEGY 패턴으로 중복을 제거하자.
마지막으로 한번 더 전달한다. 어디서든 중복을 발견하면 없애라.
G6: 추상화 수준이 올바르지 못하다.
추상화는 저차원 상세 개념에서 고차원 일반 개념으로 분리한다.
모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.
예를 들어 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 기초 클래스에 넣으면 안된다.
잘못된 추상화 수준은 거짓말이나 꼼수로 해결하지 못한다.
추상화는 소프트웨어 개발자에게 가장 어려운 작업 중 하나다.
G7: 기초 클래스가 파생 클래스에 의존한다.
개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다.
그러므로 기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다.
일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.
기초 클래스와 파생 클래스를 다른 JAR 파일로 배포하면, 그리고 기초 JAR vkdlfdl vktod JAR 파일을 전혀 모른다면, 독립적인 개별 컴포넌트 단위로 시스템을 배치할 수 있다.
G8: 과도한 정보
잘 정의된 모듈은 인터페이스가 아주 작다. 하지만 아주 작은 인터페이스로도 많은 동작이 가능하다.
잘 정의된 인터페이스는 많은 함수를 제공하지 않는다. 그래서 결합도가 낮다.
부실한 인터페이스는 반드시 호출해야 하는 온갖 함수를 제공해서 결합도가 높다.
우수한 소프트웨어 개발자는 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다.
클래스가 제공하는 메서드 수는 작을수록 좋다.
자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라.
메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라.
하위 클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 마라.
인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라.
G9: 죽은 코드
죽은 코드란 실행되지 않는 코드를 가리킨다.
시간이 지날수록 악취를 풍기기 때문에 시스템에서 꼭 제거하라.
G10: 수직 분리
변수와 함수는 사용되는 위치에 가깝게 정의한다.
지역 변수는 처음으로 사용하기 직전에 선언하며, 수직으로 가까운 곳에 위치해야 한다.
비공개 함수는 처음으로 호출한 직후에 정의한다.
G11: 일관성 부족
어떤 개념을 특정 방식으로 구현했다면 유수한 개념도 같은 방식으로 구현해야 한다.
착실하게 적용한다면 이처럼 간단한 일관성만으로도 코드를 읽고 수정하기가 대단히 쉬워진다.
G12: 잡동사니
비어 있는 기본 생성자가 왜 필요한가? 쓸데없이 코드만 복잡하게 만든다.
소스 파일은 언제나 깔끔하게 정리하라!
G13: 인위적 결합
서로 무관한 개념을 인위적으로 결합하지 않는다. 예를 들어 일반적은 enum은 특정 클래스에 속할 필요가 없다.
범용 static 함수도 마찬가지로 특정 클래스에 속할 필요가 없다.
함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치를 고민한다.
G14: 기능 욕심
마틴 파울러가 말하는 코드 냄새 중 하나다. 클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야 한다.
만약 메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작한다면 범위를 벗어난 욕심 탓이다.
다음 코드를 살펴보자.
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
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 객체에서 가져온다.
즉, calculateWeeklyPay 메서드는 HourlyEmployee 클래스의 범위를 욕심낸다.
기능 욕심은 한 클래스의 속사정을 다른 클래스에 노출하므로, 별다른 문제가 없다면 제거하는 편이 좋다.
G15: 선택자 인수
함수 호출 끝에 달리는 false 인수만큼이나 밉살스러운 코드도 없다.
다음 코드를 살펴보자.
public int calculateWeeklyPay(boolean overtime) {
int tenthRate = getTenthRate();
int tenthsWorked = getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
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;
}
초과 근무 수당을 1.5배 지급하면 true이고, 아니면 false이다.
다음과 같이 변경하는 것이 더 바람직하다.
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);
}
물론 부울 인수만이 문제라는 말이 아니다.
enum, int 등 함수 동작을 제어하려는 인수는 하나 같이 바람직하지 않다.
일반적으로, 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 더 좋다.
G16: 모호한 의도
코드를 짤 때는 의도를 최대한 분명히 밝힌다.
행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 번호 등은 모두 저자의 의도를 흐린다.
예를 들어 위 예시였던 overTimePay 함수를 다음과 같이 짤 수도 있다.
public int m_otCalc() {
return iThsWkd * iThsRte +
(int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400));
}
독자에게 의도를 분명히 표현하도록 시간을 투자할 가치가 있다.
G17: 잘못 지운 책임
소포트웨어 개발자가 내리는 가장 중요한 결정 중 하나가 코드를 배치하는 위치다.
예를 들어 PI 상수는 어디에 들어갈까? Math 클래스에? 아니면 Trigonometry 클래스에? Circle 클래스에?
여기서도 최소 놀람의 원칙을 적용한다. 코드는 독자가 자연스럽게 기대할 위치에 배치한다.
PI 상수는 삼각함수를 선언한 클래스에 넣어야 맞다.
OVERTIME_RATE 상수는 HourlyPayCalculator 클래스에 선언해야 맞다.
G18: 부적절한 static 함수
Math.max(double a, double b)는 좋은 static 메서드다. 특정 인스턴스와 관련된 기능이 아니다.
그런데 간혹 우리는 static으로 정의하면 안 되는 함수를 static으로 정의한다.
다음 예를 보자.
HourlyPayCalculator.calculatePay(employee, overtimeRate);
언뜻 보면 static 함수로 여겨도 적당하다. 하지만 함수를 재정의할 가능성이 존재한다.
수당을 계산하는 알고리즘이 여러 개일지도 모른다.
예를 들어, OvertimeHourlyPayCalculator와 StraightTimeHourlyPayCalculator를 분리하고 싶을지도 모른다. 그러므로 위 함수는 static 함수로 정의하면 안된다. Employee 클래스에 인스턴스 함수여야 한다.
일반적으로 static 함수 보다는 인스턴스 함수가 더 좋다.
조금이라도 의심된다면 인스턴스 함수로 정의하자. static 함수로 정의해야겠다면 재정의할 가능성이 없는지 따져보자.
G19: 서술적 변수
프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고, 중간 값으로 서술적인 변수 이름을 사용하는 방법이다.
FitNesse에서 가져온 다음 예를 살펴보자.
Macher match = headerPattern.matcher(line);
if(match.find()) {
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}
서술적인 변수 이름을 사용한 탓에 첫 번째 일치 그룹이 key이고, 두번째 그룹이 value라는 사실이 명확히 드러난다.
서술적인 변수 이름은 많이 써도 괜찮다. 일반적으로 많을수록 더 좋다.
G20: 이름과 기능이 일치하는 함수
다음 코드를 살펴보자.
Date newDate = date.add(5);
5일을 더하는 함수인가? 아니면 5주? 5시간? date 인스턴스를 변경하는 함수인가?
만약 date 인스턴스에 5일을 더해 date 인스턴스를 변경하는 함수라면 addDaysTo 혹은 increaseByDays 라는 이름이 더 좋다.
이름만으로 분명하지 않기에 구현을 살피거나 문서를 뒤적여야 한다면 더 좋은 이름으로 바꾸거나 더 좋은 이름을 붙이기 쉽도록 기능을 정리하자.
G21: 알고리즘을 이해하라
대다수 괴상한 코드는 사람들이 알고리즘을 충분히 이해하지 않은 채 코드를 구현한다.
구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라.
알고리즘이 올바르다고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.
G22: 논리적 의존성은 물리적으로 드러내라.
한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다. 논리적인 의존성만으로는 부족하다.
의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.
G23: If/Else 혹은 Swtich/Case 문보다 다형성을 사용하라
나는 ‘swtich 문 하나’ 규칙을 따른다.
즉, 선택 유형 하나에는 swtich 문을 한 번만 사용한다.
같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성해 swtich 문을 대신한다.
G24: 표준 표기법을 따르라
팀은 업계 표준에 기반한 구현 표준을 따라야 한다.
팀이 정한 표준은 팀원들 모두가 따라야 한다.
실제 괄호를 넣는 위치는 중요하지 않다. 모두가 동의한 위치에 넣는다는 사실이 중요하다.
G25: 매직 숫자는 명명된 상수로 교체하라
아마 소프트웨어 개발에서 가장 오래된 규칙 중 하나라 여겨진다.
예를 들어 86,400이라는 숫자는 SECONDS_PER_DAY라는 상수 뒤로 숨긴다.
쪽 당 55줄을 인쇄한다면 숫자 55는 LINES_PER_PAGE 상수 뒤로 숨긴다.
G26: 정확하라
검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다.
코드에서 뭔가를 결정할 때는 정확히 결정해야 한다.
결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충 결정해서는 안 된다.
코드에서 모호성과 부정확은 의견차나 게으름의 결과다.
G27: 관례보다 구조를 사용하라
설계 결정을 강제할 때는 규칙보다 관례를 사용한다.
명명 관례도 좋지만, 구조 자체로 강제하면 더 좋다.
예를 들어 enum 번수가 멋진 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다.
G28: 조건을 캡슐화 하라
부울 논리는 (if나 while 문에다 넣어 생각하지 않아도) 이해하기 어렵다.
조건의 의도를 분명히 밝히는 함수로 표현하라.
예를 들어
if (shouldBeDeleted(timer))
라는 코드는 다음 코드보다 좋다.
if (timer.hasExpired() && !timer.isRecurrent())
G29: 부정 조건은 피하라
부정 조건은 긍정 조건보다 이해하기 어렵다. 가능한 긍정 조건으로 표현한다.
예를 들어
if (buffer.shouldCompact())
라는 코드가 다음 코드보다 좋다.
if(!buffer.shouldNotCompact())
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: 숨겨진 시간적인 결합
때로는 시간적인 결합이 필요하다. 하지만 시간적인 결합을 숨겨서는 안된다.
함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다.
다음 예를 살펴보자.
public class MoogDriver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
satureateGradient();
reticulateSplines();
diveForMoong(reason);
}
...
}
위 코드는 세 함수가 실행되는 순서가 중요하다.
불행히도 위 코드는 이런 시간적인 결합을 강제하지 않는다.
순서대로 실행하지 않을 경우 UnsaturatedGradientException 오류가 발생할 수도 있다.
그래서 다음 코드가 더 좋다.
public class MoogDriver {
Gradient gradient;
List<Spline> splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List<Spline> splines = reticulateSplines(gradient);
diveForMoong(splines, reason);
}
...
}
위 코드는 일종의 연결 소자를 생성해 시간적인 결합을 노출한다.
그러므로 순서를 바꿔 호출할 수 없다.
G32: 일관성을 유지하라
코드 구조를 잡을 때는 이유를 고민하라. 그리고 그 이유를 코드 구조로 명백히 표현하라.
다음 FitNess 코드를 살펴보자.
public class AliasLinkWidget extends ParentWidget {
public static class VariableExpandingWidgetRoot {
...
}
}
여기서 문제는 VariableExpandingWidgetRoot 클래스가 AliasLinkWidget 클래스 범위에 속할 필요가 없다는 점이다.
다른 클래스의 유틸리티가 아닌 public 클래스는 자신이 아닌 클래스 범위 안에서 선언하면 안 된다.
G33: 경계 조건을 캡슐화하라
경계 조건은 빼먹거나 놓치기 십상이다. 경계 조건은 한 곳에서 별도로 처리한다.
다음 예제를 살펴보자.
if (level + 1 < tags.length) {
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
}
level + 1 이 두 번 나온다. 이런 경계 조건은 캡슐화하는 편이 좋다(꼭 중복이 아니더라도).
int nextLevel = level + 1;
if (nextLevel < tags.length) {
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}
G34: 함수는 추상화 수준을 한 단계만 내려가야 한다.
함수 내 모든 문장은 추상화 수준이 동일해야 한다.
그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.
다음 FitNess 예제를 살펴보자.
public String render() throws Exception {
StringBuffer html = new StringBuilder("<hr");
if(size > 0) {
html.append(" size=\\"").append(size + 1).append("\\"");
}
html.append(">");
return html.toString()
}
위 함수는 추상화 수준이 최소한 두 개가 섞여 있다.
- 수평선에 크기가 있다는 개념
- HR 태그 자체의 문법
그래서 다음과 같이 추상화 수준을 고려해 함수를 나누는 것이 좋다.
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);
}
위 코드는 뒤섞인 추상화를 멋지게 분리한다.
redner 함수는 HR 태그만 생성한다.
hrSize 함수는 높이에 따른 태그 높이를 계산한다.
따라서 위 경우는
- HR 태그 생성
- size 변수의 해석과 형식 지정
의 혼재라고 볼 수 있다.
G35: 설정 정보는 최상위 단계에 둬라.
추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안 된다.
고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.
다음은 FitNess에서 가져온 코드다.
public static void main(String[] args) throws Exception {
Arguments arguments = parseCommandLine(args);
...
}
public class Arguments {
public static final String DEFAULT_PATH = ".";
public static final String DEFAULT_ROOT = "FitNessRoot";
public static final int DEFAULT_PORT = 80;
public static final int DEFAULT_VERSION_DAYS = 14;
...
}
위의 코드처럼 설정관련상수는 최상위 단계에 둔다. 그래야 변경하기 쉽다.
G36: 추이적 탐색을 피하라
일반적으로 한 모듈은 주변 모듈을 모를수록 좋다.
구체적으로 A가 B를 사용하고, B가 C를 사용한다 해도 A가 C를 알아야 할 필요는 없다.
이를 디미터의 법칙이라 부른다. 실용주의 프로그래머들은 부끄럼 타는 코드 작성이라고도 부른다.
무엇이라 부르든 요지는 자신이 직접 사용하는 모듈만 알야한다는 뜻이다.
다시 말해, 다음과 같은 간단한 코드로 충분해야 한다.
myCollaborator.doSomething();
[자바]
J1: 긴 import 목록을 피하고 와일드 카드를 사용하라.
패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져오라.
import package.*;
J2: 상수는 상속하지 않는다.
어떤 프로그래머는 상수를 인터페이스에 넣은 다음 그 인터페이스를 상속해 해당 상수를 사용한다.
다음 코드를 살펴보자.
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
houlryRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}
TENTHS_PER_WEEK과 OVERTIME_RATE라는 상수는 어디에서 온걸까?
Employee 클래스를 살펴보자.
public abstract class Employee implements PayrollConstants {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
여기엔 없다. 그럼 PayrollConstants 를 살펴보자.
public interface PayrollConstants {
public static final int TENTHS_PER_WEEK = 400;
public static final double OVERTIME_RATE = 1.5;
}
참으로 끔찍한 관행이다! 상수를 상속 계층 맨 위에 숨겨놨다. 상속을 이렇게 사용하면 안 된다!
대신 static import 를 사용하라.
import static PayrollConstants.*;
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
houlryRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}
J3: 상수 대 Enum
자바 5는 enum을 제공한다. 마음껏 활용하라!
public static final int라는 옛날 기교를 부릴 필요가 없다.
다음 코드는 enum을 활용한 좋은 예다.
public class HourlyEmployee extends Employee {
private int tenthsWorked;
HourlyPayGrade grade;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
...
}
public enum HoulryPayGrade {
APPRENTICE {
public double rate() {
return 1.0;
}
},
LIEUTENANT_JOURNEYMAN {
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: 적절한 추상화 수준에서 이름을 선택하라
구현을 드러내는 이름을 피하라. 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라.
쉽지 않은 작업이다.
다음 Modem 인터페이스를 살펴보자.
public interface Modem {
boolean dial(String phoneNumber);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedPhoneNumber();
}
얼핏 봐서는 문제가 없다. 하지만 전화선이 연결되지 않는 일부 모뎀을 사용하는 상황을 생각해보자.
따라서 더 좋은 ‘이름 선택’전략은 다음과 같다.
public interface Modem {
boolean connect(String connectionLocator);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedLocator();
}
위 코드는 연결 대상의 이름을 더 이상 전화번호로 제한하지 않는다.
전화번호는 물론, 다른 연결 방식도 제공한다.
N3: 가능하다면 표준 명명법을 사용하라
기존 명명법을 사용하는 이름은 이해하기 더 쉽다.
예를 들어 DECORATOR 패턴을 활용한다면 클래스 이름은 Decorator라는 단어를 사용해야 한다.
흔히 팀마다 특정 프로젝트에 적용할 표준을 나름대로 고안한다.
이를 에릭 에반스는 프로젝트의 유비쿼터스 언어라 부른다.
간단히 말해, 프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.
N4: 명확한 이름
함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.
다음은 FitNess에서 가져온 코드다.
private String doRename() throws Exception {
if (refactorReferences) {
renameReferences();
}
renamePage;
pathToRename.removeNameFromEnd();
pathToRename.addNameToEnd(newName);
return PathParser.render(pathToRename);
}
이름만 봐서는 함수가 하는 일이 분명하지 않다.
renamePageAndOptinallyAllReferences라는 이름이 더 좋다.
길다는 단점은 서술성이 충분이 메꾼다.
N5: 긴 범위는 긴 이름을 사용하라
이름 길이는 범위 길이에 비례해야 한다.
만약 범위가 5줄 안팎이라면 i나 j와 같은 변수 이름도 괜찮다.
다음은 볼링 게임에서 가져온 코드다.
private void rollMany(int n, int pins) {
for (int i = 0; i < n; i++) {
g.roll(pins);
}
}
깔끔한 코드다.
만약 이름 범위가 길어진다면 이름을 정확하고 길게 짓자.
N6: 인코딩을 피하라.
이름에 유형 정보나 범위 정보를 넣어서는 안된다.
오늘날 개발 환경에서는 이름 앞에 m_ 이나 f와 같은 접두어가 불필요하다.
N7: 이름으로 부수 효과를 설명하라.
함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다.
이름에 부수 효과를 숨기지 않는다.
예를 들어 다음은 TestNG에서 가져온 코드다.
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: 테스트는 빨라야 한다.
느린 테스트 케이스는 실행하지 않게 된다.
그러므로 테스트 케이스가 빨리 돌아가게 최대한 노력한다.
'Book Notes' 카테고리의 다른 글
| [개발서적] Clean Architecture 2. 두 가지 가치에 대한 이야기 (0) | 2025.11.01 |
|---|---|
| [개발서적] Clean Architecture 1. 설계와 아키텍처 (0) | 2025.11.01 |
| [개발서적] Clean Code 15. Junit 들여다보기 (0) | 2025.10.28 |
| [개발서적] Clean Code 13. 동시성 (0) | 2025.10.28 |
| [개발서적] Clean Code 12. 창발성 (0) | 2025.10.28 |