본문 바로가기

카테고리 없음

[TIL] 유레카 sw교육과정 백엔드 28주차 내용 정리

 

최종 융합 프로젝트 마감

프로젝트 관련 PPT

먼저 프로젝트 주제 및 선정 배경

기획의도

차별화 요소

개발환경

협업 구조

 

내가 맡은 역할

 

시스템 아키텍처

프로젝트 플로우

내가 개발한 부분에 대한 내용

내가 구현 코드 핵심 내용

1. IntegratedAnalysisScheduler: 원자적 플래그를 이용한 중복 방지

여러 노드에서 배치가 실행될 때 동일한 데이터가 중복 분석되는 것을 방지하기 위해 AtomicBoolean을 활용한 '문지기(Gatekeeper)' 로직을 구현했습니다.

Java
 
@Component
@RequiredArgsConstructor
public class IntegratedAnalysisScheduler {
    private final AtomicBoolean isProcessing = new AtomicBoolean(false);

    @Scheduled(fixedDelay = 20000)
    public void executeIntegratedAnalysis() {
        // [STEP 1] CAS(Compare-And-Set)를 이용한 원자적 실행 보장
        if (!isProcessing.compareAndSet(false, true)) {
            log.warn("[Batch] 이전 작업이 진행 중입니다. 스킵합니다.");
            return;
        }

        try {
            // [STEP 2] 요약(Result)과 채점(Excellent)의 짝이 맞는 데이터만 페이징 조회
            List<ResultEventStatus> pendingResults = 
                resultEventRepository.findReadyToProcessPairs(PageRequest.of(0, 20));
            
            // ... TaskPair 구성 후 Manager에게 위임
        } finally {
            isProcessing.set(false); // 작업 완료 후 플래그 해제
        }
    }
}
  • 기술 포인트: synchronized 블록보다 성능 오버헤드가 적은 Non-blocking CAS 연산을 사용하여 스케줄러 간의 경합을 제어했습니다.

2. ConsultationAnalysisManager: 비동기 병렬 분석 트랙

상담 하나를 분석할 때 '요약 추출'과 '품질 채점'은 서로 의존성이 없습니다. 이를 CompletableFuture를 이용해 병렬로 실행함으로써 전체 처리 속도를 대폭 개선했습니다.

Java
 
@Service
public class ConsultationAnalysisManager {
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 번들 단위 독립 트랜잭션
    public void processIntegratedBundledTasks(List<TaskPair> taskPairs) {
        // [STEP 1] AI 비동기 실행 (이중 트랙 병렬화)
        CompletableFuture<List<AiExtractionResponse>> extractionFuture = CompletableFuture.supplyAsync(() ->
                geminiExtractor.extractBatch(processedRawTexts, groupType, validCodes), geminiTaskExecutor
        );

        List<CompletableFuture<QualityScoringResponse>> scoringFutures = processedRawTexts.stream()
                .map(text -> CompletableFuture.supplyAsync(() -> 
                    geminiQualityScorer.evaluate(text, manualContent), geminiTaskExecutor))
                .toList();

        // [STEP 2] 결과 수집 및 타임아웃 제어
        List<AiExtractionResponse> extractionResults = 
            extractionFuture.orTimeout(180, TimeUnit.SECONDS).join();
        // ... 결과 매칭 및 DB 저장
    }
}
  • 기술 포인트: REQUIRES_NEW를 통해 각 번들 처리를 격리했습니다. 특정 번들의 AI 분석이 실패하더라도 다른 번들의 성공 결과는 안전하게 커밋됩니다.

3. GeminiExtractor & Scorer: 지수 백오프 기반의 장애 회복력

외부 AI API는 네트워크 상태나 Rate Limit에 민감합니다. 이를 해결하기 위해 지수 백오프(Exponential Backoff) 전략을 적용했습니다.

Java
 
@Service
public class GeminiExtractor {
    @Retryable(
        retryFor = {Exception.class}, 
        maxAttempts = 3, 
        backoff = @Backoff(
            delay = 2000,      // 초기 2초 대기
            multiplier = 2.0,  // 2배씩 증가 (2s -> 4s -> 8s)
            maxDelay = 10000   // 최대 10초 상한
        )
    )
    public List<AiExtractionResponse> extractBatch(...) {
        // API 호출 로직...
    }
}
  • 기술 포인트: 일시적인 429 Too Many Requests 에러 발생 시 서버에 회복 시간을 주어 성공률을 높였습니다.
  • 백오프 공식:
  • $$Wait\ Time = delay \times (multiplier^{attempt-1})$$

4. IntegratedRetryScheduler: 자해 복구(Self-Healing) 시스템

서버가 갑자기 셧다운되어 데이터가 PROCESSING 상태로 영원히 멈추는 '좀비 데이터' 문제를 해결하기 위한 복구 엔진입니다.

Java
 
@Scheduled(fixedDelay = 600000) // 10분마다 실행
@Transactional
public void retryAndRecoverTasks() {
    // [좀비 데이터 구출] 30분 이상 갱신 없는 PROCESSING 태스크 초기화
    LocalDateTime threshold = LocalDateTime.now().minusMinutes(30);
    resultEventRepository.cleanupStaleProcessingTasks(threshold);
    
    // [실패 데이터 구제] retry_count < 3인 실패 건 자동 재시도
    List<ResultEventStatus> failedSummaries = 
        resultEventRepository.findByStatusAndRetryCountLessThan(EventStatus.FAILED, 3);
    failedSummaries.forEach(ResultEventStatus::retry);
}
  • 기술 포인트: 에러 핸들링으로 잡을 수 없는 **'인프라적 결함'**까지 고려하여 데이터 생명 주기를 완성했습니다.

🚀 마치며: 백엔드 개발자로서의 성찰

이번 프로젝트를 통해 **'장애는 피하는 것이 아니라 관리하는 것'**임을 배웠습니다. 단순히 API를 연동하는 수준을 넘어, 상태 선점 전략, 비동기 자원 격리, 자해 복구 로직을 고민하며 시스템의 견고함(Robustness)을 한 단계 높일 수 있었습니다.