Dispatch Group

iOS에서 시간이 오래 걸리는 작업은 비동기적으로, 그리고 메인 스레드가 아닌 다른 스레드에서 작업을 하는 것이 좋다. 보통 Dispatch Queue에 집어 넣어서 작업을 하게 된다. 그런데 만약 여러 비동기 작업들이 완료가 되야만 그 다음 작업을 진행할 수 있는 상황이라면 어떻게 해야 할까?

그 작업을 가능하게 해주는 것이 Dispatch Group이다.

Dispatch Group

애플의 공식 문서에 따르면, 다음과 같이 되어 있다.

DispatchGroup allows for aggregate synchronization of work. You can use them to submit multiple different work items and track when they all complete, even though they might run on different queues. This behavior can be helpful when progress can’t be made until all of the specified tasks are complete.

번역을 해보자면,

DispatchGroup은 작업의 집합된 동기화를 가능하게 해준다. 여러 개의 다른 작업을 제출하고 작업들이 모두 완료되었을 때를 추적할 때 사용할 수 있는데, 심지어 작업들이 다른 큐에서 동작하는 경우에도 가능하다. 이러한 동작은 특정한 모든 작업들이 완료될 때까지 다음 진행을 못하는 경우에 유용하다.

쉽게 말하면, 여러 작업들을 큐에 넣고 실행한 뒤에 이것들이 모두 끝나는 타이밍을 캐치할 수 있다는 뜻이다.

이러한 기능은 예전 프로젝트를 할 때 사용했던 Rx의 zip이라는 메서드와 유사하다고 느꼈다. 여러 Observable에서 작업을 비동기적으로 서버에 요청하고 이것을 다 받아온 뒤에 UI를 바꿔야 하는 작업이었는데 비동기적인 작업을 묶는 방법을 몰라서 사용했던 기억이 있다. Swift의 기본 클래스에서 이미 제공하는 기능인데 몰랐기 때문에 불필요하게 외부 라이브러리를 사용했던 것이다.

사용 방법

초기화

먼저 초기화를 한다. 아무 인자도 가지지 않는다.

1
let dispatchGroup: DispatchGroup = DispatchGroup()

작업 넣기, 완료 시점 정하기

이제 이 distpatchGroup에 작업들을 넣고 완료되는 시점을 정해주는데, 여기에는 두 가지 방법이 있다.

먼저, DispatchGroup의 인스턴스 메서드인 enter()leave()를 사용하는 것이다.

  • enter(): 해당 블록이 그룹에 들어간 것을 명시적으로 가리킨다.
  • leave(): 그룹 안의 해당 블록이 완료되었다는 것을 가리킨다.

이 두 메서드를 통해 작업을 넣고 완료 시점을 알린다. 목적과 활용 방법은 다르지만 마치 운영체제 시간에 배운 임계 영역(Critical Section)에 접근할 때 Lock이나 Semaphore를 잡고 끝나면 푸는 것과 유사하다.

1
2
3
4
5
dispatchGroup.enter()
DispatchQueue.global().async {
print(1)
dispatchGroup.leave()
}

두번째 방법은, DispatchQueue의 인스턴스 메서드인 async(group:execute:) 또는 async(group:qos:flags:execute:)를 사용하는 방법이다. 큐가 어느 그룹에 속할지 바로 정해준다는 점에서 조금 더 명시적인 것 같다.

1
2
3
DispatchQueue.global().async(group: dispatchGroup) {
print(2)
}

여기서 excute에 인자로 오는 것은 한 작업의 단위를 나타내는 DispatchWorkItem인데 후행 클로저(Trailing Closure)로 생략을 했다.

완료 시점 캐치하기

모든 작업들이 완료되는 시점을 캐치하고 처리하기 위해 DispatchGroup는 여러 인스턴스 메서드를 제공하고 있는데 크게 나누어보면 동기적(Synchronous)비동기적(Asynchronous) 처리로 나눌 수 있다.

동기적(Synchronous)

동기적인 처리는 그룹에 제출된 모든 작업들이 완료되기를 기다린 다음, 그 다음 코드를 실행한다. 다음과 같은 세 가지 메서드가 있다.

  • wait()
  • wait(timeout:): DispatchTime 타입의 만료 시간을 줘서 해당 시간이 되면 진행하도록 한다.
  • wait(wallTimeout:): 위의 메서드와 비슷하지만 DisptachWallTime 타입의 만료 시간을 제공한다.

DispatchTime은 나노 세컨드, DispatchWallTime은 마이크로 세컨드의 정확도를 가지고 있는 시간의 특정 점을 나타낸다고 하는데, 구체적으로 어떤 차이가 있는지는 모르겠다.

비동기적(Asynchronous)

비동기적인 처리는 그룹에 제출된 모든 작업들이 완료된 시점에 비동기적으로 동작하는 코드로 그 다음 코드가 먼저 실행될 수도 있다. 두 가지 메서드가 제공된다.

  • notify(queue:work:): 그룹의 작업들이 모두 끝나게 되면 인자로 전달한 큐에 DispatchWorkItem 타입의 작업을 제출해 실행하게 한다.
  • notify(qos:flags:queue:execute:): 위의 메서드와 비슷하지만 처리하는 작업이 할당될 큐의 DispatchQoS를 정할 수 있고 DispatchWorkItemFlags를 정할 수 있는 메서드이다.

Sync vs. Async

둘의 차이를 살펴보면 다음과 같다.

먼저 동기적 처리를 해보면 이런식으로 출력이 된다.

1
2
3
4
5
6
7
8
9
dispatchGroup.wait(timeout: .now() + 2.0)
print("모두 출력되었습니다.")

print("그 다음 코드")

// 1
// 2
// 모두 출력되었습니다.
// 그 다음 코드

이유는 1, 2가 처리되고 난 후에야 그 다음 print 문이 실행되기 때문이다.

그러나 비동기라면,

1
2
3
4
5
6
7
8
9
dispatchGroup.notify(queue: .global()) {
print("모두 출력되었습니다.")
}
print("그 다음 코드")

// 그 다음 코드
// 2
// 1
// 모두 출력되었습니다.

이런 식으로 나오게 된다. “그 다음 코드”가 출력되는 타이밍은 확실하게 말할 수가 없다. 이렇게 앞에 나올 때도 있고 뒤에 나올 때도 있다. 여기서 확실한 것은 “모두 출력되었습니다.”라는 문자열이 1, 2가 모두 출력된 다음 나온다는 것이다.

물론 동기적이나 비동기적인 처리 두 가지 방법 모두 1, 2가 출력되는 순서는 달라질 수 있다.

Reference