단일 AI 워크플로가 출력을 생성하기 전에 다섯 개에서 여섯 개의 외부 서비스를 거칠 수 있다. LLM API, 벡터 데이터베이스, CRM 조회, 웹 스크레이퍼, 서드파티 데이터 보강 제공자. 각각은 고유한 가동률 기록을 가지며, 어느 것도 100%가 아니다.
하나가 다운되면, 저하 계획이 없는 시스템은 완전히 멈춘다. 10분짜리 장애가 전체 파이프라인 중단으로 이어진다. 큐가 밀린다. 다운스트림 단계가 타임아웃된다. 누군가 티켓을 올린다.
해결책은 벤더에게 더 나은 SLA를 요구하는 것이 아니다. 처음부터 장애를 전제로 설계하는 것이다.
의존성 맵 작성 연습
Graceful degradation을 구현하려면 먼저 무엇에 의존하는지 파악해야 한다.
워크플로가 수행하는 모든 외부 호출을 나열한다. 빠짐없이 포함한다:
- LLM 추론 엔드포인트 (OpenAI, Anthropic, 자체 호스팅)
- 벡터 스토어 및 검색 레이어
- CRM 읽기 및 쓰기
- 데이터 보강 API (LinkedIn 데이터, 기업 정보 제공자)
- 웹 페치 또는 스크레이핑 서비스
- 다른 팀이 소유한 내부 마이크로서비스
- 인증 및 토큰 검증 엔드포인트
각 의존성에 현실적인 월간 가동률 수치를 부여한다. 벤더의 마케팅 SLA가 아닌, 공개된 상태 페이지 이력을 사용한다. 99.9% 가동률을 광고하지만 최근 90일간 세 번의 장애가 있었던 서비스는 실제로 99.9% 서비스가 아니다.
그런 다음 계산한다. 워크플로가 다섯 개의 의존성을 필요로 하고 각각의 가동률이 99.5%라면, 전체 체인의 결합 가용성은 약 97.5%다. 이는 한 달에 약 18시간의 잠재적 다운타임을 의미한다 — 부주의 때문이 아니라, 단순한 산술의 결과다.
이 연습은 보통 두 가지 반응을 낳는다. 첫째, 외부 호출이 얼마나 많은지에 대한 놀라움. 둘째, 어떤 의존성이 가장 높은 위험인지에 대한 명확성.
세 가지 저하 모드
모든 장애가 동일한 대응을 필요로 하지는 않는다. 세 가지 모드가 있다. 올바른 모드 선택은 워크플로에서 해당 의존성의 역할에 따라 달라진다.
완전 폴백 (캐시된 결과)
의존성이 느리게 변하는 데이터를 제공하고, 약간 오래된 답변이 답변 없음보다 나은 경우에 사용한다.
예시: 회사 규모와 업종을 반환하는 기업 정보 보강 호출. 보강 API가 다운되면, 타임스탬프와 함께 마지막으로 캐시된 결과를 제공한다. 48시간 전의 직원 수는 처리를 계속하기에 거의 항상 충분하다.
요구사항: 정의된 TTL을 가진 캐시 레이어, 팀이 합의한 오래됨 임계값, 그리고 결과가 캐시에서 왔음을 나타내는 출력 내 플래그.
부분 폴백 (플래그가 있는 축소된 출력)
의존성이 출력 품질에 기여하지만 출력 자체에 필수적이지 않은 경우에 사용한다.
예시: CRM 이력과 실시간 웹 데이터를 모두 사용해 잠재 고객 요약을 생성하는 워크플로. 웹 페치가 실패하면, CRM 데이터만으로 요약을 생성한다. 다운스트림 소비자가 낮은 신뢰도로 처리해야 함을 알 수 있도록 출력에 enrichment_incomplete: true를 표시한다.
이렇게 하면 파이프라인이 계속 돌아간다. 또한 명확한 감사 추적이 생성된다. 의존성이 복구되면 플래그가 달린 레코드를 재처리할 수 있다.
Graceful Rejection (빠르고 명확하게 실패)
의존성이 핵심적이고 부분적인 결과가 잘못된 결과보다 낫지 않은 경우에 사용한다.
예시: 메시지 발송 전에 반드시 실행해야 하는 컴플라이언스 검사. 컴플라이언스 API에 접근할 수 없으면, 메시지를 보내지 않는다. 추측하지 않는다. 즉시 작업을 거부하고, 이유를 로깅하고, 백오프 스케줄이 있는 재시도 큐로 라우팅한다.
핵심 단어는 빠르게다. 30초 후에 타임아웃되는 graceful rejection은 graceful하지 않다. 공격적인 타임아웃을 설정하고 — 대부분의 API 호출에 2~5초 — 일찍 거부한다. 이렇게 하면 나머지 파이프라인이 연쇄 지연으로부터 보호된다.
분리 패턴
가장 흔한 구현 실수는 폴백 로직을 메인 워크플로 함수 안에 직접 삽입하는 것이다. 오류를 확인하고, 캐시를 시도하고, 다시 확인하고, 결국 무언가를 반환하는 긴 조건 블록처럼 보인다. 두 명의 엔지니어가 손을 대고 나면, 아무도 실제로 무엇을 하는지 확신하지 못한다.
분리 패턴: 각 의존성을 원시 호출이 아닌 래핑된 클라이언트로 취급한다.
래퍼는 세 가지를 담당한다:
- 라이브 호출
- 해당 특정 의존성에 대한 폴백 동작
- 로깅 및 플래깅 계약
메인 워크플로 로직은 래퍼를 호출하고 결과 또는 구조화된 실패 객체를 받는다. 결과가 라이브 데이터에서 왔는지 캐시에서 왔는지 알지 못한다. 재시도 로직을 포함하지 않는다. 받은 것을 처리할 뿐이다.
이렇게 하면 저하 코드가 격리되고 테스트 가능해진다. 각 래퍼의 폴백을 독립적으로 단위 테스트할 수 있다. 워크플로 로직을 건드리지 않고 캐싱 전략을 교체할 수 있다. 새로운 의존성이 추가될 때, 패턴은 이미 확립되어 있다.
부수적인 이점: 장애가 발생하면, 래퍼의 로그가 어떤 의존성이 언제 실패했는지, 어떤 폴백 모드가 활성화됐는지 정확히 알려준다. 프로덕션 장애 디버깅이 몇 시간이 아닌 몇 분 만에 끝난다.
실제로 어떻게 보이는가
시간당 200개의 잠재 고객 레코드를 처리하는 워크플로는 의존성 장애를 가끔이 아니라 정기적으로 겪는다. 현실적인 가동률 수치에서, 적당히 복잡한 파이프라인은 하루에 최소 한 번의 부분 장애 이벤트를 예상해야 한다.
저하 모드로 구축된 시스템은 사람의 개입 없이 이러한 이벤트를 처리한다. 파이프라인이 계속 돌아간다. 플래그가 달린 레코드는 의존성이 복구되면 재처리된다. 운영자는 지원 티켓이 아닌 대시보드 메트릭을 본다.
저하 모드가 없는 시스템은 멈추고, 알림을 보내고, 누군가가 재시작해주기를 기다린다.
차이는 엔지니어링 영웅주의가 아니다. 의존성 맵, 세 가지 정의된 모드, 그리고 일관되게 적용된 분리 패턴이다.
AI 파이프라인을 구축하거나 감사하면서 특정 워크플로의 의존성 맵을 함께 검토하고 싶다면, 대화 시작하기 →