Kotlin Coroutine FCM Push 처리 - Dispatchers.IO vs 커스텀 디스패처 정리

바나나·2025년 6월 2일

개요

Kotlin Coroutine 환경에서 대량의 FCM Push 메시지를 처리할 때,
많은 예제나 문서에서는 Dispatchers.IO를 사용하는 것을 기본으로 제시합니다. 그러나 실무에서는 다음과 같은 이유로 커스텀 ThreadPoolTaskExecutor를 Coroutine Dispatcher로 wrapping 하여 사용하는 경우도 많습니다:

  • OutOfMemoryError 방지
  • Push 트래픽이 시스템 전체에 영향을 주는 것을 방지

이 글에서는 Dispatchers.IO와 커스텀 디스패처 각각의 장단점을 비교하고, 어떤 상황에서 어떤 선택이 더 합리적인지를 다룹니다.


Dispatchers.IO 특징

항목설명
자동 확장CPU 코어 수의 64배까지 자동 스레드 확장
idle thread 제거사용하지 않으면 자동 스레드 정리
용도파일, DB, 네트워크 등 Blocking I/O 처리
공유 자원전체 시스템에서 공용으로 사용됨

장점

  • 사용이 간단하다 (withContext(Dispatchers.IO)만 쓰면 됨)
  • 대부분의 I/O 작업에 적절한 성능

단점

  • 공용 풀이라서 Push 트래픽 폭주 시 다른 작업까지 영향을 받을 수 있음
  • 최대 스레드 수는 제한되지만 강제로 증가 가능 → 메모리 압박 발생

커스텀 Dispatcher (ThreadPoolTaskExecutor 기반)

목적

Push 트래픽이 많아졌을 때 전체 시스템에 영향을 주지 않도록 격리

예시 코드

@Bean
fun firebasePushExecutor(): ThreadPoolTaskExecutor {
    return ThreadPoolTaskExecutor().apply {
        corePoolSize = 4
        maxPoolSize = 8
        setQueueCapacity(3000)
        setThreadNamePrefix("FirebasePush-")
        setRejectedExecutionHandler(ThreadPoolExecutor.CallerRunsPolicy())
        initialize()
    }
}
val firebaseDispatcher = firebasePushExecutor.asCoroutineDispatcher()

장점

  • 스레드 풀을 격리해서 Push 실패가 다른 기능에 영향을 주지 않음
  • core/max/thread queue 제어 가능
  • Micrometer, Prometheus 등을 통한 모니터링 가능
  • 큐가 꽉 찼을 때 reject 정책 설정 가능

단점

  • 코드가 복잡해지고 관리 부담 증가

실전 비교 요약

항목Dispatchers.IO커스텀 Dispatcher
관리 용이성자동직접 관리 필요
리소스 격리없음Push 트래픽 격리 가능
OOM 방지제한적제어 가능
큐 설정불가능명시적 설정 가능
모니터링JVM 기반Spring Actuator 기반

결론

Dispatchers.IO는 단순하고 강력하지만, 격리와 안정성 측면에서 커스텀 디스패처가 더 안전한 선택일 수 있다.

  • Push 트래픽이 시스템 전체를 마비시킬 위험이 있다면 → 커스텀 Dispatcher 사용
  • 단순한 비동기 처리라면 → Dispatchers.IO 사용

최종 요약 표

상황추천
단순 외부 API / DB I/ODispatchers.IO
대량 푸시, 메시징, 격리 필요커스텀 디스패처
시스템 리스크 분리 필요커스텀 디스패처
코드 간결성 / 테스트Dispatchers.IO

부록: FCM Push 처리 구조 예시 (코루틴 기반)

val scope = CoroutineScope(firebaseDispatcher + coroutineContext)

val jobs = chunk.map { pushMessage ->
    val fcmMessage = pushMessage.toFcmMessage()
    pushMessage to scope.async {
        try {
            pushMessage to firebaseHttp2Client.send(...)
        } catch (e: Exception) {
            pushMessage to e
        }
    }
}

jobs.forEach { (pushMessage, job) ->
    val result = job.await()
    if (result.second is Throwable) {
        // 실패
    } else {
        // 성공
    }
}

실제로 의도적으로 푸시 트래픽을 커스텀 풀로 분리해두면, 장애 전파를 차단하고 운영 안정성을 크게 높일 수 있습니다.

"자동으로 잘 관리된다고 해서, 모든 상황에 안전한 건 아니다." → 이게 핵심입니다.


※ 이 글은 개발자에 의해 작성되었으며, 일부 정리 및 요약 과정에서 OpenAI GPT의 도움을 받았습니다.

Note: This document was authored and reviewed by a developer, with the assistance of OpenAI's GPT to accelerate summarization and clarity.

profile
Java/Kotlin Spring 개발자 황재명입니다.

0개의 댓글