반응형
AIKit — QA 관점의 신뢰성 설계
QA 사고방식
개발자: "이 코드가 동작하나?"
QA: "이 코드가 언제 깨지나?"
AI API를 다루면서 가장 많이 겪는 실패 시나리오:
- 네트워크 끊김
- API 키 잘못 입력
- 응답이 예상과 다른 형식
- Rate Limit (429)
- 서버 다운 (500, 503)
이걸 라이브러리 레벨에서 막아야 한다.
ResponseValidator
AI의 응답은 예측할 수 없다. "한국어로 답변해줘"라고 했는데 영어로 오거나, JSON 형식을 요청했는데 마크다운이 오거나. 응답이 제대로 왔는지 자동으로 검증하는 시스템이 필요하다.
class ResponseValidator {
constructor() {
this.rules = new Map();
this.loadDefaultRules();
}
}
검증 룰
| 룰 | 설명 | 사용 예시 |
|---|---|---|
minLength |
최소 길이 | 빈 응답 방지 |
maxLength |
최대 길이 | 토큰 낭비 방지 |
mustInclude |
필수 키워드 포함 | 특정 단어가 반드시 있어야 할 때 |
mustNotInclude |
금지 키워드 | 부적절한 내용 필터링 |
format |
형식 검증 | JSON, email, URL, number, markdown |
language |
언어 감지 | 한국어/영어/일본어/중국어 |
regex |
정규식 매칭 | 커스텀 패턴 |
사용법
const ai = new AIKit({ provider: 'openai', apiKey: '...' });
const response = await ai.chat('JSON 형식으로 사용자 정보 생성', {
validate: {
format: 'json',
minLength: 10,
mustInclude: ['name', 'email'],
language: 'english'
}
});
// 검증 실패 시 자동으로 Error throw
format 검증 내부 구현
this.addRule('format', (response, format) => {
const text = this.extractText(response);
const formats = {
json: () => {
try {
JSON.parse(text);
return { valid: true };
} catch (e) {
return { valid: false, error: 'Response is not valid JSON' };
}
},
email: () => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(text)
? { valid: true }
: { valid: false, error: 'Not valid email' };
}
};
return formats[format.toLowerCase()]();
});
에러 분류
모든 에러를 3가지 카테고리로 분류한다.
| 에러 타입 | 원인 | 재시도 가능 | 예시 |
|---|---|---|---|
| ConfigurationError | 개발자 실수 | 불가 | 잘못된 API 키, 누락된 설정 |
| APIError | 프로바이더 문제 | 가능 | 429 Rate Limit, 500 서버 에러 |
| ValidationError | 응답 문제 | 가능 | 형식 불일치, 길이 초과 |
ConfigurationError는 재시도해도 소용없다 (API 키가 틀린 건 100번 보내도 틀림). APIError와 ValidationError만 재시도한다.
자동 재시도 (Exponential Backoff)
async withRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Rate Limit 에러만 재시도
const isRateLimit = error.message.includes('429');
if (!isRateLimit) throw error;
// 1초 → 2초 → 4초 (지수 증가)
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
}
핵심은 Rate Limit만 재시도한다는 것이다. 잘못된 API 키(401)나 권한 문제(403)는 즉시 throw한다.
삽질 기록
버그 1: 캐시 키 충돌
같은 메시지를 다른 프로바이더에 보냈는데 캐시가 공유되는 문제.
// Before — 메시지만으로 캐시 키 생성
generateKey(message) {
return btoa(message);
}
// After — 프로바이더 + 모델까지 포함
generateKey(message, options) {
return btoa(JSON.stringify({ message, provider: options.provider, model: options.model }));
}
버그 2: 재시도 무한 루프
네트워크 에러와 Rate Limit 에러를 구분하지 않아서 모든 에러에 재시도 → 영원히 반복.
// Before
if (error) { retry(); } // 모든 에러 재시도
// After
const isRateLimit = error.message.includes('429');
if (!isRateLimit) throw error; // Rate Limit만 재시도
버그 3: 캐시 메모리 누수
캐시에 제한이 없어서 계속 쌓이는 문제. TTL(1시간)과 최대 항목 수(50개)를 추가해서 해결했다.
테스트 전략
테스트 피라미드:
├── Unit Tests (75%) — 각 클래스/메서드 개별 테스트
├── Integration (20%) — 어댑터 + AIKit 연동
└── E2E (5%) — 실제 API 호출 (CI에서는 skip)
| 항목 | 수치 |
|---|---|
| 총 테스트 | 45개 |
| 커버리지 | 87% |
| Statement | 89% |
| Branch | 82% |
| Function | 91% |
마무리
QA 경험이 라이브러리 설계에 직접적으로 반영된 부분이다. "어떻게 하면 깨지는가"를 먼저 생각하고 그 시나리오에 대한 방어 코드를 작성한다. 다음 편에서는 이 라이브러리를 이용해서 5분 만에 AI 챗봇을 만드는 튜토리얼을 다룬다.
기술 스택: Vanilla JavaScript, Jest, ResponseValidator
소스 코드: GitHub
반응형
'프로젝트 > AIKit' 카테고리의 다른 글
| AIKit #6 - React & Vue에서 AIKit 사용하기 (0) | 2026.02.08 |
|---|---|
| AIKit #5 - PHP 프로젝트에서 AIKit 연동하기 (0) | 2026.02.08 |
| AIKit #4 - 5분 만에 AI 챗봇 만들기 (0) | 2026.02.07 |
| AIKit #2 - Adapter Pattern으로 멀티 프로바이더 지원하기 (0) | 2026.02.06 |
| AIKit #1 - 왜 AI API 통합 라이브러리를 만들었나 (0) | 2026.02.05 |