# Clean Architecture Frontend Template — Senior Architecture Review > 검토일: 2026-09-17 > > 검토 기준: `develop` / `c10a709f2cdea9d58d2f4816b48b4c41d0b5268b` > > 검토 관점: 장기 유지보수, Clean Architecture 경계, reusable adapter/capability 설계, TypeScript 타입 안전성, 테스트 품질과 실행 신뢰성, 개발자 경험 ## 1. 결론 이 저장소를 단순한 **React 프로젝트 템플릿**으로 평가하면 현재 구조는 지나치게 크고 복잡하다. 그러나 실제 목적을 반영하면 평가는 달라진다. 이 저장소의 목표는 다음에 더 가깝다. > **비즈니스 use case와 domain을 기술 세부사항에서 보호하면서, HTTP·Server State·IndexedDB·OPFS·WebSocket·Upload/Download 같은 클라이언트 capability를 미리 구현해 두고, 새 제품 기능에서는 비즈니스 타입과 mapper/codec/policy를 결합해 최대한 재사용할 수 있는 Frontend Application Foundation.** 즉 `Starter Template + Reusable Capability Platform`의 혼합형이다. 따라서 IndexedDB runtime이 크다거나 WebSocket 구현이 복잡하다는 이유만으로 제거하거나 단순화해서는 안 된다. 이런 복잡성은 여러 feature에서 한 번 구현한 기술 문제를 재사용하기 위한 비용일 수 있다. 진짜 판정 기준은 다음이다. 1. feature/use case 개발자가 adapter 내부 구현을 알아야 하는가. 2. use case가 요구하는 비즈니스 타입을 TypeScript generic으로 안전하게 adapter binding에 적용할 수 있는가. 3. 기능별 차이를 mapper/codec/policy만 주입해 표현할 수 있는가. 4. 공통 capability가 요구사항에 맞지 않을 때 custom adapter를 자연스럽게 구현할 수 있는가. 5. 플랫폼 내부의 registry, lifecycle, retry, storage, browser API 복잡도가 application/domain으로 새지 않는가. 6. 플랫폼을 사용하는 비용이 플랫폼을 직접 새로 구현하는 비용보다 충분히 낮은가. 이 기준으로 보면 현재 저장소는 **방향은 맞고 기술적 기반도 강하지만, consumer surface와 검증 구조가 플랫폼 내부 복잡도를 충분히 숨기지 못하고 있다.** 현재 가장 중요한 문제는 “플랫폼 기능이 너무 많다”가 아니라 다음 세 가지다. - **공통 capability를 사용하는 feature 작성 경로가 아직 충분히 작지 않다.** - **테스트 taxonomy와 실행 환경이 self-contained하지 않아 테스트 결과를 신뢰하기 어렵게 만든다.** - **플랫폼 내부 구현과 CI assurance가 매우 커졌는데, 이 복잡성이 명확한 모듈 경계로 격리되지 않았다.** --- ## 2. 검토 기준선 현재 저장소 규모는 다음과 같다. | 영역 | 파일 수 | LOC | | --- | ---: | ---: | | `src/**` | 311 | 83,947 | | `src/adapters/**` | 129 | 59,120 | | `src/application/**` | 41 | 3,525 | | `src/bootstrap/**` | 13 | 1,880 | | `src/contracts/**` | 39 | 8,144 | | `src/features/**` | 24 | 2,096 | | `src/presentation/**` | 65 | 9,182 | | `tests/**` TypeScript | 308 | 72,120 | | `scripts/**` TypeScript | 123 | 28,397 | | `package.json` scripts | 114 | - | 제품 dependency는 6개, dev dependency는 29개다. 외부 라이브러리 숫자를 무작정 늘린 구조는 아니다. 복잡성의 상당 부분을 저장소가 직접 소유하고 있다는 의미다. 가장 큰 production source는 다음과 같다. | 파일 | LOC | | --- | ---: | | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | 2,744 | | `src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts` | 2,298 | | `src/adapters/realtime/websocket/websocket-connection.ts` | 2,003 | | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | 1,971 | | `src/adapters/cache-storage/public-response-cache-adapter.ts` | 1,949 | | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | 1,881 | | `src/adapters/realtime/reconnect-coordinator.ts` | 1,796 | | `src/adapters/browser-rpc/browser-rpc-runtime.ts` | 1,696 | | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | 1,649 | | `src/adapters/http/http-execution-v3.ts` | 1,602 | 큰 파일 자체를 결함으로 판정하지 않는다. 다만 이 규모에서는 내부 책임 분리가 명확하지 않으면 수정 시 regression 위험이 빠르게 증가한다. --- ## 3. 현재 구조에서 잘된 부분 ### 3.1 Clean Architecture 경계가 실제로 집행된다 이 저장소의 가장 강한 부분이다. `presentation`, `application`, feature code가 `fetch`, `IndexedDB`, `WebSocket`, browser storage 같은 native I/O를 임의로 직접 호출하는 구조가 아니며, 정적 architecture gate가 dependency 방향을 검사한다. 검토 시점 `check:architecture`는 다음 기준선을 통과했다. ```text 307 modules 929 dependencies PASS ``` 금지 dependency fixture도 실제로 거절된다. 즉 폴더 이름만 `domain/application/adapter`로 만든 구조가 아니라 **의존 방향을 executable rule로 만든 구조**다. ### 3.2 TypeScript generic을 이미 핵심 계약에 사용하고 있다 사용자가 원하는 “use case에서 결정되는 비즈니스 타입을 공통 adapter capability에 적용”할 기반은 이미 존재한다. 예를 들어 V3 HTTP executor는 다음 형태다. ```ts interface ContractHttpExecutor { execute( operation: InstalledHttpContract, input: Input, context: HttpExecutionContext, ): Promise>; } ``` 즉 HTTP runtime은 특정 `User`, `Order`, `ReferenceResource`를 알지 않는다. `Input`, `WireOutput`, `Problem` 타입 파라미터만 안다. Server State도 `BoundMutation`와 같이 feature의 command/result 타입을 generic으로 받는다. 또 `ApplicationFeatureInputs`는 module augmentation을 이용해 concrete feature를 generic application에 직접 import하지 않고도 typed feature API를 등록한다. 이 방향은 유지할 가치가 높다. ### 3.3 reference feature가 vertical slice의 실제 증거 역할을 한다 `reference-feature`는 단순 데모 화면이 아니다. - domain은 외부 DTO/React를 모른다. - application은 input/gateway 계약을 가진다. - outbound adapter가 HTTP executor를 feature gateway로 투영한다. - mapper가 transport payload와 domain/application value 사이를 분리한다. - presentation은 application input을 호출하며 HTTP나 storage를 직접 알지 않는다. - sample removal test로 feature를 제거했을 때 generic platform이 유지되는지 확인한다. 즉 목표 아키텍처를 실제 실행 가능한 vertical slice로 증명하고 있다는 점은 좋다. ### 3.4 테스트의 사고방식은 깊다 테스트는 단순 mock 호출 횟수 검증에 머무르지 않는다. 예를 들어 query/mutation test는 cancellation, scope fence, stale data, optimistic mutation, effect certainty, reconciliation, duplicate submission을 실제 lifecycle 관점에서 검증한다. HTTP/storage/realtime도 정상 경로뿐 아니라 timeout, late settlement, retry, mutation effect, lifecycle cleanup 등을 검증한다. 따라서 테스트의 핵심 문제는 **검증 깊이가 부족한 것**이 아니다. 오히려 검증 범위가 커지면서 **분류와 실행 경계가 무너진 것**이 문제다. --- ## 4. 이 저장소의 정체성을 명확히 해야 한다 현재 문서와 코드에는 `template`, `platform`, `reference feature`, `optional capability`라는 표현이 모두 존재한다. 각각 틀린 표현은 아니지만, 최상위 모델이 명확하지 않으면 이후 리팩터링에서 서로 반대 방향의 결론을 낼 수 있다. 이 저장소는 다음 두 축을 동시에 가진다고 정의하는 것이 가장 정확하다. ```text Frontend Application Foundation │ ├── Starter / Composition Skeleton │ ├── bootstrap │ ├── routing │ ├── application provider │ ├── reference feature │ └── project conventions │ └── Reusable Capability Platform ├── HTTP ├── Server State ├── Auth boundary ├── IndexedDB / OPFS ├── Cache Storage ├── Realtime / WebSocket ├── Browser RPC ├── Upload / Download ├── Diagnostics / Telemetry └── optional browser capabilities ``` 중요한 점은 **둘을 물리적으로 반드시 별도 repository/package로 분리해야 한다는 의미가 아니다.** 먼저 코드와 문서에서 책임을 명확히 해야 한다. - Starter는 “어떻게 조립하는가”를 보여준다. - Feature는 “비즈니스 요구를 어떻게 표현하는가”를 보여준다. - Capability Platform은 “기술 문제를 어떻게 재사용하는가”를 책임진다. - CI/Assurance는 “이 계약이 깨지지 않았음을 어떻게 증명하는가”를 책임진다. 이 네 영역이 같은 repository 안에 있어도 된다. 문제는 한 영역을 사용하기 위해 다른 세 영역의 내부 구현을 알아야 하는 경우다. --- ## 5. 목표 Adapter 모델 ### 5.1 use case가 adapter에 의존하면 안 된다 사용자의 표현인 “use case에서 나오는 비즈니스 타입을 adapter에 바로 적용한다”는 의도는 맞다. 다만 dependency 방향은 정확히 구분해야 한다. 원하는 구조는 다음이다. ```text Domain / UseCase │ │ owns ▼ Business Port ▲ │ implements / binds │ Feature Adapter Binding │ │ supplies generic types + mapper + policy ▼ Reusable Capability Runtime ``` use case가 `HttpAdapter`를 직접 생성하거나 import하는 구조가 아니다. use case는 자신의 port만 안다. ```ts interface UserRepository { findById(id: UserId): Promise>; } ``` 그리고 feature-owned outbound adapter/composition이 reusable HTTP capability를 구체화한다. ```ts createHttpBinding({ contract, decode: decodeUserWireDto, map: mapUser, mapFailure, }); ``` 이때 `User`는 generic type argument로 capability에 들어갈 수 있지만 capability source가 `User` domain module을 import하지는 않는다. ### 5.2 제네릭만으로 해결하지 않는다 재사용 adapter의 목표를 `Repository` 하나로 모든 기술을 추상화하는 것으로 잡으면 실패한다. HTTP, IndexedDB, WebSocket, Upload는 failure semantics와 lifecycle 자체가 다르다. 공통화 단위는 다음 조합이 적절하다. ```text Generic Type + Codec / Mapper + Policy + Capability Runtime ``` 예를 들어 HTTP는 다음을 공통 runtime이 책임질 수 있다. - timeout/deadline - cancellation - retry - auth attachment - bounded response read - effect certainty - diagnostics/telemetry - transport failure normalization Feature는 다음만 책임져야 한다. - operation identity - request input - wire schema - wire → domain mapper - feature-specific failure mapping - 필요한 retry/idempotency policy 선택 IndexedDB는 HTTP와 다른 capability factory를 가져야 한다. - key extraction - encode/decode - schema/migration policy - domain-specific repository operation WebSocket/realtime은 다시 다른 API가 맞다. - event decoder - event authority/revision policy - subscription ownership - reconnect/resume policy 즉 **“하나의 generic repository”가 아니라 “capability-specific typed factory”**를 목표로 하는 것이 맞다. ### 5.3 custom adapter escape hatch는 반드시 남긴다 Reusable capability가 80~90%의 일반 요구를 해결하더라도 제품 요구가 capability contract와 맞지 않을 수 있다. 따라서 platform API는 다음을 강제해서는 안 된다. > 모든 HTTP/storage/realtime 구현은 반드시 platform factory를 사용해야 한다. 정확한 규칙은 다음이 좋다. > 공통 capability contract가 요구사항을 보존할 수 있으면 재사용한다. 요구사항을 왜곡해야 사용할 수 있다면 feature-owned custom adapter를 구현한다. 단 application port와 architecture boundary는 동일하게 유지한다. 이 원칙이 있어야 platform abstraction이 business model을 끌어당기지 않는다. --- ## 6. 현재 구현이 목표 모델에 근접한 부분과 남은 간극 ### 6.1 이미 근접한 부분 현재 `ContractHttpExecutor`은 좋은 기반이다. feature에서는 `ReferenceOperationMap`을 통해 operation마다 request/value type을 묶고, raw executor 결과를 domain type으로 투영한다. `ApplicationFeatureInputs`도 concrete feature를 generic application에서 분리한다. 즉 **타입을 parameterize하는 방향 자체는 이미 맞다.** ### 6.2 아직 feature adapter boilerplate가 크다 `reference-http-gateway.ts`를 보면 feature가 다음을 모두 직접 작성한다. - operation map - request type projection - result type projection - `execute` 호출 - result guard - mapping contract failure 생성 이 중 일부는 feature-owned이어야 한다. 그러나 동일한 형태가 feature마다 반복된다면 재사용 platform의 consumer API가 충분히 올라오지 않은 것이다. 향후 실제 feature 2~3개를 추가해 다음을 측정해야 한다. - HTTP feature 하나 추가 시 작성해야 하는 adapter glue LOC - 동일 패턴 반복 비율 - platform 내부 type 이름을 feature 개발자가 알아야 하는 개수 - 등록해야 하는 중앙 registry 수 - 정상 query 하나를 연결하기 위해 건드리는 파일 수 이 지표가 platform usability를 판단하는 핵심이다. ### 6.3 설치 지점이 여러 곳으로 나뉜다 현재 reference feature README가 명시하는 설치 지점은 세 곳이다. - `installed-feature-contracts.ts` - `installed-feature-runtimes.tsx` - `installed-feature-adapters.ts` 현재 총 181 LOC라 파일 크기 자체는 크지 않다. 문제는 feature가 늘었을 때 **한 기능 설치가 여러 중앙 catalog 수정으로 확산되는 구조**라는 점이다. 이 구조는 계약/runtime/composition을 분리한다는 장점이 있으므로 무작정 한 객체로 합치면 안 된다. 대신 다음 목표가 필요하다. > feature-owned contribution을 각 boundary가 소비하되, 새 feature 개발자가 중앙 설치 파일의 내부 구조를 반복해서 편집하지 않게 한다. 예를 들어 feature가 contract/runtime/adapter contribution을 각각 export하고, installed catalog는 단순 aggregation만 담당하는 구조가 적절하다. ### 6.4 `ApplicationApi`는 지금은 관리 가능하지만 Service Locator로 성장할 수 있다 현재 `ApplicationApi`는 다음 플랫폼 기능을 직접 노출한다. - session - preferences - diagnostics - runtime - recovery - features 구조 자체는 잘못이 아니다. presentation에서 concrete adapter를 숨기는 역할을 한다. 그러나 기능이 계속 추가되며 모든 application capability가 하나의 root object 아래 들어가면 다음과 같은 사용 패턴이 일반화될 수 있다. ```ts const application = useApplication(); ``` 이후 모든 presentation 코드가 거대한 application service locator를 바라보게 된다. 권장 방향은 root provider를 유지하더라도 consumer API를 좁히는 것이다. ```text useSession() useRuntimeCapabilities() useReferenceFeature() ``` 즉 composition은 하나여도 presentation dependency surface는 필요한 capability만 노출한다. --- ## 7. P0 — 테스트 실행 환경이 self-contained하지 않다 이 문제는 다른 리팩터링보다 먼저 고치는 것이 좋다. 테스트가 신뢰되지 않으면 이후 구조 변경의 안전망도 신뢰할 수 없다. ### 7.1 Component test가 host `NODE_ENV`에 영향을 받는다 검토 환경의 host에는 다음이 설정되어 있었다. ```text NODE_ENV=production ``` 현재 `package.json`의 `test:component`는 다음과 같이 Vitest를 바로 실행하며 test environment를 고정하지 않는다. ```text vitest run tests/component ... ``` 그 결과: ```text pnpm test:component → React.act is not a function → 119 tests failed ``` 반면 같은 checkout에서: ```text NODE_ENV=test pnpm test:component → 20 test files passed → 130 tests passed ``` 했다. 이것은 119개 component가 각각 잘못된 것이 아니라 **test runner가 외부 shell environment를 그대로 받아 React production test path를 로드한 것**이다. 템플릿/플랫폼 저장소의 테스트는 개발자 머신의 우연한 환경 값에 따라 의미가 바뀌면 안 된다. ### 권장 - component/unit/integration runner가 필요한 environment를 명시적으로 소유한다. - test bootstrap 단계에서 잘못된 `NODE_ENV`를 fail-fast하거나 고정한다. - CI에서만 성립하는 environment assumption을 일반 test script와 분리한다. --- ## 8. P0 — `unit` test taxonomy가 실제 실행 특성과 맞지 않는다 현재 `docs/testing/taxonomy.md`는 `test:unit`을 별도 test level로 정의하지만 `tests/unit` 안에는 실제로 host-level system assurance가 포함된다. 검토 환경에서: ```text pnpm test:unit → 99 failures ``` 가 발생했다. 하지만 주요 실패는 business/application logic regression이 아니었다. 대표 원인은 다음과 같다. ```text provider sandbox unavailable: /usr/bin/bwrap is required systemctl list-units failed: Failed to connect to bus npm_config_userconfig / npm_config_prefix / npm_config_globalconfig host environment ``` `ci-artifact-contract.test.ts`는 다음을 검증한다. - bubblewrap sandbox - systemd unit - cgroup/resource limit - process guardian/supervisor - background process kill/collection - archive/provider isolation - host path protection 이 테스트들은 가치가 있다. 제거할 대상이 아니다. 그러나 **unit test가 아니다.** 권장 taxonomy는 다음과 같다. ```text tests/ ├── unit/ │ ├── domain │ ├── application │ └── pure-policy │ ├── contract/ │ └── reusable-capability contracts │ ├── component/ │ └── React / hook / UI behavior │ ├── integration/ │ ├── HTTP/MSW │ ├── IndexedDB │ └── browser runtime boundaries │ ├── system/ │ └── ci-runner/ │ ├── sandbox │ ├── cgroup │ ├── process-supervision │ └── supply-chain │ └── e2e/ ``` 특히 `pnpm test:unit`은 **지원 Node 환경만 있으면 일반 개발 머신에서 deterministic하게 실행**되어야 한다. systemd/bwrap/cgroup이 필요하면 `test:ci-runner` 또는 `test:system`처럼 요구사항이 이름에 드러나야 한다. --- ## 9. P1 — Adapter runtime은 “크기”가 아니라 내부 경계를 기준으로 재검토해야 한다 기존에 2,000~2,700 LOC 파일을 단순히 작게 나누는 것은 권장하지 않는다. 이미 adapter review의 D-07이 적절한 원칙을 가지고 있다. > state machine과 Saga 경계로만 큰 runtime을 나누고, public capability identity/failure taxonomy/persisted schema/wire semantics를 유지한다. 이 원칙을 그대로 적용하는 것이 맞다. 예를 들어 `indexeddb-runtime.ts`를 다음 이유만으로 분리해서는 안 된다. > 2,744줄이니까 500줄씩 다섯 파일로 나누자. 대신 다음 책임이 독립적으로 설명되고 테스트 가능한지 본다. - connection/open/upgrade lifecycle - transaction ownership - schema/migration state machine - quota/budget policy - serialization/codec - mutation settlement - cleanup/compensation - recovery/reconciliation WebSocket도 같은 기준이다. - connection state machine - reconnect/backoff policy - heartbeat - subscription ownership - writer/lease lifecycle - resume/gap policy - message codec **하나의 변경 이유와 하나의 failure model을 공유한다면 같은 모듈에 있어도 된다.** 반대로 독립 lifecycle을 가진 책임이 같은 2,000줄 파일에서 mutable state를 공유한다면 추출 우선순위가 높다. --- ## 10. P1 — 테스트 파일도 behavior contract 단위로 분리해야 한다 현재 가장 큰 테스트 파일은 다음과 같다. | 파일 | LOC | | --- | ---: | | `ci-artifact-contract.test.ts` | 2,538 | | `presigned-transfer.test.ts` | 2,518 | | `image-cdn-runtime.test.ts` | 2,430 | | `application-query.test.tsx` | 2,196 | | `security-followup.test.ts` | 1,859 | | `public-response-cache.test.ts` | 1,711 | | `ci-workflow-generation.test.ts` | 1,635 | 테스트 파일이 production 파일보다 커지는 것 자체는 문제가 아니다. 복잡한 state machine은 많은 테스트를 필요로 한다. 문제는 서로 다른 behavior가 하나의 파일 안에 섞이면 실패 메시지가 ownership을 알려주지 못한다는 점이다. 예를 들어 `application-query.test.tsx`는 사실상 다음 여러 계약을 검증한다. ```text query initial/background state query scope fence result budget mutation scope fence optimistic mutation unknown effect reconciliation concurrency / duplicate submission cache invalidation ``` 따라서 다음처럼 behavior 단위로 분리하는 것이 더 낫다. ```text application-query/ ├── query-state.test.tsx ├── query-scope-fence.test.tsx ├── query-budget.test.tsx ├── mutation-scope-fence.test.tsx ├── optimistic-mutation.test.tsx ├── mutation-reconciliation.test.tsx ├── duplicate-submission.test.tsx └── fixtures.ts ``` 테스트 분리의 목표도 LOC가 아니다. > 하나의 실패 파일명이 “어떤 계약이 깨졌는지”를 설명해야 한다. --- ## 11. P1 — Capability consumer API를 실제 feature 개발 비용으로 평가해야 한다 현재 플랫폼은 내부 correctness에 대한 검증은 매우 강하다. 반면 앞으로는 **새 feature를 만드는 개발자의 비용**을 별도 품질 지표로 봐야 한다. 추천하는 평가 시나리오는 실제 sample feature 2개를 추가해보는 것이다. ### 시나리오 A — 일반 REST CRUD 필요 조건: - list/detail/create/update - pagination - error mapping - optimistic mutation 하나 측정: - feature-owned LOC - platform glue LOC - 수정 파일 수 - 중앙 registry 수정 수 - transport/runtime type을 직접 알아야 하는 횟수 ### 시나리오 B — IndexedDB local draft 필요 조건: - domain draft type - save/find/remove - codec - migration 없는 단순 store 측정: - IndexedDB native API를 feature가 알아야 하는가 - runtime internal type을 import해야 하는가 - `create...Repository` 계열 binding만으로 해결되는가 이 실험 결과가 다음 platform API 리팩터링의 근거가 되어야 한다. --- ## 12. P1 — `contracts`는 계속 커지면 제2의 `shared/common`이 된다 현재 `src/contracts`는 39개 파일, 약 8,144 LOC다. `contracts`라는 이름은 편리하지만 ownership을 잃기 쉽다. 특히 다음이 모두 한 bucket으로 들어가면 문제가 된다. - error vocabulary - server-state contracts - capability registry - boundary mapper - mutation intent - runtime descriptor - compatibility metadata 공통 계약이 실제로 여러 capability가 공유하는 SSOT라면 `contracts`에 있어도 된다. 그러나 **특정 feature/adapter만 소비하는 contract는 owner 쪽에 두는 것이 더 낫다.** 판정 질문은 단순하다. > 이 타입을 바꾸는 이유가 어떤 모듈의 요구사항 변화 때문인가? 답이 항상 특정 feature/storage/http라면 global contracts가 아닐 가능성이 높다. --- ## 13. P1 — CI/Assurance는 가치가 있지만 product development path와 격리해야 한다 현재 package script는 114개, `scripts/**/*.ts`는 123개 / 약 28K LOC다. 이는 단순 frontend build script 수준을 넘어선다. 현재 저장소는 다음을 자체적으로 검증한다. - architecture graph - registry compatibility - artifact semantic validation - supply-chain evidence - reproducible build - provider sandbox - promotion/finalization - archive traversal/symlink/hardlink - release admission - security fixtures 이 기능을 단순화한다는 이유로 제거할 필요는 없다. 플랫폼 품질과 supply-chain assurance를 강하게 가져가겠다면 정당한 투자다. 다만 개발자가 일반 feature를 수정할 때 이 전체 영역을 알아야 해서는 안 된다. 권장 구조적 목표는 다음이다. ```text Product development path feature → focused test → type/lint/architecture Platform capability path adapter → capability contract/integration tests Assurance path CI runner → supply-chain/system/promotion tests ``` 세 경로의 명령과 실패 메시지가 명확히 분리되어야 한다. --- ## 14. P2 — 실제 중복은 정책 중복부터 제거한다 중복 제거의 목표를 “같은 코드 한 줄도 두 번 쓰지 않는다”로 잡으면 이 프로젝트에서는 오히려 abstraction이 과해진다. 특히 security/protocol boundary의 작은 validator는 local duplication이 가독성과 auditability에 도움이 될 수 있다. 반대로 **정책 중복은 제거해야 한다.** 현재 `use-reference-feature.ts`에는 `CREATE_REFERENCE_RESOURCE` mutation definition이 두 경로에 반복된다. 반복되는 핵심 값은 다음과 같다. ```text definitionId operationId requiresIdempotencyKey owner duplicatePolicy invalidate ``` 이것은 단순 syntax 중복이 아니라 하나의 mutation policy다. 한쪽만 수정되면 같은 feature 안에서 서로 다른 concurrency/idempotency 동작을 할 수 있다. 따라서 feature-owned mutation definition 또는 factory로 단일화하는 것이 맞다. 반대로 `isPlainRecord` 같은 3~5줄 helper는 모든 adapter에서 무조건 한 global utility로 합칠 필요가 없다. --- ## 15. P2 — compatibility re-export는 canonical import path를 흐릴 수 있다 현재 일부 UI와 Result 계층에는 canonical 정의와 compatibility re-export가 동시에 존재한다. 이런 구조가 migration window를 위해 필요하다면 괜찮다. 하지만 템플릿 출발점에서 두 경로가 모두 정식 API처럼 보이면 새 개발자가 어떤 import를 사용해야 하는지 판단해야 한다. 원칙은 다음이 적절하다. - public import path는 capability마다 하나를 canonical로 둔다. - compatibility export는 deprecated/migration purpose임을 명시한다. - migration 종료 조건이 충족되면 제거한다. - 테스트 편의를 위해 barrel export를 무한히 확장하지 않는다. --- ## 16. Hybrid Architecture를 공식 모델로 문서화할 필요가 있다 현재 저장소는 순수 horizontal Clean Architecture가 아니다. 실제 구조는 다음 hybrid에 가깝다. ```text Platform horizontal layers ├── application ├── contracts ├── adapters ├── presentation └── bootstrap Feature vertical slices └── features/ ├── domain ├── application ├── adapters ├── contracts └── presentation ``` 이 구조는 프론트엔드에서 합리적이다. 공통 HTTP/storage/query runtime을 모든 feature 안에 복사할 이유는 없고, feature domain/application은 vertical slice로 격리할 수 있기 때문이다. 따라서 전역 `src/domain`이 없다는 이유로 억지로 생성할 필요도 없다. 대신 문서에서 다음을 명시해야 한다. > platform은 horizontal capability layer를 사용하고, product business feature는 vertical slice를 사용한다. feature는 platform capability를 port 뒤에서 소비하며 platform은 concrete feature를 알지 않는다. 이 문장이 공식 architecture model이 되면 “왜 여기에는 domain이 없고 feature 안에는 domain이 있는가” 같은 혼란이 줄어든다. --- ## 17. 권장 리팩터링 순서 ### Phase 0 — 테스트 신뢰성 정상화 먼저 구조를 바꾸지 않고 검증 기반을 고친다. 1. test runner의 `NODE_ENV` 등 필수 environment를 deterministic하게 만든다. 2. `tests/unit`에서 systemd/bwrap/cgroup 의존 테스트를 분리한다. 3. `test:unit`, `test:component`, `test:integration`, `test:system`, `test:e2e`의 의미를 다시 고정한다. 4. Node version requirement와 local runner requirement를 명확하게 fail-fast한다. 이 단계가 끝나야 이후 refactoring failure를 실제 regression으로 믿을 수 있다. ### Phase 1 — reference feature를 platform consumer UX 기준으로 리팩터링 새 abstraction을 먼저 만들지 않는다. 현재 reference feature를 기준으로 다음을 측정하고 줄인다. - HTTP gateway boilerplate - mapper/result guard 중복 - query/mutation definition duplication - installed contribution 수정 지점 - platform internal type exposure 이 단계에서 capability API의 이상적인 최소 사용 형태를 결정한다. ### Phase 2 — capability-specific typed factory 정리 반복이 확인된 경우에만 factory를 만든다. 후보: ```text HTTP operation/gateway binder IndexedDB typed repository/storage binder Realtime event source binder Transfer command/session binder ``` 하나의 범용 repository/framework로 합치지 않는다. ### Phase 3 — Application/Feature installation surface 축소 - root ApplicationProvider는 유지 가능 - feature consumer hook은 narrow API 제공 - installed catalog는 aggregation 역할로 제한 - feature-owned contribution 정의를 강화 - concrete feature를 generic platform에서 import하지 않는 규칙 유지 ### Phase 4 — 거대 runtime 내부 경계 추출 characterization test를 먼저 고정한다. 우선순위 후보: 1. IndexedDB runtime 2. resumable upload 3. WebSocket connection/reconnect 4. HTTP execution V3 5. OPFS runtime/journal 파일 크기가 아니라 state machine/Saga/lifecycle ownership을 기준으로 추출한다. ### Phase 5 — 거대 테스트 분해 production runtime extraction과 같은 behavior boundary에 맞춰 test suite도 분리한다. ### Phase 6 — CI/Assurance 경로 분리 product feature 개발 loop와 release/supply-chain system assurance loop를 명령, 테스트 디렉터리, prerequisite 측면에서 명확히 구분한다. --- ## 18. 명시적으로 하지 말아야 할 리팩터링 ### 18.1 모든 adapter를 하나의 generic repository로 통합하지 않는다 HTTP, IndexedDB, Realtime, Upload는 lifecycle과 failure semantics가 다르다. 타입 파라미터가 비슷하다는 이유로 하나의 abstraction으로 합치면 business 요구를 기술 abstraction에 맞추게 된다. ### 18.2 use case가 platform adapter를 직접 import하지 않는다 `UseCase → HttpAdapter` 구조가 되면 Clean Architecture의 핵심 목적을 잃는다. Generic type binding은 feature adapter/composition에서 일어나야 한다. ### 18.3 단순화를 이유로 hardening을 제거하지 않는다 현재 retry/effect certainty/bounded read/recovery/sandbox 같은 계약은 대부분 실제 failure mode를 막기 위해 존재한다. 필요한 것은 삭제가 아니라 **internal complexity encapsulation**이다. ### 18.4 파일 길이만 보고 나누지 않는다 2,000줄이어도 하나의 cohesive state machine이면 함부로 분리하지 않는다. 반대로 300줄이어도 서로 다른 lifecycle owner가 섞여 있으면 분리 대상이다. ### 18.5 중복률 0%를 목표로 하지 않는다 local protocol validator의 작은 중복보다 policy가 여러 곳에 존재하는 중복이 더 위험하다. --- ## 19. 최종 목표 개발 경험 이 Foundation이 잘 리팩터링됐을 때 새 feature 개발자는 대략 다음만 작성하면 되어야 한다. ```text 1. Domain type / invariant 2. UseCase 3. Port 4. Transport/storage schema와 mapper 5. Capability binding 설정 6. Presentation controller/page ``` 그리고 HTTP/IndexedDB/WebSocket 내부의 다음 내용을 몰라도 되어야 한다. ```text retry scheduler abort ownership effect certainty transaction lease reconnect coordinator OPFS journal provider lifecycle telemetry delivery ``` 개념적으로 다음 형태가 목표다. ```text Business Domain ↓ UseCase ↓ Port ↓ Feature-owned Binding + mapper/codec + policy ↓ Reusable Capability Runtime ↓ Browser / Network / Native API ``` 이 구조가 성립하면 내부 runtime이 복잡하다는 사실은 문제가 아니다. 오히려 복잡한 기술 문제를 한 곳에서 해결했기 때문에 각 제품 feature는 더 단순해진다. --- ## 20. 우선순위 요약 | 우선순위 | Finding | 조치 방향 | | --- | --- | --- | | P0 | 테스트가 host `NODE_ENV` 영향을 받음 | test environment deterministic하게 고정 | | P0 | unit suite에 systemd/bwrap/cgroup 테스트 혼재 | system/CI-runner suite로 분리 | | P1 | capability consumer API의 실제 개발 비용이 불명확 | reference + 추가 feature로 사용성 측정 | | P1 | feature 설치가 여러 중앙 catalog로 확산 | feature-owned contribution + 단순 aggregation | | P1 | 거대 runtime 내부 책임 추적 비용 | state machine/Saga/lifecycle 기준 extraction | | P1 | 2K+ LOC test file 다수 | behavior contract별 suite 분리 | | P1 | `contracts`가 generic bucket으로 성장 가능 | ownership 기준 재배치 | | P1 | product dev path와 CI assurance path가 뒤섞임 | command/test prerequisite 격리 | | P2 | `ApplicationApi`의 Service Locator 성장 위험 | narrow consumer hooks/API | | P2 | feature mutation policy 중복 | feature-owned definition SSOT | | P2 | compatibility/re-export 경로 | canonical public API 지정 | | P2 | hybrid architecture 설명 부족 | platform horizontal + feature vertical 공식화 | --- ## 21. 이번 검토에서 확인한 검증 결과 코드 변경 전 기준선에서 확인한 결과다. ```text check:architecture → PASS → 307 modules / 929 dependencies ``` 앞선 검토에서 다음도 통과했다. ```text check:types → PASS lint → PASS ``` Component test는 host environment를 그대로 사용하면 실패했다. ```text pnpm test:component → FAIL → React.act is not a function → host NODE_ENV=production ``` 동일 checkout에서 test environment를 명시하면 통과했다. ```text NODE_ENV=test pnpm test:component → 20 files PASS → 130 tests PASS ``` Unit 전체는 현재 Coka host에서 green이 아니다. ```text pnpm test:unit → FAIL → 99 failures ``` 대표 원인은 다음이다. - `/usr/bin/bwrap` 미설치 - systemd bus 사용 불가 - host `npm_config_*` 환경 오염 따라서 이 결과를 application/domain regression 99건으로 해석하면 안 된다. 또한 저장소가 요구하는 Node 범위는 `>=24.11.0 <25.0.0`인데, 현재 Coka host는 Node `22.23.2`다. 정식 full verification은 지원 Node 환경에서 다시 수행해야 한다. --- ## 22. 최종 평가 현재 저장소는 설계가 부족해서 문제가 생긴 코드베이스가 아니다. 오히려 반대다. **실제 브라우저와 네트워크에서 발생할 수 있는 많은 실패를 플랫폼이 직접 책임지려 하면서 내부 correctness와 assurance가 매우 강해졌고, 그 결과 플랫폼 사용성과 유지보수 비용이 다음 병목이 된 상태**다. 따라서 앞으로의 리팩터링 목표는 abstraction을 더 추가하는 것이 아니다. 다음 세 가지가 핵심이다. 1. **비즈니스 개발자가 보는 API를 더 작게 만든다.** 2. **플랫폼 내부의 복잡성은 capability boundary 안에서 더 명확하게 격리한다.** 3. **테스트와 CI를 실제 실행 성격에 맞게 분리해 검증 결과를 신뢰할 수 있게 만든다.** 특히 이 프로젝트의 성공 여부는 “몇 개의 capability를 구현했는가”보다 다음 질문으로 판단해야 한다. > 새로운 비즈니스 기능을 구현할 때, 개발자가 domain과 use case에 집중한 채 이미 구현된 capability를 타입 안전하게 조립할 수 있는가? 그 답이 지속적으로 `예`가 되도록 만드는 것이 이후 리팩터링의 기준이 되어야 한다.