혼자 정리

4. 주석 본문

클린코드

4. 주석

tbonelee 2021. 9. 27. 02:48

4장 주석

  • 주석은 '필요악'이다
    • 주석을 유지 보수하는 것은 쉽지 않으니 오래될수록 코드와 동떨어지게 된다
      • → 없는 것보다 못하게 될 수 있음
    • 가능하다면 코드에 제대로 된 정보를 담아서 해결하자

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

  • 나쁜 코드를 주석을 통해 보완하는 것보다 코드를 정돈하는 것이 더 효율적이다.

코드로 의도를 표현하라!

  • 물론 확실히 코드만으로 의도를 설명하기 어려운 경우도 있다. 그래서 많은 개발자들이 자신의 코드도 그런 경우라고 생각하는데 잘 생각해보면 해결책이 있는 경우가 더 많으니 코드로 해결해보자.
  • ex)
    • // 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
      if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))​
    • if (employee.isEligibleForFullBenefits())​
       
    • 첫번째 코드보다 두번째가 낫다.

좋은 주석

여기서 소개하는 주석은 사실상 불가피한 주석이라 봐도 된다

법적인 주석

  • 회사가 정립한 구현 표준에 맞춰 법적인 이유로 특정 주석을 넣으라고 하는 경우
    • ex) 각 소스 파일 첫머리에 저작권 정보와 소유권 정보 표기
  • 모든 조항과 조건을 열거하는 대신, 가능하다면 표준 라이센스나 외부 문서를 참조하게 하자.

정보를 제공하는 주석

  • 예시를 보자.
    • // kk:mm:ss EEE, MMM dd, yyyy 형식
      Pattern timeMatcher = Pattern.compile(
      	"\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");​
       
    • 코드의 정규표현식의 의미를 설명해준다.
    • 그렇지만 이왕이면 시각과 날짜를 변환하는 클래스를 만들어서 코드를 옮겨주면 더 좋겠다

의도를 설명하는 주석

  • 왜 이렇게 구현했는지 개발자의 의도를 설명해줄 수 있다.
  • ex1)
    • 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; // we are greater because we are the right type.
      }​
       
    • 자기 객체를 다른 객체보다 높은 순위를 부여하기로 했다는 점을 설명
  • ex2)
    • 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);
      
      	//This is our best attempt to get a race condition
      	//by creating large number of threads.
      	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());
      }

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

  • argument나 반환값이 표준 라이브러리나 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석이 유용
    • 그럼에도 주석에 오류가 생길 가능성이 있기에 위험성이 존재한다. 따라서 이를 잘 생각하고 주석을 달자
  • ex)
    • public void testCompareTo() throws Exception
      {
      	WikiPagePath a = PathParser.parse("PageA");
      	WikiPagePath ab = PathParser.parse("PageA.PageB");
      	WikiPagePath b = PathParser.parse("PageB");
      	WikiPagePath aa = PathParser.parse("PageA.PageA");
      	WikiPagePath bb = PathParser.parse("PageB.PageB");
      	WikiPagePath ba = PathParser.parse("PageB.PageA");
      
      	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(b.compareTo(a) == 1);    // b > a
      	assertTrue(ab.compareTo(aa) == 1);  // ab > aa
      	assertTrue(bb.compareTo(ba) == 1);  // bb > ba
      }

결과를 경고하는 주석

  • ex1)
    • // Don't run unless you
      // have some time to kill.
      public void _testWithReallyBigFile()
      {
      	writeLinesToFile(10000000);
      
      	response.setBody(testFile);
      	response.readyToSend(this);
      	String responseString = output.toString();
      	assertSubString("Content-Length: 1000000000", responseString);
      	assertTrue(bytesSent > 1000000000);
      }
  • ex2)
    • 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 주석

  • 앞으로 할 일을 남겨두면 편하다.
    • ex) 당장 구현하기 어려운 업무, 더 이상 필요 없는 기능을 삭제하라는 알림, 누군가에게 문제를 봐달라는 요청, 더 좋은 이름으로 바꿔달라는 요청, 앞으로 발생할 이벤트에 맞춰 코드를 고치라는 주의 등
  • 주기적으로 TODO 주석을 점검해서 없애도 괜찮은 주석은 없애자
    • //TODO-MdM these are not needed
      // We expect this to go away when we do the checkout model
      protected VersionInfo makeVersion() throws Exception
      {
      	return null;
      }

중요성을 강조하는 주석

  • 그냥 보면 사소하게 여겨질 수 있는 부분을 주석을 통해 강조할 수 있다.
  • ex)
    • String listItemContent = match.group(3).trim();
      // the trim is real important. It removes the starting
      // spaces that could cause the item to be recognized
      // as another list.
      new ListItemWidget(this, listItemContent, this.level + 1);
      return buildList(text.substring(match.end()));

공개 API에서 Javadocs

  • javadocs를 통해 설명을 잘 해놓으면 api 이용자가 프로그램을 짤 때 유용하다.
  • 물론 잘못된 정보 주지 않게 조심하자.

나쁜 주석

주절거리는 주석

  • 애매하게 단 주석은 오히려 읽는 사람을 헷갈리게 한다.
  • ex)
    • public void loadProperties()
      {
      	try
      	{
      		String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
      		FileInputStream propertiesStream = new FileInputStream(propertiesPath);
      		loadedProperties.load(propertiesStream);
      	}
      	catch(IOException e)
      	{
      		// No properties files means all defaults are loaded
      	}
      }

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

  • 코드에 있는 내용을 굳이 반복하지 말자. 주석을 읽는 데 시간낭비하게 한다.
    • 오히려 코드보다 설명이 부실해서 주석만 읽고 가면 더 이해도가 낮게 될 수도 있다.
    • // 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");
      	}
      }

오해할 여지가 있는 주석

  • 바로 위의 코드는 this.closed가 true이면 반환되는 것이지 this.closed가 true로 변할 때 반환되지 않는다. 또한 this.closed가 true가 아닌 경우에만 타임아웃에서 예외를 던진다. 하지만 주석은 오해하기 쉽게 써놓았다.

의무적으로 다는 주석

  • ex) 억지로 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);
      }

이력을 기록하는 주석

  • 버전 관리 시스템이 없던 시절에나 필요했던 것이다.
  • 없애자.

있으나 마나 한 주석

  • 너무 당연한 사실을 알려주는 주석
  • 이러한 주석의 문제점은 읽는 사람이 중요한 주석조차 무시하게 만든다는 점이다.
  • ex1)
    • /**
      * Default constructor.
      */
      protected AnnualDateRule() {
      }
    • /** The day of the month. */
      	private int dayOfMonth;
    • /**
      * Returns the day of the month.
      *
      * @return the day of the month.
      */
      public int getDayOfMonth() {
       return dayOfMonth;
      }
       

무서운 잡음

  • javadocs도 의미 있는 내용을 제공하지 않으면 잡음일 뿐이다.

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

  • 예시를 보자.
    • ex1)
      • // does the module from the global list <mod> depend on the
        // subsystem we are part of?
        if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))​
         
      • 위의 코드보다는 주석이 필요하지 않도록 다음과 같이 개선할 수 있다.
        • ArrayList moduleDependees = smodule.getDependSubsystems();
          String ourSubSystem = subSysMod.getSubSystem();
          if (moduleDependees.contains(ourSubSystem))

위치를 표시하는 주석

  • ex)
    • // Actions ////////////////////////////////////
  • 반드시 필요할 때만 사용하자. 안 그러면 읽는 사람은 잡음으로만 여길 것이다

닫는 괄호에 다는 주석

  • 중첩이 심하고 장황한 함수라면 의미가 있을지도 모르지만 작고 캡슐화된 함수에는 잡음일 뿐이다.
  • ex)
    • 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
      }

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

  • 소스 코드 관리 시스템이 누가 언제 무엇을 추가했는지 알려주므로 굳이 달지 말자

주석으로 처리한 코드

  • 주석으로 처리한 코드는 이유가 있어 남겨놓았으리라고 생각하게 만들기 때문에 다른 사람들이 쉽게 지우지 못한다. 따라서 쌓이기 쉬운 쓰레기다
  • 어차피 소스 코드 관리 시스템이 다 기억하므로 지금 쓰지 않는 코드는 다 지우자.
  • ex)
    • InputStreamResponse response = new InputStreamResponse();
      response.setBody(formatter.getResultStream(), formatter.getByteCount());
      // InputStream resultsStream = formatter.getResultStream();
      // StreamReader reader = new StreamReader(resultsStream);
      // response.setContent(reader.read(formatter.getByteCount()));

HTML 주석

  • javadocs같은 도구로 주석을 뽑아서 웹 페이지에 올릴 거라면 주석에 HTML 태그를 삽입하는 것은 개발자가 아닌 도구가 해야 하는 일이다.
  • ex)
    /**
    * Task to run fit tests.
    * This task runs fitnesse tests and publishes the results.
    * <p/>
    * <pre>
    * Usage:
    * &lt;taskdef name=&quot;execute-fitnesse-tests&quot;
    *     classname=&quot;fitnesse.ant.ExecuteFitnesseTestsTask&quot;
    *     classpathref=&quot;classpath&quot; /&gt;
    * OR
    * &lt;taskdef classpathref=&quot;classpath&quot;
    *             resource=&quot;tasks.properties&quot; /&gt;
    * <p/>
    * &lt;execute-fitnesse-tests
    *     suitepage=&quot;FitNesse.SuiteAcceptanceTests&quot;
    *     fitnesseport=&quot;8082&quot;
    *     resultsdir=&quot;${results.dir}&quot;
    *     resultshtmlpage=&quot;fit-results.html&quot;
    *     classpathref=&quot;classpath&quot; /&gt;
    * </pre>
    */

전역 정보

  • 주석을 달거면 근처에 있는 코드에 대해서만 설명해라
  • 시스템 전반적인 정보에 대해 설명하지 말아라
  • 그런 주석은 해당 정보가 바뀌어도 추적해서 수정될 가능성이 낮다.
  • ex)
    • /**
      * Port on which fitnesse would run. Defaults to <b>8082</b>.
      *
      * @param fitnessePort
      */
      public void setFitnessePort(int fitnessePort)
      {
      	this.fitnessePort = fitnessePort;
      }

너무 많은 정보

  • ex)
    • /*
      	RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
      	Part One: Format of Internet Message Bodies
      	section 6.8. Base64 Content-Transfer-Encoding
      	The encoding process represents 24-bit groups of input bits as output
      	strings of 4 encoded characters. Proceeding from left to right, a
      	24-bit input group is formed by concatenating 3 8-bit input groups.
      	These 24 bits are then treated as 4 concatenated 6-bit groups, each
      	of which is translated into a single digit in the base64 alphabet.
      	When encoding a bit stream via the base64 encoding, the bit stream
      	must be presumed to be ordered with the most-significant-bit first.
      	That is, the first bit in the stream will be the high-order bit in
      	the first 8-bit byte, and the eighth bit will be the low-order bit in
      	the first 8-bit byte, and so on.
      */

모호한 관계

  • 주석과 주석이 설명하는 코드 사이의 관계가 명확하게 하자. 적어도 주석 과 코드를 읽고 무슨 소리인지는 알 수 있게 하라는 말이다.
  • ex) 
    • /*
      * 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];

함수 헤더

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

'클린코드' 카테고리의 다른 글

6. 객체와 자료구조  (0) 2021.10.10
5. 형식 맞추기  (0) 2021.10.06
3. 함수  (0) 2021.09.23
2. 의미 있는 이름  (0) 2021.09.19
1. 깨끗한 코드  (0) 2021.09.14