Skip to content

v0.18.0 계층 의존 허용표 (이슈 #1282, G03)

한 문장 — 앱 안의 계층이 어느 계층을 import 해도 되는가를 표 하나로 정하고, 표의 규칙을 테스트가 지킨다. 표에 없는 방향은 금지다. 목표 구조(총괄 §2)로 가는 동안 지금 코드가 어긋나는 곳은 "기준선" 으로 적어 두고 늘어나지 못하게만 한다 — 줄이는 것은 각 담당(A01·A08·A15)의 몫이다.

1. 유저 A 의 기록으로 보는 계층

유저 A 가 스쿼트를 기록하면 그 데이터는 여덟 계층을 지난다(장부 §1-1). 아래 표의 "계층" 은 그 길 위의 폴더다. 화살표는 "import 해도 된다" 이고, 표에 없으면 못 한다.

2. 허용표

계층 (폴더)import 해도 되는 것절대 안 되는 것지키는 검사
계약 core src/react/contracts/core/**contracts/core/** 끼리, types/common·types/exerciseIdentity(브랜드 재사용)그 밖 전부 — 계약의 뿌리는 아무것도 모른다contractsImportBoundaries
계약 contracts/{commands,resources,ports,persistence}/**contracts/**, types/**(현행 DTO 를 좁히는 용도)services/**·controllers/**·features/**·ui/**·React·SupabasecontractsImportBoundaries
기존 타입 src/react/types/**types/** 끼리구현 계층 전부(현행 0건 — G03 확인)
화면 ui/**contracts/core·contracts/ports(port 타입만)·contracts/workout/**(플랫폼 공통 순수 정책 — A12, ui/shared 는 같은 이름 재수출)·types/{mobileUi,desktopUi,reactComponents,recording,recordingRequirement,calendarReadModel,auth,admin}·ui/shared/**services/**·controllers/**·app*.tsx·types/supabase·types/screenRpc(DB/RPC 행 모양)·다른 플랫폼 UIfrontendImportBoundaries(UI 표현·플랫폼 분리) + contractsImportBoundaries
기능 바인딩 features/**contracts/**(contracts/workout/** 포함 — features/workout/editor 가 읽는 순수 정책)·types/**(행 모양 제외)·services/barbelicApi(파사드)·controllers/**(자기 영역)·resources/**(조회 캐시 store 의 데이터·판정 함수 — feature 선택자 입력, A09 #1342)services/barbelicRepository·types/supabase·types/screenRpc·다른 feature 의 내부frontendImportBoundaries(쓰기 소유) + contractsImportBoundaries
컨트롤러·스토어 controllers/**contracts/**·types/**(행 모양 제외)·services/**(파사드·도메인 서비스·순수 매퍼)services/barbelicRepository·types/supabase·types/screenRpc(A01에서 직접 import 0개로 이전 완료)frontendImportBoundaries(repository) + contractsImportBoundaries ①(래칫)
앱 runtime src/react/app/**(owner·auth·connectivity 수명주기, A07 #1335)contracts/**·types/**(행 모양 제외)·services/**(파사드·도메인 서비스)·controllers/**(store 세트) — controllers 와 같은 규칙. React 를 모른다(앱 셸이 주입)services/barbelicRepository·types/supabase·types/screenRpc·ui/**·features/**frontendImportBoundaries(app-facing) + contractsImportBoundaries
resource cache src/react/resources/**(owner 범위 조회 캐시 primitive·React 구독 훅, A08 #1338)contracts/**·services/readModelObservability(계측)·React(useResourceSnapshot.ts 훅 파일만 — store 본체는 React 를 모른다) — controllers 와 같은 규칙services/barbelicRepository·types/supabase·types/screenRpc·ui/**·features/**·controllers/**(캐시는 호출자를 모른다)frontendImportBoundaries(app-facing) + contractsImportBoundaries
앱 셸 app.tsx·appController.tsx·mobileApp.tsx·desktopApp.tsx·sharedShell.tsx·vite/**controllers/**·features/**·contracts/**·services/barbelicApi (플랫폼 앱은 파사드 직접 금지)types/supabase·types/screenRpc(A01에서 실제 API 반환 타입으로 이전 완료)·raw loader 이름frontendImportBoundaries(app-facing raw loader·플랫폼 앱 파사드) + contractsImportBoundaries ①(래칫)
도메인 서비스·매퍼 services/domains/**·services/*Mappers.ts·services/screenRpcAdapters.tsservices/barbelicRepository(domains 만)·types/** 전부(DB 행 → Dto 변환 지점contracts/**controllers/**·ui/**frontendImportBoundaries(repository 는 domains 만)
저장소·전송 services/barbelicRepository.ts·screenRpcContracts.ts·runtimeSchemaValidation.tstypes/supabase·types/screenRpc·contracts/core·contracts/ports/runtime(TransportPort 구현)controllers/**·ui/**·features/**frontendImportBoundaries + boundary gate(any 허용 목록)
기기 저장물 services/pendingWorkoutSaves.ts·workoutDraftCache.ts·workoutLocalCacheDb.ts·(S01) persistence/codecs/**contracts/core·contracts/commands·contracts/persistence(Decoder 구현)ui/**·controllers/**·transport(S01 이 codec 테스트로 추가)

읽는 법: 위에서 아래로 갈수록 바깥(화면)에서 안쪽(서버)이다. 화살표는 아래로만 간다 — 안쪽 계층이 바깥 계층을 import 하면 위반이다. 같은 계층 안의 import 는 자유다(플랫폼 UI 사이는 예외 — 모바일 ↮ 데스크톱).

3. 모양의 출처와 변환 지점

같은 세션이 네 모양으로 앱을 지난다(계약 §7). 변환은 세 지점에서만 한다.

변환어디서누가 소유
DB 행(DbRow, snake_case) → 내부 DTO(Dto, camelCase)services/screenRpcAdapters.ts(읽기 22개 adapt*) · services/barbelicMappers.ts(행 → 도메인) · services/sessionWriteContractV5.ts(쓰기 wire)도메인 codec A02~A06(장부 §3 "서비스·도메인·매퍼" 행)
바깥 unknown(ExternalUnknown: IndexedDB·localStorage·bridge) → 내부 DTOcontracts/persistenceDecoder 구현 = S01 persistence/codecs/** · bridge 는 services/nativeAuthSessionPolicy.ts(N01)S01 · N01
내부 DTO → 화면 모델(ViewModel)services/*ViewMappers.ts·mobileExperienceViewMappers.ts·desktopExperienceViewMappers.ts표현 계약 U01 · 매퍼 A 계열

검증 없는 as 로 unknown 을 DTO 로 바꾸지 않는다(이슈 지시). 현행 as unknown as 17곳·as any 11곳은 boundary gate 허용 목록에 사유와 함께 있고, A16 이 dual-key 기준선과 함께 0 으로 내린다.

4. 기준선과 그 처리

기준선지금누가 0 으로검사
controllers·앱 셸이 화면 RPC payload 타입을 직접 읽음3 → 0파일: 부팅 사본은 domains/bootReadModelAdapters.ts, 수동 기록은 실제 API 반환 추론A01 완료. A08/A15는 0개 경계 유지contractsImportBoundaries ① — 늘면 실패, 줄면 기준선 목록에서 지운다(테스트가 알려 준다)
UI·features 가 DB 행 타입을 읽음0같은 검사(0 유지)
AnyRecord 사용56파일(UI 2)A16·U01types/controllers.ts 는 boundary gate 허용 목록 "temporary controller helper types"

5. 새 라이브러리 결정 (ADR-5 이행)

기존 store/coordinator 를 typed 로 완성한다. 새 조회·상태 라이브러리는 도입하지 않는다. 근거(2026-09-07 실측): services/readModelQueryCache.ts 가 키 버전(늦은 답장 무시)·stale·invalidated·비행 중 합류·중단 신호를 이미 갖췄고, owner 범위는 controllers/ownerStores.ts 가 owner 전환 때 스토어 12개를 통째로 바꾸는 방식으로 이미 한다. TanStack Query 류를 넣으면 ① owner 전환·통계 세대 대기(statsRecoveryCoordinator)를 라이브러리 밖에서 다시 만들어야 하고 ② remoteDataController.ts 4,092줄의 데이터 흐름을 React 트리에 다시 매달아야 하며 ③ 두 구현이 한동안 공존한다(ADR-5 가 금지). 계약 ResourceSnapshot·ResourceCachePort(계약 §10·§17)는 현행 캐시 모양에 이름을 붙인 것이라 A08 은 이름을 맞추는 일만 남는다. 이 결정은 한 번이며, 되돌리려면 "기존 계약으로 충족할 수 없는 요구" 를 근거로 HQ 가 다시 연다.

A01의 이전 대상은 calendarReadModelStore.ts, remoteDataController.ts, appController.tsx였다. 세 파일 모두 서비스 어댑터의 출력 또는 실제 API 반환 타입을 사용하며 직접 행 타입 import 예외는 남아 있지 않다.