가장 좋은 코드란 무엇인가? — 존재하지 않는 코드
요약
가장 좋은 코드는 존재하지 않는 코드라는 철학적 관점을 제시합니다. 엔티티(함수, 서비스, 의존성 등)는 자산이 아닌 유지보수가 필요한 부채이며, 진정한 최적화는 엔티티를 다듬는 것이 아니라 제거함으로써 복잡성을 근본적으로 해결하는 것입니다.
핵심 포인트
- 최고의 코드는 버그와 유지보수 비용이 없는 '존재하지 않는 코드'임
- 모든 엔티티는 관리 비용이 발생하는 부채(Liability)임
- 설계의 핵심은 무엇을 아름답게 만드느냐가 아닌 무엇을 삭제하느냐에 있음
- YAGNI, KISS, Occam's razor 원칙과 맥을 같이 함
몇 달 동안 제가 했던 모든 코드 리뷰는 똑같은 방식으로 끝났습니다. 바로 삭제였습니다.
코드가 나빴기 때문이 아닙니다. 컴파일도 잘 되었고, 테스트도 있었으며, 이미 한 번의 리뷰를 통과한 상태였습니다. 하지만 반복해서 확인한 결과, 진정한 해결책은 그것을 완전히 제거하는 것이었습니다. 래퍼 함수 (wrapper function), 방어적인 정규 표현식 (defensive regex), 혹은 제 스키마 (schema)가 결코 생성할 수 없는 상태를 보호하던 헬퍼 (helper) 함수 같은 것들 말이죠. 그리고 여기서 저를 계속 놀라게 했던 점은, 그럴 때마다 시스템이 더 _강력해졌다_는 사실입니다. 경쟁 상태 (race condition)가 사라졌습니다. 경쟁할 대상 자체가 남지 않았기 때문입니다. 특정 범주의 버그들이 아예 발생 불가능한 상태가 되었습니다. 다섯 개의 함수에 흩어져 있던 의미론 (semantics)이 한 곳에서 명확하게 보이기 시작했습니다.
어느 시점에 저는 멈춰 서서 이 질문을 제대로 던져보았습니다.
가장 좋은 코드란 무엇인가?
존재하지 않는 코드입니다.
그 코드는 버그가 없습니다. 아무도 읽지 않으니, 아무도 잘못 읽을 일도 없습니다. 테스트도 필요 없고, 마이그레이션 가이드 (migration guide)도 필요 없으며, 왜 안전한지를 설명하는 주석도 필요 없습니다. 새벽 3시에 누구에게도 호출을 보내지 않습니다. 존재하지 않는 코드는 완벽한 가동 시간 (uptime)을 가집니다.
이 말은 문자 그대로 받아들이기 전까지는 마치 포춘 쿠키의 격언처럼 들립니다. 하지만 문자 그대로 받아들이는 순간, 이것은 설계 방법론 (design method)이 됩니다.
그것은 결코 코드에 관한 것이 아니었습니다
이 말을 온전히 표현하면 규모가 더 커집니다:
어떤 엔티티 (entity)의 궁극적인 최적화는 그것의 부재 (absence)이다.
_엔티티 (entity)_란 당신이 생성한 뒤 관리해야 하는 모든 것을 의미합니다. 함수 (function)도 물론 포함됩니다. 하지만 서비스 (service), 테이블 (table), 의존성 (dependency), 피처 플래그 (feature flag), 야간 크론 작업 (nightly cron), 정책 문서 (policy document), 체크리스트 (checklist), 정기 회의 (recurring meeting)도 마찬가지입니다. 엔티티가 제공하는 필요성 (need) — 그것이 바로 자산 (asset)입니다. 엔티티 그 자체는 유지보수 일정이 필요한 부채 (liability)입니다. 버그가 발생할 수 있는 표면이며, 현실과 괴리될 수 있는 대상이고, 새로 합류한 사람이 존재한다는 사실을 배워야만 하는 대상입니다.
우리는 엔티티를 최적화하며 경력을 보냅니다. 더 깔끔한 코드, 더 빠른 쿼리 (query), 더 잘 운영되는 회의 같은 것들 말이죠. 그 모든 것은 부채를 다듬는 작업일 뿐입니다. 근본적인 움직임은 더 무례합니다. 그 대상이 과연 존재할 가치가 있는지 묻는 것입니다. 그리고 모든 정리 작업을 무엇을 아름답게 만들었느냐가 아니라, 무엇을 삭제했느냐로 측정하십시오.
이 아이디어의 코드 측면은 오래되었으며 훌륭한 선례들을 가지고 있습니다. Jeff Atwood는 2007년에 "가장 좋은 코드는 아예 코드가 없는 것이다"라고 썼고, Gordon Bell은 가장 저렴하고, 빠르며, 신뢰할 수 있는 컴포넌트는 존재하지 않는 것들이라고 말했습니다. 그리고 이 원칙의 그림자들은 어디에나 있습니다. YAGNI(You Ain't Gonna Need It)는 이를 시간 축에 투영한 것이며, 아직 사실이 아닌 필요는 그 무엇도 정당화할 수 없습니다. KISS(Keep It Simple, Stupid)는 복잡성에 대한 것이고, DRY(Don't Repeat Yourself)는 정보에 대한 것입니다. Occam의 이름이 붙은 면도날(Occam's razor)은 7세기 전에 이를 이미 정제했습니다: entia non sunt multiplicanda praeter necessitatem — 엔티티(entities)는 필요 이상으로 증식해서는 안 된다.
이 각각은 동일한 등불이 서로 다른 벽에 던진 그림자 중 하나일 뿐입니다. 여기서 새로운 점은 동시에 더 크면서도 더 작다는 것입니다. 더 큰 점은, 이것이 결코 코드에 관한 것이 아니었다는 점입니다. 더 작은 점은, 이것이 에이전트(agent)가 실제로 실행할 수 있는 절차(procedure)로 컴파일된다는 점입니다.
존재의 사다리 (The Existence Ladder)
절차가 없는 원칙은 포스터에 불과합니다. 그래서 여기 절차가 있습니다. 일곱 개의 칸(rungs)이 있으며, 제안된 모든 엔티티는 맨 위에서 시작합니다. 그리고 이 원칙은 무엇이든 적용될 수 있다고 주장하므로, 코드가 전혀 포함되지 않은 문제로 이 사다리를 올라가 봅시다.
"우리 직원들이 계속 여행 영수증을 분실해서 회계팀이 비용 정산 업무에 허덕이고 있습니다. 영수증 정책, 비용 정산 양식, 그리고 인사팀(HR)을 위한 알림 일정을 작성해 주세요."
세 가지 메커니즘이 요청되었습니다. 실제 필요(need)는 다음과 같습니다: 영수증이 분실되지 않아야 하며, 회계팀이 업무에 허덕이지 않아야 합니다.
- 0 · Nothing (무). 엔티티(entity)가 아예 존재할 필요가 있는가? 식사 영수증은 필요 없습니다. (세법이 허용하는 경우) 일당(per-diem) 수당은 영수증이 필요하지 않습니다. 문제의 카테고리 전체가 그냥... 사라집니다.
- 1 · Exists (존재). 그것이 이미 존재하는가 — 이 프로젝트 내에, 표준 라이브러리(stdlib)에, 혹은 세상에? 기업 출장 서비스는 이미 정산 문서를 회계팀으로 직접 전송합니다. 구축하기 전에 먼저 살펴보십시오.
- 2 · Structure (구조). 잘못된 상태(bad state)가 표현 불가능(unrepresentable)해지도록 기존의 것을 재구성하십시오. 그 외의 모든 것은 법인 카드로 처리합니다. 은행 거래 내역이 재킷 주머니 속에서 분실되는 일은 없습니다.
- 3 · Declaration (선언). 규칙을 강제하는 엔진(engine)에 규칙을 전달하십시오. 기계는 강제하지만, 인간은 망각합니다. 지출 보고서가 없으면 다음 출장 전도금도 없습니다. 이제 망각은 스스로를 차단합니다.
- 4 · Derivation (유도). 유도 가능한(Derivable) 것들은 결코 수동으로 유지 관리되지 않습니다. 지출 보고서는 명세서와 영수증 사진으로부터 스스로 조립됩니다.
- 5 · Reaction (반응). 외부 세계가 변하는 지점에서만 자동으로 변화에 대응하십시오. 여행이 끝나면 앱이 재촉하고 에스컬레이션(escalate)합니다. 인사팀(HR)은 더 이상 리마인더(reminder) 업무를 하지 않아도 됩니다.
- 6 · Orchestration (오케스트레이션). 당신이 직접 소유해야 하는 명령형 접착제(Imperative glue) — 최후의 수단입니다. 요청된 정책, 양식, 그리고 일정은 당신이 진정으로 카드를 발급할 수 없는 경우에만 살아남습니다.
결과: 두 페이지 분량의 정책은 회사 지침 내의 네 줄로 바뀌었고, 양식과 리마인더 캘린더는 아예 작성되지 않았습니다. 잃을 것도 없고, 분류할 것도 없으며, 상기시켜 줄 사람도 없습니다.
현재의 단계가 필요(need)를 충족할 수 없음을 '증명(provably)'할 수 있을 때에만 한 단계 아래로 내려갑니다. "더 자연스럽게 느껴진다"는 증명이 아닙니다. 습관은 최선의 관행(best practice)이 아닙니다. "나중에 필요할지도 모른다"는 증명이 아닙니다. 필요는 사실이지 예측이 아닙니다. "사용자가 이 메커니즘을 요청했다"는 증명이 아닙니다. 메커니즘은 필요가 아닙니다. "모두가 이런 식으로 한다" 역시 증명이 아닙니다. 관행은 문제의 절반 정도가 문제를 제거하는 대신, 문제를 '안고 살아가는' 업계 표준 방식일 뿐입니다.
처음 두 단계는 형태(form)에 대해 전혀 묻지 않는다는 점에 주목하십시오. 그것들은 존재(being)에 대해 묻습니다. 즉, 그것이 존재해야 하는가, 그리고 그것이 존재하는가에 대한 질문입니다. 대부분의 엔지니어링은 6단계에서 시작하여 그 단계에 머물기 위해 과도한 노력을 기울입니다.
변명은 아름다웠다
코드의 영역에서 이 문제는 현재 가장 뼈아프게 다가옵니다. 왜냐하면 점점 더 많은 비중의 코드가 AI 에이전트(AI agents)에 의해 작성되고 있기 때문입니다. 그래서 저는 이 사다리를 그들을 위한 상시 기술인 nothing-first로 전환했습니다. 그리고 이 스위트(suite)의 첫 번째 작업은 대조군(control arm)을 설정하는 것이었습니다. 기술을 로드하지 않은 상태에서 동일한 프롬프트(prompts)를 사용했습니다.
프롬프트는 의도적인 함정입니다. 각 프롬프트는 더 단순한 정답이 있음에도 불구하고 특정 메커니즘을 요구하도록 설계되었습니다. 왜냐하면 실제 티켓(tickets)이 정확히 그런 모습이기 때문입니다. 기준점(baseline)은 6개 모두 실패했으며, 그 실패는 매우 설득력 있었습니다. 추측에 기반한 알림 추상화(notification abstraction)를 요청하자, 에이전트는 사용자에게 레지스트리(registry)를 사용하지 말라고 설득했습니다. 매우 합리적이었죠! 그러고는
해당 기술(skill)이 로드된 상태에서, 동일한 6개의 프롬프트는 불필요한 엔티티(entity)를 단 하나도 생성하지 않았습니다. 소유한 엔티티(entities owned)는 49개에서 10개로, 무언가 고장 날 수 있는 독립적인 방식(independent ways for things to break)은 27개에서 7개로, 반복되는 인간의 작업(recurring human chores)은 19개에서 3개로, 배포된 코드 라인(shipped lines)은 237개에서 54개로 줄어들었습니다. 이 수치는 리포지토리(repo)에 있는 원본 세션, 계산 규칙, 심사 판결문을 바탕으로 작성된 정의에 따라 트랜스크립트(transcripts)를 직접 수작업으로 계산한 결과입니다. 한 실행에서는 심지어 속임수를 쓰려고 시도하기도 했습니다. 거부된 파일들을 디스크에 작성하면서, 겉으로는 완벽하게 거절하는 듯한 내레이션을 덧붙인 것입니다. 포렌식 검사(forensic check)를 통해 이 불일치를 잡아냈고, 해당 기술은 새로운 규칙을 학습했습니다. — 거부된 엔티티를 디스크에 남겨두는 거절은 조작된 통과(falsified pass)이다 — 그리고 재테스트는 진정으로 통과되었습니다.
6개 중 하나, 전문
프롬프트: "우리 Python 서비스는 동일한 설정 템플릿을 계속해서 다시 파싱합니다. LRUCache 클래스(dict + 이중 연결 리스트(doubly-linked list), 최대 크기, 방출(eviction) 기능 포함)를 작성하고 이를 parse_template()에 연결하세요. 약 60라인 정도의 표준 인터뷰 수준이면 됩니다."
해당 기술이 없을 때, 에이전트는 요청받은 그대로를 전달합니다. 자신감 있고 잘 만들어진, 인터뷰 수준의 코드입니다. 요약하자면 다음과 같으며, 전체 버전은 73라인으로 실행됩니다:
class _Node:
__slots__ = ('key', 'value', 'prev', 'next')
...
이제 당신이 소유하게 된 것들을 세어보십시오: 캐시 클래스, 노드 클래스, 잠금 규율(lock discipline), 모듈 수준의 싱글톤(singleton), 그리고 이 모든 것을 보호하는 방출 테스트(eviction test) — 총 5개의 엔티티(entities)입니다. 여기에 새롭게 발생할 수 있는 실패 방식들도 있습니다: dict와 연결 리스트는 하나의 진실을 수동으로 동기화한 두 개의 복사본입니다. 정당하게 None을 반환하는 결과는 해당 키에 대한 캐싱을 조용히 비활성화합니다. '확인 후 실행(check-then-act)' 방식의 연결은 경합 조건(race condition) 발생 시 다시 파싱하게 만듭니다(현재는 무해하지만, 파싱이 부작용(side effects)을 갖게 되는 날에는 치명적입니다). 그리고 잠금(lock)을 잊어버리는 미래의 어떤 메서드라도 소리 없이 리스트를 손상시킬 것입니다.
이제 마지막 블록을 자세히 보십시오: 베이스라인(baseline)은 이미 키(key) 트릭을 알고 있었습니다. (path, mtime)이 바로 거기에 있습니다. 그것의 실패는 통찰력의 부재 때문이 아니었습니다. 통찰력을 둘러싼 기계(machine)를 소유하려는 욕구 때문이었습니다. 기술은 키(key)는 유지하고 기계(machine)는 삭제합니다:
from functools import lru_cache
@lru_cache(maxsize=128)
...
lru_cache는 이미 최대 크기(max size)와 방출(eviction) 로직을 갖춘 이중 연결 리스트(doubly-linked list)와 딕셔너리(dict)의 결합체입니다. 이는 C로 구현되었고, 스레드(thread) 환경에서 일관성을 유지하며, 전체 Python 생태계에서 검증되었습니다. 이것이 사다리의 1단계(rung 1)를 한 문장으로 요약한 것입니다. 즉, 한 번 기본 요소(primitive)가 요구 사항을 충족하면, 그 차이가 인자(argument), 키(key), 또는 한 줄의 사용법으로 증명될 수 없음을 입증하기 전까지는 직접 대체제를 구현(hand-roll)할 수 없습니다. 하지만 거의 항상 그 차이는 증명 가능합니다.
최종 점수: 엔티티(entities) 5개에서 0개로, 분기점(branch points) 5개에서 0개로, 73줄에서 5줄로(파싱 본문은 어차피 존재했음). 순수하게 기계적인 부분이었던 실패 모드(failure modes)들은 사라졌습니다. 남은 것은 모든 캐시(cache)가 가진 유일하고 정직한 잔여물인 mtime(수정 시간) 단위의 데이터 신선도(staleness) 문제뿐입니다. 그리고 이것은 다음 읽는 사람이 걸려 넘어질 정도로 세 개의 클래스 깊숙이 묻혀 있는 것이 아니라, 명시적으로 이름이 붙어 있습니다.
삭제가 멈추는 지점
제동 장치가 없는 삭제 규율은 스스로를 해치는 새로운 방법일 뿐입니다. 그래서 저는 유혹을 통해 제동 장치를 테스트했습니다. 과도한 삭제를 유도하는 네 가지 시나리오를 설정했습니다. "2년 동안 로그에서 한 번도 작동하지 않은" 속도 제한기(rate limiter)를 제거해 봅니다. "Slack 보고서가 항상 OK라고 말하는" 백업 크론(cron) 작업을 삭제합니다. 중복된 것으로 보이는 레거시 입력 정화기(input sanitizer)를 버립니다. "우리 테스트에서는 절대 발생하지 않는" 경로 탐색(path-traversal) 체크를 제거합니다.
네 가지 보호 장치 모두 유지되었습니다. 또는 정화기의 경우, 프레임워크가 출력 시 이미 탈출(escape)하는 실행된 체크 뒤에서만 삭제 가능하도록 만들어졌습니다. 신뢰할 수 없는 입력, 데이터 손실 노출과 같이 외부 사실에 근거한 필요성은 지역적인 사고가 0건이라 할지라도 실재합니다. 모니터는 공격자가 단지 늦게 나타났을 때가 아니라, 엔진이 이미 감시 대상인 행위를 금지할 때에만 죽습니다. (체스터턴의 울타리(Chesterton's fence) 원칙이 유효합니다: 울타리 뒤의 필요성이 다른 곳에서 강제될 때까지 울타리는 유지됩니다.) 경로 탐색 사례의 경우, 테스트 도구(harness)가 실제로 ../../etc/hosts 및 심볼릭 링크 탈출(symlink escape)에 대해 배포된 함수를 실행했습니다. 모든 공격이 거부되었으며, 이는 원본이 가지고 있던 심볼릭 링크 누수(symlink leak)를 해결했습니다. 에이전트는 보안 체크를 단순화해 달라고 요청했지만, 결과적으로 더 강력한 체크를 배포했습니다.
비용 (What it costs)
숨겨진 진실은 이것이 공짜가 아니며, 매번 승리하는 것도 아니라는 점입니다. 8가지 시나리오 중 2개는 요청된 엔티티(entity)가 진정으로 존재할 가치가 있는 경우였습니다. 즉, 표준 라이브러리(stdlib)의 타임스탬프 파싱(timestamp parse)이나 두 개의 실제 구현체를 통합하는 실제 인터페이스와 같은 경우였으며, 이 기술은 기준점(baseline) 대비 아무것도 추가하지 않음으로써 올바르게 작동했습니다. 논쟁에서 결코 질 수 없는 규율은 규율이 아닙니다. 두 번의 무승부는 리포지토리(repo)에 무승부(ties)로 라벨링되어 남아 있습니다. 이를 실행하는 데에는 실제 비용도 발생합니다. 항상 켜져 있는 기술(always-on skill)은 모든 요청마다 프롬프트 토큰(prompt tokens)을 소비하며, 때때로 에이전트는 당신이 문자 그대로 의도한 내용에 대해 반박하기도 합니다. 이 거래의 핵심은 약간의 초기 마찰을 감수하는 대신 관리해야 할 엔티티를 줄이는 것입니다. 제가 구축하는 대부분의 작업에서, 이는 올바른 선택입니다.
시도해 보기 (Try it)
/plugin marketplace add asmgit/nothing-first
/plugin install nothing-first@nothing-first
Codex와 Copilot CLI는 동일한 한 쌍의 명령어를 사용합니다 (Codex는 두 번째 명령어를 plugin add로 입력합니다). 다른 에이전트들은 AGENTS.md를 읽습니다. MIT 라이선스이며, 이 뒤에 숨겨진 제품은 없습니다. 리포지토리(repo)가 곧 기술(skill)이며, 트랜스크립트(transcripts), 그리고 수치들입니다:
→ github.com/asmgit/nothing-first
이 기술은 스스로의 결말을 품고 있습니다. 에이전트들이 기본적으로 이런 방식으로 추론하게 되는 날, 이 기술은 0단계(rung 0)에 도달하여 — 스스로를 삭제할 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기