안녕하세요.
리얼 허거덩거덩스한 상황인데요
문제는 그렇게 시작됐습니다.
저희 서비스 "BigPicture"는 매일 자동으로 여러 경제 지표와 뉴스를 크롤링하여 매주 경제 리포트를 생성해주는 서비스인데요
크롤링 시 모종의 이유로 실패해서 데이터를 수집하지 못한다면 큰일나겠죠~?
사실 또 저희가 크롤링 후 변환 로직에 LLM과 베타버전인 자바용 랭체인을 사용해서 예상치 못한 오류가 엄청 많았습니다.
그래서 생각해낸게...
크롤링 로직을 재시도 로직으로 감싸서 실행하자!
크
시작합니다
우선 파라미터로 메인 로직을 () -> ~~~ 로 넘겨줘야 할 것 같은데, 뭘 써야할까요?
찾아보니 Supplier라는 인터페이스가 있었습니다.
이런걸 함수형 인터페이스라 부르는데 대표적으로
Predicate<T>
- 입력값을 하나 받아서 boolean 값을 반환
- boolean test(T t)
Supplier<T>
Consumer<T>
- 입력값을 받아서 처리하지만, 반환값이 없음
- void accept(T t)
Function<T, R>
- 입력값을 받아서 처리하지만, 반환값이 없음
- R apply(T t)
이렇게 있었습니다.
저는 메인 로직들이 공통적으로 입력값을 받지 않고 CrawlingResultDto를 반환하고 있었기 때문에 Supplier를 사용하기로 했습니다.
@Builder
@AllArgsConstructor
@Getter
public class CrawlingResultDto {
private Boolean result;
private String message;
public static CrawlingResultDto of(Boolean result, String message) {
return CrawlingResultDto.builder()
.result(result)
.message(message)
.build();
}
}
CrawlingResultDto는 각 크롤링 결과를 받아옵니다
result는 성공 여부, 그리고 message는 어떤 작업을 완료했는지를 반환해줍니다
자 이제 재시도 로직입니다.
@Slf4j
public class RetryExecutor {
private final static int RETRY_COUNT = 3;
public static void execute(Supplier<CrawlingResultDto> supplier, String taskName) {
String message = "";
for (int i = 0; i < RETRY_COUNT; i++) {
try {
CrawlingResultDto result = supplier.get();
message = result.getMessage();
if (result.getResult()) {
log.info("{}: {}", taskName, message);
return;
}
} catch (Exception e) {
log.warn("{}: {}번째 시도 중 예외 발생 - {}", taskName, i + 1, e.getMessage());
}
}
}
}
일단 최대 3번 실행되게 했구요, 로직 실행 시 예외는 try-catch로 잡고 크롤링 결과(result.getResult)가 true면 바로 리턴시켜줬습니다.
실제 사용 시
public void exchangeCrawling() {
RetryExecutor.execute(() -> exchangeCrawlingService.crawling(), "ExchangeCrawlingService");
}
이런 식으로 사용됩니다.
어... 근데 찾아보니 spring-retry 라는게 있네요?
GitHub - spring-projects/spring-retry
Contribute to spring-projects/spring-retry development by creating an account on GitHub.
github.com
흠...
@Retryable(
retryFor = Exception.class,
maxAttempts = RETRY_COUNT
)
public void exchangeCrawling() {
exchangeCrawlingService.crawling();
}
음 쉽게 해결됐네요
그 물론 단순 예외 기준 재시도로는 @Retryable이 좋은 것 같긴 한데, 실패 시점에 후처리나 반환값 그리고 로깅 등의 비표준 요구사항이 있으면 제가 구현한 RetryExecutor를 쓰는게 나을 것 같습니다.
둘 다 뭐 좋다는거죠~
에... 오늘은 성공이라 하긴 좀 애매하고
일단 해결