22 분 소요

부록을 제외한 《클린코드》의 마지막 장이다. 새로운 내용이 아닌, 전체 내용을 정리하는 장이다. 깨끗한 코드를 위한 규칙들을 읽어보면 크게 새로울 것이 없다고 느껴진다. 하지만, 이미 깨끗한 코드를 작성하는 게 쉽지 않다는 사실을 안다. 이미 2번이나 《클린코드》을 정독했으니, 이제는 깨끗한 코드만이 개발 생산성을 높일 수 있다는 사실을 기억하고 꾸준히 노력할 것이다.


📆TIL(Today I Learned) 날짜

2022.05.04

📑오늘 읽은 범위

17.냄새와 휴리스틱

✍🏻책에서 기억하고 싶은 내용

C1 : 부적절한 정보
일반적으로 작성자, 최종 수정일, SPR(Software Problem Report) 번호 등과 같은 메타 정보만 주석으로 넣는다.

C2 : 쓸모 없는 주석
쓸모 없어진 주석은 재빨리 삭제하는 편이 가장 좋다.

C3 : 중복된 주석
코드만으로 충분한데 구구절절 설명하는 주석이 중복된 주석이다.

C4 : 성의 없는 주석
주석을 달 참이라면 시간을 들여 최대한 멋지게 작성한다.

C5 : 주석 처리된 코드
코드를 읽다가 주석으로 처리된 코드가 줄줄이 나오면 신경이 아주 거슬린다. 얼마나 오래된 코드인지, 중요한 코드인지 아닌지, 알 길이 없다. 그럼에도 아무도 삭제하지 않는다.
주석으로 처리된 코드를 발견하면 즉각 지워버려라! 걱정할 필요 없다. 소스코드 관리 시스템이 기억하니까.

E1 : 여러 단계로 빌드해야 한다
한 명령으로 전체를 체크아웃해서 한 명령으로 빌드할 수 있어야 한다..

E2 : 여러 단계로 테스트해야 한다
모든 테스트를 한 번에 실행하는 능력은 아주 근본적이고 아주 중요하다. 따라서 그 방법이 빠르고, 쉽고 명백해야 한다.

F1 : 너무 많은 인수
함수에서 인수 개수는 작을수록 좋다.

G1 : 한 소스 파일에 여러 언어를 사용한다
이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.

G3 : 경계를 올바로 처리하지 않는다
스스로의 직관에 의존하지 마라. 모든 경계 조건을 찾아내고, 모든 경계 조건을 테스트하는 테스트 케이스를 작성하라.

G5 : 중복
코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하라. (…) 어디서든 중복을 발견하면 없애라.

G6 : 추상화 수준이 올바르지 못하다
우수한 소프트웨어 설계자는 개념을 다양한 차원으로 분리해 다른 컨테이너에 넣는다.

G7 : 기초 클래스가 파생 클래스에 의존한다.
개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다. (…) 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.

G8 : 과도한 정보
우수한 소프트웨어 개발자는 클래스나 모듈 인터페이스에 노출할 함수를 제한할 줄 알아야 한다. 클래스가 제공하는 메서드 수는 작을수록 좋다. 함수가 아는 변수 수도 작을수록 좋다. 클래스에 들어있는 인스턴스 변수 수도 작을수록 좋다.

G9 : 죽은 코드
죽은 코드란 실행되지 않는 코드를 가리킨다. 불가능한 조건을 확인하는 if 문과 throw 문이 없는 try 문에서 catch 블록이 좋은 예다.

G10 : 수직 분리
변수와 함수는 사용되는 위치에 가깝게 정의한다.

G11 : 일관성 부족
어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다. 앞서 언급한 ‘최소 놀람의 원칙’에도 부합한다. 표기법은 신중하게 선택하며, 일단 선택한 표기법은 신중하게 따른다.

G13 : 인위적 결합
서로 무관한 개념을 인위적으로 결합하지 않는다. 예를 들어, 일반적인 enum은 특정 클래스에 속할 이유가 없다. enum이 클래스에 속한다면 enum을 사용하는 코드가 특정 클래스를 알아야만 한다. 범용 static 함수도 마찬가지로 특정 클래스에 속할 이유가 없다.

G14 : 기능 욕심
메서드가 다른 객체의 참조자(accessor)와 변경자(mutator)를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.

G15 : 선택자 인수 함수 호출 끝에 달리는 false 인수만큼이나 밉살스런 코드도 없다.
calculateWeeklyPay(false)라는 코드를 발견할 때마다 의미를 떠올리느라 골치를 앓는다.
일반적으로, 인수를 넘겨 동작을 선택하는 대신 새로운 함수를 만드는 편이 좋다.

G18 : 부적절한 static 함수
일반적으로 static 함수보다 인스턴스 함수가 더 좋다. 조금이라도 의심스럽다면 인스턴스 함수로 정의한다. 반드시 static 함수로 정의해야겠다면 재정의할 가능성은 없는지 꼼꼼히 따져본다.

G21 : 알고리즘을 이해하라
구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해하는지 확인하라. (…) 아록리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.

G25 : 매직 숫자는 명명된 상수로 교체하라
‘매직 숫자’라는 용어는 단지 숫자만 의미하지 않는다. 의미가 분명하지 않은 토큰을 모두 가리킨다.

G26 : 정확하라
코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다. 대충 결정해서는 안 된다. 호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다. 조회 결과가 하나뿐이라 짐작한다면 하나인지 확실히 확인한다.

G28 : 조건을 캡슐화하라
bool 논리는 이해하기 어렵다. 조건의 의도를 분명히 밝히는 함수로 표현하라.

if (shouldBeDeleted(timer))

라는 코드는 다음 코드보다 좋다.

if (timer.hasExpired() && !timer.isRecurrendt())

G29 : 부정 조건은 피하라
부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건으로 표현한다.

if (buffer.shouldCompact())

라는 코드는 다음 코드보다 좋다.

if (!buffer.shouldNotCompact())

G30 : 함수는 한 가지만 해야 한다.
함수를 짜다보면 한 함수 안에 여러 단락을 이어, 일련의 작업을 수행하고픈 유혹에 빠진다. 이런 함수는 한 가지만 수행하는 함수가 아니다.

public void pay() {
  for (Employee e : employees) {
    if (e.isPayday()) {
      Money pay = e.calcuatePay();
      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 : 숨겨진 시간적인 결합
떄로는 시간적인 결합이 필요하다. 하지만 시간적인 결합을 숨겨서는 안 된다. 함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다.

G32 : 일관성을 유지하라
코드 구조를 잡을 때는 이유를 고민하라. 긜고 그 이유를 코드 구조로 명백히 표현하라. 구조에 일관성이 없어 보인다면 남들이 맘대로 바꿔도 괜찮다고 생각한다.

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

if(level + 1 < tag.length) {
  parts = new Parse(body, tags, level + 1, offset + endTag);
  body = null;
}

level+1이 두 번 나온다. 이런 경계 조건은 변수로 캡슐화하는 편이 좋다.

int nextLevel = level + 1;
if(nextLevel < tag.length) {
  parts = new Parse(body, tags, nextLevel, offset + endTag);
  body = null;
}

G34 : 함수는 추상화 수준을 한 단계만 내려가야 한다.
함수 내 모든 문장은 추상화 수준이 동일해야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.

G35 : 설정 정보는 최상위 단계에 둬라
추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안 된다. 대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.

J1 : 긴 import 목록을 피하고 와일드카드를 사용하라
패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져오라.

import package.*;

import 목록은 읽기에 부담스럽다. (…) 명시적인 import 문은 강한 의존성을 생성하지만 와일드카드는 그렇지 않다. 명시적으로 클래스를 import하면 그 클래스가 반드시 존재해야 한다. 하지만 와일드카드로 패키지를 지정하면 특정 클래스가 존재할 필요는 없다. import 문은 패키지를 단순히 검색 경로에 추가하므로 진정한 의존성이 생기지 않는다. 그러므로 모듈 간에 결합성이 낮아진다.

J3 : 상수 대 Enum
int는 코드에서 의미를 잃어버리기도 한다. 반면 enum은 그렇지 않다. enum은 이름이 부여된 열거체(enumeration)에 속하기 때문이다. (…) 메서드와 필드도 사용할 수 있다. int보다 훨씬 더 유연하고 서술적인 강력한 도구다.

전문가 정신과 장인 정신은 가치에서 나온다. 그 가치에 기반한 규율과 절제가 필요하다.


❓궁금한 내용, 잘 이해되지 않는 내용

  • import 와일드카드 vs 명시적 import
    여태까지 import 와일드카드보다는 명시적으로 import할 클래스를 적어주는 것이 좋다고 생각했다. 와일드카드로 import하라고 하는 것의 의외라서 관련 내용을 좀 찾아보니 의견이 분분한 것 같다.
    와일드카드를 사용하라고 권하는 이유가 긴 import 목록이 읽기에 부담스럽기 때문이라고 하는데, IDE에서 import영역을 자동으로 가려주기에 크게 신경쓸 필요는 없을 것 같다.

댓글남기기