핵심 전제
- 프로그래밍 언어는 완전한 자기 문서화(self-documenting)가 불가능하다
- 코드는 “어떻게(how)” 동작하는지를 표현하지만, “왜(why)” 그렇게 했는지는 표현하지 못한다
- 주석은 프로그래밍 언어의 한계를 돌파하는 수단 — 실행 코드 밖에서 맥락과 근거를 전달한다
- 주석은 코드의 실패를 보완하는 용도가 아니라, 코드로 전달할 수 없는 정보를 추가하는 용도다
주석을 달기 전에 먼저 코드 자체를 충분히 명확하게 만들어야 한다. 의도를 코드로 표현할 수 있다면 코드로 표현하라.
코드만으로는 부족한 이유
- 코드는 “진실의 단일 출처(single source of truth)“이지만, 논리의 **근거(reasoning)**까지 담지는 못한다
- 비즈니스 규칙, 물리 법칙, 데이터 분석가의 요구사항 등 외부 세계의 맥락은 코드에 녹이기 어렵다
- 변수명이나 포맷팅으로 일부 정보는 전달할 수 있으나, “왜 이 방식을 선택했는가”는 설명하기 어렵다
예시:
// 이진 트리를 쓴 이유: 연결 리스트보다 탐색 성능이 우수하기 때문
const dataStructure = new BinaryTree()
- 변수명을
binaryTreeBecauseLinkedListSlower처럼 만들 수는 없다 — 주석이 훨씬 자연스럽다 - 코드의 자기 문서화는 어느 지점까지만 가능하며, 그 이상은 오히려 가독성을 해친다
추상화와 주석의 관계
- 추상화(abstraction): 복잡한 내용을 숨기고 단일 개념/엔티티로 표현하는 것
- 코드는 세부 구현에 집중하지만, 추상화는 모호함을 전제로 한다
- 주석은 인터페이스와 구현 사이의 중간 계층 역할을 한다
| 계층 | 역할 |
|---|---|
| 인터페이스 | 기본 동작(연산)을 보여줌 |
| 주석 | 사용 방법, 한계, 능력을 설명 |
| 구현 코드 | 세밀한 작동 방식을 보여줌 |
- 모듈이나 클래스의 소스를 직접 읽지 않아도 무엇을 하는지 이해할 수 있어야 한다
- 주석은 추상화를 너무 추상적이지도, 너무 구체적이지도 않게 설명하는 도구다
주석의 두 가지 유형
모듈/클래스/함수 수준 주석 (High-level Comments)
- 모듈이나 함수가 전체적으로 무엇을 하는지 설명
- 독자가 구현을 열어보지 않아도 목적과 맥락을 파악할 수 있게 한다
- 함수 시그니처 바로 위에 작성하는 JSDoc 스타일이 대표적
/**
* Returns a copy of a deeply nested data structure.
* This is used to avoid modifications by reference.
*/
function makeDeepCopy(response) {
return JSON.parse(JSON.stringify(response))
}
인라인/구현 수준 주석 (Inline Comments)
- 실제 코드 라인 사이에 위치하는 저수준(low-level) 세부 설명
- 개발자들이 “주석 불필요론”을 말할 때 주로 가리키는 유형
- 리팩터링의 초대장으로 봐야 한다 — 인라인 주석이 필요하다면 함수 분리를 먼저 고려
인라인 주석: 사용 기준
리팩터링으로 대체 가능한 경우 (지양)
함수가 너무 길어서 논리적 파트를 주석으로 나누는 경우 → 함수를 분리하라
// 나쁜 예: 주석으로 논리 구역을 나눔
function createUserAccount(email, password) {
// 이메일 유효성 검사
...
// 사용자 엔티티 생성
...
// 계정 확인 이메일 발송
...
}
// 좋은 예: 함수 분리
function createUserAccount(email, password) {
validateEmail(email)
createUserEntity(email, password)
sendConfirmationEmail(email)
}
엣지 케이스를 인라인으로 처리하는 경우도 함수로 추출 가능
// 나쁜 예: 인라인 처리 + 주석
if (typeof IntersectionObserver === undefined) {
// Safari에서 IntersectionObserver 폴리필 필요
await import('intersection-observer')
}
// 좋은 예: 함수로 추출 + 함수 수준 주석
/**
* Return an instance of the IntersectionObserver API.
* If it's not supported in the current browser, the function imports a polyfill instead.
*/
async function initIntersectionObserver() {
if (typeof IntersectionObserver === undefined) {
await import('intersection-observer')
}
...
}
인라인 주석이 유효한 경우 (권장)
- 도메인 지식 전달: 비즈니스 용어, 특정 값의 의미 설명
// D&D 캐릭터는 숙련된 스킬에 보너스 수정치(+2)를 받는다
const modifier = baseModifier + (isProficient ? 2 : 0)
- 짧고 잘 명명된 함수는 추가 주석이 불필요하지만, 복잡하고 긴 함수는 인라인 주석이 가독성을 높인다
- 원칙: “what”과 “why”는 주석으로, “how”는 코드로 남긴다
어디에 명확성을 추가할지 판단하기
- 자신에게 명백한 것이 타인에게는 불명확할 수 있다
- 특정 로직에 오래 몰입한 개발자는 복잡한 부분을 단순하게 느껴 문서화를 건너뛰기 쉽다
- 코드 리뷰 활용: 두 명 이상이 같은 부분에 질문하면 주석이 필요하다는 신호
엣지 케이스는 해당 모듈에 익숙하지 않은 팀원에게는 전혀 명백하지 않을 수 있다.
실전 사례: Deep Copy 문제
상황
const data = JSON.parse(JSON.stringify(response))
- 동료가 작성한 이 코드 — 이유를 알 수 없어 혼란 발생
- 이유를 직접 물어봐야 했고, 이런 상황이 반복될 때마다 20분씩 낭비
원인과 해결
response는 깊게 중첩된 구조 → 여러 곳에서 참조로 사용됨 → 참조 변이(mutation by reference) 방지를 위해 깊은 복사 필요- 주석 한 줄이면 “텔레파시”와 같은 효과
// 참조에 의한 변경을 방지하기 위해 깊은 복사 수행
const data = JSON.parse(JSON.stringify(response))
더 나은 접근: 함수로 추출
/**
* Returns a copy of a deeply nested data structure.
* This is used to avoid modifications by reference.
*/
function makeDeepCopy(response) {
return JSON.parse(JSON.stringify(response))
}
const data = makeDeepCopy(response)
- 주석 없이 코드만 보면 누군가 “성능 최적화”를 위해
parse/stringify를 제거할 수 있다 → 숨겨진 버그 유발 - 주석은 시간 여행 역할: 작성자가 회사를 떠난 후에도 의도를 보존한다
주석 유지보수
- 주석도 코드와 함께 관리해야 하는 유지보수 대상이다
- 코드 변경 시 관련 주석도 함께 업데이트해야 한다
주석 업데이트가 필요한 상황:
- 버그 수정을 위해 함수를 비정상적인 방식으로 변경했을 때
- 드문 비즈니스 케이스를 처리하는 로직을 추가했을 때
- 기존 코드의 동작 방식이 변경되었을 때
구식 주석(out-of-sync comments)은 주석이 없는 것보다 더 해롭다. 없으면 도움을 찾겠지만, 틀린 정보는 잘못된 방향으로 인도한다.
핵심 원칙 요약
- 주석 과다 사용 → 유지보수 부담 증가, 코드와 불일치 위험
- 주석 전무 → 독자가 머릿속에 방대한 멘탈 모델을 유지해야 함
- 올바른 사용 → 코드로 표현할 수 없는 “왜”를 설명, 프로그래밍 언어의 한계를 돌파
| 표현 수단 | 담당 역할 |
|---|---|
| 코드 | “어떻게(how)” — 동작 방식 |
| 주석 | “왜(why)” — 선택의 근거, 도메인 맥락 |
| 함수명/타입 | “무엇을(what)” — 의도의 선언 |