부르고 받는다 — 거절은 예외가 아니다
5 분2편에서 무엇이 있는지 알았다. 부른다.
결과는 값이 아니다
final ok = await client.callTool('desk.admit', {'count': 1});돌아오는 건 숫자가 아니라 콘텐츠 목록이다. 텍스트일 수도, 이미지일 수도, 여러 개일 수도 있다. 그래서 꺼내는 자리를 한 곳에 둔다.
Map<String, dynamic> decode(CallToolResult r) {
final first = r.content.first;
if (first is! TextContent) {
throw StateError('expected text content, got ${first.runtimeType}');
}
return jsonDecode(first.text) as Map<String, dynamic>;
}is! TextContent 검사를 빼고 바로 캐스트하면, 서버가 언젠가 이미지를 섞어 보낼 때 엉뚱한 자리에서 터진다. 여기서 걸러야 스택이 읽힌다.
admit 1 -> {waiting: 2}거절은 throw 로 오지 않는다
서버 3편에서 만든 거절을 불러 본다.
final refused = await client.callTool('desk.admit', {'count': 99});
final text = (refused.content.first as TextContent).text;
stdout.writeln('admit 99 -> isError=${refused.isError} "$text"');admit 99 -> isError=true "only 2 waiting"예외가 안 났다. await 가 정상 완료되고, 결과에 isError: true 가 붙어 있고, 이유가 텍스트로 들어 있다.
이걸 모르면 이렇게 쓴다.
try {
await client.callTool('desk.admit', {'count': 99});
// 여기까지 왔으니 성공
} catch (e) {
// 거절은 여기로 온다 ... 고 생각하지만 안 온다
}거절이 catch 로 안 가므로 성공으로 처리된다. 화면에 "99명 입장" 이 뜨고, 서버는 아무도 안 들여보냈다.
왜 예외가 아닌가
예외는 "이 호출이 성립하지 않았다" 는 뜻이다. 연결이 끊겼거나, 그런 도구가 없거나.
거절은 호출이 성립했고 서버가 판단한 결과다. 도구가 정상 실행됐고, 답이 "안 된다" 인 것이다. 그리고 그 답에는 이유가 붙어 있다 — 그 이유를 쓰려면 결과로 받아야 한다.
only 2 waiting 을 읽으면 2 이하로 다시 보낼 수 있다. 예외로 잡으면 그 문장이 스택트레이스 안에 묻힌다.
검증
run step3 > captures/s3.txt || die "step3: failed (a refusal should not throw)"
grep -q 'admit 1 -> {waiting: 2}' captures/s3.txt || die "the call did not go through"
grep -q 'admit 99 -> isError=true' captures/s3.txt || die "the refusal was not surfaced"
grep -q 'only 2 waiting' captures/s3.txt || die "the reason did not survive"첫 줄이 중요하다. 거절이 예외로 오면 프로그램이 죽고 종료 코드가 0이 아니다 — || die 가 거기서 잡는다.
세 번째 검사(only 2 waiting)가 실제로 결함을 하나 잡았다. 서버 4~6단계가 3단계에서 나눈 두 가지 거절을 하나로 뭉개 놓고 있었다. 서버만 돌렸으면 3단계 검사는 통과하고 이후 단계는 그 메시지를 안 보므로 영영 몰랐을 것이다. 두 샘플을 맞물려 돌려서 드러났다.
직접 돌려보기
dart run bin/step3.dart
bash verify.sh가져갈 것
decode한 곳 — 콘텐츠 종류 검사를 여기서isError를 읽는다 — 거절은 결과다- 거절이 예외로 안 오는지 검사 — 종료 코드로 확인된다
다음 편
화면을 받는다. 클라이언트에 화면이 한 줄도 없는 상태로.
샘플 실행하기
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/step3.dart