묻지 않는다 — 폴링이 낭비인 것보다 더 나쁜 이유

5 분
목표MCP 클라이언트 강좌 마지막 편. 구독하고, 알림을 받고, 그때 읽는다. 폴링이 자원을 쓰는 건 부차적이다 — 진짜 문제는 두 번 묻는 사이에 값이 두 번 바뀌면 가운데를 영영 못 본다는 것.

4편까지 최신 값을 알려면 물어야 했다. 1초마다 묻거나, 사람이 새로고침하거나.

구독한다

client.onResourceUpdated((uri) {
  // The notification says only that it changed. Read to learn what to.
  seen.add(uri);
  stdout.writeln('notified: $uri');
});

await client.subscribeResource('desk://waiting');

그리고 값이 바뀌게 만들어 본다.

await client.callTool('desk.admit', {'count': 1});
subscribed to desk://waiting
notified: desk://waiting
after the notification, reading gives {waiting: 2}
notifications received: 1

알림은 uri 만 나른다

핸들러가 받는 건 uri 하나다. 몇 명이 됐는지는 안 온다. 서버 6편에서 값을 안 싣는 이유를 다뤘고, 받는 쪽에서는 그게 읽어야 한다는 뜻이 된다.

final now = await client.readResource('desk://waiting');

한 번 더 왕복하는 게 손해 같지만, 이게 항상 옳다. 알림이 몇 개 오든 순서가 어떻든, 읽은 시점의 값이 최신이기 때문이다.

폴링의 진짜 문제

폴링을 안 쓰는 이유로 대개 자원을 든다. 1초마다 서버를 두드리면 낭비다.

그건 부차적이다. 진짜 문제는 두 번 묻는 사이를 못 본다는 것이다.

09:00:00  폴링 → waiting 3
09:00:00.4  두 명 입장 → waiting 1
09:00:00.7  한 명 도착   → waiting 2
09:00:01  폴링 → waiting 2

화면은 3에서 2로 갔다. 1이었던 순간을 못 봤다. 최종값만 필요하면 상관없지만, "0이 되면 알림" 같은 규칙이 있으면 그 규칙은 안 발동한다.

간격을 줄여도 사라지지 않는다. 좁아질 뿐이다. 그리고 좁힐수록 낭비가 는다.

끝나면 푼다

await client.unsubscribeResource('desk://waiting');

화면이 닫히면 구독을 푼다. 안 풀면 서버가 없는 상대에게 계속 보낸다.

실보드 편의 ESP32 가 이걸 화면 정의 안에 넣어 뒀다 — onReady 에 구독, onDestroy 에 해제. 생명주기가 화면에 적혀 있으면 클라이언트가 잊을 수 없다.

검증 — 폴링이면 실패

grep -q 'notified: desk://waiting' captures/s5.txt || die "no notification arrived"
grep -q 'notifications received: 1' captures/s5.txt || die "wrong notification count"
grep -q 'reading gives {waiting: 2}' captures/s5.txt || die "the read after the notification was stale"

# Polling would show up as a timer; the claim is that there is none.
grep -qE 'Timer|periodic' bin/step5.dart && die "it is polling, not subscribing"

Timer 나 periodic 이 코드에 있으면 실패한다. 결과만 보면 폴링과 구독이 똑같아 보이기 때문이다 — 둘 다 화면에 2가 뜬다.

알림 개수를 세는 것도 같은 이유다. 0이면 안 온 것이고, 여러 개면 중복 구독이다.

다섯 편이 끝났다

step1  connected to Course 1.0.0
step2  tools: desk.admit
       resources: ui://desk, desk://waiting
step3  admit 1 -> {waiting: 2}
       admit 99 -> isError=true "only 2 waiting"
step4  ui://desk — 546 B, 6 lines, type "page", title "Desk"
step5  notifications received: 1 · after the notification, reading gives {waiting: 2}

5 steps · each checked against its own claim · driven against the course server

모의 서버가 없다. 다섯 단계 전부 서버 강좌에서 만든 그 서버를 실제로 띄워서 붙는다. 그래서 두 트랙이 어긋나면 여기서 걸린다 — 실제로 3편에서 한 번 걸렸다.

직접 돌려보기

cd content/sample/course-client
dart pub get
bash verify.sh            # 다섯 단계 전부
dart run bin/step5.dart

가져갈 것

  1. onResourceUpdated + subscribeResource — 핸들러를 먼저 걸고 구독한다
  2. 알림 뒤에 읽는다 — 알림은 uri 만 나른다
  3. 폴링 금지 검사 — 결과가 같아 보이므로 코드를 본다

다음 트랙

서버와 클라이언트가 섰다. 다음은 화면 자체 — JSON 한 덩이가 어떻게 화면이 되는지.

샘플 실행하기

makemind-academy/course_client/
git clone https://github.com/makemind-academy/course_client
cd course_client
dart pub get
(cd course-server && dart pub get)
dart run bin/step5.dart
GitHub에서 열기 →