Unity에서 TCP 통신을 구현할 때 발생하는 스레드 경계 문제와 메시지 프레이밍 문제를 함께 다룬 최소 예제입니다.
- 워커 스레드 → 메인 스레드 전달:
ConcurrentQueue마샬링 - 스트림 경계 문제: 4바이트 길이 프리픽스 프레이밍
Unity의 UI·Transform·GameObject API는 메인 스레드에서만 호출할 수 있습니다. 그러나 TCP 수신은 별도 워커 스레드에서 이루어지므로, 수신 콜백에서 UI를 직접 갱신하면 예외가 발생하거나 조용히 실패합니다.
실무에서 의료기기 관리 프로그램과 VR 클라이언트 간 TCP 통신을 설계하며 같은 문제를 다룬 경험이 있어, 그 구조를 회사 코드 없이 처음부터 다시 작성했습니다.
[워커 스레드] [메인 스레드]
AcceptLoopAsync ─┐
ReceiveLoopAsync ─┼─→ ConcurrentQueue<Action> ─→ Update()에서 Dequeue
Log() ─┘ └→ OnLog 이벤트 발행
└→ UI 갱신
- 네트워크 계층은 UI를 참조하지 않고,
Action을 큐에 넣습니다. - UI 계층은 스레드를 직접 참조하지 않고, 큐에서 Invoke되는 이벤트를 구독만 합니다.
- 두 계층은
ConcurrentQueue하나로만 연결됩니다.
TCP는 스트림 프로토콜이므로 송신 측의 Write 단위와
수신 측의 Read 단위가 온전히 일치하지 않습니다.
- 뭉침: 연속 전송 시 OS가 여러 메시지를 묶어 전달
- 잘림: 버퍼보다 큰 메시지가 여러 번에 나뉘어 도착 (UTF-8 한글은 3바이트이므로 경계에서 잘리면 복구 불가)
이를 해결하기 위해 본문 앞에 4바이트 길이 헤더를 붙이고, 수신 측에서 해당 길이만큼 정확히 채워질 때까지 읽는 구조를 적용했습니다.
┌──────────────┬──────────────────────┐
│ 길이 4바이트 │ 본문 (UTF-8, 길이만큼) │
└──────────────┴──────────────────────┘
NetworkStream.ReadAsync는 요청한 바이트 수가 아니라 현재 도착한
바이트 수만 반환하므로, ReadExactAsync에서 루프를 돌며 버퍼를 채웁니다.
또한 손상되거나 악의적인 헤더가 과대 메모리 할당을 유발하지 않도록 본문 길이에 상한(64KB)을 두고 검증합니다.
| 테스트 | 프레이밍 전 | 프레이밍 후 |
|---|---|---|
| 20개 연속 전송 | 여러 메시지가 한 줄로 뭉침 | 20줄로 정확히 분리 |
| 6000바이트 한글 전송 | 중간에 잘려 문자 깨짐 | 온전히 1줄로 수신 |
| 항목 | 방식 |
|---|---|
| 접속 수락 | AcceptTcpClientAsync 비동기 루프 |
| 수신 | 클라이언트별 독립 ReceiveLoopAsync |
| 스레드 경계 | ConcurrentQueue<Action> + Update() 소비 |
| 종료 | CancellationTokenSource로 전체 루프 일괄 취소 |
| 클라이언트 목록 | lock 기반 동기화 + 브로드캐스트 시 스냅샷 복사 |
| 정리 | OnDestroy / OnApplicationQuit에서 소켓 해제 |
| 메시지 프레이밍 | 4바이트 길이 프리픽스 + ReadExact 루프 |
| 방어 처리 | 본문 길이 상한(64KB) 검증으로 손상 헤더 대응 |
- Unity 6 이상에서 프로젝트 열기
TCP_Sample재생[서버 시작]→[접속]→ 메시지 입력 후[전송]- 서버가
ECHO|메시지로 브로드캐스트하는 것을 로그에서 확인
이 예제는 스레드 경계 처리와 메시지 프레이밍 시연이 목적이므로 다음은 다루지 않습니다.
- 재연결 로직
- 인증 및 암호화
Unity 6000.0.68f1 / .NET Standard 2.1 / Windows 11