Kotlin 코루틴 완전 가이드 2026: Android 비동기 처리 마스터하기
Android 개발에 필수적인 Kotlin 코루틴을 기초부터 고급 패턴까지 체계적으로 학습합니다. suspend 함수, 스코프, 디스패처, Flow를 실전 코드와 함께 배울 수 있습니다.

Kotlin 코루틴은 Android 비동기 프로그래밍의 패러다임을 완전히 바꾸었습니다. 콜백 지옥과 AsyncTask의 시대는 지났습니다. 코루틴을 사용하면 비동기 코드를 동기 코드처럼 작성할 수 있으며, 성능과 유지보수성을 동시에 확보할 수 있습니다.
코루틴은 경량입니다(하나의 스레드에서 수천 개를 실행할 수 있습니다). 네이티브 취소를 지원하며, Jetpack과 Android 생태계에 완벽하게 통합됩니다.
기본 개념 이해하기
코드를 작성하기 전에, 코루틴이 왜 강력한지 먼저 이해해야 합니다.
코루틴이란 무엇인가
코루틴은 중단 가능한 연산의 인스턴스입니다. 스레드와 달리 코루틴은 블로킹하지 않습니다. 실행을 중단하고 해당 스레드를 다른 작업에 양보합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // Suspend without blocking
println("World!")
}
println("Hello,")
}
// Output: Hello, World!suspend 함수: 코루틴의 핵심
suspend 키워드는 해당 함수가 스레드를 블로킹하지 않고 코루틴의 실행을 중단할 수 있음을 나타냅니다.
suspend fun fetchUserData(userId: String): User {
return withContext(Dispatchers.IO) {
// Network call - runs on an IO thread
apiService.getUser(userId)
}
}
suspend fun fetchUserWithPosts(userId: String): UserWithPosts {
// Sequential execution
val user = fetchUserData(userId)
val posts = fetchUserPosts(userId)
return UserWithPosts(user, posts)
}핵심 규칙: suspend 함수는 다른 suspend 함수 내부 또는 코루틴 내부에서만 호출할 수 있습니다.
코루틴 스코프
스코프는 코루틴의 생명주기를 정의합니다. 메모리 누수를 방지하기 위해 반드시 이해해야 하는 개념입니다.
viewModelScope: ViewModel 전용 스코프
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepository.getUser(userId)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message)
}
}
}
}
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val user: User) : UserUiState()
data class Error(val message: String?) : UserUiState()
}lifecycleScope: Activity와 Fragment 전용 스코프
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UserUiState.Loading -> showLoading()
is UserUiState.Success -> showUser(state.user)
is UserUiState.Error -> showError(state.message)
}
}
}
}
}
}Android 애플리케이션에서 GlobalScope를 사용해서는 안 됩니다. GlobalScope로 실행된 코루틴은 생명주기에 바인딩되지 않으므로 메모리 누수를 유발합니다.
디스패처: 실행 스레드 제어
디스패처는 코루틴이 어떤 스레드에서 실행되는지 결정합니다.
4가지 주요 디스패처
// Main: main thread (UI)
viewModelScope.launch(Dispatchers.Main) {
textView.text = "UI update"
}
// IO: I/O operations (network, database)
viewModelScope.launch(Dispatchers.IO) {
val data = repository.fetchFromNetwork()
}
// Default: CPU-intensive computations
viewModelScope.launch(Dispatchers.Default) {
val result = heavyComputation(data)
}
// Unconfined: inherits caller context (rare use)withContext: 디스패처 전환
class ImageProcessor {
suspend fun processImage(bitmap: Bitmap): Bitmap {
return withContext(Dispatchers.Default) {
// CPU-intensive processing on Default
applyFilters(bitmap)
}
}
suspend fun saveToGallery(bitmap: Bitmap) {
withContext(Dispatchers.IO) {
// Disk write on IO
saveToFile(bitmap)
}
}
}
// Usage in ViewModel
viewModelScope.launch {
val processed = imageProcessor.processImage(originalBitmap)
imageProcessor.saveToGallery(processed)
// Automatic return to Main for UI update
_uiState.value = UiState.Success(processed)
}async/await를 활용한 병렬 실행
여러 작업을 병렬로 실행하고 결과를 조합하려면 async를 사용합니다.
suspend fun loadDashboard(): Dashboard {
return coroutineScope {
// Parallel launch
val userDeferred = async { userRepository.getUser() }
val statsDeferred = async { statsRepository.getStats() }
val notificationsDeferred = async { notificationRepository.getNotifications() }
// Await results
Dashboard(
user = userDeferred.await(),
stats = statsDeferred.await(),
notifications = notificationsDeferred.await()
)
}
}async를 사용하면 3개의 호출이 모두 병렬로 실행됩니다. 각 호출이 1초씩 걸린다면, 순차 실행의 3초가 아닌 약 1초만에 완료됩니다.
에러 처리
기본적인 try/catch
viewModelScope.launch {
try {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
} catch (e: HttpException) {
_uiState.value = UiState.Error("Server error: ${e.code()}")
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error")
}
}CoroutineExceptionHandler
class UserViewModel : ViewModel() {
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
_uiState.value = UiState.Error(throwable.message)
Timber.e(throwable, "Error in coroutine")
}
fun loadUser(userId: String) {
viewModelScope.launch(exceptionHandler) {
val user = userRepository.getUser(userId)
_uiState.value = UiState.Success(user)
}
}
}Result 래퍼 패턴
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
}
suspend fun <T> safeApiCall(apiCall: suspend () -> T): Result<T> {
return try {
Result.Success(apiCall())
} catch (e: Exception) {
Result.Error(e)
}
}
// Usage
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): Result<User> = safeApiCall {
api.getUser(id)
}
}
// In ViewModel
viewModelScope.launch {
when (val result = userRepository.getUser(userId)) {
is Result.Success -> _uiState.value = UiState.Success(result.data)
is Result.Error -> _uiState.value = UiState.Error(result.exception.message)
}
}Android 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
취소 처리: 리소스의 올바른 해제
코루틴은 협력적 취소를 지원합니다. 리소스 누수를 방지하기 위해 반드시 알아야 하는 메커니즘입니다.
스코프를 통한 자동 취소
class SearchViewModel : ViewModel() {
private var searchJob: Job? = null
fun search(query: String) {
// Cancel previous search
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300) // Debounce
val results = searchRepository.search(query)
_searchResults.value = results
}
}
}취소 상태 확인
suspend fun processLargeList(items: List<Item>) {
items.forEach { item ->
// Check if coroutine is cancelled
ensureActive()
processItem(item)
}
}
suspend fun downloadFiles(urls: List<String>) = coroutineScope {
urls.map { url ->
async {
try {
downloadFile(url)
} catch (e: CancellationException) {
cleanupPartialDownload(url)
throw e // Re-throw to propagate cancellation
}
}
}.awaitAll()
}CancellationException을 삼키지 마십시오: Exception을 캐치할 때 CancellationException은 반드시 다시 던져야 취소가 정상적으로 전파됩니다.
Flow: 리액티브 프로그래밍
Flow는 코루틴 기반의 RxJava Observable 대안입니다. 더 간결하며 코루틴과 네이티브로 통합됩니다.
Flow 생성과 수집
fun getUsers(): Flow<List<User>> = flow {
while (true) {
val users = userApi.getUsers()
emit(users)
delay(5000) // Poll every 5 seconds
}
}
// Flow from Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// Collecting in ViewModel
viewModelScope.launch {
userDao.getAllUsers()
.catch { e -> _uiState.value = UiState.Error(e.message) }
.collect { users ->
_uiState.value = UiState.Success(users)
}
}StateFlow와 SharedFlow 비교
class EventViewModel : ViewModel() {
// StateFlow: keeps last value, ideal for UI state
private val _uiState = MutableStateFlow(UiState.Initial)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// SharedFlow: for one-shot events (navigation, snackbar)
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onButtonClick() {
viewModelScope.launch {
_events.emit(UiEvent.NavigateToDetail)
}
}
}
sealed class UiEvent {
object NavigateToDetail : UiEvent()
data class ShowSnackbar(val message: String) : UiEvent()
}주요 Flow 연산자
userRepository.getUsers()
.map { users -> users.filter { it.isActive } }
.distinctUntilChanged()
.debounce(300)
.flatMapLatest { users ->
fetchUserDetails(users)
}
.catch { e ->
emit(emptyList())
}
.onEach { users ->
analytics.logUserCount(users.size)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)고급 패턴
지수 백오프 재시도
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
Timber.w("Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
// Usage
val user = retryWithBackoff {
userApi.getUser(userId)
}타임아웃
suspend fun fetchWithTimeout() {
try {
val result = withTimeout(5000L) {
api.fetchData()
}
processResult(result)
} catch (e: TimeoutCancellationException) {
showError("Request took too long")
}
}
// Or with a default value
val result = withTimeoutOrNull(5000L) {
api.fetchData()
} ?: defaultValue결론
Kotlin 코루틴은 현대 Android 개발에 필수적인 기술입니다. Jetpack 생태계와 완벽하게 통합된 우아하고 고성능의 비동기 프로그래밍 방식을 제공합니다.
체크리스트
- 메모리 누수 방지를 위해
viewModelScope와lifecycleScope를 사용합니다 - 적절한 Dispatcher를 선택합니다 (Main, IO, Default)
- try/catch 또는 Result 래퍼로 에러를 처리합니다
- 협력적 취소를 구현합니다
- 리액티브 데이터 스트림에는 Flow를 사용합니다
- UI 상태에는 StateFlow, 이벤트에는 SharedFlow를 선택합니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
코루틴을 마스터하면 Android 프로젝트와 기술 면접 모두에서 큰 경쟁력을 갖출 수 있습니다. 꾸준히 실습하고 고급 패턴도 적극적으로 활용해 보시기 바랍니다.
태그
공유
관련 기사

Kotlin 2.3 Android 면접 완벽 가이드: 이름 기반 구조 분해, KMP, 핵심 질문 정리 2026
2026년 Android 개발자 면접에서 출제되는 Kotlin 2.3 신기능을 상세히 분석합니다. 이름 기반 구조 분해, Kotlin Multiplatform, 컨텍스트 파라미터, Flow와 코루틴 코드 예제를 포함합니다.

Jetpack Compose: 고급 애니메이션 단계별 가이드
Compose 고급 애니메이션 완벽 가이드: 전환, AnimatedVisibility, Animatable, 제스처, 부드러운 Android 인터페이스를 위한 성능.

Jetpack Compose 면접 질문 20선 (2026년)
Jetpack Compose 면접에서 자주 출제되는 20가지 질문을 해설합니다. 리컴포지션, 상태 관리, 사이드 이펙트, 내비게이션, 성능 최적화, 아키텍처 패턴을 포괄적으로 다룹니다.