PLUGIN WORKFLOW MAP · msbaek-tdd 1.31.0

오케스트레이터가 짜는 전체 절차

요구사항 원천(tdd-plan-input)에서 시작해 템플릿 생성(/tdd) → 계획(tdd-plan) → 구현(tdd-rgb / tdd-feature)으로 이어지는 메인 파이프라인과, 그 바깥에서 독립적으로 쓰이는 레거시·정리 스킬들의 전체 배치.

/tdd-plan-input 요구사항 원천 (신규 1.21.0) /tdd 템플릿 문서 생성 /tdd-plan 계획 (인터랙티브) cucumber-acceptance web-app 필수 / general 선택 /tdd-rgb /tdd-feature 원천 문서는 참조로 전달 — /tdd-plan의 인자가 아니다 docs/<topic>-plan-input.md docs/<topic>-plan.md test:/feat:/refactor:
두 문서를 혼동하지 않는 것이 이 파이프라인의 핵심이다 — -plan-input.md요구사항 원천(사람이 준 규칙·예제), -plan.md작성 대상 템플릿(절차 섹션과 체크박스)이다.

1.19.0 이후 바뀐 것

1.12.0 – 1.19.0 (이전 요약)
  • 1.14.0 Walking Skeleton이 Fake Repository + 하드코딩진짜 JPA + docker MySQL로. 인수 계층은 High Level Test(JUnit)Cucumber .feature 하나로 일원화(9단계 → 8단계).
  • 1.14.1 TDD 유형 web-usecaseweb-appgeneral과 같은 축(무엇을 만드는가)에서 대조되도록.
  • 1.15.0 기어별 스킬 라우팅 명시. tdd-feature의 Phase B가 feature 범위의 high 기어임을 선언하고, high 안전장치 3종(폭발 반경 점검·시작 커밋 기록·적대적 리뷰)을 동일 적용.
  • 1.16.0 Walking Skeleton 실사용 피드백 4건 — 인수 조건에 없는 API 발명 금지, Spring Data XImpl 자동 프래그먼트 네이밍 충돌 회피, docker MySQL 기동 Testcontainers vs Docker Compose 두 대안, 예외 처리는 처음부터 @RestControllerAdvice로 분리.
  • 1.18.0–1.19.0 Walking Skeleton에 실행 SQL 로깅 추가 — 기본은 spring.jpa.show-sql, 바인딩 값·JPA 밖 경로까지 봐야 하면 p6spy로 승급. 스타터 버전은 Spring Boot 메이저에 종속(1.19.0에서 4.x 대응 안내로 교정)되고, "로그가 나온다"가 "설정이 맞다"의 증거가 아니라는 함정을 명시.

앞단계 — /tdd-plan-input (요구사항 원천)

tdd-plan이 단계 1부터 바로 시작할 수 있도록, 도메인 규칙과 예제를 질의응답으로 채워 원천 문서를 만든다. 서브에이전트에 위임하지 않는다 — 질의응답 자체가 이 스킬의 본질이다.

$ /tdd-plan-input "장바구니 금액 계산" | docs/memo.md
담는 것tdd-plan에서 쓰이는 자리
§1 범위·유형기능 목록 · TDD 유형 · 의도적으로 제외한 것단계 1 시작 전 범위 확정
§2 규칙·예제기본/특별 규칙 + 사람이 계산한 예제단계 1a 도메인 규칙(0층), “검산 전개”
§3 액터·가치역할 / 원하는 것 / 얻는 가치 단계 1b User Story 조립 + INVEST 점검
§4 경계 조건5분류 스캔(수치·크기·상태·시간순서·집계)단계 2 Gherkin Examples · 단계 3 unit test 목록
§5 흐름 신호액터 충돌·상태 전이·대안 흐름·불변식 사실만조건부 Use Case 작성 판단
§6 미확정보류된 결정 + 그 결정이 무엇에 영향을 주는지단계 1 진행 전 결정 대상
지어내다(invent)의 세 형태방어 장치
시나리오를 지어냄§1 “의도적으로 제외한 것” 목록 — 이 목록이 없으면 tdd-plan이 빈칸을 누락으로 오인한다
수치를 지어냄§4에 예제 없음 표시 → 사용자에게 계산 요청. AI 계산값은 확인 전에는 문서에 넣지 않는다 — 값이 맞더라도 규칙 해석이 틀렸다면 틀린 구현이 초록불이 된다
인터페이스를 지어냄tdd-plan 단계 E-2의 “인수 조건에 없는 API를 발명하지 않는다”

재료까지만. 완성형 User Story(As a / I want / So that)·Gherkin·unit test 목록을 이 문서에 쓰지 않는다. 그 형식화는 tdd-plan이 사용자 피드백을 받으며 하는 일이라, 미리 쓰면 그 피드백 루프를 건너뛴다. 빈칸을 만났을 때 선택지는 셋뿐이다 — 질문한다 / §6 미확정으로 올린다 / §1 제외 목록에 넣는다. 넷째(“일반 도메인 지식으로 채운다”)는 없다. 게이트에 막혔다는 이유로 목록을 채우는 것도 같은 위반이므로, 답이 두 번 유보되면 게이트를 §6으로 우회한다.

함정 — 원천 문서를 /tdd-plan의 인자로 주지 않는다

/tdd-plan의 인자는 절차 섹션과 체크박스를 가진 작성 대상 템플릿(/tdd가 생성)이다. 원천 문서를 인자로 주면 tdd-plan이 그것을 템플릿으로 오인해 요구사항 섹션을 덧쓰고, “숫자의 정본은 §2 하나”라는 장치가 무너진다. 원천 문서는 참조로 전달한다 — /tdd-plan docs/basket-plan.md — 요구사항은 docs/basket-plan-input.md 참조

진입 — /tdd

FQCN에서 패키지·클래스명을 파싱해 템플릿 문서와 빈 테스트 클래스를 만들고, 유형에 따라 절차의 길이가 갈린다.

$ /tdd <general|web-app> com.example.basket.CreateShoppingBasket
general web-app 4단계 템플릿 8단계 템플릿
같은 명령이 유형 인자 하나로 절차 길이 자체를 바꾼다 — general은 계산·상태 도메인, web-app은 인수 테스트 셋업과 Walking Skeleton까지 포함한다.

tdd-plan — 계획 단계 (인터랙티브)

인자는 /tdd가 만든 템플릿, 요구사항은 -plan-input.md 참조. 단계마다 사용자 승인 후 커밋하고, Gherkin 이후 지점에서 두 유형이 갈라져 다시 합치지 않는다.

general
1요구사항도메인 규칙(0층) + User Story, 조건부 Use Case
2Gherkin Scenarioprogrammer test 계층
3Unit Test 목록Gherkin에 없는 세밀 분기
4테스트 구현 (RGB)
web-app
1요구사항동일 + Use Case 확장절 비중↑
2Gherkin Scenario
3인수 테스트 셋업 .feature + Runner + Protocol Driver, 미구현은 @pending
4Unit Test 목록
5Walking Skeleton HTTP → 앱 → 진짜 DB(docker MySQL) 관통, 로직 없음
6테스트 구현 (RGB) 각 Green이 자기 시나리오 @pending 해제
7JPA Repository 완성 계약 테스트로 InMemory와 동등성 검증
8DSL 개선 Steps · Protocol Driver · Test Data Builder

점으로 표시된 단계(3~8)가 web-app에만 있는 구간. general은 3단계에서 곧장 RGB로 들어간다. 이전의 “High Level Test 활성화” 단계는 사라졌다 — 각 Green이 시나리오 태그를 해제하므로 마지막에 일괄 활성화할 대상이 없다.

Walking Skeleton — 두 축을 섞지 않는다

GOOS의 정의는 “자동으로 빌드·배포·테스트할 수 있는 실제 기능가장 얇은 슬라이스”다. real과 “비즈니스 로직 제외”는 충돌하지 않는다 — 축이 다르다.

질문기준
real 실행 경로가 진짜인가? fake · 하드코딩 금지 — 실제 HTTP → 실제 앱 → 실제 DB(docker MySQL, Testcontainers) 관통
thinnest 기능이 얇은가? 비즈니스 규칙(합산·할인·검증) 제외 — “너무 단순해서 흥미롭지 않을 정도”의 저장·조회 pass-through

In-Memory 저장소는 skeleton용이 아니라 RGB 사이클의 빠른 루프용이다. 전환은 주석 토글이 아니라 profile로 — @Profile("inMemory")는 Map 기반, @Profile("!inMemory")는 JPA(local·dev·stage·prod 전부). 진짜 DB가 기본이고 in-memory가 명시적 예외라는 원칙이 profile 구조에 새겨진다. 인수 테스트는 항상 local에서 돈다.

실전에서 걸리는 지점 (1.16.0)원칙
API 발명 금지 Gherkin이 요구하지 않는 쓰기(POST) 엔드포인트를 검증 편의로 만들지 않는다 — Target Design은 Protocol Driver가 확정한다. 시나리오가 전부 "이미 담긴" 상태를 전제하면 Repository로 시드하고 읽기 경로 하나만 HTTP로 관통
네이밍 충돌 Spring Data가 인터페이스 XXImpl을 커스텀 구현 프래그먼트로 자동 병합 — 포트 구현체(어댑터) 이름을 Spring Data 인터페이스가 아니라 포트 인터페이스에서 파생시켜야 BeanCurrentlyInCreationException을 피한다
예외 처리 배치 컨트롤러 안에 @ExceptionHandler를 두지 않는다 — 처음부터 @RestControllerAdvice 전역 핸들러로 분리(메서드 추출 금지 원칙의 예외가 아니라 배치 위치의 문제)
docker MySQL 기동 방법장점주의
Testcontainers (기본) 테스트가 컨테이너 수명을 소유, CI 표준 일부 Docker 환경(OrbStack 등)에서 API 버전 협상 실패 — systemProperty("api.version","1.41")로 우회
Spring Boot Docker Compose compose.yaml 하나를 bootRun·테스트가 공유, @Container 보일러플레이트 없음 lifecycle-management=start-only + skip.in-tests=false(기본 true), 의존성은 testAndDevelopmentOnly

관통 확인 — 실행 SQL 로깅 (1.18.0–1.19.0). real 원칙은 선언이 아니라 확인으로 지켜진다 — 임베디드 DB로 조용히 대체되거나 설정이 무시돼도 테스트는 그대로 초록색이다. 기본은 의존성 추가 없는 spring.jpa.show-sql=true. 바인딩된 실제 값이나 JdbcTemplate 등 JPA 밖 경로까지 봐야 하면 p6spy(decorator.datasource.p6spy.enable-logging=true, 최상위 prefix는 decorator이지 spring.이 아님)로 승급한다. 스타터 버전은 Spring Boot 메이저에 종속되며, 스타터가 설정 없이도 기본값으로 로그를 내보내므로 "로그가 나온다"는 "설정이 맞다"의 증거가 아니다 — 프로퍼티 키를 README로 재확인한다.

영속성 경계 — skeleton과 같은 시점에 확정 (1.22.0–1.27.0)

나중에 정하면 그때는 이미 우회가 쌓여 있다 — LAZY 접근이 터지는 지점마다 EAGER로 바꾸는 식으로 번지기 때문이다. 되돌리는 비용은 3단계로 쌓인다: 우연히 성공 → EAGER로 우회 → 경계를 잡으며 EAGER를 도로 LAZY로.

규칙내용
OSIV off spring.jpa.open-in-view: false항상 명시한다. 도입 시점을 판단할 항목이 아니라 JPA를 쓰는 순간부터 off다. 부재는 off가 아니라 on이다 — 항목이 없으면 Spring Boot 기본값 true가 적용된다
트랜잭션 경계 skeleton 단계 한정으로 Controller에 둔다(조회는 @Transactional(readOnly = true)). 경계만을 위해 서비스 계층을 새로 만들지 않는다
연관관계 LAZY 유지. EAGER는 전역 결정이라 목록 조회가 생기는 순간 N+1이고 그때는 되돌리기 어렵다 — 필요한 지점에서 fetch join·@EntityGraph로 명시적으로 당긴다
반환 타입 엔티티가 아니라 DTO. 엔티티를 반환하면 JSON 직렬화가 컨트롤러 바깥에서 일어나 LazyInitializationException(HTTP 500)이다. 이 실패는 테스트에 안 보인다 — 클래스 레벨 @Transactional 안에서 직렬화가 끝나므로 테스트는 초록색, 실서버만 500
실전 교정 (1.24.0)
가드가 경계보다 먼저 (쓰기 경로) 경계가 없는 동안은 메서드 단위 커밋이라 조회 결과가 곧바로 detached — 위험이 없으니 가드 없는 상태가 정상으로 보인다. 경계를 Controller로 올리는 그 변경 하나가 leak 경로를 연다. 명시적 save() 규율과 그 회귀 테스트가 경계와 같은 커밋에 들어가야 한다
조회 경로에는 순서 문제가 없다 readOnly = true는 FlushMode를 MANUAL로 두어 auto-flush가 없다 — 경계를 만드는 애노테이션이 가드를 겸한다. 존재하지 않는 위험에 대비하려다 detach를 시도하게 되는 것이 문면 탓이었다
가드로 detach를 쓰지 않는다 detach는 LAZY 유지와 배타적이다 — 조회 시점에 detach하면 이후 지연 로딩이 전부 LazyInitializationException. 쓰기 경로의 가드는 Controller 경계 테스트로 세운다
계약 테스트에서 LAZY 연관을 빼지 않는다 빼면 그 부분 계약이 어디에서도 검증되지 않는다. 포트의 계약을 “완전한 애그리게이트를 돌려준다”로 정하고 조회 메서드에 @EntityGraph를 건다 — 매핑은 LAZY, 당기는 것은 조회 지점에서 명시

조용한 실패 — 관찰된 정상 상태는 설정을 보증하지 않는다 (1.28.0)

같은 함정을 네 번 다르게 겪었다. 공통 구조는 하나다 — 눈에 보이는 정상 신호를 “설정이 맞다”의 증거로 읽는 것. 흩어져 있으면 다섯 번째를 만난 사람이 또 처음부터 당하므로 Principles로 묶었다.

신호오독사실
SQL 로그가 나온다p6spy 설정이 맞다키를 틀려도 기본값으로 로그는 나온다
테스트가 통과한다그 검증이 일어났다트랜잭션·애노테이션 위치 때문에 검증이 공허할 수 있다
설정 항목이 없다그 기능은 꺼져 있다open-in-view의 부재는 off가 아니라 on이다
그 파일이 커밋된 적 없다커밋되지 않도록 막혀 있다우연히 안 들어갔을 뿐, 규칙이 없으면 언제든 들어간다

공통 처방: 정상으로 보이는 상태를 근거로 삼지 말고, 틀렸을 때 무엇이 달라지는지를 확인한다 — 로그는 출력 유무가 아니라 형태 차이로, 테스트는 보호 장치를 제거한 실패 주입으로, 설정은 명시 여부로, 규칙은 파일 존재가 아니라 규칙 자체의 존재로 판정한다. 통과했다는 사실 자체는 정보가 아니다.

도구는 최초로 필요해진 시점에 추가한다. 관측·검증 도구(p6spy·approvaltests)를 초기 셋업에 미리 넣지 않는 이유 셋 — thinnest 원칙과 충돌, 의존성마다 비용(p6spy는 Spring Boot 메이저에 묶여 있고 어긋나면 조용히 적용되지 않는다), 도입 커밋에 Why가 남는다. 다만 침묵하는 실패는 미룰 수 없고 시끄러운 실패는 미뤄도 된다 — OSIV를 안 끄면 경계 밖 지연 로딩이 조용히 성공해 그 위에 코드가 쌓이지만(불변 규칙), p6spy가 없으면 바인딩 값을 봐야 하는 순간 그 자리에서 막힌다(트리거).

plan 완료 후 갈림

tdd-plan 완료 web-app: cucumber-acceptance (필수) / general: 선택

cucumber-acceptance는 §2 Gherkin을 그대로 .feature + Four Layer(Steps→Protocol Driver→SUT)로 셋업하고, 미구현 시나리오는 @pending으로 표시한다. web-app에서는 이것이 인수 계층이므로 별도 High Level Test를 두지 않는다 — 같은 검증이 두 계층에 중복되면 안 되기 때문. 프로젝트 제약으로 Cucumber를 도입할 수 없을 때만 대표 시나리오 1개를 JUnit 인수 테스트로 대체한다.

구현 — 기어가 스킬을 고른다

둘 다 같은 tdd-red / tdd-green / tdd-blue 에이전트에 위임한다. 차이는 사용자 피드백을 어디서 기다리는가다.

상황기어호출
낯선 도메인·기술, 학습 목적, 설계 미확정low/tdd-rgb --gear=low
유사 문제 경험 있음, 설계가 안정되어 감mid/tdd-rgb --gear=mid
이론 명확, 테스트 목록 전체를 자율로high/tdd-rgb --gear=high
이론 명확, feature 하나를 plan 합의 후 자율로high/tdd-feature

--gear를 생략하면 진행 기록에 남은 기어로 복원되고, 명시하면 기록보다 우선한다. /tdd-feature--gear를 받지 않는다 — Phase B 자율 진행이 곧 high이기 때문이다.

tdd-rgb low 기어 · 매 단계 대기 R G B R G B R G B test 1 test 2 test 3 … ●=대기 tdd-feature = feature 범위 high plan 합의 적대적 리뷰 후 검토 test 1 R·G·B → test 2 R·G·B → test 3 R·G·B … (대기 없음)
같은 아홉 개의 R/G/B 스텝. tdd-rgb는 기어가 정한 지점에서, tdd-feature는 시작 전 합의와 완료 후 적대적 리뷰 두 곳에서만 사용자를 기다린다.

tdd-rgb [plan] [--gear=low|mid|high]

  • 기어가 검토 지점의 밀도만 조절 — TDD 3법칙·한 번에 테스트 하나·phase별 커밋은 모든 기어 동일
  • 기어마다 이해 채널이 다르다: low는 편집 직접 읽기, mid는 사이클 diff 검토 + tidying 지목, high는 행동 명세 + 표본 정독
  • 업시프트 억제: 테스트가 얇은 기존 코드에 걸친 슬라이스, 오래 유지보수할 핵심 도메인
  • 다운시프트 전에 먼저 설명을 제공 — 이해 결손이면 설명으로 해소, 실패(revert·예상 밖 실패)면 즉시 다운시프트

tdd-feature [설명|plan]

  • Phase A(문제·분해·완료조건 합의)까지만 인터랙티브
  • 합의 게이트 통과 후 Phase B — feature의 모든 test를 끝까지 자율 실행
  • Phase B = feature 범위의 high 기어 → high 안전장치 동일 적용: 폭발 반경 점검 · 시작 커밋 해시 기록 · 완료 보고 전 적대적 리뷰
  • WIP=1: 한 실행에 feature 하나만. 커밋은 test:/feat:/refactor:로 분리
  • low·mid 검토 밀도가 필요하면 이 스킬이 아니라 /tdd-rgb

두 스킬 공통 — 완료 시 하드닝 제안(선택) 1.37.0 신설 · 순서는 1.40.1 기준

이 항목만 1.31.0 이후 변경을 반영했다 — 나머지 지면은 여전히 1.31.0 기준.

구현이 끝나면 두 스킬 모두 최종 보고 마지막에 하드닝 제안을 붙인다. /tdd-rgb는 기어와 무관하게 전체 완료 시점에, /tdd-feature는 feature 완료 보고에 붙인다. 제안만 하고 자동 실행하지 않는다 — 실행 여부는 사용자가 정한다. 순서는 비용순이 아니라 파이프라인순이다.

  1. 정리할 곳 찾기 — CRAP 점검(crap4java-analyzer)과 중복 스캔(dry4java-analyzer). 변경 파일 한정, 수 초~수십 초
  2. 구조 정리/system-wide-refactoring이 ①이 지목한 메서드·중복 쌍을 기법별 커밋으로 정리
  3. 테스트 강화mutate4java-runner로 뮤테이션 테스트. ②를 마친 뒤에 돌린다

③이 마지막인 이유 — tidying은 동작을 보존하지만 뮤테이션 지점 자체를 바꾼다. 메서드를 추출하면 새 뮤턴트 자리가 생겨서, 정리 전에 얻은 “전 뮤턴트를 죽였다”는 결론이 정리 후에는 성립하지 않는다. 파일당 수 분이 드는 비용도 그대로 버려진다. Uncle Bob의 파이프라인에서 Hardener가 Cleaner 다음에 오는 것과 같은 이유다. (CRAP·mutation은 Maven 프로젝트 한정, DRY는 빌드 도구와 무관.)

RGB 위임 사이클

두 실행기 모두 test 하나마다 이 세 에이전트를 순서대로 호출한다.

Red test: Green feat: Blue refactor: 실패하는 테스트만 최소 구현으로 통과 Local Tidying (변경 있을 때만) 다음 test로
세 에이전트(sonnet) 모두 커밋 형식은 재기술하지 않고 reviewable-commits.md를 참조만 한다 — 단일 출처 유지. web-app에서는 Green이 자기 시나리오의 @pending도 같은 커밋에서 해제한다.
Red의 승인 테스트 판단 (1.25.0–1.26.0)기준
Approvals인가 개별 단정인가 개수가 아니라 검증 대상으로 가른다 — 출력 형상(응답 본문·영수증·점수판)이면 Approvals, 주장이 하나면 개별 단정. 필드를 골라 단정하는 순간 고르지 않은 필드는 검증에서 빠지고, 빠졌다는 사실이 테스트에 드러나지 않는다
와이어 포맷 승인 + 도메인 출력 승인 배타적 선택이 아니라 한 파일에 두 구획 병기. 위험한 것은 교체이지 병기가 아니다 — raw만 깨지면 표현이 바뀐 것이고 둘 다 깨지면 값이 바뀐 것이라, 실패 diff가 어느 층위인지까지 알려준다. (실측: long → double 변경에 raw는 8000 → 8000.0으로 깨졌지만 printer의 “단가: 8,000원”은 그대로였다)
새 승인은 실패 주입으로 확인 Scrubber는 최소로. 재직렬화하면 도입 목적인 와이어 포맷 검증력을 스스로 없앤다. 승인 파일(*.approved.txt)은 명세이므로 추적하고, 실행 산물(*.received.txt)만 gitignore

독립 진입점 — 메인 파이프라인 밖

plan → RGB 흐름과 무관하게, 필요할 때 바로 부른다.

tdd-legacy <FQCN|경로>

테스트 없는 기존 코드 → Characterization → Approval → Mutation test 안전망 구축 후 개선 스킬로 handoff.

tdd-tidy [commit-ref]

git diff로 변경 파일 자동 탐지 → RGB 없이 tdd-blue의 Local Tidying만 독립 실행.

system-wide-refactoring [commit-ref]

분석 → 후보 제시 → 확인 → 별도 브랜치·기법별 커밋 → PR 생성.

개별 리팩토링 기법 (18개)

순서가 없다 — tdd-blue / tdd-tidy 단계에서 상황에 맞는 것만 이름으로 호출한다.

discover-value-objectfirst-class-collectionencapsulate-collection extract-method-objectintroduce-special-casereplace-conditional-with-poly segregate-functional-coreseparate-query-modifierreplace-loop-with-pipeline replace-temp-with-queryintroduce-assertionintroduce-parameter-object explicit-parametersintent-revealing-namesnaming-process decompose-conditionalconsolidate-conditionallift-up-conditional

1.31.0에서 18개에 복사돼 있던 공통 절차(대상 파일 수집·후보 제시 형식·테스트·커밋 규칙)를 정본 1개로 추출했다 — 각 스킬에는 기법 고유의 것만 남는다: 무엇을 찾는가 · 어떻게 바꾸는가 · 그 기법의 실패 조건. 브랜치+PR은 기본에서 빠지고 조건절로 옮겼다(폭발 반경이 큰 영역, 여러 파일의 공개 API를 바꿀 때, 팀이 리뷰를 요구할 때).