크롤링 파이프라인 중에는
LLM API와 임베딩 API 호출하는 스텝이 있습니다.
아무래도 외부 API이다보니 장애가 발생할 수 있다는 생각이 들어서
서킷 브레이커 패턴을 도입해보자는 생각이 들었습니다.
여기서 서킷 브레이커 패턴은 연속된 요청이 실패할경우
서킷을 열어 오픈 상태로 만들어 요청을 외부 API로 보내지 않고,
하프-오픈 상태가 되었을 때 테스트 패킷을 보내본 뒤에 성공할 경우 다시 서킷을 닫아 외부 API 콜을 진행하는 패턴입니다.
이와 관련된 라이브러리로는 대표적으로 Resilience4j가 있었는데요.
스프링 부트와 통합이 잘 되어있어 바로 도입을 해보았습니다.
https://resilience4j.readme.io/docs/getting-started
Introduction
Resilience4j is a lightweight fault tolerance library designed for functional programming. Resilience4j provides higher-order functions (decorators) to enhance any functional interface, lambda expression or method reference with a Circuit Breaker, Rate Lim
resilience4j.readme.io
Resilience4j는 서킷 브레이커뿐만 아니라
Retry와 Rate Limiter 또한 코어 모듈로 가지고 있습니다.
Retry는 재시도 관련 로직인데
기존에는 스프링 배치의 단순 Retry를 적용했습니다.
하지만 LLM API는 Rate Limit라고 일정 시간동안 토큰의 개수를 제한하고 있는만큼,
재시도를 일정한 간격으로 진행하는 대신 지수적으로 증가시킬 필요가 있었습니다.
이 기능은 Spring 7.x 버전부터는 @Retryable이라는 기능으로 가능하지만
테크포크는 스프링 6.2.15를 사용하므로 불가능하여 Resilience4j에서 지원하는 모듈을 사용했습니다.
https://docs.spring.io/spring-framework/reference/core/resilience.html
Resilience Features :: Spring Framework
@Retryable is an annotation that specifies retry characteristics for an individual method (with the annotation declared at the method level), or for all proxy-invoked methods in a given class hierarchy (with the annotation declared at the type level). @Ret
docs.spring.io
마지막으로 Rate Limiter는 요청을 실행하기 전에
요청이 제한되지 않도록 속도를 조절하는 기능을 담당하는 모듈입니다.
말그대로 Rate Limit가 걸리지 않도록 도와주는 모듈입니다.
실제 구현
# Resilience4j 설정 (LLM API 호출용)
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 5
failure-rate-threshold: 60
slow-call-rate-threshold: 70
slow-call-duration-threshold: 45s
wait-duration-in-open-state: 60s
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
record-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
- java.io.IOException
- org.springframework.web.client.ResourceAccessException
ratelimiter:
configs:
default:
limit-for-period: 15
limit-refresh-period: 1m
timeout-duration: 10s
retry:
configs:
default:
max-attempts: 3
wait-duration: 2s
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
exponential-max-wait-duration: 10s
retry-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
- java.io.IOException
- org.springframework.web.client.ResourceAccessException
yml 파일을 통해 각 모듈에 대한 파라미터 값을 설정했습니다.
공식 문서에서도 스프링 부트와 통합할 땐 yml 파일을 권장하여 예제 코드를 제공하고 있었습니다.
https://resilience4j.readme.io/docs/getting-started-3
Getting Started
Getting started with resilience4j-spring-boot2 or resilience4j-spring-boot3
resilience4j.readme.io
/**
* Anthropic Claude 기반 LLM 클라이언트 구현체
* Resilience4j를 통한 Circuit Breaker, Rate Limiter, Retry 패턴 적용
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AnthropicLlmClient implements LlmClient {
private final AnthropicChatModel chatModel;
@Override
@Retry(name = "llmApi")
@CircuitBreaker(name = "llmApi")
@RateLimiter(name = "llmApi")
public String call(String systemPrompt, String userPrompt) {
Prompt prompt = new Prompt(List.of(
new SystemMessage(systemPrompt),
new UserMessage(userPrompt)
));
return chatModel.call(prompt).getResult().getOutput().getText();
}
}
사용법은 다음과 같이 어노테이션을 붙여주기만 하면 적용됩니다.
어노테이션을 붙이는 순서와 상관없이
내부 우선순위에 따라
Retry -> CircuitBreaker -> RateLimiter 순으로 동작합니다.
따로 우선순위를 주는 AspectOrder 설정도 있지만 일단 그 부분은 제외했습니다.
현재 각 모듈의 파라미터 값을 임의로 지정한만큼 테스트를 통한 튜닝이 필요합니다.
목적은 일시적인 네트워크 장애는 서킷이 열리지 않으며,
외부 API가 먹통이 됐을 때는 서킷이 열려 크롤링이 실패하도록,
그리고 정상 작동시에는 Rate Limit에 걸리지 않도록 튜닝하는 것입니다.
꽤나 어려울 거 같습니다만... 재밌네요!
'프로젝트 > Techfork' 카테고리의 다른 글
| [26/01/07] 오늘의 개발 일지 - Resilience4j 설정 개선 및 JdbcTemplate를 활용한 배치 처리 (0) | 2026.01.08 |
|---|---|
| [26/01/06] 오늘의 개발일지 - AsyncItemProcessor의 도입과 Spring Batch 메타데이터 활용 (0) | 2026.01.07 |
| [26/01/03] 오늘의 개발 일지 - Spring Batch JobExecutionListener 도입 (0) | 2026.01.04 |
| [25/01/02] 오늘의 개발 일지 - RSS 크롤링 성능 및 안정성 개선 (0) | 2026.01.03 |
| [26/01/01] 오늘의 개발 일지 - OCI 서버 배포 완료 (0) | 2026.01.01 |