Big Project · AI's EYE 36
AI 시스템의 투명한 Fallback 설계
AI 설명을 만들 모델이 실패하면 Agent 기능 전체를 오류로 끝내는 것이 정직해 보인다. 규칙 기반 답변을 내면 AI를 가장하는 것 아닌지 걱정된다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: AI 실패, 그런데 시스템은 멈추지 않는다?
1장: AI 실패, 그런데 시스템은 멈추지 않는다?
솔라는 모니터 한구석에 띄워 둔 기술 노트에서 눈을 떼지 못했다. 짧은 문장이었지만, 어딘가 석연치 않은 구석이 자꾸만 마음을 불편하게 했다.
Gemini가 실패하면 fallback_started를 표시하고 같은 세 도구 순서를 서버 코드가 완료한다.
AI 모델이 실패하면, 서버 코드가 그 일을 대신 마친다. 솔라는 이 문장을 몇 번이고 다시 읽었다. 읽을수록 명쾌해지기는커녕, 정직하지 않다는 느낌만 강해졌다. 마치 무대 위 마술사가 트릭에 실패하자, 조수가 재빨리 달려 나와 아무렇지 않은 척 마술을 끝내버리는 장면을 본 기분이었다.
“언니, 이거 좀 이상하지 않아?”
결국 솔라는 노트북을 들고 거실에 있는 루나에게 다가갔다. 루나는 솔라가 가리키는 화면의 한 줄을 잠시 들여다보았다.
“AI가 실패했는데, 왜 서버 코드가 자기가 AI인 척 마무리를 하는 거야? 이건 그냥 사용자를 속이는 거잖아. AI가 못 했으면 깔끔하게 ‘실패했습니다’라고 오류를 보여주는 게 정직한 거 아니야?”
솔라의 목소리에는 단호함이 섞여 있었다. AI 시스템의 신뢰는 투명성에서 온다고 생각했다. 실패를 감추고 어떻게든 성공한 것처럼 포장하는 것은 신뢰를 깎아 먹는 행위였다.
루나는 솔라의 말을 가만히 듣고는, 잠시 생각에 잠겼다. 그리고는 대답 대신 솔라의 노트북을 자신 쪽으로 조금 더 끌어당겼다.
“이 시스템이 실패했을 때와 성공했을 때의 로그를 한번 그려볼까? 솔라 네가 생각하는 ‘정직한 실패’부터 시작해보자.”
루나는 빈 텍스트 편집기를 열어 빠르게 무언가를 입력했다.
// 시나리오 1: AI 실패 시 시스템 전체 중단
1. Run_ID: a1b2-c3d4
2. Status: RUNNING
3. Action: Gemini_API_Call
- Input: "지난주 운영 데이터 분석해 줘."
4. Event: Gemini_API_Response
- Status: FAILED (Error: 429 Quota Exceeded)
5. Status: FAILED
6. Final_Output: null
“이런 모습이겠지. AI 호출이 실패하면, 전체 실행 상태도 ‘실패’가 되고 모든 게 중단되는 거.”
“응, 바로 그거야. 깔끔하잖아. 뭐가 잘못됐는지 명확하고.”
솔라가 만족스럽다는 듯 고개를 끄덕였다. 루나는 그 아래에 이어서 두 번째 시나리오를 그려나갔다. 솔라가 봤던 바로 그 문장이 묘사하는 상황이었다.
// 시나리오 2: AI 실패 시 Fallback 실행
1. Run_ID: e5f6-g7h8
2. Status: RUNNING
3. Action: Gemini_API_Call
- Input: "지난주 운영 데이터 분석해 줘."
4. Event: Gemini_API_Response
- Status: FAILED (Error: 429 Quota Exceeded)
5. Event: fallback_started
6. Action: Server_Code_Execution
- Tool_Call: get_summary_data() -> { "summary": "..." }
- Tool_Call: get_trend_data() -> { "trend": "..." }
- Tool_Call: get_staffing_comparison() -> { "comparison": "..." }
7. Status: COMPLETED_WITH_FALLBACK
8. Final_Output: { "summary": "...", "trend": "...", "comparison": "..." }
솔라는 두 번째 로그를 뚫어지라 쳐다보았다. 첫 번째 로그와 달리, FAILED에서 끝나지 않고 fallback_started라는 낯선 이벤트를 거쳐 끝까지 실행되고 있었다. 서버 코드가 get_summary_data 같은 도구들을 직접 호출해서 결과를 만들어냈다. 솔라의 처음 생각과 정확히 일치하는, ‘AI를 가장하는’ 과정이었다.
“봐, 역시 그렇잖아. AI가 할 일을 서버가 대신 하고 있어. 이건…”
솔라가 말을 끝맺기도 전에, 루나가 물었다.
“AI가 원래 하기로 한 일이 정확히 뭐였지?”
“응? 그야… 운영 데이터를 분석해서…”
말을 이어가던 솔라는 순간 멈칫했다. 이 시스템에서 AI가 하는 일은 단순히 정해진 도구 세 개를 순서대로 호출하는 것만이 아니었다. 그 도구 호출로 얻은 결과, 즉 summary, trend, comparison 데이터를 종합해서 인간이 이해할 수 있는 자연어 분석 보고서를 ‘생성’하는 역할까지 포함되어 있었다. 하지만 두 번째 로그 어디에도 그 ‘생성’ 과정은 보이지 않았다.
“아…”
솔라의 입에서 작은 탄식이 흘러나왔다.
“그러네. AI는 원래 이 데이터들로 ‘왜 이런 결과가 나왔는지’ 설명하는 문장을 만들어야 하는데, 이 로그에는 그게 없네. 그냥 정해진 계산만 실행하고, 그 결과 데이터만 툭 던져주고 끝났구나.”
AI가 만들어냈어야 할 유창한 분석 문장은 어디에도 없었다. 그저 결정론적인 코드 실행의 결과물만 남아있을 뿐이었다. 시스템은 AI인 척 ‘설명’을 지어내지 않았다. 할 수 있는 일, 즉 정해진 도구를 호출하고 데이터를 가져오는 일까지만 하고 멈춘 것이다.
솔라는 고개를 들어 루나를 보았다. 자신의 첫 번째 판단이 너무 성급했다는 것을 깨달았다. ‘시스템 전체 중단’만이 유일한 정직함은 아니었다.
“그렇다면… AI가 하는 창의적인 일, 즉 자연어 설명을 만드는 부분만 실패로 처리하는 게 더 정확한 접근일 수도 있겠다. AI 설명을 만들 모델이 실패하면, 그냥 그 Agent 기능 전체를 오류로 끝내는 게 정직해 보여. 데이터를 보여주는 것까지는 괜찮아도, AI가 만든 것처럼 보이는 결과물은 아니니까.”
솔라의 생각이 한 단계 나아갔다. 이제 그녀에게 ‘정직함’은 ‘전체 중단’이 아니라, ‘AI의 역할과 아닌 것의 분리’와 가까워졌다. AI의 실패를 시스템 전체의 실패와 동일시할 필요는 없었다.
하지만 여전히 풀리지 않은 매듭이 남아있었다. 솔라의 새로운 생각은 그럴듯했지만, 그녀가 처음 봤던 시스템은 거기서 한 걸음 더 나아갔다. Agent 기능을 오류로 끝내지 않고, 규칙 기반의 결과라도 보여주며 서비스를 계속 이어갔다.
“근데 언니, 이 시스템은 결국 에이전트 기능을 오류로 끝내지 않았잖아. 규칙 기반으로 결과를 보여줬지. 그럼 그건 AI인 척하는 거 아닐까? 어떤 조건이 있어야 그게 ‘가장’이 아니라고 할 수 있는 걸까?“
2장: Fallback의 정직성: 출처를 명확히 밝히는 방법
2장: Fallback의 정직성: 출처를 명확히 밝히는 방법
솔라의 질문이 채 끝나기도 전에, 루나는 이미 노트북 화면에 새로운 비교 창을 띄워두고 있었다. 이전 장에서 함께 살펴봤던 로그 기록이 아니었다. 대신, 시스템이 사용자에게 최종적으로 전달하는 결과물처럼 보이는 세 개의 간결한 JSON 객체가 나란히 놓여 있었다. 루나는 이전 대화에서 솔라가 제기했던 문제를 이 세 가지 예시 안에 녹여낸 듯했다.
각 객체는 summary, trend, comparison이라는 동일한 데이터 구조를 가졌지만, 마지막 필드인 source의 값이 서로 달랐다. 첫 번째는 gemini_tool_agent, 두 번째는 rule_fallback, 그리고 세 번째는 presentation_fallback 이었다. 솔라는 이 화면 구성을 보고, 자신의 마지막 질문—규칙 기반 fallback이 AI를 가장하지 않을 조건—에 대한 대답이 이 source 필드에 담겨 있음을 직감했다.
“언니, 이게 언니의 대답이야?” 솔라가 화면을 가리키며 물었다.
“규칙 기반 답변을 내면 AI를 가장하는 것 아닌지 걱정된다고 했지. 이 세 가지 경우를 보면서, 언제 가장이 되고 언제 정직한 정보 전달이 되는지 한번 생각해보자.”
솔라의 시선이 두 번째 객체에 꽂혔다. source: "rule_fallback". 바로 솔라가 기술 노트에서 봤던, AI 실패 시 작동하는 시스템의 결과물이었다.
“솔직히 잘 모르겠어. 그냥 source에 rule_fallback이라고 이름표 하나 붙인다고 해서 문제가 해결되는 건 아니잖아. 어쨌든 AI가 해야 할 분석 결과를 시스템이 대신 내놓는 건 똑같으니까. 이건 ‘이 사과는 가짜입니다’라고 작게 써 붙인 플라스틱 사과 같은 거 아니야? 속이려는 의도가 여전히 느껴진단 말이지.”
솔라의 비유는 날카로웠다. 행위의 본질은 바뀌지 않은 채, 면피용 꼬리표만 달아둔 것처럼 보일 수 있었다.
루나는 솔라의 비유를 잠시 곱씹더니, 고개를 저었다.
“플라스틱 사과 비유는 조금 달라. 그건 진짜 사과와 똑같이 보이게 만들려는 노력이 들어갔을 때의 이야기지. 이 source 필드는 그런 위장의 목적이 아니야. 오히려 시스템이 사용자에게 거는 ‘약속’의 종류를 명확히 하는 신호에 가까워.”
루나는 첫 번째 객체를 손가락으로 가리켰다. source: "gemini_tool_agent".
“이 source는 시스템이 뭐라고 약속하는 걸까?”
“음… ‘이 결과는 Gemini AI가 데이터를 분석하고 그 의미를 해석해서 만든 지능적인 결과물입니다’라는 뜻이겠지.”
“맞아. AI의 추론, 요약, 창의적인 연결 능력을 기대하게 만들지. 그렇다면 source: "rule_fallback"은?”
루나의 손가락이 두 번째 객체로 옮겨갔다. 솔라는 잠시 머뭇거렸다. 플라스틱 사과라는 처음 생각에서 벗어나, ‘약속’이라는 관점으로 다시 생각해야 했다.
”…’AI가 작동하지 않아서, 미리 정해진 규칙에 따라 계산된 결과만 보여드립니다. 여기에는 AI의 해석이나 추론이 없습니다’라는 약속인가?”
순간 솔라의 머릿속에서 무언가 ‘탁’ 하고 맞아 들어가는 느낌이 들었다. 이건 속임수가 아니었다. 이것은 거래 조건의 변경을 솔직하게 고지하는 행위였다.
“아, 알겠다. 이건 ‘이 사과는 가짜입니다’가 아니라, 레스토랑 메뉴에 ‘오늘의 셰프 특선 파스타’가 품절돼서 대신 ‘기본 토마토 파스타만 가능합니다’라고 알려주는 거랑 같구나.”
셰프의 창의성이 담긴 특별 요리를 기대했던 손님에게, 셰프의 창의성은 없지만 레시피대로 정확하게 만든 기본 요리를 제공하는 것. 손님은 자신이 무엇을 받게 될지 명확히 알고 선택할 수 있다. rule_fallback이라는 출처 표시는 바로 그 ‘기본 토마토 파스타’ 안내판이었던 것이다. 시스템은 AI인 척 흉내 내는 것을 포기하고, 대신 자신이 할 수 있는 결정론적이고 신뢰할 수 있는 계산 결과를 정직하게 내어주고 있었다.
솔라는 source 필드의 의미를 완전히 새롭게 이해했다. 이것은 단순한 꼬리표가 아니라, 결과물의 성격을 규정하고 사용자의 기대를 관리하는 핵심적인 ‘출처 구분자’ 역할을 하고 있었다.
스스로의 깨달음에 만족한 솔라는 고개를 끄덕였다. 하지만 이내 새로운 질문이 고개를 들었다. 그녀의 시선은 어느새 루나가 만들어 둔 세 번째 예시, source: "presentation_fallback"에 머물러 있었다.
“알겠어. 출처를 명확히 밝히는 건 정말 중요하네. ‘셰프 특선’이 아니라 ‘기본 파스타’라고 알려주면 되니까. 그런데… 그 ‘기본 토마토 파스타’의 레시피는 누가 정하는 거지? 규칙 기반으로 결과를 보여준다고 해도, 그 내용이 AI가 만든 것처럼 그럴듯하게 보이면 결국 또 과장하는 거 아닐까? 어디까지가 ‘과장 없는’ 정보라고 할 수 있는 걸까?“
3장: 과장 없는 정보: Fallback 내용의 한계와 원칙
3장: 과장 없는 정보: Fallback 내용의 한계와 원칙
솔라의 시선은 이전 장에서 루나가 띄워 둔 세 번째 예시, source: "presentation_fallback"에 꽂혀 있었다. 출처를 밝히는 것이 정직함의 첫걸음이라는 건 알았지만, 그 내용물 자체의 정직함은 또 다른 문제였다. ‘기본 토마토 파스타’라고 메뉴판에 써두었더라도, 주방장이 레시피에 없는 재료를 임의로 넣어 맛을 흉내 냈다면 그건 정직한 요리일까?
솔라가 미처 질문을 정리하기도 전에, 루나가 키보드를 두드렸다. 화면에 있던 세 개의 JSON 객체 중 두 번째, rule_fallback 예시의 내용이 바뀌었다. 특정 데이터 필드 하나가 비어 있는 상황을 가정한 새로운 시나리오였다.
// 시나리오 A: 데이터 부재 시, presentation_fallback
{
"summary": { ... },
"trend": { ... },
"comparison": {
"staffing_diff": null
},
"source": "presentation_fallback"
}
루나는 이어서 그 옆에 또 다른 시나리오를 만들었다. 똑같이 comparison 데이터가 없는 상황이었지만, 결과는 사뭇 달랐다.
// 시나리오 B: 데이터 부재 시, 추정치를 생성하는 rule_fallback
{
"summary": { ... },
"trend": { ... },
"comparison": {
"staffing_diff_estimated": "-5% (추정치)"
},
"source": "rule_fallback"
}
“어떤 ‘기본 파스타’가 더 마음에 들어?”
루나가 물었다. 시나리오 A는 데이터가 없으면 없다고(null) 정직하게 보여주고, 그 출처를 presentation_fallback으로 명시했다. 이는 ‘데이터가 없으니 화면에 보여줄 것도 없다’는, 표현 계층에서의 대응이다. 반면 시나리오 B는 rule_fallback이라는 이름 아래, ‘추정치’라는 딱지를 붙여 그럴듯한 숫자를 만들어냈다.
솔라는 망설임 없이 시나리오 B를 가리켰다.
“나라면 B가 더 나을 것 같아. A는 그냥 아무것도 안 보여주는 거잖아. 텅 빈 화면보다는 추정치라도 있는 게 사용자 입장에선 훨씬 유용하지. 어쨌든 AI가 아니라고 밝혔고, 추정치라고도 써뒀으니까 정직한 거 아니야?”
솔라의 생각은 합리적이었다. 정보를 조금이라도 더 주려는 시도는 긍정적으로 보였다. 규칙 기반 fallback도 결국은 사용자가 원하는 정보를 AI처럼 ‘만들어내는’ 것이라면, 이 정도는 허용될 수 있는 범위 아닐까?
“그럴듯하네. 그럼 B 시나리오에서 ‘-5% (추정치)‘라는 값은 어디서 왔을까?”
루나의 질문에 솔라는 잠시 생각에 잠겼다. ‘추정치’라는 말은 무언가 계산이 있었다는 뜻이다.
“음… 아마도 ‘이런 데이터가 없으면, 지난주 평균값을 대신 사용하라’ 같은 규칙이 서버 코드에 들어있지 않을까?”
“좋은 추측이야. 그 규칙은 누가 만들었지?”
“그야… 개발자가 만들었겠지.”
대답을 하고 난 순간, 솔라는 무언가 잘못되었음을 깨달았다. 개발자가 심어놓은 ‘지난주 평균값’이라는 규칙. 그것은 AI가 상황을 보고 추론한 결과가 아니다. 그렇다고 시스템이 원본 데이터를 가지고 결정론적으로 계산한 값도 아니었다. 그것은 ‘AI라면 아마 이렇게 추론했을 것이다’ 라고 사람이 짐작해서 흉내 낸, 미리 짜인 각본이었다.
마치 셰프가 토마토소스가 떨어지자, ‘손님들은 아마 비슷한 붉은 소스를 원할 거야’ 라고 짐작하고 케첩을 부어 내놓은 ‘기본 토마토 파스타’와 같았다. 이름은 같지만, 그 내용은 약속된 레시피를 벗어났다.
솔라의 얼굴에 당혹감이 스쳤다.
“아… 이건 그냥 AI 문장을 흉내 내는 거구나. ‘지난주 데이터와 비교했을 때 약 5% 감소했을 것으로 보입니다’ 같은 AI의 그럴듯한 문장을, 그냥 개발자가 코드로 만들어 둔 거네. source를 rule_fallback이라고 붙여도, 내용물 자체가 과장되어 있었구나.”
솔라가 보았던 기술 노트의 한 구절이 머릿속을 스쳐 지나갔다.
fallback도 서버가 계산한 staffing target과 capacity 결과만 사용하며 모델 문장을 흉내 내 새 수치를 만들지 않는다.
이제야 그 문장의 진짜 의미가 보였다. 이 원칙은 ‘과장 없는’ fallback의 내용물에 대한 명확한 경계선을 긋고 있었다. Fallback은 AI가 실패했을 때 비상 발전기처럼 동작해야 하지만, 발전기가 내보내는 전기는 반드시 시스템이 직접 생산한, 신뢰할 수 있는 것이어야 했다. 다른 곳에서 빌려오거나, 그럴듯하게 흉내 낸 ‘유사 전기’를 만들어서는 안 된다는 뜻이었다.
시스템이 가진 데이터로 결정론적 계산이 가능하면, 그 결과만 보여준다. 계산이 불가능하면, null이나 ‘데이터 없음’을 반환한다. 이것이 바로 ‘과장 없음’의 원칙이었다. 시나리오 B의 ‘-5% (추정치)‘는 이 경계선을 넘었다. 그것은 존재하는 데이터를 보여주는 것이 아니라, 존재하지 않는 정보를 만들어낸 행위였다.
솔라는 루나가 만든 두 시나리오를 다시 보았다. 처음에는 더 유용해 보였던 시나리오 B가 이제는 위험하고 부정직하게 느껴졌다.
“알겠다. Fallback의 정직성은 두 단계네. 첫째, AI가 만들지 않았다고 출처를 밝히는 것. 둘째, 그 내용물도 AI를 흉내 내지 않고, 오직 서버가 결정론적으로 계산할 수 있는 ‘진짜’ 데이터에만 기반해야 한다는 것. 이 둘이 모두 지켜져야 ‘과장 없는’ 서비스라고 할 수 있구나.”
솔라는 모델의 실패를 시스템 전체의 실패로 여기던 처음의 생각에서 한참을 더 나아왔다. 이제 그녀는 모델 의존적인 기능과 결정론적인 계산을 분리하고, 실패 시에도 투명한 출처와 과장 없는 내용으로 서비스를 유지하는 구체적인 방법을 이해하게 되었다.
문득, 솔라는 이 모든 장치가 단지 ‘정직함’만을 위한 것인지 궁금해졌다. 이렇게까지 엄격한 원칙을 세워가며 fallback을 구현하는 것이 시스템 전체에는 어떤 이점을 주는 걸까? 단순히 사용자에게 오류 화면을 보여주지 않는 것 이상의 의미가 있을 것 같았다.
“언니, 이렇게 정교하게 fallback을 설계하는 게… 시스템 전체의 안정성이나 신뢰도에 구체적으로 어떤 도움을 주는 거야? 그냥 ‘착한 시스템’이 되려는 것 이상의 목표가 있을 것 같은데.”
4장: AI 실패 속의 굳건함: 서비스 연속성의 비밀
4장: AI 실패 속의 굳건함: 서비스 연속성의 비밀
이전 대화가 끝나고, 솔라의 머릿속은 ‘정직한 fallback’을 설계하기 위한 두 가지 원칙으로 깔끔하게 정리되었다. 출처를 명확히 밝히는 것, 그리고 내용물을 과장하지 않는 것. 하지만 그녀의 마지막 질문—“이런 설계가 시스템 전체에 어떤 도움을 주는가?”—은 아직 답을 찾지 못한 채 공중에 떠 있었다.
솔라가 골똘히 생각에 잠겨 있는 동안, 루나는 조용히 노트북 화면을 전환했다. 이번에는 로그나 JSON 객체가 아니었다. 화면에는 두 개의 단순한 흐름도가 나란히 그려져 있었다. 하나는 ‘시스템 A’, 다른 하나는 ‘시스템 B’라는 제목이 붙어 있었다. 두 흐름도 모두 Run_Start에서 시작해 Gemini_API_Call 단계를 거쳤지만, 그 이후의 경로가 확연히 달랐다.
시스템 A의 흐름도는 Gemini_API_Call에서 ‘실패(429 오류)‘를 만나자마자 붉은색 RUN_FAILED 상태로 곤두박질치며 끝나 있었다. 더 이상의 다음 단계는 없었다.
시스템 B의 흐름도는 똑같이 Gemini_API_Call에서 ‘실패(429 오류)‘를 만났지만, 거기서 멈추지 않았다. 화살표는 Fallback_Started라는 새로운 단계를 가리켰고, 이어서 Server_Code_Execution을 거쳐 초록색 RUN_COMPLETED 상태로 차분하게 마무리되었다.
루나는 아무 말 없이 두 흐름도를 가리켰다. 솔라는 이 두 그림이 자신의 마지막 질문에 대한 대답임을 직감했다.
“언니, 이건… 시스템이 AI 오류를 처리하는 두 가지 방식을 비교하는 거구나.”
“응. 솔라 네가 보기에 어떤 시스템이 더 안정적이고 신뢰할 수 있어 보여?”
솔라는 잠시 망설였다. 표면적으로는 당연히 B였다. 오류로 서버리기는 것보다야 어떻게든 작업을 마치는 편이 나으니까. 하지만 솔라의 마음 한구석에는 여전히 찜찜함이 남아있었다. 그녀가 처음 가졌던 생각의 잔재였다.
“솔직히 말하면… B가 그냥 임시방편처럼 느껴져. AI가 실패했다는 근본적인 문제는 해결되지 않았잖아. 단지 오류 화면을 보여주는 대신, 다른 결과를 보여주며 문제를 덮는 것뿐 아니야? 이게 시스템의 본질적인 신뢰도를 높여준다고 말할 수 있을까?”
솔라의 의심은 타당했다. 그것은 마치 엔진에 문제가 생긴 자동차를 정비소로 보내는 대신, 일단 갓길에 세워두고 ‘운행 종료’ 스티커를 붙이는 것처럼 보일 수도 있었다.
루나는 솔라의 말을 듣고 고개를 끄덕였다. 그리고는 질문의 초점을 살짝 비틀었다.
“사용자가 아니라, 이 시스템의 실행 상태를 지켜보는 다른 프로그램의 입장에서 생각해보면 어떨까? 예를 들어, 매시간 이 분석 작업을 실행하고 결과가 나오면 다음 작업을 자동으로 시작하는 스케줄러가 있다고 해보자.”
다른 프로그램의 입장. 솔라는 미처 생각해보지 못한 관점이었다. 그녀는 스케줄러가 되었다고 상상하며 두 흐름도를 다시 바라봤다.
시스템 A의 경우, 스케줄러는 RUN_FAILED라는 예상치 못한 상태를 받게 된다. 이건 ‘경고’다. 스케줄러는 이 작업이 왜 실패했는지 알 수 없다. 쿼터 문제인지, 네트워크 오류인지, 아니면 코드 버그인지. 자동화된 절차는 여기서 멈춘다. 경고 알람이 울리고, 한밤중에 담당 개발자가 호출될지도 모른다. 시스템 전체의 흐름에 예측 불가능한 ‘구멍’이 생긴 것이다.
반면 시스템 B의 경우, 스케줄러는 RUN_COMPLETED라는 상태를 받는다. 비록 그 결과물에 source: rule_fallback이라는 꼬리표가 붙어 있겠지만, 스케줄러의 관점에서 이것은 ‘성공적인 완료’다. 작업은 약속된 형식의 결과물을 내놓고 정상적으로 끝났다. 스케줄러는 이 결과를 바탕으로 다음 작업을 순조롭게 진행할 수 있다. 시스템 전체의 흐름은 끊기지 않고 이어진다. 서비스의 연속성이 지켜진 것이다.
“아…”
솔라의 입에서 나지막한 탄성이 터져 나왔다. 그녀는 이제야 이 설계의 진짜 목적을 깨달았다. 이것은 단순히 오류를 ‘덮는’ 행위가 아니었다. 이것은 AI 모델의 실패라는 예측 불가능한 이벤트를, 시스템 전체의 입장에서는 예측 가능하고 안정적인 ‘완료’ 상태로 바꾸는 고도의 장애 관리 전략이었다.
기술 노트에서 봤던 한 문장이 퍼즐 조각처럼 완벽하게 맞춰졌다.
테스트는 예외와 불완전 응답 모두 마지막 run_completed까지 도달하는지 확인한다.
이 테스트의 목표는 AI가 성공하는지를 확인하는 것이 아니었다. AI가 어떤 식으로 실패하든, 시스템이 항상 안정적인 run_completed 상태로 작업을 마칠 수 있는지를 보장하는 것이었다. AI의 실패를 시스템의 실패와 분리하는 것. 이것이 바로 시스템을 굳건하게 만드는 핵심이었다.
“이건… AI의 실패를 다루는 일종의 ‘장애 회복 패턴’이구나. 모델의 변덕스러움에 시스템 전체가 휘둘리지 않도록, 결정론적인 출구 전략을 미리 만들어두는 거네. 모델 의존적인 기능과 시스템의 안정적인 작동을 분리하는 게 핵심이었어.”
솔라는 자신이 처음 가졌던 생각—AI가 실패하면 시스템 전체가 멈춰야 정직하다—이 얼마나 단편적이었는지 깨달았다. 진정한 신뢰성은 ‘실패 없음’이 아니라, ‘실패에도 불구하고 무너지지 않음’에서 나왔다. 이 fallback 설계는 단순히 ‘착한 시스템’이 되기 위한 것이 아니라, 예측 불가능한 AI와 함께 살아가기 위한 필수적인 생존 전략이었다.
생각이 여기까지 미치자, 솔라의 시야가 넓어졌다. 그녀가 지금까지 본 것은 하나의 분석 에이전트 사례에 불과했다. 이 강력한 ‘장애 회복 패턴’을 다른 종류의 AI 시스템에도 적용할 수 있을까?
“언니, 이 원칙은 정말 대단하다. 그런데 이 분석 에이전트가 아니라, 예를 들면 실시간으로 상품을 추천하는 시스템이나 사용자와 대화하는 챗봇 같은 곳에서도 이런 식의 fallback을 설계할 수 있을까? 맥락이 완전히 다른데, 어떻게 적용해야 할지 감이 잘 안 와.”
5장: 투명한 Fallback: 다른 AI 시스템에 적용하기
5장: 투명한 Fallback: 다른 AI 시스템에 적용하기
솔라의 손가락이 노트북 트랙패드 위에서 초조하게 맴돌았다. 화면에는 그녀가 방금 전까지 쇼핑하던 사이트의 상품 페이지가 열려 있었다. 페이지를 새로고침 할 때마다 ‘당신을 위한 추천 상품’ 섹션은 로딩 아이콘만 뱅글뱅글 돌다가, 엉뚱하게도 그녀가 이미 구매한 상품이나 전혀 관련 없는 카테고리의 물건들을 툭 뱉어냈다. 마치 실패를 감추려는 어설픈 거짓말처럼 보였다.
지난 며칠간 루나와 함께 파고들었던 ‘AI의 정직성’이라는 주제가 현실의 조악한 구현물 위에서 흐릿해지는 기분이었다. 분명 분석 에이전트 사례에서는 모델 의존적인 기능과 결정론적인 계산을 분리하는 명쾌한 원칙이 있었다. 하지만 지금 눈앞의 추천 시스템처럼, 사용자와의 상호작용이 핵심인 시스템에서는 그 경계가 훨씬 모호하게 느껴졌다.
“언니, 이건 어떻게 생각해?”
솔라는 결국 노트북을 들고 루나에게 다가갔다. 루나는 솔라의 질문이 무엇에 관한 것인지 이미 알고 있다는 듯, 자신의 모니터에 띄워 둔 빈 텍스트 편집기를 가리켰다. 거기에는 마치 설계도 초안처럼 보이는 간단한 구조가 적혀 있었다.
// 추천 시스템 (Recommendation System)
- 기능: 사용자에게 개인화된 상품 목록을 제공한다.
- 핵심 컴포넌트: ?
- 모델 의존 기능: ?
- 결정론적 계산/데이터: ?
솔라가 지난 장 마지막에 던졌던 질문—추천 시스템이나 챗봇에도 이 원칙을 적용할 수 있을까—에 대한 루나의 대답 방식이었다. 이론을 설명하는 대신, 직접 설계해볼 판을 깔아준 것이다.
“이 분석 에이전트 사례는 너무 특수해. 운영 데이터를 다루니까 결정론적으로 계산할 부분이 명확히 있었던 거잖아. 하지만 추천 시스템은… 모델이 전부 아니야? 모델이 실패하면 그냥 아무것도 보여주지 않는 게 맞는 거 같은데.”
솔라의 말은 그녀가 맨 처음 가졌던 생각, 즉 ‘AI 실패 = 시스템 중단’으로 회귀한 것처럼 들렸다. 새로운 문제 앞에서 배운 원칙을 적용하는 데 주저함이 느껴졌다. 그녀는 이 사례가 특정 분석 에이전트에만 적용되는 특수한 해결책이라고 생각하고 있었다.
루나는 솔라의 말에 대답하는 대신, 텍스트 편집기에서 핵심 컴포넌트: ? 라고 적힌 줄에 커서를 옮겼다.
“이 추천 시스템의 가장 중요한 구성 요소가 뭘까? AI 모델?”
“당연하지. 사용자의 구매 이력, 클릭 기록 같은 걸 분석해서 ‘이 사람이 다음에 살 만한 물건’을 예측하는 AI 모델이 핵심이지.”
“좋아. 그럼 그 예측이 ‘모델 의존 기능’이겠네.”
루나가 솔라의 대답을 그대로 모델 의존 기능 항목 아래에 타이핑했다.
- 모델 의존 기능: 사용자의 행동 패턴을 기반으로 한 '개인화된 상품 목록' 생성
“자, 이제 어려운 부분이야. 이 시스템에서 모델을 완전히 들어냈다고 상상해 봐. 우리에게 남는, 결정론적으로 확실하게 보여줄 수 있는 데이터는 뭐가 있을까? 아예 없을까?”
솔라는 잠시 생각에 잠겼다. AI의 ‘개인화된 예측’이 없다면… 그냥 빈칸인가? 아니었다. 쇼핑몰 사이트에는 개인화되지 않았지만 분명히 존재하는 정보들이 있었다.
”…아! ‘주간 베스트셀러 목록’이나 ‘신상품 목록’ 같은 게 있네. 그건 개인화된 추천은 아니지만, 시스템에 실제로 존재하는, 조작되지 않은 데이터잖아.”
유레카를 외치듯 솔라의 목소리가 한 톤 높아졌다. 그녀는 루나의 의도를 파악하고 직접 키보드를 넘겨받아 결정론적 계산/데이터 항목을 채워 넣기 시작했다.
- 결정론적 계산/데이터: '주간 베스트셀러', '카테고리별 인기 상품', '신상품 목록' 등, 개인화와 무관하게 집계된 데이터
이제 솔라의 눈앞에는 이전 분석 에이전트 사례와 똑같은 구조가 모습을 드러내고 있었다. 하나는 AI의 추론에 의존하는 가변적이고 창의적인 부분, 다른 하나는 AI 없이도 존재하는 확정적이고 사실적인 부분. 두 영역이 명확히 분리되었다.
“그럼 이제 답이 나오네.”
솔라는 스스로 설계 초안을 완성해나가기 시작했다. 그녀의 손가락이 빠르게 움직였다.
// 추천 시스템 Fallback 설계 스케치
1. **실패 정의**: 개인화 추천 모델이 타임아웃, 5xx 에러, 또는 의미 없는 빈 결과를 반환하는 경우.
2. **Fallback 트리거**: `fallback_started` 이벤트를 기록한다.
3. **Fallback 동작**:
a. '주간 베스트셀러 목록' 데이터를 호출한다.
b. 사용자에게 보여주는 UI 컴포넌트의 제목을 '당신을 위한 추천'에서 '지금 가장 인기 있는 상품'으로 변경한다.
c. 최종 결과물의 `source` 필드를 `reco_model_v3`가 아닌 `fallback_bestseller`로 명시한다.
4. **최종 상태**: 시스템은 페이지 렌더링 실패나 전체 오류 없이, fallback 컴포넌트를 포함하여 정상적으로 `COMPLETED` 상태에 도달한다.
스스로 작성한 설계 스케치를 보며 솔라는 감탄했다. 처음에는 막막하고 특수한 사례라고 생각했던 원칙들이, 문제의 구조를 ‘모델 의존 기능’과 ‘결정론적 계산’으로 분리하자 놀랍도록 일관되게 적용되었다.
UI 제목까지 바꾸는 디테일은 ‘과장하지 않는다’는 원칙을 완벽하게 구현한 것이었다. 개인화 추천이 아니면서 ‘당신을 위한 추천’이라고 말하는 것은 거짓말이기 때문이다. 출처를 fallback_bestseller로 명시하는 것은 시스템 내부와 사용자 모두에게 이 결과의 본질을 투명하게 공개하는 약속이었다.
“결국… 어떤 AI 시스템이든 마찬가지구나.”
솔라가 나지막이 말했다.
“모델 의존적인 기능과 결정론적으로 계속할 수 있는 계산을 분리하고, 실패가 발생하면 그 출처를 정직하게 공개하면서, 과장 없이 서비스를 유지하는 것. 이게 바로 AI 시스템을 위한 회복 설계의 핵심 가이드라인이었어.”
그녀는 더 이상 ‘이 사례는 특수하다’고 생각하지 않았다. 분석 에이전트는 단지 그 원칙을 처음 배우기 위한 훌륭한 교재였을 뿐이다. 이제 솔라는 어떤 AI 에이전트를 만나더라도 이 ‘Agent 회복 설계’라는 렌즈를 통해 시스템의 안정성과 정직성을 어떻게 확보할지 질문하고 답을 찾아나갈 수 있게 되었다. 그녀는 완성된 설계 스케치를 만족스럽게 바라보았다. 더 이상 루나에게 던질 질문은 남아있지 않았다. 스스로 답을 찾는 방법을 터득했기 때문이다.