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 프로젝트와 기술 면접 모두에서 큰 경쟁력을 갖출 수 있습니다. 꾸준히 실습하고 고급 패턴도 적극적으로 활용해 보시기 바랍니다.

작성자
Anthony Fillion-Maillet풀스택 개발자, SharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 4월 2일 업데이트
태그
공유
관련 기사

Kotlin Flow vs StateFlow vs SharedFlow: 2026 안드로이드 면접 질문
2026년 안드로이드 면접관이 묻는 Kotlin Flow vs StateFlow vs SharedFlow 질문을 명확한 답변, 비교 표, 실무용 코드와 함께 정리했습니다.

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

Android Room Database 2026 완전 가이드: 마이그레이션, 관계 및 Coroutines 연동
Room Database의 마이그레이션, 엔티티 관계, Kotlin Coroutines 및 Flow와의 통합을 상세히 다룹니다. 2026년 프로덕션 환경에 적합한 패턴을 포함한 실전 튜토리얼입니다.