해결한 문제와 담당 역할
LLM 기능을 여러 서비스에 붙이면 모델 연동, 장애 대응, 토큰 예산, 캐시, 호출 기록을 서비스마다 따로 만들게 됩니다.
OpenAI 호환 API를 만들고, 인증 뒤에 공통 정책이 정해진 순서대로 돌게 했습니다.
캐시(똑같은 요청, 비슷한 질문), 할당량, 모델 선택, 재시도 한도, 서킷 브레이커, 대체 모델 전환을 넣었습니다.
일반 응답, SSE 스트리밍, 도구 호출, 관리 화면까지 테스트했습니다.
핵심 요구사항
- 할당량이나 예산을 넘긴 요청은 모델을 부르기 전에 끝납니다.
- 똑같은 요청이 캐시에 있으면 유사도 검색도, 모델 호출도 하지 않습니다.
- 재시도 한도를 다 쓰면 다음 대체 모델을 부르지 않습니다.
기술 선택과 이유
OpenAI 호환 API 하나로
- 적용 방식
- 모든 서비스가 OpenAI 호환 /v1/chat/completions로 호출합니다.
- 검토한 대안
- 서비스마다 전용 API를 두거나 모델 SDK를 직접 붙이기
- 선택 이유
- 이미 OpenAI SDK를 쓰는 코드는 주소만 바꾸면 되고, 정책은 게이트웨이에서 한 번에 적용됩니다.
정책 순서는 코드로
- 적용 방식
- 인증 이후 할당량, 입력 검사, 캐시, 모델 선택·호출, 대체 모델, 출력 검사와 기록 순서를 파이프라인에 명시했습니다.
- 검토한 대안
- 기능마다 WebFilter로 등록하고 순서는 설정에 맡기기
- 선택 이유
- 할당량 검사가 모델 호출보다 먼저 도는지 테스트로 확인할 수 있어야 했습니다. 설정에 맡기면 순서가 바뀌어도 알기 어렵습니다.
재시도에는 한도를
- 적용 방식
- 재시도 한도와 서킷 브레이커를 거친 뒤에만 대체 모델로 넘어갑니다.
- 검토한 대안
- 성공할 때까지 모델을 돌아가며 호출
- 선택 이유
- 기본 모델 장애가 무제한 재시도로 이어지지 않도록 호출 횟수와 후보 범위를 제한하기 위한 선택입니다.
주요 구현과 관련 테스트
핵심 로직과 동작을 확인한 테스트를 함께 정리했습니다.
주요 구현
GatewayPipeline.execute
backend/src/main/java/com/example/gateway/api/GatewayPipeline.java
주요 구현
GatewayPipeline.execute
backend/src/main/java/com/example/gateway/api/GatewayPipeline.java코드
소스 파일 열기QuotaOutcome quota = quotaGuard.evaluate(request, estimatedTokens);
if (quota != QuotaOutcome.ALLOWED) {
return rejected(request, mode, quota, elapsed(start));
}
구현 내용
예상 토큰으로 조직 할당량부터 확인하고, 넘으면 모델을 부르기 전에 끝냅니다.
관련 테스트
RequestLogStoreTest.quotaRejectPathCreatesRowWithNullProviderAndOutcomebackend/src/test/java/com/example/gateway/observability/RequestLogStoreTest.java테스트 결과정책 순서와 모델 호출 여부가 예상대로 나옵니다.
추가 구현
TwoStageCache.lookup
backend/src/main/java/com/example/gateway/cache/TwoStageCache.java
추가 구현
TwoStageCache.lookup
backend/src/main/java/com/example/gateway/cache/TwoStageCache.java코드
소스 파일 열기if (exactLookup.hit()) {
return exactLookup;
}
return semantic.lookup(request);
구현 내용
똑같은 요청은 바로 캐시에서 돌려주고, 없을 때만 비슷한 질문을 찾습니다.
관련 테스트
TwoStageCacheTest.differentlyWordedSimilarPromptFallsThroughToSemanticHitbackend/src/test/java/com/example/gateway/cache/TwoStageCacheTest.java테스트 결과일반, 스트리밍, 도구 호출 응답 형식이 맞게 나옵니다.
추가 구현
FallbackChain.dispatch
backend/src/main/java/com/example/gateway/resilience/FallbackChain.java
추가 구현
FallbackChain.dispatch
backend/src/main/java/com/example/gateway/resilience/FallbackChain.java코드
소스 파일 열기lastErrorType = RETRY_BUDGET_EXHAUSTED;
events.add(FallbackEvent.failed(
attempt, candidate.provider(), candidate.model(), lastErrorType
));
return FallbackResult.ofFailure(attempt, lastErrorType, events);
구현 내용
재시도 한도를 다 쓰면 다음 모델을 부르지 않고 실패 이벤트를 남깁니다.
관련 테스트
FallbackChainTest.exhaustedBudgetBlocksFallbackWithoutCallingProviderbackend/src/test/java/com/example/gateway/resilience/FallbackChainTest.java테스트 결과서버 없이도 요청 흐름과 모델 선택 결과를 둘러볼 수 있습니다.
테스트 결과
할당량 넘긴 요청
모델을 부르지 않고 거절
관련 코드backend/src/main/java/com/example/gateway/api/GatewayPipeline.java같은 요청 2번
모델 호출은 1번, 두 번째는 캐시에서 응답
관련 코드TwoStageCacheTest.identicalRepeatHitsExactlyAndSkipsProvider재시도 한도를 다 씀
다른 모델로 넘어가지 않고 실패 이유를 기록
관련 코드FallbackChainTest.exhaustedBudgetBlocksFallbackWithoutCallingProvider테스트 방법 보기
| 구분 | 테스트 방법 | 결과 |
|---|---|---|
| 단위 테스트 | 할당량, 캐시, 모델 선택, 대체 모델을 가짜 모델로 하나씩 돌려 봅니다. | 정책 순서와 모델 호출 여부가 예상대로 나옵니다. |
| 통합 테스트 | WebFlux API로 일반 요청과 SSE 요청을 각각 보냅니다. | 일반, 스트리밍, 도구 호출 응답 형식이 맞게 나옵니다. |
| 정적 데모 | 관리 화면을 정적 파일로 빌드합니다. | 서버 없이도 요청 흐름과 모델 선택 결과를 둘러볼 수 있습니다. |
프로젝트 범위
검증 한계와 미구현 범위
- 기본 실행은 테스트 모델·임베딩과 메모리 저장소입니다. 실제 모델 호출·과금·클라우드 배포는 검증하지 않았습니다.
- Redis Testcontainers 테스트와 운영 환경 검증은 구분합니다. PostgreSQL·pgvector의 운영 성능과 고가용성은 검증하지 않았습니다.
- SSE는 완성된 가짜 응답을 잘라서 보내는 방식이라, 실제 모델 토큰을 실시간으로 중계한 건 아닙니다.
향후 개선
- 실제 모델을 붙여서 정책별 비용과 지연을 따로 재 보려고 합니다.
관련 자료