225 lines
7.5 KiB
Text
225 lines
7.5 KiB
Text
<!-- task=api_consistency plan=0 tag=IMPROVE -->
|
|
|
|
# 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<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
|
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<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
|
Req data) async {
|
|
if (!isAlive) return Future.error(StateError('not connected'));
|
|
final requestNonce = ++nonce;
|
|
// ...
|
|
return completer.future;
|
|
}
|
|
```
|
|
|
|
After:
|
|
```dart
|
|
Future<Res> sendRequest<Req extends GeneratedMessage, Res extends GeneratedMessage>(
|
|
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<TimeoutException>())` 단언, 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<void> {
|
|
if (!this.isAliveFlag || this.transport === null) {
|
|
throw new NotConnectedError();
|
|
}
|
|
// 큐가 가득 찬 경우 빈 자리가 생길 때까지 대기
|
|
while (this.writeQueue.length >= Communicator.MAX_WRITE_QUEUE_SIZE) {
|
|
await new Promise<void>((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 (신규 큐 테스트 포함)
|
|
```
|