동시성 프로그래밍은 여러 작업이 동시에 실행될 수 있도록 설계된 프로그래밍 기법입니다. 작업들이 정확히 동시에 실행되는 것은 아니더라도, 프로그램의 흐름 상에서 여러 작업이 병렬적으로 진행되는 것처럼 보이도록 설계됩니다. 동시성의 목적은 시스템 자원을 더 효율적으로 사용하고, 사용자 경험을 개선하기 위함입니다.
병렬 처리와 동시 처리의 차이
- 동시 처리(Concurrency): 여러 작업이 같은 시간에 실행될 수 있도록 하는 프로그래밍 기법입니다. 다만, 작업들이 반드시 동시에 실행되는 것은 아니며, CPU가 각 작업을 빠르게 전환하여 실행하는 것처럼 보일 수 있습니다.
- 병렬 처리(Parallelism): 실제로 여러 작업이 동시에 실행되는 것을 의미합니다. 병렬 처리는 다중 코어 CPU나 여러 장치에서 각 작업을 별도의 CPU 코어에서 독립적으로 실행할 수 있을 때 가능해집니다.
쉽게 말하면, 동시성은 작업의 스케줄링에 관한 것이고, 병렬성은 실제로 여러 작업이 동시에 실행되는 것을 의미합니다. 병렬성은 동시성의 한 부분으로, 하드웨어의 지원을 통해 이루어집니다.
iOS에서의 동시성 처리 방식
iOS는 동시성 처리를 위해 다양한 방법과 기술을 제공합니다. 주요한 동시성 처리 방식은 다음과 같습니다.
- Grand Central Dispatch (GCD)
- GCD는 Apple의 저수준 동시성 API로, 작업을 큐(queue)에 넣고 적절한 쓰레드에서 실행되도록 스케줄링해줍니다. GCD를 사용하면 복잡한 쓰레드 관리 없이, 병렬적인 작업을 쉽게 수행할 수 있습니다.
- GCD에서 사용할 수 있는 주요 큐는 다음과 같습니다:
- Main Queue: 메인 스레드에서 작업을 실행하는 큐로, UI 업데이트 작업은 반드시 메인 큐에서 처리되어야 합니다.
- Global Queues: 시스템에서 제공하는 전역 큐로, 백그라운드에서 병렬 작업을 실행할 수 있습니다. 우선순위에 따라 여러 큐가 제공됩니다 (e.g., high, default, low).
- Custom Queues: 개발자가 직접 만든 큐로, 특정 작업을 직렬적으로 처리하거나 병렬적으로 처리할 수 있습니다.
- GCD는 비동기(Asynchronous) 및 동기(Synchronous) 작업 처리를 지원하며, 비동기 작업은 작업이 완료될 때까지 현재 실행 흐름을 블록하지 않습니다.
- Operation Queue
- Operation 및 OperationQueue는 GCD보다 더 상위 수준의 동시성 API입니다. GCD보다 더 많은 제어권을 제공하며, 작업 간의 의존성 설정, 작업 취소, 우선순위 설정 등 다양한 기능을 사용할 수 있습니다.
- OperationQueue는 비동기 작업을 관리하는 데 적합하며, GCD와 달리 작업의 상태를 추적할 수 있는 장점이 있습니다.
- Async/Await (iOS 13 이상)
- Swift에서는 **비동기 함수(async/await)**를 사용하여 동시성을 처리할 수 있습니다. 이는 기존의 콜백 기반 비동기 처리 방식을 대체하는 직관적이고 깔끔한 코드 구조를 제공합니다.
- async/await를 사용하면 비동기 작업을 마치 동기 작업처럼 다룰 수 있어 코드 가독성을 크게 향상시킵니다.
func fetchData() async throws -> Data {
let url = URL(string: "https://example.com")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
iOS에서 멀티코어 활용 방안
iOS 기기는 대부분 멀티코어 CPU를 가지고 있기 때문에, 멀티코어를 활용하는 것이 동시성 프로그래밍에서 중요한 부분입니다. iOS는 여러 코어를 효과적으로 사용하여 애플리케이션의 성능을 높일 수 있는 다양한 방법을 제공합니다.
- GCD와 OperationQueue는 멀티코어 활용을 자동으로 처리합니다. iOS는 작업을 여러 CPU 코어에 분산시켜, 병렬로 실행할 수 있는 작업들을 나눠 처리합니다.
- QoS(Quality of Service): GCD와 OperationQueue는 작업의 중요도에 따라 작업을 스케줄링합니다. 시스템은 각 작업의 우선순위와 장치의 상태를 기반으로 멀티코어에서 최적화된 방식으로 작업을 분배합니다.
- 예: 사용자 인터페이스 업데이트는 메인 큐에서, 백그라운드 데이터 처리 작업은 글로벌 큐에서 실행됩니다.
멀티코어를 활용하는 방식은 기본적으로 iOS가 제공하는 동시성 API를 사용할 때 자동으로 이루어지며, 개발자는 작업의 우선순위를 적절히 설정하고 적합한 큐에서 작업을 실행하기만 하면 됩니다.
외부에서 데이터를 받아와서 UI 요소에 반영하는 상황에서는 비동기적으로 데이터를 처리하고 UI 업데이트를 할 수 있도록 동시성 프로그래밍이 활용됩니다. 이 과정에서 UI와 네트워크 통신은 각각 별도의 작업으로 처리되어, UI가 멈추지 않도록 설계하는 것이 중요합니다. 일반적으로 iOS에서 이 작업을 처리하는 방식은 다음과 같습니다
1. 네트워크 요청을 비동기적으로 처리
네트워크 요청(예: API 호출)은 시간이 걸릴 수 있는 작업이므로 메인 스레드에서 직접 실행하면 앱이 멈추거나 느려질 수 있습니다. 이 때문에 네트워크 작업은 백그라운드에서 실행하고, 데이터가 도착한 후에 UI 업데이트는 메인 스레드에서 수행됩니다.
예시 1: GCD를 사용한 비동기 네트워크 요청
DispatchQueue.global(qos: .background).async {
// 백그라운드에서 네트워크 요청
let data = fetchDataFromNetwork()
DispatchQueue.main.async {
// 데이터를 받은 후 메인 스레드에서 UI 업데이트
self.label.text = data.title
}
}
- DispatchQueue.global(qos: .background).async: 네트워크 요청을 백그라운드 스레드에서 실행하여, 메인 스레드에서 다른 UI 작업들이 계속 실행될 수 있도록 합니다.
- DispatchQueue.main.async: UI 요소 업데이트는 반드시 메인 스레드에서 수행해야 하므로, 네트워크 요청이 끝난 후에 메인 큐에서 UI를 업데이트합니다.
예시 2: async/await을 사용한 비동기 처리 (iOS 13 이상)
func updateUIWithNetworkData() async {
do {
let data = try await fetchDataFromNetwork()
// 메인 스레드에서 UI 업데이트
self.label.text = data.title
} catch {
print("Failed to fetch data")
}
}
Task {
await updateUIWithNetworkData()
}
- await fetchDataFromNetwork(): 비동기적으로 네트워크 데이터를 받아옵니다.
- UI 업데이트는 따로 명시하지 않아도 자동으로 메인 스레드에서 실행되기 때문에 DispatchQueue.main을 명시할 필요가 없습니다.
2. 동시성의 목적
위 예시에서 볼 수 있듯이, 동시성 프로그래밍을 사용하는 이유는 네트워크 작업처럼 시간이 오래 걸리는 작업이 UI 업데이트를 방해하지 않도록 하는 데 있습니다. 네트워크 요청은 시간이 걸리지만, 그동안 사용자는 UI에서 다른 작업을 할 수 있고, 작업이 완료된 후에 UI 요소에 데이터를 반영합니다.
3. 동시성 처리 시 주의점
- UI 업데이트는 항상 메인 스레드에서 해야 합니다. 비동기적으로 데이터를 받아오는 작업은 백그라운드에서 처리하더라도, UI 관련 작업은 메인 큐에서 실행되어야 합니다. 그렇지 않으면 앱이 크래시하거나 UI가 제대로 반영되지 않습니다.
- 경합 조건(Race Conditions): 여러 스레드에서 동시에 데이터를 수정할 경우 예상치 못한 결과가 발생할 수 있으므로, 필요한 경우 동기화 처리 또는 순서 제어가 필요합니다.
4. 동시성 처리의 활용 시나리오
- 데이터 로드 및 화면 반영: 외부에서 데이터를 받아와 테이블뷰, 컬렉션뷰, 또는 특정 UI 요소에 반영할 때.
- 이미지 다운로드 및 표시: 네트워크에서 이미지를 비동기적으로 다운로드한 후 UIImageView에 반영할 때.
- 사용자 입력과 백그라운드 작업 분리: 사용자가 UI 상에서 작업을 하는 동시에 백그라운드에서 네트워크 요청, 데이터 처리, 파일 저장 등의 작업이 진행되는 경우.
1. GCD의 큐 종류
- Main Queue:
- 메인 스레드에서 실행되는 작업을 처리합니다.
- UI 업데이트 작업은 반드시 메인 큐에서 실행해야 합니다.
- 예를 들어, 사용자 인터페이스의 버튼 클릭에 반응하여 UI 요소를 업데이트할 때 사용됩니다.
- Global Queue:
- 백그라운드에서 실행되는 작업을 처리합니다.
- 여러 가지 QoS(Quality of Service) 레벨이 있으며, 각각 우선순위에 따라 다르게 설정할 수 있습니다 (e.g., .userInteractive, .userInitiated, .utility, .background).
- CPU에 부하를 주지 않으면서 동시에 여러 작업을 처리할 수 있습니다.
차이점:
- 메인 큐는 UI 작업을 위한 것이고, 글로벌 큐는 비UI 작업을 위한 것입니다. 메인 큐는 단일 스레드에서 실행되기 때문에 UI가 중단되지 않도록 설계되어 있습니다. 반면, 글로벌 큐는 여러 스레드에서 병렬로 작업을 수행할 수 있어, CPU 자원을 효율적으로 활용할 수 있습니다.
2. 비동기 작업의 순서 관리
비동기 작업의 순서를 관리하기 위해 여러 가지 방법을 사용할 수 있습니다. 일반적으로 두 가지 접근 방식이 있습니다: **체이닝(Chaining)**과 Dispatch Group을 사용하는 것입니다.
예시 1: 체이닝 (Chaining)
체이닝 방식은 각 비동기 함수가 이전 작업이 완료된 후에 호출되는 구조입니다. 이 경우, 각 작업을 비동기적으로 수행하고 결과를 바탕으로 다음 작업을 실행합니다.
func fetchTouristAttractions(keyword: String) async -> [TouristAttraction] {
// "경기" 키워드로 관광지 정보 가져오기
}
func fetchNearbyRestaurants(for attractions: [TouristAttraction]) async -> [Restaurant] {
// 주어진 관광지 근처 맛집 정보 가져오기
}
func fetchData() async {
let attractions = await fetchTouristAttractions(keyword: "경기")
let restaurants = await fetchNearbyRestaurants(for: attractions)
// 결과를 UI에 업데이트
}
- await 키워드를 사용하여 각 함수가 완료될 때까지 기다린 후 다음 작업을 실행합니다. 이 방법은 각 작업의 순서를 명확히 할 수 있습니다.
예시 2: Dispatch Group
여러 비동기 작업을 동시에 실행하면서 결과를 집계할 필요가 있을 때는 Dispatch Group을 사용할 수 있습니다.
let group = DispatchGroup()
var attractions: [TouristAttraction] = []
var restaurants: [Restaurant] = []
group.enter()
fetchTouristAttractions(keyword: "경기") { result in
attractions = result
group.leave()
}
group.enter()
fetchNearbyRestaurants(for: attractions) { result in
restaurants = result
group.leave()
}
group.notify(queue: .main) {
// 모든 비동기 작업이 완료된 후 UI 업데이트
updateUI(attractions: attractions, restaurants: restaurants)
}
DispatchGroup을 사용하면 여러 작업을 동시에 수행하면서, 모든 작업이 완료될 때까지 기다릴 수 있습니다. notify 메소드를 사용하여 모든 작업이 끝난 후 UI 업데이트를 수행할 수 있습니다.
DispatchGroup은 여러 비동기 작업을 묶어서 동시에 처리할 때 매우 유용합니다. 이렇게 하면 각 작업이 완료될 때까지 기다렸다가, 모든 작업이 끝난 후에 필요한 후속 처리를 수행할 수 있습니다.
예시: 두 개의 컬렉션 뷰를 위한 Dispatch Group 사용
홈 화면에 두 개의 컬렉션 뷰가 있고, 각각 전국 기준 관광지 정보와 위치 기준 관광지 정보를 동시에 로드하고 싶을 때 DispatchGroup을 사용할 수 있습니다. 이 방법으로 두 비동기 작업이 모두 완료된 후에 UI를 업데이트하면 코드도 깔끔해집니다.
코드 예시
let group = DispatchGroup()
var nationalAttractions: [TouristAttraction] = []
var locationAttractions: [TouristAttraction] = []
// 전국 기준 관광지 정보 가져오기
group.enter()
fetchNationalAttractions { attractions in
nationalAttractions = attractions
group.leave()
}
// 위치 기준 관광지 정보 가져오기
group.enter()
fetchLocationBasedAttractions { attractions in
locationAttractions = attractions
group.leave()
}
// 모든 작업이 완료된 후 UI 업데이트
group.notify(queue: .main) {
// 컬렉션 뷰 데이터 설정
self.nationalCollectionView.reloadData()
self.locationCollectionView.reloadData()
// 추가적인 UI 업데이트가 필요하다면 여기서 처리
}
설명
- DispatchGroup 생성: DispatchGroup 인스턴스를 생성합니다.
- 비동기 작업 추가: 각각의 비동기 작업에서 group.enter()를 호출하여 작업이 시작됨을 알리고, 작업이 완료되면 group.leave()를 호출하여 작업이 완료됨을 알립니다.
- 작업 완료 대기: group.notify(queue: .main)를 사용하여 모든 작업이 완료된 후 UI를 업데이트하는 클로저를 지정합니다. 이 클로저는 메인 스레드에서 실행되므로 UI 업데이트에 안전합니다.
장점
- 코드 가독성: 비동기 작업을 묶어서 처리하면 코드가 더 간결하고 이해하기 쉬워집니다.
- 동시성: 두 작업이 동시에 진행되므로, 네트워크 요청 시간을 단축할 수 있습니다.
- UI 업데이트의 일관성: 두 작업이 완료된 후에 한 번에 UI를 업데이트하므로, 사용자에게 일관된 경험을 제공합니다.
DispatchGroup과 async/await은 비동기 작업을 처리하는 두 가지 다른 접근 방식입니다. 각각의 사용 사례와 장단점이 있으며, 상황에 따라 선택할 수 있습니다.
1. DispatchGroup vs. async/await
- DispatchGroup:
- 여러 비동기 작업의 완료를 기다리는 데 사용합니다.
- 복잡한 비동기 흐름을 관리할 수 있지만, 코드가 다소 복잡해질 수 있습니다.
- 콜백 기반의 프로그래밍 모델을 사용하므로 가독성이 떨어질 수 있습니다.
- async/await:
- 비동기 코드를 동기적으로 작성할 수 있게 해 주는 구문입니다.
- 코드 가독성이 높고, 흐름을 더 쉽게 이해할 수 있습니다.
- Swift 5.5 이후에 도입되었으며, 비동기 함수가 다른 비동기 함수를 호출할 수 있습니다.
2. 예시: DispatchGroup을 async/await으로 변환
다음은 DispatchGroup을 사용한 예제와 이를 async/await로 변환한 예제입니다.
DispatchGroup 예제
let group = DispatchGroup()
var nationalAttractions: [TouristAttraction] = []
var locationAttractions: [TouristAttraction] = []
// 전국 기준 관광지 정보 가져오기
group.enter()
fetchNationalAttractions { attractions in
nationalAttractions = attractions
group.leave()
}
// 위치 기준 관광지 정보 가져오기
group.enter()
fetchLocationBasedAttractions { attractions in
locationAttractions = attractions
group.leave()
}
// 모든 작업이 완료된 후 UI 업데이트
group.notify(queue: .main) {
self.nationalCollectionView.reloadData()
self.locationCollectionView.reloadData()
}
async/await로 변환
func fetchAttractions() async {
async let nationalAttractions = fetchNationalAttractions()
async let locationAttractions = fetchLocationBasedAttractions()
// 두 작업이 모두 완료될 때까지 대기
let attractions1 = await nationalAttractions
let attractions2 = await locationAttractions
// UI 업데이트는 메인 스레드에서 수행
DispatchQueue.main.async {
self.nationalCollectionView.reloadData()
self.locationCollectionView.reloadData()
}
}
설명
- async let:
- 두 비동기 작업을 동시에 시작합니다. 각 작업이 완료될 때까지 대기하지 않으며, 작업이 끝나는 대로 결과를 받아올 수 있습니다.
- await:
- await 키워드를 사용하여 각 비동기 작업의 완료를 기다립니다. 이 경우, nationalAttractions와 locationAttractions는 두 작업이 모두 완료된 후에 사용됩니다.
- UI 업데이트:
- DispatchQueue.main.async를 사용하여 UI 업데이트를 메인 스레드에서 수행합니다.
구분하여 사용하기
- 복잡한 비동기 흐름: 여러 비동기 작업을 그룹으로 묶고, 모든 작업이 완료된 후에 처리할 필요가 있을 때는 DispatchGroup을 사용할 수 있습니다.
- 간결하고 직관적인 코드: 코드의 가독성을 높이고 싶다면 async/await를 사용하는 것이 좋습니다. 특히 Swift 5.5 이상에서는 async/await의 장점이 큽니다.
'정보' 카테고리의 다른 글
가상 메모리(Virtual Memory)의 개념과 동작 원리에 대해 설명해주세요. (2) | 2024.10.04 |
---|---|
암호화와 보안의 기본 개념, iOS 앱 보안을 위한 방안에 대해 설명해주세요. (2) | 2024.10.04 |
자료구조의 종류와 iOS 개발에서 자주 사용되는 자료구조에 대해 설명해주세요. (5) | 2024.10.02 |
알고리즘의 시간 복잡도와 공간 복잡도의 개념, 빅오 표기법에 대해 설명해주세요. (0) | 2024.09.24 |
iOS에서 메모리 사이즈와 관련된 개념과 고려 사항에 대해 설명해주세요. (0) | 2024.09.24 |