이 글은 개발자 필독서인 클린 코드를 읽으며 습득한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.
코드로 대다수 의도를 표현할 수 있다. 주석으로 달려는 설명을 함수로 만들어 표현하는 방법이 존재한다.
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
if (employee.isEligibleForFullBenefits())
주석을 달지 않는 것이 가장 좋은 방법이지만, 주석이 필요하거나 유익한 경우가 존재한다.
주석으로 기본적인 정보를 제공하면 편리하고 유용하나, 가능하다면 함수 이름에 정보를 담는 편이 낫다.
// 테스트 중인 Responder 인스턴스를 반환한다.
protected abstract Responder responderInstance();
// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w* \\d* \\d*");
이왕이면 클래스를 만들어 코드를 옮겨주어 주석을 없애는 방법을 추천한다.
public class DateTimeParser {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("kk:mm:ss EEE, MMM dd, yyyy");
public static LocalDateTime parseDateTime(String dateTimeText) {
try {
return LocalDateTime.parse(dateTimeText, DATE_TIME_FORMATTER);
} catch (DateTimeParseException e) {
System.err.println("Failed to parse date-time: " + e.getMessage());
return null;
}
}
}
String exampleDateTime = "14:30:00 Sun, Jul 27, 2024";
LocalDateTime dateTime = DateTimeParser.parseDateTime(exampleDateTime);
때때로 주석은 결정에 깔린 의도까지 설명한다.
public int compaerTo(Object o) {
if(o instanceof WiliPagePath) {
WikiPagePath p = (WikiPagePath) o;
String compressedName = StringUtil.join(names, "");
String compressedArgumentName = StringUtil.join(p.names, "");
}
return 1; // 옳은 유형이므로 정렬 순위가 더 높다.
}
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
String text = "'''bold text'''";
ParentWidget parent = new BoldWidget(new MockWidgetRoot(), 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());
}
애매한 인수나 반환 값은 그 의미를 읽기 좋게 표현하면 이해하기 쉬워진다.
인수나 반환 값이 표준 라이브러리나 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석이 유용하다.
주석이 올바른지 검증하기 쉽지 않기 때문에 그릇된 주석을 달아놓을 확률이 높다. 그러므로 더 나은 방법이 없는지 고민하고 정확히 달도록 각별히 주의한다.
public void testCompareTo() throws Exception {
WikiPagePath a = PathParser.parse("PageA");
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);
}
public static SimpleDateFormat makeStandardHttpDateFormat() {
// SimpleDateFormat은 스레드에 안전하지 못하다.
// 따라서 각 인스턴스를 독립적으로 생성해야 한다.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
자칫 대수롭지 않다고 여겨질 무언가의 중요성을 강조하기 위해 사용하기도 한다.
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말로 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
주석에 담긴 잘못된 정보로 인해 발생하는 이슈를 파악하느라 시간을 소모하게 된다.
// this.close가 true일 때 반환되는 유틸리티 메소드다.
// 타임아웃에 도달하면 예외를 던진다.
public stnchronized void waitForClose(final long timeoutMillis) throws Exception {
if(!closed) {
wait(timeoutMillis);
if(!closed) {
throw new Exception("MockResponseSender could not be closed");
}
}
}
this.closed
가 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);
}
너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석은 있으나 마나 한 주석이다.
/**
* 기본 생성자
*/
protected AnnualDateRule() {}
/** 달 중 날짜 */
private int DayOfMonth;
/**
* 달 중 날짜를 반환한다.
*
* @return 달 중 날짜
*/
public int getDayOfMonth() {
return dayOfMonth;
}
개발자가 코드를 읽으면서 자동으로 주석을 건너뛰게 되어, 나중에는 코드와 주석이 불일치하게 된다.
private void startSending() {
try {
doSending();
} catch(SocketException e) {
// 정상. 누군가 요청을 멈췄다.
} catch(Exception e) {
try {
response.add(ErrorResponser.makeExceptionString(e));
response.closeAll();
} catch(Exception e1) {
// 이게 뭐야!
}
}
}
있으나 마나 한 주석을 달기보다는 코드를 정리하고 개선하도록 한다.
private void startSending() {
try {
doSending();
} catch(SocketException e) {
// 정상. 누군가가 요청을 멈췄다.
} catch(Exception e) {
addExceptionAndCloseResponse(e);
}
}
private void addExceptionAndcloseResponse(Exception e) {
try {
response.add(errorResponder.makeExceptionString(e));
response.closeAll();
} catch(Exception e1) {}
}
단지 문서를 제공해야 한다는 잘못된 욕심으로 탄생한 주석은 아무런 이익도 주지 못한다.
/** The name. */
private String name;
/** The version. */
private String version;
/** The licenceName. */
private String licenceName;
/** The version. */
private String info;
ctrl
+ c
, ctrl
+ v
오류가 존재한다.주석이 필요하지 않도록 코드를 개선하는 방향으로 개발하도록 한다.
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
중첩이 심하고 장황한 함수라면 의미가 있지만, 작고 캡슐화된 함수에는 잡음일 뿐이다.
그러므로 닫는 괄호에 주석을 다는 대신 함수를 줄이도록 한다.
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try {
while ((line = in.readLine()) != null) {
lineCount++;
charCount += line.length();
String words[] = line.split("\\W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} //try
catch (IOException e) {
System.err.println("Error:" + e.getMessage());
} //catch
} //main
}
오랫동안 코드에 방치되어 부정확하고 쓸모없는 정보로 변하기 일쑤이므로, 소스 코드 제어 시스템에 저장하는 편이 낫다.
도구로 주석을 뽑아 웹 페이지에 올릴거라면, 주석에 HTML 태그를 삽입하는 책임은 도구가 져야 한다.
주석을 달 때 근처에 있는 코드만 기술하고, 시스템 전반적인 정보를 기술하지 않도록 한다.
/**
* 적합성 테스트가 동작하는 포트: 기본값은 <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];
짧고 한 가지만 수행하며 이름을 잘 붙은 함수가 주석으로 헤더를 추가한 함수보다 낫다.
📖 참고
- 로버트 C. 마틴, 『Clean Code 클린 코드 애자일 소프트웨어 장인 정신』, 박재호·이해영 옮김, 케이앤피북스(2010), p97-119.