명세로부터 코드를 생성한다는 주장에 숨은 두 가지 오해를 짚고, OpenAI Symphony 사례를 통해 명세가 결국 코드처럼 될 수밖에 없고 그마저도 불안정하며, 서둘러 만든 명세는 ‘슬롭’이 되기 쉽다는 점을 논한다.
이 글은 본질적으로 이 만화를 장문의 글로 확장한 것입니다:

오랫동안 저는 지금부터 쓰려는 이런 글이 필요하지 않았습니다. 누군가 명세로부터 코드를 생성한다는 아이디어를 꺼내면, 저는 위 이미지를 보여 주곤 했고 보통은 그걸로 충분했습니다.
하지만 에이전틱 코딩 옹호자들은 중력을 거스르는 방법을 찾아내서 명세 문서만으로 코드를 생성할 수 있다고 주장합니다. 게다가 그들은 논점을 충분히 흐려 놓았기 때문에, 저는 위 만화가 왜 그들의 주장이 오해를 부르는지에 대한 추가 코멘터리를 받을 만하다고 믿게 되었습니다.
제 경험상 그들의 옹호는 두 가지 흔한 오해에 뿌리를 두고 있습니다:
그들은 에이전틱 코딩을, 에이전틱 코딩을 차세대 아웃소싱으로 생각하는 신봉자들에게 마케팅할 때 이 오해에 기대고 있습니다. 그들은 엔지니어가 명세 문서를 작성하는 매니저로 바뀌고, 그 명세를 에이전트 팀에 넘겨 일을 시키는 모습을 꿈꿉니다. 그런데 이게 성립하려면 일을 하는 것보다 일을 명세하는 것이 더 싸야 합니다.
그들은 에이전틱 코딩이 유지보수 불가능한 쓰레기를 만들어낼까 걱정하는 회의론자들에게 마케팅할 때 이 오해에 기대고 있습니다. 명세 문서를 거치게 하면 품질이 좋아지고 더 나은 엔지니어링 관행을 촉진한다는 주장입니다.
구체적인 예시를 통해 왜 이것들이 오해라고 믿는지 풀어 보겠습니다.
OpenAI가 명세 문서로부터 프로젝트를 생성하는 예시로 내세우는 OpenAI의 Symphony 프로젝트에서 시작하겠습니다.
Symphony 프로젝트는 “명세”( SPEC.md )에서 생성되었다고 주장하는 에이전트 오케스트레이터입니다. 제가 따옴표를 친 이유는 이 파일이 명세라기보다는 마크다운 형태의 의사코드에 더 가깝기 때문입니다. 문서의 겉을 조금만 긁어 봐도 데이터베이스 스키마를 산문으로 쏟아 넣은 것 같은 내용이 나옵니다:
4.1.6 Live Session (Agent Session Metadata)
State tracked while a coding-agent subprocess is running.
Fields:
session_id(string,<thread_id>-<turn_id>)thread_id(string)turn_id(string)codex_app_server_pid(string or null)last_codex_event(string/enum or null)last_codex_timestamp(timestamp or null)last_codex_message(summarized payload)codex_input_tokens(integer)codex_output_tokens(integer)codex_total_tokens(integer)last_reported_input_tokens(integer)last_reported_output_tokens(integer)last_reported_total_tokens(integer)turn_count(integer)
- Number of coding-agent turns started within the current worker lifetime.
… 또는 코드의 산문 덤프 같은 내용도 있습니다:
8.3 Concurrency Control
Global limit:
available_slots = max(max_concurrent_agents - running_count, 0)Per-state limit:
max_concurrent_agents_by_state[state]if present (state key normalized)otherwise fallback to global limit
The runtime counts issues by their current tracked state in the
runningmap.8.4 Retry and Backoff
Retry entry creation:
Cancel any existing retry timer for the same issue.
Store
attempt,identifier,error,due_at_ms, and new timer handle.Backoff formula:
Normal continuation retries after a clean worker exit use a short fixed delay of
1000ms.Failure-driven retries use
delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms).Power is capped by the configured max retry backoff (default
300000/ 5m).Retry handling behavior:
Fetch active candidate issues (not all issues).
Find the specific issue by
issue_id.If not found, release claim.
If found and still candidate-eligible: * Dispatch if slots are available. * Otherwise requeue with error
no available orchestrator slots.If found but no longer active, release claim.
… 혹은 이런 것처럼 모델의 코드 생성을 돌봐 주려고 명시적으로 추가된 섹션도 있고:
6.4 Config Fields Summary (Cheat Sheet)
This section is intentionally redundant so a coding agent can implement the config layer quickly.
tracker.kind: string, required, currentlylineartracker.endpoint: string, defaulthttps://api.linear.app/graphqlwhentracker.kind=linear- …
… 혹은 아예 코드1 그 자체도 있습니다:
- Reference Algorithms (Language-Agnostic)
16.1 Service Startup
function start_service(): configure_logging() start_observability_outputs() start_workflow_watch(on_change=reload_and_reapply_workflow) state = { poll_interval_ms: get_config_poll_interval_ms(), max_concurrent_agents: get_config_max_concurrent_agents(), running: {}, claimed: set(), retry_attempts: {}, completed: set(), codex_totals: {input_tokens: 0, output_tokens: 0, total_tokens: 0, seconds_running: 0}, codex_rate_limits: null } validation = validate_dispatch_config() if validation is not ok: log_validation_error(validation) fail_startup(validation) startup_terminal_workspace_cleanup() schedule_tick(delay_ms=0) event_loop(state)
명세 문서가 코드(혹은 어떤 경우에는 문자 그대로 코드)처럼 읽히는데도, 에이전틱 코딩 옹호자들이 이것을 코드의 대체물로 마케팅하는 건 꽤나 불성실하다고 느낍니다.
오해하지는 마세요. 저는 명세 문서에 의사코드나 레퍼런스 구현이 절대 포함되면 안 된다고 말하는 것이 아닙니다. 둘 다 명세 작업에서 꽤 흔합니다. 하지만 명세 문서가 코드 대체물이라고 주장하려면, 그 문서가 코드처럼 읽히지 않아야 합니다.
제가 이 이야기를 꺼내는 이유는 Symphony가 첫 번째 오해를 잘 보여 준다고 믿기 때문입니다:
오해 1: 명세 문서는 대응하는 코드보다 더 단순하다
작동하는 구현을 신뢰성 있게 생성할 수 있을 만큼 명세 문서를 정밀하게 만들려고 하면, 당신은 필연적으로 그 문서를 코드로(혹은 코드와 강하게 닮은 무언가로) 비틀어야만 합니다(예: 매우 구조화되고 형식적인 영어).
We know in the meantime that the choice of an interface is not just a division of (a fixed amount of) labour, because the work involved in co-operating and communicating across the interface has to be added. We know in the meantime —from sobering experience, I may add— that a change of interface can easily increase at both sides of the fence the amount of work to be done (even drastically so). Hence the increased preference for what are now called "narrow interfaces". Therefore, although changing to communication between machine and man conducted in the latter's native tongue would greatly increase the machine's burden, we have to challenge the assumption that this would simplify man's life.
A short look at the history of mathematics shows how justified this challenge is. Greek mathematics got stuck because it remained a verbal, pictorial activity, Moslem "algebra", after a timid attempt at symbolism, died when it returned to the rhetoric style, and the modern civilized world could only emerge —for better or for worse— when Western Europe could free itself from the fetters of medieval scholasticism —a vain attempt at verbal precision!— thanks to the carefully, or at least consciously designed formal symbolisms that we owe to people like Vieta, Descartes, Leibniz, and (later) Boole.
에이전틱 코더들은 엔지니어링 노동이 요구하는 “좁은 인터페이스”(읽자면: 코드)에서 벗어날 수 없다는 사실을 어렵게 배우는 중입니다. 그들이 할 수 있는 건 그 노동을 겉보기에는 다른 무언가로 변환하는 것뿐인데, 그 무언가 역시 같은 정밀함을 요구합니다.
게다가 명세로부터 코드를 생성하는 것은 애초에 신뢰성 있게 작동하지도 않습니다! 저는 실제로 Symphony README가 제안하는 대로 해 보았습니다:
Tell your favorite coding agent to build Symphony in a programming language of your choice:
Implement Symphony according to the following spec: https://github.com/openai/symphony/blob/main/SPEC.md
저는 Claude Code에게 제가 선택한 프로그래밍 언어로 Symphony를 만들라고 시켰습니다(제 블로그 이름에서 짐작하셨겠지만 Haskell2입니다). 그리고 작동하지 않았습니다. 결과는 제 Gabriella439/symphony-haskell 저장소에서 확인할 수 있습니다.
버그가 여러 개 있었을 뿐 아니라(Claude에게 고치라고 프롬프트해야 했고 그 수정은 커밋 히스토리에서 볼 수 있습니다), 심지어 “작동하는” 것처럼 보일 때(즉: 에러 메시지가 없을 때)조차 codex 에이전트는 아무 말 없이 빙빙 돌기만 하면서 다음 샘플 Linear 티켓에 대해 아무런 진전도 내지 못했습니다:
Create a new blank repository
No need to create a GitHub project. Just create a blank
gitrepository
즉, Symphony의 “언어적 정밀성에 대한 헛된 시도”(Dijkstra의 표현을 빌리자면)는 여전히 작동하는 구현을 신뢰성 있게 생성하지 못합니다3.
이 문제는 Symphony에만 국한되지도 않습니다. YAML 같은 유명한 명세에서도 같은 문제가 보입니다. YAML 명세는 극도로 상세하고 널리 쓰이며 적합성 테스트 스위트까지 포함하고 있지만, YAML 구현의 대다수는 여전히 명세를 완전히 준수하지 못합니다.
Symphony는 명세를 더 확장해서 불안정함을 고치려 할 수도 있지만, 명세는 이미 꽤 길고 포함된 Elixir 구현 크기의 1/6에 달합니다! 명세가 더 커지면 Borges의 단편 「정확성에 관하여(On Exactitude in Science)」를 다시 쓰는 셈이 될 겁니다:
…In that Empire, the Art of Cartography attained such Perfection that the map of a single Province occupied the entirety of a City, and the map of the Empire, the entirety of a Province. In time, those Unconscionable Maps no longer satisfied, and the Cartographers Guilds struck a Map of the Empire whose size was that of the Empire, and which coincided point for point with it. The following Generations, who were not so fond of the Study of Cartography as their Forebears had been, saw that that vast Map was Useless, and not without some Pitilessness was it, that they delivered it up to the Inclemencies of Sun and Winters. In the Deserts of the West, still today, there are Tattered Ruins of that Map, inhabited by Animals and Beggars; in all the Land there is no other Relic of the Disciplines of Geography.
명세 작업은 코딩보다 더 어려워야 합니다. 보통 우리가 일을 시작하기 전에 명세 문서를 작성하는 이유는, 프로젝트를 사색적이고 비판적인 렌즈로 보게 만들기 위해서입니다. 코딩이 시작되면 우리는 기어를 바꾸고 행동 편향에 이끌리게 되기 때문입니다.
그렇다면 왜 저는 이것이 오해라고 말할까요:
오해 2: 명세 작업은 코딩 작업보다 더 사려 깊어야 한다
문제는, 기술 기업에서 노동을 줄이고 가치를 깎아내리려는 산업 전반의 압박 때문에 이런 종류의 사려 깊음이 더 이상 당연한 것이 아니게 되었다는 점입니다. “사람들에게 명세 작업은 코딩보다 쉬워야 한다고 말해 왔다”는 전제로 시작하면, 실패를 자초하는 셈입니다. 전달 속도를 최적화하면 명세 작성이 요구하는 어렵고 불편한 일을 해낼 방법이 없습니다. 그렇게 해서 겉보기에는 명세 문서처럼 보이지만, 자세히 들여다보면 무너져 내리는 Symphony 같은 “명세”가 나옵니다.
사실 Symphony 명세는 AI가 쓴 슬롭처럼 읽힙니다. 섹션 10.5는 제가 말하는 슬롭의 특히 심각한 예인데, 예컨대 이런 발췌가 있습니다:
linear_graphqlextension contract:
Purpose: execute a raw GraphQL query or mutation against Linear using Symphony's configured tracker auth for the current session.
Availability: only meaningful when
tracker.kind == "linear"and valid Linear auth is configured.Preferred input shape:
{ "query": "single GraphQL query or mutation document", "variables": { "optional": "graphql variables object" } }
querymust be a non-empty string.
querymust contain exactly one GraphQL operation.
variablesis optional and, when present, must be a JSON object.Implementations may additionally accept a raw GraphQL query string as shorthand input.
Execute one GraphQL operation per tool call.
If the provided document contains multiple operations, reject the tool call as invalid input.
operationNameselection is intentionally out of scope for this extension.Reuse the configured Linear endpoint and auth from the active Symphony workflow/runtime config; do not require the coding agent to read raw tokens from disk.
Tool result semantics:
- transport success + no top-level GraphQL
errors->success=true- top-level GraphQL
errorspresent ->success=false, but preserve the GraphQL response body for debugging- invalid input, missing auth, or transport failure ->
success=falsewith an error payloadReturn the GraphQL response or error payload as structured tool output that the model can inspect in-session.
이건 “명세처럼 생긴” 문장들을 마구 주워 담은 것에 불과합니다. 에이전트의 산출물처럼 읽히죠. 일관성도 목적도 없고 큰 그림에 대한 이해도 없습니다.
이런 명세 문서는 사람이 썼더라도 필연적으로 슬롭일 수밖에 없습니다. 명료함이나 선명함이 아니라 납기 시간을 최적화하고 있기 때문입니다. 지금의 엔지니어링 환경에서는 명세가 신중한 사고와 숙고의 산물이라는 것을 더 이상 당연하게 여길 수 없습니다.
명세는 원래 시간을 절약하기 위한 장치가 아니었습니다. 납기 시간을 최적화하고 있다면, 중간 단계로 명세 문서를 거치는 것보다 아예 코드를 바로 작성하는 편이 더 나을 가능성이 큽니다.
더 일반적으로, 여기에는 “garbage in, garbage out” 원칙이 적용됩니다. 명료함과 디테일이 부족한 문서를 입력하면, 코딩 에이전트가 그 부족한 명료함과 디테일을 신뢰성 있게 채워 줄 것이라고 기대할 수 있는 세계는 없습니다. 코딩 에이전트는 독심술사가 아니며, 설령 독심술사라 해도 당신의 생각이 혼란스러우면 할 수 있는 일이 많지 않습니다.
코드 스니펫에 하이라이트가 없는 이유가 궁금하다면, 제가 원본 문서에서 찾은 그대로 GitHub-flavored markdown을 정확히 보존하고 있기 때문입니다. 모든 코드 스니펫이 text로 명시적으로 주석 처리되어 있습니다(이 문서가 AI 생성임을 보여 주는 많은 징후 중 하나입니다). 사실 이것은 모델이 요청의 정신이 아니라 글자 그대로만 따르는 사례일 가능성이 큽니다. 제 생각에 무슨 일이 있었냐면, 사람이 AI에게 프로젝트의 초기 초안을 산문 명세로 바꾸라고 요청했고, AI가 코드 스니펫에 text 라벨을 붙이면 코드보다 산문에 더 가까워진다고 판단한 것 같습니다. ↩
사람들은 종종 저에게 “Haskell보다 더 주류 언어로 코드를 생성하면 더 좋은 결과를 얻을 것”이라고 말합니다. 이에 대한 제 답은 이렇습니다. 에이전트가 Haskell 코드를 생성하는 데 어려움을 겪는다면, 그건 에이전트가 훈련 데이터 바깥으로 신뢰성 있게 일반화할 능력이 없다는 뜻입니다. ↩
제가 뭘 잘못 쥐고 있다고 생각한다면, 같은 연습을 여러분도 직접 해 볼 수 있습니다. ↩
Copyright © 2026 Gabriella Gonzalez. This work is licensed under CC BY-SA 4.0