
코루틴 안티패턴 12건, Flow 최적화 5건. IDEA 2026.1 새 인스펙션을 제 안드로이드 사이드 프로젝트에 돌리고 나온 숫자입니다.
6년차가 짠 코드인데도요.
정식 출시된 IntelliJ IDEA 2026.1 Ultimate 를 깔고, 평소처럼 사이드 프로젝트 폴더를 열었어요. 30초쯤 지나니까 우측 거터에 노란 줄이 쫙 깔리더라고요. "이거 다 새로 잡힌 건가?" 싶어서 Problems 탭을 열어보니 17건. 잠시 멍해졌습니다.
IDEA 2026.1 코틀린 코루틴 인스펙션 12종, 어디까지 잡아주나
새 인스펙션은 제 나름대로 크게 세 그룹으로 정리됩니다.
1. 취소·구조적 동시성 (4종)
suspendCoroutine 이 취소를 보장하지 못하는 지점, CoroutineContextWithJob(Job 이 든 컨텍스트를 빌더에 넘기는 케이스), suspend 함수 내부의 runBlocking, 그리고 암시적 CoroutineScope 리시버 캡처. 이 네 가지가 부모-자식 Job 관계를 끊거나 스레드를 막아버리는 지점을 짚어줍니다.
2. 성능·가독성 (7종)
Async without Await, forEach { it.join() } → joinAll(), map { it.await() } → awaitAll() 같은 병렬화 제안에다, Flow 체인 최적화 3종까지. map{}.filterNotNull()을 mapNotNull{} 한 줄로 합치는 게 가장 자주 잡혔어요.
3. 최신 관용구 (1종)
kotlin.coroutines.coroutineContext 프로퍼티 대신 kotlinx.coroutines.currentCoroutineContext() 함수를 쓰라는 권장입니다. 인라인 람다 안에서 오래된 컨텍스트가 캡처되는 케이스를 막아주는 셈이지요.

안드로이드 사이드 프로젝트에서 잡힌 코루틴 안티패턴 12건
부끄럽지만 분류해보면 이렇게 됩니다.
- Scope 오용 5건. ViewModel 바깥에 둔 GlobalScope.launch 로깅 코드, Activity에서 만들고 cancel() 안 부른 CoroutineScope(Dispatchers.IO) 가 대표 사례였습니다.
- Dispatcher 누락 4건. Room의 suspend DAO를 부른 다음에 추가 직렬화 로직이 Main 스레드에서 그대로 도는 부분이 잡혔어요.
- suspend 함수 내부 GlobalScope 3건. "fire and forget" 로깅이라고 그냥 둔 코드에 Async without Await 까지 겹쳐서 한 곳에 경고 세 개가 동시에 떴습니다.
특히 1번이 뼈아팠어요. "Implicit CoroutineScope receiver" 경고가 처음 떴을 때, 제가 람다 안에서 바깥 스코프 리시버를 무심코 끌어다 쓴 지점을 한 번에 짚어주더라고요. 6년 동안 못 본 자리였습니다.
// before — 경고 3개 동시 발생
suspend fun logEvent(event: Event) {
GlobalScope.async { // Async without Await
api.send(event) // Deferred 결과 무시
}
}
// after
suspend fun logEvent(event: Event) = withContext(Dispatchers.IO) {
runCatching { api.send(event) }
}

Flow 최적화 5건과 Quick Fix 자동 적용 범위
Flow 쪽은 의미론이 동일한 자동 수정이 많아서 부담이 적었습니다.
- map { ... }.filterNotNull() → mapNotNull { ... } 병합 2건
- 리팩토링 잔재로 남아 있던 빈 map 같은 중복 연산자 제거 1건
- flow { emit(...) }.flatMapConcat, .toList().first() 같은 복잡한 체인을 flowOf, firstOrNull로 단순화 2건
마지막 두 건이 압권이었네요. 8줄짜리 Flow 체인이 3줄로 줄어들었습니다. 가독성만 좋아진 게 아니라 중간 Flow 객체 할당도 사라지는 부분이에요.
awaitAll() 자동 변환은 절대 그냥 누르지 마세요
여기서 한 박자 쉬어가야 합니다. map { it.await() } → awaitAll() 자동 변환은 의미가 살짝 달라져요.
awaitAll() 은 자식 중 하나가 실패하면 즉시 예외를 던집니다. (구조적 동시성 안에 있다면 부모 스코프가 형제 자식들까지 취소해주는 식이고요.) 반면 map { it.await() } 은 첫 Deferred 를 기다리는 동안 두 번째 예외가 즉시 노출되지 않을 수 있어요. try-catch 위치가 어긋나 있으면 동작이 바뀌는 케이스입니다.
저도 한 번 자동 적용 눌렀다가, 기존 단위 테스트 두 개가 빨갛게 변하는 걸 보고 멘붕이 오더라고요. 예외 처리 의도가 바뀐 거였습니다. 의미론이 다른 Quick Fix는 diff를 한 줄 한 줄 읽고 적용하는 게 안전합니다.

안드로이드 스튜디오 반영 시차와 6년차 우선순위
현재 안정 빌드인 Android Studio Panda 4(2025.3.4) 와 프리뷰인 Quail(2026.1.x) 모두 인스펙션 반영 시점은 공식 공지가 별도로 없는 상태입니다. 우선은 IDEA Ultimate 에서 안드로이드 모듈을 열어 검사하는 쪽이 빠릅니다.
개인적으로는 12종을 다 켜기보다 세 개 먼저 활성화하는 쪽을 권합니다. RunBlocking in suspend function, Async without Await, CoroutineContextWithJob. 이 셋이 실제 운영 장애로 이어졌던 경험과 가장 정확히 일치하는 지점이에요. 팀에 도입하실 거면 joinAll/awaitAll 자동 변환은 PR diff가 폭증하고 동작이 바뀌니까, 코드 리뷰 필수 항목으로 컨벤션에 박아두시는 게 안전합니다.
마치며
문제가 17개 떴을 때, 솔직히 6년이 헛수고였나 싶었어요. 근데 한 시간쯤 고치다보니까 생각이 달라졌습니다. 이건 제 코드가 형편없었던 게 아니라, 2020년에 베스트였던 패턴과 2026년에 권장되는 패턴이 다른 자리였어요. 정적 분석 도구가 그 간격을 메워주는 거였습니다.
다음 EAP에서는 어떤 인스펙션이 또 추가될지, 그때도 한 시간 정도는 마음 비우고 노란 줄 따라갈 생각이에요.
'Android 개발 > Kotlin' 카테고리의 다른 글
| Kotlin 2.4 RC context parameters로 LoggerContext 6인자 날린 후기 (0) | 2026.06.10 |
|---|---|
| KMP 2.3 production-ready 선언, 6년차 안드러가 본 진짜 도입선 (0) | 2026.06.02 |
| Kotlin 2.3.20 name-based destructuring, 안드 코드 7곳에 끼워본 일주일 (0) | 2026.06.01 |
| Kotlin 2.4.0-RC 풀렸다 — context parameters 안정화·name-based destructuring 본인 코드에 한 시간 적용 (0) | 2026.05.27 |
| 코루틴 처음 만났을 때 가장 헷갈렸던 것들 (0) | 2026.05.04 |