모바일 개발의 멀티스레딩 및 동시성: 개념, 원리 및 작동 방식

저자: IT Sectr 게시일: 2026-03-12 읽는 시간: 13 분

모든 모바일 애플리케이션은 네트워크에서 데이터 로드, 사용자 터치 처리, 인터페이스 애니메이션, 파일 저장 등 여러 작업을 동시에 수행합니다. 이 모든 코드가 단일 스레드에서 실행되면 네트워크 지연이 발생할 때마다 애플리케이션이 멈춥니다. 멀티스레딩과 동시성(concurrency)은 애플리케이션이 반응성과 효율성을 유지할 수 있게 하는 핵심 개념입니다. 이 기사에서는 Main Thread와 RunLoop부터 Kotlin 코루틴과 iOS의 Combine까지 모든 주요 도구를 다룹니다. 내용은 Apple 공식 GCD 문서를 기반으로 합니다.

주요 포인트

  • Main Thread — UI 작업을 위한 유일한 스레드; 다른 모든 작업은 Background로 이동
  • GCD 및 OperationQueue — iOS의 주요 멀티스레딩 메커니즘
  • Coroutines 및 Flow — Kotlin/Android의 현대적인 비동기 표준
  • RxJava, RxSwift 및 Combine — 데이터 스트림 작업을 위한 리액티브 프레임워크
  • Race Condition, Deadlock 및 Livelock — 동기화가 필요한 고전적인 멀티스레딩 문제
  • 도구 선택은 플랫폼과 작업 복잡성에 따라 달라짐: 간단한 호출에는 Async/Await으로 충분, 복잡한 스트림에는 Rx 또는 Combine

멀티스레딩이란?

멀티스레딩은 애플리케이션이 여러 코드 조각을 동시에 실행할 수 있는 능력입니다. 각 조각은 별도의 스레드(Thread)에서 실행됩니다 — 자체 호출 스택을 가진 경량 프로세스입니다. 모바일 개발에서 스레드는 Main Thread(UI 스레드)와 Background Threads(백그라운드 스레드)의 두 범주로 나뉩니다.

운영 체제 자체가 프로세서 코어 간 스레드 분배를 관리합니다. 최신 기기에는 6~8개의 코어가 있어 병렬 실행이 실제로 작업 속도를 높일 수 있습니다. 그러나 스레드 생성은 비용이 많이 드는 작업이므로 Thread를 직접 작업하는 것은 권장되지 않습니다. 대신 더 높은 수준의 추상화인 DispatchQueue, OperationQueue, CoroutineDispatcher가 사용됩니다.

동시성(Concurrency)은 멀티스레딩보다 더 넓은 개념입니다. 동시성은 컨텍스트 스위칭을 통해 단일 코어에서도 작업을 "동시에" 실행할 수 있음을 의미합니다. 비동기성(Async/Await)은 작업이 스레드를 차단하지 않고 결과를 기다리는 동안 제어권을 반환하는 프로그래밍 모델입니다. 최신 언어(Kotlin, Swift, Dart)에는 Async/Await이 내장되어 있습니다.

IT Sectr에서는 프로젝트 시작 시 올바른 멀티스레딩 아키텍처에 특별한 주의를 기울입니다. 초기 단계에서 발생한 실수는 데이터 경합, 데드락, 부하 시 애플리케이션 불안정성 등 찾기 어려운 버그로 이어집니다. 당사의 모든 프로젝트는 계획 단계에서 동시성 아키텍처 검토를 거칩니다.

메인 스레드 및 백그라운드 스레드

Main Thread(메인 스레드)는 모바일 애플리케이션에서 UI에 접근할 수 있는 유일한 스레드입니다. Android에서는 UI Thread, iOS에서는 Main Thread라고 합니다. 모든 인터페이스 작업(텍스트 변경, 애니메이션, 터치 처리)은 Main Thread에서만 수행됩니다. 메인 스레드에서 무거운 작업(파일 로드, JSON 파싱)이 실행되면 인터페이스가 응답을 멈춥니다. Android에서는 ANR(Application Not Responding)이 발생하고, iOS에서는 화면이 "멈춥니다".

Background Threads(백그라운드 스레드)는 UI와 관련 없는 모든 작업(네트워크 요청, 데이터베이스 작업, 이미지 처리, 암호화)을 위해 설계되었습니다. 완료 후 결과는 표시를 위해 Main Thread로 전달됩니다. 각 플랫폼은 스레드 간 전환을 위한 자체 도구를 제공합니다: iOS의 DispatchQueue.main.async, Android의 runOnUiThread 또는 withContext(Dispatchers.Main).

RunLoop — iOS 메인 스레드의 이벤트 처리 루프입니다. RunLoop는 이벤트(터치, 타이머, 알림)를 기다렸다가 적절한 핸들러로 디스패치합니다. Android의 해당 개념은 각 Main Thread와 연결된 Looper입니다. Main Looper는 큐에서 메시지를 무한히 추출하여 처리를 위해 Handler에 전달합니다. RunLoop와 Looper를 이해하면 메모리 누수와 인터페이스 "끊김"을 방지하는 데 도움이 됩니다.

GCD 및 OperationQueue (iOS)

Grand Central Dispatch(GCD)는 C 언어 수준에서 멀티스레딩을 관리하기 위한 Apple 라이브러리입니다. GCD는 DispatchQueue(작업 큐)와 함께 작동합니다. 개발자는 수동으로 스레드를 생성하지 않으며, GCD가 스레드 풀을 관리하여 사용 가능한 프로세서 코어에 작업을 분배합니다. DispatchQueue에는 두 가지 유형이 있습니다: Serial Queue(직렬 큐 — 작업이 순차적으로 실행)와 Concurrent Queue(동시 큐 — 작업이 동시에 실행 가능).

Main DispatchQueue는 메인 스레드에 바인딩된 직렬 큐입니다. Global Queues는 서로 다른 우선순위(QoS — Quality of Service)를 가진 동시 큐입니다: userInteractive, userInitiated, utility, background. 올바른 QoS 선택은 성능에 중요합니다: .userInteractive — UI에 영향을 주는 작업(애니메이션, 렌더링)용; .background — 시간에 중요하지 않은 작업(동기화, 캐시 정리)용.

OperationQueue는 추가 기능이 있는 GCD 위의 추상화입니다: 작업 취소, 작업 간 종속성 설정, 최대 동시 작업 수 제어. 작업은 Operation 클래스(또는 BlockOperation)의 객체입니다. 예: 이미지를 로드한 다음 필터를 적용하고 그 후에만 표시해야 하는 경우 — 종속성이 있는 OperationQueue가 완벽하게 처리합니다. GCD에서는 DispatchGroup 또는 세마포어를 사용하여 이러한 단계를 수동으로 동기화해야 합니다.

Swift 5.5+의 Async/Await — GCD의 현대적인 대안입니다. asyncawait 키워드는 비동기 코드를 선형적이고 읽기 쉽게 만듭니다. 함수는 async로 표시되고 호출은 await를 통해 대기됩니다. 시스템 자체가 컨텍스트 전환을 관리합니다: 기본적으로 async 함수는 백그라운드 스레드에서 실행되고 UI 업데이트는 MainActor에서 실행됩니다. @MainActor는 메인 스레드에서 코드 실행을 보장하는 속성입니다.

Coroutines 및 Flow (Kotlin)

Coroutines(코루틴)은 JetBrains가 개발한 Kotlin용 경량 스레드입니다. 일반 스레드와 달리 코루틴은 특정 Thread에 바인딩되지 않습니다. 수천 개의 코루틴이 큰 오버헤드 없이 여러 스레드에서 실행될 수 있습니다. CoroutineScope는 코루틴의 수명 주기를 관리합니다: viewModelScope는 ViewModel에, lifecycleScope는 Activity/Fragment에 바인딩됩니다. 범위가 소멸되면 모든 하위 코루틴이 자동으로 취소됩니다.

Dispatchers는 코루틴이 실행될 스레드 풀을 결정합니다: Dispatchers.Main — UI 스레드; Dispatchers.IO — 네트워크 요청 및 디스크 작업용; Dispatchers.Default — CPU 집약적 계산용. 디스패처를 전환하려면 withContext를 사용합니다. 코루틴은 구조적 동시성(structured concurrency)을 지원합니다: 각 코루틴에는 부모가 있으며, 부모가 취소되면 모든 하위 코루틴이 취소됩니다. 이는 메모리 누수와 중단된 작업을 방지합니다.

Flow — 코루틴 라이브러리의 콜드 비동기 데이터 스트림입니다. Flow는 값을 순차적으로 방출합니다: (1) 프로듀서가 데이터를 생성하고, (2) 연산자가 스트림을 변환하고, (3) 컬렉터가 결과를 소비합니다. LiveData와 달리 Flow는 복잡한 연산자 체인(map, filter, flatMapConcat, catch)을 지원하며 완전히 스레드 안전합니다. StateFlowSharedFlow는 Flow의 핫 변형으로, UI 상태 및 일회성 이벤트(Snackbar, 탐색)에 이상적입니다.

Channel — 코루틴 간 데이터 전달을 위한 또 다른 코루틴 추상화입니다. Channel은 큐처럼 작동합니다: 하나의 송신자(send)와 하나 이상의 수신자(receive). 버퍼링된 채널(Channel(UNLIMITED), Channel(BUFFERED))은 오버플로 시 동작을 구성할 수 있습니다. Channel은 콜백 기반 API를 코루틴으로 연결하기 위해 Flow와 함께 자주 사용됩니다: callbackFlow { … }.

IT Sectr에서는 모든 Android 프로젝트에서 코루틴과 Flow를 적극적으로 사용합니다. 이를 통해 동기처럼 보이는 비동기 코드를 작성할 수 있고, 테스트가 쉬우며(runTest, TestDispatcher), 수동 스레드 관리가 필요하지 않습니다. 데이터 로딩이 있는 간단한 코루틴 예제:

kotlin
class UserRepository(
    private val api: UserApi,
    private val dao: UserDao
) {
    suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
        return@withContext try {
            val users = api.fetchUsers()
            dao.insertAll(users)
            users
        } catch (e: Exception) {
            dao.getAll()
        }
    }
}

Rx 및 Combine

리액티브 프로그래밍 — 데이터가 비동기 스트림(Observable, Publisher)으로 전파되는 패러다임입니다. RxJava/RxKotlin은 Android에서 가장 인기 있는 구현으로, .NET Rx에서 포팅되었습니다. RxSwift는 iOS용 유사 라이브러리입니다. 주요 구성 요소: Observable(이벤트 소스), Observer(구독자), Scheduler(스레드 관리), Operators(스트림 변환).

Combine은 iOS 13에서 도입된 Apple의 리액티브 프로그래밍 프레임워크입니다. Combine은 Publisher(발행자) 및 Subscriber(구독자) 프로토콜을 사용합니다. RxSwift와 달리 Combine은 SDK에 내장되어 있으며 SwiftUI와 긴밀하게 통합됩니다. Combine의 연산자: map, filter, combineLatest, zip, debounce, throttle — 데이터 바인딩부터 검색어 디바운스까지 대부분의 시나리오를 다룹니다.

Future 및 Promise — 단일 비동기 결과 작업을 위한 패턴입니다. Future는 나중에 사용할 수 있는 값을 나타냅니다. Promise는 값을 제공하겠다는 약속입니다. Rx에서는 Single(하나의 성공적인 응답 또는 오류)이고, Combine에서는 Future Publisher입니다. 실제로 Future/Promise는 단일 API 요청에 편리하고, Observable/Publisher는 연속적인 스트림(위치 정보, 텍스트 입력)에 적합합니다.

Callback 및 Delegate — 비동기 작업을 위한 고전적인 패턴입니다. Callback은 인수로 전달되어 작업 완료 시 호출되는 함수입니다. Delegate는 이벤트 처리기 메서드가 있는 프로토콜을 구현하는 객체입니다. 단점: "콜백 지옥"(중첩된 콜백)과 오류 처리의 복잡성. NotificationCenter(iOS) 및 EventBus(Android)는 브로드캐스트 이벤트 메커니즘으로, 느슨하게 결합된 통신에 유용하지만 암시적 종속성으로 이어집니다.

멀티스레딩 문제 (Race Condition, Deadlock)

멀티스레딩은 높은 성능의 문을 열지만 동시에 찾기 어려운 오류의 위험을 만듭니다. 가장 일반적인 것: Race Condition(경합 조건), Deadlock(교착 상태), Livelock(활성 교착 상태) 및 Starvation(기아). 이러한 문제를 이해하는 것은 모든 모바일 개발자에게 필수적인 기술입니다.

Race Condition

Race Condition은 둘 이상의 스레드가 동기화 없이 동일한 데이터를 동시에 읽고 쓸 때 발생합니다. 결과는 어떤 스레드가 먼저 실행되는지에 따라 달라집니다. 고전적인 예: 두 스레드가 카운터를 증가시킵니다. "읽기 → 증가 → 쓰기" 작업은 원자적이지 않으므로 동시에 실행되면 하나의 증가분이 "손실"됩니다. 해결책 — 원자적 연산(AtomicInteger, AtomicReference) 또는 잠금(Mutex, Semaphore, synchronized)을 사용하는 것입니다.

Deadlock

Deadlock — 각 스레드가 리소스를 보유하고 다른 스레드가 보유한 리소스를 기다리는 상황입니다. 어떤 스레드도 진행할 수 없습니다. 발생 조건: 상호 배제, 보유 및 대기, 선점 불가, 순환 대기. 방지: 잠금 획득의 단일 순서 설정, 타임아웃이 있는 tryLock 사용, Lock-Free 알고리즘(ConcurrentHashMap, CopyOnWriteArrayList) 적용.

Livelock 및 Starvation

Livelock — 스레드가 차단되지는 않았지만 유용한 작업 없이 지속적으로 서로 리소스를 "전달"합니다. 예: 복도에서 두 사람이 만나고 둘 다 같은 방향으로 이동하며 비켜줍니다. Starvation — 다른 스레드가 지속적으로 가로채기 때문에 스레드가 리소스에 액세스하지 못합니다. 해결책: 공정한 잠금(fair locks), 주의하여 스레드 우선순위 사용.

동기화 도구

멀티스레딩 문제를 방지하기 위해 동기화 프리미티브가 사용됩니다: Mutex(상호 배제), Semaphore(동시 액세스 수 제한), Lock(tryLock이 있는 인터페이스), Synchronized(JVM 수준 잠금), @MainActor(Swift — 메인 스레드에서 실행 보장). Android에서는 Executors.newFixedThreadPool, newCachedThreadPool을 통해 ThreadPool도 사용 가능합니다. 그러나 수동 풀 관리는 레거시 프로젝트의 특권이며, 새 프로젝트에서는 코루틴을 사용하는 것이 좋습니다.

도구 플랫폼 유형 특징
DispatchQueue (GCD)iOS작업 큐직렬/동시, QoS 우선순위, 스레드 풀 시스템 관리
OperationQueueiOS작업 큐종속성, 취소, maxConcurrentOperationCount
Coroutines + FlowAndroid코루틴경량, 구조적 동시성, StateFlow, Channel
RxJava / RxKotlinAndroid리액티브 스트림Observable, Schedulers, 풍부한 연산자 세트
CombineiOS리액티브 스트림Publisher/Subscriber, SwiftUI 통합
Async/Await + TaskiOS / Android비동기 모델선형 코드, @MainActor, 구조적 동시성

자주 묻는 질문

Main Thread와 Background Thread의 차이점은 무엇인가요?

Main Thread(UI 스레드)는 인터페이스 렌더링과 터치 처리를 담당합니다. Background Thread는 백그라운드 작업(데이터 로딩, 계산, 네트워크 작업)을 수행합니다. Main Thread 차단은 인터페이스 멈춤을 유발합니다(Android에서는 ANR, iOS에서는 frozen UI).

Race Condition이란 무엇이며 어떻게 방지하나요?

Race Condition — 두 스레드가 동시에 공유 데이터에 접근하고 결과가 실행 순서에 따라 달라지는 경합 상태입니다. 동기화를 통해 방지: Mutex, Semaphore, Lock, Synchronized, @MainActor 또는 원자적 연산.

Coroutines 또는 RxJava: Android에서 무엇을 선택해야 하나요?

Coroutines는 Android의 현대적 표준입니다(JetBrains, Google 지원). RxJava/RxKotlin은 풍부한 연산자 세트를 가진 리액티브 접근 방식입니다. Coroutines는 비동기 호출에 더 간단하고, RxJava는 복잡한 데이터 스트림에 더 강력합니다. IT Sectr에서는 새 프로젝트에 Coroutines + Flow를 사용합니다.

Deadlock과 Livelock이란 무엇인가요?

Deadlock — 두 스레드가 서로의 리소스를 기다리는 상호 차단입니다. Livelock — 스레드가 차단되지는 않았지만 유용한 작업 없이 지속적으로 리소스를 전달합니다. 두 문제 모두 적절한 잠금 순서와 타임아웃으로 해결됩니다.

iOS에서 DispatchQueue가 왜 필요한가요?

DispatchQueue는 스레드 관리를 위한 Grand Central Dispatch(GCD)의 추상화입니다. Main Queue는 메인 스레드에서 작업을 실행하고, Global Queues는 백그라운드 스레드에서 실행합니다. Serial Queue는 순차적 실행을 보장하고, Concurrent Queue는 병렬 실행을 보장합니다. 현대 프로젝트에서는 GCD가 종종 Async/Await 및 Task로 대체됩니다.

요약

  • Main Thread — UI 전용; 다른 모든 작업은 Background에서
  • GCD 및 OperationQueue — iOS 멀티스레딩의 기초; Async/Await — 현대적 대안
  • Coroutines 및 Flow — Android 표준; 구조적 동시성이 누수 방지
  • RxJava, RxSwift, Combine — 복잡한 데이터 스트림을 위한 리액티브 프레임워크
  • Race Condition 및 Deadlock — 주요 문제; 잠금과 적절한 리소스 획득 순서로 해결
  • Thread Pool은 시스템(GCD) 또는 프레임워크(코루틴)에 의해 관리; 수동 스레드 생성은 권장되지 않음
  • 도구 선택은 플랫폼에 따라 다름: Android는 Coroutines, iOS는 GCD/Combine

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의