[Swift] Concurrency - Collections & Parallelization (2)
안녕하세요, 루피입니다.
이번 포스팅은 Parallelization 2번째 시간입니다. 오늘은 Swift의 내장 동시성 시스템을 활용하여 데이터 변환 작업을 효율적으로 수행하는 방법에 대해 알아보겠습니다. 특히 비동기 및 병렬 처리와 결합하는 방법에 대해 정리 해보려합니다. 바로 시작합니다.
왜 비동기 컬렉션 처리가 필요한가?
우리가 일상적으로 작성하는 코드의 대부분은 본질적으로 일련의 데이터 변환으로 구성됩니다. 네트워크 응답, 사용자 입력, 데이터베이스 쿼리 등 다양한 형태의 데이터를 받아서 로직을 실행하고, 최종적으로 저장하거나 사용자에게 표시하는 과정이죠.
하지만 기존의 동기적 처리 방식은 성능상 한계가 있습니다.
// 기존 동기 처리 - 순차적 실행
class FavoritesManager {
func loadFavorites() throws -> [Movie] {
try favoriteIDs.map { id in
try database.loadMovie(withID: id) // 각각 순차적으로 실행
}
}
}
이 방식은 각 데이터베이스 호출이 완료될 때까지 기다린 후 다음 작업을 수행하므로, 전체 처리 시간이 길어집니다.
비동기 map과 forEach 구현하기
1. asyncMap 구현
Swift의 표준 라이브러리 map은 아직 async 클로저를 지원하지 않습니다. 하지만 Sequence 프로토콜을 확장하여 직접 구현할 수 있습니다:
extension Sequence {
func asyncMap<T>(
_ transform: (Element) async throws -> T
) async rethrows -> [T] {
var values = [T]()
for element in self {
try await values.append(transform(element))
}
return values
}
}
주요 특징:
rethrows키워드로 클로저가 던지는 경우에만 오류를 던짐- 순차적 실행으로 예측 가능한 결과 순서 보장
2. asyncForEach 구현
마찬가지로 forEach의 비동기 버전도 구현할 수 있습니다:
extension Sequence {
func asyncForEach(
_ operation: (Element) async throws -> Void
) async rethrows {
for element in self {
try await operation(element)
}
}
}
3. 실제 사용 예시
class FavoritesManager {
func loadFavorites() async throws -> [Movie] {
try await favoriteIDs.asyncMap { id in
try await database.loadMovie(withID: id)
}
}
}
class MovieListViewController: UIViewController {
func markSelectedMoviesAsFavorites() {
Task {
await tableView.indexPathsForSelectedRows?.asyncForEach {
[movies] indexPath in // 값 캡처로 안전성 확보
let movie = movies[indexPath.row]
await favoritesManager.markMovieAsFavorite(movie)
}
}
}
}
병렬 처리로 성능 향상하기
비동기 처리만으로도 UI 반응성은 향상되지만, 여전히 순차적으로 실행됩니다. 진정한 성능 향상을 위해서는 병렬 처리가 필요합니다.
1. concurrentForEach 구현
TaskGroup을 활용하여 병렬 실행이 가능한 forEach를 구현할 수 있습니다.
extension Sequence {
func concurrentForEach(
_ operation: @escaping (Element) async -> Void
) async {
await withTaskGroup(of: Void.self) { group in
for element in self {
group.addTask {
await operation(element)
}
}
}
}
}
2. concurrentMap 구현
map의 병렬 버전은 결과 순서를 보장하기 위해 다른 접근법을 사용합니다.
extension Sequence {
func concurrentMap<T>(
_ transform: @escaping (Element) async throws -> T
) async throws -> [T] {
let tasks = map { element in
Task {
try await transform(element)
}
}
return try await tasks.asyncMap { task in
try await task.value
}
}
}
핵심 아이디어:
- 모든 요소에 대해
Task를 즉시 생성 (병렬 시작) asyncMap을 사용하여 순서대로 결과 수집
3. 병렬 처리 활용 예시
class FavoritesManager {
func loadFavorites() async throws -> [Movie] {
try await favoriteIDs.concurrentMap { [database] id in
try await database.loadMovie(withID: id)
}
}
}
class MovieListViewController: UIViewController {
func markSelectedMoviesAsFavorites() {
Task {
await tableView.indexPathsForSelectedRows?.concurrentForEach {
[movies, favoritesManager] indexPath in
let movie = movies[indexPath.row]
await favoritesManager.markMovieAsFavorite(movie)
}
}
}
}
언제 어떤 방식을 사용할까?
| 상황 | 권장 방식 | 이유 |
|---|---|---|
| 의존성이 있는 작업 | asyncMap, asyncForEach |
순서 보장 필요 |
| 독립적인 I/O 작업 | concurrentMap, concurrentForEach |
대기 시간 활용 |
| CPU 집약적 작업 | concurrentMap, concurrentForEach |
멀티코어 활용 |
| 소수의 간단한 작업 | asyncMap, asyncForEach |
오버헤드 방지 |
https://www.swiftbysundell.com/articles/async-and-concurrent-forEach-and-map/
Building async and concurrent versions of forEach and map | Swift by Sundell
Let’s take a look at how we can utilize Swift’s built-in concurrency system when performing data transformations using functions like forEach and map.
www.swiftbysundell.com
오늘도 화이팅입니다!!