HTTP & WebSocket 기술 분석
2 HTTP 공개·전송 계층
4 WebSocket 상태 단계
0 기본 HTTP 재시도 횟수
16 KB Socket 고정 버퍼 크기
01 / Executive Summary

먼저 설명할 핵심

이 시스템은 HTTP 요청·응답과 실시간 WebSocket을 별도 파이프라인으로 운영한다. 공통점은 바이너리 프로토콜과 암호화지만, 상태 관리와 장애 복구 책임은 서로 다르다.

30초 답변

“HTTP는 WebClient가 Unity용 공개 API와 콜백 라우팅을 담당하고, ProtoWebClient가 요청을 직렬 큐로 전송합니다. WebSocket은 NetSocket이 연결 상태·프레임·송수신을, SocketClient가 핸드셰이크·인증·방·릴레이 프로토콜을 맡습니다. HTTP에는 타임아웃 기반 재시도가 있지만 백오프와 멱등성 구분이 없고, 소켓에는 자동 재접속과 세션 복원이 없습니다.”

답변할 때 구분해야 할 것

“코드가 실제로 하는 일”과 “운영 환경에서 발생할 수 있는 영향”을 분리한다. 예를 들어 정적 HttpClient가 공유된다는 것은 사실이고, 한 인스턴스의 취소가 다른 인스턴스에 영향을 줄 수 있다는 것은 API 의미와 현재 다중 인스턴스 생성 구조를 결합한 리뷰 판단이다.

02 / Architecture

책임과 경계

기술 리뷰에서는 클래스를 나열하기보다 각 계층이 무엇을 소유하고 어디까지 책임지는지 설명하는 편이 명확하다.

HTTP 공개 계층

WebClient는 URL별 Unity GameObject 인스턴스, 프로토콜별 요청 API, 응답 캐스팅과 사용자 콜백을 소유한다.

Client/Assets/Haegin/Network/Web/WebClient.cs:252 · Client/Assets/Haegin/Network/Web/WebClient.cs:350

HTTP 전송 계층

ProtoWebClient는 큐, 타임아웃, 직렬화, 암호화, 플랫폼별 HTTP 구현과 완료 큐를 소유한다.

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:92 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:197

메인 스레드 브리지

ThreadSafeDispatcher는 백그라운드 완료 처리를 Unity 메인 스레드의 Update()에서 실행한다.

Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:59 · Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:105

Socket 기반 계층

NetSocket은 상태 머신, 연결 ID, 버퍼, 프레임, 암복호화, 송수신 루프를 담당한다.

Client/Assets/Haegin/Network/Socket/NetSocket.cs:20 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:93

Socket 프로토콜 계층

SocketClient는 Handshake → Auth, Alive, 방 관리, 릴레이 이벤트를 구체화한다.

Client/Assets/Haegin/Network/Socket/SocketClient.cs:34 · Client/Assets/Haegin/Network/Socket/SocketClient.cs:60

게임 오케스트레이션

CoopManager는 소켓 인증 대기, 방 상태, 메인 스레드 전환과 게임 상태 정리를 담당한다.

Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:165 · Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:459

설계 해석

HTTP는 “요청 순서를 보장하는 단일 차선”에 가깝고, WebSocket은 “연결을 유지하면서 양방향 이벤트를 처리하는 상태 머신”에 가깝다. 따라서 HTTP의 핵심 질문은 큐·타임아웃·콜백 상관관계이고, WebSocket의 핵심 질문은 연결 수명·프레임 무결성·재접속 후 상태 복구다.

03 / HTTP Flow

HTTP 요청과 응답 흐름

요청은 즉시 병렬 전송되지 않는다. readyQueue에 들어간 뒤 현재 요청 하나만 처리되고, 완료 응답은 doneQueue를 거쳐 Unity 프레임에서 콜백으로 전달된다.

1. Public API RequestHandshake2 등에서 콜백 필드와 Protocol을 설정
2. Enqueue ReqAndRes 생성 후 readyQueue에 추가
3. Serialize Encrypted byte + Hash + MessagePack/XXTea payload 구성
4. POST 플랫폼에 따라 HttpClient, HttpWebRequest, UnityWebRequest 사용
5. Complete 응답 복호화·역직렬화 후 doneQueue로 이동
6. Dispatch Update → Process → OnProcessing → callback

정확한 순서

  1. WebClient.Request(...)가 프로토콜을 ProtoWebClient.Request(...)로 전달한다.
  2. ReqAndRes가 요청 당시 Suid, 암호화 단계와 ProtocolReq를 캡처한다.
  3. Process()가 현재 요청이 없을 때 큐의 선두를 currentRAR로 잡는다.
  4. 요청 본문은 [Encrypted:1][Hash:4][ProtocolReq:N] 형태로 구성된다.
  5. 응답은 암호화 플래그를 먼저 읽고 XXTea 키를 선택한 뒤 ProtocolRes로 역직렬화한다.
  6. 완료 응답을 doneQueue로 옮기고, 다음 Unity Update()에서 프로토콜별 Process(req, res)가 호출된다.

코드 확인 Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:33 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:240 · Client/Assets/Haegin/Network/Web/WebClient.cs:2749

순차 전송의 장점과 한계

장점은 응답 순서와 공유 인증 상태를 단순하게 유지하는 것이다. 반면 느린 요청 하나가 뒤의 모든 요청을 막는 head-of-line blocking이 발생하고, 요청 우선순위나 독립 취소가 없다.

04 / WebSocket Flow

연결, 인증, 송수신 흐름

소켓은 TCP 스트림처럼 자체 프레임을 구성하지만 실제 전송 수단은 ClientWebSocket의 Binary 메시지다.

1. Connect 상태를 Connecting으로 CAS 전환하고 새 connection ID 발급
2. Open ClientWebSocket.ConnectAsync 완료
3. Init Cancellation, IV, 송수신 버퍼 초기화
4. Handshake Common IV로 HandshakeQ 전송, 서버 키와 IV 수신
5. Auth Suid/Nickname 전송, RemoteId/Remote IV 수신
6. Relay 방 프로토콜과 RelayQ/RelayA 양방향 처리
7. Alive 7초 주기 heartbeat와 RTT 기반 시각 보정

연결 상태 머신

Disconnected → Connecting

Interlocked.CompareExchange로 중복 연결 진입을 막는다.

Connecting → Connected

WebSocket 연결과 초기화 후 상태를 전환하고 HandshakeQ를 보낸다.

Connected → Disconnecting

현재 connection ID와 상태가 모두 일치하는 호출만 종료 권한을 얻는다.

Disconnecting → Disconnected

최대 5초 graceful close 후 Cancellation과 Socket을 정리하고 이벤트를 발행한다.

코드 확인 Client/Assets/Haegin/Network/Socket/NetSocket.cs:93 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:168 · Client/Assets/Haegin/Network/Socket/SocketClient.cs:60

프로토콜 디스패치

수신 프레임은 ProtocolSerializer.Deserialize 이후 ProtocolId로 분기된다. 역직렬화 실패나 알 수 없는 ProtocolId는 false를 반환해 연결 종료로 이어진다. Handshake 버전 불일치와 Auth 실패도 명시적으로 연결을 끊는다.

Client/Assets/Haegin/Network/Socket/SocketClient.Process.cs:10 · Client/Assets/Haegin/Network/Socket/SocketClient.ProcessHandshakeA.cs:8 · Client/Assets/Haegin/Network/Socket/SocketClient.ProcessAuthA.cs:8

05 / Error Handling

에러는 어디에서 어떻게 처리되는가

이 구현은 전송 예외, 프로토콜 오류, 업무 결과 코드를 서로 다른 채널로 전달한다. 분석에서는 “에러를 잡는다”보다 “누가 어떤 정보로 어떤 복구를 결정하는가”를 설명해야 한다.

계층 실패 종류 현재 처리 상위 전달 판단
ProtoWebClient 직렬화·역직렬화·큐 처리 로그 후 PWC 음수 코드 발행 또는 null 반환 ErrorOccurred(int) 오류 분류는 있으나 문맥과 원인 체인이 약함
HTTP 전송 네트워크 예외·응답 없음 로그 또는 빈 분기 후 타임아웃 재시도에 의존 RetryOccurred, RetryFailed 즉시 실패와 시간 초과가 일관되게 모델링되지 않음
WebClient 서버 ErrorRes·업무 Result 요청 종류별 Process(req,res)에서 ErrorCode 변환 각 요청 callback API 사용은 단순하지만 매핑 중복이 큼
NetSocket 송신·수신·암호·프레임 오류 로그 후 현재 connection ID를 대상으로 Disconnect OnDisconnected fail-closed 정책은 명확하나 원인 정보가 손실됨
SocketClient Auth 실패·서버 Error 프로토콜 OnError 발행, Auth 실패 시 Disconnect CoopManager 업무 오류와 연결 오류 이벤트가 분리됨
CoopManager 인증 실패·10초 타임아웃·비정상 종료 false 반환, 방 상태 초기화 UI 호출 결과 사용자 흐름은 복구하지만 재접속은 하지 않음

관측성 문제

ThreadSafeDispatcher.Update()는 디스패치된 콜백 예외를 빈 catch로 삼킨다. 결과적으로 서버 응답은 정상이어도 사용자 callback에서 발생한 버그가 로그 없이 사라질 수 있다.

Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:114

06 / Timeout & Retry

HTTP 재시도와 Socket 재연결

HTTP 재시도의 실제 동작

요청 시작

startTime = DateTime.Now. 기본 TimeOut은 WebGL 2초, 그 외 20초다.

ElapsedTime > TimeOut

현재 요청의 Retry 값을 1 증가시키고 전송을 Abort한다.

retry ≤ RetryMax

RetryOccurred만 발행한다. 현재 요청은 큐 선두에 남아 다음 프레임에서 즉시 다시 전송된다.

retry > RetryMax

현재 요청과 준비 큐 전체를 Reset하고 RetryFailed를 발행한다.

코드 확인 Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:171 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:261 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:763

항목 현재 구현 운영 의미
기본 재시도 RetryMax = 0 첫 타임아웃에서 즉시 실패. 호출자가 값을 설정한 경우만 재시도
간격 별도 delay 없음 장애 서버에 즉시 재전송되어 부하가 겹칠 수 있음
백오프·지터 없음 여러 클라이언트가 같은 시점에 재시도하는 thundering herd 가능
멱등성 프로토콜 구분 없음 구매·소비처럼 중복 실행에 민감한 요청은 서버 idempotency key에 의존해야 함
실패 범위 Reset()이 readyQueue 전체 제거 하나의 요청 실패가 뒤에 대기 중인 무관 요청도 취소

WebSocket은 재연결하지 않는다

NetSocket은 연결 종료와 자원 정리만 제공한다. CoopManager.EnsureConnected()는 기능 호출 시 한 번 연결을 보장하지만, 플레이 중 비정상 종료 후 자동 재시도·동일 방 재합류·누락 상태 동기화는 구현하지 않는다. 현재 정책은 끊김을 세션 종료로 보고 ResetRoomState()를 호출하는 것이다.

Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:453 · Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:579

리뷰 판단

자동 재접속을 단순히 ConnectAsync() 반복으로 추가하면 충분하지 않다. 재연결 후 Room ID, 역할, 마지막 확정 시퀀스, 게임 스냅샷을 복구하지 않으면 “연결됨”과 “플레이 가능한 일관된 상태”가 다르기 때문이다.

07 / Protocol & Security

직렬화, 프레이밍, 암호화

HTTP 본문

Encrypted 1 byte
Hash 4 bytes
ProtocolReq MessagePack 원문 또는 XXTea 암호문

Handshake 이전·이후 암호화 키를 xxtea1, xxtea2로 분리한다. 인증 성공 후에는 서버 Hash와 첫 키를 XOR해 두 번째 키를 만든다.

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:292 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:905

WebSocket 프레임

Length UInt32 little-endian, 전체 길이
Key Index 1 byte
Protocol MessagePack + 선택적 AES-CBC 암호문

수신은 최소 5바이트를 기다린 뒤 length를 읽고, 전체 프레임이 버퍼에 쌓일 때까지 반환한다. Key Index가 0보다 크면 해당 IV로 복호화한 뒤 프로토콜을 역직렬화한다.

Client/Assets/Haegin/Network/Socket/NetSocket.cs:223 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:327

보안상 중요한 사실

Critical

활성 non-WebGL HttpClient 경로는 서버 인증서 검증 콜백에서 항상 true를 반환한다. HTTPS를 사용해도 서버 신원 검증이 무력화되어 중간자 공격에 취약하다.

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:150 · Client/ProjectSettings/ProjectSettings.asset:839

XXTea와 AES-CBC는 payload 기밀성을 제공할 수 있지만 TLS 서버 인증을 대체하지 않는다. 또한 코드에 보이는 프레임은 별도 MAC이나 AEAD 태그를 포함하지 않으므로 무결성 보장은 전송 채널과 프로토콜 구현의 다른 부분에 의존한다.

08 / Threading & Concurrency

스레드 전환과 동시성

경로 실행 컨텍스트 보호 방식 주의점
HTTP ready/done queue Unity Update + HTTP continuation 각 Queue 객체 lock currentRAR와 정적 HttpClient는 별도 전역 경계
HTTP callback ThreadSafeDispatcher를 거친 메인 스레드 pending/runQueue lock callback 예외가 무시됨
Socket send buffer 호출 스레드 + 비동기 송신 Task sendLock 버퍼 초과 시 연결 종료
Socket receive 비동기 Task continuation connection ID + CancellationToken 이벤트가 백그라운드 스레드에서 발생할 수 있음
Coop game apply Unity 메인 스레드 UniTask.Post 이벤트별 전환 누락 여부를 지속 확인해야 함

콜백 덮어쓰기 위험

WebClienthandshakedResult, authResult, maintenanceCheckResult 같은 콜백을 요청 객체가 아니라 인스턴스 필드에 저장한다. 전송 큐가 요청 실행을 직렬화하더라도 같은 종류의 두 번째 요청을 첫 응답 전에 등록하면 첫 요청이 두 번째 콜백을 호출할 수 있다.

리뷰 판단 Client/Assets/Haegin/Network/Web/WebClient.cs:163 · Client/Assets/Haegin/Network/Web/WebClient.cs:769

정적 HttpClient 취소 범위

ProtoWebClient.Client는 정적이지만 Abort()는 인스턴스 타임아웃에서 Client.CancelPendingRequests()를 호출한다. URL별 WebClient가 여러 개 존재할 수 있으므로 한 인스턴스의 타임아웃이 공유 클라이언트의 다른 진행 요청까지 취소할 수 있다.

High Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:78 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:206 · Client/Assets/Haegin/Network/Web/WebClient.cs:252

Connection ID가 해결하는 문제

소켓의 증가 ID는 이전 연결에서 늦게 도착한 송수신 continuation이 새 연결을 종료하거나 버퍼를 처리하는 것을 막는다. DisconnectAsync(socketId)와 루프 조건이 동일한 ID인지 확인하므로 stale task를 구별할 수 있다.

코드 확인 Client/Assets/Haegin/Network/Socket/NetSocket.cs:22 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:261 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:297

09 / Strengths & Tradeoffs

설명할 가치가 있는 강점

강점은 “좋다”가 아니라 어떤 실패를 줄이고 어떤 비용을 치르는지로 설명한다.

설계 선택 구체적 이점 대가
Strength HTTP 직렬 큐 인증 Hash·키 변경과 요청 순서를 단순하게 유지 느린 요청이 전체 큐를 막음
Strength connection ID 이전 연결의 늦은 비동기 작업이 새 연결을 오염시키는 문제 완화 모든 비동기 경로가 ID를 정확히 전달해야 함
Strength fail-closed frame 처리 복호화·프로토콜 오류 상태에서 계속 진행하지 않음 단일 잘못된 메시지가 세션 전체 종료로 이어짐
Strength 프로토콜 계층 분리 NetSocket은 transport에 집중하고 SocketClient가 도메인 이벤트를 제공 이벤트 수명과 스레드 규약이 타입으로 강제되지 않음
Strength 메인 스레드 브리지 Unity API를 안전한 실행 컨텍스트에서 호출 예외·취소·요청 문맥 전달이 약함
10 / Risk Review

주요 리스크와 심화 검토 지점

심각도는 “발생 가능성 × 영향 × 탐지 난이도”를 기준으로 한 리뷰 판단이다.

심각도 리스크 영향 근거
Critical HTTP 인증서 검증 무조건 통과 서버 사칭·중간자 공격 탐지 불가 Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:153
High 정적 HttpClient 전체 요청 취소 한 URL의 타임아웃이 다른 WebClient 요청을 연쇄 취소 Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:220
High 동종 요청 콜백 필드 덮어쓰기 응답이 잘못된 호출자에게 전달 Client/Assets/Haegin/Network/Web/WebClient.cs:163
High 소켓 인증 타임아웃 cleanup이 Disconnect를 보장하지 않음 백그라운드 수신·Alive timer와 소켓 수명 누수 가능 Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:204 · Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:884
High 자동 재접속·세션 복구 없음 일시적 망 전환도 코옵 세션 종료 Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:579
Medium 백오프·지터·멱등성 정책 없음 장애 시 재시도 폭주와 중복 업무 처리 가능 Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:763
Medium 수신 length 상한 검증 없음 16KB를 넘는 선언 길이에서 진전 없이 버퍼 고갈 후 종료 Client/Assets/Haegin/Network/Socket/NetSocket.cs:338
Medium 디스패치 callback 예외 은폐 클라이언트 오류의 원인 추적 어려움 Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:114
Medium Socket send buffer full 시 즉시 연결 종료 순간 burst가 세션 장애로 확대 Client/Assets/Haegin/Network/Socket/NetSocket.cs:227

과장하지 말아야 할 부분

length 상한 미검증이 곧바로 대규모 메모리 할당 취약점은 아니다. 이 구현은 고정 16KB 버퍼를 사용하므로 선언 길이만큼 할당하지 않는다. 실제 문제는 처리 불가능한 프레임을 조기에 거부하지 않아 수신 버퍼가 가득 찬 뒤 예외와 연결 종료로 이어진다는 점이다.

11 / Improvement Roadmap

개선 우선순위

전면 재작성보다 보안과 장애 전파 범위를 먼저 줄이고, 이후 요청 상관관계와 세션 복구를 구조화하는 순서가 현실적이다.

  1. 인증서 검증 복원

    기본 플랫폼 검증을 사용하고, 개발 인증서가 필요하면 빌드 환경별 명시적 trust store 또는 제한된 pinning을 적용한다.

  2. 요청별 CancellationToken과 응답 컨텍스트 도입

    CancelPendingRequests()를 제거하고 요청별 취소를 사용한다. callback도 ReqAndRes 또는 Task 결과에 결합한다.

  3. 재시도 정책을 프로토콜 메타데이터로 분리

    멱등 요청만 exponential backoff + jitter로 재시도하고, 구매·소비는 idempotency key와 서버 중복 제거를 전제로 한다.

  4. 에러 모델과 관측성 통합

    Transport, Timeout, Serialization, Protocol, Business 결과를 구조화하고 request ID, protocol ID, elapsed, attempt를 로그에 포함한다.

  5. 소켓 종료 수명 보장

    인증 타임아웃·실패 경로에서 반드시 await DisconnectAsync() 후 참조를 비우고 timer와 event 구독을 정리한다.

  6. 재접속을 세션 복구 프로토콜로 설계

    backoff 연결, resume token, room ID, last acknowledged sequence, snapshot/delta 동기화를 하나의 복구 상태 머신으로 만든다.

  7. 프레임·버퍼 정책 명시

    최대 프레임 크기를 상수로 검증하고, send backpressure 또는 bounded queue로 burst를 흡수하며 초과 정책을 메트릭으로 노출한다.

개선 후 기대하는 인터페이스

SendAsync<TRes>(ProtocolReq request, RequestPolicy policy, CancellationToken ct)
→ NetworkResult<TRes> { Kind, ProtocolId, Attempt, Elapsed, Value, Exception }

핵심은 async 형태 자체가 아니라 요청·응답·취소·오류가 동일한 수명 객체에 묶이는 것이다.

12 / Technical Q&A

예상 질문과 답변

먼저 30초 요약을 말하고, 세부 근거나 대안을 확인할 때 심화 설명으로 확장한다.

Q1. WebClient와 ProtoWebClient의 책임을 어떻게 나눴나요?
30초 답변

WebClient는 Unity-facing facade와 protocol callback router이고, ProtoWebClient는 queue, transport, serialization, timeout을 담당합니다. 업무 결과 변환과 전송 메커니즘을 분리한 구조입니다.

Deep Dive

WebClient.Update()가 전송 엔진을 tick하고, 완료된 ReqAndRes를 요청 ProtocolId에 따라 typed Process로 보냅니다. 다만 callback이 요청 객체가 아닌 필드에 있어 완전한 request context 분리는 아닙니다.

Evidence

Client/Assets/Haegin/Network/Web/WebClient.cs:350 · Client/Assets/Haegin/Network/Web/WebClient.cs:2749 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:197

Q2. HTTP 요청을 왜 큐로 직렬화했을까요?
30초 답변

Handshake와 Auth에서 키와 Hash가 바뀌는 프로토콜이라 요청 순서를 단순하게 보장하려는 선택으로 보입니다. 모바일 게임의 로그인·구매 API를 한 차선으로 관리하면 상태 추론이 쉬워집니다.

Tradeoff

느린 요청 하나가 전체를 막고, 독립적인 telemetry나 maintenance 요청까지 같은 큐라면 응답성이 낮아집니다. 병렬화가 필요하면 인증 상태 의존성과 요청 그룹을 먼저 분류해야 합니다.

Evidence

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:94 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:261

Q3. 현재 HTTP 재시도 정책을 평가해보세요.
30초 답변

기본 재시도는 0회이고, 설정 시 타임아웃마다 즉시 같은 요청을 다시 보냅니다. 횟수 제한은 있지만 backoff, jitter, 오류 분류, 멱등성 구분이 없어 운영 장애 대응 정책으로는 부족합니다.

Improvement

연결 오류·408·429·5xx처럼 일시적 실패만 재시도하고, Retry-After를 존중하며 exponential backoff와 jitter를 사용합니다. 비멱등 요청은 idempotency key 없이는 자동 재시도하지 않습니다.

Evidence

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:176 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:763

Q4. 정적 HttpClient 사용은 잘못된 것 아닌가요?
30초 답변

공유 HttpClient 자체는 connection pooling과 socket exhaustion 방지 측면에서 합리적입니다. 문제는 인스턴스 단위 Abort가 공유 객체의 CancelPendingRequests()를 호출해 취소 범위를 전역으로 넓힌 점입니다.

Nuance

따라서 “static을 없앤다”보다 요청별 CancellationToken을 전달하고 HttpClient는 재사용하는 방향이 적절합니다. 수명과 취소 범위를 분리해야 합니다.

Evidence

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:78 · Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:220

Q5. 소켓에서 connection ID가 왜 필요한가요?
30초 답변

취소된 이전 연결의 async receive/send가 늦게 완료될 수 있기 때문입니다. 증가 ID를 루프와 disconnect 조건에 넣으면 stale continuation이 현재 연결을 건드리는 것을 막을 수 있습니다.

Limit

ID만으로 게임 세션 연속성은 해결되지 않습니다. transport 세대를 구분할 뿐이며, 재접속 후 room state와 message sequence 복원은 별도 프로토콜이 필요합니다.

Evidence

Client/Assets/Haegin/Network/Socket/NetSocket.cs:22 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:168

Q6. WebSocket 메시지 경계는 어떻게 처리하나요?
30초 답변

WebSocket Binary 메시지 위에 별도 length-prefix 프레임을 둡니다. 4바이트 전체 길이, 1바이트 key index, payload 순서이며 수신 버퍼에 전체 길이가 모이면 한 프레임씩 처리합니다.

Risk

최소 길이는 검사하지만 최대 길이를 먼저 거부하지 않습니다. 고정 16KB 버퍼보다 큰 선언 길이는 처리 완료가 불가능하므로 명시적 MaxFrameSize 검증이 필요합니다.

Evidence

Client/Assets/Haegin/Network/Socket/NetSocket.cs:223 · Client/Assets/Haegin/Network/Socket/NetSocket.cs:338

Q7. 소켓 끊김 시 자동 재접속을 바로 넣으면 되나요?
30초 답변

연결 재시도만 넣으면 transport는 복구돼도 게임 상태는 복구되지 않습니다. 동일 방 복귀, 사용자 역할, 마지막 처리 sequence, 스냅샷과 delta 재적용까지 포함한 세션 복구 상태 머신이 필요합니다.

Current Policy

현재는 끊김을 세션 종료로 처리하고 room state를 초기화합니다. 기능 범위가 명확하다는 장점은 있지만 일시적 네트워크 변경에도 게임이 종료됩니다.

Evidence

Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:579 · Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:884

Q8. Unity 메인 스레드 전환은 안전하게 처리됐나요?
30초 답변

HTTP는 ThreadSafeDispatcher, 코옵 이벤트는 UniTask.Post로 메인 스레드에 넘깁니다. Unity 오브젝트 접근 경계를 의식한 구조입니다.

Caveat

ThreadSafeDispatcher는 실행된 Action의 예외를 삼키므로 안전성과 관측성이 교환됐습니다. 예외를 구조화해 로깅하고 요청 문맥을 붙여야 운영 디버깅이 가능합니다.

Evidence

Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:59 · Client/Assets/Haegin/Common/ThreadSafeDispatcher.cs:114 · Client/Assets/Game/Scripts/GamePlay/Coop/CoopManager.cs:481

Q9. 이 코드에서 가장 먼저 수정할 한 가지는 무엇인가요?
30초 답변

인증서 검증을 무조건 통과시키는 코드를 제거하겠습니다. 재시도나 구조 개선보다 보안 경계가 먼저이며, 변경 범위도 비교적 작고 효과가 명확합니다.

Follow-up

그 다음은 요청별 취소와 callback context 결합입니다. 두 문제를 해결하면 교차 요청 취소와 콜백 오배달이라는 데이터 정합성 위험을 함께 줄일 수 있습니다.

Evidence

Client/Assets/Haegin/Network/Web/Source/ProtoWebClient.cs:153

Q10. 이 구현을 테스트한다면 어떤 시나리오를 우선하나요?
30초 답변

HTTP는 timeout 직전 응답과 retry 응답 경합, 동종 요청 중첩, 다중 URL 취소 격리를 우선합니다. Socket은 partial frame, oversized length, send buffer full, 이전 connection의 늦은 receive, auth timeout cleanup을 테스트합니다.

Method

transport를 주입 가능한 인터페이스로 만들고 deterministic clock과 fake server를 사용합니다. 재시도 횟수뿐 아니라 callback 대상, queue 보존, cancellation scope, 최종 socket/timer disposal을 검증합니다.

Success Criteria

오류가 발생했을 때 “실패 이벤트가 왔다”만 보지 않고, 다른 요청·연결·게임 상태가 오염되지 않았음을 함께 확인합니다.

13 / Final Checklist

빠른 복습

  • WebClient는 facade + callback router라고 설명한다.
  • ProtoWebClient는 한 번에 한 HTTP 요청만 처리한다.
  • 기본 RetryMax는 0, non-WebGL Timeout은 20초다.
  • 재시도에는 backoff, jitter, 멱등성 분류가 없다.
  • 공유 HttpClient와 전역 취소 범위의 차이를 설명한다.
  • 콜백 필드 덮어쓰기 조건을 구체적으로 말한다.
  • ThreadSafeDispatcher의 메인 스레드 이점과 예외 은폐를 함께 말한다.
  • NetSocket의 4단계 상태와 connection ID 목적을 설명한다.
  • Socket frame은 length 4 + key index 1 + payload다.
  • 복호화·프로토콜 오류는 fail-closed로 연결 종료된다.
  • 자동 재접속과 세션 복구는 별개라고 설명한다.
  • 첫 개선은 인증서 검증 복원이라고 답한다.

마지막 한 문장

“현재 구현은 모바일 게임 통신을 단순한 직렬 HTTP와 명시적 상태의 WebSocket으로 분리한 구조지만, 운영 수준에서는 보안 검증, 요청별 수명, 재시도 정책, 관측성, 세션 복구를 보강해야 합니다.”