개발 Story

다중 작업이 동시에 끝날 때 까지 기다리려면 DispatchGroup

StudySpare 2021. 12. 7. 16:41
반응형

 

순서에 상관없이 여러 작업이 끝난 뒤에 어떤 작업이 실행되어야 하는 경우가 있다.

몇년 전에 나도 같은 케이스를 만나서 두 작업이 모두 완료 되는 순간을 어떻게 알 수 있을까 고민한 적이 있다.

다양한 방법을 사용할 수 있겠지만, 나는 그 때 DispatchGroup을 활용하기로 결정했다.

 

 


예를 들어 Thumbnail과 Description을 별도의 URL에서 받아서 노출해야 하는 경우가 있다고 해보자.

썸넬을 받아오는 Task 하나, 글자를 받아오는 Task 하나가 각각 동시에 실행 되도록 한다.

둘 중 어느 작업이 먼저 끝날지는 알 수 없다.(또한 어느 것을 먼저 받아오는 것이 더 효율적인지도 알 수 없다. 그 판단은 시스템이 한다.)

두 작업이 모두 완성 되면, 두가지 정보를 조합해서 화면에 노출하도록 한다.

 

위 시나리오대로 코드를 작성하기 전에 DispatchGroup에 대해서 알아보자.

 

 

DispatchGroup 
iOS8+
iPadOS8+

일련의 Task를 그룹지어 해당 그룹 내 Task들의 동기화 처리를 할 수 있다. 여러 작업을 추가 할 수 있고, 해당 작업들이 동일 queue나 서로 다른 queue에서 비동기로 실행되도록 스케쥴링 할 수 있다. 그룹 내 모든 작업이 완료 되면 completionHandler가 호출 된다. 또한 group내 모든 작업이 끝나기를 동시에 기다렸다가 실행을 종료 시킬 수 있다.(You can also wait synchronously for all tasks in the group to finish executing.)

References DispatchGroup in AppleDeveloper

 

AppleDeveloper 사이트의 설명을 옮긴 것이다. 마지막 문장에서 말하는 DispatchGroup의 용법이 지금 하고자 하는 작업에서 키포인트가 되는 문장이다. 다중 작업이 동시에 끝날 때까지 기다리려면 DispatchGroup을 이용하면 된다.

 

사용법

 DispatchGroupenter()를 하면, 반드시 leave()도 해주어야 한다.

let dispatchGroup = DispatchGroup()

dispatchGroup.enter()  //첫번째 Task 진입
Task.detached(priority: .utility) {
	//첫번째 Task 처리
	dispatchGroup.leave()	//첫번째 Task 완료
}

dispatchGroup.enter()  //두번째 Task 진입
Task.detached(priority: .utility) {
	//두번째 Task 처리
	dispatchGroup.leave()	//두번째 Task 완료
}

dispatchGroup.wait() //dispatchGroup내 모든 task가 leave처리 될때까지 대기
dispatchGroup.notify(queue: .main) {
	//dispatchGroup 내 task가 모두 완료되면, main queue에서 이 후 작업을 수행.
}

 

처음 접할 때는 이런 걸 쓸 일이 있을까 했는데, 신기하게도 얼마지나지 않아서 쓸일이 생겼더랬다.

막상 실제 예제가 확 와닿는게 없어서 억지로 예제를 만들어보았다.

예제를 통해 살펴보자.

 

 

 


예제: 현재 날씨 정보

현재 있는 도시의 현재 날씨를 알려주는 아래와 같은 화면을 만들어보려고한다. 지도와 도시img를 구성하는 건 지금 다루지 않는다.

그 아래 날씨 아이콘/ 온도 정보/ 날씨 설명을 가져오는 작업에 DispatchGroup을 이용해볼 것이다.

날씨 정보 API는 OpenWeatherMap과 WeatherAPI 두 개를 이용 한다.

두 API 모두 상세한 날씨 정보를 제공하고 있어서, 하나만 이용해서 충분히 개발이 가능하다. 하지만 여기서는 DispatchGroup사용을 위해 정보를 나누어서 가져오도록 했다.

https://openweathermap.org/onecall
https://www.weatherapi.com/api-explorer.aspx

 

실제로 개발을 하다보면 수없이 변경되는 요구 사항과 예상치도 못한 UI 조합을 만나는 경우가 생긴다. 그러면 기존에 있는 API를 가지고 정보를 쪼개서 가져오는 일도 생긴다.

두가지 Task를 구성한다.

 

Task 1. OpenWeatherMap에서 온도 및 날씨 정보를 가져온다.

Task 2. WeatherAPI에서 날씨 이미지(Icon)을 가져온다

 

Task1로 WeatherTask작성한다. OpenWeatherMap에서 온도 및 날씨 정보를 가져온다.

 

/**
 Weather API
 */
struct WeatherAPI {
    //https://openweathermap.org/onecall
    private static let cityName = "Seoul"
    private static let apiKey = "********************************"
    
    
    public static var url: URL? = {
        let weatherAPI =
        "https://api.openweathermap.org/data/2.5/onecall?\(SeoulGeo.paramDescription)&exclude=minutely,hourly,daily,alerts&appid=\(WeatherAPI.apiKey)&units=metric"
        return URL.init(string: weatherAPI)
    }()
    
}

class WeatherTask {
    
    func fetch(completionHandler: @escaping (Result<WeatherResponse, WeatherError>) -> Void) {
        guard let url = WeatherAPI.url else {
            print("Not Found URL")
            
            completionHandler(.failure(.invalidAPI))
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else {
                completionHandler(.failure(.noData))
                return
            }
            
            let weatherResponse = try? JSONDecoder().decode(WeatherResponse.self, from: data)

            if let weatherResponse = weatherResponse {
                print("weatherResponse\n \(String(describing: weatherResponse))")
                completionHandler(.success(weatherResponse))
            } else {
                completionHandler(.failure(.noData))
            }
            
            
            
        }.resume()
        
    }
    
}

 

Task2로 WeatherIconTask작성한다. WeatherAPI에서 온도 및 날씨 정보를 가져온다.

 

/**
 WeatherIcon API
 */
struct WeatherIconAPI {
    //https://www.weatherapi.com/api-explorer.aspx
    private static let cityName = "Seoul"
    private static let apiKey = "**********************************"
    
    
    public static var url: URL? = {
        let api =
        "https://api.weatherapi.com/v1/current.json?key=\(WeatherIconAPI.apiKey)&\(SeoulGeo.qParamDescription)&aqi=no"
        
        return URL(string: api)
    }()
    
}

class WeatherIconTask {
    func fetch(completionHandler: @escaping (Result<URL, WeatherError>) -> Void) {
        guard let url = WeatherIconAPI.url else {
            print("Not Found URL")
            
            completionHandler(.failure(.invalidAPI))
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else {
                completionHandler(.failure(.noData))
                return
            }

            //parse JsonObject
            guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any],
               let current = jsonObject["current"] as? [String:Any],
               let condition = current["condition"] as? [String:Any],
               let icon = condition["icon"] as? String,
               let iconURL = URL(string: "https:\(icon)") else {
                   completionHandler(.failure(.noData))
                   return
            }
            
            print("iconURL: \(iconURL.description))")
            completionHandler(.success(iconURL))
            
        }.resume()
        
    }
}

 

refreshWeatherInformation() 이라는 함수를 작성한다. 이곳에서 두 API를 호출해서 두 종류의 데이터 구성이 끝나면, 화면을 구성하도록 작업한다. 각 API를 호출하는 Task를 dispatchGroup에 넣고, 완료 될 때 main thread에서 알림을 받아 처리하도록 한다.

 

    
    private func refreshWeatherInfomation() {
        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter()
        var weatherInfo: WeatherResponse?
        WeatherTask().fetch { result in
            switch result {
            case .success(let weatherResponse):
                print("weatherResponse \(String(describing: weatherResponse) )")
                weatherInfo = weatherResponse
                break
            case .failure(let error):
                print("error \(error)")
                break
            }
            dispatchGroup.leave()
        }

        dispatchGroup.enter()
        var data: Data?
        WeatherIconTask().fetch { result in
            switch result {
            case .success(let url):
                data = try? Data(contentsOf: url)
                break
            case .failure(let error):
                print("error \(error)")
                break
            }
            dispatchGroup.leave()
        }
        
        dispatchGroup.wait()
        dispatchGroup.notify(queue: .main) {
            
            guard let data = data,
                    let image = UIImage.init(data: data) else {
                return
            }
            self.weatherImage?.image = image
            
            self.temperatureTextField?.text = "\(weatherInfo?.current.temp ?? 0.0)  ℃"
            self.weatherDescriptionTextView?.text = weatherInfo?.current.weather.first?.description ?? ""
            
        }
    }

 

이제 원하는 위치에서 refreshWeatherInformation() 함수를 호출하면, API 호출한 뒤 화면을 갱신해 줄 것이다.

 

 

 

 

References

DispatchGroup in AppleDeveloper

Councurrency in SwiftDocuments

 

 

반응형