[CleanCode] -17. 냄새와 휴리스틱

Young Min Sim ·2021년 5월 17일
0

CleanCode

목록 보기
16/16

앞 장에서 배웠던 모든 내용들을 리마인드하는 목차
점진적인 개선, JUnit, SerialDate 에서 리팩토링을 하면서 사용했던 기교와 휴리스틱을 정리


1. 주석

주석은 코드가 변함에 따라 지속적으로 업데이트 되지 않을 확률이 높으므로 신중하게 달아야 한다.
'가능하다면' 주석보다는 코드로 표현하는게 좋다.

주석의 좋은 예

const int EARLIEST_DATE_ORDINAL = 2;      // 1/1/1900
const int LATEST_DATE_ORDINAL = 3344231;  // 12/31/9999

2. 함수

너무 많은 인수

함수에서 인수의 개수는 작으면 작을 수록 좋다.
인수가 많아지면 다음과 같은 방식을 고려해볼 수 있다.

// swift 코드
func makeCircle(x: Double, y: Double, radius: Double)
func makeCircle(center: Point, radius: Double)

출력 인수

출력 인수는 직관을 정면으로 위배한다.
appendFooter(s) 가 s 를 변경하는 함수인 경우 s.appendFooter() 가 역할 측면에서 더 적절
s의 상태를 변경하는 것은 s가 가진 함수에서 하는 것이 더 적절하다.

플래그 인수

플래그 인수는 그 자체로 함수가 여러 기능을 수행한다는 증거가 되므로 피해야 마땅하다.
render(boolean isSuite) 보단 renderForSuite() 와 renderForSingleTest() 가 나음


3. 일반

경계 처리

자신의 직관에만 의존하기 보단
모든 경계조건, 모든 예외를 찾아내어 테스트케이스를 작성하고 테스트하라

중복

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라
추상화 수준을 높였으므로 구현이 빨라지고 오류가 적어진다.

  • 가장 흔한 유형은 똑같은 코드가 여러 차례 나오는 중복
  • 좀 더 미묘한 유형으로는 switch/case, if/else 문으로 똑같은 조건을 거듭 확인하는 중복
    -> 이런 중복은 다형성으로 대체

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

저차원 개념은 파생 클래스에, 고차원 개념은 기초 클래스에 넣어야 한다.
또한 고차원 개념과 저차원 개념을 섞어서는 안 된다.

과도한 정보

인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어서 정보를 제한하고 결합도를 낮춰라.
(즉 최소한의 연관된 메서드, 변수만을 담아 응집도를 높이는 것이 좋다)

부적절한 static 함수

Math.max(double a, double b) 는 좋은 static 메서드다.

하지만 HourlyPayCalculator.calculatePay(employee, overtimeRate); 는 언뜻 보면 static 함수처럼 여겨도 적당해 보이지만,
수당을 계산하는 알고리즘이 여러개라면 overtimeHourlyPayCalculator, StraightHourlyPayCalculator 로 분리하고 싶을지도 모른다. 조금이라도 의심스럽다면 인스턴스 함수로 정의하는게 좋다.

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

Date newDate = date.add(5);

5일을 더하는 함수인지, 5주인지, 5시간인지 명확하지가 않다.
만약 5일을 더해 date 인스턴스를 변경하는 함수라면 addDaysTo 라는 식의 이름이 좋다.

알고리즘을 이해하라

단순히 테스트 케이스가 통과한다는 사실만으로 구현이 끝났다고 선언하기엔 부족하다.
알고리즘이 돌아가는 방식을 확실히 이해 해야한다.
그리고 이를 위해선 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성해야 한다.

if, switch 문보다는 다형성을 사용하라

저자: 나는 'switch문 하나' 규칙을 따른다. 유형 하나에는 switch 문을 한 번만 사용한다. 같은 선택을 수행하는 다른 코드에서는 다형성 객체를 생성하여 switch 문을 대신한다.

첫 구현 시엔 우선 switch 문으로 작성한 후, 해당 타입을 사용하는 switch 문이 많아지고 케이스가 많아진다면 추상 팩토리 패턴을 이용하여 객체를 반환하도록 리팩토링 하는 것도 좋을 것 같습니다.

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

소프트웨어 개발에서 가장 오래된 규칙 중 하나로, 숫자는 명명된 상수 뒤로 숨기라는 의미이다.

조건을 캡슐화하라

if (timer.hasExpired() && !timer.isRecurrent()) 보다
if (shouldBeDeleted(timer)) 가 좋다.

함수는 한 가지만 해야 한다

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

위 코드는 세 가지 임무를 수행하므로 아래와 같이 나눠줄 수 있다.

public void pay() {
  for (Emplyee 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);
}

읽는 사람에게 의도를 더 분명하게 전달할 수 있다.

숨겨진 시간적인 결합

때로는 시간적인 결합이 필요하지만, 이를 숨겨서는 안된다.

public class MoogDiver {
    Gradient gradient;
    List<Spine> splines;
    
    public void drive(String reason) {
        saturateGradient();
        reticulateSplines();
        diveForMoog(reason);
    }
}

위 코드는 세 함수가 실행되는 순서가 중요하다.
하지만 시간적인 결합을 강제하지는 않기 때문에 다음코드가 더 좋다.

public class MoogDiver {
    Gradient gradient;
    List<Spline> splines;
    
    public void dive(String reason) {
        Gradient gradient = saturateGradient();
        List<Spline> splines = reticulateSplines(gradient);
        diveForMoog(splines, reason);
    }
}

위 코드에서 각 함수가 내놓는 결과는 다음 함수에 필요하다. 그러므로 순서를 바꿔 호출할 수가 없다.
함수가 복잡해진다는 불평이 있을 수는 있지만, 시간적 결합성이 드러내어 개발자의 실수를 줄일 수 있다는 점에서 이점이 존재.

일관성을 유지하라

구조에 일관성이 없다면 남들이 맘대로 바꿔도 괜찮다고 생각한다.
시스템 전반에 걸쳐 구조가 일관성이 있다면 남들도 일관성을 따르고 보존한다.

추이적 탐색을 피하라 (디미터법칙)

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다.
예를 들어 a.getB().getC().doSomething()은 바람직하지 않다.
이러한 추이적 탐색은 결합도를 높이므로 중간에 구조를 수정하기 쉽지 않게 되어, 아키텍처가 굳어진다.
다시 말해, 다음과 같은 간단한 코드로 충분해야 한다.

myCollaborator.doSomething();


4. 이름

서술적인 이름을 사용하라

이름은 서술적으로, 신중하게 골라야 한다.
소프트웨어 가독성의 90%는 이름이 결정한다.
대충 정하기에 이름은 너무나도 중요하다.


boolean method(int year) {
    return year % 4 == 0 && (!(year % 100 == 0) || year % 400 == 0);
}

// 좀 더 서술적인 이름
boolean method(int year) {
    bool fourth = year % 4 == 0
    bool hundredth = (year % 100 == 0)
    bool fourHundredth = year % 400 == 0
    return fourth && (!hundredth || fourHundredth);
}

명확한 이름

함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.

private String doRename() throws Exception {
    if (refactorReference)
        renameReferences();
    renamePage();
    
    pathToRename.removeNameFromEnd();
    pathToRename.addNameToEnd(newName);
    return PathParser.render(pathToRename);
}

위 코드에서 이름만 봐서는 함수가 하는 일이 분명하지 않다.
대신 renamePageAndOptionallyAllReferences라는 이름이 더 좋다.

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

이름 길이는 범위 길이에 비례해야 한다.
범위가 5줄 안팎이라면 아주 짧은 i, j 같은 이름을 사용해도 괜찮다.
하지만 범위가 길어지면 긴 이름을 사용하자.


5. 테스트

불충분한 테스트

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

커버리지 도구를 사용하라

커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다.
전혀 실행되지 않는 if 혹은 case 문 블록을 찾아주므로, 테스트가 불충분한 모듈, 클래스, 함수를 찾기 쉬워진다.

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

때로는 요구 사항이 불분명하기에 프로그램 동작 방식을 확신하기 어렵다. 해당 부분은 테스트 케이스를 주석으로 처리하거나 @Ignore을 붙여 표현한다. 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달렸다.

Xcode 에서는 특정 유닛 테스트만 disable 시키거나, 코드로 skip 할 수 있는 메서드가 존재

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

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

실패 패턴을 살펴라

때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.
예를 들어 입력이 5자를 넘기는 테스트 케이스가 연달아 모두 실패한다면? 혹은 인수로 음수를 넘기는 테스트가 모두 실패한다면?
때로는 테스트 보고서에서 빨간색/녹색 패턴만 보고도 아!라는 깨달음을 얻을 수 있다.

테스트는 빨라야 한다

느린 테스트 케이스는 점점 더 실행하지 않게 되고 결국 버그의 조기 발견이 힘들어진다.

0개의 댓글