정보/레벨 1

iOS 앱에서 Multi-threading을 구현하는 방법은 무엇인가요?

밤새는 탐험가89 2024. 11. 13. 15:00

iOS 앱에서 **멀티스레딩(Multi-threading)**을 구현하는 방법으로는 GCD(Grand Central Dispatch), Operation 및 OperationQueue, 그리고 **Swift Concurrency (async/await)**가 주로 사용됩니다. 각 방법은 비동기 작업을 효과적으로 관리하고, 앱의 성능과 사용자 경험을 향상시키는 데 중요한 역할을 합니다.

 

1. Grand Central Dispatch (GCD)

GCD는 Apple에서 제공하는 멀티스레딩 API로, 간단하게 비동기 작업을 처리할 수 있습니다. 주로 비동기 큐와 동기/비동기 메서드를 사용하여 작업을 백그라운드에서 처리하고, 필요할 때 메인 스레드로 전환하는 방식으로 구현합니다.

  • DispatchQueue.main: 메인 큐, UI 업데이트는 항상 메인 큐에서 실행되어야 합니다.
  • DispatchQueue.global(): 백그라운드 큐, 네트워크 호출이나 데이터 처리 같은 CPU 집중 작업을 수행할 때 사용합니다.
DispatchQueue.global().async {
    // 백그라운드 작업 (예: 네트워크 호출, 데이터 처리)
    let result = performHeavyTask()
    
    DispatchQueue.main.async {
        // 메인 큐에서 UI 업데이트
        self.updateUI(with: result)
    }
}

 

위 코드에서는 performHeavyTask() 작업을 백그라운드 큐에서 실행하고, 결과를 메인 큐에서 UI에 반영하여 앱의 반응성을 유지합니다.

 

 

2. Operation 및 OperationQueue

Operation은 GCD를 기반으로 한 더 높은 수준의 API로, 작업 간의 종속성 설정이나 재사용이 가능한 작업을 객체 형태로 정의하는 등 GCD보다 더 많은 기능을 제공합니다. OperationQueue를 통해 여러 Operation을 큐에 넣어 비동기로 실행할 수 있으며, 큐의 우선순위와 동시성 처리를 조절할 수 있습니다.

  • Operation: 작업을 객체로 정의하여 관리할 수 있습니다.
  • OperationQueue: 여러 Operation 객체를 관리하고, 비동기 실행종속성 설정이 가능합니다.
let operationQueue = OperationQueue()

let operation1 = BlockOperation {
    // 작업 1
    print("Operation 1")
}

let operation2 = BlockOperation {
    // 작업 2
    print("Operation 2")
}

// 종속성 설정: operation1이 완료된 후 operation2가 실행됨
operation2.addDependency(operation1)

operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)

 

위 코드에서는 작업 간의 종속성을 설정하여 operation1이 완료된 후 operation2가 실행되도록 합니다.

 

 

3. Swift Concurrency (async/await)

Swift Concurrency는 Swift 5.5부터 도입된 비동기/동시성 처리를 위한 최신 기능으로, async와 await 키워드를 사용해 비동기 작업을 동기 코드처럼 간단하게 구현할 수 있습니다. TaskTaskGroup 등을 통해 멀티스레딩 작업을 보다 안전하고 직관적으로 처리할 수 있습니다.

  • async/await: 비동기 함수는 async 키워드로 선언되며, 호출 시 await 키워드로 호출하여 비동기 작업을 동기 코드처럼 처리할 수 있습니다.
  • Task: async 함수 외부에서 비동기 작업을 생성하는 방법입니다.
  • TaskGroup: 여러 작업을 동시에 처리하고, 완료 시 결과를 수집할 수 있습니다.
func fetchData() async -> Data {
    // 네트워크 호출 (예시)
}

func updateUI(with data: Data) {
    // UI 업데이트
}

Task {
    let data = await fetchData()
    await MainActor.run {
        updateUI(with: data)
    }
}

 

위 코드에서는 fetchData()를 비동기로 호출하고, UI 업데이트를 메인 스레드에서 수행하여 비동기 작업을 동기 코드처럼 간단하게 표현합니다.

 

선택 기준

  • 간단한 비동기 작업: GCD 사용.
  • 작업 간 종속성이나 재사용이 필요한 작업: Operation & OperationQueue 사용.
  • 최신 Swift Concurrency 기능 사용 가능(iOS 15 이상): async/await 사용.

 

 

동시성 프로그래밍에서 Race Condition을 방지하는 방법은 무엇인가요?

Race Condition여러 스레드가 동시에 동일한 자원에 접근하여 데이터의 일관성을 깨뜨리는 문제로, 동시성 프로그래밍에서 발생할 수 있는 주요 문제 중 하나입니다. Race Condition을 방지하기 위해 자원의 접근을 순차적으로 제어하거나 스레드 간의 접근을 동기화하는 다양한 방법이 있습니다.

 

DispatchQueue의 Serial Queue 사용

 직렬 큐(Serial Queue)는 하나의 스레드에서만 순차적으로 작업을 처리하므로, 동시에 여러 스레드가 접근하지 못하게 제어할 수 있습니다. 공통 자원에 대한 접근을 직렬 큐로 제한하면 Race Condition을 방지할 수 있습니다.

let serialQueue = DispatchQueue(label: "com.example.serialQueue")

serialQueue.async {
    // 자원에 접근하는 작업
    sharedResource += 1
}

 

이 코드는 sharedResource에 접근하는 작업을 직렬 큐에서 수행하여 동시에 다른 스레드에서 접근하는 것을 방지합니다.

 

Dispatch Semaphore 사용

**세마포어(Semaphore)**는 특정 자원에 접근할 수 있는 스레드 수를 제한하는 방식으로, 여러 스레드가 동일 자원에 접근하는 것을 조절할 수 있습니다. 동시에 하나의 스레드만 자원에 접근할 수 있도록 세마포어를 1로 설정하여 Race Condition을 방지합니다.

let semaphore = DispatchSemaphore(value: 1)

func updateSharedResource() {
    semaphore.wait() // 접근 시작
    sharedResource += 1
    semaphore.signal() // 접근 종료
}

 

semaphore.wait()로 접근을 시작하고, 작업이 끝나면 semaphore.signal()로 다른 스레드가 자원에 접근할 수 있게 합니다. 이를 통해 하나의 스레드가 자원에 접근하는 동안 다른 스레드는 대기합니다.

 

 

Swift의 Actor 사용 (Swift Concurrency)

Swift 5.5부터 도입된 Actor 모델데이터를 보호하기 위한 스레드 안전한 방식을 제공합니다. Actor는 내부 데이터에 대한 접근을 단일 스레드에서만 처리하도록 제한해, 여러 스레드가 동시에 데이터에 접근하지 못하도록 합니다.

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

let counter = Counter()

Task {
    await counter.increment()
    print(await counter.getValue())
}

 

Actor 내부에서 value 프로퍼티는 Actor가 관리하여, 동시에 접근하는 Race Condition을 방지할 수 있습니다.

 

메인 스레드에서 UI 업데이트를 해야 하는 이유는 무엇인가요?

OS 앱에서 UI 업데이트는 반드시 메인 스레드에서 수행해야 하는 이유는 UIKit이 메인 스레드에서의 작업을 전제로 설계되었기 때문입니다. iOS는 메인 스레드를 UI 요소와 이벤트 처리의 전담 스레드로 사용하며, 이를 통해 UI 업데이트가 일관되고 안정적으로 이루어지도록 보장합니다.

이유 1: UIKit은 메인 스레드에서만 안전하게 작동

UIKit 프레임워크의 모든 UI 컴포넌트(예: UIView, UILabel, UIButton)는 메인 스레드에서 안전하게 작동하도록 설계되었습니다. 메인 스레드 이외의 스레드에서 UI를 업데이트하면 UIKit의 상태가 예기치 않게 변경될 수 있어 앱이 비정상적으로 동작하거나 충돌이 발생할 수 있습니다.

이유 2: 사용자 경험의 일관성 유지

메인 스레드는 UI와 사용자 이벤트 처리를 전담하므로, UI 업데이트가 메인 스레드에서 이루어져야 사용자가 보는 화면과 상호작용이 일관성을 유지합니다. 만약 메인 스레드 외부에서 UI를 업데이트한다면, 다른 스레드의 작업 타이밍에 따라 UI가 엉뚱한 시점에 업데이트되거나 화면 깜빡임이 발생할 수 있습니다.

이유 3: 데이터 레이스 및 동기화 문제 방지

UI 업데이트를 여러 스레드에서 동시에 수행하면 Race Condition이 발생할 수 있습니다. Race Condition은 여러 스레드가 동시에 동일한 자원에 접근하여 데이터의 일관성이 깨지는 문제로, 메인 스레드가 아닌 곳에서 UI를 변경하면 Race Condition이 발생할 가능성이 높아집니다. 메인 스레드에서만 UI 업데이트를 처리하면 이러한 동기화 문제를 예방할 수 있습니다.

메인 스레드에서 UI 업데이트하는 방법

메인 스레드에서 UI를 업데이트하기 위해 DispatchQueue.main.async 또는 OperationQueue.main.addOperation을 사용하여 작업을 메인 스레드에서 실행합니다.

DispatchQueue.global().async {
    let data = fetchData()  // 백그라운드 작업 (예: 네트워크 요청)

    DispatchQueue.main.async {
        self.updateUI(with: data)  // 메인 스레드에서 UI 업데이트
    }
}

요약

  • UIKit은 메인 스레드에서 안전하게 작동하도록 설계되었습니다.
  • 메인 스레드에서 UI를 업데이트하면 사용자 경험의 일관성을 유지할 수 있습니다.
  • 여러 스레드에서 UI를 접근하면 Race Condition이 발생할 수 있으므로, UI 업데이트는 메인 스레드에서만 수행해야 합니다.