본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 27. · 4 Views
도구 실행 결과 처리 완벽 가이드
LLM 애플리케이션에서 도구 실행 결과를 안정적으로 처리하는 방법을 다룹니다. 성공과 실패를 구분하고, 재시도 전략을 세우며, 견고한 폴백 메커니즘까지 구현하는 실전 기법을 배웁니다.
목차
1. 성공 실패 핸들링
김개발 씨는 LLM 기반 챗봇 서비스를 개발하고 있습니다. 외부 API를 호출하는 도구를 만들었는데, 어느 날 갑자기 서비스가 먹통이 되었습니다.
로그를 확인해보니 API 응답이 실패했을 때 아무런 처리도 하지 않아서 전체 시스템이 멈춰버린 것이었습니다.
성공/실패 핸들링은 도구 실행 결과를 체계적으로 분류하고 각 상황에 맞게 대응하는 것입니다. 마치 택배 기사님이 배송 성공, 부재중, 주소 오류 등 상황별로 다르게 처리하는 것과 같습니다.
이것을 제대로 구현하면 예상치 못한 오류에도 서비스가 안정적으로 동작합니다.
다음 코드를 살펴봅시다.
// 도구 실행 결과를 담는 타입 정의
interface ToolResult<T> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
retryable: boolean;
};
}
// 도구 실행 함수
async function executeTool<T>(
toolName: string,
params: Record<string, unknown>
): Promise<ToolResult<T>> {
try {
const response = await callExternalAPI(toolName, params);
// 성공 시 데이터와 함께 반환
return { success: true, data: response };
} catch (err) {
// 실패 시 에러 정보를 구조화하여 반환
return {
success: false,
error: {
code: err.code || 'UNKNOWN_ERROR',
message: err.message,
retryable: isRetryableError(err)
}
};
}
}
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 회사에서 LLM을 활용한 고객 상담 챗봇을 개발하는 프로젝트에 투입되었습니다.
챗봇은 날씨 조회, 주문 확인, 배송 추적 등 여러 가지 외부 도구를 호출해야 했습니다. 처음에 김개발 씨는 단순하게 생각했습니다.
"API 호출하고 결과 받아서 보여주면 되지 뭐." 그래서 try-catch도 없이 바로 API를 호출하는 코드를 작성했습니다. 며칠 동안은 아무 문제가 없었습니다.
그런데 어느 날 새벽, 긴급 호출이 왔습니다. 챗봇 서비스 전체가 응답을 멈춘 것이었습니다.
원인은 날씨 API 서버가 잠시 다운되었는데, 이를 처리하는 코드가 없어서 전체 애플리케이션이 크래시된 것이었습니다. 그렇다면 성공/실패 핸들링이란 정확히 무엇일까요?
쉽게 비유하자면, 이것은 마치 병원의 응급실 트리아지 시스템과 같습니다. 환자가 들어오면 의료진은 먼저 상태를 파악합니다.
경증인지, 중증인지, 긴급 수술이 필요한지에 따라 완전히 다른 대응을 합니다. 도구 실행 결과도 마찬가지입니다.
성공했는지, 실패했는지, 실패했다면 어떤 종류의 실패인지를 먼저 분류해야 합니다. 성공/실패 핸들링이 없던 코드는 어떤 모습일까요?
개발자들은 모든 API 호출이 항상 성공한다고 가정하고 코드를 작성했습니다. 하지만 현실은 그렇지 않습니다.
네트워크가 불안정할 수 있고, 서버가 과부하 상태일 수 있으며, 인증 토큰이 만료되었을 수도 있습니다. 이런 상황을 고려하지 않은 코드는 마치 안전벨트 없이 고속도로를 달리는 것과 같습니다.
바로 이런 문제를 해결하기 위해 구조화된 결과 타입을 사용합니다. 위 코드의 ToolResult 타입을 살펴보면, success 필드로 성공 여부를 명확히 구분합니다.
성공하면 data에 결과가 담기고, 실패하면 error 객체에 상세 정보가 담깁니다. 특히 retryable 필드는 이 에러가 재시도하면 해결될 수 있는 종류인지를 알려줍니다.
코드를 한 줄씩 살펴보겠습니다. 먼저 ToolResult 인터페이스는 제네릭 타입 T를 받아서 다양한 도구의 결과를 담을 수 있게 설계되었습니다.
success가 true일 때는 data가 있고, false일 때는 error가 있는 구조입니다. executeTool 함수는 try-catch로 감싸서 어떤 예외가 발생하더라도 프로그램이 크래시되지 않도록 합니다.
성공 시에는 단순히 데이터를 반환하고, 실패 시에는 에러를 분석하여 구조화된 형태로 반환합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 이커머스 플랫폼의 주문 조회 기능을 구현한다고 가정해봅시다. 주문 API 호출이 실패했을 때, 단순히 "오류가 발생했습니다"라고 표시하는 것과 "현재 주문 시스템 점검 중입니다.
5분 후 다시 시도해주세요"라고 안내하는 것은 사용자 경험에서 큰 차이가 납니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 모든 에러를 동일하게 처리하는 것입니다. "잘못된 요청"과 "서버 과부하"는 완전히 다른 종류의 에러입니다.
전자는 재시도해도 소용없고, 후자는 잠시 후 재시도하면 성공할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
선배 박시니어 씨의 코드 리뷰를 받은 후, 김개발 씨는 모든 도구 호출에 구조화된 결과 처리를 적용했습니다. 그 후로 서버가 다운되는 일은 다시 발생하지 않았습니다.
실전 팁
💡 - 성공과 실패를 boolean 하나로 구분하되, 실패의 종류는 에러 코드로 세분화하세요
- 모든 외부 호출은 반드시 try-catch로 감싸고, 예외를 구조화된 에러로 변환하세요
2. 재시도 전략
김개발 씨는 성공/실패 핸들링을 구현한 후 자신감이 생겼습니다. 그런데 새로운 문제가 발생했습니다.
간헐적으로 네트워크 타임아웃이 발생하는데, 사용자가 직접 다시 시도해야 하는 불편함이 있었습니다. 선배 박시니어 씨가 말했습니다.
"자동으로 재시도하는 로직을 넣어보는 게 어때?"
재시도 전략은 일시적인 오류가 발생했을 때 자동으로 다시 시도하여 성공 확률을 높이는 기법입니다. 마치 전화 통화 중 끊어졌을 때 다시 거는 것처럼 자연스러운 개념입니다.
단, 무작정 재시도하면 오히려 문제를 악화시킬 수 있으므로 전략적으로 접근해야 합니다.
다음 코드를 살펴봅시다.
// 지수 백오프를 적용한 재시도 함수
async function retryWithBackoff<T>(
fn: () => Promise<ToolResult<T>>,
options: { maxRetries: number; baseDelay: number }
): Promise<ToolResult<T>> {
const { maxRetries, baseDelay } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const result = await fn();
// 성공하거나 재시도 불가능한 에러면 즉시 반환
if (result.success || !result.error?.retryable) {
return result;
}
// 마지막 시도가 아니면 대기 후 재시도
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`재시도 ${attempt + 1}/${maxRetries}, ${delay}ms 대기`);
await sleep(delay);
}
}
return { success: false, error: { code: 'MAX_RETRIES', message: '최대 재시도 횟수 초과', retryable: false } };
}
김개발 씨는 재시도 로직을 구현하기로 마음먹었습니다. 처음에는 단순하게 생각했습니다.
"실패하면 바로 다시 호출하면 되지 않을까?" 그래서 while 루프로 성공할 때까지 계속 재시도하는 코드를 작성했습니다. 테스트 환경에서는 잘 동작하는 것 같았습니다.
그런데 프로덕션에 배포하자마자 문제가 터졌습니다. 외부 API 서버가 잠시 과부하 상태가 되었는데, 김개발 씨의 코드가 쉬지 않고 요청을 보내는 바람에 상황이 더 악화되었습니다.
API 제공 업체에서 연락이 왔습니다. "귀사에서 초당 수백 건의 요청이 들어오고 있습니다.
이게 의도된 건가요?" 그렇다면 재시도 전략이란 정확히 무엇이고, 어떻게 해야 할까요? 쉽게 비유하자면, 재시도 전략은 마치 바쁜 식당에 전화하는 것과 같습니다.
통화 중이라고 바로 다시 걸면 계속 통화 중일 가능성이 높습니다. 잠시 기다렸다가 다시 걸면 성공 확률이 올라갑니다.
그리고 몇 번 시도해도 안 되면 포기하고 다른 식당을 알아봐야 합니다. **지수 백오프(Exponential Backoff)**라는 개념이 바로 이것입니다.
첫 번째 재시도 전에는 1초를 기다립니다. 두 번째 재시도 전에는 2초, 세 번째는 4초, 네 번째는 8초...
이런 식으로 대기 시간을 지수적으로 늘립니다. 이렇게 하면 서버가 회복할 시간을 주면서도 결국에는 재시도를 할 수 있습니다.
위 코드를 살펴보겠습니다. retryWithBackoff 함수는 세 가지 핵심 요소를 담고 있습니다.
첫째, 최대 재시도 횟수(maxRetries)를 제한하여 무한 루프를 방지합니다. 둘째, 기본 대기 시간(baseDelay)에 2의 거듭제곱을 곱하여 지수적으로 대기 시간을 늘립니다.
셋째, retryable 플래그를 확인하여 재시도할 가치가 있는 에러만 재시도합니다. for 루프 안에서 먼저 함수를 실행합니다.
성공하면 바로 결과를 반환합니다. 실패했는데 재시도 불가능한 에러라면 역시 바로 반환합니다.
재시도 가능한 에러일 때만 대기 후 다음 시도로 넘어갑니다. 실제 현업에서 재시도 전략은 어디에 활용될까요?
클라우드 서비스를 사용하는 거의 모든 애플리케이션에서 재시도는 필수입니다. AWS, GCP, Azure 모두 공식 SDK에 재시도 로직이 내장되어 있습니다.
특히 분산 시스템에서는 네트워크 지연, 일시적인 서버 오류 등이 빈번하게 발생하므로 재시도 없이는 안정적인 서비스를 만들 수 없습니다. 주의해야 할 점도 있습니다.
**멱등성(Idempotency)**을 고려해야 합니다. 예를 들어 결제 API를 재시도했는데, 첫 번째 요청이 실제로는 성공했다면 어떻게 될까요?
중복 결제가 발생할 수 있습니다. 따라서 재시도가 안전한 작업인지 반드시 확인해야 합니다.
또한 **지터(Jitter)**를 추가하면 더 좋습니다. 여러 클라이언트가 동시에 재시도할 때 같은 시간에 요청이 몰리는 것을 방지하기 위해, 대기 시간에 약간의 무작위 값을 더합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 지수 백오프를 적용한 후, 간헐적인 네트워크 오류는 대부분 자동으로 복구되었습니다.
사용자는 뒤에서 재시도가 일어나는지도 모른 채 자연스럽게 서비스를 이용할 수 있게 되었습니다.
실전 팁
💡 - 재시도 횟수는 보통 3~5회가 적당하며, 너무 많으면 응답 지연이 심해집니다
- POST 요청처럼 부작용이 있는 API는 멱등성 키를 함께 전송하여 중복 실행을 방지하세요
3. 에러 메시지 포맷
김개발 씨가 만든 챗봇이 점점 안정화되어 가고 있었습니다. 그런데 운영팀에서 불만이 들어왔습니다.
"에러가 났을 때 로그를 봐도 뭐가 문제인지 모르겠어요. 그냥 'Error occurred'라고만 나와요." 문제 해결에 필요한 정보가 에러 메시지에 담겨 있지 않았던 것입니다.
에러 메시지 포맷은 오류 정보를 체계적으로 구조화하여 디버깅과 모니터링을 쉽게 만드는 것입니다. 마치 의사가 진단서를 작성할 때 증상, 원인, 처방을 명확히 구분하는 것과 같습니다.
잘 설계된 에러 메시지는 문제 해결 시간을 획기적으로 단축시킵니다.
다음 코드를 살펴봅시다.
// 체계적인 에러 포맷 정의
interface StructuredError {
code: string; // 기계가 읽을 수 있는 에러 코드
message: string; // 사람이 읽을 수 있는 메시지
details?: {
toolName: string; // 어떤 도구에서 발생했는지
input?: unknown; // 입력값 (민감정보 제외)
timestamp: string; // 발생 시각
requestId: string; // 추적용 요청 ID
cause?: string; // 원인이 된 하위 에러
};
suggestion?: string; // 해결 방법 제안
}
// 에러 생성 유틸리티 함수
function createToolError(toolName: string, err: Error, requestId: string): StructuredError {
return {
code: categorizeError(err),
message: getHumanReadableMessage(err),
details: {
toolName,
timestamp: new Date().toISOString(),
requestId,
cause: err.message
},
suggestion: getSuggestion(err)
};
}
김개발 씨는 에러 메시지의 중요성을 깨닫지 못했습니다. "에러가 났으면 났다고만 알면 되지, 뭘 그리 복잡하게 해야 하나?" 그런 생각이었습니다.
그러던 어느 날 새벽 3시, 긴급 알람이 울렸습니다. 서비스 장애가 발생한 것입니다.
김개발 씨는 로그를 열어보았습니다. "Error: Something went wrong." 그게 전부였습니다.
어떤 도구에서 에러가 났는지, 어떤 요청에서 발생했는지, 원인이 무엇인지 전혀 알 수 없었습니다. 두 시간 동안 코드를 뒤지고 나서야 문제를 찾을 수 있었습니다.
만약 에러 메시지에 충분한 정보가 있었다면 5분이면 해결할 수 있었을 문제였습니다. 그렇다면 좋은 에러 메시지 포맷이란 무엇일까요?
쉽게 비유하자면, 에러 메시지는 범죄 수사 보고서와 같아야 합니다. 좋은 보고서에는 언제, 어디서, 무엇이, 왜 발생했는지가 명확히 기록되어 있습니다.
에러 메시지도 마찬가지입니다. 단순히 "에러 발생"이 아니라 구체적인 맥락 정보가 필요합니다.
위 코드의 StructuredError 인터페이스를 살펴보겠습니다. code는 기계가 읽을 수 있는 에러 코드입니다.
"NETWORK_TIMEOUT", "AUTH_EXPIRED", "INVALID_INPUT" 같은 형태로 정의합니다. 모니터링 시스템이나 알림 규칙에서 이 코드를 기준으로 분류할 수 있습니다.
message는 사람이 읽을 수 있는 설명입니다. 개발자뿐 아니라 운영팀도 이해할 수 있는 수준으로 작성합니다.
"네트워크 연결 시간이 초과되었습니다"처럼 명확하게 표현합니다. details 객체는 디버깅에 필요한 상세 정보를 담습니다.
어떤 도구에서 발생했는지(toolName), 언제 발생했는지(timestamp), 추적을 위한 requestId, 그리고 원인이 된 하위 에러(cause)까지 포함합니다. suggestion은 매우 유용한 필드입니다.
"API 키를 확인해주세요", "잠시 후 다시 시도해주세요" 같은 해결 방법을 제시합니다. 새벽에 장애 대응하는 개발자에게 이 한 줄이 얼마나 큰 도움이 되는지 모릅니다.
실제 현업에서는 어떻게 활용할까요? 대형 서비스들은 requestId를 기반으로 한 분산 추적 시스템을 운영합니다.
사용자가 "오류가 났어요"라고 문의하면, 해당 시점의 requestId만 알면 전체 요청 흐름을 추적할 수 있습니다. 어떤 서버에서, 어떤 도구가, 어떤 입력값으로 실패했는지 한눈에 파악할 수 있습니다.
주의해야 할 점도 있습니다. 에러 메시지에 민감한 정보를 노출하면 안 됩니다.
사용자 비밀번호, API 키, 개인정보 등이 로그에 남으면 보안 문제가 됩니다. 입력값을 기록할 때는 반드시 민감 정보를 마스킹해야 합니다.
또한 에러 메시지는 일관된 포맷을 유지해야 합니다. 어떤 에러는 JSON이고 어떤 에러는 평문이면 로그 분석 도구가 제대로 파싱할 수 없습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 구조화된 에러 포맷을 적용한 후, 장애 대응 시간이 평균 2시간에서 15분으로 줄었습니다.
운영팀도 만족하고, 김개발 씨도 새벽에 더 푹 잘 수 있게 되었습니다.
실전 팁
💡 - 에러 코드는 카테고리_세부사항 형태로 계층적으로 설계하세요 (예: NETWORK_TIMEOUT, AUTH_INVALID_TOKEN)
- requestId는 UUID v4를 사용하고, 모든 로그에 일관되게 포함시키세요
4. 실습 견고한 도구 실행기
지금까지 배운 내용을 종합하여 실제로 사용할 수 있는 도구 실행기를 만들어 봅시다. 김개발 씨는 선배에게 배운 모든 것을 하나로 합쳐 "어떤 상황에서도 죽지 않는" 견고한 실행기를 만들기로 했습니다.
견고한 도구 실행기는 성공/실패 핸들링, 재시도 전략, 에러 포맷팅을 모두 통합한 실전용 유틸리티입니다. 마치 튼튼한 방탄 조끼처럼 예상치 못한 공격에도 서비스를 보호합니다.
이 실행기를 사용하면 개별 도구마다 에러 처리를 구현할 필요가 없습니다.
다음 코드를 살펴봅시다.
// 견고한 도구 실행기 클래스
class RobustToolExecutor {
constructor(private options: { maxRetries: number; baseDelay: number; timeout: number }) {}
async execute<T>(toolName: string, fn: () => Promise<T>): Promise<ToolResult<T>> {
const requestId = generateUUID();
return this.retryWithBackoff(async () => {
try {
const result = await this.withTimeout(fn(), this.options.timeout);
return { success: true, data: result };
} catch (err) {
const error = createToolError(toolName, err, requestId);
console.error(`[${requestId}] Tool execution failed:`, JSON.stringify(error));
return { success: false, error };
}
});
}
private async withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('TIMEOUT')), ms)
);
return Promise.race([promise, timeout]);
}
private async retryWithBackoff<T>(fn: () => Promise<ToolResult<T>>): Promise<ToolResult<T>> {
// 앞서 구현한 재시도 로직 적용
for (let i = 0; i <= this.options.maxRetries; i++) {
const result = await fn();
if (result.success || !result.error?.retryable) return result;
if (i < this.options.maxRetries) await sleep(this.options.baseDelay * Math.pow(2, i));
}
return { success: false, error: { code: 'MAX_RETRIES', message: '최대 재시도 초과', retryable: false } };
}
}
김개발 씨는 드디어 실전에 투입할 도구 실행기를 만들기로 했습니다. 지금까지 배운 모든 것을 하나의 클래스로 통합하는 작업입니다.
선배 박시니어 씨가 조언했습니다. "좋은 코드는 한 번 작성하고 여러 곳에서 재사용하는 거야.
매번 try-catch 쓰고, 재시도 로직 복붙하고, 에러 포맷 신경 쓰는 건 비효율적이지." 그 말을 듣고 김개발 씨는 RobustToolExecutor 클래스를 설계하기 시작했습니다. 먼저 클래스의 구조를 살펴보겠습니다.
constructor에서는 재시도 횟수, 기본 대기 시간, 타임아웃 설정을 받습니다. 이 값들은 환경에 따라 다르게 설정할 수 있습니다.
개발 환경에서는 빠른 피드백을 위해 짧게, 프로덕션에서는 안정성을 위해 길게 설정하는 것이 일반적입니다. execute 메서드가 핵심입니다.
도구 이름과 실행할 함수를 받아서 처리합니다. 가장 먼저 requestId를 생성합니다.
이 ID는 이 요청의 전체 생명주기 동안 일관되게 사용됩니다. withTimeout 메서드는 Promise.race를 사용한 영리한 패턴입니다.
실제 작업과 타임아웃 Promise를 경쟁시켜서, 타임아웃이 먼저 끝나면 에러를 발생시킵니다. 이렇게 하면 무한히 대기하는 상황을 방지할 수 있습니다.
retryWithBackoff는 앞서 배운 재시도 로직을 그대로 적용합니다. 성공하거나 재시도 불가능한 에러면 즉시 반환하고, 그렇지 않으면 지수적으로 증가하는 대기 시간 후에 재시도합니다.
이 실행기를 사용하면 어떻게 될까요? 개발자는 더 이상 개별 도구마다 에러 처리를 구현할 필요가 없습니다.
단순히 **executor.execute('날씨조회', () => fetchWeather(city))**처럼 호출하면 됩니다. 타임아웃도, 재시도도, 에러 로깅도 모두 자동으로 처리됩니다.
실제 현업에서 이런 패턴은 매우 흔합니다. AWS SDK, Google Cloud 라이브러리 등 대부분의 클라우드 SDK는 내부적으로 비슷한 구조를 가지고 있습니다.
직접 HTTP 요청을 보내는 대신 SDK를 사용하면 이런 안정성 기능을 무료로 얻을 수 있는 것이죠. 주의해야 할 점도 있습니다.
모든 도구에 동일한 설정을 적용하는 것이 항상 최선은 아닙니다. 실시간 채팅 응답에는 짧은 타임아웃이 필요하고, 대용량 파일 처리에는 긴 타임아웃이 필요합니다.
상황에 따라 옵션을 다르게 전달할 수 있도록 유연하게 설계해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
도구 실행기를 완성한 후, 새로운 도구를 추가할 때마다 에러 처리 코드를 작성하는 시간이 사라졌습니다. 코드도 깔끔해지고, 버그도 줄었습니다.
실전 팁
💡 - 실행기 인스턴스는 싱글톤으로 관리하거나 의존성 주입을 통해 전달하세요
- 환경변수로 타임아웃, 재시도 횟수 등을 설정할 수 있게 만들면 운영이 편리해집니다
5. 실습 폴백 메커니즘
김개발 씨의 도구 실행기는 훌륭하게 동작했습니다. 하지만 박시니어 씨가 새로운 질문을 던졌습니다.
"재시도를 다 했는데도 실패하면 어떡할 거야? 그냥 사용자에게 에러 메시지만 보여줄 건가?" 김개발 씨는 한 번도 생각해보지 못한 문제였습니다.
폴백 메커니즘은 주요 방법이 실패했을 때 대체 방법을 실행하는 것입니다. 마치 비행기의 보조 엔진처럼 주 엔진이 고장 나도 안전하게 착륙할 수 있게 합니다.
완벽한 시스템은 없으므로, 실패에 대비한 플랜 B가 항상 필요합니다.
다음 코드를 살펴봅시다.
// 폴백 체인을 지원하는 도구 실행기
class FallbackToolExecutor extends RobustToolExecutor {
async executeWithFallback<T>(
primary: { name: string; fn: () => Promise<T> },
fallbacks: Array<{ name: string; fn: () => Promise<T> }>
): Promise<ToolResult<T>> {
// 1. 먼저 주 도구 실행 시도
const primaryResult = await this.execute(primary.name, primary.fn);
if (primaryResult.success) return primaryResult;
// 2. 실패하면 폴백 체인을 순차적으로 시도
console.log(`[Fallback] ${primary.name} 실패, 대체 도구 시도 중...`);
for (const fallback of fallbacks) {
const result = await this.execute(fallback.name, fallback.fn);
if (result.success) {
console.log(`[Fallback] ${fallback.name}으로 성공`);
return result;
}
}
// 3. 모든 폴백도 실패하면 캐시된 데이터 또는 기본값 반환
const cached = await this.getCachedData<T>(primary.name);
if (cached) {
console.log(`[Fallback] 캐시 데이터 사용`);
return { success: true, data: cached, fromCache: true };
}
return { success: false, error: { code: 'ALL_FALLBACKS_FAILED', message: '모든 대체 수단 실패', retryable: false } };
}
}
// 사용 예시: 날씨 조회
const weather = await executor.executeWithFallback(
{ name: 'OpenWeatherAPI', fn: () => fetchFromOpenWeather(city) },
[
{ name: 'WeatherAPI', fn: () => fetchFromWeatherAPI(city) },
{ name: 'AccuWeather', fn: () => fetchFromAccuWeather(city) }
]
);
김개발 씨는 폴백이라는 개념을 처음 들었습니다. "실패했으면 그냥 실패한 거 아닌가요?" 그렇게 물었더니 박시니어 씨가 웃으며 말했습니다.
"넷플릭스가 AWS 장애 때 어떻게 했는지 알아? 그냥 '서비스 점검 중'이라고 띄우고 끝낸 게 아니야.
다른 리전의 서버로 트래픽을 돌리고, 그것도 안 되면 미리 캐시해둔 인기 콘텐츠 목록이라도 보여줬어." 그렇습니다. 폴백 메커니즘은 "최악의 상황에서도 최선의 사용자 경험을 제공하자"는 철학입니다.
쉽게 비유하자면, 폴백은 마치 예비 타이어와 같습니다. 고속도로에서 타이어가 펑크 나면 어떻게 할까요?
예비 타이어로 교체하고 계속 달립니다. 예비 타이어도 없다면?
견인 서비스를 부릅니다. 그것도 안 되면?
최소한 갓길에 안전하게 정차는 해야 합니다. 위 코드의 executeWithFallback 메서드를 살펴보겠습니다.
먼저 **주 도구(primary)**를 실행합니다. 대부분의 경우 여기서 성공합니다.
가장 신뢰할 수 있고, 가장 정확한 데이터를 제공하는 도구입니다. 주 도구가 실패하면 폴백 체인을 순차적으로 시도합니다.
날씨 조회의 경우 OpenWeather API가 실패하면 WeatherAPI를, 그것도 실패하면 AccuWeather를 시도하는 식입니다. 각 업체의 장애 시점이 다르므로 하나만 살아 있어도 서비스를 계속할 수 있습니다.
모든 외부 API가 실패하면 마지막으로 캐시 데이터를 확인합니다. 10분 전 날씨 정보라도 없는 것보다는 낫습니다.
사용자에게 "현재 실시간 조회가 어렵습니다. 10분 전 기준 데이터입니다"라고 안내하면 됩니다.
실제 현업에서 폴백은 매우 중요합니다. 대형 서비스들은 **서킷 브레이커(Circuit Breaker)**와 함께 폴백을 사용합니다.
특정 서비스가 연속으로 실패하면 아예 호출을 차단하고 바로 폴백으로 넘어갑니다. 이렇게 하면 장애가 전파되는 것을 막을 수 있습니다.
주의해야 할 점도 있습니다. 폴백 데이터는 주 데이터와 품질이 다를 수 있습니다.
사용자에게 폴백 데이터를 보여줄 때는 이 사실을 명확히 알려야 합니다. 또한 캐시 데이터의 유효기간을 설정하여 너무 오래된 데이터가 제공되지 않도록 해야 합니다.
또한 폴백 체인이 너무 길면 응답 시간이 급격히 늘어납니다. 실시간성이 중요한 서비스라면 폴백 대신 빠르게 에러를 반환하는 것이 나을 수도 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 폴백 메커니즘을 도입한 후, 외부 API 장애가 발생해도 서비스가 완전히 멈추는 일은 없어졌습니다.
사용자들은 "이 서비스는 항상 돌아가네"라고 느끼게 되었습니다. 박시니어 씨가 김개발 씨의 어깨를 두드리며 말했습니다.
"이제 네가 만든 서비스는 정말 믿을 수 있겠어. 잘했어."
실전 팁
💡 - 폴백 서비스들 간의 응답 형식을 통일하는 어댑터 패턴을 적용하면 코드가 깔끔해집니다
- 주기적으로 캐시를 갱신하는 백그라운드 작업을 돌려서 폴백 시에도 신선한 데이터를 제공하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Context Fundamentals - AI 컨텍스트의 기본 원리
AI 에이전트 개발의 핵심인 컨텍스트 관리를 다룹니다. 시스템 프롬프트 구조부터 Attention Budget, Progressive Disclosure까지 실무에서 바로 적용할 수 있는 컨텍스트 최적화 전략을 배웁니다.
프로덕션 워크플로 배포 완벽 가이드
LLM 기반 애플리케이션을 실제 운영 환경에 배포하기 위한 워크플로 최적화, 캐싱 전략, 비용 관리 방법을 다룹니다. Airflow와 서버리스 아키텍처를 활용한 실습까지 포함하여 초급 개발자도 프로덕션 수준의 배포를 할 수 있도록 안내합니다.
LangChain LCEL 완벽 가이드
LangChain Expression Language(LCEL)를 활용하여 AI 체인을 우아하게 구성하는 방법을 배웁니다. 파이프 연산자부터 커스텀 체인 개발까지, 실무에서 바로 활용할 수 있는 핵심 개념을 다룹니다.
Human-in-the-Loop Workflow 완벽 가이드
AI 시스템에서 인간의 판단과 승인을 통합하는 Human-in-the-Loop 워크플로를 알아봅니다. 자동화와 인간 감독의 균형을 맞추는 핵심 패턴을 초급자도 이해할 수 있게 설명합니다.
Router 패턴으로 배우는 LLM 워크플로 설계
LLM 애플리케이션에서 여러 전문가 모델을 효율적으로 연결하고 태스크에 따라 적절한 처리 경로를 선택하는 Router 패턴을 배웁니다. 실무에서 바로 적용할 수 있는 도메인별 라우터와 전문가 선택 시스템을 직접 구현해봅니다.