피드백, 오타 지적 환영합니다
나쁜 코드에 주석을 달지 마라. 새로 짜라
잘 달린 주석은 그 어떤 정보보다 유용하다. 그러나 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만든다.
부정확한 주석은 아예 없는 주석보다 훨씬 더 나쁘다. 부정확한 주석은 독자를 현혹하고 오도한다. 진실은 한곳에만 존재한다. 바로 코드다. 그러므로 우리는 주석을 가능한 줄이도록 꾸준히 노력해야 한다.
코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다. 모듈을 짜고 보니 짜임새가 엉망이고 알아먹기 어렵다. 지저분한 모듈이라는 사실을 자각한다. 그래서 자신에게 이렇게 말한다.
"이런! 주석을 달아야 겠다!"
아니다! 코드를 정리해야 한다!
자신이 저지른 난장판을 주석으로 설명하려 애쓰는 대신에 그 난장판을 깨끗이 치우는 데 시간을 보내라!
// 직원에게 복지 혜택을 받을 자격이 있는 지 검사한다
if((employee.flags & HOURLY_FLAG) &&
(employee.age > 65))
if(employee.isEligibleForFullBenefits())
몇 초만 더 생각하면 코드로 대다수 의도를 표현할 수 있다. 많은 경우 주석으로 달려는 설명을 함수로 만들어 표현해도 충분하다.
어떤 주석은 필요하거나 유익하다. 여기서는 글자 값을 한다고 생각하는 주석 몇 가지를 소개한다.
때로는 회사가 정립한 구현 표준에 맞춰 법적인 이유로 특정 주석을 넣으라고 명시한다. 예를 들어 각 소스 파일 첫 머리에 주석으로 들어가는 저작권 정보와 소유권 정보는 필요하고도 타당하다.
// Copyright (C) 2003,2004,2006 by Object Mentor, Inc. All rights reserved.
// GNU General Public License 버전 2 이상을 따르는 조건으로 배포한다.
때로는 기본적인 정보를 주석으로 제공하면 편리하다. 예를 들어 다음 주석은 추상 메서드가 반환할 값을 설명한다.
// 테스트 중인 Responder 인스턴스를 반환한다.
protected abstract Responder responderInstance();
그러나 함수 이름을 responderBeingTested로 바꾸면 주석이 필요없어진다.
// kk:mm:ss EEE MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile(
"\\d*:\\d*\\d* \\w*, \\w* \\d*, \\d*");
위 주석은 코드에서 사용한 정규표현식이 시각과 날짜를 뜻한다고 설명한다. 이왕이면 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 더 깔끔하다. 그러면 주석이 필요없어진다.
때때로 주석은 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명한다.
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder =
new WidgetBuilder(new Class[] {
BoldWidget.class
});
String text = "'''bold text'''";
ParentWidget parent =
new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);
//스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
for (int i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
}
저자가 문제를 해결한 방식에 동의하지 않을지도 모르지만 어쨌거나 저자의 의도는 분명히 드러난다.
때때로 모호한 인수나 반환값을 그 의미를 읽기 좋게 표현하면 이해하기 쉬워진다.
assertTrue(a.compareTo(a)==0); // a == a
그러나 위와 같은 주석은 그릇된 주석을 달아놓을 위험이 상당히 높으므로 더 나은 방법이 없는지 고민하고 정확히 달도록 각별히 주의한다.
// 여유 시간이 충분하지 않다면 실행하지 마십시오
public void _testWithReallyBigFile() {
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}
위와 같은 주석은 특정 테스트 케이스를 꺼야하는 이유를 설명하는 주석이다.물론 요즘에는 @ignore 속성을 이용해 테스트케이스를 꺼버리지만 위 주석은 매우 적절한 지적이다.
public static SimpleDateFormat makeStandardHttpDateFormat() {
//SimpleDateFormat is not thread safe,
//so we need to create each instance independently.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
위 주석은 아주 합리적이다. 프로그램 효율을 높이기 위해 정적 초기화 함수를 사용하려던 열성적인 프로그래머가 주석 때문에 실수를 면한다.
때로는 '앞으로 할일'을 //TODO 주석으로 남겨두면 편하다. 다음은 함수를 구현하지 않은 이유와 미래모습을 //TODO 주석으로 설명한 예제다.
// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요없다.
protected VersionInfo makeVersion() throws Exception{
return null;
}
TODO 주석은 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술한다. 그래도 TODO로 떡칠한 코드는 바람직하지 않다. 그러므로 주기적으로 TODO 주석을 점검해 없애도 괜찮은 주석은 없애라고 권한다.
자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해서도 주석을 사용한다.
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
설명이 잘 된 공개 API는 참으로 유용하고 만족스럽다. 표준 자바 라이브러리에서 사용한 Javadocs가 좋은 예다. Javadocs가 없다면 자바 프로그램을 짜기가 아주 어려우리라.
대다수 주석이 이 범주에 속한다. 일반적으로 대다수 주석은 허술한 코드를 지탱하거나 엉성한 코드를 변명하거나, 미숙한 결정을 변명하는 등 프로그래머가 주절거리는 독백에서 크게 벗어나지 못한다.
public void loadProperties() {
try {
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
} catch (IOException e) {
// 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 뜻이다.
}
}
저 주석은 무슨 뜻일까? 물론 저자에게야 의미가 있겠지만 그 의미가 다른 사람들에게는 전해지지 않는다. 답을 알아내려면 다른 코드를 뒤져보는 수밖에 없다. 이해가 안되어 다른 모듈까지 뒤져야 하는 주석은 독자와 제대로 소통하지 못하는 주석이다.
그런 주석은 바이트만 낭비할 뿐이다.
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception {
if (!closed) {
wait(timeoutMillis);
if (!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
코드의 내용을 그대로 중복한 주석은 코드보다 더 많은 정보를 제공하지 못하며, 코드보다 부정확해 독자가 함수를 대충 이해하고 넘어가게 만든다.
엔진 후드를 열어볼 필요가 없다며 고객에게 아양떠는 중고차 판매원과 비슷하다.
모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석기 그지없다.
이런 주석은 코드를 복잡하게 만들며, 거짓말을 퍼뜨리고, 혼돈과 무질서를 초래한다.
/**
*
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
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 = duration;
cdList.add(cd);
}
예를 들어 위 코드는 모든 함수에 Javadocs를 넣으라는 규칙이 낳은 괴물이다. 위와 같은 주석은 아무 가치도 없다. 오히려 코드만 헷갈리게 만들며, 거짓말할 가능성을 높이며, 잘못된 정보를 제공할 여지만 만든다.
/**
* Changes (from 11-Oct-2001)
* --------------------------
* 11-Oct-2001 : Re-organised the class and moved it to new package
* com.jrefinery.date (DG);
* 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate
* class (DG);
* 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate
* class is gone (DG); Changed getPreviousDayOfWeek(),
* getFollowingDayOfWeek() and getNearestDayOfWeek() to correct
* bugs (DG);
* 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
* 29-May-2002 : Moved the month constants into a separate interface
* (MonthConstants) (DG);
* 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
* 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Implemented Serializable (DG);
* 29-May-2003 : Fixed bug in addMonths method (DG);
* 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG);
* 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);
*/
때때로 사람들은 모듈을 편집할 때마다 모듈 첫머리에 주석을 추가한다. 일종의 일지 혹은 로그가 된다. 예전에는 소스 코드 관리 프로그램이 없었으므로 이러한 관례가 바람직했다. 하지만 이제는 혼란만 가중할 뿐이다.
완전히 제거하는 편이 좋다.
때때로 프로그래머는 소스 파일에서 특정 위치를 표시하려 주석을 사용한다.
// Actions. ////////////////////////////////////////////////////
극히 드물지만 위와 같은 배너 아래 특정 기능을 모아놓으면 유용한 경우도 있다. 하지만 일반적으로 위와 같은 주석은 가독성만 낮추므로 제거해야 마땅하다. 특히 뒷부분에 슬래시로 이어지는 잡음은 제거하는 편이 좋다.
배너를 남용하면 독자가 흔한 잡음으로 여겨 무시한다.
중첩이 심하고 장황한 함수라면 의미가 있을지도 모르지만 작고 캡슐화된 함수에는 잡음일 뿐이다. 그러므로 닫는 괄호에 주석을 달아야 겠다는 생각이 든다면 대신에 함수를 줄이려 시도하자.
주석으로 처리한 코드는 다른 사람들이 지우기를 주저한다. 이유가 있어 남겨놓았으리라고, 중요하니까 지우면 안된다고생각한다.
그래서 질 나쁜 와인병 바닥에 앙금이 쌓이듯 쓸모없는 코드가 점차 쌓여간다.
소스 코드 관리 시스템이 우리를 대신해 코드를 기억해준다. 이제는 주석으로 처리할 필요가 없다. 그냥 코드를 삭제하라. 잊어버릴 염려는 없다. 약속한다.
주석을 달아야 한다면 근처에 있는 코드만 기술하라. 코드 일부에 주석을 달면서 시스템 전반적인 정보를 기술하지마라.
주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 한다. 이왕 공들여 주석을 달았다면 적어도 독자가 주석과 코드를 읽어보고 무슨 소린지 알아야 하지 않겠는가?
/*
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
여기서 필터 바이트란 무엇일까? +1과 관련이 있을까? 아니면 *3과 관련이 있을까?
주석을 다는 목적은 코드만으로 설명이 부족해서다.
주석 자체가 다시 설명을 요구하니 안타깝기 그지없다.
짧은 함수는 긴 설명이 필요 없다. 짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.
공개 API는 Javadocs가 유용하지만 공개하지 않을 코드라면 Javadocs는 쓸모가 없다. 시스템 내부에 속한 클래스와 함수에 Javadocs를 생성할 필요는 없다. 유용하지 않을 뿐만 아니라 Javadocs 주석이 요구하는 형식으로 인해 코드만 보기 싫고 산만해질 뿐이다.