[Clean Code] Ⅳ. 주석

Clean Code 3판을 읽고 정리한 글입니다

Ⅳ. 주석

나쁜 코드에 주석을 달지 말아라. 새로 짜라.

브라이언 W. 커니핸 P.J 플라우거

주석

  • 잘달린 주석 : 그 어떤 정보보다 유용
  • 경솔하고 근거 없는 주석 : 코드 이해 어렵게 만듬
  • 오래되고 조잡한 주석 : 거짓과 잘못된 정보를 퍼뜨려 악영향

주석은 필요악

  • 주석은 순수하게 선하지 않다
  • 프로그래밍 언어 자체가 표현력이 풍부하다면
  • 아니면 개발자(우리자신)가 프로그래밍 언어를 사용해 의도를 표현할 능력이 있다면
  • 주석은 전혀 필요하지 않을 것이다
  • 주석은 실패를 의미하며 줄이려 노력해야 한다
    • 주석이 없이는 자신을 표현을 할 수 없기에 사용
    • 주석의 내용을 코드로 의도를 표현하는 것이 가장 좋다
    • 코드는 변화하고 진화하며 일부가 옮겨지고 조각이 나뉘거나 병합된다
    • 주석은 코드를 따라가지 못한다 : 주석을 유지보수 하는 것은 불가능
    • 코드만이 정확한 정보를 제공한다
    • 부정확한 주석은 없는 주석보다 나쁘다

주석은 나쁜 코드를 보완하지 않는다

주석이 필요한 이유? 나쁜 코드

  • 모듈을 작성하니 엉망이고 알어먹기 어려움
  • 그래서 주석을 달아야겠다? → No! 코드를 정리해야 한다
  • 표현력이 풍부, 깔끔하고 주석이 없는 코드 > 복잡하고 어수선하나 주석이 아주 많이 달려있는 코드
  • 어지름을 주석으로 설명하는 노력으로 그 어지름을 치우는 데에 시간과 노력을 투자하라

코드로 의도를 표현하라

1
2
3
4
5
6
7
8
9
// 어떤 쪽이 좋을까
//////////////////

// 1.
// 지구언에게 복지 혜택을 받을 자격이 있는지 검사한다.
if((employee.flags & HOURLY_FLAG) && (employee.age > 65))

// 2.
if (employee.isEligibleForFullBenefits())
  • 몇 초만 더 생각하면 코드로 대다수 의도를 표현 가능
  • 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분

좋은 주석

글자값을 한다고 생각되는 주석

법적인 주석

  • 회사가 정립한 구현 표준에 맞춰 법적인 이유로 넣는 주석
  • ex)각 소스 파일 첫머리에 주석으로 들어가는 저작권/소유권 정보 : 필요하고도 타당
  • 가능하다면, 표준 라이선스나 외부 파일을 참조
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // Copyright (C) 2003, 2004, 2005 by Object Montor, Inc. All right reserved.
    // GNU General Public License
    ```
    ### 정보를 제공하는 주석
    - 예1)기본정보를 주석으로 제공
    - 유용함
    - 가능하면 함수 이름에 정보를 담는 편이 더 좋음 - 네이밍의 중요성
    - 예2)정규표현식의 결과물 형태를 주석으로
    - 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 주석이 필요가 없음
    ```java
    // 1.
    // 테스트 중인 Responder 인스턴스를 반환
    protected abstract Responder responderInstance();

    // 위의 경우 함수이름에 정보를 담아 다음처럼 작성하면 주석을 없앨 수 있다
    protected abstract Responder responderBeingTested();

    // 2.
    // 좋은 주석
    // kk:mm:ss EEE, MMM dd, yyyy 형식이다.
    Pattern timeMatcher = Pattern.compile("\\d*:\\d*\\d* \\w*, \\w*, \\d*, \\d*");
    ```

    ### 의도를 설명하는 주석
    ```java
    // 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
    for (int i = 0; i > 2500; i++) {
    WidgetBuilderThread widgetBuilderThread =
    new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
    Thread thread = new Thread(widgetBuilderThread);
    thread.start();
    }
    때로는 주석으로 구현 이해를 도와주는 걸 넘어서 결졍에 깔린 의도를 설명하기도 한다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public int compareTo(Object o) {
    if (o instanceof WikiPagePath) {
    WikiPagePath p = (WikiPagePath) o;
    String compressName = StringUtil.join(names, "");
    String compressedArgumentName = StringUtil.join(p.names, "");
    return compressedName.compareTo(compressedArgumentName);
    }
    return 1; // 오른쪽 유형이므로 정렬 순위가 더 높다.
    }

의미를 명료하게 밝히는 주석

1
2
3
4
5
6
7
8
9
public void testCompareTo() throws Exception {
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
WikiPagePath b = PathParser.parse("PageB");

assertTrue(a.compareTo(a) == 0); // a == a
asserTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab
}
  • 모호한 인수나 반환값은 그 자체를 명확하게 만드는게 더 좋다 - 이해가 쉬어지므로
  • 인수/반환값이 표준라이브러리 이거나 변경하지 못하는 코드? 의미를 명료하게 밝히는 주석이 유용
  • 그릇된 주석의 위험, 올바른지 검증이 안되는 위험 - 주석의 위험
  • 더 나은 방법이 없는지 고민할 것

결과를 경고하는 주석

  • 다른 프로그래머에게 결과를 경고할 목적
    1
    2
    3
    // 여유 시간이 충분하지 않다면 실행하지 마십시오.
    public void _testWithReallyBigFile() {
    }
  • ex)시간이 굉장히 오래걸리는 테스트 케이스
  • JUnit에선 어노테이션으로 간단하게 @Ignore("실행이 너무 오래걸림")

TODO 주석

  • //TODO 주석
    1
    2
    3
    4
    5
    // TODO-MdM 현재 필요하지 않다.
    // 체크아웃 모델을 도입하면 함수가 필요 없다.
    protected VersionInfo makeVersion() throws Exception {
    return null;
    }
  • 앞으로 할일, 현재 이유, 미래 모습등
  • 필요하지만 당장 구현하기 어려운 업무를 기술
  • ex)더 좋은 이름을 떠올려달라, 문제를 봐달라, 더이상 필요없는 기능을 삭제해라, 이벤트에 맞춰 코드를 고쳐라
  • 요새 IDE는 TODO주석을 찾아줌 : 주석을 잊을 위험은 없음, 그래도 한번씩 점검해서 없어도 괜찮은 주석은 삭제
  • FIXME : 문제가 있지만 당장 수정할 필요는 없을 때(가능하면 빨리 수정하는 것이 좋음)

중요성 강조 주석

공개 API에서의 Javadocs

설명이 잘된 공개 API는 참으로 유용하고 만족스럽다

  • ex) 표준 자바 라이브러리에서 사용한 Javadocs가 좋은 예
  • 공개 API를 구현한다면 반드시 훌륭한 Javadocs를 작성하라
  • 어느 주석과 마찬가지로 Javadocs도 잘못될 가능성이 존재

정말 좋은 주석 - 주석을 달지 않을 방법을 찾아낸 주석!

나쁜 주석 = 대부분의 주석, 대다수의 주석

  • 많은 주석 : 허술한 코드 지탱, 엉성한 코드 변명, 미숙한 결정 합리화

주절거리는 주석

  • 특별한 이유 없이 다는 주석 → 시간 낭비
  • 주석을 막상 단다면 충분히 시간을 들여 최고의 주석을 달도록 노력
  • 이해가 안되어 다른 모듈을 뒤져야하는 주석 → 독자와 소통하지 못하는 주석 → 바이트 낭비
  • 아래가 바이트 낭비의 예 : 주석의 의미를 알려면 다른 모듈을 뒤져야 함
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void loadProperties() {
    try {
    String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
    FileInputStream propertiesStream = new FileInputStream(propertiesPath);
    loadedProperties.load(propertiesStream);
    } catch (IOException e) {
    // 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 의미다.
    }
    }

같은 이야기 중복하는 주석

1
2
3
4
5
6
7
8
9
10
// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
if (!closed) {
wait(timeoutMillis);
if (!closed) {
throw new Exception("MockResponseSender could not be closed");
}
}
}

→ 코드 내용이 그대로 설명하는 주석

  • 주석이 코드보다 더 많은 정보를 제공하지 못함
  • 의도/근거를 설명하는 주석도 아니고 코드보다 읽기가 쉽지도 않음
  • 코드보다 부정확 → 독자가 함수를 대충 이해하고 넘어가게 만듬

오해 여지가 있는 주석

1
2
3
4
5
6
7
8
9
// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
if (!closed) {
wait(timeoutMillis);
if (!closed)
throws new Exception("MockResponseSend could not be close");
}
}0
  • this.closed가 true로 되는 순간에 메서드 반환이 아닌 true여야 반환되는 것이다
  • 주석만 본 프로그래머는 왜 코드가 느려지는지 알 수가 없음

의무적으로 다는 주석

  • 모든 함수에 Javadocs를 달거나 모든 필드에 주석 : 어리석기 그지 없음
  • 코드를 복잡하게 만들고 거짓말을 퍼뜨리고 혼동과 무질서를 초래하는 아무 가치가 없는 주석
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    *
    * @param title CD 제목
    * @param author CD 저자
    * @param tracks CD 트랙 숫자
    * @param durationInMinutes CD 길이(단위: 분)
    */
    public void addCD(String title, String author, int tracks, int durationInMinutes) {
    CD cd = new CD();
    cd.title = title;
    cd.author = author;
    cd.tracks = tracks;
    cd.duration = durationInMinutes;
    cdList.add(cd);
    }

이력을 기록하는 주석

  • 소스 코드 관리 시스템이 없었던 시절의 바람직한 관례
  • 현재는 혼란만 가중하므로 완전히 제거할것

있으나 마나 하는 주석

→ 너무 당연한 사실을 언급하고 새로운 정보를 제공못하는 주석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 기본 생성자
*/
protected AnnualDateRule() {
/** 월 중 일자 */
private int dayOfMonth;
/**
* 월 중 일자를 반환한다.
*
* @return 월 중 일자
*/
public int getDayOfMonth() {
return dayOfMonth;
}

}
  • 개발자가 주석을 무시하는 습관에 빠지게 함 → 코드를 읽으며 주석을 건너뜀 → 코드 바뀌면서 주석은 거짓말이 됨
  • 감정표현등 → 분풀이 주석으로 할 노력으로 코드 구조를 개선했었어야

무서운 잡음

  • 특정 오픈소스의 Javadocs에는 의미 없는 잡음이 존재

함수나 변수로 표현가능하면 주석을 달지 말라

주석 표현 버전
1
2
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (module.getDependSubsystems().contains(subSysMod.getSubSystem()))
주석을 없애고 코드로 의미를 표현 버전
1
2
3
ArrayList moduleDependees = smodule.getDependSubSystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))

위치를 표시하는 주석

1
// Actions /////////////////////////////////////////////
  • 가독성이 떨어짐, 뒷부분의 잡음 제거 필요
  • 배너 : 눈에 띄며 주의 환기 → 반드시 필요할 때만 아주 드물게 사용할 것

닫는 괄호에 다는 주석

  • 중괄호가 많아서 주석이 필요할 정도? 중첩이 심하고 장환한 함수라는 뜻
  • 클린코드가 지향하는 작고 캡슐화된 함수에서 해당 주서은 잡음일 뿐
  • 닫는 괄호에 주석을 달야야겠다는 생각이 든다면 함수를 작게 줄이려고 노력해야함

공로를 돌리거나 저자를 표시하는 주석

  • 코드 오염시키지 말아라
  • 코드에 오랫동안 방치되어 점점 쓸모 없어지고 부정확한 정보로 변함
  • 이런 정보는 소스 코드 관리 시스템에 저장할 것

주석으로 처리한 코드

주석으로 처리한 코드는 절대 작성하지 말 것
1
2
3
4
5
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamRead(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));
Apache commons의 코드
1
2
3
4
5
6
7
8
9
10
11
12
this.bytePos = writeBytes(pngIdBytes, 0);
//hdrPos = bytePos;
writeHeader();
writeResolution();
//dataPos = bytePos;
if (writeImageData()) {
wirteEnd();
this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);
} else {
this.pngBytes = null;
}
return this.pngBytes;
  • 주석 처리한 코드 : 밉살스러운 관행
    • 이유가 있다고 남겼다고 사람들이 생각함 → 지우기 주저
    • 점점 쓸모없는 코드 주석이 쌓여감
    • 1960년대 에는 주석으로 처리한 코드가 유용
    • 소스 관리 시스템이 알아서 코드를 기억해줌 → 코드 주석처리가 필요가 없음
    • 그냥 코드를 삭제하라

HTML 주석

혐오 그 자체

  • Javadocs에 html 태그

    태그 난리

  • IDE에서 조차 읽기 어려움
  • Javadocs등의 도구로 주석을 뽑아 웹에 올리려면 해당 태그 삽입은 프로그래머가 아닌 도구가 책임

전역 정보

  • (주석을 달아야 한다면) 근처 있는 코드만 기술
  • 코드 일부에 주석을 달면서 시스템 정반적 정보 기술 X
  • 아래 예시)
    • 이미 정보가 중복됨
    • 주석은 기본 포트 정보를 기술
    • 함수는 포트 기본값 통제 불가 → 이 주석은 밑의 함수가 아닌 다름 함수의 내용 설명
    • 즉 포트 기본값 설정 코드가 변해도 이 주석이 변하리라는 보장이 없음
      1
      2
      3
      4
      5
      6
      7
      8
      /**
      * 적합성 테스트가 동작하는 포트: 기본값은 <b>8082</b>.
      *
      * @param fitnessePort
      */
      public void setFitnessePort(int fitnessePort) {
      this.fitnewssePort = fitnessePort;
      }

너무 많은 정보

  • 주석에 스펙 역사나 관련없는 정보 장황하게 늘어놓지 마라

모호한 관계

Apache commons의 주석
1
2
3
4
5
/*
* 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다).
* 그리고 헤더 정보를 위해서 200 바이트를 더한다.
*/
this.pngBytes = new byte[((this. width + 1) * this.height * 3) + 200];
  • 위의 주석을 보고 드는 생각
    • 필터 바이트는 무엇?
    • +1과 관련이 있나? 아니면 *3과 관련이 있나? 아니면 둘다?
    • 한 픽셀이 한 바이트인가?
    • 200을 추가하는 이유는?
  • 주석 다는 목적 : 코드 만으로 설명이 부족해서
  • 주석 자체가 다시 설명을 요구하는 안 좋은 주석

함수 헤더

  • 짧은 함수 : 긴 설명이 필요 없음
  • 짧고 한 가지만 수행하며 이름을 잘 붙인 함수 >>>>> 주석으로 헤더를 추가한 함수

비공개 코드의 Javadocs

  • 공개 API에선 Javadocs가 유용
  • 공개하지 않을 코드라면 Javadocs가 쓸모가 없음
  • 시스템 내부에 속한 클래스와 함수에 Javadocs를 생성할 필요가 없음

예제

예제)

  • 주석을 잘 달았다고 착각하게 만들지만 바람직하지 않은 코드
    주석을 잘 달았다고 착각하게 만들지만 바람직하지 않은 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    /**
    * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다. 사용한 알고리즘은 에라스토테네스의 체다.
    * <p>
    * 에라스토테네스: 기원전 276년 ...(후략)
    * </p>
    * 알고리즘은 상당히 단순하다. 2에서 시작하는 정수 배열을 대상으로 2의 배수를 모두 제거한다.
    * 다음으로 남은 정수를 찾아 이 정수의 배수를 모두 지운다. 최대 값의 제곱근이 될 때까지 이를 반복한다.
    *
    * @author Alphonse
    * @version 13 Feb 2002 atp
    */
    import java.util.*;

    public class GeneratePrimes {
    /*
    * @param maxValue는 소수르 찾아낼 최대 값
    */
    public static int[] generatePrimes(int maxValue) {
    if (maxValue >= 2) { // 유일하게 유요한 경우
    // 선언
    int s = maxValue + 1; // 배열 크기
    boolean[] f = new booleans[s];
    int i;

    // 배열을 참으로 초기화
    for (i = 0; i < s; i++)
    f[i] = true;

    // 소수가 아닌 알려진 숫자를 제거
    f[0] = f[1] = false;

    // 체
    int j;
    for (i = 2; i < Math.sqrt(s) + 1; i++) {
    if (f[i]) { // i가 남아 있는 숫자라면 이 숫자의 배수를 구한다.
    for (j = 2 * i; j < s; j += i)
    f[j] = false; // 배수는 소수가 아니다.
    }
    }

    // 소수 개수는?
    int count = 0;
    for (i = 0; i < s; i++) {
    if (f[i])
    count++; // 카운트 증가
    }

    int[] primes = new int[count];

    // 소수를 결과 배열로 이동한다.
    for (i = 0, j = 0; i < s; i++) {
    if (f[i]) // 소수일 경우에
    primes[j++] = i;
    }

    return primes; // 소수를 반환한다.
    }
    else // maxValue < 2
    return new int[0]; // 입력이 잘못되면 비어 있는 배열을 반환한다.
    }
    }
위의 코드의 리팩터링 결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* 이 클래스는 사용자가 지정한 최대 값까지 소수를 구한다.
* 알고리즘은 에라스토테네스의 체다.
* 2에서 시작하는 정수 배열을 대상으로 작업한다.
* 처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
* 배열에 더 이상 배수가 없을 때까지 반복한다.
*/
public class PrimeGenerator {
private static boolean[] crossedOut;
private static int[] result;

public static int[] generatePrimes(int maxValue) {
if (maaxValue < 2)
return new int[0];
else {
uncrossIntegersUpTo(maxValue);
crossOutMultiples();
putUncrossedIntegerIntoResult();
return result;
}
}

private static void uncrossIntegersUpTo(int maxValue) {
crossedOut = new boolean[maxValue + 1];
for (int i = 2; i < crossedOut.length; i++)
crossedOut[i] = false;
}

private static void crossOutMultiples() {
int limit = determineIterationLimit();
for (int i = 2; i <= limit; i++)
if (notCrossed(i))
crossOutMultiplesOf(i);
}

private static int determineIterationLimit() {
// 배열에 있는 모든 배수는 배열 크기의 제곱근보다 작은 소수의 인수다.
// 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다
dobule iterationLimit = Math.sqrt(crossedOut.length);
return (int) iterationLimit;
}

private static void crossOutMultiplesOf(int i) {
for (int multiple = 2 * i; multiple < crossedOut.length; multiple += i)
crossedOut[multiple] = true;
}

private static boolean notCrossed(int i) {
return crossedOut[i] == false;
}

private static void putUncrossedIntegersIntoResult() {
result = new int[numberOfUncrossedIntegers()];
for (int j = 0, i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
result[j++] = i;
}

private static int nubmerOfUncrossedIntegers() {
int count = 0;
for (int i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
count++;

return count;
}
}

요약

  • 주석을 최대한 쓰지 말자
  • 쓰려면 좋은 주석
  • 주석보다는 어노테이션
  • JavaDoc

Related POST

공유하기