Blog
spec-kitspec-driven-developmentimplementationgithub-issuesclaude-codeai

[Spec Kit ⑥] Implement & Converge — 실행과 수렴, 그리고 완료의 증명

잘 분해된 작업 목록을 실제 코드로 바꾸는 /speckit.implement, 작업을 GitHub 이슈로 잇는 /speckit.taskstoissues, 그리고 '정말 다 했는가'를 묻는 /speckit.converge까지 — AI가 짠 코드를 신뢰하는 방법을 dq-monitor 예제로 끝까지 따라갑니다.

Data Dynamics2026年6月16日25 min read
This post is not yet translated. The original Korean version is shown below.

5부에서 우리는 dq-monitor의 아키텍처를 plan.md로 설계하고, 그것을 의존성 순서가 있는 작업 목록(tasks.md)으로 분해한 뒤, /speckit.analyze로 spec·plan·tasks 사이의 모순까지 걸러냈습니다. 이제 손에 든 것은 "검증된 청사진"입니다. 그런데 청사진은 건물이 아닙니다. 이번 6부의 질문은 단 하나입니다 — AI가 이 작업들을 실제 코드로 옮길 때, 우리는 그 코드를 어떻게 신뢰하는가? 그리고 더 어려운 질문, "이제 다 끝났다"는 것을 무엇으로 증명하는가?

이 글에서 배우는 것

  • /speckit.implementtasks.md를 의존성 순서대로 실행해 코드를 만드는 방식과, 그 과정에서 사람이 끼어야 하는 지점
  • "AI가 짠 코드를 어떻게 믿는가?"에 대한 SDD의 답 — 코드 → 작업 → 요구사항 → 헌법으로 이어지는 추적성(traceability)
  • /speckit.taskstoissues로 작업 목록을 GitHub 이슈로 옮겨 기존 팀 워크플로(이슈·PR·보드)에 잇는 법
  • /speckit.converge로 코드베이스를 산출물과 대조해 잔여 작업을 회수하고 "정말 끝났는가"를 닫는 법
  • 신뢰·검증 실무 습관과, implement를 끝으로 착각하는 흔한 함정들

이 글은 Spec Kit 시리즈의 6부입니다. 5부에서 만든 dq-monitorplan.md·tasks.md가 준비돼 있다고 전제합니다.


1. Implement는 "맡기고 잊기"가 아니다

/speckit.implement의 정의는 단순합니다 — 계획에 따라 모든 작업을 실행해 기능을 구현한다. AI 에이전트는 tasks.md를 위에서부터 읽어 내려가며, 의존성 순서대로 각 작업을 코드로 옮깁니다. T001이 데이터 모델을 만들면 T002가 그 모델을 쓰는 검사 엔진을 짜고, 그 위에 알림 라우팅이 얹히는 식입니다.

여기서 가장 흔한 오해를 먼저 깨야 합니다. implement는 "엔터 치고 커피 마시러 가는" 명령이 아닙니다. 마치 자동조종 같지만, SDD에서 implement의 핵심 가치는 "사람이 검토할 수 있는 단위로 코드가 흘러나온다"는 데 있습니다. 작업이 작게 쪼개져 있기 때문에, AI는 한 번에 한 작업씩 산출하고, 사람은 한 번에 한 작업씩 검토합니다. 검증 루프를 짧게 유지하는 것 — 그것이 implement 단계에서 사람이 하는 일입니다.

Loading diagram…

"AI가 짠 코드를 어떻게 믿는가?"

이 시리즈를 관통하는 질문이 여기서 정점에 이릅니다. 바이브 코딩에서는 이 질문에 답할 수 없었습니다. 코드가 그럴듯해 보여도, 그것이 원래 의도를 만족하는지 대조할 기준이 없었기 때문입니다(1부의 "검증 불가" 실패 모드).

SDD의 답은 코드 자체의 품질이 아니라 추적성에 있습니다.

모든 코드 라인은 하나의 작업(task) 으로 거슬러 올라가고, 모든 작업은 하나의 요구사항(requirement) 으로 거슬러 올라가며, 모든 요구사항은 헌법(constitution) 의 원칙 아래 놓입니다.

이 사슬이 있으면 "이 코드를 믿느냐"는 질문이 "이 코드가 어떤 작업을 구현했고, 그 작업이 어떤 요구사항을 만족하며, 그 요구사항이 우리 원칙에 부합하는가"라는 검증 가능한 질문으로 바뀝니다. 신뢰는 막연한 느낌이 아니라, 따라갈 수 있는 경로에서 나옵니다.

질문바이브 코딩SDD (implement)
이 코드가 왜 여기 있나?"AI가 그렇게 짰어요"T014를 구현, → FR-007 요구사항
이게 맞게 동작하나?느낌상 괜찮아 보임작업에 명시된 테스트가 통과
빠뜨린 건 없나?알 수 없음tasks.md의 체크박스로 추적
원칙을 어겼나?비교 기준 없음헌법 게이트로 대조

2. dq-monitor를 모듈 단위로 쌓아 올리기

5부의 tasks.mddq-monitor의 작업을 의존성 순서로 배열해 두었습니다. /speckit.implement를 호출하면 에이전트는 이 순서를 그대로 따라 모듈을 쌓아 올립니다. 실제 흐름은 대략 다음과 같습니다.

$ /speckit.implement
 
▶ specs/001-realtime-dq-monitor/tasks.md 로드 — 미완료 작업 27개
 
[Phase 1] 기반 (Foundation)
  T001  데이터 모델: DataSource, Metric, Threshold, Alert ........ 구현 → 테스트 통과 ✓
  T002  설정 로더(YAML) + 스키마 검증 ............................ 구현 → 테스트 통과 ✓
  ⏸  체크포인트: 사람 검토 대기 (Phase 1 diff)
 
[Phase 2] 검사 엔진 (Checks Engine)
  T007  신선도(freshness) 검사 규칙 ............................... 구현 → 테스트 통과 ✓
  T008  정합성(completeness/null-rate) 검사 규칙 ................. 구현 → 테스트 통과 ✓
  T009  이상치(anomaly, z-score) 검사 규칙 ....................... 구현 → 테스트 통과 ✓
  T010  검사 스케줄러(주기 실행) ................................. 구현 → 테스트 통과 ✓
  ⏸  체크포인트: 사람 검토 대기 (Phase 2 diff)
 
[Phase 3] 알림 라우팅 (Alert Routing)
  T014  알림 라우터(심각도 → 채널 매핑) .......................... 구현 → 테스트 통과 ✓
  T015  Slack / Email 발송 어댑터 ................................ 구현 → 테스트 통과 ✓
  ...

⏸ 체크포인트는 우연이 아닙니다. 의존성의 한 층(phase)이 끝날 때마다 멈춰, 사람이 그 층의 diff를 한꺼번에 검토할 수 있게 합니다. 데이터 모델이 잘못 잡혔다면 검사 엔진을 다 짜기 전에 잡아야 하니까요. 토대가 틀어진 채 위층을 쌓으면, 비용은 기하급수적으로 커집니다.

한 작업이 코드가 되는 모습

추상적인 흐름만으로는 감이 잡히지 않으니, 작업 하나가 실제로 어떻게 코드와 테스트가 되는지 들여다봅니다. T007 — 신선도 검사 규칙을 예로 들겠습니다. tasks.md의 원래 항목은 이랬습니다.

- [ ] T007 [P] 신선도(freshness) 검사 규칙 구현
      - 입력: DataSource.last_updated_at, Threshold.max_staleness_minutes
      - 출력: CheckResult(status: OK|WARN|FAIL, observed, expected)
      - FR-003(신선도 임계 초과 시 경보)을 만족해야 함
      - 경계 조건: last_updated_at == now, null인 경우

에이전트가 이 작업을 구현하면 다음과 같은 코드와 테스트가 나옵니다. 주목할 점은 작업에 적힌 요구사항 ID(FR-003)와 경계 조건이 그대로 코드·테스트에 반영된다는 것입니다.

# src/dq_monitor/checks/freshness.py
from datetime import datetime, timezone
from dq_monitor.models import CheckResult, CheckStatus, DataSource, Threshold
 
 
def check_freshness(source: DataSource, threshold: Threshold, *, now: datetime) -> CheckResult:
    """데이터 소스의 신선도를 임계값과 대조한다. (FR-003)"""
    if source.last_updated_at is None:
        # 경계 조건: 갱신 이력이 없으면 FAIL로 본다.
        return CheckResult(
            status=CheckStatus.FAIL,
            observed=None,
            expected=threshold.max_staleness_minutes,
            detail="last_updated_at is null",
        )
 
    staleness_min = (now - source.last_updated_at).total_seconds() / 60.0
    if staleness_min > threshold.max_staleness_minutes:
        status = CheckStatus.FAIL
    elif staleness_min > threshold.max_staleness_minutes * 0.8:
        status = CheckStatus.WARN  # 임계의 80%를 넘으면 선제 경고
    else:
        status = CheckStatus.OK
 
    return CheckResult(
        status=status,
        observed=round(staleness_min, 2),
        expected=threshold.max_staleness_minutes,
    )
# tests/checks/test_freshness.py
from datetime import datetime, timedelta, timezone
from dq_monitor.checks.freshness import check_freshness
from dq_monitor.models import CheckStatus, DataSource, Threshold
 
NOW = datetime(2026, 6, 26, 12, 0, tzinfo=timezone.utc)
THRESHOLD = Threshold(max_staleness_minutes=60)
 
 
def _source(updated_at):
    return DataSource(name="orders", last_updated_at=updated_at)
 
 
def test_fresh_data_is_ok():
    r = check_freshness(_source(NOW - timedelta(minutes=10)), THRESHOLD, now=NOW)
    assert r.status == CheckStatus.OK
 
 
def test_stale_data_fails():  # FR-003 핵심 경로
    r = check_freshness(_source(NOW - timedelta(minutes=90)), THRESHOLD, now=NOW)
    assert r.status == CheckStatus.FAIL
    assert r.observed == 90.0
 
 
def test_near_threshold_warns():
    r = check_freshness(_source(NOW - timedelta(minutes=55)), THRESHOLD, now=NOW)
    assert r.status == CheckStatus.WARN
 
 
def test_null_last_updated_fails():  # tasks.md에 명시된 경계 조건
    r = check_freshness(_source(None), THRESHOLD, now=NOW)
    assert r.status == CheckStatus.FAIL
$ pytest tests/checks/test_freshness.py -q
....                                                      [100%]
4 passed in 0.06s

이 작은 사이클 — 작업 → 코드 → 테스트 → 통과 — 이 27번 반복되면 dq-monitor가 됩니다. 그리고 매 사이클의 테스트가 곧 게이트입니다. 테스트가 빨갛게 뜨면 다음 작업으로 넘어가지 않습니다. AI가 "다 됐습니다"라고 말해도, 게이트를 통과하지 못하면 다 된 게 아닙니다.

핵심: implement 단계에서 사람의 일은 "코드를 한 줄씩 다시 짜는 것"이 아니라, 작업 단위로 끊어 검토하고, 테스트를 게이트로 삼고, 어긋난 작업에만 개입하는 것입니다. 검토 단위가 작을수록 신뢰는 커집니다.


3. taskstoissues — SDD를 팀의 일상 워크플로에 잇기

tasks.md는 훌륭한 실행 계획이지만, 텍스트 파일 하나에 머물러 있는 한 팀의 일상과는 떨어져 있습니다. 대부분의 팀은 이미 GitHub 이슈로 일을 쪼개고, PR로 리뷰하고, 프로젝트 보드로 진척을 추적합니다. /speckit.taskstoissues는 그 간극을 메웁니다 — 작업 목록을 추적용 GitHub 이슈로 변환합니다.

이것은 단순한 "복사 붙여넣기"가 아니라 SDD와 기존 협업 도구를 잇는 다리입니다. 각 작업이 이슈가 되면, 그 작업은 담당자·라벨·마일스톤·보드 칼럼을 갖게 되고, PR이 그 이슈를 Closes #N으로 닫을 수 있게 됩니다. 이미 GitHub 위에서 일하는 팀에게는 더없이 자연스러운 연결입니다.

생성된 이슈는 대략 이런 모습입니다. 핵심은 이슈 본문이 원래 작업과 그 작업이 가리키는 spec/plan을 역참조한다는 점입니다.

Issue #14 — [T014] 알림 라우터: 심각도 → 채널 매핑
 
라벨: spec-kit, phase-3-alert-routing, task
마일스톤: dq-monitor v1
---
## 작업
검사 결과의 심각도(WARN/FAIL)를 알림 채널로 라우팅하는 라우터를 구현한다.
 
## 출처 (Traceability)
- 작업: `specs/001-realtime-dq-monitor/tasks.md` → T014
- 요구사항: `spec.md` → FR-007 (심각도별 채널 분기)
- 설계: `plan.md` → §4.2 알림 파이프라인
 
## 완료 조건 (Definition of Done)
- [ ] WARN → #data-quality Slack 채널
- [ ] FAIL → #data-quality + on-call 이메일
- [ ] 동일 알림 5분 내 중복 발송 억제(dedup)
- [ ] 단위 테스트: 심각도별 라우팅 분기 4종
 
## 의존성
- Blocked by: #10 (검사 스케줄러), #13 (Alert 모델)

이렇게 만들어진 이슈는 SDD의 추적성을 GitHub의 가시성과 결합합니다. 보드를 보면 "Phase 3 알림 라우팅이 5개 중 2개 남았다"가 한눈에 보이고, 각 이슈를 열면 그 일이 필요한지(어떤 FR을 만족하는지)가 적혀 있습니다. 매니저에게는 진척이, 개발자에게는 맥락이 동시에 제공됩니다.

일상 도구taskstoissues가 채우는 것
이슈 트래커각 작업 = 추적 가능한 이슈(담당·라벨·마일스톤)
Pull RequestCloses #14로 코드와 작업을 연결
프로젝트 보드phase 라벨이 곧 칼럼/스윔레인
스탠드업"어느 FR이 막혔는가"를 이슈 단위로 보고

taskstoissues는 선택적 단계입니다. 혼자 빠르게 도는 작업이라면 tasks.md만으로 충분합니다. 하지만 팀이 GitHub에서 협업하고 진척을 외부에 보고해야 한다면, 이 다리가 SDD를 "별도 의식"이 아니라 "원래 하던 일"로 녹여 줍니다.


4. Converge — "정말 다 했는가"를 닫는 마지막 질문

implement가 끝나고 모든 테스트가 초록색이면 끝일까요? 여기에 SDD의 가장 미묘하고 가치 있는 단계가 있습니다. /speckit.converge코드베이스를 산출물(spec·plan·tasks)과 대조해, 남은 일을 새 작업으로 덧붙입니다.

왜 이 단계가 필요할까요? implement 도중에는 미묘한 드리프트(drift) 가 쌓이기 때문입니다. 작업을 구현하다 보면 명세에 있던 어떤 엣지 케이스가 슬그머니 빠지고, 계획에 적힌 비기능 요구사항(예: "p95 지연 200ms 이하")이 코드에는 반영되지 않은 채 넘어가고, 임시방편(# TODO: 재시도 로직)이 그대로 남기도 합니다. 각 작업의 테스트는 통과했는데도, 명세 전체로 보면 빈틈이 있는 상태입니다.

converge는 "각 작업이 통과했는가"가 아니라 "명세가 약속한 것이 코드에 다 들어왔는가" 를 묻습니다. 그리고 발견한 빈틈을 그냥 보고하고 끝내는 게 아니라, tasks.md에 새 작업으로 회수합니다. 이것이 루프를 명세로 되돌려 닫는 방식입니다.

$ /speckit.converge
 
코드베이스 ↔ 산출물 대조 중...
  spec.md: 12개 기능 요구사항(FR), 4개 비기능 요구사항(NFR)
  plan.md: 6개 설계 결정
  tasks.md: 27개 작업 (27 완료)
  코드: 41개 모듈, 38개 테스트 파일
 
── 수렴 리포트 ─────────────────────────────────────────
✓ FR-001~FR-012  구현·테스트 확인
✓ NFR-001 (검사 주기 ≤ 1분)  스케줄러에서 확인
⚠ 드리프트 3건 발견 → 신규 작업으로 회수:
 
  [신규] T028  NFR-002 미충족: 알림 발송 p95 지연 측정·계측 부재
         근거: plan.md §5 "p95 < 200ms" 명시, 코드에 메트릭 계측 없음
  [신규] T029  FR-009 부분 구현: 알림 dedup이 Slack에만 적용, 이메일 누락
         근거: spec.md FR-009 "모든 채널에 5분 dedup", email 어댑터 미적용
  [신규] T030  잔여 TODO: src/.../retry.py:42 재시도 백오프 미구현
         근거: plan.md §4.2 "지수 백오프 재시도" 설계 대비 미완
 
3개 작업을 tasks.md에 추가했습니다. /speckit.implement로 이어서 처리하세요.
────────────────────────────────────────────────────────

이 리포트가 답하는 질문이 바로 "우리 정말 다 한 거 맞아?" 입니다. 사람의 기억이나 "느낌"이 아니라, 명세라는 객관적 기준에 코드를 대조해 답합니다. 그리고 발견된 잔여 작업(T028~T030)은 다시 implement의 입력이 됩니다. 빈틈이 0이 될 때까지 이 작은 루프가 돕니다.

converge는 "버그를 찾는" 단계가 아니라 "약속과 결과를 정렬(align)하는" 단계입니다. 각 테스트가 통과해도 명세 전체와는 어긋날 수 있습니다 — 그 어긋남을 잡아 명세로 되먹이는 것이 SDD가 "완료"를 증명하는 방식입니다.


5. 실행–수렴 루프 한눈에 보기

implement와 converge, 그리고 taskstoissues가 어떻게 맞물리는지 전체 그림으로 정리하면 다음과 같습니다. 핵심은 하나의 닫힌 루프라는 점입니다. converge가 빈틈을 찾는 한, 우리는 implement로 되돌아갑니다.

Loading diagram…

루프를 말로 풀면 이렇습니다.

  1. tasks.md의 작업을 implement가 코드와 테스트로 바꾼다 (작업 단위 검토·테스트 게이트).
  2. 선택적으로 taskstoissues가 작업을 GitHub 이슈로 내보내 팀의 PR·보드와 연결한다.
  3. converge가 완성된 코드를 spec·plan·tasks와 대조한다.
  4. 빈틈이 있으면 새 작업으로 회수tasks.md에 덧붙이고 → 1번으로 돌아간다.
  5. 빈틈이 0이 되면, 코드가 명세를 만족함이 증명된 상태로 완료된다.

이 루프가 1부에서 말한 "검증 가능한 명세"의 약속을 끝에서 회수합니다. 완료는 "AI가 다 됐다고 말한 순간"이 아니라 "명세와 코드가 정렬된 순간" 입니다.


6. 신뢰·검증 실무 — 그리고 피해야 할 함정

implement와 converge를 잘 돌리는 것은 결국 몇 가지 습관으로 압축됩니다. 도구가 강제하지 않는 부분에서 사람이 규율을 지켜야 합니다.

지켜야 할 실무

습관왜 중요한가
작게 검토 가능한 단위로작업이 작아야 diff가 작고, diff가 작아야 사람이 실제로 검토한다. 한 PR에 작업 1~2개.
테스트를 게이트로"통과해 보임"이 아니라 "테스트가 통과함". 빨간 테스트 위에 다음 작업을 쌓지 않는다.
추적성을 유지커밋·PR 메시지에 작업 ID와 FR을 남긴다. 코드 → 작업 → 요구사항 경로가 끊기지 않게.
converge를 건너뛰지 않기implement의 모든 테스트가 통과해도 converge 전까지는 "완료"가 아니다.

흔한 함정

  • 대량 산출물을 통째로 수락implement가 한 번에 20개 작업을 쏟아내도, "diff가 길어서" 대충 스크롤하고 승인하면 추적성은 장식이 됩니다. 검토하지 않은 추적성은 추적성이 아닙니다.
  • 테스트 없는 구현 — 테스트가 없으면 게이트가 없고, 게이트가 없으면 converge가 대조할 객관적 통과 기준이 없습니다. "동작하는 것처럼 보임"으로 회귀합니다.
  • implement를 끝으로 착각 — 가장 흔하고 비싼 함정입니다. 모든 작업 체크박스가 채워졌다고 끝이 아닙니다. 작업 단위로는 통과해도 명세 전체로는 빈틈이 남기 마련이고, 그 빈틈을 닫는 단계가 converge입니다. implement에서 멈추면 1부의 "검증 불가"로 슬그머니 되돌아갑니다.

한 줄 요약: 신뢰는 "AI를 믿는 것"이 아니라 "경로를 검증할 수 있게 만드는 것" 입니다. 작은 단위, 테스트 게이트, 끊기지 않는 추적성, 그리고 converge — 이 넷이 AI가 짠 코드를 프로덕션에 올릴 수 있게 만듭니다.


마치며

6부에서 우리는 청사진을 건물로 바꿨습니다. /speckit.implementdq-monitor의 작업을 의존성 순서대로 코드와 테스트로 옮기되, "맡기고 잊기"가 아니라 작업 단위로 검토하고 테스트를 게이트로 삼았습니다. /speckit.taskstoissues로 그 작업들을 GitHub 이슈·PR·보드에 연결해 SDD를 팀의 일상에 녹였고, /speckit.converge로 코드를 명세와 대조해 잔여 작업을 회수함으로써 "정말 끝났는가"를 닫았습니다.

핵심은 변하지 않습니다 — AI가 짠 코드를 신뢰하는 비결은 더 똑똑한 모델이 아니라 코드에서 작업으로, 요구사항으로, 헌법으로 이어지는 추적 가능한 경로입니다. 완료는 선언이 아니라 증명입니다.

다음 7부에서는 지금까지 흩어 본 모든 단계를 하나로 모읍니다. dq-monitor를 헌법부터 수렴까지 0에서 1로 끝까지 만들어 본 엔드투엔드 케이스 스터디와, 그 과정에서 실제로 부딪혔던 함정·교훈을 회고로 정리하며 시리즈를 닫겠습니다.

참고 자료