스펙 부채(Spec Debt)는 해결한다고 사라지지 않습니다. 이동할 뿐입니다.
요약
소프트웨어 개발 과정에서 발생하는 '스펙 부채(Spec Debt)'의 개념과 이를 해결하는 구체적인 사례를 다룹니다. 모호한 요구사항이 테스트와 구현에 미치는 영향을 분석하고, 명확한 명세 작성을 통해 기술적 부채를 관리하는 방법을 제시합니다.
핵심 포인트
- 스펙 부채는 해결되는 것이 아니라 다른 형태로 이동함
- 모호한 요구사항은 테스트 통과 여부와 상관없이 잠재적 결함 유발
- 타임아웃 측정 기준과 재시도 횟수 정의의 명확성 강조
- 단순 테스트 통과를 넘어 에이전트 관점의 설계 필요성
서문
본론으로 들어가기 전에 한 가지 솔직하게 말씀드리고 싶습니다. 이 글에서 다루는 프레임워크 중 그 어떤 것도 제 것이 아닙니다. 여기에 담긴 아이디어들은 저보다 훨씬 더 깊고 오래 이 문제를 고민해 온 두 분으로부터 온 것이며, 제가 한 마디 더 하기 전에 그분들에게 온전한 공로를 돌려야 마땅합니다.
Dan Shapiro — Glowforge의 CEO이자 Wharton Research Fellow이며, 이 모든 대화에 어휘를 부여한 분입니다. 그의 블로그 포스트 "The Five Levels: from Spicy Autocomplete to the Dark Factory"는 제가 하려는 모든 말의 개념적 중추입니다. 원문을 읽어보세요. 짧고 날카로우며, 가장 좋은 방식으로 당신을 불편하게 만들 것입니다. danshapiro.com
Issue #7은 7개의 이슈 동안 신중하게 구축된 프로젝트에서 문서화된 7개의 스펙 부채(spec debt) 항목으로 끝났습니다. 모든 항목은 테스트를 통과하고 있었습니다. 그 어떤 것도 스스로를 드러내지 않았습니다. 그것들은 다른 질문을 던짐으로써 발견되었습니다. "이것이 통과하는가?"가 아니라 "두 번째 에이전트(agent)라면 이 단계로부터 무엇을 구축할 것인가?"라는 질문 말입니다.
Issue #8은 이 7가지 항목을 모두 수정하며, 그것들을 찾아낸 도구를 재사용 가능한 무언가로 구축합니다.
7가지 수정 사항
각 항목을 하나씩 처리하며, 개별 수정 후 매번 테스트 스위트(test suite)를 실행했습니다. 일괄 처리(batching)하지 않았습니다. 이 규율이 중요합니다. 만약 수정 사항이 무언가를 망가뜨린다면, 어떤 수정 사항이 그것을 망가뜨렸는지 알아야 하기 때문입니다.
수정 1 — 타임아웃 측정의 모호성 (Timeout measurement ambiguity)
# 수정 전
And the response is returned within 12 seconds
...
"제출되는 순서 중(Of the order being submitted)"이라는 표현은 시계를 클라이언트 측 HTTP 요청 발송 시점에 고정합니다. 이는 단계 정의(step definition)에서 time.time()이 캡처되는 바로 그 순간과 같습니다. 이 고정점이 없다면, 두 번째 구현체는 서버 수신 시점, 마지막 재시도(retry) 시도 시점, 또는 응답 본문(response body)이 완전히 읽혔을 때를 기준으로 측정할 수 있습니다. 부하(load) 상황에서 이 세 가지 방식은 모두 서로 다른 수치를 만들어냅니다.
수정 2 — "재시도됨(Retried)" vs "총 시도 횟수(total attempts)"
# 수정 전
And the payment gateway is not retried more than 2 times
...
"Retried 2 times"는 두 가지 유효한 영어 해석이 가능합니다. 즉, 2번의 재시도(retries)를 의미하여 총 3번의 요청이 발생하는 것인지, 아니면 최대 2번까지 재시도(retried up to 2 times)한다는 의미로 총 2번의 요청을 의미하는 것인지입니다. "No more than 2 charge requests total"는 재시도가 아닌 요청(requests) 횟수를 세며, "total"이라는 단어를 통해 초기 시도(initial attempt)가 포함됨을 명확히 합니다. 이로 인해 스텝 정의(step definition)의 단언(assertion) 방식도 변경되었습니다. 응답 본문(response body)의 retry_count 필드를 신뢰하는 방식에서, 모의 서버(mock server)에서의 실제 호출 횟수를 확인하는 방식으로 바뀌었습니다. 더 강력한 단언(assertion)이지만 결과는 동일합니다.
수정 3 — 메커니즘 없는 "Released"
# 수정 전
And the inventory reservation is released
...
"Released"는 무엇이 일어났는지는 말해주지만, 어떻게 일어났는지 또는 어떤 항목에 대해 일어났는지는 말해주지 않습니다. 새로 작성된 문구는 항목의 이름을 명시하고 인벤토리 서비스(inventory service)로 요청이 전송됨을 구체화합니다. 이 수정 과정에서 하나의 간극(gap)이 드러나기도 했습니다. 현재 구현체는 인벤토리 서비스로의 별도 API 호출 대신 응답 본문 필드(inventory_released: true)를 통해 해제(release)를 알리고 있습니다. 이제 스펙(spec)은 의도된 동작을 기술합니다. 구현체는 아직 이에 완전히 일치하지 않습니다. 이는 미래의 과제이지만, 이제 그 간극이 숨겨져 있지 않고 가시화되었습니다.
수정 4 — "Explicit user action" — 완전히 제거됨
# 수정 전
And no order is confirmed without explicit user action
...
이 스텝은 코드베이스 어디에도 존재하지 않는 후속 확인 흐름(POST /orders/{id}/confirm 또는 그에 상응하는 흐름)을 암시합니다. 부분적 가용성(partial availability) 시나리오에서는 어떤 주문도 확인되지 않기 때문에 이 스텝은 아무런 문제 없이 통과(pass)되지만, 이는 확인 흐름이 구현되었기 때문이 아닙니다. 잘못된 이유로 통과되는 스펙 스텝은 안전망이 아닙니다. 그것은 거짓된 보증입니다. 만약 향후 과제에서 확인 흐름이 구축된다면, 새로운 시나리오에서 이를 정확하게 명시해야 합니다. 이 스텝을 그대로 두는 것은 에이전트(agent)가 스펙에 없는 엔드포인트(endpoint)를 임의로 만들어내도록 유도하는 결과를 초래할 것입니다.
수정 5 — 값에 대한 단언(assertions) 없는 존재 여부 확인
수정 6 — 방법을 명시하지 않은 "An order exists"
# 수정 전
Given an order was successfully placed and confirmed with order ID "aaa00000-..."
...
"Successfully placed and confirmed"는 결과(outcome)를 설명할 뿐, 메커니즘(mechanism)을 설명하지는 않습니다. "Created via POST /orders"는 실제 생성 흐름(creation flow)이 기대된다는 점을 명시적으로 나타냅니다. 현재의 단계 정의(step definition)는 주문을 인메모리 저장소(in-memory store)에 직접 심고 있는데, 이는 지름길(shortcut)입니다. 재작성된 버전은 명세의 의도(spec intent)와 단계 구현(step implementation) 사이에 문서화된 간극을 만듭니다. 숨겨진 간극이 아니라, 눈에 보이는 간극입니다.
Fix 7 — 정의 없이 "Correct" 사용
# Before
And the notification contains the correct order id and total
...
"Correct"는 독자가 알 수 없는 맥락에 따라 상대적입니다. 재작성된 버전은 When 절에서 설정된 기대값(expected values)을 하드코딩합니다. 원본 단계를 읽는 두 명의 에이전트(agent)는 모두 알림 본문(notification body)을 확인하는 무언가를 구현하겠지만, 한 명은 When 절의 값과 비교할 수도 있고, 다른 한 명은 계산된 총액(computed total)과 비교할 수도 있으며, 세 번째 사람은 단순히 필드의 존재 여부만 확인할 수도 있습니다. 재작성된 버전은 이 세 가지 해석을 모두 제거합니다.
이 수정 사항은 스텁(stub)이 숨기고 있던 문제도 잡아냈습니다. 알림 모의 객체(notification mock)가 알림 ID로 UUID가 아닌 "mock-notif-001"을 반환하고 있었습니다. 형식 단언(format assertion)이 이를 즉시 잡아냈습니다. 이것이 바로 구체적인 단언(concrete assertions)을 추가했을 때 얻는 가치입니다. 한 번도 유효하지 않았지만 한 번도 확인되지 않았던 스텁 데이터(stub data)를 표면 위로 드러냅니다.
감사 프레임워크 (The audit framework)
7가지 항목을 모두 수정한 후, 저는 진단 도구를 독립된 문서인 docs/spec-audit-framework.md로 구축했습니다. 전체 문서는 리포지토리(repo)에 있습니다. 그 핵심 내용은 다음과 같습니다.
다섯 가지 질문 — 모든 기능 파일(feature file)의 모든 시나리오에 대해 다음을 질문하세요:
Q1: 이 시나리오의 소유자는 누구인가?
이 시나리오가 속한 팀, 서비스 또는 도메인(domain)의 이름을 말할 수 있습니까? 만약 답변에 "그리고 또한("and also")"이 포함된다면, 그 시나리오는 잘못된 파일에 있는 것입니다.
Q2: 이 시나리오가 남겨두는 결정 사항은 무엇인가?
모든 Given, When, Then 절에 대하여: 두 명의 에이전트가 서로 다른 구현을 만들더라도 둘 다 통과할 수 있습니까? 만약 그렇다면, 해당 단계는 명세가 불충분한(underspecified) 상태입니다.
Q3: 파일 내의 모든 용어가 정의되어 있습니까?
표준 HTTP 개념이나 기본 타입이 아닌 명사(noun)는 시나리오(scenario) 또는 Background 절에 정의되어야 합니다. 어떤 용어를 이해하기 위해 다른 파일을 읽거나 동료에게 물어봐야 한다면, 그것은 스펙 부채(spec debt)입니다.
Q4: 이 시나리오는 동작을 설명합니까 아니면 구현을 설명합니까?
단계(Steps)는 호출자(caller)의 관점에서 시스템이 무엇을 하는지 설명해야 합니다. 데이터베이스 필드 이름, 함수 이름, 내부 상태 코드와 같은 내부 개념을 참조하는 모든 단계는 구현(implementation)을 스펙으로 누설(leaking)시키는 것입니다.
Q5: 이 시나리오는 무엇을 말하지 않고 있어야 합니까?
시나리오가 암시하지만 명시적으로 지정하지 않은 엣지 케이스(edge cases), 에러 상태(error states), 경계 조건(boundary conditions) 목록을 작성하세요. 각각은 프로덕션 사고(production incident)가 될 준비를 하는 침묵의 가정입니다.
여섯 가지 부채 유형:
| 클래스 | 어떤 모습인가 |
|---|---|
| UNDERSPECIFIED | 단계는 존재하지만 결정을 열어 둠 |
| ... |
수동 감사에서 프레임워크가 발견한 것
다섯 가지 질문을 네 개의 고정된 기능 파일(feature files) 전체에 적용하자, Issue #7 수동 감사에서는 포착하지 못한 한 항목이 드러났습니다.
order_status_good.feature의 Given 절은 이제
모든 수정 후의 스코어카드
교육용이 아닌 네 개의 모든 피처 파일(feature files)에 프레임워크를 적용했습니다:
order_creation.feature — 5개 시나리오, 1개의 부채 항목 남음 (스텝 정의(step definition) 레벨에서의 누수된 추상화 (LEAKY ABSTRACTION) — Fix 3에서 발생한 재고 해제 메커니즘의 공백)
order_status_good.feature — 2개 시나리오, 1개의 부채 항목 남음 (누수된 추상화 (LEAKY ABSTRACTION) — 스텝 정의가 POST /orders를 통하지 않고 주문을 직접 생성함)
notification_service.feature — 2개 시나리오, 0개의 부채 항목
order_status_bad.feature — 교육용 산출물로 유지되었으며, 부채 감사는 수행하지 않음
수정 후 부채 밀도: 시나리오당 0.22개 항목. 남은 두 항목 모두 스텝 정의(step definition) 레벨의 누수된 추상화 (LEAKY ABSTRACTION)입니다. 가장 위험도가 높은 두 클래스인 모호한 수치 (AMBIGUOUS COUNT) 또는 암시적 흐름 (IMPLICIT FLOW) 항목은 전혀 남아있지 않습니다.
불편한 진실
7개의 스펙 부채(spec debt) 항목을 수정하고, 8회차에 걸쳐 신중하게 구축된 프로젝트에 구조화된 감사 프레임워크를 적용한 결과, 2개의 부채 항목이 남았습니다. 두 항목 모두 다른 부채를 해결했던 동일한 세션에서 도입되었습니다. 즉, 정확한 스펙 스텝(spec step)은 작성되었으나, 해당 스텝의 구현 과정에서 지름길(shortcut)을 택한 것입니다.
스펙 부채는 부채를 수정한다고 해서 제거되지 않습니다. 그것은 이동할 뿐입니다.
실질적인 결론은 다음과 같습니다: 스텝 정의(step definition)를 단순한 테스트 하네스(test harness) 코드가 아니라, 스펙 표면(spec surface)의 일부로 취급하십시오. 스텝 정의가 스펙이 명시하는 내용과 미묘하게 다르게 동작한다면, 설령 테스트가 통과하더라도 그것은 스펙 부채입니다. 감사 프레임워크는 이 두 가지를 모두 잡아낼 수 있지만, 이는 피처 텍스트(feature text)뿐만 아니라 스텝 정의에도 Q4를 적용할 때만 가능합니다.
언급할 가치가 있는 또 다른 발견은 notification_service.feature가 부채 항목 0개를 기록했다는 점입니다. 이 파일은 이전 파일들이 무엇을 잘못했는지에 대한 8회차 분량의 축적된 교훈을 바탕으로 작성되었습니다. 부채가 없다는 것은 우연이 아닙니다. 그것은 다음 스펙을 작성하기 전에 나쁜 스펙이 어떤 모습인지 알고 있었던 결과입니다.
스펙을 작성하기 가장 좋은 시기는 몇 개의 나쁜 스펙을 작성해 본 후입니다. 소급하여 감사하고 앞으로 나아가며 수정하는 것이 현실적인 경로입니다.
다음 이슈: 프롬프트는 일회용입니다. 기술(Skills)은 인프라입니다 — 세션 수준의 프롬프트에서 버전이 관리되고 재사용 가능한 기술 정의(skill definitions)로의 개념적 전환. 레이어 2가 시작됩니다.
출처 및 추가 읽기
- Cucumber + Gherkin 문서
- Dan Shapiro — The Five Levels: from Spicy Autocomplete to the Dark Factory
- Nate B. Jones — natebjones.com
- 프로젝트 저장소 (Project repository)
- 스펙 감사 프레임워크 (Spec audit framework)
- 세션 결과 — Issue #8
이 기사는 AI 도구의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기