# API 일관성 개선 ## 이 파일을 읽는 구현 에이전트에게 각 항목의 체크리스트를 완료하면서 `[ ]`를 `[x]`로 표시하라. 중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 `CODE_REVIEW.md`의 검증 결과 섹션에 붙여넣어라. 최종 검증도 마찬가지로 실행 후 출력을 기록하라. 계획과 다르게 구현한 부분은 이유와 함께 `CODE_REVIEW.md`의 "계획 대비 변경 사항"에 기록하라. --- ## 배경 프로젝트 전반 평가에서 발견된 두 가지 API 비일관성을 수정한다. | 항목 | 문제 | 영향 | |------|------|------| | IMPROVE-1 | Dart `sendRequest`에 timeout 파라미터 없음 | 응답 없으면 무한 대기 | | IMPROVE-2 | TypeScript write queue 크기 제한 없음 | await 없이 대량 send 시 무한 증가 가능 | Go/Kotlin/Python/TypeScript는 `sendRequest`에 timeout을 지원한다. Dart만 없다. Go/Kotlin/Python은 write queue를 64개로 제한한다. TypeScript와 Dart는 제한이 없다. Dart write queue는 Promise chaining 방식이라 구조상 추가가 어려우므로 이번 범위에서 제외한다. ## 의존 관계 및 구현 순서 IMPROVE-1과 IMPROVE-2는 독립적이다. 이 순서로 진행한다. --- ### [IMPROVE-1] Dart `sendRequest` timeout 파라미터 추가 **문제** [dart/lib/src/communicator.dart](../../dart/lib/src/communicator.dart) — `sendRequest`에 timeout 파라미터가 없다. ```dart Future sendRequest( Req data) async { ``` 다른 언어 비교: | 언어 | timeout 기본값 | |------|--------------| | Go | `time.Duration`, 기본 30s | | Kotlin | `timeoutMs: Long = 30_000L` | | Python | `timeout: float = 30.0` | | TypeScript | `timeoutMs = 30000` | | **Dart** | **없음 — 무한 대기** | **해결 방법** `sendRequest`에 named 파라미터 `{Duration timeout = const Duration(seconds: 30)}`를 추가한다. 응답 대기를 `completer.future.timeout(timeout, onTimeout: ...)` 으로 감싼다. timeout 발생 시 `_pendingRequests`에서 해당 nonce를 제거하고 `TimeoutException`을 전파한다. Before: ```dart Future sendRequest( Req data) async { if (!isAlive) return Future.error(StateError('not connected')); final requestNonce = ++nonce; // ... return completer.future; } ``` After: ```dart Future sendRequest( Req data, {Duration timeout = const Duration(seconds: 30)}) async { if (!isAlive) return Future.error(StateError('not connected')); final requestNonce = ++nonce; // ... return completer.future.timeout(timeout, onTimeout: () { _pendingRequests.remove(requestNonce); throw TimeoutException( 'sendRequest timeout for nonce $requestNonce', timeout); }); } ``` `dart:async`의 `TimeoutException`을 사용한다 (`import 'dart:async'`는 이미 있음). **수정 파일 및 체크리스트** - [x] `dart/lib/src/communicator.dart` — `sendRequest`에 `{Duration timeout = const Duration(seconds: 30)}` named 파라미터 추가 - [x] `dart/lib/src/communicator.dart` — `completer.future.timeout(timeout, onTimeout: ...)` 적용, pending 정리 포함 - [x] `dart/test/communicator_test.dart` — `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` 테스트 추가 **테스트 작성** - 파일: `dart/test/communicator_test.dart` - 테스트명: `sendRequest가 timeout 내 응답 없으면 TimeoutException을 던진다` - 시나리오: `FakeTransport`에 packet을 보내지만 응답을 보내지 않음, `Duration(milliseconds: 50)` timeout으로 호출, `throwsA(isA())` 단언, timeout 후 `_pendingRequests`가 비어있는지 확인 **중간 검증** ```bash cd dart && dart test test/communicator_test.dart # 새 timeout 테스트 포함 전체 PASS cd dart && dart analyze # No issues found ``` --- ### [IMPROVE-2] TypeScript write queue maxsize 제한 **문제** [typescript/src/communicator.ts](../../typescript/src/communicator.ts) — write queue가 크기 제한 없는 `Array`다. ```typescript private writeQueue: QueueItem[] = []; ``` Go/Kotlin/Python은 64개 제한 도달 시 큐에 빈 자리가 생길 때까지 발신을 블로킹한다. TypeScript는 `await`하지 않고 대량 `send()`를 발행하면 큐가 무한 증가한다. **해결 방법** `MAX_WRITE_QUEUE_SIZE = 64` 상수를 추가한다. 큐가 가득 찼을 때 waiter Promise를 등록하고, 항목이 소비될 때마다 waiter를 깨운다 (Python의 `asyncio.Queue`와 동일한 시맨틱). `shutdown()` 시 모든 waiter를 깨워 `NotConnectedError`로 종료한다. 추가할 멤버: ```typescript private static readonly MAX_WRITE_QUEUE_SIZE = 64; private writeQueueWaiters: Array<() => void> = []; ``` `queuePacket` 변경: ```typescript async queuePacket(base: PacketBase): Promise { if (!this.isAliveFlag || this.transport === null) { throw new NotConnectedError(); } // 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기 while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) { await new Promise((resolve) => { this.writeQueueWaiters.push(resolve); }); if (!this.isAliveFlag) throw new NotConnectedError(); } // 이하 기존 코드 동일 ... } ``` `runWriteQueue`에서 항목 소비 후 waiter 해제: ```typescript const item = this.writeQueue.shift(); if (this.writeQueueWaiters.length > 0) { this.writeQueueWaiters.shift()?.(); } ``` `shutdown()`에서 waiters 플러시: ```typescript while (this.writeQueueWaiters.length > 0) { this.writeQueueWaiters.shift()?.(); } ``` `initialize()`에서 `writeQueueWaiters` 초기화 추가: ```typescript this.writeQueueWaiters = []; ``` **수정 파일 및 체크리스트** - [x] `typescript/src/communicator.ts` — `MAX_WRITE_QUEUE_SIZE = 64` 상수 추가 - [x] `typescript/src/communicator.ts` — `writeQueueWaiters` 멤버 추가 및 `initialize()` 초기화 - [x] `typescript/src/communicator.ts` — `queuePacket()`에 full 시 대기 로직 추가 - [x] `typescript/src/communicator.ts` — `runWriteQueue()`에서 waiter 해제 - [x] `typescript/src/communicator.ts` — `shutdown()`에서 waiters 플러시 - [x] `typescript/test/communicator.test.ts` — 큐 full 시 대기 후 처리 테스트 추가 **테스트 작성** - 파일: `typescript/test/communicator.test.ts` - 테스트명: `write queue가 MAX_WRITE_QUEUE_SIZE 초과 시 대기 후 순서대로 처리된다` - 시나리오: FakeTransport에서 write를 인위적으로 지연, 65번째 `queuePacket` 호출이 블로킹되다가 64번째가 처리되면 진행됨을 확인 **중간 검증** ```bash cd typescript && npx tsc --noEmit # 타입 오류 없음 cd typescript && npx vitest run test/communicator.test.ts # 새 큐 테스트 포함 PASS ``` --- ## 수정 파일 요약 | 파일 | 항목 | |------|------| | `dart/lib/src/communicator.dart` | IMPROVE-1 | | `dart/test/communicator_test.dart` | IMPROVE-1 | | `typescript/src/communicator.ts` | IMPROVE-2 | | `typescript/test/communicator.test.ts` | IMPROVE-2 | ## 최종 검증 ```bash # Dart cd dart && dart analyze # No issues found cd dart && dart test # 전체 PASS (신규 timeout 테스트 포함) # TypeScript cd typescript && npx tsc --noEmit # 타입 오류 없음 cd typescript && npx vitest run # 전체 PASS (신규 큐 테스트 포함) ```