CS/Clean Code

CS Clean Code - 주석

강태종 2022. 2. 22. 03:16

주석

사실상 주석은 기껏해야 필요악이다. 우리에게 프로그래밍 언어를 치밀하게 사용해 의도를  표현할 능력이 있다면, 주석은 거의 필요하지 않으니라. 아니, 필요하지 않으리라. 그러므로 주석이 필요한 상황에 처하면 곰곰이 생각하기 바란다. 상황을 역전해 코드로 의도를 표현할 방법은 없을까?

 

주석을 무시하는 이유? 주석은 너무 자주 거짓말을 하니까. 주석은 오래될수록 코드에서 멀어지고, 그릇될 가능성이 커진다. 이유는 단순하다. 프로그래머들이 주석을 유지하고 보수하기란 현실적으로 불가능하니까.

 

코드는 변화하고 진화한다. 일부가 옮겨지거나 조각이 나눠지고 합쳐진다. 불행하게도 주석은 언제나 코드를 따라가지 않는다. 주석이 코드에서 분리되어 점점 더 부정확한 고아로 변하는 사례가 너무도 흔하다.

 

프로그래머들이 주석을 엄격하게 관리해야 한다고, 그래서 복구성과 관련성과 정확성이 언제나 높아야 한다고 주장할 수 있다. 그 의견에 동의한다. 하지만 나라면 코드를 깔끔하게 정리하고 주석이 필요 없는 방향으로 에너지를 쏟겠다.

 

느낀점
주석을 작성하면 유지보수를 해야하고, 오래된 주석을 봤을 때 그 주석이 맞는지 검증까지 해야한다. 심지어 코드의 변경사항까지 있으면 주석도 수정해야 한다. 코드를 수정하는 작업은 IDE의 도움을 받을 수 있지만, 주석은 힘들다. 옳바르지 않은 주석 남용을 조심해야겠다.

 

진실은 한곳에만 존재한다. 바로 코드다. 코드만이 정확한 정보를 제공하는 유일한 출처이다. 그러므로 우리는 주석을 가능한 줄이도록 꾸준히 노력해야한다.
나쁜 코드에 주석을 달지 마라. 새로 짜라.
- 브라이언 W. 커니핸, P.J. 플라우거

 

주석은 나쁜 코드를 보완하지 못한다.

코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다. 모듈을 작성하고 보니 지저분한 모듈이라는 사실을 자각한다. 그래서 이렇게 말한다. "이런! 주석을 달아야겠다.!"

 

자신이 저지른 난장판을 주석으로 설명하려 애쓰는 대신에 그 난장판을 깨끗이 치우는 데 시간을 보내라!

 

코드로 의도를 표현하라!

몇 초만 더 생각하면 코드로 대다수 의도를 표현할 수 있다. 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분하다.

if ((employee.flags & HOURLY_FLAG) &&
    (employee.age > 65))

if (employee.isEligibleForFullBenefits())

 

좋은 주석

어떤 주식은 필요하거나 유익하다. 하지만 명심하기를 바란다. 정말로 좋은 주석은 주석을 달지 않을 방법을 찾아낸 주석이라는 사실을

 

법적인 주석

때로는 회사가 정립한 구현 표준에 맞춰 법적인 이유로 특정 주석을 넣으라고 명시한다. 저작권 정보와 소유권 정보는 필요하고도 타당하다. 다행스럽게도 요즘 IDE는 주석을 자동으로 축소해준다.

 

정보를 제공하는 주석

때로는 기본적인 정보를 주석으로 제공하면 편리하다. 물론 함수 이름에 정보를 담는 편이 더 좋고, 클래스를 만들어 코드를 옮겨주면 더 좋고 더 깔끔해진다.

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

 

의도를 설명하는 주석

때때로 주석은 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명한다.

public int compareTo(Object o) {
    if (o instance of WikiPagePath) {
        ...
    }

    return 1; // 오른쪽 유형이므로 정렬 순위가 더 높다.
}

 

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

일반적으로 인수나 반환값 자체를 명확하게 만들면 더 좋겠지만, 표준 라이브러리나 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석은 유용하다. 물론 그 주석이 그릇된 정보를 제공할 위험이 있고, 주석을 검증하는 작업도 쉽지 않다.

 

결과를 경고하는 주석

코드를 수정하여 근본적인 문제를 없애는 것이 가장 좋다. 하지만 문제를 해결하기 쉽지않은 상황에서 경고하는 주석은 매우 합리적이다. 주석으로 다른 프로그래머의 실수를 면할 수 있다.

public static SimpleDateFormat makeStandardHttpDateFormat() {
    // SimpleDataFormat은 스레드에 안전하지 못하다.
    // 따라서 각 인스턴스를 독립적으로 생성해야 한다.
    SimpleDateFormat df = new SimpleDateFormat();
    df.setTimeZome(TimeZone.getTimeZone("GMT"));
    return df;
}

 

 

TODO 주석

때로는 '앞으로 할 일'을 TODO 주석으로 남겨두면 편리하다. 다음은 함수를 구현하지 않은 이유와 미래 모습을 설명한 예제이다.

// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception {
    return null;
}

TODO의 옳바른 예시

  • 더 이상 필요 없는 기능을 삭제하라는 알림
  • 누군가에게 문제를 봐달라는 요청
  • 더 좋은 이름을 떠올려달라는 부탁
  • 앞으로 발생할 이벤트에 맞춰 코드를 고치라는 주의
  • 하지만 어떤 용도로 사용하든 시스템에 나쁜 코드를 남겨 놓는 핑계가 되어서는 안 된다.

요즘 IDE에서 TODO 주석을 모아서 보여주는 기능을 제공하므로 주석을 잊을 염려는 없다. 그렇다고 TODO로 떡칠한 코드가 좋다는 것은 아니다. 주기적으로 TODO를 점검해야 한다.

 

중요성을 강조하는 주석

자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해 주석을 사용한다.

 

공개 API에서 Javadocs

설명이 잘 된 공개 API는 참으로 유용하고 만족스럽다. 표준 자바 라이브러리에서 사용한 Javadocs가 좋은 예이다. 하지만 Javadocs도 역시 독자를 오도하거나, 그릇된 정보를 전달할 가능성이 존재한다.

 

나쁜 주석

대다수 주석이 이 범주에 속한다. 일반적으로 대다수 주석은 허술한 코드를 지탱하거나, 엉성한 코드를 변명하거나, 미숙한 결정을 합리화하는 등 프로그래머가 주절거리는 독백에서 크게 벗어나지 못한다.

 

주절거리는 주석

특별한 이유 없이 마지못해 주석을 단다면 전적으로 시간낭비다. 주석을 달기로 결정했다면 충분한 시간을 들여 최고의 주석을 달도록 노력한다.

public void loadProperties() {
    try {
        String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
        FileInputStream propertiesStream = new FileInputStream(propertiesPath);
        loadedProperties.load(propertiesStream);
    } catch(IOException e) {
        // 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 의미다.
    }
}

catch 블록에 있는 주석은 무슨 뜻일까?

  • loadedProperties.load를 호출하기 전에 읽어 들이는가?
  • loadedProperties.load가 예외를 잡아 기본값으로 읽은 후 예외를 던져주는가?
  • loadedProperties.load가 파일을 읽어 들이기 전에 모든 기본값부터 읽어 들이는가?

결국 답을 알아내려면 코드를 뒤져야하고, 독자와 제대로 소통하지 못하는 주석은 바이트만 낭비할 뿐이다.

 

같은 이야기를 중복하는 주석

// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
    if (!closed) {
        wait(timeoutMillis) {
            if (!closed) {
                throw new Exception();
            }
        }
    }
}
  • 주석이 코드보다 더 많은 정보를 제공하지 못한다.
  • 코드를 정당화하는 주석도 아니다
  • 의도나 근거를 설명하는 주석도 아니다
  • 실제로 코드보다 부정확해 그릇된 정보를 줄 수 있다.

주석이 코드 내용을 그대로 중복했으며, 자칫하면 코드보다 주석을 읽는 시간이 더 오래 걸린다.

 

오해할 여지가 있는 주석

// this.closed가 true일 때 반환되는 유틸리티 메서드다.
// 타임아웃에 도달하면 예외를 던진다.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
    if (!closed) {
        wait(timeoutMillis) {
            if (!closed) {
                throw new Exception();
            }
        }
    }
}
  • this.closed가 true로 변하는 순간 에러를 반환하지 않는다. (timeout을 기다리고 끝났을 때 true면 에러를 반환한다.)
  • 타임아웃에 도달하면 예외를 던진다. (타임아웃에 도달했을 때 this.closed가 true면 반환하지 않는다.)

 

의무적으로 다는 주석

모든 함수에 주석을 달아야 한다는 규칙은 어리석기 그지없다. 이런 주석은 코드를 복잡하게 만들며 거짓말을 퍼뜨리고 혼동과 무질서를 초래한다.

 

개인적인 생각

  • 주석이 너무 많으면 오히려 코드 보기가 어렵다.
  • 코드를 수정하면 주석도 수정하는 불필요한 작업이 생긴다.
  • 만약 수정하지 못한 주석은 잘못된 정보를 줄 수 있다.

 

이력을 기록하는 주석

예전에는 모든 모듈 첫머리에 변경 이력을 기록하는 관례가 바람직했다. 하지만 지금은 소스 코드 관리 시스템이 있고, 혼론만 가중할 뿐이다.

 

있으나 마나 한 주석

너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석이다.

/*
 * 기본 생성자
 */
protected AnnualDateRule() {
    
}

의미없는 주석은 주석을 무시하는 습관을 만든다. 코드를 읽으면서 주석을 건너뛰어 주석의 역할을 못하거나, 코드를 변경했을 때 주석을 무시하여 거짓된 주석을 기록하게 됩니다.

 

함수나 변수로 표현할 수 있다면 주석을 달지 마라

주석이 필요하지 않도록 코드를 개선하는 편이 더 좋았다.

// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))

 

위치를 표시하는 주석

  • 때때로 프로그래머는 소스 파일에서 특정 위치를 표시하려 주석을 사용한다. 일반적으로 가독성만 낮추므로 제거해야 마땅하다.
  • 너무 자주 사용하지 않는다면 배너는 눈에 띄며 주의를 환기한다. 그러므로 반드시 필요할 때만 드물게 사용하는 것이 좋다.

 

닫는 괄호에 다는 주석

중첩이 심하고 크기가 큰 함수라면 의미가 있을지도 모르지만, 크기가 큰 함수는 작은 함수로 변경하는 것이 좋고, 작은 함수에서 닫는 괄호에 다는 주석은 오히려 가독성을 떨어트린다.

 

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

주석이 있으면 코드에 관해 누구한테 물어볼지 아니까 유용하다고 여길 수 있다. 하지만 현실적으로 이런 주석은 그냥 오랫동안 코드에 방치되어 점차 부정확하고 쓸모없는 정보로 변한다. 소스 코드 관리 시스템을 적극 사용하자

 

주석으로 처리한 코드

1960년대 즈음에는 주석으로 처리하는 코드가 유용했다. 하지만 요즘은 우수한 소스 코드 관리 시스템이 있고, 우리를 대신해서 기억해준다.

InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultStream);
// response.setContent(reader.read(formatter.getByteCount()));

주석으로 처리된 코드는 다른 사람들이 지우기를 주저한다. 이유가 있어 남겨놓다고 오해하기 쉽다.

 

전역정보

주석을 달아야 한다면 근처에 있는 코드만 기술하라. 코드 일부에 주석을 달면서 시스템 전반적인 정보를 기술하지 마라. 주석이 중복된 설명을 할 위험이 있고, 주석을 관리하기 어려워진다.

/*
 * 적합성 테스트가 동작하는 포트 : 기본값은 <b>8082</b>
 *
 * @param fitnesseport
 */
public void setFitnessePort(int fitnesseport) {
    this.fitnesseport = fitnesseport;
}

위 주석은 시스템 어딘가에 있는 다른 함수를 설명하고 있으며, 중복된 설명으로 오히려 가독성을 떨어트리고, 다른 함수에서 포트값이 변경됐을 때 주석이 변경된다는 보장이 없다.

 

너무 많은 정보

주석에다 흥미로운 역사나 관련없는 정보를 장황하게 늘어놓지 마라.

 

개인적인 생각

  • 보통 읽기 어려운 코드를 주석으로 작성하는데 주석마저 읽기 어려워진다.
  • 코드 내용이 변경됐을 때 주석에 변경 사항을 적용하기 어려워진다.

 

모호한 관계

주석과 코드는 관계가 명백해야 한다. 이왕 공들여 작성했다면 주석과 코드를 읽었을 때 이해할 수 있어야 한다.

/*
 * 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다).
 * 그리고 헤더 정보를 위해 200바이트를 더한다.
 */
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

 

  • 필터 바이트란 무엇인가?
  • 필터 바이트는 +1과 관련이 있을까 *3과 관련이 있을까?
  • 200을 추가하는 이유는?

 

함수 헤더

짧은 함수는 긴 설명이 필요 없다. 짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.

 

비공개 코드에서 Javadocs

공개 API는 Javadocs가 유용하지만 공개하지 않을 코드라면 쓸모가 없다. 오히려 주석이 요구하는 형식으로 인해 코드만 보기 싫고 산만해질 뿐이다.