충분히 상세한 스펙은 곧 코드다
게시일: 2026년 3월 31일 | 원문 작성일: 2026년 3월 17일 | 저자: Gabriella Gonzalez | 원문 보기
핵심 요약
스펙 문서에서 코드를 생성할 수 있다는 에이전틱 코딩의 주장을 OpenAI Symphony 프로젝트를 사례로 비판합니다.
- 정밀한 스펙 = 코드 — 스펙의 정밀도를 높이면 결국 코드(또는 의사코드)로 수렴해요. Symphony의 SPEC.md가 이를 증명해요.
- 스펙 → 코드 변환은 불안정 — 저자가 직접 시도했지만 작동 안 했어요. YAML처럼 유명한 스펙조차 완전한 구현을 보장 못 해요.
- ”쉬운 코딩” 마케팅이 슬롭을 양산 — 스펙 작성을 코딩보다 쉽다고 포장하면, 깊이 없는 “스펙처럼 생긴” 문서만 나와요.
• • •
이 글은 본질적으로 이 커밋스트립 만화4를 글 한 편 분량으로 풀어낸 거예요.
오랫동안 이런 글을 쓸 필요가 없었어요. 누군가 “스펙에서 코드를 생성할 수 있다”는 얘기를 꺼내면, 위 만화를 보여주는 것만으로 충분했거든요.
그런데 에이전틱 코딩 옹호자들이 중력을 거스르는 방법을 찾았다고 나서기 시작했어요. 스펙 문서만으로 코드를 뚝딱 만들어낼 수 있다는 거예요. 게다가 물을 워낙 흐려놓은 탓에, 위 만화만으로는 부족하고 왜 그들의 주장이 오해의 소지가 있는지 좀 더 풀어서 설명할 필요가 생겼어요.
제 경험상 이런 주장은 두 가지 흔한 오해에서 비롯돼요.
오해 1: 스펙 문서는 대응하는 코드보다 단순하다
에이전틱 코딩을 “차세대 아웃소싱”이라고 믿는 신봉자들한테 먹히는 논리예요. 엔지니어가 관리자로 변신해서, 스펙 문서만 써주면 에이전트 팀이 알아서 일해주는 꿈을 꾸는 거죠. 이건 일을 명세하는 게 일을 직접 하는 것보다 저렴할 때만 성립해요.
오해 2: 스펙 작업은 코딩 작업보다 더 신중해야 한다
”에이전틱 코딩이 유지보수 불가능한 쓰레기를 양산할 것”이라고 걱정하는 회의론자들한테 먹히는 논리예요. 스펙 문서를 한 단계 거치면 품질이 올라가고, 더 나은 엔지니어링 관행이 자리 잡는다는 거죠.
구체적인 사례로 왜 이것들이 오해인지 보여드릴게요.
코드를 살짝 가린 것뿐
OpenAI의 Symphony5 프로젝트부터 볼게요. OpenAI가 “스펙 문서에서 프로젝트를 생성하는 법”의 모범 사례로 내세우는 프로젝트예요.
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 running map.
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:
16. 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: 스펙 문서는 대응하는 코드보다 단순하다
스펙 문서의 정밀도를 작동하는 구현을 안정적으로 뽑아낼 수 있는 수준까지 끌어올리면, 결국 그 문서는 코드로 수렴해요. 아니면 코드와 거의 다를 바 없는 것(극도로 구조화되고 형식적인 영어 같은)이 되거나요.
우리는 이제 인터페이스를 선택하는 것이 단순히 (정해진 양의) 노동을 나누는 게 아니라는 걸 알아요. 인터페이스를 사이에 두고 협업하고 소통하는 데 드는 비용이 추가되니까요. 우리는 이제 — 쓰라린 경험에서, 덧붙이자면 — 인터페이스를 바꾸면 양쪽 다 할 일이 쉽게 (그것도 급격히) 늘어날 수 있다는 것을 알아요. 그래서 이른바 “좁은 인터페이스”에 대한 선호가 커진 거예요. 따라서, 기계와 인간 사이의 소통을 인간의 모국어로 바꾸면 기계의 부담이 크게 늘겠지만, 그것이 인간의 삶을 단순하게 해줄 거라는 가정에는 의문을 제기해야 해요.
수학의 역사를 잠깐 돌아보면 이 의문이 얼마나 정당한지 알 수 있어요. 그리스 수학은 말과 그림에 머물렀기 때문에 정체되었고, 이슬람 “대수학”은 기호화를 소심하게 시도했다가 수사적 양식으로 되돌아가면서 죽었어요. 현대 문명 세계는 — 좋든 나쁘든 — 서유럽이 중세 스콜라주의의 족쇄, 즉 언어적 정밀성을 향한 헛된 시도에서 벗어났을 때에야 비로소 등장할 수 있었어요. 비에타, 데카르트, 라이프니츠, 그리고 (훗날) 불에 같은 사람들 덕분에 세심하게, 혹은 최소한 의식적으로 설계된 형식적 기호 체계가 그것을 가능하게 했어요.
에이전틱 코딩 옹호자들은 엔지니어링 노동이 요구하는 “좁은 인터페이스”(즉, 코드)에서 벗어날 수 없다는 걸 뼈저리게 배우고 있어요. 겉모습만 바꿀 수 있을 뿐, 요구되는 정밀도는 똑같거든요.
불안정성
게다가, 스펙에서 코드를 생성하는 건 안정적으로 작동하지도 않아요! 실제로 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에 제가 좋아하는 언어(제 블로그 이름에서 짐작하셨겠지만 Haskell27)로 Symphony를 만들어 달라고 했는데, 작동하지 않았어요. 결과물은 Gabriella439/symphony-haskell 저장소에서 확인할 수 있어요.
버그가 여러 개 있었을 뿐 아니라(Claude한테 수정을 시켜야 했고, 커밋 기록에서 그 수정 내역을 볼 수 있어요), “작동하는” 상태(에러 메시지가 안 뜬다는 의미)에서도 codex 에이전트는 아래의 아주 간단한 Linear 티켓을 두고 아무 진전 없이 빙빙 돌기만 했어요:
Create a new blank repository
No need to create a GitHub project. Just create a blank git repository
다시 말해, Symphony의 “언어적 정밀성을 향한 헛된 시도”(다익스트라의 표현을 빌리면)는 여전히 작동하는 구현을 안정적으로 만들어내지 못해요3.
이 문제는 Symphony만의 것이 아니에요. YAML처럼 유명한 스펙에서도 똑같은 현상이 벌어져요. YAML 스펙은 극도로 상세하고, 널리 쓰이며, 적합성 테스트 스위트까지 갖추고 있지만, 대다수의 YAML 구현체가 여전히 스펙을 온전히 따르지 못해요.
Symphony가 불안정성을 잡으려고 스펙을 더 늘릴 수도 있겠지만, 이미 꽤 길어서 함께 포함된 Elixir 구현의 1/6 크기에 달해요! 스펙이 더 커진다면 보르헤스8의 단편 “학문의 엄밀함에 대하여”를 재현하게 될 거예요:
…그 제국에서 지도 제작술은 극도의 완벽함에 이르러, 한 지방의 지도가 도시 하나를 통째로 차지했고, 제국의 지도는 지방 하나를 통째로 차지했다. 시간이 흐르자 그 터무니없는 지도들로는 부족해져서, 지도 제작자 길드는 제국과 크기가 같고 지점 대 지점으로 일치하는 지도를 만들었다. 선대만큼 지도학에 열중하지 않았던 후대 사람들은 그 거대한 지도를 쓸모없다고 여겼고, 약간의 무자비함과 함께 태양과 겨울의 가혹함에 내맡겨 버렸다. 서쪽 사막에는 지금도 그 지도의 너덜너덜한 잔해가 남아 있어, 동물들과 거지들이 그 안에 살고 있다. 온 나라 어디에도 지리학이라는 학문의 다른 유물은 없다.
슬롭
스펙 작업은 원래 코딩보다 더 어려운 거예요. 작업 전에 스펙 문서를 쓰는 이유는 프로젝트를 한 발 떨어져서 비판적으로 바라보게 만들기 위해서예요. 일단 코딩이 시작되면 “일단 해보자” 모드로 기어가 바뀌니까요.
그렇다면 왜 이것이 오해라고 하냐면요:
오해 2: 스펙 작업은 코딩 작업보다 더 신중해야 한다
문제는 이런 신중함을 더 이상 기대할 수 없게 됐다는 거예요. 테크 기업들이 앞다퉈 노동을 줄이고 평가절하하는 업계 분위기 때문이에요. “스펙 작업은 코딩보다 쉬워야 한다”는 전제에서 출발하면, 실패는 예정된 거예요. 납품 속도를 최적화하면서 스펙 작성이 요구하는 고되고 불편한 사고를 해낼 방법은 없어요. 그래서 겉으로는 스펙처럼 보이지만 자세히 보면 와르르 무너지는 Symphony의 “스펙” 같은 게 나오는 거예요.
사실 Symphony 스펙은 AI가 뱉어낸 슬롭 그 자체예요. 섹션 10.5가 특히 심한 예시인데, 이런 부분이 있어요:
linear_graphql extension 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 payload
- transport success + no top-level GraphQL
- Return the GraphQL response or error payload as structured tool output that the model can inspect in-session.
”스펙처럼 생긴” 문장들을 잡동사니로 모아놓은 것에 불과해요. 일관성도 없고, 목적도 없고, 큰 그림에 대한 이해도 없어요. 전형적인 에이전트 산출물이에요.
이런 스펙 문서는 인간이 작성했더라도 필연적으로 슬롭일 수밖에 없어요. 일관성이나 명확성이 아닌 납품 시간에 맞춰 최적화하고 있으니까요. 지금의 엔지니어링 분위기에서 스펙이 신중한 사고와 숙고의 결과물이라고 가정하는 건 순진한 거예요.
• • •
결론
스펙은 원래 시간을 아끼려고 만드는 게 아니에요. 납품 속도를 최적화하는 중이라면, 중간에 스펙 문서를 끼우는 것보다 코드를 직접 짜는 게 나을 가능성이 높아요.
더 넓게 보면, “쓰레기가 들어가면 쓰레기가 나온다”는 원칙이 여기에도 그대로 적용돼요. 명확성과 구체성이 빠진 문서를 넣고 코딩 에이전트가 알아서 그 빈칸을 채워주길 바라는 건 몽상이에요. 코딩 에이전트는 독심술사가 아니에요. 설령 독심술사라 해도, 본인의 생각이 정리가 안 되어 있으면 해줄 수 있는 게 없어요.
• • •
주석
-
코드 스니펫에 구문 강조가 없는 이유가 궁금하시다면, 원본 문서의 GitHub 플레이버드 마크다운을 그대로 보존하고 있기 때문이에요. 모든 코드 스니펫이 명시적으로
text로 표기되어 있어요(이 문서가 AI로 생성되었다는 여러 징후 중 하나). 사실 이건 모델이 요청의 글자는 따르되 정신은 따르지 않는 전형적인 예라고 봐요. 아마 인간이 AI에게 프로젝트 초안을 산문 형태의 스펙으로 변환해 달라고 요청했고, AI는 코드 스니펫에text레이블을 붙이면 코드보다 산문처럼 보일 거라고 판단한 것 같아요. ↩ -
사람들이 자주 “Haskell 대신 더 주류 언어로 코드를 생성하면 더 나은 결과를 얻을 수 있다”고 해요. 제 답은 이래요: 에이전트가 Haskell 코드를 생성하는 데 어려움을 겪는다면, 에이전트가 훈련 데이터를 넘어서 신뢰성 있게 일반화할 수 없다는 뜻이에요. ↩
-
제가 잘못 사용한 거라고 생각하신다면, 직접 같은 실험을 해보실 수도 있어요. ↩
-
[역주] CommitStrip은 프로그래머의 일상을 소재로 한 프랑스 웹코믹으로, 개발자 커뮤니티에서 널리 공유돼요. ↩
-
[역주] Symphony는 OpenAI가 2026년 초에 공개한 오픈소스 에이전트 오케스트레이터예요. Linear 이슈 트래커와 연동해서 AI 코딩 에이전트들을 자동으로 배분하고 관리하는 시스템이에요. ↩
-
[역주] 에츠허르 다익스트라(Edsger Dijkstra, 1930-2002)는 최단 경로 알고리즘, 구조적 프로그래밍 등으로 유명한 컴퓨터 과학의 선구자예요. 형식적 방법론과 프로그래밍의 본질에 대한 날카로운 에세이로도 알려져 있어요. ↩
-
[역주] 저자의 블로그 이름이 “Haskell for all”이에요. Haskell은 순수 함수형 프로그래밍 언어로, 실무보다는 학술적 영향이 크고 학습 곡선이 높기로 유명해요. LLM 훈련 데이터에 Haskell 코드가 상대적으로 적어서, 에이전트의 일반화 능력을 시험하기에 좋은 선택이에요. ↩
-
[역주] 호르헤 루이스 보르헤스(Jorge Luis Borges, 1899-1986)는 아르헨티나의 작가로, 무한, 미로, 자기참조적 구조를 다룬 단편소설로 유명해요. 여기서 인용된 “학문의 엄밀함에 대하여”는 지도와 영토의 관계를 통해 “완벽한 모사는 원본 자체와 같아진다”는 역설을 보여줘요. ↩
저자 소개: Gabriella Gonzalez는 “Haskell for all” 블로그를 운영하는 소프트웨어 엔지니어로, 함수형 프로그래밍과 프로그래밍 언어 이론에 대한 글을 씁니다.
참고: 이 글은 Gabriella Gonzalez가 자신의 블로그에 게시한 아티클을 번역한 것입니다.
원문: A sufficiently detailed spec is code - Gabriella Gonzalez, Haskell for all (2026년 3월 17일)
생성: Claude (Anthropic)