IT 잡동사니

클린코드 17장 냄새와 휴리스틱 리뷰

케키키케 2022. 3. 10. 04:02

자바

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

패키지에 클래스를 둘 이상 사용한다면 와일드 카드를 사용해 패키지 전체를 가져올 것을 권장한다.

import 문이 길어지면 가독성이 떨어진다.

 import package.*;

 

상수는 상속하지 않는다

TENTHS_PER_WEEK 와 OVERTIME_RATE 상수의 출처는 어디일까?

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(
        hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime) //여기
      );
    }
    ...
  }

부모 클래스인 Employee에 있나?

  public abstract class Employee implements PayrollConstants {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
  }

없다.

Employee는 PayrollConstants를 구현했다. 

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(
        hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
      );
    }
    ...
  }

 

상수 대 Enum

자바 5에서는 enum을 제공한다.

이름이 부여된 열거체 enum을 이용하는 것이 좋다.

public static final int 보다 더 서술적이다.

상수와 함수식 및 정보를 같이 관리하기 때문에 상태와 행위 둘을 모두 같은 곳에서 관리한다는 점에서 장점을 가진다.

  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) // enum에서 rate()함수의 return 값 가져옴
      );
    }
  }

  public enum HourlyPayGrade {
    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();
  }

 

이름

서술적인 이름을 사용하라

이름이 적합한지 계속 확인해야 한다.

잘 지은 이름은 부연 설명이나 주석없이도 코드의 의도를 알기 쉽다.

적절한 이름으로 예상 가능한 코드를 만들도록 하자.

아래 코드는 서술적이지 못한 이름을 사용한 예시와 이를 개선한 예시를 보여준다.

 public int x() {
    int q = 0;
    int z = 0;
    for (int kk = 0; kk < 10; kk++) {
      if (l[z] == 10)
      {
        q += 10 + (l[z + 1] + l[z + 2]);
        z += 1;
      }
      else if (l[z] + l[z + 1] == 10)
      {
        q += 10 + l[z + 2];
        z += 2;
      } else {
        q += l[z] + l[z + 1];
        z +=2;
      }
    }
    return q;
  }
  
  
  
  public int score() {
    int score = 0;
    int frame = 0;
    for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
      if (isStrike(frame)) {
        score += 10 + nextTwoBallsForStrike(frame);
        frame += 1;
      }
      else if (isSpare(frame)) {
        score += 10 + nextBallForSpare(frame);
        frame += 2;
      } else {
        score += twoBallsInFrame(frame);
        frame += 2;
      }
    }
    return score;
  }

 

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

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

참 어렵다. 별 문제 없는 것으로 보인다.

그런데... 요새는 다이얼을 사용하지 않는다. 연결 방법은 다양할 수 있다. 그래서 connect로 변경한다.

  public interface Modem {
    boolean dial(String phoneNumber); // phoneNumber
    boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedPhoneNumber()l
  }
  
  public interface Modem {
    boolean connect(String connectionLocator); // connectionLocator
    boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedLocator();
  }

 

 

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

만약, DECORATOR 패턴을 활용한다면, 클래스 이름에 Decorator라는 단어를 사용한다.

*데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴

package designpattern.decorator;

abstract class ToppingDecorator extends Pizza {
    protected Pizza pizza;

    ToppingDecorator(final Pizza pizza) {
        this.pizza = pizza;
    }

    public abstract String getDescription();
}

패턴은 표준 중 하나이다. 

또 다른 예를 들면, 자바에서 객체를 문자열로 변환하는 함수는 toString이라는 이름을 많이 쓴다. 

이처럼 관례를 따르는 편이 좋다.

 

명확한 이름

doRename()? renamePage()? 두 함수의 차이를 이름만으로는 알기 어렵다. 
메소드명을 renamePageAndOptionallyAllReferences로 변경한다.

이름이 길다는 단점을 서술성이 충분히 메꾼다.

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

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

 

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

이름 길이는 범위 길이에 비례해야 한다. 

범위가 작으면 짧은 이름을 사용해도 괜찮지만, 범위가 길어지면 긴 이름을 사용한다.

범위가 5줄 안팎이라면 i나 j와 같은 변수 이름도 괜찮다.

이름이 짧은 변수나 함수는 범위가 길어지면 의미를 잃는다.

범위가 길수록 정확하고 길게 짓는다.

private void rollMany(int n, int pins)
  {
    for (int i=0; i<n; i++)
      g.roll(pins);
  }

 

 

인코딩을 피하라

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

오늘날 개발 환경에서는 이름 앞에 m_이나 f와 같은 접두어는 불필요하다.

프로젝트 이름이나 하위 시스템 이름에 (시작적 이미지 시스템이라는 뜻으로) vis_ 와 같은 접두어도 불필요하다.

헝가리안 표기법은 피하자. (과거에 많이 본 듯)

*헝가리안 표기법 : 변수나 함수의 이름 앞에 데이터 타입을 명시하는 표기법

 

 

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

함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다.

실제 여러 작업을 수행하는 함수에다가 동사 하나만 달랑 사용하는 것이 좋지 않다.

아래 코드에서 메소드는 단순히 oos만 가져오지 않는다. 기존에 oos가 없으면 생성하고, 이를 반환한다.

따라서 createOrReturnOos 라는 이름이 더 적합하다.

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

 

 

테스트

불충분한 테스트, 사소한 테스트를 건너뛰지 마라!

테스트 케이스는 몇 개나 만들어야 충분할까?

많은 사람들이 "이 정도면 충분하지 않을까"라는 척도를 사용한다. 

하지만 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다.

테스트 케이스가 확인하지 않은 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전하다.

 

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

커버리지 도구는 테스트가 빠뜨리는 공백을 알려준다.

https://soft.plusblog.co.kr/77

 

IntelliJ Code Coverage Test - 인텔리제이 테스트

IntelliJ를 이용해서 자바 애플리케이션을 개발 할 때, Coverage 테스트를 쉽게 할 수 있다. 별도의 플러그인을 설치해야하는 다른 개발 툴과는 다르게 인텔리 제이는 Coverage 테스트를 위한 툴을 내장

soft.plusblog.co.kr

 

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

때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확인하기 어렵다.

불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 @Ignore를 붙여 표현한다.

 

-> 이런 경우라면, 코드를 수정해야 하는 것이 아닌가? 테스트하기 어려운 코드!

 

 

경계 조건을 테스트하라

알고리즘의 메인 조건은 잘 짜놓고, 경계가 되는 곳의 조건에서 실수하는 경우가 흔하다.

 

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

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

 

실패 패턴을 살펴라

테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 꼼꼼히 짠 테스트 케이스는 실패 패턴을 드러낸다.

테스트를 돌렸을 때, 초록불, 빨간불만 보고도 바로 문제를 파악할 수 있다.

ex) 멤버_생성실패_아이디누락() 테스트가 실패한다면? 

 

테스트는 빨라야 한다.

느린 테스트 케이스는 실행하지 않게 된다. 

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

- 스프링을 예로 들면, 테스트에 필요한 @Bean만 로드하도록 한다.

- 덩치가 큰 기반 클래스를 경계하자.

- @Before 와 @After 메서드는 테스트 하나당 한 번씩 실행된다. 무시할 만큼 작은 오버헤드일 수도 있지만, 어떤 경우엔 제법 많은 시간을 낭비할 때도 있고, 그 시간은 계속 누적된다.

- 파일 I/O보다 느린 I/O는 없다. 기능 수행에 꼭 필요한 파일 I/O 도 있지만, 꼭 필요하지는 않거나 오히려 테스트하려는 동작을 방해하는 경우도 많이 있다. 불필요한 파일 I/O는 제거한다.

 

 

 

 

 

 

 

 

 

 

고생하셨습니다!