--- title: Sample Fixture를 버리는 예제가 아니라 Adoption 계약으로 만들기 source_type: blog status: verified confidence: high tags: [blog, ca-tmpl, sample-fixture, adoption] related_projects: [ca-tmpl] last_reviewed: 2026-07-03 canonical_sources: - wiki/projects/ca-tmpl/sample-fixture-and-adoption audience: backend-engineer target_publish: status_label: ready --- # Sample Fixture를 버리는 예제가 아니라 Adoption 계약으로 만들기 ## Parent / 부모 (필수) - 핵심 canonical: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] - 관련 개념 문서: [[wiki/concepts/sample-fixture-and-adoption]] - sample fixture/adoption의 일반 배경. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다. ## 타깃 독자 / Target reader - 독자 profile: template project의 sample domain을 어떻게 유지/제거할지 고민하는 개발자. - 이미 안다고 가정하는 것: sample app, fixture, template adoption. - 처음 듣는다고 가정하는 것: sample을 데모가 아니라 architecture rule과 operational contract를 검증하는 corpus로 사용하는 방식. ## 도입 / Hook Template repository의 sample code는 애매합니다. 남겨두면 실제 서비스 코드처럼 오해받고, 지우면 skeleton이 정말 동작하는지 보여줄 corpus가 사라집니다. 특히 Clean Architecture skeleton에서는 sample이 단순 CRUD 데모를 넘어 envelope, authorization, transaction, idempotency, outbox, OpenAPI snapshot 같은 계약을 실제 흐름으로 건드리는 역할을 합니다. ca-tmpl은 sample을 “나중에 지울 예제”로만 보지 않았습니다. `sample-portfolio` module을 fixture로 유지하고, 동시에 `sampleOffTest`로 sample이 빠진 classpath에서도 core test suite가 컴파일/실행되는지 확인합니다. sample-on과 sample-off를 둘 다 검증하는 구조입니다. 이 글은 sample fixture를 adoption 계약으로 다루는 이유와, 아직 실제 외부 프로젝트 adoption 경험으로 말하면 안 되는 부분을 정리합니다. ## 본문 outline / Body outline 1. sample domain의 목적 - 데모가 아니라 contract proof. 2. sample-on과 sample-off를 둘 다 검증하는 이유. 3. `sampleFixture` configuration과 `sampleOffTest` source set. 4. `SampleRemovalSmokeContractTest`가 막는 회귀. 5. 외부 adoption 사례는 없음. ## 본문 / Body 좋은 skeleton에는 작동하는 예제가 필요합니다. 문서만 보고 architecture rule을 이해하기는 어렵습니다. ca-tmpl의 `sample-portfolio`는 WorkLog 도메인을 통해 use case, controller, persistence adapter, id generation, validation, idempotency, outbox, OpenAPI snapshot 같은 표면을 실제로 건드립니다. 그래서 sample은 “보여주기 화면”이 아니라 contract를 깨뜨렸을 때 테스트가 반응하는 corpus입니다. 하지만 sample이 production runtime에 섞이면 다른 문제가 생깁니다. downstream project가 template을 가져간 뒤에도 sample package가 core module의 production dependency에 남아 있으면, sample을 지우는 순간 build가 깨질 수 있습니다. 더 나쁘게는 production app이 sample route나 sample bean을 몰래 품은 채 출발할 수 있습니다. 그래서 ca-tmpl은 sample 제거를 runtime toggle이 아니라 build/test classpath 문제로 다룹니다. 핵심은 `sampleFixture` configuration과 `sampleOffTest`입니다. ordinary test는 sample fixture를 볼 수 있습니다. sample-on axis에서 sample이 contract corpus로 작동해야 하기 때문입니다. 반면 `sampleOffTest`는 같은 app-bootstrap core test source를 sample-portfolio 없이 컴파일하고 실행합니다. 즉 sample이 빠져도 core skeleton이 sample type에 의존하지 않는지 확인합니다. `SampleRemovalSmokeContractTest`는 이 경계를 여러 방식으로 확인합니다. production module의 build.gradle에서 `sample-portfolio`가 test 또는 sampleFixture scope 밖으로 들어오지 않는지 봅니다. app-bootstrap core test가 `dev.caskeleton.sample.portfolio.*`를 import하지 않는지도 확인합니다. `sampleOffTest` source set과 task가 선언되어 있는지, CI workflow에 `sample-off` job과 `./gradlew :app-bootstrap:sampleOffTest`가 있는지도 검사합니다. GitHub Actions에도 sample-off axis가 있습니다. ordinary quality-gates job은 sample-on axis이고, `sample-off` job은 sample-portfolio가 compile/runtime classpath에 없는 상태에서 `:app-bootstrap:sampleOffTest`와 architecture dependency matrix를 돌립니다. 이것은 실제 외부 프로젝트 adoption을 검증했다는 뜻은 아닙니다. 하지만 template 내부에서는 “sample을 지워도 core가 sample에 기대지 않는다”는 방향을 테스트로 표현합니다. 이 방식은 sample을 무조건 오래 남기자는 뜻도 아닙니다. downstream project에서는 sample을 지울 수 있습니다. 다만 지우기 전에 sample-off build가 green이어야 합니다. sample을 먼저 지워서 어떤 계약이 깨졌는지 모르게 만드는 것보다, sample-on으로 reference behavior를 보고 sample-off로 제거 가능성을 확인하는 편이 안전합니다. 주의할 점도 있습니다. canonical에는 예전 `sample-ticket` 12 scenario matrix 같은 계획성 문장과 현재 `sample-portfolio` 구현이 함께 남아 있습니다. 이 글에서 구현 사실로 말할 수 있는 것은 `sample-portfolio`, `sampleFixture`, `sampleOffTest`, `SampleRemovalSmokeContractTest`, CI sample-off job, 로컬 `./gradlew check` 범위입니다. 외부 프로젝트가 ca-tmpl을 adoption했고 도입 시간이 줄었다는 식의 주장은 아직 없습니다. ## 코드 예제 / Code samples (있다면) ```groovy // 출처: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] // 실제 파일: app-bootstrap/build.gradle, ca-tmpl @f6fbd4e196b4 configurations { sampleFixture { canBeConsumed = false canBeResolved = false } } sourceSets { sampleOffTest { java.srcDirs = sourceSets.test.java.srcDirs resources.srcDirs = sourceSets.test.resources.srcDirs compileClasspath += sourceSets.main.output runtimeClasspath += sourceSets.main.output } } ``` ```groovy // 출처: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] // 실제 파일: app-bootstrap/build.gradle, ca-tmpl @f6fbd4e196b4 dependencies { sampleFixture project(':sample-portfolio') } tasks.register('sampleOffTest', Test) { description = 'Compiles and runs the core test suite without sample-portfolio on the classpath.' testClassesDirs = sourceSets.sampleOffTest.output.classesDirs classpath = sourceSets.sampleOffTest.runtimeClasspath systemProperty 'ca.sample.mode', 'off' } ``` ```java // 출처: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] // 실제 파일: app-bootstrap/.../SampleRemovalSmokeContractTest.java, ca-tmpl @f6fbd4e196b4 void sampleClassIsAbsentFromTheSampleOffTestClasspath() { Assumptions.assumeTrue( "off".equals(System.getProperty("ca.sample.mode")), "sample classpath absence is verified only by sampleOffTest"); assertThat(isClassPresent("dev.caskeleton.sample.portfolio.SamplePortfolioApplication")) .as("sampleOffTest must not contain the sample-portfolio jar") .isFalse(); } ``` ```yaml # 출처: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] # 실제 파일: .github/workflows/ci-quality-gates.yml, ca-tmpl @f6fbd4e196b4 sample-off: runs-on: ubuntu-latest steps: - name: sampleOffTest + clean architecture dependency matrix working-directory: src run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace ``` ## Sources / 근거 (canonical 인용 필수, derived layer 의무) - [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] - 이 글의 1차 canonical. `sample-portfolio`, sample fixture/adoption decision, `sampleFixture`, `sampleOffTest`, `SampleRemovalSmokeContractTest`, CI sample-off job, 외부 adoption 미검증 경계를 따른다. - [[wiki/concepts/sample-fixture-and-adoption]] - 관련 개념 문서. template sample과 adoption strategy의 일반 배경으로만 둔다. ## 사실 vs 의견 / Fact vs opinion 구분 - 사실: ca-tmpl에는 `sample-portfolio` module, `sampleFixture` configuration, `sampleOffTest` task, `SampleRemovalSmokeContractTest`, CI `sample-off` job이 존재한다. 근거: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] - 사실: `./gradlew check`가 2026-07-02 기준 통과했고, sample-off 관련 task가 check graph에 포함되어 실행된 것으로 기록되어 있다. 근거: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] - 사실: 외부 프로젝트 adoption 사례, hosted release 차단 사례, adoption 시간 측정값은 없다. 근거: [[wiki/projects/ca-tmpl/sample-fixture-and-adoption]] - 의견: sample은 빨리 지울 데모보다 architecture contract를 증명하는 corpus로 남기는 편이 skeleton 학습에 유리하다. - 알지 못하는 것: 실제 template consumer의 migration friction, sample 제거에 걸린 시간, 조직별 adoption pattern. ## 답할 수 있는 범위 / Answer boundary - 자신 있게 답할 수 있는 후속 질문: - sample domain이 어떤 contract를 검증하는 corpus인가? - sample-on과 sample-off를 둘 다 검증하는 이유는 무엇인가? - `sampleOffTest`가 runtime toggle이 아니라 classpath contract인 이유는 무엇인가? - 다음 글로 넘길 부분: - 실제 외부 프로젝트 adoption report. - sample 제거 자동화 script. - Backstage나 Cookiecutter 같은 generator형 adoption과의 비교. ## 게시 체크리스트 / Publish checklist - [x] 모든 사실 주장에 canonical 링크 있음 - [x] 사실 vs 의견 분리 명시됨 - [x] 금지 마케팅 표현 없음 - [x] 코드 예제 출처 명시 - [x] 타깃 독자 가정과 톤 일치 - [x] `/lint` 통과 - [ ] 게시 URL 기록 (게시 후): ## Related / 관련 - 후속 글 후보: [[wiki/blog/ca-tmpl-clean-architecture-package-layout-2026-07-02]] - 후속 글 후보: [[wiki/blog/ca-tmpl-devops-ci-supply-chain-dx-2026-07-02]]