Free Seat에 장애 주입하기: 로컬 우선 에이전트를 위한 오프라인 폴백 계약
요약
본 글은 로컬 우선(local-first) 에이전트 시스템에서 네트워크 장애 발생 시의 안정적인 폴백 계약 설계 방안을 제시합니다. 단순 재시도 루프를 넘어, 실패 유형별로 적절한 대응 전략과 사용자에게 응답 출처(provenance)를 명확히 알려주는 '계약' 구축이 중요함을 강조합니다.
핵심 포인트
- 단순 재시도보다 폴백 계약이 더 많은 실패 시나리오에 대비한다.
- 실패 유형을 분류하고, 각 상황별로 적절한 로컬/원격 대응 전략을 정의해야 한다.
- 응답 출처(provenance)를 명확히 밝혀 사용자 신뢰도를 유지하는 것이 핵심이다.
사건 보고서는 세 줄로 되어 있었습니다. 리팩터 턴(refactor turn)이 원격 좌석(remote seat)을 대상으로 실행되었고, 사무실 Wi-Fi가 11초 동안 끊겼으며, 그 턴은 아무것도 반환하지 않았습니다. 로컬 모델은 여전히 로드되어 있었고, 레포지토리도 디스크에 남아 있었으며, 사용자는 여전히 스피너를 기다리고 있었습니다. 코드베이스의 어떤 부분도 다음에 무엇이 되어야 하는지를 정의한 적이 없었기 때문에, 답은 스택 트레이스와 손실된 턴(lost turn)이었습니다.
그 간극은 네트워킹 버그가 아니며, 더 열심히 재시도한다고 해서 메워지지 않습니다. 그것은 동일한 요청을 처리할 수 있는 두 좌석 사이의 누락된 계약입니다. 아래 패턴은 단일 팀의 사고라기보다는 일반적인 로컬 우선 에이전트 설정들의 복합체이지만, 실패 형태가 너무 자주 반복되어 기록해 둘 가치가 있습니다.
폴백 계약이 재시도 루프보다 나은 이유
재시도 루프는 하나의 질문에 답합니다. 얼마나 여러 번 시도할 것인가. 반면, 폴백 계약은 네 가지 질문에 답합니다. 어떤 실패가 재시도할 가치가 있는지, 어떤 것은 턴을 로컬 좌석으로 넘겨야 하는지, 각 경우 사용자가 얼마나 기다려야 하는지, 그리고 인터페이스가 사용자에게 실제로 어느 좌석이 응답했는지 알려주는 방법입니다. 마지막 질문은 대부분의 로컬 우선 프로젝트들이 건너뛰는 부분이며, 신뢰를 가장 빠르게 떨어뜨리는 부분입니다.
출처(provenance)가 보이지 않을 때, 저하된 로컬 답변은 건강한 원격 답변과 동일하게 보입니다. 그러면 사용자는 작은 모델의 약한 출력을 네트워크 탓이 아니라 제품 자체의 문제로 돌립니다. 출처를 명시하는 계약은 눈에 보이지 않는 저하를 설명 가능한 이벤트로 바꾸어 주는데, 이는 로컬 경로를 원격 경로와 일치시키는 것보다 훨씬 저렴한 해결책입니다.
단계 1: 거주지를 선택하기 전에 실패 분류하기
유용한 설계 단위는 '온라인 대 오프라인'이 아니라, 서로 다른 올바른 응답을 가진 작은 실패 클래스 집합입니다. 표에 있는 예산(budgets)은 측정된 값이 아니라 논의를 위한 예시적인 시작점이며, 각 팀은 자체 지연 시간 데이터로부터 이를 설정해야 합니다.
| Failure class | Observable signal | Wrong response | Contract response |
|---|---|---|---|
| Hard partition | Connect error in under a second | Retry three times, then fail the turn | Local seat takes the turn immediately, output labeled local |
| ... | |||
| The 핵심 통찰은 타임아웃이 취소(cancellation)가 아니라는 것입니다. 마감 시간 이후에 포기된 요청이라도 원격 좌석을 점유하고 해당 좌석이 가진 예산(budget)을 계속 소모할 수 있습니다. 이것이 바로 계약(contract)이 첫 번째 시도 전에 트어닝(turn)에 비가역성 키(idempotency key)를 찍어야 하는 이유입니다. 그래야 나중에 로컬 답변이 두 개의 상충되는 출력(divergent outputs)을 병합하는 대신 중복된 원격 결과를 억제할 수 있습니다. |
2단계: 예산을 데이터로 인코딩하기
스프린트 기간 동안 산문(prose) 속에 존재하는 예산은 코드에서 멀리 벗어나기 쉽습니다. 따라서 이를 동결된 dataclass에 담아 테스트가 프로덕션 기본값(production defaults)을 건드리지 않고도 엄격한 예산을 구성할 수 있게 하고, 값들이 한 곳에서 검토 가능하도록 만듭니다.
# fallback_contract.py
from __future__ import annotations
...
남은 예산(remaining-budget) 산술 계산이 보이는 것보다 더 중요합니다. 원격 시도가 이미 2초를 소모했다면, 로컬 좌석에 신선한 1.5초를 전달하는 것은 사용자 인내심 한계를 조용히 위반하는 것입니다. 나머지 값으로 제한(Clamping to the remainder)하면 전체 트어닝을 공지된 범위 내에 유지할 수 있습니다.
3단계: 실패를 기다리는 대신 주입하기
실제 분할(partition)을 기다리는 것은 테스트 전략이 아닙니다. 몇 가지 가짜 전송 계층(fake transports)만으로도 밀리초 단위로 모든 실패 클래스를 재현할 수 있으며, 네트워크 접근 없이 CI에서 실행됩니다.
# fake_seats.py
import time
...
각 가짜는 실제 전송 계층과 동일한 형태를 반환하므로 프로덕션 호출 지점(production call site)은 변경되지 않습니다. 이 제약 조건은 강제할 가치가 있습니다. 만약 가짜가 run_turn에서 특별한 분기(special branch)가 필요하다는 순간, 해당 하네스(harness)는 배포될 코드를 테스트하는 것을 멈춘 것입니다.
4단계: 출력뿐만 아니라 출처에 대해 단언하기
가장 가치 있는 단언은 사용자에게 무엇이 전달되는지에 관한 것입니다. 조용히 폴백(fallback)하고 아무것도 보고하지 않는 턴(turn)은, 반환하는 텍스트가 완벽하게 합리적일지라도 실패한 테스트입니다.
# test_fallback_contract.py
import pytest
from fallback_contract import Budget, Turn, run_turn
...
주입된 지연 시간(latency)이 숨겨지지 않고 보이는 상태로 실행 시간을 보고하는 방식으로 스위트(suite)를 실행하세요:
python -m pytest test_fallback_contract.py -q --durations=5
느린 좌석 케이스가 런타임(runtime)을 지배하며, 이것 자체가 진단입니다. 만약 그 항목이 지속 시간 보고서에서 사라진다면, 더미(fake)는 더 이상 잠자고 있지 않으며 타임아웃 경로가 실행되지 않고 있다는 의미입니다.
무료 서버가 실제로 이기는 곳과 그렇지 않은 곳
로컬 우선(Local-first)은 도덕적 입장이 아니며, 세 가지 트레이드오프(tradeoffs)가 이를 결정합니다. 지연 시간(Latency)이 가장 중요합니다: 작은 로컬 모델은 예측 가능한 첫 토큰까지의 시간이 있지만 긴 생성에서는 크게 뒤처질 수 있는 반면, 원격 좌석은 가변적인 네트워크 오버헤드를 가지지만 일단 시작하면 훨씬 강력한 처리량(throughput)을 갖습니다. 비밀 정보(Secrets)가 두 번째입니다: 자격 증명(credentials)과 개인 소스 코드는 장치에 남아 있어야 하며, 축소된 페이로드만 네트워크를 가로질러 전송되어야 하므로 원격 경로는 로컬 마스킹 단계가 강제하는 어떤 경계도 상속받습니다. 오프라인 기능(Offline capability)은 세 번째이며, 이것만이 로컬 좌석이 무조건적으로 이기는 유일한 경우입니다.
이것은 MonkeyCode의 무료 모델 액세스와 무료 서버 옵션이 워크플로우에 단순히 부착되는 것이 아니라 관련성을 갖는 좁은 지점입니다. 무료 원격 좌석은 계약 테스트를 가짜(fakes)에 대해서만 실행하는 대신 실제 전송 동작(real transport behavior)에 대해 저렴하게 실행할 수 있게 해주는데, 이는 위 아치(harness)가 제공할 수 없는 정확한 관찰 결과입니다. 사용 가능성(Availability), 모델 선택, 그리고 모든 사용 제한은 읽는 시점에 제품 문서를 통해 확인해야 합니다. 왜냐하면 이들은 변경되기 때문입니다. 공개: 본 기사는 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다.
한계점 및 건너뛰어야 할 사람들
이 하네스는 성능(performance)을 증명하는 것이 아니라 폴백 동작(fallback behavior)을 증명합니다. 예산 표의 모든 숫자는 테스트를 읽기 쉽게 만들기 위해 선택된 자리 표시자일 뿐이며, 여기서 지연 시간(latency)이나 비용 비교는 측정치로 인용되어서는 안 됩니다. 동기식 예제 역시 진행 중인 원격 작업(in-flight remote work)을 취소할 수 없으므로, 비멱등성 키(idempotency key)를 통한 중복 억제는 선택 사항이 아니라 필수적입니다.
로컬 추론 경로가 없는 팀은 이 패턴을 채택해서는 안 됩니다. 그들은 좌석이 하나뿐이며 올바른 설계는 가시적인 재시도 상태(visible retry state)를 가진 대기열(queue)이어야 합니다. 엄격한 데이터 거주지 규칙(data-residency rule)을 따르는 팀은 계약 자체를 완전히 뒤집고 원격 좌석을 설계상 사용 불가능한 것으로 취급해야 하며, 이는 로컬 경로가 유일한 경로가 되게 하고 출처 레이블(provenance label)을 불필요하게 만듭니다. 그 외의 모든 팀은 두 개의 가짜 전송 계층(fake transports), 하나의 매개변수화된 테스트(parametrized test), 그리고 출처 필드(provenance field)로 시작한 다음, 실제 사고가 발생함에 따라 실패 테이블을 확장할 수 있습니다.
만약 자체적인 실패 클래스(failure classes)를 매핑하고 있다면, 같은 하네스를 실제로 사용 가능한 좌석(free seat)에 오후 동안 실행하는 것은 추측된 예산(guessed budgets)을 관찰된 예산으로 대체하는 저렴한 방법입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기