소스 코드를 삭제하고 다른 언어로 다시 생성했습니다
요약
코드를 지식의 산출물로 보는 새로운 관점을 제시합니다. 비즈니스 규칙, 아키텍처 결정, 계약 등을 기록한 지식을 바탕으로 AI 에이전트가 코드를 재생성할 수 있음을 실험을 통해 증명합니다.
핵심 포인트
- 코드는 지식(계약, 결정, 가정)의 빌드 산출물로 진화하고 있음
- 문서화 대신 실행 가능한 계약과 비즈니스 규칙 기록의 중요성 강조
- AI 에이전트를 활용한 코드 재생성 실험을 통해 지식 기반 개발 가능성 확인
모든 팀은 결국 자신의 시스템에 대해 똑같은 말을 하게 됩니다. '이제는 왜 이렇게 작동하는지 아무도 모른다'라고 말이죠.
코드는 여전히 그 자리에 있습니다. 컴파일되고, 배포되며, 새벽 3시에도 트래픽을 처리합니다. 하지만 사라진 것은 그것을 만들어낸 지식입니다. 4년 전 누군가가 고객과 합의했던 규칙, 아키텍처 (Architecture) 결정을 강제했던 제약 조건, 결제 경로에 이상한 우회 방법 (workaround)이 존재하는 이유 같은 것들 말입니다. 그 지식은 사람 안에 머물렀고, 사람들이 떠날 때 함께 떠나버렸습니다.
남아 있는 것은 화석입니다. 결정 자체는 없이, 결정이 내려졌었다는 증거뿐인 것이죠.
우리가 이를 받아들였던 이유는, 최근까지도 코드가 실제로 실행될 수 있는 유일하고 지속적인 산물(artifact)이었기 때문입니다. 문서화 (Documentation)는 예의상의 문제였고, 명세서 (Specifications)는 건물이 세워지면 버려지는 비계 (scaffolding)와 같았습니다. 코드 이외의 모든 것은 그것이 진실인지에 의존하는 것이 없었기에 부패해갔습니다.
저는 최근까지도 상황이 여전히 그러한지 테스트하며 시간을 보냈습니다. 이 포스트는 작동하지 않았던 부분들을 포함하여 제가 발견한 내용입니다.
실험
저는 작은 커머스 시스템을 구축했습니다. 고객(customers), 기본 주소 불변성(default-address invariant)을 가진 주소록, 그리고 주소에 대한 참조 대신 주소의 복사본을 캡처하는 주문(orders) 시스템입니다. 특별할 것 없는, 실제 시스템을 채우는 종류의 비즈니스 로직 (business logic)입니다.
그런 다음 그 지식을 기록했습니다. 정확히 말하면 문서화 (Documentation)는 아니었습니다. ID와 관계를 가진 개별 파일 형태의 비즈니스 규칙 (Business rules), 탈락한 대안들과 그 이유를 포함한 아키텍처 결정 (Architecture decisions), 아무도 검증하지 않은 가정들, 그리고 HTTP 경계에서의 동작을 given/when/then 형식으로 기술한 계약 (contracts)입니다.
그 후 그 지식으로부터 두 가지 구현체 (implementations)를 생성했습니다. 하나는 TypeScript로, 하나는 Python으로 작성했습니다. 어느 쪽도 다른 쪽을 참고하여 작성되지 않았습니다.
두 구현체 모두 동일한 러너 (runner)에 의해 실행되는 동일한 17가지 계약 시나리오를 통과합니다:
============================================================
typescript http://127.0.0.1:54123
============================================================
...
직접 클론(clone)하여 node verify.mjs를 실행해 볼 수 있습니다. 의도적으로 의존성(dependencies)을 전혀 포함하지 않았으므로, 설치할 것도 없고 신뢰해야 할 것도 없습니다.
그런 다음 impl/python을 삭제하고, 그 지식을 유능한 코딩 에이전트(coding agent)에게 전달한 뒤, 코드가 다시 생성되어 돌아오는 과정을 지켜보십시오.
이 글이 실제로 주장하는 바
AI가 소프트웨어를 작성한다는 뜻이 아닙니다. 누구나 들어본 이야기이며, 대부분의 사람들은 그 사실에 실망해 왔습니다.
이 주장은 더 좁은 범위이며, 제 생각에는 더 유용합니다. 코드는 바이너리(binaries)가 코드의 빌드 산출물(build artifact)인 것과 같은 방식으로, 지식의 빌드 산출물이 되고 있습니다.
바이너리(binaries)는 컴파일러(compiler) + 테스트(tests)에 의해 검증된 코드(code)의 빌드 산출물(build artifact)입니다.
코드(code)는 계약(contracts) + 재생성(regeneration)에 의해 검증된 지식(knowledge)의 빌드 산출물(build artifact)입니다.
컴파일러 출력물을 검토하는 사람은 거의 없습니다. 아무도 바이너리를 저장하고 소스 코드를 삭제하지 않습니다. 우리는 소스 코드를 검토하고, 툴체인(toolchain)을 신뢰하며, 테스트로 검증합니다. 사다리의 칸이 이동한 것입니다.
이 비유는 불완전하며, 이를 즉시 언급할 가치가 있습니다. 컴파일러는 결정론적(deterministic)이고 의미론을 보존(semantics-preserving)하지만, 모델(model)은 둘 다 아닙니다. 바로 그렇기 때문에 여기서 오른쪽 열이 한 단계 아래의 열보다 더 중요합니다. 비유는 방향을 설정하고, 검증(verification)이 무게를 실어줍니다.
모두가 건너뛰는 부분
이 글에서 한 가지만 얻어간다면, 바로 이것입니다.
계약(contracts)은 생성된 코드가 아니라 지식(knowledge)이어야 합니다.
만약 테스트가 구현(implementation)과 동일한 프롬프트(prompt)에서 동시에 생성된다면, 그 테스트는 당신의 의도(intent)를 검증할 수 없습니다. 그것은 단지 모델이 자기 자신과 일관성을 유지했음을 확인할 뿐입니다. 그것이 아무것도 아니라는 뜻은 아니지만, 당신이 요청한 것을 검증하는 것은 아닙니다.
따라서 계약은 지식으로서 작성되고 검토되며, 지식으로서 버전 관리(versioned)되어야 하고, 인간이 의도적으로 변경할 때만 변경되어야 합니다. 재생성된 코드는 그것이 생성되기 _이전_에 존재했던 계약 세트(contract suite)를 반드시 충족해야 합니다.
두 가지 결과가 뒤따르며, 두 가지 모두 보이는 것보다 훨씬 중요합니다.
계약(Contracts)은 반드시 인터페이스 경계(interface boundary)에 위치해야 합니다: HTTP 호출, 명령줄 동작(command-line behaviour), 데이터 형식(data formats), 관찰 가능한 효과(observable effects) 등이 이에 해당합니다. 내부 클래스(internal classes)나 함수 이름(function names)이 되어서는 안 됩니다. 구현 내부로 침투하는 계약은 해당 구현이 재생성되는 것을 견딜 수 없으며, 다른 언어로 재생성되는 것은 말할 것도 없습니다. 제 데모에서 계약 실행기(contract runner)에는 TypeScript나 Python에 대한 언급이 전혀 포함되어 있지 않습니다. 그러한 제약 조건이야말로 스택 독립성(stack independence)을 단순한 슬로건이 아닌 하나의 속성(property)으로 만드는 요소입니다.
그리고 계약 세트(contract suite)는 오직 반증(falsify)할 수 있을 뿐, 결코 증명(prove)할 수는 없습니다. 이에 대해서는 아래에서 더 자세히 다루겠습니다. 왜냐하면 이것이 여기서 가장 중요한 제한 사항이기 때문입니다.
기존 방식과의 차이점
2025년은 업계가 사양(specifications) 중심으로 이동한 해였습니다. GitHub Spec Kit은 에이전트 워크플로우(agent workflows)에 구조를 부여했습니다. Amazon Kiro는 요구사항(requirements), 설계(design), 작업(tasks)을 중심으로 한 IDE를 구축했습니다. Tessl은 사양은 내구성이 있고 코드는 일회용(disposable)이라는 점을 가장 강력한 상업적 논거로 제시했습니다. OpenAI의 Sean Grove는
에서 이를 기억하기 쉽게 표현했습니다: 코드는 사양의 손실이 발생하는 투영(lossy projection)입니다.저는 그들 모두의 의견에 동의합니다. 다만 두 가지 구분이 필요하며, 이 둘은 서로 다른 구분입니다.
기능 범위 중심 도구(feature-scoped tools)에 반대하며. Spec Kit과 Kiro는 "AI로 다음 기능을 어떻게 만들 것인가"에 답합니다. 사양(spec)이 작성되고 구현을 주도하지만, 머지(merge) 이후에는 그것이 여전히 유효한지 아무도 보장하지 않습니다. 그것은 결함이 아니라, 해당 도구들의 목적 그 자체입니다.
제가 관심을 갖는 질문은 다릅니다: 코드가 저렴해진 시대에, 어떻게 10년 동안 시스템을 소유할 것인가? 이 질문은 기능 범위 중심의 관점이 생략할 수 있는 모든 것을 강제합니다. 단일 기능이 아닌 시스템 전체를 고려해야 합니다. 인수인계(hand-off)가 아닌 생애주기(lifecycle)를 고려해야 합니다. 드리프트 탐지(Drift detection), 부채 모델(debt model), 출처(Provenance), 그리고 브라운필드 코드(brownfield code)를 위한 경로까지 포함해야 합니다.
플랫폼에 반대하며. Tessl은 기능 범위(feature-scoped)를 한정하지 않으며, 그렇지 않은 척하는 것은 불공정할 것입니다. 여기서의 차이점은 범위가 아니라 소유권(ownership)입니다. 제가 설명하고 있는 것은 방법론(methodology)입니다. 즉, 개방형 스키마(open schema), 누구나 구현할 수 있는 컨벤션(conventions), 그리고 의존할 레지스트리(registry)가 없는 방식입니다. 오직 한 회사의 플랫폼 내부에서만 컴파일되는 지식은 제가 주장하는 스택 독립적 지식(stack-independent knowledge)이 아닙니다.
드리프트(Drift)가 진정한 적이다
이 접근 방식을 망가뜨리는 것은 잘못된 생성(generation)이 아닙니다. 그것은 코드에 직접 적용된 후 지식(knowledge)에 전혀 반영되지 않은 화요일 오후의 핫픽스(hotfix)입니다. 그런 일을 수십 번 반복하면 여러분의 지식 베이스(knowledge base)는 문서(documentation)가 항상 그래왔듯, '자신 있게 틀린 상태'가 되어버립니다.
따라서 드리프트(drift)는 방향성을 가지며, 두 방향은 서로 반대되는 의미를 갖습니다.
**코드보다 앞서가는 지식(Knowledge ahead of code)**은 정상입니다. 누군가 무언가를 결정했지만, 구현(implementation)이 아직 따라잡지 못한 상태입니다. 그것은 백로그(backlog)입니다.
**지식보다 앞서가는 코드(Code ahead of knowledge)**는 결함(defect)이며, 이를 감지하는 것은 생각보다 간단합니다. 왜냐하면 코드에 대한 이해가 필요 없기 때문입니다.
만약 변경 사항이 모듈의 생성된 경로(generated paths)를 건드렸지만, 해당 모듈의 지식에 상응하는 변경 사항이 포함되어 있지 않다면, 그것은 코드 우선 드리프트(code-ahead drift)입니다.
순수하게 구조적인 문제입니다. 소스(source)는 변하지 않았는데 빌드 아티팩트(build artifact)가 변한 것입니다. 저는 이를 머지(merge)를 차단하는 CI 체크(CI check)로 실행하고 있습니다.
비상 상황은 여전히 발생하며, 이를 금지하는 방법론은 첫 번째 장애 발생 시 무시되기 마련입니다. 그래서 탈출구(escape hatch)를 마련해 두었습니다. 핫픽스(hotfix)를 배포하되, 이를 이유 및 담당자와 함께 드리프트 부채(drift debt)로 기록하고, 조정(reconciliation) 작업을 수행하는 것입니다. 부채는 지표(metrics)에 계속 가시적으로 남아 있어야 합니다. 부채를 숨기는 탈출구는 서류 작업만 더해진 부패(rot)에 불과하기 때문입니다.
"우리도 해봤습니다. 그걸 MDA라고 불렀죠."
만약 당신이 20년 전에 시스템을 구축했다면 이미 이 말을 타이핑하고 있을 것이며, 당신의 말이 맞습니다.
제 답변의 솔직한 버전은 MDA의 생성기(generators)가 더 나빴다는 것이 아닙니다. 실행당 관점에서 볼 때, 그 결정론적 템플릿 엔진(deterministic template engines)들은 그 어떤 언어 모델(language model)보다 훨씬 예측 가능했습니다. 그렇지 않다고 주장하는 사람은 무언가를 팔려고 하는 사람입니다.
MDA의 실제 실패 원인은 변환(transformation)을 '구조적으로 올바른 것(correct by construction)'으로 취급했다는 점에 있었습니다. 생성기(generator) 외부에서 그 출력이 의도(intent)와 일치하는지 확인할 수 있는 장치가 아무것도 없었습니다. 모델과 생성된 시스템이 현실에서 벗어날 때, 이를 잡아낼 수 있는 것이 아무것도 없었습니다.
이러한 접근 방식은 생성기가 확률적(stochastic)이기 때문에 오류를 범할 수 있다는 점을 전제합니다. 그러한 가정 때문에 생성 전에 계약(contracts)을 작성하고, 계약이 생성된 산출물이 아닌 지식(knowledge)으로서 존재하며, 출처(provenance)를 기록하는 것입니다. 결정론(Determinism)은 생성의 안전성을 보장하는 요소가 아니었습니다. 독립적인 검증(Independent verification)이 그 역할을 합니다.
변하지 않은 사실은 하나입니다. 이 방식은 동작(behaviour)을 계약할 수 있는 곳에서 가장 잘 작동한다는 점입니다. 이는 MDA에서도 마찬가지였고, 지금도 마찬가지입니다.
내 데모로 이 테스트를 실행해 보았고, 실패했습니다
위의 두 구현체는 모두 동일한 세션에서 동일한 에이전트(agent)가 작성한 것이며, 이는 어떤 증거도 되지 못합니다. 이는 소프트웨어 버전으로 치면 자기 숙제를 스스로 채점하는 것과 같습니다. 그래서 저는 제대로 된 테스트를 실행했습니다.
저는 Python 구현체를 저장소(repository)에서 제외하고, 독립적인 에이전트에게 지식 파일(knowledge files)에만 접근 권한을 주었습니다. TypeScript도, 계약 실행기(contract runner)도, README도 없었습니다. 단 한 번의 시도였으며, 테스트 스위트(suite)를 먼저 실행해 볼 기회도 없었습니다.
결과는 17점 만점에 14점이었습니다.
모든 실패는 한 가지 이유에서 발생했습니다. 계약 실행기가 order.shippingAddress.line을 읽으려 했으나, 재생성된 구현체는 shippingAddress를 객체(object)가 아닌 단순 문자열(bare string)로 반환했습니다.
이것은 구현상의 실수가 아니었습니다. 제 지식(knowledge)에는 그것이 어떤 형태여야 하는지 명시되어 있지 않았습니다. 두 선택 모두 방어 가능하며, 에이전트는 하나를 선택했고, 계약은 조용히 다른 쪽을 가정했습니다.
점수를 매기기 전, 에이전트는 지식에서 답을 찾을 수 없는 18개의 질문을 나열했습니다. 가장 최악인 것은 실패 그 자체가 아니었습니다.
지식 어디에도 edit-address 엔드포인트(endpoint)가 없습니다. CT-010에는 "고객이 주소를 수정할 때..."라고 되어 있지만, 인터페이스 문서에는 POST, GET, DELETE만 나열되어 있습니다. 제가 하나를 만들어냈습니다.
하나의 계약(contract)이 제 인터페이스 문서에는 포함되지 않은 기능을 실행하고 있었습니다. 그 모순은 제가 문서를 작성한 이후 저장소(repository)에 그대로 남아 있었으며, 어떤 스키마 검증기(schema validator)도 이를 잡아낼 수 없었습니다. 왜냐하면 그것은 구조적인 문제가 아니라 의미론적 격차(semantic gap)였기 때문입니다. 오직 재생성(regeneration)만이 이를 찾아냈습니다.
또한, 제 글이 스스로 모순되는 부분도 찾아냈습니다. 한 규칙은 "항상 정확히 하나의 주소가 기본값입니다"라고 시작했으면서, 끝부분에서는 주소가 없는 경우를 설명하며 마무리되었습니다. 또 다른 규칙은 비밀번호가 없는 시스템임에도 불구하고 "잘못된 비밀번호와 동일한 일반 메시지"를 언급하고 있었습니다.
그래서 저는 코드가 아닌 지식(knowledge)을 수정했습니다. 계약에 맞추기 위해 구현(implementation)을 수정했다면, 다음 재생성 시에도 정확히 똑같은 방식으로 실패했을 것입니다. 그리고 저는 계약을 건드리지 않았습니다. 계약은 지식이며, 빌드를 통과시키기 위해 계약을 변경하는 것은 여기서 할 수 있는 가장 파괴적인 행동이기 때문입니다. 계약이 옳았습니다. 제 문서가 틀렸던 것입니다.
제가 이 이야기를 하는 이유는, 오직 성공하기만 하는 데모는 가치가 거의 없기 때문이며, 이것이 바로 제가 주장하는 바로 그 방법론을 증명하고 있기 때문입니다: 재생성은 소프트웨어를 생산하는 방법일 뿐만 아니라, 당신이 기록하는 데 실패했던 것이 무엇인지 찾아내는 방법이기도 합니다.
그 후 저는 동일한 에이전트(agent)를 수정된 지식으로 다시 보냈습니다. 17개 중 17개 모두 성공했습니다.
그 결과물은 단순한 재구성이 아닌 진정한 독립적 작업물이었습니다. 원본의 277줄에 비해 481줄에 달했고, 내부 구조가 달랐으며, 원본에는 없던 뮤텍스(mutex)도 포함되어 있었습니다. 심지어 수정된 규칙을 통해 기본 주소가 이미 나열된 작업들을 통해서만 변경된다는 점이 명확해지자, 첫 번째 라운드에서 스스로 만들어냈던 엔드포인트(endpoint)를 삭제하기까지 했습니다.
머릿속에 담을 수 있을 만큼 작은 시스템에서 이루어진 전체 루프는 다음과 같습니다: 재생성, 실패, 부채(debt) 발견, 지식 수정, 재생성, 통과.
그리고 두 번째 라운드에서 한 가지를 더 찾아냈는데, 이것이 제가 발견한 것들 중 가장 마음에 드는 결과입니다:
POST /reset및GET /health가 지식(knowledge)에서 완전히 누락되어 있습니다. 지식만으로 재생성된 세 번째 구현체에는 이들이 포함되지 않을 것이며, 점수를 매길 수 없는(unscoreable) 상태가 될 것입니다.
저는 도메인(domain)을 주의 깊게 문서화했습니다. 하지만 제가 기록하지 않은 것은 _테스트(tests)_에 필요한 스캐폴딩(scaffolding)이었습니다. 왜냐하면 그것은 테스트 하네스(test harness)를 작성한 사람, 즉 제 머릿속에만 존재했기 때문입니다. 이것이 바로 지식 재생성(knowledge regeneration) 과정에서 손실되는 지식의 종류이며, 정확히 아무도 기록할 생각을 하지 않는 종류의 지식입니다. 이제 그것은 하나의 파일이 되었습니다.
이 글에서 다른 것은 아무것도 얻지 못하더라도 훔쳐갈 가치가 있는 한 가지 기술은 다음과 같습니다: 점수를 매기기 전에, 재생성하는 에이전트(agent)에게 무엇을 추측해야 했는지 물어보십시오. 그 추측들은 당신의 테스트가 그것들을 포착하든 못하든 간에 당신의 지식 부채(knowledge debt)가 됩니다. 저의 경우 첫 번째 시도에서 18개를 나열했습니다. 그중 3개는 계약을 놓치게 만들었습니다. 나머지 15개는 단지 아직 실행되지 않았을 뿐인 실제 공백(gaps)이었습니다.
처음에 내가 틀렸던 부분
저는 이것의 첫 번째 버전을 문서화 연습으로 작성했습니다. 지식을 기록하고, 구조화하고, 그것으로부터 생성하는 것입니다. 그 초안의 단어 수를 세어보니: "knowledge" 91회, "agent" 4회였으며, 그 4회 중 2회는 다른 사람들의 제품에 관한 것이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기