Kotlin Coroutine 환경에서 대량의 FCM Push 메시지를 처리할 때,
많은 예제나 문서에서는 Dispatchers.IO를 사용하는 것을 기본으로 제시합니다. 그러나 실무에서는 다음과 같은 이유로 커스텀 ThreadPoolTaskExecutor를 Coroutine Dispatcher로 wrapping 하여 사용하는 경우도 많습니다:
이 글에서는 Dispatchers.IO와 커스텀 디스패처 각각의 장단점을 비교하고, 어떤 상황에서 어떤 선택이 더 합리적인지를 다룹니다.
| 항목 | 설명 |
|---|---|
| 자동 확장 | CPU 코어 수의 64배까지 자동 스레드 확장 |
| idle thread 제거 | 사용하지 않으면 자동 스레드 정리 |
| 용도 | 파일, DB, 네트워크 등 Blocking I/O 처리 |
| 공유 자원 | 전체 시스템에서 공용으로 사용됨 |
withContext(Dispatchers.IO)만 쓰면 됨)Push 트래픽이 많아졌을 때 전체 시스템에 영향을 주지 않도록 격리
@Bean
fun firebasePushExecutor(): ThreadPoolTaskExecutor {
return ThreadPoolTaskExecutor().apply {
corePoolSize = 4
maxPoolSize = 8
setQueueCapacity(3000)
setThreadNamePrefix("FirebasePush-")
setRejectedExecutionHandler(ThreadPoolExecutor.CallerRunsPolicy())
initialize()
}
}
val firebaseDispatcher = firebasePushExecutor.asCoroutineDispatcher()
| 항목 | Dispatchers.IO | 커스텀 Dispatcher |
|---|---|---|
| 관리 용이성 | 자동 | 직접 관리 필요 |
| 리소스 격리 | 없음 | Push 트래픽 격리 가능 |
| OOM 방지 | 제한적 | 제어 가능 |
| 큐 설정 | 불가능 | 명시적 설정 가능 |
| 모니터링 | JVM 기반 | Spring Actuator 기반 |
Dispatchers.IO는 단순하고 강력하지만, 격리와 안정성 측면에서 커스텀 디스패처가 더 안전한 선택일 수 있다.
| 상황 | 추천 |
|---|---|
| 단순 외부 API / DB I/O | Dispatchers.IO |
| 대량 푸시, 메시징, 격리 필요 | 커스텀 디스패처 |
| 시스템 리스크 분리 필요 | 커스텀 디스패처 |
| 코드 간결성 / 테스트 | Dispatchers.IO |
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.