iOS/Swift

[Swift] Concurrency - Collections & Parallelization (2)

kimsangjunzzang 2025. 6. 9. 16:01

안녕하세요, 루피입니다.

 

이번 포스팅은 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
        }
    }
}

핵심 아이디어:

  1. 모든 요소에 대해 Task를 즉시 생성 (병렬 시작)
  2. 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

 

오늘도 화이팅입니다!!