- Add gateway contract for inbound queue ordering (03+02_gateway_contract) - Update receive thread model across Dart, Go, Kotlin, Python, TypeScript - Implement queue ordering logic in BaseClient and Communicator - Add comprehensive tests for queue ordering in all languages - Update documentation (PORTING_GUIDE, PROTOCOL, README) - Archive task completion logs for completed subtasks
13 KiB
13 KiB
언어별 구현 포팅 가이드
공통 원칙
- 레퍼런스 구현체:
go/,dart/ - 동시성·close-once 구조는 Go 구현체를 우선 참고하고, Dart 구현체는 public API와 parser map 사용 예시를 함께 참고한다
- 프로토콜 정의는
PROTOCOL.md가 단일 기준이다 - 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다
- Protocol Buffers는 이 프로젝트의 핵심 프로토콜 의존성으로 취급한다. 그 외 런타임 구현은 가능한 한 native platform API와 standard library를 우선한다
- protobuf 외 외부 의존성은 구현하려는 기능에 정확히 대응하는 작고 focused된 라이브러리일 때만 허용한다. 넓은 프레임워크, 대형 런타임 계층, 부가 기능이 과도한 패키지를 좁은 기능 하나 때문에 추가하지 않는다
- protobuf 외 외부 의존성을 추가해야 한다면 해당 언어에서 실용적인 native 대안이 없는지 먼저 검토하고, README에 이유와 범위를 명시한다
- TCP/TLS/WebSocket, 바이너리 버퍼, 타이머, 동시성/비동기 처리처럼 플랫폼이 기본 제공하는 기능은 외부 패키지보다 기본 API를 우선한다
반드시 유지해야 하는 구조
| 요소 | Go 기준 | 설명 |
|---|---|---|
| Transport 분리 | Transport interface |
WritePacket과 Close만 있으면 됨. 소켓 종류(TCP/WS)에 무관하게 Communicator가 동작 |
| typeName 라우팅 | TypeNameOf() |
protobuf 메시지 이름 기반. 언어마다 추출 방식이 다르므로 PROTOCOL.md의 언어별 표 확인 |
| nonce 단조 증가 | atomic.Int32 |
연결당 1에서 시작, 송신마다 +1. thread-safe하게 구현 |
| connCloseOnce | sync.Once |
Close가 여러 번 불려도 한 번만 수행되어야 함 |
| addListener / addRequestListener 상호 배타 | panic or throw | 같은 typeName에 두 가지 동시 등록 금지. 프로토콜 계약 |
| heartbeat 자동 처리 | baseClient 내부 |
앱 코드에서 HeartBeat를 직접 등록하거나 처리하지 않음 |
| inbound queue | bounded FIFO (권장 capacity 64) | 프레임 파서와 dispatch 사이에 연결당 하나의 수신 큐를 둔다. 큐가 가득 차면 읽기를 일시 중단해 backpressure를 적용한다. 단, Browser WS, OkHttp WS처럼 transport pause가 불가능한 환경에서는 64 capacity의 Bounded Input Gate를 두며, 이를 초과할 시 강제로 disconnect(connection close/destroy)를 수행해 memory exhaustion을 차단한다. response 패킷(responseNonce > 0)은 FIFO 순서 보장이 불필요하므로 큐를 거치지 않고 직접 handleResponse를 실행해 비동기 대기를 즉시 완료시킨다 |
| receive coordinator | 단일 루프 | 수신 큐에서 패킷을 순서대로 꺼내 request handler 호출 → listener 호출 순서로 처리한다. 자동 응답은 같은 루프 이터레이션에서 write queue에 enqueue한다 |
| close/drain/cancel 정책 | doClose + sync.Once |
Close 호출 시 이미 큐에 들어간 패킷을 drain한 뒤 coordinator를 종료한다. 전송 오류 발생 시에는 drain 없이 즉시 폐기한다. 미완료 sendRequest는 오류로 cancel한다. 상세 계약은 PROTOCOL.md의 Receive Ordering and Backpressure 섹션 참조 |
새 언어 추가 절차
- 새 언어 디렉터리를 만들기 전에
agent-ops/skills/project/add-proto-socket-crosstest-language/templates/의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다. PROTOCOL.md와VERSIONING.md의 현재 protocol version을 기준으로 지원 범위를 정한다.- canonical proto인
proto/message_common.proto에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는tools/check_proto_sync.sh로 검증 가능해야 한다. Transport와Communicator를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다.- 같은 언어 단위 테스트를 작성한 뒤
agent-ops/skills/project/add-proto-socket-crosstest-language/SKILL.md의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다. - README의 Implementations 표를
Available로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시키고, protobuf 외 런타임 의존성이 있다면 native 대안 검토 결과, 기능 범위와 라이브러리 범위가 맞는 이유, 필요성을 문서화한다.
템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다.
C# (Unity / .NET)
핵심 매핑
| Go | C# |
|---|---|
Transport interface |
ITransport interface |
baseClient[Self] |
BaseClient<TSelf> generic class |
ParserMap |
Dictionary<string, Func<byte[], IMessage>> |
| goroutine | Task + CancellationToken |
context.Context |
CancellationToken |
sync.Once |
Interlocked 또는 SemaphoreSlim(1,1) |
atomic.Bool |
volatile bool 또는 Interlocked.Exchange |
| channel (writeQueue) | Channel<T> (System.Threading.Channels) |
주의사항
- Unity 환경:
System.Threading.Channels가 Unity 버전에 따라 미지원일 수 있다. 대안으로ConcurrentQueue<T>+ ManualResetEvent 조합을 사용한다 - 메인 스레드 제약: Unity는 UnityEngine API를 메인 스레드에서만 호출할 수 있다. 수신 콜백을 메인 스레드로 dispatch하는 래퍼(
SynchronizationContext)가 필요하다. Communicator 코어는 스레드 무관하게 유지하고, 래퍼 레이어에서 처리한다 - protobuf:
Google.ProtobufNuGet 패키지 사용.typeof(T).FullName이 proto qualified name과 일치하는지 반드시 검증한다. PROTOCOL.md의 typeName 표 참고 - 제네릭 헬퍼:
AddListenerTyped<T>,SendRequestTyped<TReq, TRes>패턴은 C# 제네릭으로 그대로 구현 가능하다 doClose패턴: C#의 람다 캡처 동작이 Go와 동일하므로 그대로 이식 가능하다
Kotlin (Android)
핵심 매핑
| Go | Kotlin |
|---|---|
Transport interface |
interface Transport |
baseClient[Self] |
abstract class BaseClient<Self> 또는 generic open class |
| goroutine | CoroutineScope + launch |
context.Context |
CoroutineContext + Job |
| channel (writeQueue) | Channel<T> (kotlinx.coroutines) |
sync.Once |
AtomicBoolean + compareAndSet |
atomic.Bool |
AtomicBoolean |
sync.RWMutex |
ReentrantReadWriteLock 또는 Mutex (coroutines) |
주의사항
- Coroutine Scope 관리: 클라이언트 생성 시
CoroutineScope(SupervisorJob() + Dispatchers.IO)를 만들고,Close()시scope.cancel()로 정리한다. SupervisorJob을 사용해야 하위 Job 하나 실패가 전체를 취소하지 않는다 - protobuf:
com.google.protobuf:protobuf-kotlin사용. typeName은descriptor.fullName으로 추출. PROTOCOL.md 표에서 Kotlin 행 확인 - Android 메인 스레드: Unity와 동일하게 UI 콜백은
withContext(Dispatchers.Main)으로 전환한다. Communicator 코어는 IO Dispatcher에서 동작 - 직렬화된 쓰기: Go의 writeQueue(buffered channel 64) 패턴을
Channel(capacity = 64)+ 별도 write coroutine으로 구현한다 Self타입 전달: Kotlin 제네릭의 타입 소거(type erasure) 때문에 런타임에Self인스턴스를 직접 넘겨야 한다. Go의self Self필드 패턴을 그대로 사용한다
Swift (iOS / macOS)
핵심 매핑
| Go | Swift |
|---|---|
Transport interface |
protocol Transport |
baseClient[Self] |
class BaseClient<Self> 또는 protocol + associated type |
| goroutine | Task (Swift Concurrency) |
context.Context |
Task 취소 (cooperative cancellation) |
| channel (writeQueue) | AsyncStream 또는 actor 기반 queue |
sync.Once |
DispatchOnce 또는 actor 격리 |
atomic.Bool |
OSAllocatedUnfairLock 또는 actor |
주의사항
- Actor 활용: Swift 5.5+ actor를 사용하면 mutex/lock 없이 상태 보호가 가능하다.
Communicator를 actor로 구현하면sync.RWMutex없이 동일한 안전성을 확보할 수 있다 - protobuf: SwiftProtobuf 패키지 사용. typeName은
Message.protoMessageName으로 추출. PROTOCOL.md 표에서 Swift 행 확인 connCloseOnce패턴: Swift에는DispatchOnce가 deprecated됐다. actor의 상태 변수(var isClosed = false) + actor 격리로 대체한다- WebSocket:
URLSessionWebSocketTask(iOS 13+) 또는Network.framework의NWConnection사용 - 메모리 관리: 클로저에서
[weak self]캡처를 빠뜨리면 retain cycle이 발생한다.doClose, heartbeat 타이머 클로저 모두[weak self]를 사용한다 Self제약: Swift에서 제네릭Self는 프로토콜 associated type으로 처리하거나, 구체 타입을 생성자에서 전달하는 Go의self Self필드 패턴을 그대로 사용한다
Python
핵심 매핑
| Go | Python |
|---|---|
Transport interface |
Protocol (typing) 또는 ABC |
baseClient[Self] |
Generic[Self] + TypeVar |
| goroutine | asyncio.Task |
context.Context |
asyncio.Event 또는 CancelScope (anyio) |
| channel (writeQueue) | asyncio.Queue(maxsize=64) |
sync.Once |
asyncio.Lock + bool flag |
atomic.Bool |
asyncio 단일 스레드 내에서는 plain bool 가능 |
주의사항
- asyncio 기반:
async/await+asyncio.Queue로 Go의 goroutine + channel 구조를 근사한다. 동기 API는 제공하지 않는다 - 타입 안전성 한계: Python에는 런타임 제네릭이 없다.
AddListenerTyped처럼 완전한 타입 안전 헬퍼 구현이 어렵다.TypeVar와@overload로 가능한 범위까지만 표현하고, 나머지는 docstring으로 보완한다 - protobuf:
protobuf패키지 사용. typeName은message.DESCRIPTOR.name으로 추출. PROTOCOL.md 표에서 Python 행 확인 - 스레드 안전: asyncio는 단일 스레드이므로 대부분의 lock이 불필요하다. 단, 멀티스레드 환경에서 쓸 경우
asyncio.Lock을 사용한다 connCloseOnce패턴: bool flag + asyncio.Lock으로 구현한다.asyncio.Event의set()은 멱등하므로 활용 가능하다- 에러 처리: Go의 명시적 error 반환 대신 exception을 사용하되, public API에서는 구체 exception 타입을 정의해서 문서화한다
TypeScript
핵심 매핑
| Go | TypeScript |
|---|---|
Transport interface |
interface Transport |
baseClient[Self] |
abstract class BaseClient<T> |
| goroutine | Promise + async/await |
context.Context |
AbortSignal (AbortController) |
| channel (writeQueue) | Async Queue (배열 기반 큐 또는 async iterator) |
sync.Once |
boolean 플래그 (단일 스레드이므로 충분) |
atomic.Bool |
boolean |
주의사항
- 크로스 환경 (Browser & Node.js): 코어 로직(
Communicator,BaseClient)은 런타임 환경에 독립적으로 작성한다. 브라우저에서는 내장WebSocket을 사용하고, Node.js에서는net,tls,http,https등 Node.js 내장 모듈 기반 Transport를 사용한다. TypeScript 런타임에 외부 WebSocket 패키지를 추가하지 않는다 - protobuf: protobuf는 핵심 프로토콜 계층이므로 예외로 취급한다. 현재 TypeScript 구현은 canonical schema에 맞춘 native codec을 사용하지만, protobuf binding/runtime을 도입해야 한다면 프로토콜 계층 용도로만 제한한다.
typeName은 로컬MessageType.typeName값이PROTOCOL.md표와 일치해야 한다 - 단일 스레드 (Event Loop): JS/TS는 단일 스레드 기반이므로 메모리 동시 접근에 대한 Mutex/Lock(
sync.RWMutex)은 불필요하다. 단, 비동기 컨텍스트(await) 간의 논리적 상태 오염은 주의한다 - 바이너리 처리: Node.js의
Buffer대신 표준 웹 API인Uint8Array를 기준으로 작성하여 브라우저 환경 호환성을 확보한다 connCloseOnce패턴: 단순isClosed: boolean플래그로 멱등성을 보장한다. 만약Close()내에await가 포함될 경우 중복 실행(re-entrancy) 방지에 유의한다- 제네릭 헬퍼: TypeScript의 강력한 타입 시스템을 활용해
addListener<T>,sendRequest<TReq, TRes>등을 완벽히 타입 안전한 API로 구현한다
공통 금지사항
다음은 Dart 구현체의 레거시 패턴이다. 새 구현체에 복제하지 않는다.
- 생성자·에러 핸들러에서
print()/console.log()직접 호출 — 로거 인터페이스를 두거나 생략한다 isAlive,nonce를 public mutable 필드로 노출 — 내부 상태는 캡슐화한다onDisconnected와dispose를 별도 메서드로 분리 — Go처럼Close()하나로 통일한다- heartbeat 타이머를 재귀 호출로 구현 — 루프 또는 단순 타이머 재설정으로 구현한다
- TCP와 WebSocket 클라이언트에 heartbeat·disconnect 로직 중복 —
baseClient패턴으로 공유한다