✅ 끝났다 (2026-09-08). A·B 둘 다 실측으로 닫혔다. A(무작위 끊김) — 1시간 넘게 켜 둬도 끊기지 않는다. 판정 15초 / 핑 5초로 여유가 0 → 10초다. B(드래그) — 창을 20초 이상 끌어도 유지된다 (T-041 · 2026-09-07).
이 사고는 값이 틀려서가 아니라 두 값의 짝이 안 맞아서 났다. 그래서 값을 고치는 데서 끝내지 않고 "핑 주기 ≤ 판정 시간 ÷ 3"을 규칙으로 못 박고 테스트로 묶었다 — 그게 이 일감의 실제 산출물이다.
배경
독립된 문제가 둘이다. 각각 하나씩, 총 두 개를 골라야 한다.
| 증상 | 원인 | 빈도 | |
|---|---|---|---|
| A | 아무 조작도 안 했는데 연결이 끊긴다 | 서버 판정(5초)과 클라 핑 주기(5초)의 여유가 0 | 수십 분에 한 번 |
| B | 창을 5초 이상 끌면 끊긴다 | 소켓 루프가 Unity 메인 스레드에 묶여 있다 | 확정적 |
A와 B는 서로를 풀어 주지 않는다. 둘 다 결정이 필요하다. ⚠️ 에디터에서는 B가 재현되지 않는다 — 창 제어 코드가
#if !UNITY_EDITOR다. 빌드에서만 나온다.
⚠️ 전제 — 창 드래그는 없앨 수 없다
B를 볼 때 "드래그를 빼면 되지 않나"가 먼저 떠오르는데, 그게 안 되는 것이 이 문제의 본질이다.
- 이 게임은 데스크톱 위에 상주하는 방치형이라 창이 배경에 얹혀 보여야 한다. 그래서 OS 타이틀바를 지웠다(보더리스 + 투명).
- 타이틀바를 지우면 창을 옮길 손잡이도 사라진다. 그래서 UI 패널(
WindowDragArea)이 타이틀바 역할을 한다. 창 이동은 빼도 되는 기능이 아니라 타이틀바를 포기한 대가로 반드시 있어야 하는 기능이다. - 이동은 직접 좌표 계산 대신
WM_SYSCOMMAND(SC_MOVE_HTCAPTION)으로 OS 이동 루프에 위임한다 — 스냅 · 더블클릭 최대화 · 모니터 간 이동 · DPI 전환이 전부 공짜로 따라오기 때문이다. (DesktopWindow 규칙.md§5-7) - 이 위임은 데스크톱 상주형의 특수 사정이다. 전체화면 게임은 타이틀바가 있어 이 코드도, 이 문제도 없다.
대가가 이번 문제다. OS 이동 루프가 도는 동안 Unity 메인 스레드만 멈추는데 (다른 스레드는 멀쩡하다), 하필 소켓 송수신이 메인 스레드에 얹혀 있어 같이 멈춘다.
그래서 B의 선택지는 "드래그를 없앤다"가 아니라 ① 메인 루프가 멈춰도 소켓은 살아 있게 한다(B-1) ② OS 위임을 포기하고 직접 구현한다(B-2) 둘 중 하나다.
경위 — 왜 한 번 되돌렸는가
클라가 시험 적용해 증상이 사라지는 것까지 실측 확인했지만, 되돌렸다.
효과를 낸 수정(Task.Run + ConfigureAwait(false))이 서버 담당 폴더
(Assets/Scripts_Server/Network/MikaNetwork.Client/)에 있었고, 그 방식이 유일한 답도 아니다.
후보마다 경제 밸런스(좀비 부당 적립) · 창 제어 설계(OS 위임을 버릴 것인가) · 패킷 추가와 얽힌다.
클라 단독으로 정할 문제가 아니다.
- 되돌린 커밋
45b55c6→ 되돌림 커밋2fb9dcb. 클라 몫은 여기서 그대로 꺼내 쓸 수 있다. - 이후 B는 B-1로 결정되어 서버 몫이 다시 들어갔다(아래 "할 일" 참조). 클라 몫이 들어가기 전까지는 드래그 증상이 그대로 재현된다.
할 일
B — B-1로 결정됨 (서버 몫 · 클라 몫 둘 다 완료)
- [x]
B-1소켓 루프를 스레드풀로 — 적용 완료 (add9b9f· 2026-09-02)MikaClient.ConnectAsync가Task.Run으로 세션 루프를 띄우고, 세션·커넥터의await가ConfigureAwait(false)로 호출자 컨텍스트를 캡처하지 않는다- 스레드가 갈리면 드러날 종료 경로도 함께 막았다 —
Disconnect()멱등 가드 (Interlocked.Exchange) ·ReceiveLoop예외 처리 ·IsConnectedvolatile ·Dispose순서 - 원본(
Server/MikaNetwork.Lib/MikaNetwork.Client/)과 Unity 수동 사본 (Assets/Scripts_Server/Network/MikaNetwork.Client/)을 함께 고쳤다 - 회귀 테스트:
Server/WSGameServer.Tests/Network/ClientSessionShutdownTest.cs
- [x] 클라 몫 — 핑 생성을 백그라운드 타이머로 옮긴다 — 완료 (T-041 · 2026-09-07)
- B-1은 큐를 비우는 쪽만 푼다.
PingManager가 핑을Update()에서 만들었으므로 드래그로Update가 멈추면 큐에 들어갈 핑 자체가 없었다. 둘은 한 쌍이다 System.Threading.Timer송신 ·Stopwatch단조 시계 · 타이머 정리에 더해, 🔑 긴 프레임 정지 뒤 오탐 종료 가드가 함께 들어갔다 — 이게 없으면 서버가 안 끊어도 밀린 Pong을 오판해 클라가 스스로 앱을 내린다(Flush와 판정의 실행 순서가 보장되지 않는다)- ⛔ 핑 주기 2초(A-2)는 가져오지 않았다 — A-1로 판정이 15초가 됐으므로 5초가 규칙에 맞는 값이다
- ✅ 빌드 실측 통과 (2026-09-07) — 창을 20초 이상 끌어도 알림·서버 로그 둘 다 없다
- B-1은 큐를 비우는 쪽만 푼다.
A — A-1로 결정됨 · 적용 정정 완료 (2026-09-02)
- [x] A-1 채택 — 서버 판정을 늘린다.
A-2(핑 주기 2초)·A-3(판정 시간을 패킷으로 하달)은 밀렸다 - [x] 🔴
add9b9f가 늘린 것은 판정 시간이 아니라 검사 주기였다 — 되돌렸다Global.SessionIdleTimeout5초 → 15초 (판정 시간. 이게 여유를 만드는 값이다)SessionWatchdog.Interval15초 → 5초 (검사 주기. 판정보다 짧아야 한다)- 15초는 새로 정한 값이 아니다 — 작업슬롯 3.3이
2026-08-04에 확정해 둔 값이고, 코드가 5초로 내려가 있던 것이 회귀였다.
기존 테스트
SessionIdleSweepTest도 이미 15초를 전제로 짜여 있었다 - 검사 주기를 늘리면 증상의 빈도만 낮아지고 좀비 세션 수명은 오히려 늘어난다(판정 + 검사 지연).
같은 함정을 다시 밟지 않도록
SessionWatchdog에 경고 주석을 박았다
- [x] 📌 "핑 주기 ≤ 판정 시간 ÷ 3"을 규칙으로 못 박았다
- 작업슬롯 3.3에 부등식 세 개를 표로 명시 (핑 주기 ≤ 판정 ÷ 3 · 검사 주기 < 판정 · 판정 < 채취 기준 주기) + 두 번 어긴 경위
- 게임기획코어 5장 하트비트 행에도 얹었다
- 코드 양쪽(
Global.SessionIdleTimeout·PingManager.PingIntervalSeconds) 주석에 서로가 짝이라는 것과 한쪽만 고치지 말라는 경고를 넣었다 - 테스트가 지킨다 —
SessionIdleSweepTest.검사_주기는_판정_시간보다_짧다신설 (SessionWatchdog.Interval을internal로 열었다). 핑 주기는 클라 상수라 서버가 모르므로 문서·주석으로만 묶었다
- [x]
Global.cs의 낡은 주석 정정 — "세 번 연속 놓쳐야 끊기는 값" 이 판정 15초 복구로 도로 맞는 말이 됐다. 불변식과 T-004 경고를 함께 적었다 - [ ] ⚠️ T-004가 남았다 — 배포 전 필수.
판정 15초는 채취 기준 주기 30초 기준으로 안전하다. 그런데 지금 개발 서버는
GatherSpeedMultiplier = 6.0이라 실효 주기가 5초여서, 좀비가 최대 3주기를 부당 적립한다. 개발 중에는 무해하지만 배수를 되돌리기 전에는 이 보장이 없다. T-004에 메모를 걸어 두었다
후속 (급하지 않음)
- [ ]
MikaServerSession도IsConnected가시성·Dispose순서가 클라 세션과 같은 상태다. 서버 세션은 이미 스레드풀에서 돌아 이번 증상과 무관해 이번에는 건드리지 않았다
후보에서 뺀 것 둘 —
클라 핑만 백그라운드 타이머(단독 무효, 빌드 실측 확인) ·TCP keepalive(앱이 죽어도 응답해 이슈 #10의 취지를 무효화).
완료 조건
빌드에서 창을 10초 이상 끌어도 연결이 유지되고, 몇 시간을 켜 둬도 무작위 끊김이 없다.
서버 콘솔에 정상 세션이 무응답으로 끊었다로 잡히지 않는다.
검증 현황 (2026-09-08 — 전부 통과)
| 상태 | |
|---|---|
| 서버 테스트 | ✅ 198개 전부 통과 (불변식 테스트 2개 포함) |
| 장시간 무작위 끊김 (증상 A) | ✅ 실측 통과 (2026-09-08) — 1시간 넘게 켜 둬도 끊기지 않는다 |
| 드래그 20초 (증상 B) | ✅ 빌드 실측 통과 (2026-09-07 · T-041) |
⚠️ 이 일감의 완료 조건 중 드래그 항목은 T-041이 닫았다. 여기서는 서버 값과 공용 규칙까지가 범위다.
📌 빌드로 확인하기 전에는 닫지 않았다. 2026-08-28에
dotnet build만 통과시키고 불완전한 수정을 올린 전례가 있다 — 런타임 증상은 런타임으로 검증한다.
막고 있는 것 / 선행 일감
- ✅ 결정은 끝났다 — A-1 · B-1 (이슈 #19).
- ✅ 드래그 증상은 T-041(클라)이 닫았다 (2026-09-07 · 빌드 실측).
- 드래그 증상은 T-041(클라)이 남았다 — 선행 없이 착수 가능하다.
- T-004는 배포 전 게이트로 남는다 —
GatherSpeedMultiplier = 6.0이면 실효 채취 주기가 5초라, 판정 15초 동안 좀비 세션이 3주기를 부당 적립한다. 두 값은d2c52c6에서 함께 내려온 짝이다. 개발 중에는 무해하므로 이 일감을 막지는 않는다.
관련 커밋
45b55c6시험 적용 (되돌림)2fb9dcb철회add9b9f이슈 #19 수정 — A-1 · B-1 (B-1 서버 몫 완료 / ⚠️ A-1은 검사 주기에 들어갔다)
참고
- 조사 경위:
.claude/Agent/2026-08-28-ping-disconnect-cause.md - B-1 적용 경위:
.claude/Agent/2026-08-29-client-session-threadpool.md - 창 이동을 OS에 위임한 근거:
DesktopWindow 규칙.md§5-7 - A의 전조는
.claude/Agent/2026-08-14-editor-server-console.md94행에 이미 "남아 있는 문제"로 적혀 있었다 - 하트비트를 넣은 이유: 이슈 #10 (좀비 유저)
Server/WSGameServer/Common/Global.cs·MikaNetwork.Server/MikaServer.cs·Assets/Scripts_Client/Managers/PingManager.cs·WindowManager.cs