Chapter 17 - 냄새와 휴리스틱
코드 냄새
주석
- 부적절한 정보
- 다른 시스템(소스 코드 관리 시스템 등)에 저장할 정보는 주석으로 부적절하다.
- 일반적으로 작성자, 최종 수정일, SPR(Software Problem Report)와 같은 메타 정보만 주석으로 넣는다.
- 쓸모 없는 주석
- 오래된 주석, 엉뚱한 주석, 잘못된 주석은 재빨리 삭제해야 한다.
- 중복된 주석
- 코드만으로 충분한데 구구절절 설명하는 주석은 중복이다.
- 성의 없는 주석
- 주석을 작성할 것이라면 시간을 들여 최대한 멋지게 작성한다.
- 주석 처리된 코드
- 주석 처리된 코드는 코드 관리 시스템이 기억하기 때문에 삭제하라
환경
- 여러 단계로 빌드해야 한다.
- 빌드는 간단히 한 단계로 끝나야 한다.
- 한 명령으로 전체를 체크아웃해서 빌드할 수 있어야 한다.
- 여러 단계로 테스트해야 한다.
- 모든 단위 테스트는 한 명령으로 돌려야 한다.
함수
- 너무 많은 인수
- 함수에서 인수의 개수는 작을수록 좋다.
- 넷 이상의 인수는 최대한 피한다.
- 출력 인수
- 함수에서 뭔가의 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경한다.
- 예를 들어,
appendFooter(report)
대신report.appendFooter()
을 사용하자
- 플래그 인수
- boolean 인수는 함수가 여러 기능을 수행한다는 증거이다.
- 예를 들어,
render(true)
대신renderForSuite()
와renderForSigleTest()
로 함수를 나눠라
- 죽은 함수
- 아무도 호출하지 않는 함수는 삭제한다.
- 소스 코드 관리 시스템이 모두 기억하므로 걱정할 필요 없다.
일반
- 한 소스 파일에 여러 언어를 사용한다.
- 이상적으로 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.
- 현실적으로는 불가피하지만 노력을 기울여 한 소스 파일에서 언어 수와 범위를 최대한 줄이자
- 당연한 동작을 구현하지 않는다.
- 함수나 클래스는 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.
- 경계를 올바로 처리하지 않는다.
- 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하라
- 안전 절차 무시
- 컴파일러 경고, IDE 경고 등을 주의깊게 보자
- 실패하는 테스트 케이스를 나중으로 미루지 말자
- 중복
- 똑같은 코드가 여러 차례 나오는 중복은 함수로 교체한다.
- switch/case, if/else 문으로 똑같은 조건을 거듭 확인하는 조건은 다형성(polymorphism)으로 대체하라
- 알고리즘이 유사하나 코드가 서로 다른 경우 Template Method 패턴이나 Strategy 패턴으로 중복을 제거한다.
- 추상화 수준이 올바르지 못한다.
- 세부 구현과 관련한 상수, 변수, 유틸리티 함수는 파생 클래스에서 넣고, 고차원 개념은 기초 클래스에 넣는다.
- 기초 클래스가 파생 클래스에 의존한다.
- 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.
- 과도한 정보
- 잘 정의된 인터페이스는 많은 함수를 제공하지 않고, 결합도가 낮다.
- 클래스가 제공하는 메서드 수는 작을수록 좋고, 인스턴스 분수 수도 작을수록 좋다.
- 함수가 아는 변수 수도 작을수록 좋다.
- 죽은 코드
- 불가능한 조건을 확인하는 if 문과 throw 문이 없는 try 문에서 catch 블록은 죽은 코드이다.
- 수직 분리
- 변수와 함수는 사용되는 위치에 가깝게 정의한다.
- 지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다.
- private 함수는 처음으로 호출한 직후에 정의한다.
- 일관성 부족
- 예를 들어, 한 함수에서
response
라는 변수에HttpServletResponse
인스턴스를 저장했다면, 다른 함수에서도 일관성 있게 동일한 변수 이름을 사용한다.
- 예를 들어, 한 함수에서
- 잡동사니
- 아무도 사용하지 않는 변수와 함수, 정보를 제공하지 못하는 주석은 제거해라
- 인위적 결합
- 서로 무관한 개념을 인위적으로 결합하지 않는다.
- 예를 들어, 일반적인
enum
은 특정 클래스에 속할 이유가 없고, 범용static
함수도 특정 클래스에 속할 이유가 없다.
- 기능 욕심
- 메서드가 다른 객체의 getter, setter를 사용해 그 객체 내용을 조작한다면 클래스 범위를 욕심내는 탓이다.
- 하지만 때로는 어쩔 수 없는 경우도 생긴다.
- 선택자 인수
- boolean, enum, int 인수로 함수 동작을 제어하는 것을 바람직하지 않다.
- 대신 새로운 함수를 만드는 편이 좋다.
- 모호한 의도
- 코드를 짤 때는 의도를 최대한 분명히 밝힌다.
- 잘못 지운 책임
- 코드는 독자가 자연스럽게 기대할 위치에 배치한다.
- 부적절한 static 함수
- 함수를 재정의할 가능성이 전혀 없는 경우, 메서드를 소유하는 객체에서 가져오는 정보가 아닌 경우에 static 함수를 사용한다.
- 일반적으로 인스턴스 함수가 더 좋으므로 조금이라도 의심스럽다면 인스턴스 함수로 정의한다.
- 서술적 변수
- 계산을 몇 단계로 나누고 중간값에 좋은 변수 이름만 붙여도 읽기 쉬운 모듈로 바뀐다.
1 2 3 4 5
// Bad Matcher match = hedaerPattern.matcher(line); if(match.find()) { headers.put(match.group(1).toLowerCase(), match.group(2)); }
1 2 3 4 5 6 7
// Good Matcher match = hedaerPattern.matcher(line); if(match.find()){ String key = match.group(1); String value = match.group(2); headers.put(key.toLowerCase(), value); }
- 이름과 기능이 일치하는 함수
Date newDate = date.add(5)
는 5시간, 5주, 5일 중에 무엇을 더하는 함수인지 알 수 없다.
- 알고리즘을 이해하라
- 구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라
- 논리적 의존성은 물리적으로 드러내라
- 의존하는 모든 정보를 명시적으로 요청하는편이 좋다.
- if/else 혹은 switch/case 문보다 다형성을 사용하라
- 표준 표기법을 따르라
- 팀이 정한 표준은 팀원들 모두가 따라야 한다.
- 매직 숫자는 명명된 상수로 교체하라
- 정확하라
- 코드에서 뭔가를 결정할 때는 정확히 결정한다.
- 예를 들어, null을 반환할 가능성이 있는지, 조회 결과의 개수가 예측값과 동일한지, 동시성으로 인한 문제가 있는지 등을 정확히 파악해야 한다.
- 관례보다 구조를 사용하라
- 설계 결정을 강제할 때는 규칙보다 관례, 특히 구조 자체를 강제하면 더 좋다.
- 예를 들면, 추상 메서드를 사용하여 반드시 모두 구현하도록 강제할 수 있다.
- 조건을 캡슐화하라
- 부울 논리는 조건의 의도를 분명히 밝히는 함수로 표현하라
- 예를 들어,
if(timer.hasExpired()&&!timer.isRecurrent())
보다if(shouldBeDeleted(timer))
가 더 좋다.
- 부정 조건은 피하라
- 부정 조건은 긍정 조건보다 이해하기 어려우므로, 가능하면 긍정 조건을 표현하라
- 예를 들어,
if(!buffer.shouldNotCompat())
보다if(buffer.shouldCompat())
이 더 좋다.
함수는 한 가지만 해야 한다.
1 2 3 4 5 6 7 8 9
// Bad 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 16 17
// Good 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); }
- 숨겨진 시간적인 결합
- 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러내자
1 2 3 4 5 6 7 8 9 10 11 12 13
// Bad // 함수들이 실행되는 순서가 중요하지만 강제하진 않다. // 따라서 사용하는 사람이 잘못된 순서로 호출할 수 있다. public class MoogDirver { Gradient gradient; List<Spline> splines; public void dive(String reason) { saturateGradient(); reticulateSplines(); diveForMoog(reason); } }
1 2 3 4 5 6 7 8 9 10 11 12
// Good // 시간적인 결합을 노출한다. public class MoogDirver { Gradient gradient; List<Spline> splines; public void dive(String reason) { Gradient gradient = saturateGradient(); List<Spline> splines = reticulateSplines(gradient); diveForMoog(splines, reason); } }
- 일관성을 유지하라
- 경계 조건을 캡슐화하라
- 경계 조건은 한 곳에서 별도로 처리한다.
- 함수는 추상화 수준을 한 단계만 내려가야 한다.
- 또한 함수 내 모든 문장은 추상화 수준이 동일해야 한다.
- 설정 정보는 최상위 단계에 둬라
- 추이적 탐색을 피하라
- 디미터의 법칙 (
a.getB().getC().doSomethig()
) - 클래스에서 사용하는 모듈은 해당 클래스에 필요한 서비스를 모두 제공해야 한다.
- 원하는 메서드를 찾느라 객체 그래프를 따라 시스템을 탐색할 필요가 없어야 한다.
- 디미터의 법칙 (
자바
- 긴 import 목록을 피하고 와일드카드를 사용하라
- 상수는 상속하지 않는다.
- 대신 static import를 사용하라
- 상수 대 Enum
- 자바 5부터 enum을 제공하므로 적극 활용하자
이름
- 서술적인 이름을 사용하라
- 이름을 성급하지 정하지 않고, 서술적인 이름을 신중하게 고른다.
- 적절한 추상화 수준에서 이름을 선택하라
- 구현을 드러내는 이름은 피하라
- 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라
- 가능하다면 표준 명명법을 사용하라
- 디자인 패턴, toString 등은 가능하면 관례를 따르는 편이 좋다.
- 명확한 이름
- 함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.
- 긴 범위는 긴 이름을 사용하라
- 이름의 길이는 범위 길이에 비례해야 한다.
- 인코딩을 피하라
- 이름으로 부수 효과(side-effect)를 설명하라
- 함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다.
테스트
- 불충분한 테스트
- 테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다.
- 커버리지 도구를 사용하라
- 사소한 테스트를 건너뛰지 마라
- 무시한 테스트는 모호함을 뜻한다
- 경계 조건을 테스트하라
- 버그 주변은 철저히 테스트하라
- 실패 패턴을 살펴라
- 테스트 커버리지 패턴을 살펴라
- 테스트는 빨라야 한다.
This post is licensed under CC BY 4.0 by the author.