J
[Swift] Swift Concurrency 본문
Swift Concurrency
Swift Concurrency는 Swift에서 비동기 작업을 안전하고 직관적으로 관리할 수 있도록 제공되는 기능.
비동기 함수, `async`/`await`, `actor`, `Task`, `TaskGroup` 등의 개념을 포함하여 비동기 작업, 동시성 문제를 간결하게 해결할 수 있음.
이를 통해 비동기 코드를 동기 코드처럼 작성할 수 있음.
1. 비동기 프로그래밍과 동시성
비동기 프로그래밍은 여러 작업을 동시에 처리하여 응답성을 향상시키는 것이 목표.
네트워크 요청, 파일 I/O, 데이터베이스 작업 등은 비동기로 실행되어야 UI가 블로킹되지 않고, 사용자에게 원활한 경험을 제공할 수 있음.
- 여기서 동시성(Concurrency)은 여러 작업을 동시에 실행하는 것이 아니라, 병렬로 처리할 수 있는 여러 작업을 효율적으로 스케줄링하는 것을 의미.
2. `async`와 `await`
`async`와 `await`는 Swift에서 비동기 작업을 관리하는 새로운 키워드로, 비동기 작업을 동기 코드처럼 작성할 수 있도록 도와 줌.
2.1. `async` 함수
`async`는 비동기 함수를 정의할 때 사용. 해당 함수는 호출되면 즉시 반환되며, 비동기적으로 실행.
함수의 결과가 필요할 때는 `await` 키워드를 사용하여 그 결과를 기다릴 수 있음.
func fetchData() async -> String {
// 비동기적으로 데이터를 가져오는 작업
return "Fetched Data"
}
2.2. `await` 사용
`await`는 비동기 함수의 결과를 기다리기 위해 사용.
함수가 완료될 때까지 대기하며, 완료되면 결과를 반환받을 수 있음.
func processData() async {
let data = await fetchData()
print(data) // "Fetched Data"
}
2.3. `async` 함수 호출
비동기 함수는 비동기 실행 환경에서 호출되어야 함.
이를 위해서 Swift에서는 `Task`를 사용하여 비동기 작업을 실행할 수 있음.
Task {
await processData()
}
2.4 동기 vs 비동기 함수
동기 함수 | 비동기 함수 (`async`/`await`) | |
처리 방식 | 함수가 완료될 때까지 호출을 블로킹 | 작업이 완료되지 않아도 다음 코드로 진행 |
코드 가독성 | 복잡한 비동기 코드를 처리하기 어려움 | 동기 코드처럼 비동기 작업을 간결하게 작성 가능 |
에러 처리 | `try`/`catch`로 처리 | `async`와 `throws`를 함께 사용 가능 |
3. `Task`와 `TaskGroup`
3.1. Task
Task는 Swift Concurrency의 비동기 작업의 단위이며, `Task`를 사용하면 비동기 작업을 생성하고 실행할 수 있음.
Task(priority: .high) {
let data = await fetchData()
print(data)
}
- `Task`는 우선순위를 설정하여 더 중요한 작업을 먼저 실행 가능.
3.2. TaskGroup
`TaskGroup`은 여러 비동기 작업을 동시 실행하고, 그 결과를 그룹화하여 처리할 수 있게 도와 줌.
여러 개의 비동기 작업을 동시에 실행하면서 각각의 결과를 기다리거나 병렬 처리가 가능.
func fetchMultipleData() async {
await withTaskGroup(of: String.self) { group in
group.addTask { await fetchData() }
group.addTask { await fetchData() }
for await result in group {
print(result) // 여러 작업의 결과 출력
}
}
}
4. `actor`: 동시성 제어
`actor`는 Swift에서 데이터 경쟁 문제를 방지하기 위해 제공되는 동시성 제어 기능.
여러 스레드에서 동일한 객체에 접근할 때 발생하는 경쟁 상태(race condition)를 방지하고, 데이터 무결성을 유지하는 데 중요한 역할.
4.1. `actor` 사용 예시
actor Counter {
var count = 0
func increment() {
count += 1
}
func getCount() -> Int {
return count
}
}
let counter = Counter()
Task {
await counter.increment()
let currentCount = await counter.getCount()
print(currentCount) // 1
}
- `actor` 내부의 상태는 자동으로 동기화되어 여러 스레드에서 접근할 때 안전.
- 여러 스레드가 동시에 접근해도 데이터의 경쟁이 발생하지 않으며, 동기화된 환경에서 안전하게 작업을 처리 가능.
5. 에러 처리
`async`와 `throws`를 함께 사용하여 비동기 함수에서 발생하는 에러를 처리할 수 있음.
5.1. 비동기 함수에서 에러 처리
func fetchData() async throws -> String {
// 네트워크 에러 발생 가능
throw NSError(domain: "NetworkError", code: 1, userInfo: nil)
}
Task {
do {
let data = try await fetchData()
print(data)
} catch {
print("Error fetching data: \\\\(error)")
}
}
- 비동기 함수에서 `throws` 키워드를 사용하여 에러를 발생시킬 수 있음.
- `try`와 `await`를 함께 사용하여 비동기 작업에서 발생하는 에러 처리 가능.
6. GCD와 Swift Concurrency 비교
Swift에서 비동기 작업을 처리하기 위한 두 가지 주요 방식은 GCD(Grand Central Dispatch)와 Swift Concurrency.
두 방식 모두 비동기 작업을 처리하고 동시성을 관리하지만, 접근 방식, 사용 용이성, 안전성 측면에서 차이가 존재.
6.1. 코드 작성 방식
GCD (Grand Central Dispatch)
GCD는 큐(Dispatch Queue)를 기반으로 관련 작업을 스케줄링하고 비동기 처리를 제어.
GCD에서 작업을 처리하려면 클로저를 사용하여 작업을 큐에 적재하고, 큐가 작업을 적절한 스레드에서 처리.
예시: GCD를 사용하여 비동기적으로 데이터를 가져오는 코드
DispatchQueue.global().async {
let data = fetchData() // 네트워크에서 데이터를 가져옴
DispatchQueue.main.async {
updateUI(with: data) // UI 업데이트는 메인 스레드에서 처리
}
}
- `DispatchQueue.global().async`: 비동기적으로 글로벌 큐에서 백그라운드 작업 실행.
- `DispatchQueue.main.async`: 백그라운드 작업이 완료된 후, 메인 스레드에서 UI 업데이트.
Swift Concurrency
Swift Concurrency는 `async`와 `await` 키워드를 사용하여 비동기 작업을 동기 코드처럼 작성할 수 있음
예시: Swift Concurrency를 사용하여 비동기적으로 데이터를 가져오는 코드
func processData() async {
let data = await fetchData() // 비동기적으로 데이터를 가져옴
updateUI(with: data) // UI 업데이트
}
Task {
await processData()
}
- `await`: 비동기 함수의 결과를 기다리는 동안 다른 작업은 진행.
- `Task`: 비동기 작업을 실행하는 단위.
GCD와 Swift Concurrency 코드 작성 방식 차이점
- GCD는 큐와 클로저를 사용해 작업을 관리하기 때문에 비동기 작업이 끝나면 중첩된 클로저로 인해 코드가 복잡해질 수 있음.
- Swift Concurrency는 비동기 작업을 동기 코드처럼 작성할 수 있어, 가독성과 유지보수성이 뛰어나며, 클로저 중첩이 없고 코드 흐름이 직관적.
6.2. 동시성 제어 및 안전성
GCD
GCD에서는 경쟁 상태(race condition)가 발생할 수 있는 자원에 대한 동시 접근을 막기 위해 수동으로 동기화 처리를 해야 함.
주로 `DispatchSemaphore`나 `DispatchBarrier` 등을 사용하여 동기화를 구현.
예시: GCD에서 동시성 문제 해결
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
semaphore.wait() // 자원 확보
sharedResource.update() // 공유 자원 업데이트
semaphore.signal() // 자원 반환
}
- `DispatchSemaphore`를 사용해 자원 접근을 제한하여 동시성 문제를 해결.
Swift Concurrency
Swift Concurrency는 `actor`를 통해 동시성 문제를 자동으로 해결.
`actor`는 여러 스레드에서 동시 접근을 막고 상태를 안전하게 보호.
즉, 경쟁 상태가 발생하지 않도록 Swift에서 자동으로 동기화를 처리.
예시: `actor`를 사용하여 동시성 문제 해결
actor BankAccount {
var balance: Int = 0
func deposit(amount: Int) {
balance += amount
}
}
let account = BankAccount()
Task {
await account.deposit(amount: 100)
}
- `actor` 내부의 상태는 자동으로 동기화되므로, 여러 스레드에서 안전하게 접근 가능.
GCD와 Swift Concurrency 동시성 제어 및 안전성 차이점
- GCD는 동시성 문제를 해결하기 위해 수동 동기화가 필요하고, 이를 위해 코드에서 `DispatchSemaphore`나 `DispatchBarrier`를 사용해야 함.
- Swift Concurrency는 `actor`로 이러한 동시성 문제를 자동으로 해결하여, 복잡한 동기화 코드를 작성하지 않고 안전한 동시성 처리가 가능.
6.3. 에러 처리
GCD
GCD에서는 비동기 작업에서 발생하는 에러를 수동으로 처리해야 함.
일반적으로 비동기 작업의 성공 또는 실패 여부를 클로저 내에서 처리하는 방식을 사용.
예시: GCD에서 에러 처리
DispatchQueue.global().async {
do {
let data = try fetchData()
DispatchQueue.main.async {
updateUI(with: data)
}
} catch {
DispatchQueue.main.async {
showError(error)
}
}
}
- 에러가 발생하면 직접 `catch` 블록을 통해 처리.
Swift Concurrency
Swift Concurrency는 비동기 작업에서 발생하는 에러를 `async`와 `throws` 키워드를 함께 사용하여 처리.
동기 함수에서의 에러 처리 방식과 동일하게 처리가 가능.
예시: Swift Concurrency에서 에러 처리
func fetchData() async throws -> String {
// 네트워크 에러 발생 가능
throw NetworkError.failed
}
Task {
do {
let data = try await fetchData()
updateUI(with: data)
} catch {
showError(error)
}
}
- 비동기 함수에서 에러가 발생하면 `try`와 `await`를 통해 자연스럽게 에러 처리.
GCD와 Swift Concurrency 에러 처리 차이점
- GCD에서는 에러를 클로저 내에서 수동으로 처리해야 하며, 에러 처리 로직이 복잡해질 수 있음.
- Swift Concurrency는 동기 함수와 동일하게 `try`/`catch` 구문을 통해 에러를 처리하므로, 코드가 더 직관적이고 간결.
6.4. 코드 가독성 및 유지보수성
GCD
GCD는 클로저 중첩을 많이 사용하기 때문에, 비동기 작업이 많아지면 코드가 복잡해지고 가독성이 떨어질 수 있음.
작업 간의 의존 관계가 많을 경우, 콜백 지옥(callback hell)이 발생할 수 있음.
예시: 중첩된 GCD 클로저
DispatchQueue.global().async {
let data = fetchData()
DispatchQueue.main.async {
updateUI(with: data)
DispatchQueue.global().async {
let processedData = processData(data)
DispatchQueue.main.async {
updateUI(with: processedData)
}
}
}
}
- 클로저 중첩이 많아질수록 코드가 복잡해지고 유지보수가 어려움.
Swift Concurrency
Swift Concurrency는 비동기 작업을 동기 코드처럼 작성할 수 있어 가독성이 좋은 편.
중첩된 클로저가 없으며, 작업 간의 의존 관계를 명확하게 표현할 수 있음.
예시: Swift Concurrency로 비동기 작업 처리
func fetchData() async -> String {
return "data"
}
func processData(_ data: String) async -> String {
return "processedData"
}
Task {
let data = await fetchData()
let processedData = await processData(data)
updateUI(with: processedData)
}
- 비동기 작업을 순차적으로 실행하면서도 코드가 간결하고 가독성이 높음.
GCD와 Swift Concurrency 코드 가독성 및 유지보수성 차이점
- GCD는 클로저 중첩으로 인해 코드가 복잡해질 수 있고, 유지보수가 어려울 수 있음.
- Swift Concurrency는 동기 코드처럼 비동기 작업을 작성할 수 있어 가독성이 뛰어나고 유지보수가 용이.
6.5. Swift Concurrency와 GCD 한 눈에 비교하기
GCD (Grand Central Dispatch) | Swift Concurrency | |
사용 방식 | 큐와 클로저를 사용한 비동기 작업 처리 | `async`/`await`를 사용한 비동기 작업 관리 |
동시성 제어 | DispatchQueue, DispatchGroup, Semaphore 등 | `Task`, `TaskGroup`, `actor`를 사용하여 동시성 제어 |
코드 가독성 | 복잡한 클로저 구조로 인해 가독성이 낮아질 수 있음 | 동기 코드처럼 비동기 코드를 간결하게 작성 가능 |
에러 처리 | 수동으로 에러 처리 | `async`와 `throws`로 간편한 에러 처리 가능 |
성능 최적화 | GCD가 멀티코어 활용에 적합 | 경량화된 `Task`로 비동기 작업을 최적화함 |
7. Swift Concurrency의 장점
- 코드 가독성
- 기존 콜백 지옥이나 중첩된 클로저 대신, 동기 코드처럼 비동기 코드를 작성하기에 코드의 가독성과 유지보수성이 좋음.
- 안전한 동시성 제어
- `actor`로 동시성 문제를 쉽게 해결할 수 있으며, 경쟁 상태를 방지할 수 있음.
- 에러 처리 통합
- 비동기 함수에서도 `try`/`catch`를 사용하여 에러를 처리할 수 있어, 비동기 작업에서 발생하는 예외 상황을 효과적으로 관리할 수 있음.
- 성능 최적화
- 경량화된 `Task`로 여러 비동기 작업을 효율적으로 처리 가능.
8. 정리
Swift Concurrency는 비동기 작업을 동기 코드처럼 작성할 수 있게 도와주는 강력한 기능.
`async`/`await`, `Task`, `actor` 등을 통해 복잡한 비동기 코드를 간결하게 만들고, 동시성 문제를 안전하게 해결할 수 있음.
비동기 작업의 가독성, 안정성, 성능을 크게 향상시키며, 복잡한 비동기 로직을 쉽게 관리할 수 있게 도와 줌.
'Swift' 카테고리의 다른 글
[Swift] Generic (0) | 2024.04.15 |
---|---|
[Swift] GCD vs Swift Concurrency: 스레드 관리와 성능 (0) | 2024.04.15 |
[Swift] GCD (0) | 2024.04.13 |
[Swift] ARC (0) | 2024.04.13 |
[Swift] mutating (0) | 2024.04.12 |