ETC

포스트 하나에 10초? 코루틴으로 AI 요약 파이프라인 병렬화하기

Stitchhhh 2026. 2. 24. 18:09

최근 기술 블로그 RSS를 스크래핑하고 AI로 내용을 요약하는 'Morning Commit' 프로젝트의 파이프라인을 개선했습니다. 구현 과정에서 마주친 성능 병목 지점과 이를 Kotlin Coroutines로 해결한 과정을 정리했습니다.

1. 문제 상황: 순차 처리의 한계

기존 방식은 RSS 피드에서 추출한 포스트들을 하나씩 순서대로 처리했습니다.

  • 프로세스: HTML 스크래핑 → AI 요약(LLM API 호출) → 데이터 가공 및 저장
  • 소요 시간: 포스트 당 평균 약 10초

포스트 하나당 10초는 짧아 보일 수 있지만, 배치 작업의 특성상 누적 시간은 무시할 수 없습니다. 한 번에 처리해야 할 포스트가 30개만 되어도 5분이 소요되며, 연동하는 블로그 소스가 늘어날수록 전체 작업 완료 시간은 선형적으로 증가하는 구조였습니다.

기존 코드 (Sequential)

Kotlin
 
feed.entries
    .filter { /* 날짜 필터링 */ }
    .mapNotNull { entry ->
        // 포스트 하나당 약 10초 대기 발생
        val fullContent = htmlScraper.scrapeContent(link)
        val analysisResult = summaryService.analyze(fullContent)
        // ... Post 객체 생성
    }

2. 해결책: Kotlin Coroutines와 Semaphore

스크래핑과 AI API 호출은 대표적인 I/O Bound 작업입니다. CPU 자원을 소모하기보다는 네트워크 응답을 기다리는 시간이 대부분이므로, 이를 병렬로 처리하면 전체 대기 시간을 획기적으로 줄일 수 있습니다.

다만, 무분별한 병렬 처리는 AI API의 Rate Limit(호출 제한)을 초과하거나 시스템에 과부하를 줄 수 있습니다. 이를 방지하기 위해 Semaphore를 사용하여 동시에 실행되는 작업의 개수를 제한했습니다.

구현 핵심

  1. runBlocking & Dispatchers.IO: Spring Batch의 동기적인 흐름을 유지하면서 코루틴을 실행하기 위해 runBlocking을 사용했으며, I/O 작업에 최적화된 스레드 풀을 활용했습니다.
  2. async & awaitAll: 각 포스트 처리 로직을 비동기로 실행하고, 모든 결과가 준비되었을 때 한꺼번에 수집하도록 구현했습니다.
  3. Semaphore(5): 동시에 실행되는 코루틴 개수를 5개로 제한하여 안정적인 API 호출 환경을 구축했습니다.

3. 변경된 코드 (Parallel)

Kotlin
 
// 동시 처리 제한을 위한 세마포어 설정 (동시성 5)
val semaphore = Semaphore(5)

runBlocking(Dispatchers.IO) {
    filteredEntries.map { entry ->
        async {
            // 세마포어 허가를 획득한 코루틴만 내부 로직 수행
            semaphore.withPermit {
                try {
                    val link = entry.link ?: return@withPermit null
                    
                    // 비동기 스레드 차단 없이 병렬 수행
                    val fullContent = htmlScraper.scrapeContent(link)
                    val analysisResult = summaryService.analyze(fullContent)
                    
                    if (analysisResult == null || analysisResult.isPromotional) return@withPermit null

                    Post(
                        // ... 객체 생성 로직
                    )
                } catch (e: Exception) {
                    log.error("Failed to process entry: ${entry.title}", e)
                    null
                }
            }
        }
    }.awaitAll().filterNotNull() // 전체 결과 수집 및 유효 데이터 필터링
}

4. 결과 및 결론

병렬 처리를 도입한 후, 배치 작업의 전체 소요 시간이 크게 단축되었습니다.

  • 성능 향상: 10개의 포스트를 처리할 때 기존에는 100초가 걸렸으나, 동시성 5를 적용한 후에는 약 20~25초 수준으로 완료되었습니다.
  • 안정성: Semaphore를 통해 동시 실행 수를 제어함으로써, API 서버로부터 'Too Many Requests' 에러를 받지 않고 안정적으로 데이터를 수집할 수 있었습니다.

단순히 "빠르게" 만드는 것보다 시스템의 제약 사항을 고려하여 "효율적이고 안전하게" 설계하는 것이 실무적인 병렬 처리의 핵심임을 배울 수 있었습니다.