본문 바로가기
Swift

[Swift] 비동기 호출 CallBack지옥과 DispatchGroup

by Jimmy_iOS 2023. 8. 19.

 

서버에서 가져온 데이터를 화면에 표시

영화 데이터를 서버에서 요청하여 받아오는 작업을 할 때,

한 화면에서 여러 개의 데이터를 요청해야 하는 경우가 있습니다.

 

예를 들어, 해리포터와 관련된 영화 목록이나

트랜스포머와 관련된 영화 목록과 같은

영화 목록 데이터를 여러 개 받아오는 경우가 있습니다.

 

서버에서 받아온 데이터를 화면에 보여줄 때,

CollectionView나 TableView를 통해 보여주게 됩니다.

이때, 언제 reloadData() 메서드를 호출해 뷰를 갱신시켜줄지에 대해 고민하게 됩니다.

 

예시 화면과 코드

 

var movieInfo1: [MovieResult] = []
var movieInfo2: [MovieResult] = []
var movieInfo3: [MovieResult] = []
var movieInfo4: [MovieResult] = []

override func viewDidLoad() {
    movieManager.requestRecommendation(movieID: 100) { data in
        self.movieInfo1 = data.results
        self.posterCollectionView.reloadData()
    }
    movieManager.requestRecommendation(movieID: 101) { data in
        self.movieInfo2 = data.results
        self.posterCollectionView.reloadData()
    }
    
    movieManager.requestRecommendation(movieID: 102) { data in
        self.movieInfo3 = data.results
        self.posterCollectionView.reloadData()
    }
    
    movieManager.requestRecommendation(movieID: 103) { data in
        self.movieInfo4 = data.results
        self.posterCollectionView.reloadData()
    }
}

코드 설명

서버에 movieID를 요청하면 영화와 관련된 목록을 보내주게됩니다.

그러면 받아온 목록을 movieInfo1 ~ 4 Array에 넣어주고

CollectionView의 데이터를 reloadData() 메서드를 호출해 갱신해줍니다.

movieManager.requestRecommendation(movieID:)DispatchQueue.global().async로 작동하게 됩니다.

뒤에 따라오는 클로저는 DispatchQueue.main.async로 작동하게 됩니다.

 

reloadData() 호출 횟수를 줄일 수는 없을까?

위 코드에서는 reloadDate() 메서드를 반복해서 사용하고 있습니다.

그러면 모든 요청을 완성했을 때 한 번만 reloadData()를 사용하면 되지 않을까? 라는 생각을 하게됩니다.

무시무시한 콜백 지옥…

movieManager.requestRecommendation(movieID: 101) { data in
    self.movieInfo1 = data.results
    self.movieManager.requestRecommendation(movieID: 102) { data in
        self.movieInfo2 = data.results
        self.movieManager.requestRecommendation(movieID: 103) { data in
            self.movieInfo3 = data.results
            self.movieManager.requestRecommendation(movieID: 104) { data in
                self.movieInfo4 = data.results
                self.posterCollectionView.reloadData()
            }
        }
    }
}

모든 요청을 마무리 하고 난 뒤에 데이터를 갱신하기 위해서는 요청이 완료된 시점에 요청을 추가하고, 그 요청이 완료된 시점에 요청을 추가하는 방식을 사용하게 되면 위와 같은 콜백 지옥에 빠지게 됩니다.

 

그렇다면 어떻게 하면 콜백 지옥을 만들지 않으면서 모든 요청이 마무리 됐을 때 데이터를 갱신할 수 있을까요?

DispatchGroup

DispatchGroup을 활용하면 비동기 작업을 group으로 묶어주고

group의 작업이 끝났을 때를 알려주고 알려준 시점에 원하는 작업을 추가하는 방법을 사용할 수 있습니다.

코드

let group = DispatchGroup()

DispatchQueue.global().async(group: group) {
    self.movieManager.requestRecommendation(movieID: 101) { data in
        self.movieInfo1 = data.results
    }
}

DispatchQueue.global().async(group: group) {
    self.movieManager.requestRecommendation(movieID: 102) { data in
        self.movieInfo2 = data.results
    }
}
DispatchQueue.global().async(group: group) {
    sleep(1)
    self.movieManager.requestRecommendation(movieID: 103) { data in
        self.movieInfo3 = data.results
    }
}
DispatchQueue.global().async(group: group) {
    sleep(1)
    self.movieManager.requestRecommendation(movieID: 104) { data in
        self.movieInfo4 = data.results
    }
}

group.notify(queue: .main) {
    self.posterCollectionView.reloadData()
}

코드 실행 화면

하지만 추가적인 문제점 발생

위 코드를 보면 정상적으로 동작할 것 같지만 한 가지 문제점이 있습니다.

DispatchQueue.global().async(group: group) {
    sleep(1)
    self.movieManager.requestRecommendation(movieID: 103) { data in
        self.movieInfo3 = data.results
    }
}

바로 비동기 코드 안에 또 다른 비동기 코드가 있기 때문에 작업이 언제 끝나는지 파악하기 어렵습니다.

self.movieManager.requestRecommendation(movieID: 103) 요청 메서드는 비동기 코드이므로 작업을 다른 스레드로 넘기고 바로 제어권을 반환합니다.

DispatchQueue.global().async(group: group)은 위 요청의 완료 여부와 상관 없이 즉시 작업이 완료되었다고 인식합니다.

그 결과, 의도와는 다르게 2번째 섹션과 3번째 섹션은 데이터가 들어오기 전에 reloadData() 메서드가 호출되어 화면에 정상적으로 표시되지 않는 문제가 발생합니다.

 

enter(), leave()

enter(), leave() 메서드를 활용하면 문제를 해결할 수 있는데요.

코드

let group = DispatchGroup()

group.enter() // group reference Count +1
self.movieManager.requestRecommendation(movieID: 101) { data in
    self.movieInfo1 = data.results
    group.leave() // group reference Count -1
}


group.enter() // group reference Count +1
self.movieManager.requestRecommendation(movieID: 102) { data in
    self.movieInfo2 = data.results
    group.leave() // group reference Count -1
}

group.enter() // group reference Count +1
self.movieManager.requestRecommendation(movieID: 103) { data in
    self.movieInfo3 = data.results
    group.leave() // group reference Count -1
}

group.enter() // group reference Count +1
self.movieManager.requestRecommendation(movieID: 104) { data in
    self.movieInfo4 = data.results
    group.leave() // group reference Count -1
}


group.notify(queue: .main) {
    self.posterCollectionView.reloadData()
}

비동기 메서드를 시작할 때 group.enter()메서드를 호출해 group의 Reference Count를 올려줍니다.

그리고 작업이 완료되면 group.leave() 메서드를 호출해 group의 Reference Count를 내려줍니다.

 

group.notify() 에서 group의 Reference Count를 추적해 +- 가 0이 되면 클로저를 실행하게 됩니다.


Uploaded by

N2T