RPC 공통 전송과 도메인 구현을 나누고 좁은 포트로 연결 — v0.18.0 A01 (2026-09-07)
- 기간: 2026-09-07, 사용자 지시 “1288 진행해줘”. 계획 ID A01 / Phase 1 Step 4.
- 랜딩: 구현·검증 후 PR 제출 단계. main 병합·staging·Production 반영은 아직 완료하지 않았다. 상태 추적: #1288의 연결 PR.
- 기준: main
16b8e99684a4cb152b2ff5d89b5aea10a7f055e0; 선행 G01/G02/G05/G03/D03 병합 및 최종 인계 확인. - 설계: RPC 전송과 도메인 추출 경계. 총괄 A01로 돌아가기.
- 정본:
rpcTransport.ts,rpcMutationAdapter.ts,domains/repositories/*Repository.ts,domains/servicePorts.ts; G03 core 계약 버전 1 유지. - 도구: TypeScript AST를 이용한 일회성 추출/등록 도구는 작업 폴더 밖 scratch에 보관하며 제품에 포함하지 않았다. coverage 장부는 기존 도구로 재생성.
- 게이트: 기존 RPC/쓰기 행동 검사, 새 transport·domain·port·boot adapter 검사, 타입·import 경계, 전체
npm run check, unused, 앱/문서 build. 최종 실행 수치와 원격 CI는 연결 PR에 기록한다.
1. 배경
G03가 타입 계약을 만들었지만 기존 서비스들은 5천 줄이 넘는 Repository namespace에 의존했다. 뒤의 A02–A06이 독립적으로 구현하려면 실제 목적지와 최초/최종 공용 배선 담당이 필요했다.
2. 문제 제기
전송과 화면 정책의 구분이 코드에 없었다
화면 RPC 호출에는 SDK 호출·취소, strict codec, 오류 보고, 부팅 캐시와 기기 사본 처리가 같이 있었다. 쓰기 RPC는 전체 응답과 오류의 참조를 보존하는 별도 관문을 사용했다. 두 경계를 같은 반환형으로 일괄 교체하면 호출부의 오류 판정이 달라질 수 있었다.
얇은 domain wrapper만으로는 구현 소유가 나뉘지 않았다
도메인 서비스가 분리돼 있어도 실제 함수는 같은 Repository에 남았다. controller 세 곳도 RPC 행 타입을 직접 읽었다. 전체 codec 이전을 A01에 섞으면 후속 도메인 작업의 범위가 사라지므로 대표 경로와 공용 의존 경계부터 만들었다.
3. 해결 방안
단일 시도의 transport와 기존 오류 envelope를 보존하는 mutation adapter를 나눴다. 응답 검증·기기 사본 정책은 query adapter에 남겼다. 기본 timeout이나 retry 횟수를 바꾸지 않고 명시 timeout/signal이 주어진 경우만 전송을 제한한다.
도메인 9곳의 대표 함수 10개를 실제 factory 구현으로 옮기고 필요한 helper만 주입한다. 나머지 기존 함수는 영역별 명시 임시 port로 좁힌다. 모든 이식이 끝날 때의 제거 조건과 담당은 정본 장부에 적었다.
4. 적용한 내용
TransportPort.callRpc구현: 기존 client와 RPC 이름·인자를 그대로 전달하고 data는 unknown으로 반환. timeout·abort·인증 준비 오류 정규화·진단 경계 추가. 전송 자체의 retry 없음.rpcMutationAdapter추출: 기존 전체 응답·error·status 참조와report:false를 보존한다.- workout/plan/catalog/profile/social/group/import/admin/stats factory 9개에서 대표 실제 함수 10개를 소유한다. 기존 Repository의 함수 본문은 제거했다.
- G03 feature port 17개를
servicePorts.ts에서 실제 조립하고 공개 API가 사용한다. 기존 함수 참조와 반환 추론을 보존한다. - 부팅 사본 adapter와 실제 API 반환 추론으로 controller/앱 셸의 행 타입 직접 import 3곳을 제거했다. 래칫 기준선을 0으로 갱신했다.
- 실제 이동으로 깨진 소스 위치 테스트는 RPC 호출·오류·raw fallback 부재·공개 함수 참조 검사로 전환하고 명부 등재분을 신고했다.
작업 중 드러난 것
G03 문서의 port 수는 16개였지만 컴파일 fixture의 실제 객체는 17개였다. A01은 기존 17개를 모두 연결했으며 새 capability를 추가한 것은 아니다.
Motra 인입 결과는 기존 코드에 독립 DTO 검증이 없었다. A01에서 새로운 응답 거부 규칙을 만들지 않고 기존 mutation 반환 타입만 임시 연결했다. 이 타입 참조와 codec 이전은 A05의 제거 조건에 포함한다.
G05 coverage에는 선행 작업에서 남은 미분류 파일 14개가 있었다. 이번 작업의 새 경로만 분류하고 기존 14개는 HQ 인계로 유지한다. 이 도구의 전체 실패를 성공으로 기록하지 않는다.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 전송 담당 | Repository 안 직접 호출 → 독립 transport와 mutation adapter |
| 도메인 실제 목적지 | 0 → 9파일, 대표 함수 10개 실제 이전 |
| G03 공개 port 실제 연결 | compile fixture만 → 실제 조립 객체 17개 |
| 앱/컨트롤러 DB 행 타입 import | 3 → 0파일 |
| 기존 RPC/쓰기 행동 기준선 | 변경 전 50/50 통과, 최초 transport 연결 후 50/50 통과 |
| 변경 경계 집중 검사 | transport·domain factory·실제 wiring·mutation·abort·boot·import 경계 58/58 통과 |
| 최종 전체 검사/빌드 | ci:local --verify-only 6단계 통과(1분 20초): check:static·unused·앱 build·전체 테스트 포함. 전체 2,754개 중 2,751 통과·0 실패·DB 연결 3개 skip |
| 실제 DB·브라우저·실기기 | 로컬 미실행. DB/schema·서버 함수·UI 표현 변경 없음 |
| main/staging/Production | PR 제출 전에는 미반영. 병합·배포 상태는 구현 완료와 구분 |
최초 npm run check는 이동 전 소스 위치를 요구한 기존 테스트에서 실패했다. 해당 검사를 실제 호출/공개 연결로 전환한 뒤 최종 ci:local이 같은 정적 검사와 전체 테스트를 모두 통과했다. TypeScript unused는 기존 3파일의 14개 기준선만 유지하며 새 억제는 없다. 앱·문서 빌드의 기존 500kB chunk 경고는 별도 번들 경계 U05에서 다룬다.
PR #1316의 최초 원격 검사에서 Node 22의 테스트 모듈 로딩 차이를 발견했다. ESM으로 직접 읽은 TS 모듈과 tsx가 CJS 의존으로 읽은 모듈이 중복 생성되어 함수 참조·캐시 연결 단언이 실패했다. 연결을 검증하는 테스트 6파일은 createRequire로 같은 모듈 그래프를 사용하도록 고쳤으며 단언과 제품 코드는 유지했다. 로컬 Node v22.23.2에서 실패를 재현한 뒤 최종 npm run check가 2,751 통과·0 실패·DB 연결 3개 skip으로 완료됐다. 원격 재검사 결과는 PR에 기록한다.
6. 이번 개선으로 향상된 것
도메인 담당자가 실제 구현을 옮길 파일과 최소 의존이 생겼다. 공용 파일의 최종 연결은 A01 담당이 맡고, 새로운 도메인 로직을 추가할 때 다른 영역의 전체 Repository API를 가져오지 않아도 된다.
전송은 취소·timeout 뒤에 늦게 끝나는 요청을 관측하고 결과를 한 번만 확정한다. timeout이 서버 미저장을 뜻한다고 해석하지 않으며 기존 영수증·dispatcher 재시도 정책은 그대로 둔다. 기기 사본과 화면 cache 정책이 transport로 유입되는 것은 import 검사로 막는다.
남은 것
A02–A06의 나머지 구현·codec 이식, A07 owner lifecycle, A08 resource cache, A15 얇은 shell, A16 잔여 느슨한 타입 제거는 후속 범위다. R01은 임시 port·helper·legacy 반환 참조가 제거됐는지 확인한다. 이번 작업은 전체 Repository 이전이나 운영 배포 완료를 의미하지 않는다.