[Swift] Swift의 동시성 프로그래밍 feat: DispatchQueue

이정훈·2023년 7월 13일
0

Swift 파헤치기

목록 보기
6/10
post-thumbnail

이번 포스트에서는 Swift에서 동시성 프로그래밍(Concurrency Programming)을 구현하기 위해 GCD란 무엇이고, DispatchQueue에 대한 사용 방법을 정리하기 해보려고 한다.


왜 동시성 프로그래밍으로 구현해야 하는가?

기본적으로 iOS의 main 스레드에서는 UI를 그리는 작업, 사용자 이벤트 처리와 같은 인터페이스와 관련된 작업을 수행하게 된다.

또한 개발자가 코드를 작성하면서 따로 스레드에 대한 처리를 하지 않는다면 기본적으로 main 스레드에서 동작하게 된다.

한 가지 예로 네트워크와 관련된 작업은 서버로 request을 보내고 서버로 부터 response를 전달 받는 작업이기 때문에 기기 자체에서 수행하는 작업에 비해 비교적 시간이 오래 걸리는 작업이라고 할 수 있다. 특히 서버 상태에 따라 response가 언제 도착할지는 아무도 알 수가 없다.

만약 이러한 네트워크 작업을 스레드 처리를 하지 않고 main 스레드에서 작동하게 된다면 어떻게 될까?

아마 서버로 부터 요청 값을 전달 받기 전까지 다른 작업을 수행할 수 없고 그 말인 즉 인터페이스에 대한 작업을 수행할 수 없어 화면이 멈춰 보일것이다.

이렇게 화면이 멈추는 현상을 방지하기 위해 네트워크 작업과 같이 시간이 오래 걸리는 작업은 main 스레드 외에 다른 백그라운드 스레드에서 수행하여 여러 작업을 동시적으로 수행한다면 이러한 문제를 발생하지 않는다.

참고로 위의 상황은 단순히 예를 든것이고 실제 네트워크 요청을 위해 사용하는 URLSession 같은 경우 내부적으로 멀티 스레드로 수행하도록 설계되어있기 때문에 개발자가 직접 백그라운드 스레드로 구현할 필요는 없다.

그럼 어떻게 여러 개의 작업을 여러 개의 스레드를 사용하여 멀티 스레드로 구현할 수 있는지, Swift에서는 어떤 방법을 제공하는지 알아보려고 한다.


GCD란 무엇인가

Grand Central Dispatch, 일명 GCD는 시스템에서 관리하는 Queue에 작업을 전달하여 Apple OS에서 멀티 코어 하드웨어가 동시 코드 실행을 가능하도록 하는 Swift 언어의 요소 혹은 런타임 라이브러리이다.

GCD에는 코드를 동시에 수행 하도록 하는 여러 기능을 제공하고, 그 중 Dispatch Queue에 대해 알아보려고 한다.


Dispatch Queue

위에서 설명한 것과 같이 GCD 방식에서는 Queue에 수행 해야하는 작업을 전달한다.

Queue(큐)에 대해 간단히 짚고 넘어가자면, 먼저 들어온 것이 먼저 나가는 선입선출(FIFO) 구조라고 불리는 자료구조의 한 종류 이다.

예를 들어 Dispatch Queue로 작업이 전달 되었다고 할때, Dispatch Queue는 전달된 작업을 main 스레드 또는 백그라운드 스레드에서 serially(순차적) 혹은 concurrently(동시적)으로 작업을 수행될 수 있도록 관리하는 Queue이다.

Dispatch Queue에 전달된 작업은 시스템 즉 OS에서 관리하는 스레드 풀에서 수행되기 때문에 개발자가 Dispatch Queue로 작업만 넘겨 준다면 OS에서 사용 가능한 스레드에 작업을 적절히 할당하여 작업이 수행될 수 있도록 한다.

Dispatch Queue에 작업을 전달하는 방식은 sync(동기) 방식과 async(비동기) 방식 두가지가 존재한다.

sync(동기) 방식은 현재 스레드에 존재하는 작업을 Dispatch Queue에 전달하여 다른 스레드에서 수행하고 해당 작업이 종료되어 반환될 때까지 현재 스레드에서 다음 코드 라인의 실행을 기다리는 방식이고

async(비동기) 방식은 마찬가지로 현재 스레드에 존재하는 작업을 Dispatch Queue에 전달하여 다른 스레드에서 수행하고 해당 작업이 종료되는 것을 기다리지 않고 바로 현재 스레드의 다음 코드 라인을 실행하는 방식이다.

Dispatch Queue 구현하기

Dispatch Queue를 생성하는 방법은 두가지로 DispatchQueue class 내부에 미리 정의 되어 있는 queue를 사용하는 방법과 사용자 커스텀 queue를 생성하는 방법이 있다.

이번 포스트에서는 Swift 내부에서 제공하는 Dispatch Queue에 대한 사용 방법을 알아보고 기회가 된다면 다른 포스트를 통해서 사용자 커스텀 queue를 다루어 보려고 한다.

Swift에서 기본적으로 제공하는 Dispatch Queue의 종류는 다음과 같다.

  • main queue
  • global queue

아래는 DispatchQueue class 내부에 정의 되어 있는 Dispatch Queue를 반환하는 프로퍼티와 메서드이다.

  • main: 현재 프로세스의 main 스레드와 연관된 main queue를 반환하는 프로퍼티로 해당 queue는 serially(순차적)으로 작업을 수행한다.
  • global(): global queue를 반환하는 method로 해당 queueconcurrently(동시적)으로 작업을 수행한다.

순차적과 동시적

위에서 설명한 순차적으로 작업을 수행하는 것과 동시적으로 작업을 수행하는 것이 무엇인지 살펴보자

먼저 아래의 그림은 queue 대기열에 들어온 작업을 순차적으로 수행하는 과정을 보여준다.

queue 대기열에 들어온 작업들은 하나의 스레드를 사용하여 한 작업이 끝나면 다른 작업을 순차적으로 진행하게 된다.

대표적인 serial queue인 main queue의 경우 대기열에 들어온 작업은 main 스레드만을 사용하여 한번에 한 작업씩 순차적으로 수행하게 된다.

다음으로 아래의 그림은 queue 대기열에 들어온 작업들을 동시적으로 수행하는 과정을 보여준다.

이번에는 queue 대기열에 들어온 작업을 여러 스레드에 나누어 동시적으로 수행하게 된다.

대표적인 concurrent queue인 global queue의 경우 대기열에 들어온 작업을 여러 스레드에 할당하여 동시에 작업을 수행하게 된다.

※ 참고로 어떤 스레드에 작업을 할당할 것인지는 OS에서 알아서 처리하기 때문에 어떤 작업이 몇번 스레드에 할당되는지는 중요하지 않고 한 스레드에서 순차적으로 수행되는지, 여러 스레드에서 동시에 진행되는지만 확인하면 된다.

코드에서 구현

이제부터 위의 개념을 토대로 코드 레벨에서 어떻게 구현할 수 있는지 알아보자

기본적인 형태는 다음과 같다.

import Foundation

DispatchQueue.{큐종류}({qos옵션}).{async/sync} {
	//Task
}

async 혹은 sync 메서드로 전달되는 클로저를 하나의 작업 단위로 보면된다.

그렇다면 아래의 코드들이 의미하는 바가 무엇인지 하나씩 살펴보자

import Foundation

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

위의 코드는 기존 스레드에서 벗어나 작업을 global queue로 전달하고 전달한 작업은 현재 스레드에서 async(비동기)로 기다린다는 의미이다.

import Foundation

DispatchQueue.main.async {
	//Task
}

//아래 코드는 에러 발생
//DispatchQueue.main.sync {}

위의 코드는 기본 스레드에서 벗어나 main queue에 작업을 전달하고 전달한 작업을 현재 스레드에서 async(비동기)로 기다린다는 의미이다.

참고로 main queue에서는 sync 방식은 사용할 수 없다.

이유를 간단히 설명하자면 위에서도 설명 했듯이 기본적으로 main 스레드에서는 UI와 관련된 작업을 수행하게 된다. 만약 main 스레드에서 sync로 네트워크 작업과 같이 시간이 비교적 오래 걸리는 작업을 전달하게 된다면 main 스레드는 작업을 순차적으로 처리하기 때문에 네트워크 작업을 하는 동안 다른 작업을 수행할 수 없다. 즉, UI와 관련된 작업을 수행할 수 없기 때문에 앞선 작업이 끝날때까지 화면이 멈추게 된다. 만약 이러한 작업이 예상보다 오래 걸린다면 DeadLock교착상태에 빠져 앱이 멈춘것처럼 보이기 때문에 Apple에서 의도적으로 사용할 수 없도록 설계한게 아닌가 싶다.

이제 실제 Task에서 수행할 코드를 작성하여 결과 값과 함께 코드를 살펴 보도록 하자

import Foundation

DispatchQueue.main.async {
    for _ in 0..<10 {
        print("🐱")
    }
}

DispatchQueue.main.async {
    for _ in 0..<5 {
        print("🐶")
    }
}

//🐱
//🐱
//🐱
//🐱
//🐱
//🐱
//🐱
//🐱
//🐱
//🐱
//🐶
//🐶
//🐶
//🐶
//🐶

위의 코드는 수행할 작업을 비동기main queue에 전달하고 있다. main queue의 경우 작업이 들어온 순차적으로 수행하기 때문에 🐱가 모두 출력된 후에 🐶가 출력되는 것을 볼 수 있다.

DispatchQueue.global().async {
    for _ in 0..<10 {
        print("🐷")
    }
}

DispatchQueue.global().async {
    for _ in 0..<5 {
        print("🐻")
    }
}

//🐻
//🐷
//🐷
//🐻
//🐷
//🐷
//🐷
//🐻
//🐷
//🐻
//🐷
//🐻
//🐷
//🐷
//🐷

위의 코드는 수행할 작업을 비동기global queue에 전달하고 있다. global queue는 동시적으로 작업을 수행하기 때문에 🐻 출력과 🐷 출력이 동시에 수행되고 있는 것을 확인할 수 있다.

DispatchQueue.global().sync {
    for _ in 0..<5 {
        print("🐼")
    }
}

DispatchQueue.global().async {
    for _ in 0..<5 {
        print("🐮")
    }
}

//🐼
//🐼
//🐼
//🐼
//🐼
//🐮
//🐮
//🐮
//🐮
//🐮

위의 코드도 마찬가지로 수행할 작업을 global queue로 전달하고 있다. 하지만 global queue는 동시적으로 작업을 수행할 수 있음에도 작업을 sync로 전달하도록 하여 해당 작업이 모두 끝날때까지 다음 작업을 수행하지 못하기 때문에 🐼가 모두 출력된 후에 🐮가 출력되는 것을 확인할 수 있다.

global(qos:)

지금까지 global queue에 대한 Dispatch Queue를 생성하기 위해 global() 메서드를 사용하였는데 사실 이 global() 메서드는 qos라는 파라미터를 가지고 있고 기본값으로 .default가 설정되어 있다.

qos 파라미터의 의미는 quality-of-service의 약자로 queue로 전달되는 작업의 우선순위를 결정할 수 있게 한다.

이번 포스트에서는 간단히 어떤 종류의 우선순위가 있는지 살펴본다.

  • userInteractive
  • userInitiated
  • default
  • background
  • unspecified

우선순위는 위에서 아래로 높음에서 낮음 순

각 우선순위에 대한 설명은 Apple Developer 문서에서 확인


Reference

https://zeddios.tistory.com/516
https://zeddios.tistory.com/519
https://sujinnaljin.medium.com/ios-%EC%B0%A8%EA%B7%BC%EC%B0%A8%EA%B7%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-gcd-5-c8e6eee3327b
https://sujinnaljin.medium.com/ios-%EC%B0%A8%EA%B7%BC%EC%B0%A8%EA%B7%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-gcd-4-a621eca0a1d2
https://sujinnaljin.medium.com/ios-%EC%B0%A8%EA%B7%BC%EC%B0%A8%EA%B7%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-gcd-3-1e706a74086b
https://www.youtube.com/watch?v=EL5n2U8XNpQ&list=PLqK3bFbiW77MBn0ofPRjGDBU9Ytnz9-6S&ab_channel=iOS%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%95%A8%EB%9F%B0
https://developer.apple.com/documentation/dispatch/dispatchqueue

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글