Smell & Heuristics

최완식·2022년 7월 12일
0

Clean Code

목록 보기
15/15
post-thumbnail

15장(JUnit)과 16장(SerialDate Refactoring)은 코드가 많고, 지배적인 내용이 아니라 판단하여 생략하였다. 17장, 냄새와 휴리스틱을 정리해본다.

개인적으로 가장 실용적이며 중요한 절이라는 생각이 든다. 어떻게 변경할지에 대한 생각이 떠오지 않으면 결국 아무일도 할 수 없기 때문이다. 이 장에서는 그 냄새에 대해 정리했다.

주석

부적절한 정보

Git 시스템에 저장할 정보는 주석으로 적절치 못하다. 주석은 기술적인 설명을 부연하는 수단이다.

쓸모 없는 주석

주석은 빨리 낡는다. 쓸모 없어질 주석은 아예 달지 않고, 쓸모 없어진 주석은 빨리 제거한다.

중복된 주석

코드로 충분한데도 구구절절 설명하는 주석은 제거한다. i++ // i증가

성의 없는 주석

주석을 작성할 것이라면 최대한 멋지게 작성한다. 주절대지 않고 문법과 구두점을 바로 사용하며 단어를 신중히 선택한다.

주석 처리된 코드

주석으로 처리된 코드는 흉물 그 자체다. 즉각 지워라. 정말로 필요하다면 이전 버전을 가져오면 된다.

환경

여러 단계로 빌드해야 한다

빌드는 간단히 한 단계로 끝나야 한다.

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

모든 단위 테스트는 한 명령으로 돌려야 한다.

함수

너무 많은 인수

함수에서 인수 개수는 작을수록 좋다. 넷 이상은 피한다.

출력 인수

직관을 정면으로 위배한다. 인수는 입력으로 간주하기 때문이다.

플래그 인수

Boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다. 피해라.

죽은 함수

아무도 호출하지 않는 함수는 삭제한다. 어차피 Git이 기억한다.

일반

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

소스 파일 하나에 언어 하나만 사용하자. 현실적으로는 여러 언어가 불가피 하나, 범위와 언어 수를 줄이도록 노력해야 한다.

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

함수나 클래스는 다른 프로그래머가 당연하게 여길만한 동작과 기능을 제공해야 한다.

// String을 Enum으로 변환
Day day = DayDate.StringToDay(String dayName);
  • "Monday"Day.MONDAY으로 변환하겠지?
  • "Mon"Day.MONDAY으로 변환하겠지?
  • "monday"Day.MONDAY으로 변환하겠지?

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

스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아네고, 이를 테스트하는 케이스를 작성하라.

안전 절차 무시

컴파일러 경고 끄기, 테스트 케이스 제쳐두기와 같은 태도는 자칫하면 끝없는 디버깅을 일으킬 수 있는 것들이다.

중복

가장 중요한 규칙이다. 중복은 추상화할 기회다. 중복을 없애라.

  • 똑같은 코드가 중복으로 나오는 경우
  • if/else로 똑같은 조건을 거듭 확인하는 경우
    • 다형성으로 해결한다.
  • 알고리즘이 유사하나, 코드가 다른 경우
    • Template Method pattern, Strategy pattern

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

추상화로 개념을 분리할 때는 철저해야 한다.

  • 고차원 개념은 Base Class, 저차원 개념은 Derived Class에 넣는다.
  • 세부 구현과 관련된 것들은 기초 클래스에 넣지 않는다.

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

왜 추상화했는가? 결국 고차원 개념과 저차원 개념을 분리하여 독립성을 확보하기 위함이다. 기초 클래스가 파생 클래스에 의존하는 것은 뭔가 문제가 있다.

과도한 정보

잘 정의된 모듈은 인터페이스가 매우 작다. 결합도가 낮다.

  • 자료, 유틸리티, 상수, 변수, 메서드를 숨겨라.
  • 정보를 제한해 결합도를 낮춰라

죽은 코드

실행되지 않는 코드는 죽은 코드다.

  • 불가능한 조건을 확인하는 if문, switch/case문
  • 시스템에서 제거하라.

수직 분리

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

  • 지역변수: 사용하기 직전에 선언한다. 수직적으로 가까운 곳에 배치한다.
  • 비공개 함수: 처음으로 호출한 직후에 정의한다.

일관성 부족

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

잡동사니

비어있는 기본 생성자는 굳이 필요없다. 잡동사니들은 항상 정리한다.

인위적 결합

무관한 개념을 인위적으로 결합하지 않는다.

  • 일반적으로 사용되는 enum은 특정 class에 속할 이유가 없다.
  • 범용 static 함수도 마찬가지다.
  • 이는, 당장 편한 위치에 넣어버린 결과다.

기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야 한다.

선택자 인수

선택자 인수(Bool, enum, Int)는 큰 함수를 여럿 함수로 쪼개지 않으려는 게으름의 소산이다.

  • 함수를 만들어 해결하라.

모호한 의도

코드를 짤 때는 의도를 최대한 분명히 밝힌다.

  • 행 바꾸기
  • 변수 및 함수 이름 정하기

잘못 지운 책임

코드 배치는 최소 놀람의 원칙, 즉 독자가 자연스럽게 기대할 위치에 배치한다.

부적절한 static 함수

인스턴스와 정말 무관한 경우에 static 함수를 사용하라. 인스턴스에서 메서드를 재정의해야 할 가능성이 있다면 인스턴스 함수로 정의한다.

서술적 변수

계산을 여러 단계로 나누어 서술할 수 있는 변수로 작성하라.

  • 많을 수록 좋다.

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

이름만으로 분명하지 않아 구현을 살펴야 한다면, 좋은 이름으로 바꾸거나, 더 이름을 붙이기 쉽도록 정리해야 한다.

Date newDate = date.add(5);
  • 5일을 더하는 건가?
  • 5주? 5시간?
  • 인스턴스를 변경하는 건가?
  • 새로운 인스턴스를 반환하는 건가?
  • addDaysTo, datsLater와 같은 이름으로 의도를 분명하게 해야 한다.

알고리즘을 이해하라

구현이 끝났다고 말하기 전에, 알고리즘을 확실히 이해하는지 확인하라.

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

의존하는 모듈이 상대 모듈에 대해 무언가를 가정하면 안된다. 명시적으로 요청하라.

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

switch문은 하나만 사용한다.

  • 당장 쉬운 선택이라 사용한다.
  • 수정하려면 switch 문을 모두 봐야 한다.

표준 표기법을 따르라

업계 표준과 팀이 정한 표준을 따른다.

  • 업계 표준을 따르는 경우, 추가 설명이 필요없이 코드로 충분해야 한다.
  • 팀이 정한 표준의 경우, 팀원 모두가 따라야 한다.

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

일반적으로 코드에서 숫자를 사용하지 마라. 변수로 선언하여 의미를 부여하고, 실수를 방지해라.

정확하라

코드에서 무언가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외 처리할 방법을 분명히 이해해라.

관례보다 구조를 사용하라

설정을 강제 할 때는 구조 자체로 강제하라. 기초 클래스는 파생 클래스가 추상 메서드를 반드시 구현하도록 강제한다.

조건을 캡슐화하라

Bool 논리는 이해하기 어렵다. 캡슐화하여 맥락을 제공하라.

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

부정 조건은 피하라

부정 조건은 긍정 조건보다 이해하기 어렵다.

if (buffer.shouldCompact())if (!buffer.shouldNotCompact())

함수는 한 가지만 해야 한다

여럿 동작을 수행하는 함수가 있다면 쪼개라.

숨겨진 시간적인 결합

함수가 호출되는 순서를 반환 값을 통해 시간적 결합을 드러내라.

public void dive(String reason) {
    satuateGradient();
    reticulateSplines();
    diveForMoog(reason);
}
public void dive(String reason) {
    Gradient gradient = saturateGradient();
    List<Spline> splines = reticulateSplines(gradient);
    diveForMoog(splines, reason);
}
  • 아래의 경우 순서를 변경하여 호출할 수 없다.

일관성을 유지하라

코드 구조를 잡을 때는 이유를 고민하라.

경계 조건을 캡슐화하라

경계 조건은 빼먹거나 놓치기 십상이다. 한곳에서 별도로 처리하라.

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

함수 내 모든 문장은 추상화 수준이 동일해야 한다.

  • 추상화 수준 분리는 리팩터링을 수행하는 가장 중요한 이유 중 하나다.

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

기본값 상수나, 설정 관련 상수를 저차원 함수에 숨겨서는 안된다.

추이적 탐색을 피하라

추이적 관계: 원소의 원소를 원소로 하는 집합.

  • A가 B를 사용하고, B가 C를 사용하더라도 A가 C를 알필요는 없다.

디미터의 법칙: 직접 사용하는 모듈만 알아야 한다.

  • 내가 사용하는 모듈이 내게 필요한 서비스를 모두 제공해야 한다.

이름

서술적인 이름을 사용하라

소프트웨어 가독성의 90%은 이름이 결정한다.

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

구현을 드러내는 이름은 피하라.

public interface Modem {
    boolean dial(String phoneNumber);
    boolean disconnect(); 
}
  • 전화선에서 사용하는 Modem을 가정하고 있다.
public interface Modem {
    boolean connect(String connectionLocator);
    boolean disconnect(); 
}
  • 케이블을 통한 Modem에도 적용할 수 있는 인터페이스로 변모했다.

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

디자인 패턴은 쉬운 의사소통을 하기 위한 표준이다. 이를 자주 사용하라.

명확한 이름

길더라도 서술적인 이름이라면 그 단점을 모두 보완한다.

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

이름이 짧은 변수나 함수는 범위가 길어지면 의미를 잃는다. 반대로 범위가 매우 짧다면 짧은 변수도 좋을 때가 있다. (for i in ~)

인코딩을 피하라

헝가리안 표기법을 사용하지 마라. (m_, vis_)

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

이름에 부수효과를 숨기지 않는다.

public ObjectOutputStream getOos() throws IOException {
    if (m_oos == null) {
        m_oos = new ObjectOutputStream(m_socket.getOutputStream());
    }
    return m_oos;
}
  • 단순히 oos를 가져오는 코드가 아니다.
  • oos가 없으면 생성한다.
  • createOrReturnOos가 보다 좋다.

테스트

불충분한 테스트

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

커버리지 도구를 사용하라

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

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

사소한 테스트는 짜기 쉽다. 쉬운데 건너뛰지 마라.

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

요구사항이 불분명한 경우 테스트 케이스를 주석처리하게 된다. 어떻게 보면 신호가 되는 부분이다.

경계조건을 테스트하라

알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.

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

버그는 서로 모이는 경향이 있다. 발견한 곳 주변을 철저히 테스트하라.

실패 패턴을 살펴라

때로는 테스트가 실패하는 패턴으로 문제를 진단할 수 있다.

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

통과하는 테스트가 실행하는 코드, 실행하지 않는 코드를 보면 원인을 파악할 수도 있다.

테스트는 빨라야 한다

느린 테스트는 실행하지 않게 된다. 테스트가 빠르게 돌아가도록 하라.

결론

  • 이 모든 목록은, 이전에 말했던 가치 체계를 피력할 뿐이다.
  • 장인 정신과 전문가 정신을 가지고 이행하라.

Reference

profile
Goal, Plan, Execute.

0개의 댓글