# 언어별 구현 포팅 가이드 ## 공통 원칙 - **레퍼런스 구현체: `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를 직접 등록하거나 처리하지 않음 | --- ## 새 언어 추가 절차 1. 새 언어 디렉터리를 만들기 전에 `agent-ops/skills/project/add-proto-socket-crosstest-language/templates/`의 문서를 복사해 패키지 README와 체크리스트 초안으로 사용한다. 2. `PROTOCOL.md`와 `VERSIONING.md`의 현재 protocol version을 기준으로 지원 범위를 정한다. 3. canonical proto인 `proto/message_common.proto`에서 언어별 protobuf binding을 생성한다. Go처럼 generator 전용 option이 필요하면 언어 패키지 안에 copy를 둘 수 있지만, message schema는 `tools/check_proto_sync.sh`로 검증 가능해야 한다. 4. `Transport`와 `Communicator`를 먼저 구현하고, 그 위에 TCP/TLS+TCP/WS/WSS client/server를 올린다. 5. 같은 언어 단위 테스트를 작성한 뒤 `agent-ops/skills/project/add-proto-socket-crosstest-language/SKILL.md`의 runner 배치 규칙에 맞춰 양방향 crosstest를 추가한다. 6. README의 Implementations 표를 `Available`로 바꾸기 전에 formatter/linter, 단위 테스트, cross-language tests를 모두 통과시키고, protobuf 외 런타임 의존성이 있다면 native 대안 검토 결과, 기능 범위와 라이브러리 범위가 맞는 이유, 필요성을 문서화한다. 템플릿은 시작점과 완료 조건만 고정한다. 실제 소스 구조, 패키지 매니저, async runtime은 각 언어의 관용적 선택을 따른다. --- ## C# (Unity / .NET) ### 핵심 매핑 | Go | C# | |----|----| | `Transport` interface | `ITransport` interface | | `baseClient[Self]` | `BaseClient` generic class | | `ParserMap` | `Dictionary>` | | goroutine | `Task` + `CancellationToken` | | `context.Context` | `CancellationToken` | | `sync.Once` | `Interlocked` 또는 `SemaphoreSlim(1,1)` | | `atomic.Bool` | `volatile bool` 또는 `Interlocked.Exchange` | | channel (writeQueue) | `Channel` (`System.Threading.Channels`) | ### 주의사항 - **Unity 환경**: `System.Threading.Channels`가 Unity 버전에 따라 미지원일 수 있다. 대안으로 `ConcurrentQueue` + ManualResetEvent 조합을 사용한다 - **메인 스레드 제약**: Unity는 UnityEngine API를 메인 스레드에서만 호출할 수 있다. 수신 콜백을 메인 스레드로 dispatch하는 래퍼(`SynchronizationContext`)가 필요하다. Communicator 코어는 스레드 무관하게 유지하고, 래퍼 레이어에서 처리한다 - **protobuf**: `Google.Protobuf` NuGet 패키지 사용. `typeof(T).FullName`이 proto qualified name과 일치하는지 반드시 검증한다. PROTOCOL.md의 typeName 표 참고 - **제네릭 헬퍼**: `AddListenerTyped`, `SendRequestTyped` 패턴은 C# 제네릭으로 그대로 구현 가능하다 - **`doClose` 패턴**: C#의 람다 캡처 동작이 Go와 동일하므로 그대로 이식 가능하다 --- ## Kotlin (Android) ### 핵심 매핑 | Go | Kotlin | |----|--------| | `Transport` interface | `interface Transport` | | `baseClient[Self]` | `abstract class BaseClient` 또는 generic open class | | goroutine | `CoroutineScope` + `launch` | | `context.Context` | `CoroutineContext` + `Job` | | channel (writeQueue) | `Channel` (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` 또는 `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` | | 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`, `sendRequest` 등을 완벽히 타입 안전한 API로 구현한다 --- ## 공통 금지사항 다음은 Dart 구현체의 레거시 패턴이다. 새 구현체에 복제하지 않는다. - 생성자·에러 핸들러에서 `print()` / `console.log()` 직접 호출 — 로거 인터페이스를 두거나 생략한다 - `isAlive`, `nonce`를 public mutable 필드로 노출 — 내부 상태는 캡슐화한다 - `onDisconnected`와 `dispose`를 별도 메서드로 분리 — Go처럼 `Close()` 하나로 통일한다 - heartbeat 타이머를 재귀 호출로 구현 — 루프 또는 단순 타이머 재설정으로 구현한다 - TCP와 WebSocket 클라이언트에 heartbeat·disconnect 로직 중복 — `baseClient` 패턴으로 공유한다