[TIL] 동시성 프로그래밍 - Grand Central Dispatch (GCD)

숑이·2023년 7월 24일
2

iOS

목록 보기
7/26
post-thumbnail

오늘은 iOS에서 동시성 프로그래밍을 구현하기 위해서 사용하는 GCD(Grand Central Dispatch)에 대해서 자세하게 공부하고, 정리하는 시간을 가졌습니다.

인터넷 검색을 통해 여러 자료를 토대로 공부하고 제 마음대로 정리하는 글이라 틀린 부분이 있을 수 있습니다. 틀린 부분이 있다면 댓글로 알려주시면 감사하겠습니다!

동시성 프로그래밍이란?

iOS에서 동시성 프로그래밍이란 앱 내에서 여러 작업들을 동시에 처리하는 것을 의미합니다.

그렇다면, 동시성 프로그래밍을 했을 때 얻을 수 있는 이점은 무엇이 있을까요?

예를들어 네트워크 통신을 통해 데이터를 가져오는 작업(5초짜리), 이미지를 로드하는 작업(2초짜리)과 같이 오래걸리는 작업을 동시에 실행하지 않고, 순차적으로 실행한다면 7초라는 시간이 걸리는 반면에 두 작업을 동시에 처리한다면, 2초만에 이미지는 로드될 것이고, 데이터는 5초만에 가져와서 5초만에 두 작업이 모두 완료됩니다.

즉, 동시성 프로그래밍으로 시간을 절약할 수 있다는 이점이 있는 것 입니다.

여기서 작업을 순차적으로 처리한다는 의미는 1개의 스레드에서 모든 일을 감당한다는 것이고,

작업을 동시에 처리한다는 의미는 여러개의 스레드에게 작업을 분배해서 처리한다는 의미와 같습니다.

자!!, 정리하자면 우리는 여러개의 작업을 하나의 스레드에서 처리하는 것보다 여러개의 스레드에 분산시켜서 작업을 하는 것이 시간 절약을 할 수 있기 때문에 더욱 효율적이다... 까지 이해를 했습니다!

그렇다면 어떻게 여러개의 스레드에 작업들을 분산시켜 줄 수 있을까요?
iOS에서는 어떤 특별한 대기열 큐(Queue)에 작업을 보내버리면 됩니다! 그러면 GCD라는 녀석이 알아서 적절한 스레드에 작업을 분산시키는 일을 합니다.

무슨 말인지 모르겠죠...? 더 자세히 알아보겠습니다.

메인 스레드(UI 스레드)

iOS에서는 UI관련 작업을 메인 스레드라는 곳에서 담당해서 처리합니다.
메인 스레드가 아닌 다른 스레드에서 UI를 변경하려한다면, 에러가 발생합니다.
그리고, 메인 스레드는 단 1개만 존재합니다.

왜 UI를 메인 스레드에서 담당하는 것인지 궁금하다면 이 글을 참고해주세요

우리가 보통 아무런 처리를 하지 않고 코드를 작성하는 경우 메인 스레드에서 코드가 동작하게 됩니다.

네트워크통신()
이미지불러오기()
Task1()
Task2()

따라서 위 4줄의 코드는 아래와 같이 메인 스레드에서 동작하게 됩니다

메인 스레드는 1개 밖에 없는데 이렇게 시간이 오래 걸리는 작업을 몰아서 주면, 작업이 다 끝날 때까지 UI를 그리지 못하겠죠? 따라서 이렇게 메인 스레드에게 오래 걸리는 작업을 시키면, 버벅거리는 현상이 나타나게 됩니다.

그래서 우리는 메인 스레드가 UI를 그릴 수 있는 충분한 여유를 주기 위해서 동시성 프로그래밍을 사용하는 것 입니다!

GCD(Grand Central Dispatch)

GCD는 대기열 큐(Queue)에 있는 작업들을 하나씩 꺼내서 적절한 스레드를 생성하고 작업들을 스레드에 배치하는 역할을 합니다.

즉, 우리는 오래걸리는 작업들을 메인 스레드가 아닌 다른 스레드에게 넘겨주기 위해 Queue라는 곳에 던져버리면 됩니다. 그럼 GCD가 알아서 적절한 스레드에게 그 일을 배치할테니까요 :)

여기까지 이해했다면 메인 스레드에서 대기열 큐로 작업을 보내는 과정을 코드로 작성해보겠습니다.

DispatchQueue

혹시 Dispatch의 뜻을 아시나요?

Dispatch의 뜻은 "보내다"입니다. DispatchQueue를 직역하면 "큐에 보내다" 이고요.

메인 스레드의 작업을 대기열 큐로 넘겨준다고 했었죠? 그 대기열 큐가 바로 DispatchQueue입니다.
즉, DispatchQueue에 오래걸리는 작업을 던져주면, GCD가 알아서 스레드를 지정해주는 것이죠.

그럼 직접 DispatchQueue를 통해 작업을 큐로 보내볼게요.

DispatchQueue.global().async {
	// 큐에 보낼 작업
    sleep(3)
    print("The End...")
}
print("Asynchronous")

DispatchQueue 클로저 내부의 코드들은 작업의 한 묶음이 되어서 DispatchQueue로 작업을 보내게 됩니다. 위 코드를 더 자세하게 설명하자면,

global DispatchQueue에 비동기(async)로 작업을 보내게 됩니다.

global은 DispatchQueue의 한 종류라고 생각하시면 됩니다. DispatchQueue의 종류에는 main, global, custom 이 존재합니다. 자세한 내용은 조금 이따 알아보도록 할게요

위 코드의 실행 결과가 어떻게 나올까요?
3초 후에 The End...를 출력하고, 마지막에 Asynchronous 출력을 한 후 종료될까요?

이 물음에 대한 답을 알기 위해서는 비동기(Asynchronous)와 동기(Synchronous)에 대해서 알아야합니다.

비동기(Asynchronous)

우리는 메인 스레드의 작업을 DispatchQueue에 보내면, GCD가 알아서 적절한 스레드에 분배해준다고 이해했습니다. 그런데 여기서 동기와 비동기의 개념이 등장합니다.

DispatchQueue에 작업을 보내고, 그 작업이 끝날 때까지 기다리느냐(Synchronous), 아니면 기다리지 않고 바로 다음 작업을 수행하느냐(Asynchronous)가 바로 그것입니다.

메인 스레드에서 DispatchQueue를 통해 작업을 Queue로 보내고, 작업이 끝나기를 기다리는 것이 아니라 그냥 작업을 보내놓고, 바로 다음 작업을 수행하는 것 입니다.
그렇다면, 메인 스레드는 네트워크 통신에 걸리는 시간만큼 시간을 절약할 수 있겠죠??

동기(Synchronous)

동기는 비동기의 반대입니다. 큐에 작업을 보내고, 작업이 끝나기까지 기다렸다가 다음 작업을 수행하는 것 입니다.

비동기와는 반대로 네트워크 통신이 끝날 때까지 이미지 불러오기 작업을 처리하지 못하니까 시간 절약을 할 수 없겠죠??

이제 동기와 비동기에 대해서 이해했다고 생각하고, 아까 봤던 코드를 다시 보겠습니다.

DispatchQueue.global().async {
	// 큐에 보낼 작업
    sleep(3)
    print("The End...")
}
print("Asynchronous")

global DispatchQueue에 비동기(async)로 작업을 보냈죠.
그러면 메인스레드는 보낸 작업의 완료를 기다리지 않고, 다음 코드를 바로 실행합니다.
이제 실행 결과가 짐작이 가시나요???!!!

다시 자세하게 설명하자면, 클로저 내부의 작업들은 DispatchQueue로 보내졌고, GCD에 의해서 적절한 스레드에 배치가 될 것 입니다. 그리고 비동기(async)이기 때문에 해당 작업에 대한 완료를 기다리지 않고, 출력을 하겠죠??!! 결과적으로 다음과 같은 출력이 나오게 됩니다.

Asynchronous
The End...

이번에는 동기로 작업을 큐에 보내보겠습니다.

DispatchQueue.global().sync {
    sleep(3)
    print("The End...")
}
print("Synchronous")

작업을 동기로 큐에 보내고 있죠? 그러면 메인스레드는 이 작업이 끝날 때까지 다음 작업을 수행하지 않습니다.
즉, 3초 기다렸다가 The End...를 출력하고, Synchronous를 출력한 뒤 종료되겠죠?

The End...
Synchronous

출력 결과는 위와 같습니다.

여기까지 이해했다면, 이제 왜 비동기 프로그래밍을 해야하는지에 대한 답변을 할 수 있으리라 생각합니다.

왜 비동기 프로그래밍을 해야하는가?

메인 스레드는 UI를 담당하는 스레드라고 했고, 메인 스레드는 단 1개 밖에 존재하지 않는다고 했었죠.
근데 오래 걸리는 작업을 동기로 처리하게 된다면, 1개 밖에 없는 메인스레드는 그 작업이 끝날 때까지 아무것도 하지 못합니다. 즉, 앱이 버벅거리거나 멈춰있는 것처럼 보이게 됩니다.
그래서 비동기 프로그래밍을 하는 것 입니다.

비동기를 통해 메인 스레드의 중단 없이 끊임없이 동작하는 앱을 만들 수 있습니다.

Serial(직렬) 과 Concurrent(동시)

우리는 지금까지 메인 스레드 입장에서 오래걸리는 작업을 큐에 보내는 것까지만 생각을 했습니다.
그렇다면 GCD는 어떤 방식으로 큐에 있는 작업들을 스레드에게 배치 해줄까요?
스레드에 작업을 할당하는 방법으로 크게 2가지로 나눠볼 수 있습니다.

  1. 한 개 Thread 작업 몰빵 - Serial

큐의 모든 작업들이 하나의 스레드에 할당되어 **순차적으로** 처리됩니다.

  1. 여러 개 Thread에 작업 분산 - Concurrent

큐의 모든 작업들이 여러개의 스레드에 분산되어 처리됩니다.

위 두가지의 경우를 각각 Serial(직렬), Concurrent(동시)라고 합니다
이렇게 큐에 있는 작업들을 하나의 스레드에 몰아줄지, 아니면 분산시킬지는 큐의 특성에 따라 결정됩니다.
Serial은 DispatchQueue가 Serial Queue특성을 갖는 경우, Concurrent는 Concurrent Queue특성을 갖는 경우 입니다.

그렇다면, 어떤 경우에 Serial을, Concurrent를 사용할까요?

Serial과 Concurrent는 언제 사용할까?

이 물음에 대한 답은 작업 순서의 중요도에 있습니다.

다시 복습해보면, SerialQueue에 담긴 작업들은 하나의 스레드에 배치되어 순차적으로 실행됩니다.

Task 1 실행 -> Task 1 종료 후 Task 2 실행 -> Task 2 종료 후 Task 3 실행

이렇게 작업에 순서가 중요한 경우. 꼭 이 순서대로 실행되어야만 하는 경우에 Serial을 사용합니다.

반대로, 실행 순서가 중요하지 않은 경우에는 Concurrent를 사용하는 것이 효율적이겠죠??!

이렇게요 ㅎㅎ

요약

지금까지 배운 내용을 요약해볼게요

  • 동시성 프로그래밍을 통해 여러 작업들을 동시에 처리함으로써 시간 절약을 할 수 있다.
  • iOS에서는 동시성 프로그래밍을 위한 방법 중 하나로 GCD(Grand Central Dispatch)를 제공한다.
  • 오랜 시간이 걸리는 작업을 메인 스레드에서 하는 경우 UI작업에 방해가 되므로 다른 스레드에서 처리할 수 있도록 해야 한다.
  • DispatchQueue에 작업을 보내면 큐의 특성에 따라 GCD가 Serial 혹은 Concurrent 로 작업을 쓰레드에 할당한다.
  • 비동기로 작업을 보내는 경우 완료되는 것을 기다리지 않고, 바로 다음 작업을 할 수 있다. 반면에 동기는 완료를 기다려야 한다.
  • 동시성 프로그래밍을 적절하게 사용해서 앱의 성능과 반응성을 최적화하는 것이 중요하다!

지금까지 한 내용이 이해됐길 바라며.... 다음 내용으로 넘어가보겠습니다.

DispatchQueue의 종류

위에서 DispatchQueue의 종류로는 main, global, custom이 있다고 했었던 것 기억하시나요?!
실제로 DispatchQueue는 3가지로 구분됩니다.

  1. 메인 큐
  2. 글로벌 큐
  3. 커스텀(프라이빗) 큐

큐의 종류(main, global, custom)에 따라 큐의 특성(serial, concurrent)이 다르기 때문에 상황에 따라 적절하게 사용하는게 중요하겠죠?

큐의 종류에 따라 다른 특성을 그림으로 표현해보면 위와 같습니다. 이해가 되시리라 생각합니다 ㅎㅎ

Main Queue

  • 오직 한개만 존재
  • Serial 특성을 가지는 Queue
  • 이곳에 할당된 작업은 Main Thread(UI Thread)에서 처리

메인 스레드에서 처리되는 특징을 가지고 있으니까 당연히 UI 관련 작업을 해줘야겠죠?!
그리고, 메인 스레드는 오직 1개만 존재한다고 했었죠? 그렇다면, 큐에서 작업을 분산하고 싶어도 메인 스레드가 1개밖에 없으니까 분산시킬 수 없겠죠!! 따라서 하나의 스레드에 작업이 몰빵되는 Serial 특성을 가질 수 밖에 없어요.

DispatchQueue.main.async {
	// UI작업
}

코드로는 위와 같이 작성합니다.

Global Queue

  • Cuncurrent 특성을 가지는 Queue
  • QoS(Quality Of Service)에 따라 여러개의 종류로 나뉨(6종류)

글로벌 큐는 메인 큐와는 다르게 Cuncurrent 특성을 가지는 큐입니다. 그렇다면, 글로벌 큐는 작업의 순서가 중요하지 않은 경우에 작업을 여러개의 스레드에 분산시켜서 효율적으로 처리하고 싶을 때 사용하면 되겠네요!

DispatchQueue.global().async {
	// task
}

글로벌 큐는 위와 같이 qos를 따로 지정하지 않을 수도 있고,

DispatchQueue.global(qos: .utility).async {
	// task
}

이처럼 qos를 직접 지정하면, 작업의 중요도를 결정할 수 있습니다.

그러면 QoS의 종류를 알아보겠습니다.

UserInteractive

사용자와 직접 상호작용하는 작업 (ex. UI 업데이트, 애니메이션 등)
유저와 상호작용을 하는 작업들을 처리하다 보니 작업을 처리하는데 드는 소요시간은 상당히 짧은편 입니다.

UserInitiated

사용자가 initiated 한 뒤로 즉각적인 처리가 이루어져야 하는 작업에 대해 사용합니다. 작업이 끝날 때까지 유저가 인터렉션을 할 수 없습니다.

default

qos를 선택하지 않으면 기본값으로 선택되는 qos입니다.

utility

즉각적인 결과가 필요하지 않은 경우에 사용합니다. (ex. 데이터 다운로드/불러오기, progress indicator와 함께 길게 실행되는 작업)

background

유저가 인지하지 못하는 백그라운드에서 처리되는 작업의 경우에 사용하는 qos입니다. 데이터 미리 가져오기, 로컬 DB에 데이터를 저장하는 작업, 백업, 동기화 등의 중요도가 높지 않은 작업에 사용합니다.

unspecified

QoS 정보가 없음을 나타냅니다. 거의 사용할 일이 없습니다.

우선순위 : UserInteractive > UserInitiated > default > utility > background > unspecified

실제로 서로 다른 DispatchQueue를 우선순위가 다르도록 QoS를 설정했을 때, 중요도가 높은 작업에 더 많은 스레드를 할당합니다.

Custom Queue

  • 커스텀으로 만듬
  • 디폴트로 Serial 특성을 가진 Queue이면서 Concurrent로 설정이 가능.
  • QoS 설정 가능
let customSerialQueue = DispatchQueue(label: "syoneE")

이렇게 생성자 파라미터로 label을 붙이면 Custom Queue가 됩니다. label만 붙여줬기 때문에 디폴트값인 Serial 특성을 가집니다. qos를 따로 설정안하면 OS가 알아서 추론합니다.

let customSerialQueue = DispatchQueue(label: "syoneE", qos: .background, attribute: .concurrent)

그리고 이렇게 concurrent 특성을 가지고, qos가 background인 커스텀 큐를 마음대로 만들수도 있습니다.

진짜 최종 진짜진짜...

GCD하나 배우는데 많은 내용을 알아봤습니다. 동시성 프로그래밍, sync/async, serial/concurrent, 디스패치 큐의 종류, qos 등등...

모든 내용을 이해했다면 이제 다음의 물음에 대해서 스스로 답할 수 있어야합니다.

  1. 동시성 프로그래밍이 무엇인가?
  2. GCD는 어떤 방식으로 동작하고, 필요성은 무엇인가?
  3. Asynchronous 와 Synchronous에 대해서 설명하라.
  4. Serial 과 Concurrent 에 대해서 설명하라.
  5. DispatchQueue의 종류와 각각의 특징을 설명하라.
  6. Global DispatchQueue 의 Qos 에는 어떤 종류가 있는지, 각각 어떤 의미인지 설명하시오.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

자문 자답하는 시간을 가지고 글 마무리 하도록 하겠습니다...

1. 동시성 프로그래밍이 무엇인가?

동시성 프로그래밍이란 시간 절약을 위해 여러 작업들을 동시에 처리함으로써 작업 효율을 높이기 위한 기술입니다. 동시성 프로그래밍을 통해 오래 걸리는 작업을 메인 스레드가 아닌 다른 스레드에서 처리하도록 할 수 있고, 따라서 성능과 반응성이 높은 앱을 만들 수 있습니다.

2. GCD는 어떤 방식으로 동작하고, 필요성은 무엇인가?

DispatchQueue에 있는 작업들을 FIFO 방식으로 꺼내서 적절한 스레드를 생성해 배치합니다. 개발자가 오래 걸리는 작업을 DispatchQueue에 보내기만 하면, GCD가 알아서 적절한 스레드를 생성하고 작업을 배치하기 때문에 개발자는 간단하게 비동기 프로그래밍을 할 수 있습니다.

3. Asynchronous 와 Synchronous에 대해서 설명하라.

비동기는 DispatchQueue에 작업을 보낸 후, 응답을 기다리지 않고 즉시 다음 작업을 진행하는 것을 의미합니다. 반면에 동기는 DispatchQueue에 작업을 보낸 후, 응답이 올 때까지 다음 작업을 진행하지 않습니다.

  • (꼬리 질문) 비동기로 처리된 작업이 완료됐을 때, 어떻게 처리하는가?
    completionHandler를 사용할 수 있습니다.

4. Serial 과 Concurrent 에 대해서 설명하라.

Serial은 작업의 순서가 중요한 경우 Serial Queue에 있는 작업을 하나의 스레드에 할당해서 순차적으로 작업을 처리합니다.
Concurrent는 작업의 순서가 중요하지 않은 경우 Concurrent Queue에 있는 작업을 여러 개의 스레드에 할당해서 동시에 처리합니다.

5. DispatchQueue의 종류와 각각의 특징을 설명하라.

종류는 Main Queue, Global Queue, Custom Queue가 있습니다.
메인 큐는 오직 한 개만 존재하고, Serial 특성을 가지며, 메인 큐에 들어온 작업은 메인 스레드에서 처리됩니다.
글로벌 큐는 Concurrent 특성을 가지며, QoS를 설정 할 수 있습니다.
커스텀 큐는 디폴트로 Serial 특성을 가지며, Concurrent로 설정할 수 있고, QoS를 설정할 수 있습니다.

6. Global DispatchQueue 의 Qos 에는 어떤 종류가 있는지, 각각 어떤 의미인지 설명하시오.

UserInteractive : UI업데이트, 애니메이션 등 사용자와 상호 작용을 위한 작업에 사용합니다.
UserInitiated : 사용자가 생성한 뒤로 즉각적인 처리를 해야하는 경우 사용합니다.
default : qos를 설정하지 않으면 기본적으로 설정되는 qos입니다.
utility : 즉각적인 결과가 필요하지 않은 경우에 사용합니다.
background : 사용자가 인지하지 못하는 백그라운드에서 처리하는 작업에 사용합니다.
unspecified : qos정보가 없음을 의미합니다.

QoS에 따라 작업의 중요도를 결정할 수 있습니다.
작업의 중요도는 UserInteractive가 가장 높고, unspecified가 가장 낮습니다.
작업의 중요도가 높은 작업은 낮은 작업보다 많은 스레드를 할당받을 수 있습니다.

profile
iOS앱 개발자가 될테야

0개의 댓글