모바일 개발에서 Thread — 개념, 유형 및 스레드 관리

저자: IT Sectr 게시일: 2026-03-16 읽는 시간: 11 분

Thread — 프로세서 시간의 기본 단위로, 자체 스택을 가지고 다른 스레드와 독립적으로 실행됩니다. 모바일 개발에서 스레드는 긴 작업 중에도 인터페이스의 응답성을 유지하기 위해 병렬 실행에 사용됩니다. Android는 java.lang.Thread, Executors 및 Kotlin Coroutines를 지원하고, iOS는 Thread(Objective-C), GCD 및 OperationQueue를 지원합니다. Android Thread Documentation에 따르면 네이티브 스레드를 생성하려면 운영체제가 스택을 위해 약 1MB를 할당해야 합니다.

핵심 내용

  • Thread — CPU 스케줄링의 최소 단위: 각 스레드는 독립적이며 자체 스택을 가짐
  • 스레드 생성은 Android에서 약 1MB, iOS에서 512KB의 스택이 필요하므로 풀이 직접 생성보다 효율적
  • Android: Thread, Executors, HandlerThread, Coroutines — 4가지 스레드 추상화 수준
  • iOS: Thread(저수준), GCD(DispatchQueue), OperationQueue(고수준)
  • Thread safety — mutable 데이터 공유 접근에는 동기화 필요: locks, atomic, serial queues

Thread란

Thread(실행 스레드) — 운영체제가 CPU 코어에 스케줄할 수 있는 명령어의 독립적인 시퀀스입니다. 각 프로세스(애플리케이션)에는 최소 하나의 스레드인 Main Thread가 포함됩니다. 추가 스레드는 작업의 병렬 실행을 위해 생성됩니다. 각 스레드는 자체 프로그램 스택(지역 변수 포함), 프로그램 카운터(PC) 및 레지스터를 가집니다. 힙 메모리는 프로세스의 모든 스레드가 공유합니다.

모바일 OS에서는 스레드가 선점형 멀티태스킹(preemptive multitasking)으로 스케줄됩니다: OS는 언제든지 스레드 실행을 중단하고 다른 스레드에 제어권을 넘길 수 있습니다(컨텍스트 스위치). 컨텍스트 스위치는 CPU 레지스터 저장/복원, TLB 업데이트, 캐시 플러시가 필요하므로 비용이 큰 작업입니다(1-10마이크로초). 이것이 과도한 수의 스레드(수백, 수천)가 성능을 저하시키는 이유입니다 — OS가 실행보다 전환에 더 많은 시간을 소비합니다.

스레드와 프로세스 — 다른 개념입니다. 프로세스는 전용 가상 메모리를 가진 애플리케이션 인스턴스입니다. 프로세스 내의 스레드는 이 메모리를 다른 스레드와 공유합니다. Android에서 애플리케이션의 각 구성 요소(Activity, Service, BroadcastReceiver)는 하나의 프로세스에서 작동하지만 다른 스레드에서 실행될 수 있습니다. iOS 애플리케이션도 GCD 또는 Thread를 통해 추가 스레드를 생성할 수 있는 단일 프로세스입니다.

스레드의 생명주기: 상태와 전이

각 스레드는 Java/Kotlin(Android) 및 NSThread(iOS)에서 다섯 가지 상태를 거칩니다: New(생성됨), Runnable(실행 준비), Running(CPU에서 실행 중), Blocked/Waiting(리소스 또는 알림 대기), Terminated(종료됨). 상태 간 전이는 OS 스케줄러와 동기화 프리미티브에 의해 관리됩니다. 개발자는 스레드 우선순위(Thread.setPriority())와 상태(sleep, join, interrupt)에 영향을 줄 수 있습니다.

Android에서 스레드는 사용 중인 모니터(synchronized)를 획득하려 하거나 Object.wait() 또는 Thread.sleep()을 호출할 때 Blocked 상태가 됩니다. iOS에서는 NSCondition.wait(), pthread_cond_wait() 또는 dispatch_semaphore_wait() 호출 시 그렇습니다. Blocked 상태에서 스레드는 CPU를 소비하지 않지만 메모리(스택)를 점유합니다. 스레드는 다른 스레드에서 인터럽트(interrupted)되어 InterruptedException(Java) 또는 isCancelled(Kotlin Coroutines)를 받을 수 있습니다.

상태설명전이 메서드
New스레드 생성됨, 시작되지 않음Thread() 생성자
Runnable실행 준비 완료, CPU 대기thread.start()
RunningCPU 코어에서 실행 중OS 스케줄러
Blocked/Waiting리소스, 모니터 또는 알림 대기 중synchronized, wait(), sleep()
Terminatedrun() 완료 또는 인터럽트로 종료run() 완료, interrupt()

Context Switch와 그 비용

Context Switch(컨텍스트 스위치) — OS가 현재 스레드의 상태(레지스터, PC, TLB)를 저장하고 다른 스레드의 저장된 상태를 로드하는 작업입니다. 모바일 시스템(Linux + ART, iOS의 XNU)에서 컨텍스트 스위치는 1-10마이크로초가 걸립니다. 스레드가 100마이크로초에 작업을 수행하고 컨텍스트 스위치에 5마이크로초가 걸리면 5%의 시간이 낭비됩니다. 컨텍스트 스위치를 최소화하기 위해 iOS는 work stealing이 있는 GCD를 사용하고, Android는 fixedThreadCount가 있는 풀을 사용합니다.

Android의 Thread: Thread에서 Coroutines까지

Android는 저수준 java.lang.Thread에서 현대적인 코루틴으로 진화했습니다. 각 추상화 수준은 더 적은 오버헤드로 더 많은 기능을 제공합니다. Thread는 기본 클래스이지만 직접 생성은 권장되지 않습니다: 새 스레드가 풀에서 관리되지 않고 모니터링 및 취소가 어렵습니다. AsyncTask(API 30부터 deprecated)는 한 걸음 앞섰지만 메모리 누수와 불편한 구성 처리 문제가 있었습니다.

HandlerThread — Looper가 있는 Thread의 특별한 하위 클래스로, 메시지 큐를 처리할 수 있습니다. 백그라운드 스레드에서 Room이나 파일에 데이터 쓰기와 같은 순차적 작업 실행에 사용됩니다. HandlerThread는 start()를 호출하여 생성되며, 이후 Handler(handlerThread.looper)를 통해 메시지와 Runnable을 보낼 수 있습니다. handlerThread.quit() 호출은 Looper를 중지하고 스레드를 종료합니다.

kotlin
// Android: Thread, HandlerThread 및 Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors

class ThreadExample {

    // 1. 직접 Thread 생성(권장하지 않음)
    fun directThread() {
        val thread = Thread(Runnable {
            Thread.sleep(1000)
            print("Direct thread executed")
        })
        thread.start()
    }

    // 2. HandlerThread — 순차적 백그라운드 작업용
    fun handlerThreadExample() {
        val handlerThread = HandlerThread("BackgroundQueue")
        handlerThread.start()

        val handler = Handler(handlerThread.looper)
        handler.post {
            // 백그라운드 스레드에서 순차적 실행
            Thread.sleep(500)
            print("HandlerThread: 작업 완료")
        }

        // 스레드 중지(작업 완료 시 실행)
        handlerThread.quitSafely()
    }

    // 3. Executors — 스레드 풀
    fun executorExample() {
        val executor = Executors.newFixedThreadPool(4)
        for (i in 1..10) {
            executor.execute {
                print("Task $i on thread ${Thread.currentThread().getName()}")
            }
        }
        executor.shutdown()
    }

    // 4. Kotlin Coroutines — 현대적 표준
    suspend fun coroutineExample() = kotlinx.coroutines.withContext(
        kotlinx.coroutines.Dispatchers.Default
    ) {
        print("Coroutine on thread: ${Thread.currentThread().getName()}")
    }
}

예제 ThreadExample는 Android의 4가지 스레드 추상화 수준을 모두 보여줍니다. Thread 직접 생성은 가장 저수준이고 비효율적인 접근 방식입니다. HandlerThread는 백그라운드에서 순차적 작업에 유용합니다. Executors.newFixedThreadPool(4)는 최대 10개 작업의 병렬 실행을 위해 4개 스레드 풀을 생성합니다. Dispatchers.Default를 사용하는 Kotlin Coroutines는 현대적이고 효율적이며 안전한 방법입니다.

HandlerThread: 순차적 백그라운드 작업

HandlerThread — 내장 Looper와 메시지 큐를 가진 Thread의 특수화된 하위 클래스입니다. start()를 호출하여 생성되며, 이후 Handler(handlerThread.looper)를 통해 Runnable과 메시지를 보낼 수 있습니다. HandlerThread는 작업을 엄격히 순차적으로 실행합니다 — 이전 작업이 완료될 때까지 다음 작업이 시작되지 않습니다. 이는 작업 순서가 중요한 Room이나 파일에 데이터를 쓸 때 편리합니다. quitSafely() 호출은 현재 작업 완료 후 Looper를 중지합니다.

iOS의 Thread: Thread, GCD 및 OperationQueue

iOS도 스레드 작업의 세 가지 수준을 제공합니다. Thread(Swift의 Thread, Objective-C의 NSThread) — 네이티브 스레드를 직접 생성하는 저수준 API입니다. DispatchQueue를 통한 GCD(Grand Central Dispatch) — iOS 개발자를 위한 주요 도구로, 스레드 풀을 자동으로 관리합니다. OperationQueue — 종속성, 우선순위 및 취소를 지원하는 GCD 위의 고수준 추상화입니다.

현대 iOS 개발에서 Thread의 직접 사용은 극히 드뭅니다 — GCD가 자동 메모리 및 스레드 관리로 필요한 모든 기능을 제공합니다. Thread는 특정 경우에만 사용됩니다: 스레드 로컬 스토리지(threadDictionary) 설정, 백그라운드 스레드용 RunLoop 생성, pthread_t를 기대하는 C 라이브러리와의 통합 등입니다.

swift
import Foundation

class ThreadManager {

    // 1. Thread(저수준)
    func createThread() {
        let thread = Thread {
            // 코드가 새 스레드에서 실행됨
            print("Current thread: \(Thread.current)")
        }
        thread.name = "com.app.worker"
        thread.qualityOfService = .utility
        thread.start()
    }

    // 2. GCD — DispatchQueue
    func gcdExample() {
        // 병렬 큐
        let queue = DispatchQueue(label: "com.app.concurrent",
                                 qos: .utility,
                                 attributes: .concurrent)

        queue.async {
            print("GCD async task")
        }

        // 쓰기 동기화를 위한 Barrier
        queue.async(flags: .barrier) {
            // 쓰기 중 독점 접근
            print("Barrier write: exclusive access")
        }
    }

    // 3. 종속성이 있는 OperationQueue
    func operationQueueExample() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 2
        queue.qualityOfService = .background

        let download = BlockOperation {
            print("Downloading...")
        }
        let process = BlockOperation {
            print("Processing...")
        }
        let save = BlockOperation {
            print("Saving...")
        }

        // 종속성: download -> process -> save
        process.addDependency(download)
        save.addDependency(process)

        queue.addOperations([download, process, save], waitUntilFinished: false)
    }
}

// GCD 배리어를 통한 스레드 안전 컬렉션
class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.app.concurrent",
                                       attributes: .concurrent)

    var count: Int {
        return queue.sync { array.count } // concurrent read
    }

    func append(_ element: T) {
        queue.async(flags: .barrier) { // exclusive write
            self.array.append(element)
        }
    }
}

클래스 ThreadSafeArray는 GCD 배리어를 통한 Concurrent Read / Exclusive Write 패턴을 보여줍니다. queue.sync{}를 통한 읽기는 여러 스레드에서 병렬로 실행됩니다. queue.async(flags: .barrier)를 통한 쓰기는 쓰기가 완료될 때까지 다른 모든 작업(읽기와 쓰기 모두)을 차단합니다. 이는 synchronized 블록보다 효율적인데, 쓰기가 없을 때는 읽기를 차단하지 않기 때문입니다.

iOS Thread vs GCD: Thread를 직접 사용해야 하는 경우

iOS에서 Thread를 직접 사용하는 것은 세 가지 경우에 정당화됩니다: 스레드 로컬 스토리지(Thread.current.threadDictionary) — 스레드에 바인딩된 데이터 저장; 백그라운드 스레드에서 performSelector:onThread:로 특수 RunLoop 생성; pthread_t를 기대하는 C/C++ 라이브러리와의 통합. 다른 모든 경우에는 DispatchQueue를 통한 GCD가 선호됩니다 — 스레드 풀과 전력 소비를 자동으로 관리합니다.

스레드 동기화: locks, atomic, serial queues

Race condition(경쟁 상태)은 두 개 이상의 스레드가 동시에 공유 데이터에 접근하고 최소한 하나의 스레드가 쓰기를 수행할 때 발생합니다. 결과는 실행 순서(타이밍)에 따라 달라지며 예측할 수 없습니다. 경쟁 상태를 방지하기 위해 동기화 프리미티브가 사용됩니다. 모바일 개발에서는 잠금(synchronized, NSLock), 원자적 연산(AtomicInteger, iOS atomic 속성), 큐(serial queue)를 사용할 수 있습니다.

프리미티브 선택은 시나리오에 따라 다릅니다. 단순한 카운터와 플래그에는 원자적 연산(AtomicInteger, atomic property)으로 충분합니다. 여러 작업이 있는 크리티컬 섹션에는 — 잠금(synchronized, NSLock). 복잡한 데이터 구조에는 — serial DispatchQueue 또는 GCD 배리어. 잠금은 이해하기 쉽지만 데드락과 라이브락에 취약합니다. 큐는 더 복잡하지만 더 안전합니다.

kotlin
// Android/Kotlin에서의 동기화
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Counter {

    // 1. AtomicInteger — 단순 카운터용
    private val atomicCount = AtomicInteger(0)
    fun incrementAtomic() = atomicCount.incrementAndGet()

    // 2. synchronized — 크리티컬 섹션용
    @Synchronized
    fun synchronizedOperation() {
        // 한 번에 하나의 스레드만
        doWork()
    }

    // 3. 코루틴의 Mutex — suspend-safe
    private val mutex = Mutex()
    suspend fun mutexOperation() {
        mutex.withLock {
            // 보호된 코드 — 스레드 안전
            doWork()
        }
    }

    private fun doWork() { /* critical section */ }
}

// 데드락 예제: A가 B를 잠금, B가 A를 잠금
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun methodA() = synchronized(lockA) {
        Thread.sleep(100)
        synchronized(lockB) { print("OK") }
    }

    fun methodB() = synchronized(lockB) {
        Thread.sleep(100)
        synchronized(lockA) { print("OK") }
    }
}

Counter는 동기화의 세 가지 접근 방식을 보여줍니다. AtomicInteger.incrementAndGet() — 잠금 없는 원자적 연산(CAS). @Synchronized — Java의 내장 모니터, 전체 객체를 잠급니다. Mutex.withLock — 코루틴 뮤텍스, 스레드를 차단하는 대신 코루틴을 일시 중단합니다(더 효율적). DeadlockExample은 클래식 데드락을 보여줍니다: 두 스레드가 다른 순서로 잠금을 획득합니다.

Thread Pools: Executors가 Thread보다 나은 이유

Thread Pool(스레드 풀) — 작업 실행을 위해 재사용되는 미리 생성된 스레드의 집합입니다. 각 작업마다 새 스레드를 생성하는(비용 큼) 대신 풀은 풀에서 여유 스레드를 가져옵니다. 여유 스레드가 없으면 작업은 큐에 들어갑니다. 풀은 자동으로 크기를 관리합니다: 피크 부하 시 새 스레드가 생성되고, 유휴 스레드는 종료됩니다. 이렇게 하면 스레드 생성 오버헤드가 수십 배 감소합니다.

Android에서 Executors.newFixedThreadPool(4)는 4개 스레드의 풀을 생성합니다. 동시에 10개의 작업이 오면 4개가 즉시 실행을 시작하고 6개는 큐에서 대기합니다. Executors.newCachedThreadPool()은 필요에 따라 스레드를 생성하고(무제한) 60초 후 유휴 스레드를 종료합니다. iOS의 경우 GCD가 CPU 코어 수와 현재 부하에 맞는 크기의 글로벌 큐 풀을 자동으로 제공합니다.

Kotlin Coroutines에서 스레드 풀은 디스패처 내부에 숨겨져 있습니다. Dispatchers.Default는 CPU 코어 수와 동일한 크기의 풀을 사용합니다(최소 2). Dispatchers.IO — 64개 스레드(수백 개의 IO 바운드 작업에 충분하며, 대부분 입출력을 기다리며 CPU를 점유하지 않음). 각 디스패처는 부하에 따라 자동으로 풀을 조정하여 유휴 시 배터리를 절약합니다.

자주 묻는 질문

모바일 개발에서 Thread란 무엇인가요?

Thread — 애플리케이션에서 코드 실행의 기본 단위입니다. 각 프로세스는 여러 스레드를 가질 수 있으며, 메모리를 공유하지만 자체 스택을 가집니다. 모바일 개발에서 스레드는 UI를 차단하지 않고 작업을 병렬 실행하는 데 사용됩니다. Android는 Thread, Executors, HandlerThread 및 Coroutines를 사용합니다. iOS는 Thread, GCD(DispatchQueue) 및 OperationQueue를 사용합니다.

Thread를 직접 생성하는 것이 권장되지 않는 이유는?

Thread 생성은 Android에서 약 1MB, iOS에서 약 512KB의 스택 할당이 필요하며 이는 비용이 큰 작업입니다. 1000개 작업에 대해 1000개 스레드를 직접 생성하면 스택만으로 약 1GB가 필요하며 컨텍스트 스위치 오버헤드가 추가됩니다. Thread 대신 풀(Executors, GCD) 또는 코루틴을 사용하세요 — 스레드를 재사용하여 오버헤드를 수십 배 줄입니다.

경쟁 상태(race condition)란 무엇이며 어떻게 방지하나요?

Race condition — 여러 스레드가 동시에 공유 데이터에 쓰기 접근할 때의 예측 불가능한 동작입니다. 세 가지 방법으로 방지할 수 있습니다: 원자적 유형(AtomicInteger) 사용, 잠금(synchronized, NSLock) 사용, 큐(serial DispatchQueue, Kotlin의 Actor)를 통한 접근 직렬화. 최선의 방법은 공유 mutable 상태를 최소화하고 불변성(immutability)을 사용하는 것입니다.

Thread와 코루틴의 차이점은?

Thread — 약 1MB의 스택을 차지하고 OS 코어에 바인딩된 네이티브 시스템 객체입니다. 코루틴 — 특정 스레드에 바인딩되지 않고 차단 없이 일시 중단(suspend)될 수 있는 Kotlin의 경량 실행 단위입니다. 하나의 스레드가 수천 개의 코루틴을 실행할 수 있습니다. 코루틴은 메모리 효율이 더 좋고 콜백 없이 비동기 코드를 작성할 수 있습니다.

모바일 앱에서 데드락을 어떻게 감지하나요?

Deadlock은 ANR 없이 애플리케이션이 완전히 멈추는 것으로 나타납니다. Android에서는 Thread.getAllStackTraces()를 사용하여 모든 스레드의 스택 덤프를 가져옵니다 — 두 스레드가 서로의 잠금을 기다리고 있을 것입니다. iOS에서는 — Thread.callStackSymbols. 도구: Android Studio Profiler(Threads 탭), Instruments(iOS, Thread State View). 예방: 고정된 순서로 잠금 획득, 타임아웃이 있는 tryLock 사용.

요약

  • Thread — CPU의 최소 단위: 자체 스택으로 독립 실행, 힙 메모리 공유
  • 다섯 가지 상태: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android의 진화: Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS 제공: Thread, GCD(DispatchQueue), OperationQueue — 저수준에서 고수준까지
  • Race condition은 잠금(synchronized, NSLock), 원자적 유형, serial 큐로 해결
  • Deadlock은 잠금 교차 획득으로 발생 — 고정된 순서로 방지
  • Thread Pool은 새 Thread 생성보다 효율적: 스레드 재사용, 컨텍스트 스위치 오버헤드 감소

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

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

프로젝트 논의

더 읽어보기