본문 바로가기
프로젝트/AIKit

AIKit #3 - QA 개발자가 만든 AI 라이브러리

by 루까(Luka) 2026. 2. 7.
반응형

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

반응형