본문 바로가기

Book Review/Clean Code

클린코드 4장_주석

주석

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

  • 코드에 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다.
  • 표현력이 풍부하고 깔끔하며, 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.

2. 코드로 의도를 표현하라.

  • 확실히 코드만으로 의도를 설명하기 어려운 경우가 존재한다. 그래서 많은 개발자가 이걸 설명하는데 코드는 훌륭한 수단이 아니라는 의미로 해석한다.
// 직원에게 복지혜택을 받을 자격이 있는지 검사한다.
if((employee.flags & HOURLY_FLAG) && employee.age > 65))
if(employee.isEligibleForFullBenefits())

위 코드와 아래 코드를 비교했을 때 아래 코드가 더 알기 쉽다.

3. 좋은 주석

  • 어떤 주석은 필요하거나 유익하다. 하지만 정말로 좋은 주석은, 주석을 달지 않을 방법을 찾아낸 주석이다.

법적인 주석

// Copyright (C) 2003, 2004, 2005 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.comile(
"\\\\d*:\\\\d*:\\\\d* \\\\w*, \\\\w* \\\\d*, \\\\d*); 
  • 위에 제시한 주석은 코드에서 사용한 정규표현식이 시각과 날짜를 뜻한다고 설명한다.
  • 이왕이면 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 깔끔할 수도 있다.

의도를 설명해주는 주석

  • 때때로 주석은 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명한다.
public int compareTo(Object o)
{
	if(o instanceof WikiPagePath)
	{
		WikiPagePath p = (WikiPagePath) o;
		String compressedName = StringUtil.join(names, "");
		String compressedArgumentName = StringUtil.join(p.names, "");
		return compressedName.compareTo(compressedArgumentName);
	}
	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(), "'''bold text'''");
	AtomicBoolean failFlag = new AtomicBoolean();
	failFlag.set(false);

	// 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
	for (int i = 0; i < 2500; 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")
	WikiPagePath ab = PathParser.parse("PageA.PageB");
	WikiPagePath b = PathParser.parse("PageA")
	WikiPagePath aa = PathParser.parse("PageA.PageB");
  WikiPagePath bb = PathParser.parse("PageA")
	WikiPagePath ba = PathParser.parse("PageA.PageB");

	assertTrue(a.compareTo(a) == 0); // a == a
	assertTrue(a.compareTo(b) != 0); // a != b
	assertTrue(ab.compareTo(ab) == 0); // ab == ab
	assertTrue(a.compareTo(b) == -1); // a < b
	assertTrue(aa.compareTo(ab) == -1); // aa < ab
	assertTrue(ba.compareTo(bb) == -1); // ba < bb
	assertTrue(a.compareTo(a) == 1); // b > a
	assertTrue(ab.compareTo(aa) == 1); // ab > ab
	assertTrue(bb.compareTo(ba) == 1); // bb > ba
}

결과를 경고하는 주석

  • 때로 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용하기도 한다.
// 여유 시간이 충분하지 않다면 실행하지 마십시오.
public void _testWithReallyBigFile()
{
	writeLinesToFile(1000000);
	
	......
}
  • 요즘에는 @Ignore 속성에 문자열로 넣어주는 방식을 많이 사용하고 있다.
public static SimpleDataFormat makeStandardHttpDateFormat()
{
	// SimpleDataFormat은 스레드에 안전하지 못하다
	// 따라서 각 인스턴스를 독립적으로 생성해야 한다
	SimpleDataFormat df = new SimpleDataFormat("EEE, dd MMM yyyy HH:mm:ss z");
	df.setTimeZone(TimeZone.getTimeZone("GMT"));
	return df
}
  • 다음은 주석이 아주 적절한 예제이다.

TODO 주석

  • 때로는 ‘앞으로 할 일’을 // TODO 주석으로 남겨두면 편하다.
  • TODO 주석은 프로그래머가 필요하다 여기지만, 당장 구현하기 어려운 업무를 기술한다.
// TODO-MdM은 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception
{
	return null;
}

중요성을 강조하는 주석

  • 자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해서도 주석을 사용한다.
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level+1);
return buildList(text.substring(match.end()));

공개 API에서 Javadocs

  • 공개 API를 구현한다면 반드시 훌륭한 Javadocs를 작성한다.

4. 나쁜 주석

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

4.1 주절거리는 주석

public void loadProperties()
{
	try
	{
		String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
		FileInputStream propertiesStream = new FileInputStream(propertiesPath);
		loadProperties.load(propertiesStream);
	}
	catch(IOException e)
	{
		// 속성 파일이 없다면 기본값을 모두 메모리로 읽어 들였다는 의미이다.
	}
}
  • IOException이 발생하면 속성 파일이 없는데, 누가 모든 기본값을 읽어들이는지 모호함
  • 이해가 안되어 다른 모듈까지 뒤져야 하는 주석은 독자와 제대로 소통하지 못하는 주석이다.\

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

// 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");
	}
}
  • 주석이 코드보다 더 많은 정보를 제공하지 못한다.
  • 자칫하면 코드보다 주석을 읽는 시간이 더 오래 걸릴 수 있다.
public abstract class ContainerBase implements Container, Lifecycle, Pipeline, MBeanRegistration, Serializable
{
	/**
	* 이 컴포넌트의 프로세서 지연값
	*/
	protected int backgroundProcessorDelay = -1;
/**
	* 이 컴포넌트를 지원하기 위한 생명주기 이벤트
	*/
	protected LifecycleSupport lifecycle = new LifecycleSupport(this);
/**
	* 이 컴포넌트를 위한 컨테이너 이벤트 Listener
	*/
	protected ArrayList listeners = new ArrayList();
/**
	*  컨테이너와 관련된 Loader 구현
	*/
	protected Loader loader = null;
/**
	* 컨테이너와 관련된 Logger 구현
	*/
	protected Log logger = null;
......

  • 쓸데없고 중복된 Javadocs가 매우 많다. 위 주석은 코드만 지저분하고 정신 없게 만든다.

4.3 오해할 여지가 있는 주석

  • 때때로 의도는 좋았으나, 프로그래머가 딱 맞을 정도로 엄밀하게는 주석을 달지 못하기도 한다.
  • 4.2 주석을 보면 this.closed가 true로 변하는 순간에 메서드는 반환되지 않으며, this.closed가 true여야 메서드는 반환된다.
    • 하지만 어떤 프로그래머가 true로 변하는 순간에 반환되리라는 생각으로 경솔하게 함수를 호출할 수 있다는 위험성을 가지고 있다.

4.4 의무적으로 다는 주석

  • 모든 함수에 Javadocs를 달거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석기 그지없다. 이런 주석은 코드를 복잡하게 만들며, 거짓말을 퍼뜨리고, 혼동과 무질서를 초래한다.
/**
	*
	* @param title CD 제목
	* @param author CD 저자
	* @param tracks CD 트랙 숫자
	* @param durrationInMinutes 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);
}

4.5 이력을 기록하는 주석

  • 때때로 사람들은 모듈을 편집할 때 마다 모듈 첫머리에 주석을 추가한다. 예전에는 모든 모듈 첫 머리에 변경 이력을 기록하고 관리하는 관례가 바람직했지만, 당시에는 소스 코드 관리 시스템이 없어서 그랬고 이제는 혼란만 가중하여 완전히 제거하는게 좋다.

4.6 있으나 마나 한 주석

  • 때때로 있으나 마나 한 주석을 접한다. 너무 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석이다.
  • 아래와 같은 주석은 지나친 참견이라 개발자가 주석을 무시하는 습관에 빠질 수 있다.
/**
* 기본 생성자
*/
protected AnnualDateRule() {
}

/** 월 중 일자 */
private int dayOfMonth;

/**
* 월 중 일자를 반환한다.
*
* @return 월 중 일자
*/
public int getDayOfMonth(){
	return dayOfMonth;
}
  • 아래 주석의 첫 번째 주석은 적절해 보인다. catch 블록을 무시해도 괜찮다는 이유를 설명하는 주석이다.
  • 하지만 두 번째 주석은 전혀 쓸모가 없다.
private void startSending()
{
	try
	{
		doSending();
	}
	catch(SocketException e)
	{
		// 정상 누군가 요청을 멈췄다
	}
	catch(Exception e)
	{
		try
		{
			response.add(ErrResponder.makeExceptionString(e));
		}
		catch(Exception e1)
		{
			// 이게 뭐야!
		}
	}
}
private void startSending()
{
	try
	{
		doSending();
	}
	catch(SocketException e)
	{
		// 정상 누군가 요청을 멈췄다
	}
	catch(Exception e)
	{
		try
		{
			response.add(ErrResponder.makeExceptionString(e));
		}
		catch(Exception e1)
		{
			addExceptionAndCloseResponse(e)
		}
	}

	private void addExceptionAndCloseResponse(Exception e)
	{
		try
		{
			response.add(ErrorResponder.makeExceptionString(e));
			response.closeAll();
		}
		catch(Exception e1)
		{
		}
}

4.7 무서운 잡음

  • 함수나 변수로 표현할 수 있다면 주석을 달지 마라.
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())

위 주석을 없애고 아래와 같이 표현할 수 있다.

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

4.8 위치를 표시하는 주석

// Actions //////////////////////////////////
  • 위와 같이 위치를 표시하기 위해 슬래시로 위치를 표시하면 가독성을 낮추므로 제거하는 편이 좋다.
  • 하지만 너무 자주 사용하지 않는다면 배너는 눈에 띄며 주의를 환기해서 반드시 필요할 때만, 아주 드물게 사용하는 편이 좋다.

4.9 닫는 괄호에 다는 주석

  • 닫는 괄호에 주석을 달아야겠다는 생각이 든다면 대신에 함수를 줄이기위해 시도하자
public class wc {
public static void main(String[] args){
	BufferedReader in = new BufferedReader(new InpuStreamReader(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.....
			....
	} // try
}

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

  • 소스 코드 관리 시스템으로 저장할 수 있는걸 굳이 주석으로 표시 할 필요는 없다.
/* 릭이 추가함 */

4.11 주석으로 처리한 코드

  • 주석으로 처리된 코드는 다른 사람들이 지우기를 주저하고, 질 나쁜 와인병 바닥에 앙금이 쌓이듯 쓸모 없는 코드가 점차 쌓여간다.
  • 1960년대 즈음에는 주석으로 처리한 코드가 유용했지만, 오래전부터 우수한 소스 코드 관리 시스템을 사용해왔기 때문에, 소스 코드 관리 시스템이 우리를 대신에 코드를 기억해준다. 이제는 주석으로 처리할 필요가 없다.
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultStream);
// response.setContent(reader.read(formatter.getByteCount());

4.12 HTML 주석

  • 소스코드에서 HTML 주석은 혐오 그 자체이다.

4.13 전역 정보

  • 주석을 달아야 한다면 근처에 있는 코드만 기술하라. 코드 일부에 주석을 달면서 시스템의 전반적인 정보를 기술하지 마라.
  • 아래 주석은 바로 아래 함수가 아니라 시스템에 어딘가에 있는 다른 함수를 설명한다는 말이다.
/**
* 적합성 테스트가 동작하는 포트: 기본값은 <b>8082</b>.
*
* @param fitnessPort
*/

4.14 너무 많은 정보

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

4.15 모호한 관계

  • 주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 한다.
/*
* 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다)
* 그리고 헤더 정보를 위해 200바이트를 더한다.
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
  • 여기서 필터 바이트란 무엇인지 모호하며, +1과 관련이 있는지 *3과 관련이 있는지 모른다.
  • 주석 자체가 다시 설명을 요구하는 상황은 없어야한다.

4.16 함수 헤더

  • 짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.

4.17 비공개 코드에서 Javadocs

  • 공개 API는 Javadocs가 유용하지만 공개하지 않을 코드라면 Javadocs는 쓸모가 없다.

예제

/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다. 사용된 알고리즘은
 * 에라스토테네스의 체다.
 * <p>
 * 에라스토테네스: 기원전 276년에 리비아 키레네에서 출생, 기원전 194년에 사망
 * 지구 둘레를 최초로계산한 사람이자 달력에 윤년을 도입한 사람.
 * 알렉산드리아 도서관장을 역임.
 * </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 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]){
                    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];
    }
}
/**
 *이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다.
 *알고리즘은 에라스토테네스의 체다.
 * 2에서 시작하는 정수 배열을 대상으로 작업한다.
 *처음으로 남아 있는 정수를 찾아 배수를 모두 제거한다.
 *배열에 더이상 남아 있는 배수가 없을 때 까지 반복한다.
 */
import java.util.*;

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();
            returnresult;
        }
    }

    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() {
        // Every multiple in the array has a prime factor that
        // is less than or equal to the root of the array size,
        // so we don't have to cross out multiples of numbers
        // larger than that root.
        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) {
        returncrossedOut[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;
    }
}

참고 자료

  • Clean Code 애자일 소프트웨어 장인 정신
 

'Book Review > Clean Code' 카테고리의 다른 글

클린코드 6장_객체와 자료구조  (0) 2022.09.09
클린코드 5장_형식 맞추기  (0) 2022.09.06
클린 코드 3장 정리 #1  (0) 2022.07.30
클린 코드 2장 정리 #2  (0) 2022.07.29
클린 코드 2장 정리 #1  (0) 2022.07.28