Clean Code & Refactoring

[Clean Code] 4장. 주석 : Comments

유자맛바나나 2022. 4. 17. 04:43

[Clean Code 시리즈 포스팅]

[Clean Code] 1장. 깨끗한 코드: Clean Code

[Clean Code] 2장. 의미 있는 이름 : Meaningful Names

[Clean Code] 3장. 함수 : Functions

[Clean Code] 4장. 주석 : Comments (Now)

[Clean Code] 5장. 형식 맞추기 : Formatting

[Clean Code] 6장. 객체와 자료구조 : Objects and Data Structures

[Clean Code] 9장. 단위 테스트 : Unit Tests

 

 

 

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

  • 우리는 코드로 의도를 표현하지 못해, 그러니까 실패를 만회하기 위해 주석을 사용한다. 주석을 달 때 마다 자신에게 코드로 의도를 표현할 수 있는 표현력이 없다는 사실을 푸념해야 마땅하다
  • 주석을 무시하는 이유는 거짓말을 하기 때문이다. 주석은 오래될수록 코드에서 멀어진다. 오래될수록 완전히 잘못될 가능성도 커진다. 이유는 단순하다. 프로그래머들이 주석을 유지하고 보수하기란 현실적으로 불가능하기 때문이다
  • 주석이 코드에서 분리되어 점점 더 부정확한 고아로 변하는 사례는 너무도 흔하다
  • 프로그래머들이 주석을 엄격하게 관리해야 한다고 주장할수도 있으나, 코드를 깔끔하게 정리하고 표현력을 강화해 애초에 주석이 필요 없는 방향으로 에너지를 쏟겠다

 

 

진실은 한곳에만 존재한다. 바로 코드다. 코드만이 자기가 하는 일을 진실되게 말한다.
코드만이 정확한 정보를 제공하는 유일한 출처다.
그러므로 우리는 주석을 가능한 줄이도록 꾸준히 노력해야 한다.

 

 

 

 

 

❑ 코드로 의도를 표현하라!

  • 코드만으로 의도를 설명하기 어려운 경우가 분명 존재한다. 하지만 다음 예제 코드 두 개를 봤을 때 더 좋은 쪽은 명확하다
  • 좋은 주석과 나쁜 주석의 종류를 살펴 보기 전 "코드로 의도를 표현하라"라는 내용이 나오는 이유는 주석 사용을 최대한 지양하고, 최대한 코드로 작성할 것을 강조하는 것이다

Bad Case

// 직원에게 복지 혜택을 받을 자격이 있는 지 검사한다
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

Good Case

if ((employee.isEligibleForFullBenefits())

 

❑ 좋은 주석의 종류

  • 정말로 좋은 주석은 주석을 달지 않는 것임을 명심해야 한다. 글자 값을 하는 몇 가지 사례는 다음과 같다
  • 저자가 소개하는 몇 가지 종류가 있지만 명확히 구분되는 개념은 아니다. 책을 읽고 느낀 점은 주석을 달기 전 "코드로 표현할 수 없는 부분인가?"라는 대원칙을 준수하기 위해 끊임 없이 자문해야 한다는 것이다.

 

1. 법적인 주석

  • 때로는 회사의 표준에 맞춰 법적인 이유로 특정 주석을 넣어야할 수 있다.

Example Code

// Copyright (C) 2022 by CitronBanana, Inc. All rights reserved.
...

 

2. 정보를 제공하는 주석

  • 때로는 기본적인 정보를 주석으로 제공하면 편리하다

Example Code

// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
  • 위에 제시한 주석은 코드에서 사용한 정규표현식이 시각과 날짜를 뜻한다고 설명한다.

 

3. 의도를 설명하는 주석

  • 주석은 결정에 깔린 의도를 설명할 때 도움이 될 수 있다

Example Code

public void testConcurrentAddWidgets() throws Exception {
    ...
    
    // 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
    for (int i = 0; i < 25000; i++) {
        WidgetBuilderThread widgetBuilderThread = new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
        Thread thread = new Thread(widgetBuilderThread);
        thread.start();
    }
}

 

  • 앞 뒤 코드를 알 필요는 없다. 반복문의 의도를 밝히는 데 사용된 주석의 내용이 도움이 되는 주석인가에 대해서만 생각해보자

 

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

  • 이름으로 의미가 모호한 인수(Parameter)나 반환값의 의미를 명확하게 표현할 수 없을 때 주석을 사용하면 도움이 될 수 있다
  • 예를 들어, 반환값이 표준 라이브러리와 같이 변경하지 못하는 코드에 속할 경우가 해당한다
  • 단, 잘못된 주석을 달아놓을 위험과 주석 자체가 올바른 것인지 검증하기가 쉽지 않아 위험하다. 따라서 의미를 명확히 밝히는 주석은 필요한 주석인 동시에 위험한 주석이 될 가능성이 높기도 하다.

Example Code

public void testCompareTo() throws Exception {
    WikiPagePath a = PathParser.parse("PageA");
    WikiPagePath b = PathParser.parse("PageB");
    ...
    
    assertTrue(a.compareTo(a) == 0); // a == a
    assertTrue(a.compareTo(b) != 0); // a != b
    ...
}
  • a.compareTo(a) == 0 이라는 코드만으로 의미가 명확하지 않아 a == a 라는 주석을 사용한 것

 

5. 결과를 경고하는 주석

  • 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용한다
  • @Disabled 속성을 이용해 테스트 케이스를 꺼버리는 방식으로도 할 수 있다 (책에서 나온 @Ignore는 JUnit4까지만 지원한다)

Example Code

@Test
@Disabled("수행하는데 오래걸리므로 시간이 충분하면 돌릴 것")
public void testWithBigData() throws Exception {
    System.out.println("testWithBigData");
}
  • 앞 뒤 코드를 알 필요는 없다. 반복문의 의도를 밝히는 데 사용된 주석의 내용이 도움이 되는 주석인가에 대해서만 생각해보자

 

6. TODO 주석

  • '앞으로 할 일'을 //TODO 주석으로 남겨두면 편하다. 프로그래머가 필요하다 여기지만 당장 구현하기 어려운 업무를 기술한다

 

7. 중요성을 강조하는 주석

  • 별 것 아니라고 여길 수 있는 무언가의 중요성을 강조하기 위해 사용될 수 있다

Example Code

// 여기서 trim은 문자열에서 시작 공백을 제거하므로 매우 중요하다
// 공백이 있을 경우 다른 이름으로 인식할 수 있다.
String inputNames = input.trim();

 

8. 공개 API에서 Javadocs

  • 설명이 잘 된 API를 작성하기 위한 Javadocs는 매우 유용하다. 표준 자바 라이브러리에서 사용한 Javadocs가 좋은 예다.
  • 하지만 여느 주석과 마찬가지로 Javadocs 역시 잘못된 정보로 독자를 오도할 가능성이 존재한다.

 

 

 

❑ 나쁜 주석의 종류

  • 대다수 주석이 이 범주에 속한다. 일반적으로 주석은 허술한 코드를 지탱하거나, 엉터리 코드를 변명하거나 합리화하는 등 프로그래머의 독백에서 크게 벗어나지 못한다

 

1. 주절거리는 주석 / 의무적으로 작성하는 주석

  • 특별한 이유 없이 의무감 또는 프로세스에 따라야 해서 작성하는 주석은 시간낭비다

 

2. 오해할 여지가 있는 주석

  • 당연한 말이지만 주석의 내용이 오해할 수 있는 여지가 있으면 안된다.

 

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

  • 소스 코드 관리 시스템은 누가 언제 무엇을 추가했는지 귀신처럼 기억한다. 저자 이름으로 코드를 오염시킬 필요가 없다.

 

4. HTML 주석

  • "소스코드에서 HTML 주석은 혐오 그 자체다"
  • HTML 주석은 편집기/IDE에서조차 읽기 어렵다. 

 

5. 전역 정보

  • 주석을 달아야 한다면 근처에 있는 코드만 기술해라. 코드 일부가 아닌 시스템 전반적인 정보를 기술하면 혼란만 일으킬 수 있다. 

 

6. 비공개 코드에서 Javadocs

  • 공개 API는 Javadocs가 유용하지만 공개하지 않을 코드라면 Javadocs는 쓸모가 없다.
  • 뿐만 아니라 Javadocs가 요구하는 형식으로 인해 코드만 보기 싫고 산만해질 뿐이다.

 

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

  • 주석이 같은 코드 내용을 그대로 중복하는 경우다
  • 아래 예제 코드는 tomcat의 코드인데 유명한 오픈 소스에서도 저자가 말하는 나쁜 예시가 있다는 게 의외였다

Example Code(tomcat code)

public abstract class ContainerBase implements Container, Lifecycle, Pipeline {

    /**
     * 이 컴포넌트의 프로세서 지연값
     */
    protected int backgroundProcessorDelay = -1;

    /**
     * 이 컴포넌트를 지원하기 위한 생명주기 이벤트
     */
    protected LifecycleSupport lifecycle = new LifecycleSupport(this);

    /**
     * 이 컴포넌트를 위한 컨테이너 이벤트 Listener
     */
    protected ArrayList listeners = new ArrayList();

    ...

}

 

 

8. 이력을 기록하는 주석

  • 소스 코드 관리 시스템이 없을 땐 변경 이력을 기록하고 관리하는 관례가 바람직했다. 하지만 이제는 혼란만 생길 뿐이니 제거하는 것이 좋다.

Example Code

/**
 * 2022-04-17: 사용하지 않는 주석 제거
 * 2022-03-02: 변수명 리팩토링
 * ...
 */
public class Comentary {
    ...
}

 

9. 있으나 마나 한 주석

  • 새로운 정보를 제공하지 않는, 당연한 사실을 언급하는 주석은 있으나 마나하다.

Example Code

public class Comentary {

    /** 기본 생성자 */
    public Comentary() {
    }
    
    /** 월 중 일자 */
    public int dayOfMonth;

    ...
}

 

10. 함수나 변수로 표현할 수 있는 것은 주석을 달지 마라

  • 함수나 변수로 표현할 수 있다면, 주석을 달아서 설명하는 것을 피해야 한다.

Example Code

[Refactoring 전]

// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (smodule.getDependSubSystems().contains(subSysMod.getSubSystem())) {
    ...
}


[Refactoring 후]

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

 

  • 코드 줄은 늘어났지만 주석의 내용을 적절한 변수를 사용해 이해할 수 있도록 변경했다

 

11. 위치를 표시하는 주석

  • 너무 자주 사용하지 않는다면 위치를 표시하는 주석은 눈에 띄며 주의를 환기한다.
  • 하지만 반드시 필요할 때 아주 드물게 사용하는 것이 좋다. 너무 자주 사용하면 독자가 흔한 잡음으로 여겨 무시한다

Example Code(JavaScript, React)

  /*
    Hooks
  */
  useEffect(() => {
    ...
  }, []);

  useEffect(() => {
    ...
  }, [simulator, version]);
  • React Hook을 모아놓는 용도로 주석을 사용함

 

12. 닫는 괄호에 사용하는 주석

  • 중첩이 심하고 장황한 함수라면 의미가 있을 수 있지만 일반적으로 지향해야할 작고 캡슐화된 함수에는 잡음일 뿐이다
  • 따라서 닫는 괄호에 주석을 사용하지말고 함수를 줄이려 시도하는 것이 마땅하다

Example Code

public static void main(String[] args) {
    try {
        while() {
            
        } // while
    } // try
    catch(Exception e) {
       
    } // catch
} // main

 

13. 주석으로 처리한 코드

  • 주석으로 처리된 코드는 다른 사람들이 삭제하길 주저한다. 이유가 있어 남겨 놓았을거라고, 중요하니까 지우면 안 된다고 생각한다. 이렇게 쓸모 없는 코드는 점차 쌓여간다.
  • "소스 코드 관리 시스템이 우리를 대신해 코드를 기억해준다. 그냥 코드를 삭제하라. 잃어버릴 염려는 없다. 약속한다."
  • 실무에서도 이와 같은 오래된 주석이 오랜 시간동안 남아있는 것을 봤다. 저자가 설명한 것처럼 뭔가 이유가 있어서, 중요하니까 남겨뒀겠지 하고 일단은 그대로 둔다. 가독성을 해치고, 잡음이 될 뿐인 코드는 과감히 삭제하자.

Example Code

public NicknameList readNicknameListV2() {

    List<ReadNicknameDto> readNicknameDto = memberService.findMembers().stream()
        .map(member -> new ReadNicknameDto(member.getNickname())).collect(Collectors.toList());
//    List<ReadNicknameDto> readNicknameDto = memberService.findMembers().stream()
//            .map(member -> new ReadNicknameDto(member.getNickname()))
//            .collect(Collectors.toList());

    return new NicknameList<List<ReadNicknameDto>>(readNicknameDto.size(), readNicknameDto);
}
  • 오랫동안 사용하지 않은 주석이라면 과감히 지우자

 

14. 전역 정보

  • 주석을 달아야 한다면 근처에 있는 코드만 기술해라. 코드 일부가 아닌 시스템 전반적인 정보를 기술하면 혼란만 일으킬 수 있다. 

Example Code

public static void main(String[] args) {
    try {
        while() {
            
        } // while
    } // try
    catch(Exception e) {
       
    } // catch
} // main

 

15. 너무 많은 정보

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

Example Code

/*
 고대 그리스의 수학자 에라토스테네스가 만들어 낸 소수를 찾는 방법. 
 이 방법은 마치 체로 치듯이 수를 걸러낸다고 하여 '에라토스테네스의 체'라고 부른다.
 */
public static void getPrimeNumbers(int maxValue) {
    ...
}

 

16. 모호한 정보

  • 당연한 얘기지만 주석은 설명하고자 하는 코드를 명확히 설명해야 한다. 그리고 설명하고자 하는 코드와 관계가 명백해야 한다.

Example Code(Apache common)

/*
 모든 픽셀을 담을 만큼 충분한 배열로 시작한다(여기에 필터 바이트를 더한다).
 그리고 헤더 정보를 위해 200바이트를 더한다.
 */
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
  • 위 코드에서 주석과 코드를 보면서 다음과 같은 의문점을 가질 수 있다
  • 필터 바이트란 무엇인지 알 수 없다. +1과 관련이 있는가? 아니면 *3과 관련이 있는가? 아니면 둘 다?
  • 한 픽셀이 한 바이트인가? 200을 추가하는 이유는?

 

 

 

❑ Reference

클린 코드 | 로버트 C.마틴 | 인사이트