📕 목차
1. 주석은 나쁜 코드를 보완하지 못한다
2. 코드로 의도를 표현하라!
3. 좋은 주석
• 법적인 주석
• 정보를 제공하는 주석
• 의도를 설명하는 주석
• 의미를 명료하게 밝히는 주석
• 결과를 경고하는 주석
• TODO 주석
• 중요성을 강조하는 주석
• 공개 API에서 Javadocs
4. 나쁜 주석
• 주절거리는 주석
• 같은 이야기를 중복하는 주석
• 오해할 여지가 있는 주석
• 의무적으로 다는 주석
• 이력을 기록하는 주석
• 있으나 마나 한 주석
• 무서운 잡음
• 함수나 변수로 표현할 수 있다면 주석을 달지 마라
• 위치를 표시하는 주석
• 닫는 괄호에 다는 주석
• 공로를 돌리거나 저자를 표시하는 주석
• 주석으로 처리한 코드
• HTML 주석
• 전역 정보
• 너무 많은 정보
• 모호한 관계
• 함수 헤더
• 비공개 코드에서 Javadocs
• 예제
1. 주석은 나쁜 코드를 보완하지 못한다
💡 나쁜 코드에 주석을 달지 마라. 새로 짜라. - 브라리언 W. 커니핸, P. J. 플라우거
- 주석은 순수하게 선하지 못하다. 기껏해야 필요악이다.
- 코드로 의도를 표현하지 못해, 실패를 만회하기 위해 사용한다. (주석은 언제나 실패를 의미한다.)
- 주석은 오래될 수록 코드에서 멀어져 거짓말을 한다. (유지, 보수가 불가능하다)
- 진실은 언제나 코드에만 존재한다. 따라서 주석을 가능한 줄이도록 노력해야 한다.
- 주석을 추가하는 이유는 코드 품질이 나쁘기 때문이다.
- 난장판을 주석으로 설명 (X) → 코드를 정리해야 한다 (O)
2. 코드로 의도를 표현하라!
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((emplotee.flags & HOURLY_FLAG) && (employee.age > 65)
if (employee.isEligibleForFullBenefits())
위 두 코드는 같은 기능을 한다. 어느 쪽이 나은가?
대부분의 경우 주석을 함수로 만들어 표현해도 충분하다.
3. 좋은 주석
💡 정말 좋은 주석은, 주석을 달지 않을 방법을 찾아낸 주석이다.
📌 법적인 주석
// Copyright (C) 2003, 2004, 2005 by Object Montor, Inc. All right reserved.
// GNU General Public License
- 각 소스 파일 첫머리에 주석으로 들어가는 저작권 정보와 소유권 정보는 필요하고 타당하다.
- 표준 라이선스, 외부 문서 참조 등을 넣어도 좋다.
📌 정보를 제공하는 주석
// 테스트 중인 Responder 인스턴스를 반환
protected abstract Responder responderInstance();
- 때로는 기본적인 정보를 주석으로 제공하면 편리하다.
- 가능하다면, 함수 이름에 정보를 담는 편이 더 좋다. (ex. responderBeingTested)
// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\d*:\\d*\\d* \\w*, \\w*, \\d*, \\d*");
- 이왕이면 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 깔끔하다. (그러면 주석 불필요)
📌 의도를 설명하는 주석
// 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
for (int i = 0; i > 2500; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
- 때때로 주석은 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명한다.
- 저자가 문제를 해결한 방식에 동의하지 않을 수는 있으나 의도는 분명하다.
📌 의미를 명료하게 밝히는 주석
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(a.compareTo(ab) == -1); // a < ab
- 모호한 인수나 반환값을 읽기 좋게 표현하면 이해가 쉬워진다.
- 인수나 반환 값이 표준 라이브러리나 변경 불가능한 코드에 속하면 주석이 유용하다.
- 다만, 주석이 올바른지 검증하기가 쉽지 않으므로 더 나은 방법을 고민해보자.
📌 결과를 경고하는 주석
// 여유 시간이 충분하지 않다면 실행하지 마십시오.
public void _testWithReallyBigFile() {
}
- 최근에는 언더바(_) 대신 @Ignore("실행이 너무 오래 걸린다.")라고 쓴다.
📌 TODO 주석
// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception {
return null;
}
- '앞으로 할 일'을 구현하지 않은 이유와 미래 모습으로 달면 좋다.
- TODO 주석은 프로그래머가 필요하다 여기지만 당장 구현이 어려운 업무를 기술한다.
- 더 이상 필요 없는 기능을 삭제하라는 알림
- 누군가에게 문제를 봐달라는 요청
- 더 좋은 이름을 떠올려 달라는 부탁
- 앞으로 발생할 이벤트에 맞춰 코드를 고치라는 주의
📌 중요성을 강조하는 주석
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
- 대수롭지 않다고 여겨질 뭔가의 중요성 강조를 위해서 사용
📌 공개 API에서 Javadocs
- 설명이 잘 된 공개 API는 참으로 유용하고 만족스럽다.
- 공개 API를 구현한다면 반드시 훌륭한 Javadocs를 작성하라.
- 하지만 이 또한 독자를 오도하거나, 잘못 위치하거나, 그릇된 정보를 전달할 가능성이 있다.
4. 나쁜 주석
📌 주절거리는 주석
💡주석을 달기로 했다면 충분한 시간을 들여 최고의 주석을 달도록 노력하라
public void loadProperties() {
try {
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
} catch (IOException e) {
// 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 의미다.
}
}
- 저자 외엔 별 다른 의미가 전해지지 않는다. 답을 알아내려면 다른 코드를 뒤져보는 방법밖에 없다.
- 이해가 안 되어 다른 모듈까지 뒤져야 하는 주석은 독자와 제대로 소통하지 못하는 주석이다.
📌 같은 이야기를 중복하는 주석
// 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");
}
}
}
- 주석이 코드보다 더 많은 정보를 제공하지 못한다.
- 아무런 의미도 제공하지 못하고, 불필요한 주석
📌 오해할 여지가 있는 주석
- 위의 코드는 "this.closed가 true로 변하는 순간에 반환"이 아닌, "true여야 반환"된다.
- 주석의 살짝 잘못된 정보로 인해, 함수를 호출한 프로그래머가 자신의 코드가 느려터진 이유를 찾지 못하게 만든다.
📌 의무적으로 다는 주석
/**
*
* @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);
}
- 모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석은 짓이다.
📌 이력을 기록하는 주석
* 변경 이력 (11-Oct-2001부터)
* ------------------------------------------------
* 11-Oct-2001 : 클래스를 다시 정리하고 새로운 패키징
* 05-Nov-2001: getDescription() 메소드 추가
* 이하 생략
- 최근에는 소스 코드 관리 시스템이 있으므로, 완전히 제거하라.
📌 있으나 마나 한 주석
/*
* 기본 생성자
*/
protected AnnualDateRule() {
}
- 너무나 당연한 사실을 언급하여 새로운 정보를 제공하지 못한다.
📌 무서운 잡음
/** The name */
private String name;
/** The version */
private String version;
- 아무런 기능도 없는, 단순히 문서를 제공해야 한다는 잘못된 욕심으로 탄생한 잡음이다.
📌 함수나 변수로 표현할 수 있다면 주석을 달지 마라
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (module.getDependSubsystems().contains(subSysMod.getSubSystem()))
ArrayList moduleDependencies = smodule.getDependSubSystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
(와 ㅋㅋ 이건 좀 소름돋네)
📌 위치를 표시하는 주석
// Actions /////////////////////////////////////////////
- 극히 드물게 소스 파일 특정 위치(ex. 배너 아래 특정 기능 모아둔 라인)를 표현할 때 유용한 경우도 있다.
- 일반적으로 가독성만 낮추므로 제거하라.
- 너무 자주 사용하지만 않는다면 배너는 눈에 띄며 주의를 환기하므로, 아주 드물게 사용하는 게 좋다. (슬래시 잡음은 지워라)
📌 닫는 괄호에 다는 주석
- while, try, if 등의 닫는 괄호에 while, try, if 주석을 다는 경우가 있다.
- 애초에 작고 캡슐화된 함수에서는 잡음일 뿐이다.
📌 공로를 돌리거나 저자를 표시하는 주석
/* 릭이 추가함 */
- 소스 코드 관리 시스템이 관리하므로 불필요하다.
📌 주석으로 처리한 코드
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;
- 주석으로 쓰인 코드는 다른 사람들이 함부로 지우기를 주저하다 차곡차곡 쌓인다.
- 소스 코드 관리 시스템이 코드를 기억해준다. 전부 삭제하라. 잃어버릴 염려는 없다.
📌 HTML 주석
/**
* <p/>
* <pre>
* 용법 :
* ........
* </pre>
*/
- 소스 코드에서 HTML 주석은 혐오 그 자체다.
- Javadocs 같은 도구로 주석을 뽑아 웹 페이지로 올릴 것이라면, 주석에 HTML 태그를 삽입하는 책임은 프로그래머가 아니라 도구가 가져가야 한다.
📌 전역 정보
/**
* 적합성 테스트가 동작하는 포트: 기본값은 <b>8082</b>.
*
* @param fitnessePort
*/
public void setFitnessePort(int fitnessePort) {
this.fitnewssePort = fitnessePort;
}
- 주석을 달아야 한다면 근처에 있는 코드만 기술하라
- 코드 일부에 주석을 달면서 시스템 전반적 정보를 기술하지 마라
- 위의 코드는 함수가 통제하지도 못하는 설정값에 대한 설명을 덧붙이고 있다.
📌 너무 많은 정보
- 불필요하며 관련 없는 정보를 늘어놓은 주석
📌 모호한 관계
- 주석과 주석이 설명하는 코드는 명백한 연관 관계가 있어야 한다.
- 이왕 공들여 달았다면, 독자가 주석과 코드를 보고 이해할 수 있어야 한다.
📌 함수 헤더
- 짧은 함수는 긴 설명이 필요없다.
- 짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.
📌 비공개 코드에서 Javadocs
- 공개하지 않을 코드라면 Javadocs는 쓸모가 없다.
- 시스템 내부의 클래스와 함수에 Javadocs를 생성할 필요가 없다.
📌 예제
/**
* 이 클래스는 사용자가 지정한 최댓값까지 소수를 생성한다.
* 알고리즘은 에라토스테네스의 체다.
* <p>
* 에라스토테네스: 기원전 276년에 리비아 키레네에서 출생, 기원전 194년에 사망한 그리스의 수학자이다.
* 지구 둘레를 최초로 계산한 사람이자 달력에 윤년을 도입한 사람.
* 알렉산드리아 도서관장을 역임.
* </p>
* 알고리즘은 상당히 단순하다. 2부터 시작하는 정수 배열을 대상으로 한다.
* 배열에서 가장 작은 수를 찾아 배수를 모두 제거한다.
* 다음으로 남은 수를 찾아서 배수를 모두 제거한다.
* 이 과정을 배열에 더 이상 배수가 없을 때까지 반복한다.
*
* @author jayang
* @version 13 Feb 2002 atp
*/
public class GeneratePrimes {
/**
* 사용자가 지정한 최댓값까지 소수를 생성한다.
* @param maxValue 이 메소드가 소수를 찾을 최댓값
* @return 소수의 리스트
*/
public static int[] generatePrimes(int maxValue) {
if (maxValue >= 2) { // 유효한 경우
// 선언
int s = maxValue + 1; // 배열 크기
boolean[] f = new boolean[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가 남아있는 숫자이면
// 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 { // 유효하지 않은 경우
return new int[0]; // 0개의 소수를 포함하는 배열을 반환한다.
}
}
}
지금까지 배운 내용을 기반으로 위의 코드를 리팩터링하면 다음과 같다.
/**
* 이 클래스는 사용자가 지정한 최댓값까지 소수를 생성한다.
* 알고리즘은 에라토스테네스의 체다.
* 2에서 시작하는 정수 배열을 대상으로 작업한다.
* 처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
* 배열에 더 이상 배수가 없을 때까지 반복한다.
*/
public class PrimeGenerator {
private static boolean[] crossedOut;
private static int[] result;
public static int[] generatePrimes(int maxValue) {
if (maxValue < 2)
return new int[0];
else {
uncrossIntegersUpTo(maxValue);
crossOutMultiples();
putUncrossedIntegersIntoResult();
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() {
// 배열에 있는 모든 배수는 배열 크기의 제곱근보다 작은 소수의 인수다.
// 따라서 이 제곱근보다 더 큰 숫자의 배수는 제거할 필요가 없다.
double 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 numberOfUncrossedIntegers() {
int count = 0;
for (int i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
count++; // 소수
return count;
}
}