45. 유휴 타임아웃 판정과 활동 시각 갱신 (C#)
난이도 하해설 — 유휴 타임아웃 판정과 활동 시각 갱신 (C#)
난이도: 하
요약
LastActivity 가 수신(OnReceive) 경로에서만 갱신되고 송신 경로에서는 갱신되지
않는다. 서버가 대형 자산을 오래 보내는 동안 클라이언트는 수신만 하므로 세션의
LastActivity 가 정체되고, IdleReaper 가 이 정상 연결을 유휴로 오판해 전송 도중
끊어 버린다(다운로드 중 강제 절단).
문제점
-
(A) 활동 시각 갱신의 비대칭:
OnReceive에서만LastActivity를 갱신한다. "활동"을 수신으로만 정의했기 때문에 서버→클라 단방향 전송 구간은 활동으로 집계되지 않는다.- 재현: 30초 넘게 걸리는 맵/스냅샷 전송 중 클라가 아무것도 안 보내면
SendLargeAsset로 활발히 보내는 중에도now - LastActivity > 30s가 되어 끊긴다. - 근본 원인: 연결의 생존/활성 지표를 수신 활동에만 의존.
- 재현: 30초 넘게 걸리는 맵/스냅샷 전송 중 클라가 아무것도 안 보내면
-
(B) 판정 기준의 단순함:
Sweep은 오직LastActivity만 본다. 진행 중인 아웃바운드 전송이나 애플리케이션 하트비트 같은 다른 생존 신호를 고려하지 않아 (A)의 정체를 그대로 "유휴"로 확정한다.
수정안
활동 시각을 송·수신 양쪽에서 갱신하거나, "유휴"의 정의를 애플리케이션 하트비트 기준으로 바꾼다. 진행 중 전송을 고려하면 더 견고하다.
public void OnReceive(byte[] data) { Touch(); /* ... */ }
public void SendLargeAsset(byte[] chunk)
{
// ... 소켓 send ...
Touch(); // (A) 송신도 활동으로 집계
}
private void Touch() => LastActivity = DateTime.UtcNow;
// (B) 판정: 진행 중 전송이 있으면 유휴로 보지 않는다.
public void Sweep(IEnumerable<Session> sessions)
{
var now = DateTime.UtcNow;
foreach (var s in sessions)
if (!s.HasPendingSend && now - s.LastActivity > IdleTimeout)
Disconnect(s);
}
더 견고한 대안: 앱 레벨 하트비트(ping/pong) 를 도입해 방향과 무관하게 "상대가 살아 있는가"를 판정하고, idle 타임아웃은 하트비트 미수신으로만 트리거한다.
더 나은 설계
- 하트비트 기반 liveness: 데이터 트래픽과 분리된 주기 ping 으로 죽은 연결을 감지하면 대형 전송/무통신 구간에 영향받지 않는다. TCP keepalive 는 보조로.
- 분리된 두 타임아웃: "무통신 유휴"와 "느린 전송/백프레셔"를 다른 정책으로 관리.
- 전송 진행 관측: 아웃바운드 큐 진척을 활동 지표에 포함.
면접 포인트
- "활동"의 정의를 수신에만 두면 서버 주도 전송 중 오절단이 난다 — 양방향으로 정의.
- liveness(살아 있는가)와 idle(방치되었는가)는 다른 개념. 하트비트로 분리.
해설 — 유휴 타임아웃 판정과 활동 시각 갱신 (C++)
난이도: 하
요약
lastActivity 가 수신(onReceive) 경로에서만 갱신되고 송신에서는 갱신되지 않아,
서버가 대형 자산을 오래 전송하는 동안 클라가 수신만 하면 lastActivity 가 정체되고
IdleReaper 가 전송 중인 정상 연결을 유휴로 오판해 끊는다.
문제점
- (A) 활동 시각 갱신의 비대칭: "활동"을 수신으로만 정의해 서버→클라 단방향 전송
구간이 집계되지 않는다. 30초 넘는 다운로드 중 클라가 무통신이면
sendLargeAsset으로 활발히 보내는 중에도 유휴 판정으로 절단된다. 근본 원인은 생존/활성 지표를 수신에만 의존한 것. - (B) 판정 기준의 단순함:
sweep이lastActivity만 보고 진행 중 아웃바운드 전송이나 앱 하트비트를 고려하지 않아 (A)의 정체를 그대로 유휴로 확정한다.
수정안
struct Session {
int id = 0;
Clock::time_point lastActivity = Clock::now();
bool hasPendingSend = false;
void touch() { lastActivity = Clock::now(); }
void onReceive(const unsigned char*, size_t) { touch(); /* ... */ }
void sendLargeAsset(const unsigned char*, size_t) {
// ... 소켓 send ...
touch(); // (A) 송신도 활동으로 집계
}
};
void IdleReaper::sweep(std::vector<Session*>& sessions) {
auto now = Clock::now();
for (auto* s : sessions)
if (!s->hasPendingSend && now - s->lastActivity > kIdleTimeout) // (B)
disconnect(s);
}
더 견고하게는 앱 레벨 하트비트(ping/pong)로 방향과 무관하게 상대 생존을 판정하고,
idle 타임아웃은 하트비트 미수신으로만 트리거한다. steady_clock 사용은 적절(월클럭
역행 영향 없음).
더 나은 설계
- 하트비트 기반 liveness + TCP keepalive 보조.
- 유휴/느린전송 타임아웃 분리, 아웃바운드 진척을 활동 지표에 포함.
면접 포인트
- "활동"을 수신에만 두면 서버 주도 전송 중 오절단. 양방향으로 정의.
- liveness 와 idle 은 다른 개념 — 하트비트로 분리하는 설계.
steady_clockvssystem_clock: 타임아웃엔 단조 시계.