Clean Code Is Not the Same as Clear Code: Comments Were Never the Problem
Clean Code가 곧 Clear Code는 아닙니다 — 문제는 주석이 아니었습니다
깨끗한 코드가 항상 코드의 의도와 배경까지 설명하지는 않습니다. 글은 정규식의 의미, 47이라는 제한값의 이유처럼 코드에 담기 어려운 정보를 주석으로 남겨야 하며, 코드가 명백한 부분에는 주석을 쓰지 말자고 주장합니다.
- 주제
AI 요약
‘주석을 쓴다는 건 좋은 코드를 만들지 못했다는 뜻’이라는 관념을 반박하는 글입니다. 모든 함수가 이름과 구조만으로 의도를 설명할 수는 없습니다. 코드가 복잡해서 읽기 어려운 경우도 있고, 코드는 간단하고 잘 작성됐지만 그 선택의 이유가 코드 어디에도 드러나지 않는 경우도 있습니다. 글에서는 산길의 급커브 표지판을 예로 들며, 표지판이 있다는 사실이 도로 설계의 실패를 뜻하지 않듯 주석도 코드의 실패를 보완하는 장치가 될 수 있다고 설명합니다.
코드가 명확해도 의미까지 드러나지는 않습니다
예시로 제시한 C# 코드는 FlightNumbers.IsValid 메서드와 source-generated regex를 사용해 항공편 번호 형식을 검사합니다. 이름과 구현은 깔끔하고 별도의 리팩터링도 필요하지 않습니다. 하지만 정규식에 익숙하지 않으면 다음 입력 중 무엇이 유효한지 바로 알기 어렵습니다.
BA123U212349W5A99123
정규식은 앞부분에 대문자 두 개, 대문자와 숫자 조합, 숫자와 대문자 조합 중 하나를 요구합니다. 이어서 숫자 1~4개가 오고 마지막에는 대문자 하나가 선택적으로 붙습니다. 따라서 BA123, U21234, 9W5A는 통과하지만 99123은 처음 두 글자에 문자가 없어 실패합니다. 코드는 정확하지만 이 규칙이 IATA 항공편 번호 형식이라는 사실까지 알려주지는 않습니다.
여기에 “IATA flight number”라는 맥락과 항공사 코드의 형태, 숫자 길이, 선택적 접미사, 공백 제거와 대문자 정규화 규칙을 짧게 적으면 독자는 문서와 테스트를 찾아다니지 않고도 바로 이해할 수 있습니다. 이때 주석은 코드가 무엇을 하는지 반복하지 않고, 코드가 표현하지 못한 도메인 규칙을 전달합니다.
코드에는 선택의 이유가 남지 않습니다
두 번째 예시는 private const int MaxConcurrentRequests = 47;입니다. 이름도 명확하고 매직 넘버도 상수로 분리했지만, 47이라는 숫자가 왜 필요한지는 코드만 보고 알 수 없습니다. 외부 제공자의 문서에는 초당 요청 제한이 50으로 적혀 있으므로 누군가는 47을 실수로 보고 50으로 바꿀 수 있습니다. 그러면 실제 트래픽에서 제공자가 요청을 간헐적으로 거부하지만, 원인을 재현하기 어려워집니다.
주석은 이 선택의 배경을 기록합니다. 제공자는 50 requests per second를 문서화했지만 실제 limiter는 1.2초 구간의 burst를 측정하고, 50으로 설정하면 재시도가 제한을 넘깁니다. 그래서 47을 유지하며 제공자가 새 제한을 발표하면 다시 확인해야 한다는 내용입니다. 코드에는 현재 선택인 47만 남고, 주석에는 그 선택을 만든 관찰과 재검토 조건이 남습니다. 이유가 사라지면 방어적인 설정이 버그처럼 보입니다.
글에서 제시한 유용한 주석의 역할은 네 가지입니다.
1. 정규식, bit trick, 수식처럼 구현이 완벽해도 ‘무엇을 뜻하는지’ 읽기 어려운 코드를 설명합니다. 2. 이상하거나 임의로 보이는 선택의 이유를 기록합니다. 3. 코드를 바꿀 때 무엇이 얼마나 깨지는지 알려줍니다. 4. 임시 결정의 만료 조건을 남깁니다. 무엇을 시도했는지, 무엇이 실패했는지, 언제 다시 검토할지를 적습니다.
이 네 가지에 해당하지 않는 주석은 삭제 대상에 가깝습니다. 반대로 이 정보를 전달하는 주석을 clean code라는 이유만으로 지우면 정리가 아니라 정보 삭제가 됩니다.
Git history만으로는 현재 상태의 이유를 찾기 어렵습니다
주석 대신 좋은 commit message를 남기면 된다는 반론도 다룹니다. 하지만 오래된 파일의 git log에는 apply editorconfig, fix, fix again, PR feedback, final fix, final fix (actually) 같은 메시지가 쌓이기 쉽습니다. 47이라는 값이 왜 생겼는지 그 안에 있더라도, 어떤 커밋을 찾아야 하는지부터 알기 어렵습니다.
commit message는 해당 커밋에서 무엇이 바뀌었고 왜 바꿨는지를 설명하는 데는 적합합니다. 반면 현재 코드가 왜 지금과 같은 상태인지 설명하려면 여러 커밋을 순서대로 재구성해야 합니다. 포맷 변경, 이름 변경, 파일 분리, squash merge가 이어지면 원래 이유는 관련 없는 변경 사이에 묻힙니다. 작성자였던 사람이 퇴사하면 질문할 대상도 사라집니다.
특히 개발자는 이유가 있다는 사실을 모르면 그 이유를 찾으러 가지 않습니다. 47은 수수께끼보다 오타처럼 보입니다. 코드 바로 위의 주석은 값을 50으로 바꾸기 전에 독자를 멈춰 세우지만, Git history는 이미 의심을 품고 검색을 시작한 뒤에야 답을 제공합니다. 주석은 현재 코드를 보고 있는 자리에서 현재 상태를 설명합니다.
주석도 코드와 같은 변경 대상입니다
“코드를 바꾸면 주석도 함께 고쳐야 한다”는 반론에 대해서는, 그 작업이 특별한 부담이 아니라고 말합니다. 메서드 동작을 바꾸면 테스트를 수정하고, 매개변수 이름을 바꾸면 호출부를 수정합니다. 해당 코드의 의미를 설명하는 주석을 바꾸는 일도 같은 종류의 작업입니다. 주석이 별도 위키나 오래된 Confluence 문서가 아니라 수정하는 코드 바로 위에 있다면 함께 갱신하면 됩니다.
이 논리는 DRY 원칙과도 닮았습니다. 중복을 없애려고 비슷한 코드 두 부분을 하나의 함수로 합치면, 시간이 지나면서 서로 다른 요구사항을 처리하기 위한 매개변수와 플래그가 늘어날 수 있습니다. 결국 ProcessOrder(order, true, false, null, customer, true, 3, "legacy", false, skipValidation: true)처럼 호출부만 보고 동작을 알기 어려운 함수가 만들어집니다. 중복은 항상 나쁜 것이 아니며, 추상화도 항상 좋은 것이 아닙니다. 읽기 쉬운 두 구현을 유지하는 편이 복잡한 공통 함수보다 나을 때가 있습니다.
“주석을 절대 쓰지 말라”와 “모든 줄에 주석을 달라”는 모두 체크리스트에 불과합니다. 주석도 상황에 따라 선택해야 합니다.
나쁜 주석은 무엇이 문제인가
글은 주석을 모든 줄에 달자는 주장도 분명히 거부합니다. retryCount++ 위에 “재시도 횟수를 증가시킨다”고 쓰는 주석은 코드와 같은 내용을 반복할 뿐입니다. .NET의 XML documentation 주석에서 메서드가 사용자를 가져오고 매개변수가 id라는 사실만 다시 적는 방식도 정보가 없습니다.
MaxRetries = 5 위에 “최대 3번 재시도”라고 쓰는 주석은 코드와 모순됩니다. “임시 우회책이니 나중에 삭제”라는 TODO도 티켓, 버전, 날짜, 삭제 조건이 없으면 영구적으로 남습니다. 주석 처리한 옛 코드 역시 다시 사용할 것이라는 보장이 없으므로 삭제하는 편이 낫습니다. 긴 메서드 위에 구현을 세 문단으로 설명해야 한다면 먼저 리팩터링을 시도하고, 리팩터링 뒤에도 남는 복잡성만 주석으로 설명해야 합니다.
글의 실용적인 기준은 한 문장입니다. “한 줄을 쓰기 전에 멈춰서 생각해야 했다면, 무엇을 생각했는지 적습니다.” 코드가 명백했다면 반복 설명을 남기지 않습니다. 반대로 “잠깐, 여기서는 조심해야 한다”고 생각한 순간이 있었다면 다음 독자도 같은 지점에서 멈출 가능성이 높으므로 그 이유를 주석에 기록합니다. clean code가 ‘무엇을 했는지’를 보여준다면, 좋은 주석은 작성자가 ‘무엇을 알고 있었는지’를 보존합니다.
dev.to 반응
- @unitbuilds —
^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$를 배우고 싶은 사람들을 위한 분석입니다.[A-Z]{2}|는 문자 2개,[A-Z]\d|는 문자와 숫자,\d[A-Z]는 숫자와 문자입니다.()는 그룹을 뜻합니다.([A-Z]{2}|[A-Z]\d|\d[A-Z])가 첫 번째 그룹이고,(\d{1,4})가 두 번째 그룹이며 1~4개의 숫자입니다.([A-Z]?)가 세 번째 그룹이고?는 선택 사항을 뜻하므로 문자가 선택적으로 붙습니다. 따라서 첫 두 자리는 문자 2개, 문자와 숫자, 숫자와 문자 중 하나입니다.BA123은 문자 2개와 숫자 3개라 통과합니다.U21234는 문자와 숫자 뒤에 숫자 4개가 와서 통과합니다.9W5A는 숫자와 문자, 숫자 1개, 문자라서 통과합니다.99123은 첫 두 자리에 문자가 없어 첫 번째 그룹에서 실패합니다. 이제 정규식을 이해했습니다 😁- @georgekobaidze — “‘이제 정규식을 이해했습니다’라고 말한 사람은 역사상 아무도 없을 것 같습니다” 😄
- @unitbuilds — 😂 99%의 경우에는 이런 것만 알면 정말 충분합니다. 와일드카드와 anchor가 나오는 마지막 1%가 문제이고, 보통 사람들이 그 부분에서 실수합니다.
- @georgekobaidze — LLM이 처음 등장했을 때 제 첫 반응 중 하나는 “좋아, 이제 누가 정규식을 쓰고 읽는지 알겠군. 나는 아니야”였습니다 😄
- @technogamerz — 정말 정말 좋은 글입니다!
- @georgekobaidze — 좋게 읽어주셔서 감사합니다! 🙏 원래는 다른 내용을 쓸 예정이었지만 이 글을 너무 오래 미뤄왔습니다. 더 기다리는 건 맞지 않다고 느껴져서 드디어 올렸습니다. 🙂
- @mikachu — 또 해냈습니다! clean code와 주석의 논쟁을 이렇게 명확하게 다시 구성한 글이라 좋았습니다. 사람들이 계속 놓치는 실제 구분을 정확히 짚었다고 생각합니다. 특히 47과 50의 예시는 계속 기억에 남을 것 같습니다. 이 글이 “모든 곳에 주석을 달자”는 주장으로 흐르지 않은 점도 좋았습니다. “멈춰서 생각했다면 생각한 내용을 적는다”는 기준은 그럴듯한 원칙이 아니라 매일 실제로 쓸 수 있는 규칙처럼 느껴집니다. 좋은 글 감사합니다. 정말 즐겁게 읽었습니다.
- @mariobermonti — 유머가 좋았습니다. 광고 속 버거와 상자 안의 실제 버거 이야기를 보고 소리 내 웃었습니다. 🤣
원문: dev.to / 번역·요약: Trawling