PLUGIN WORKFLOW MAP · msbaek-tdd 1.31.0
오케스트레이터가 짜는 전체 절차
요구사항 원천(tdd-plan-input)에서 시작해 템플릿 생성(/tdd) →
계획(tdd-plan) → 구현(tdd-rgb / tdd-feature)으로
이어지는 메인 파이프라인과, 그 바깥에서 독립적으로 쓰이는 레거시·정리 스킬들의 전체 배치.
-plan-input.md는 요구사항 원천(사람이 준 규칙·예제), -plan.md는 작성 대상 템플릿(절차 섹션과 체크박스)이다.1.19.0 이후 바뀐 것
-
1.21.0
tdd-plan-input신설 — 계획 앞에 요구사항 원천 문서 단계가 생겼다. 주제 한 줄이나 흩어진 메모에서 시작해 질의응답으로 §1~§6을 채운다. 예제 수치의 정본은 사람 계산이고, 빈칸은 채우지 않고 질문 / §6 미확정 / §1 제외 목록 셋 중 하나로 보낸다. - 1.20.0 경계 조건 식별에 집계 경계 범주 추가 — 같은 키가 여러 항목으로 나뉠 때 검증 단위(항목별)와 반영 단위(합산)가 어긋나면 그 차이가 곧 결함이다(오버셀).
-
1.22.0–1.24.0
영속성 경계 확정과 실전 교정. 적용 순서는
경계 먼저→ 가드가 경계보다 먼저, 가드는detach→ Controller 경계 테스트(detach는 LAZY 유지와 배타적), 계약 테스트는 LAZY 연관을검증에서 제외→@EntityGraph로 완전한 애그리게이트. -
1.25.0–1.26.0
Approvals 판단 기준을 개수가 아니라 검증 대상으로 — 출력 형상이면 Approvals, 주장 하나면 개별 단정.
와이어 포맷 승인과 도메인 출력 승인은
배타적 선택→ 한 파일에 두 구획 병기. -
1.27.0
OSIV off를
기본→ 불변 규칙으로. 그리고 설정 항목의 부재는 off가 아니라 on이다. - 1.28.0 네 곳에 흩어져 있던 같은 함정을 “조용한 실패” 원칙으로 묶고, 도구는 최초로 필요해진 시점에 추가한다는 규칙을 함께 올렸다.
-
1.29.0–1.31.0
상시 로드 분량 정리 — Mocking 근거(
tdd-rgb511줄로),tdd-plan1082 → 478줄, 리팩토링 18개 스킬의 공통 절차를 정본 1개로 추출(합계 −842줄). 브랜치+PR은 기본에서 빠지고 조건절로.
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-usecase→ web-app —general과 같은 축(무엇을 만드는가)에서 대조되도록. -
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에서 쓰이는 자리 |
|---|---|---|
| §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-plan — 계획 단계 (인터랙티브)
인자는 /tdd가 만든 템플릿, 요구사항은 -plan-input.md 참조.
단계마다 사용자 승인 후 커밋하고, Gherkin 이후 지점에서 두 유형이 갈라져 다시 합치지 않는다.
점으로 표시된 단계(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가 인터페이스 X의 XImpl을 커스텀 구현 프래그먼트로 자동 병합 — 포트 구현체(어댑터) 이름을 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 완료 후 갈림
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 [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 완료 보고에 붙인다. 제안만 하고 자동 실행하지 않는다 — 실행 여부는
사용자가 정한다. 순서는 비용순이 아니라 파이프라인순이다.
- 정리할 곳 찾기 — CRAP 점검(
crap4java-analyzer)과 중복 스캔(dry4java-analyzer). 변경 파일 한정, 수 초~수십 초 - 구조 정리 —
/system-wide-refactoring이 ①이 지목한 메서드·중복 쌍을 기법별 커밋으로 정리 - 테스트 강화 —
mutate4java-runner로 뮤테이션 테스트. ②를 마친 뒤에 돌린다
③이 마지막인 이유 — tidying은 동작을 보존하지만 뮤테이션 지점 자체를 바꾼다. 메서드를 추출하면 새 뮤턴트 자리가 생겨서, 정리 전에 얻은 “전 뮤턴트를 죽였다”는 결론이 정리 후에는 성립하지 않는다. 파일당 수 분이 드는 비용도 그대로 버려진다. Uncle Bob의 파이프라인에서 Hardener가 Cleaner 다음에 오는 것과 같은 이유다. (CRAP·mutation은 Maven 프로젝트 한정, DRY는 빌드 도구와 무관.)
RGB 위임 사이클
두 실행기 모두 test 하나마다 이 세 에이전트를 순서대로 호출한다.
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 흐름과 무관하게, 필요할 때 바로 부른다.
테스트 없는 기존 코드 → Characterization → Approval → Mutation test 안전망 구축 후 개선 스킬로 handoff.
git diff로 변경 파일 자동 탐지 → RGB 없이 tdd-blue의 Local Tidying만 독립 실행.
분석 → 후보 제시 → 확인 → 별도 브랜치·기법별 커밋 → PR 생성.
개별 리팩토링 기법 (18개)
순서가 없다 — tdd-blue / tdd-tidy 단계에서 상황에 맞는 것만 이름으로 호출한다.
1.31.0에서 18개에 복사돼 있던 공통 절차(대상 파일 수집·후보 제시 형식·테스트·커밋 규칙)를 정본 1개로 추출했다 — 각 스킬에는 기법 고유의 것만 남는다: 무엇을 찾는가 · 어떻게 바꾸는가 · 그 기법의 실패 조건. 브랜치+PR은 기본에서 빠지고 조건절로 옮겼다(폭발 반경이 큰 영역, 여러 파일의 공개 API를 바꿀 때, 팀이 리뷰를 요구할 때).