# 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를 타입 안전하게 조립할 수 있는가?
그 답이 지속적으로 `예`가 되도록 만드는 것이 이후 리팩터링의 기준이 되어야 한다.