← 블로그 목록

좋은 주석은 코드를 대신 설명하지 않고 의도와 계약을 남긴다

Google C++ Style Guide, PEP 8·257, Javadoc, Go 문서 주석 가이드를 바탕으로, 어떤 주석은 지우고 어떤 주석은 남겨야 하는지 정리한다.

좋은 주석은 코드를 대신 설명하지 않고 의도와 계약을 남긴다

좋은 주석은 코드를 대신 설명하지 않고 의도와 계약을 남긴다

코드 주석을 둘러싼 논쟁은 종종 “주석은 나쁘다”와 “주석은 꼭 필요하다” 사이에서 끝난다. 공식 스타일 가이드를 같이 보면 실제 결론은 더 단순하다. 이름과 구조로 드러나는 내용은 코드가 직접 말하게 하고, 코드만으로는 드러나지 않는 의도, 제약, 계약은 주석으로 남기라는 것이다.

Google C++ Style Guide는 가장 좋은 코드는 스스로 설명되며, 주석은 특히 왜 그렇게 했는지, 어떤 대안 대신 이 방식을 골랐는지, 어떤 부분이 까다로운지를 설명해야 한다고 말한다. Python의 PEP 8도 코드와 모순되는 주석은 없는 것보다 나쁘고, 한 줄짜리 주석은 눈에 보이는 동작을 반복하면 오히려 산만하다고 적는다. 즉 문제는 “주석을 다느냐”보다 무엇을 주석으로 남기느냐다.


먼저 구분해야 할 두 종류의 주석

주석은 크게 두 층으로 나누는 편이 이해가 쉽다.

구현 주석

함수 내부나 복잡한 로직 옆에 붙는 주석이다. 이런 주석은 보통 비즈니스 규칙, 성능·동시성 제약, 외부 시스템과의 호환성, 우회 구현 이유처럼 코드만으로 설명되지 않는 맥락을 남길 때 가치가 있다.

문서화 주석

문서화 문자열(docstring), 자바독(Javadoc), Go 문서 주석처럼 선언 바로 위에 붙어 API 문서로 추출되는 주석이다. PEP 257은 공개 모듈, 함수, 클래스, 메서드에 docstring을 두라고 하고, 함수나 메서드의 동작, 인수, 반환값, 부작용, 예외, 호출 제약을 문서화하라고 설명한다. Oracle의 Javadoc 문서도 선언 바로 앞의 주석만 문서화 대상으로 인식하며, 첫 문장은 그 대상을 요약하는 짧고 완결된 문장이어야 한다고 적는다. Go도 공개 이름마다 문서 주석이 있어야 하고, 패키지 주석은 패키지가 무엇을 위한 것인지 기대치를 먼저 세워야 한다고 안내한다.

이 구분을 놓치면 “주석은 왜를 설명해야 한다”는 말이 반만 맞게 된다. 함수 내부 주석은 왜와 제약을 더 많이 다루고, 공개 API 문서는 무엇을 보장하는지라는 계약을 먼저 분명하게 적는 편이 맞다.


지워도 되는 주석은 거의 늘 같은 모양이다

PEP 8은 한 줄 주석을 드물게 쓰라고 하고, 눈에 보이는 동작을 그대로 설명하는 주석은 불필요하고 산만하다고 예시까지 든다. Google C++ Style Guide도 코드가 무엇을 하는지를 그대로 반복하지 말고, 필요하면 코드를 더 읽기 쉽게 바꾸라고 권한다.

이 기준으로 보면 다음 같은 주석은 대개 지워도 된다.

x = x + 1  # x를 1 증가시킨다
items.sort() // 배열을 정렬한다
list.add(item); // 리스트에 아이템을 추가한다

이런 주석은 이해를 돕지 않는다. 오히려 “왜 증가시키는지”, “왜 여기서 정렬하는지”, “왜 지금 추가하는지” 같은 진짜 질문을 가린다.

비슷하게, 이름을 제대로 지어야 할 자리를 주석으로 메우는 것도 오래 못 간다. 예를 들어 x, tmp, flag 같은 이름 옆에 긴 주석이 달려 있다면, 먼저 이름과 구조를 고치는 편이 보통 더 낫다.


남겨야 하는 주석은 코드만으로 알 수 없는 사실을 담는다

반대로, 코드만 읽어서는 복원하기 어려운 정보는 주석이 유효하다. Google C++ Style Guide가 강조하는 것도 이런 부분이다. 구현이 까다롭거나, 특정 대안을 버리고 지금 방식을 택한 이유가 있거나, 락 범위·센티널 값·수명 규칙처럼 이름만으로 다 드러나지 않는 제약이 있으면 주석이 필요하다.

실무에서 특히 남길 가치가 큰 정보는 이런 쪽이다.

예를 들어 할인 코드라면 이렇게 고치는 편이 낫다.

DEFAULT_MEMBER_DISCOUNT_RATE = get_default_member_discount_rate()

def calculate_member_discount(price):
    # 현재 멤버십 정책상 기본 회원 할인은 세전 금액 기준의 기본 할인율을 사용한다.
    # 등급별 할인은 별도 프로모션 서비스에서 처리하므로 여기서는 섞지 않는다.
    return price * (1 - DEFAULT_MEMBER_DISCOUNT_RATE)

여기서 중요한 것은 기본 할인율을 적용한다는 사실 자체보다, 왜 그 할인율을 여기서 가져오는지, 세전 금액 기준인지, 왜 다른 할인 규칙을 여기서 처리하지 않는지다. 이런 정보가 나중의 변경 비용을 줄인다.


공개 API 주석은 “왜”보다 먼저 “무엇을 보장하는지”를 써야 한다

문서화 주석은 구현 주석과 역할이 조금 다르다. PEP 257은 함수나 메서드 docstring이 동작, 인수, 반환값, 부작용, 예외, 호출 제약을 설명해야 한다고 정리한다. Oracle의 Javadoc 문서도 첫 문장을 요약 문장으로 쓰고, 필요한 경우 @param, @throws 같은 태그로 계약을 분명하게 적으라고 안내한다.

즉 공개 함수 앞의 주석은 먼저 이런 질문에 답해야 한다.

예를 들면 이런 식이다.

def load_profile(user_id):
    """사용자 프로필을 읽어 온다.

    비활성 계정은 None을 반환한다.
    네트워크 오류가 발생하면 ProfileLoadError를 일으킨다.
    """

이 주석은 “데이터를 조회한다” 같은 눈에 보이는 동작을 되풀이하지 않으면서도, 호출자가 알아야 할 계약을 빠르게 전달한다.


나쁜 주석보다 더 위험한 것은 오래된 주석이다

PEP 8은 코드와 모순되는 주석은 없는 것보다 나쁘다고 못 박는다. 이 기준은 생각보다 중요하다. 오래된 주석은 개발자를 잘못된 확신으로 이끈다. 테스트가 없는 경우에는 더 그렇다.

예를 들어 주석은 “재시도 횟수는 제한돼 있다”는 취지인데 실제 코드는 다른 값으로 동작하고 있으면, 문제는 단순한 오타가 아니다. 운영 정책을 잘못 이해하게 만들고, 디버깅 방향까지 틀어지게 한다.

그래서 주석은 쓰는 것만큼 없애는 것도 중요하다. 코드 리팩터링을 했는데 주석이 그대로 남아 있다면, 그 주석은 정보 자산이 아니라 잠재 버그에 가깝다.


지금 기준으로 기억할 원칙은 다섯 가지면 충분하다

  1. 코드가 스스로 설명할 수 있는 내용은 주석으로 반복하지 않는다.
  2. 구현 주석은 , 제약, 우회 이유, 비정상 규칙을 남긴다.
  3. 공개 API 주석은 계약, 입출력, 예외, 부작용을 먼저 쓴다.
  4. 주석이 길어지는 이유가 나쁜 이름이나 복잡한 구조라면, 먼저 코드를 고친다.
  5. 코드와 어긋난 주석은 즉시 지우거나 고친다.

결국 좋은 주석의 기준은 화려한 문장이 아니다. 코드만 읽어서는 알 수 없는 사실을 정확하게 남겼는가에 가깝다. 주석이 코드를 대신 설명하려 들면 금방 낡고, 코드가 스스로 말하지 못하는 의도와 계약을 붙잡아 주면 오래 버틴다.

참고 자료

← 목록으로
Related

함께 읽으면 좋은 글

클린 코드프로그래밍소프트웨어 설계
좋은 프로그램은 빨리 끝나는 코드보다 읽히고 수정되는 코드에 가깝다

좋은 프로그램의 기준은 성능 하나로 정해지지 않는다. 이름, 함수 크기, 주석, 일관성, 리팩토링 가능성처럼 다른 사람이 읽고 고칠 수 있는 코드의 조건을 다시 정리한다.

BDD테스트요구사항
BDD를 테스트 문법으로만 이해하면 놓치게 되는 것

Dan North, Martin Fowler, Cucumber 문서를 기준으로, 행동 주도 개발이 단순한 '주어진 상황-행동-결과' 문법이 아니라 요구사항과 테스트를 같은 언어로 연결하려는 시도였다는 점을 다시 정리한다.

리팩토링기술 부채프로그래밍
게임 프로젝트에서 리팩토링이 생존을 결정한다

출시 직전에 터지는 버그, 기능 추가할 때마다 무너지는 구조. 게임 프로젝트에서 리팩토링을 미루면 어떤 일이 벌어지는지, 그리고 실전에서 리팩토링을 언제, 어떻게 해야 하는지를 경험을 바탕으로 정리한다.