원격 제어 프로그램의 관리 서버에서 PC 수에 따라 반복되는 작업을 정리했다. pong 기록은 5초 단위로 묶고, 업데이트 큐 점검은 서버 한 곳으로 모았다.

PC 수에 따라 늘어나던 반복 작업
관리 서버는 현장 PC의 에이전트와 WebSocket으로 통신한다. 연결 상태는 pong과 HTTP 하트비트 등으로 확인한다.
상태를 확인할 때마다 서버에서 처리할 작업도 늘어난다. 현장 PC가 늘어날수록 같은 점검이 반복되는 구조를 살펴봤다.
이 글에서 다룰 반복 작업은 크게 세 가지다. 서버와 관리 화면의 처리 흐름을 나눠 정리했다.
- pong을 받을 때마다 수행하던 SQLite 상태 기록
- 하트비트마다 반복하던 전체 업데이트 큐 점검
- 관리 화면에서 PC 목록 전체의 상태를 조회하던 방식
이 변경은 구현과 검증, 배포를 마친 뒤 한 커밋으로 기록했다. 뒤늦게 여러 변경을 한 번에 정리하는 셈이 됐다.
pong 기록을 5초 단위로 묶었다
pong이 올 때마다 바로 쓰던 SQLite 기록을 모으도록 바꿨다. PC별로 모은 상태는 5초 단위 트랜잭션으로 기록한다.
아래 코드는 실제 구현의 흐름을 줄인 단순화한 예시다. 그대로 실행할 코드는 아니며, 타이머 정리와 종료 처리는 생략했다.
// 단순화한 예시: 실제 구현을 줄여 옮긴 것
const pending = new Map(); // pcId -> 서버가 pong을 받은 시각
const MAX_PENDING = 1000;
let retryScheduled = false;
// 시각 역행과 비활성 PC 걸러내기는 UPDATE 조건에서 처리
const update = db.prepare(`UPDATE clients SET last_seen_at = ?
WHERE id = ? AND enabled = 1
AND (last_seen_at IS NULL OR last_seen_at < ?)`);
const commit = db.transaction((batch) => {
for (const [id, at] of batch) update.run(at, id, at);
});
function flush() {
const batch = new Map(pending);
try {
commit(batch); // 묶음 전체를 트랜잭션 한 번으로
pending.clear();
retryScheduled = false;
} catch (e) {
retryScheduled = true; // 큐는 그대로 두고 타이머로만 재시도
scheduleFlush();
}
}
function onPong(pcId) {
if (!pending.has(pcId) && pending.size >= MAX_PENDING) {
if (!retryScheduled) flush(); // 가득 차면 먼저 커밋
if (pending.size >= MAX_PENDING) return; // 실패 중이면 이번 기록은 건너뜀
}
pending.set(pcId, new Date().toISOString());
scheduleFlush(); // 최대 5초 뒤 flush 예약
}
PC 한 대에서 5초 안에 여러 번 pong이 와도, 이 방식에서는 최신 시각 하나만 남는다. 기록 횟수가 pong 수가 아니라 묶음 수를 따라가게 된다.

지연 기록과 쓰기 실패도 다뤘다
기록을 모아 쓰면 순서가 어긋나는 경우도 고려해야 했다. 늦게 반영된 시각이 최신 상태를 덮지 않도록 했다.
등록 해제나 비활성화 이후 남은 기록도 처리 대상이었다. 커밋 메시지에는 다음과 같은 방어 장치가 적혀 있다.
- PC별 상태 시각이 역행하는 기록은 무시했다.
- 등록 해제·비활성화된 PC에는 지연 기록을 적용하지 않았다.
- 대기 큐 상한을 1,000개로 제한했다.
- DB 쓰기에 실패하면 큐를 보존하고 타이머 하나에서만 재시도했다.
온라인 여부와 마지막 연결 시각은 종전 기준을 유지했다. 화면 표시는 하트비트와 pong을 확인한 시각에 따른다.
업데이트 큐 점검은 서버에서 한 번에
이전에는 에이전트의 하트비트마다 전체 업데이트 큐를 확인했다. PC가 늘면 같은 큐를 점검하는 횟수도 늘어나는 구조였다.
이 점검을 서버 단일 sweep으로 옮겼다. 큐는 서버가 자기 주기로 한 번 훑고, 하트비트는 상태 보고만 하도록 역할을 나눴다.

sweep 주기는 PC 수와 관계없이 5초다. 수동 업데이트 요청이나 결과 수신에 따른 즉시 처리는 그대로 뒀다.
자동 업데이트 롤아웃 중단 기록에서는 대기열의 중단 처리를 다뤘다. 이번에는 업데이트 큐를 점검하는 주기를 분리했다.
에이전트의 첫 접속·재접속·하트비트에는 jitter를 적용했다. 미완료 HTTP 하트비트가 있으면 다음 요청을 겹쳐 보내지 않게 했다.
소켓 연결은 15초 안에 끝나도록 제한했다. 프로그램 종료 시에는 진행 중인 연결 시도도 취소하도록 했다.
관리 화면은 필요한 PC만 조회했다
관리 화면에서는 PC 목록과 접속 상태를 따로 다루도록 했다. PC 카탈로그와 압축 접속 상태(presence)를 분리한 것이다.
변경 여부는 revision으로 확인하고 기존 API도 유지했다. 예전 화면과의 호환성을 남기면서 조회 범위를 조정했다.
PC 탭과 파일 탐색기 목록은 50대 단위로 보여 준다. 상태 조회도 화면에 보이거나 선택한 PC 위주로 줄였다.

커밋 메시지에는 서버 테스트 403개와 에이전트 보조 테스트 235개 통과가 기록됐다. 1,000대 규모의 PC 목록 회귀 시험도 포함됐다.
이 시험은 운영 데이터가 아닌 별도 테스트 환경에서 1,000개 목록의 페이지 조회와 1,000개 pong의 한 번 커밋을 확인한 것이다. 실제 1,000대 동시 접속이나 운영 성능을 보장하는 부하 시험은 아니다.
수정 전후 부하 수치도 남기지 않았다. 그래서 DB 쓰기나 응답 시간이 얼마나 줄었는지는 단정하지 않는다.
이번 작업에서 남긴 점
이번에는 PC 수만큼 되풀이되던 작업의 위치와 주기를 바꿨다. 기록을 묶는 만큼 상태 순서와 실패 후 복구도 함께 고려했다.
- 반복 점검은 실행 위치와 호출 횟수를 함께 살펴봐야 했다.
- 쓰기 지연에는 상태 역행과 삭제 후 기록을 막는 처리가 필요했다.
- 큰 변경을 커밋 없이 모으면 나중에 정리할 내용도 늘었다.
실제 규모의 부하는 별도 실측이 필요한 부분으로 남겨 뒀다. 트랜잭션 동작은 SQLite 공식 문서를 참고했다.
'프로젝트 > 원격 제어 프로그램' 카테고리의 다른 글
| 배포 버전 현재 상태 문서를 세우고, 과거 릴리스·업데이트 기록은 보존했다 (0) | 2026.10.19 |
|---|---|
| 자기 PC 원격 접속을 막고 최근 연결 기록 기준을 다시 잡았다 (0) | 2026.10.18 |
| 비동기 GPU 인코딩에서 peer가 바뀌던 문제, 프레임에 소유자를 심다 (0) | 2026.10.18 |
| 멀티 모니터 원격 뷰어, 창 하나를 전제로 짠 세션·수명 구조 풀기 (0) | 2026.10.17 |
| 자동 업데이트 롤아웃, 중단 버튼만으론 멈추지 않았다 (0) | 2026.10.16 |