Add PORTING_GUIDE.md
This commit is contained in:
parent
cab031ea28
commit
9a0ebd0e22
1 changed files with 157 additions and 0 deletions
157
PORTING_GUIDE.md
Normal file
157
PORTING_GUIDE.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# 언어별 구현 포팅 가이드
|
||||||
|
|
||||||
|
## 공통 원칙
|
||||||
|
|
||||||
|
- **레퍼런스 구현체: `go/`**
|
||||||
|
- Dart 구현체는 레거시 패턴이 있으므로 참고하지 않는다
|
||||||
|
- 프로토콜 정의는 `PROTOCOL.md`가 단일 기준이다
|
||||||
|
- 각 언어의 관용적 패턴을 따르되, 아래 핵심 구조는 반드시 유지한다
|
||||||
|
|
||||||
|
### 반드시 유지해야 하는 구조
|
||||||
|
|
||||||
|
| 요소 | 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를 직접 등록하거나 처리하지 않음 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.Protobuf` NuGet 패키지 사용. `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 타입을 정의해서 문서화한다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
### 핵심 매핑
|
||||||
|
|
||||||
|
| Go | Rust |
|
||||||
|
|----|------|
|
||||||
|
| `Transport` interface | `trait Transport` |
|
||||||
|
| `baseClient[Self]` | `struct BaseClient<S>` + generic bounds |
|
||||||
|
| goroutine | `tokio::spawn` |
|
||||||
|
| `context.Context` + cancel | `tokio::select!` + `CancellationToken` (tokio-util) |
|
||||||
|
| channel (writeQueue) | `mpsc::channel(64)` (tokio) |
|
||||||
|
| `sync.Once` | `Once` (std) 또는 `OnceLock` |
|
||||||
|
| `atomic.Bool` | `AtomicBool` (std::sync::atomic) |
|
||||||
|
| `sync.RWMutex` | `RwLock<T>` (tokio 또는 std) |
|
||||||
|
|
||||||
|
### 주의사항
|
||||||
|
|
||||||
|
- **소유권 모델**: `doClose` 클로저를 `Arc<Mutex<...>>`로 감싸지 않으면 소유권 문제가 발생한다. Go의 `self Self` 필드는 Rust에서 `Arc<Self>` 또는 `Weak<Self>`로 대응한다. Weak를 선호해서 순환 참조를 방지한다
|
||||||
|
- **protobuf**: `prost` 크레이트 사용. typeName은 `prost::Message`의 descriptor에서 추출. PROTOCOL.md 표에서 Rust 행 확인
|
||||||
|
- **async runtime**: tokio를 기준 런타임으로 한다. `async-std`는 혼용하지 않는다
|
||||||
|
- **`connCloseOnce` 패턴**: `tokio::sync::OnceCell` 또는 `std::sync::Once`로 구현한다. `CancellationToken`을 함께 사용하면 readCtx/writeCtx 패턴을 자연스럽게 표현할 수 있다
|
||||||
|
- **`Transport` trait**: `async fn`을 trait에서 사용하려면 `async-trait` 크레이트가 필요하다 (Rust 1.75 미만). 1.75 이상이면 `impl Future` 반환으로 대체 가능하다
|
||||||
|
- **제네릭 헬퍼**: `AddListenerTyped`, `SendRequestTyped`는 Rust 제네릭으로 완전하게 구현 가능하다. trait bound를 `T: prost::Message + Default`로 잡으면 된다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공통 금지사항
|
||||||
|
|
||||||
|
다음은 Dart 구현체의 레거시 패턴이다. 새 구현체에 복제하지 않는다.
|
||||||
|
|
||||||
|
- 생성자·에러 핸들러에서 `print()` / `console.log()` 직접 호출 — 로거 인터페이스를 두거나 생략한다
|
||||||
|
- `isAlive`, `nonce`를 public mutable 필드로 노출 — 내부 상태는 캡슐화한다
|
||||||
|
- `onDisconnected`와 `dispose`를 별도 메서드로 분리 — Go처럼 `Close()` 하나로 통일한다
|
||||||
|
- heartbeat 타이머를 재귀 호출로 구현 — 루프 또는 단순 타이머 재설정으로 구현한다
|
||||||
|
- TCP와 WebSocket 클라이언트에 heartbeat·disconnect 로직 중복 — `baseClient` 패턴으로 공유한다
|
||||||
Loading…
Reference in a new issue