본문 바로가기

카테고리 없음

DispatchQueue, DispatchGroup를 사용하여 비동기적 함수 작성하기

본 글은 인프런 엘런님의 강의를 수강하며 공부한 것을 정리한 내용입니다.

강의 :  https://www.inflearn.com/course/iOS-Concurrency-GCD-Operation/dashboard

 

본 포스팅은 평소에 동기적으로 작동하는 함수를 비동기적으로 작동하도록 고쳐나가는 과정을 포함한다.

DispatchQueue를 활용하여 작업들을 비동기적으로 보내고 각 작업들이 끝난 후 escaping 클로저를 통해 작업이 끝났음을 알린다.

더 나아가 DispatchGroup을 활용하여 작업을 그룹단위로 묶어 처리하도록 한다.

 

1. DispatchQueue를 활용한 작업의 비동기 처리 방식

코드를 작성하기 앞서 작업을 비동기로 처리한다는 의미를 짚고 넘어가겠다.

 

기존에 (예를 들어 viewDidLoad코드 안) 코드를 작성하면 작업들이 전부 메인스레드에서 차례로 처리된다.

 

동기적으로 모든 작업을 하나의 스레드에서 진행

이는 하나의 스레드에서 모든 일을 다 처리하게 되고 비효율적으로 앱이 작동하게 되므로 비동기로 처리할 수 있는 작업들은 비동기로 처리하여 효율을 높일 수 있다.

 

이때 비동기라는 의미는 작업을 다른 스레드로 보내고, 다른 스레드로 보낸 작업이 끝나든 말든 다시 원래의 스레드로 돌아와 그 다음 작업을 처리하는것을 의미한다. 

 

비동기로 작업을 다른 스레드로 보내버림

task1을 다른 스레드로 보내버린 후, 즉시 돌아와 그 다음 작업인 task2를 수행한다.

 

Swift에서는 해당 비동기 작업을 DispatchQueue를 통해 구현할 수 있고, async 를 통해 비동기 작업을 수행할 수 있다.

 

2. 동기적 함수를 비동기적 함수로 바꾸기

간단한 함수 하나를 작성하자.

하나의 튜플을 인수로 받아 튜플을 구성하는 수의 합을 리턴하는 함수이다. 다만 2초정도 쉬고 리턴하는 함수이다.

 

func slowAdd(_ tuple: (Int, Int)) -> Int {
    sleep(2)
    return tuple.0 + tuple.1
}

 

그 다음엔 튜플 배열을 하나 만들어 for문을 통해 배열의 모든 원소를 slowAdd 함수를 통해 더하는 작업을 진행한다.

     let time = timeCheck {
            let array: [(Int, Int)] = [(0,1), (1,3), (1,2), (55,32), (12,31), (2,5)]
            array.forEach { element in
                let res = slowAdd(element)
                print("slow add 결과: \(res)")
            }
        }
        
     print("걸린 시간 : \(time)")

 

결과는 다음과 같다.

 

이제 방금 위의 작업 중 더하는 작업을 DispatchQueue global 큐를 통해 비동기로 작업할것이다.

 

비동기로 작업을 진행하면 보다 더 일을 분배시킬 수 있고, 효율적이게 된다는 점이 있지만 다른 스레드로 보낸 작업들이 처리 완료될 때가 언제인지 알지 못한다는 점이 발생한다.

이를 위해 각 작업들이 끝날때 마다 escaping 클로저를 통해 비동기로 처리한 작업이 끝났음을 알려주는 장치를 마련한다.

 

그렇다면 비동기 처리를 위한 함수에 필요한 요소는

1. 작업을 처리할 비동기 큐

2. 작업이 끝났음을 알려주는 escaping 클로저

3. escaping 클로저 내의 작업을 처리할 큐 (이 또한 비동기로 작동해야한다.) 

가 된다.

 

해당 요소를 인자로 받고, 함수 내에서는 작업을 비동기로 처리할 수 있도록 큐에다 등록하고, 결과를 escaping 클로저로 전달한다.

코드로 작성하면 아래와 같다.

 

func asyncAddTask(_ tuple: (Int, Int), workingQueue: DispatchQueue, completionQueue: DispatchQueue, completion: @escaping (Error?, Int) -> ()) {
    
    // 작업 비동기적으로 진행
    workingQueue.async {
        var error: Error?
        error = .none
        
        // 덧셈 작업 처리
        let res = slowAdd(tuple)
        
        // 결과를 컴플리션 핸들러로 전달. 결과 또한 비동기로 처리
        completionQueue.async {
            completion(error, res)
        }
    }
}

 

이렇게 되면 비동기로 처리하는 함수 완성이다. 이제 실행시켜보자.

 let workingQueue = DispatchQueue.global()
        let completionQueue = DispatchQueue.global()
        
        let time = timeCheck {
            let array: [(Int, Int)] = [(0,1), (1,3), (1,2), (55,32), (12,31), (2,5)]
            array.forEach { element in
                asyncAddTask(element,
                             workingQueue: workingQueue, completionQueue: completionQueue) { error, res in
                    print("slow add 결과: \(res)")
                }
            }
        }
        
        print("걸린 시간 : \(time)")

 

결과는 아래와 같다.

다소 처음에 실행했던 결과와 많이 다른것을 확인할 수 있다.

걸린시간도 현저히 줄어들고 순서도 달라진것을 확인할 수 있다. 왜그럴까?? 

 

이는 여기서 걸린시간이란 asyncAddTask함수를 통해 작업을 다른스레드로 보내고 돌아온 시간을 의미하기 때문에 slow add 하느라 걸린 시간은 포함되지 않기 때문이다.

 

또한 작업을 각각 다른 스레드로 보낼 수 있기 때문에 slow add한 결과 순서도 차례되로 되지 않았음을 확인할 수 있다.

 

 

3. DispatchGroup을 활용하여 작업을 그룹화하고 그룹단위의 작업이 끝난 순간을 알아내 활용하기.

DispatchGroup은 작업을 그룹화 시키는 것이다.

내가 비동기 큐로 보낸 작업들을 하나의 그룹으로 묶어 관리하고 싶다면 DispatchGroup을 사용하면된다.

이렇게 작업을 그룹화한다는것은 예를들어 앱을 켰을때, 모든 이미지와 텍스트들이 서버로부터 넘어와 화면에 보여야 하는 것을 하나의 작업그룹으로 묶는것을 들 수 있다. 이때 해당 그룹의 작업들이 모두 끝나야만 사용자는 해당 화면의 버튼을 누를 수 있게 할 수 있도록 구현할 수 있다.

 

 

 

작업을 그룹화

 

작업을 그룹화 시키는 방법에는 두가지가 있다.

첫번째는 DispatchQueue에 작업을 등록할때 그룹을 지정해주는 것이다.

코드는 아래와 같다.

 

let group = DispatchGroup()
        let array: [(Int, Int)] = [(0,1), (1,3), (1,2), (55,32), (12,31), (2,5)]
        array.forEach { element in
            workingQueue.async(group: group) {
                slowAdd(element)
            }
        }

 

두번째는 DispatchGroup().enter()로 그룹안에 진입하고, 작업이 끝날 때 DispatchGroup().leave()를 사용하여 그룹안에 작업을 넣는것이다.

이것은 만약 비동기 클로저 내 비동기 함수같이 비동기큐에서 비동기로 보내고 그 안에서 비동기로 보내고....처럼 비동기로 보내진 작업들을 그룹화시킬때 사용될 수 있다.

 

코드는 아래와 같다.

let group = DispatchGroup()
        let array: [(Int, Int)] = [(0,1), (1,3), (1,2), (55,32), (12,31), (2,5)]
        
        array.forEach { element in
            group.enter()
            workingQueue.async {
                slowAdd(element)
                group.leave()
            }
        }

 

작업 들어가기전에 enter(), 작업 끝나고 코드 블럭을 탈출하기 전에 leave()를 통해 그룹에 넣고자 하는 작업들을 관리할 수 있다.

 

작업들을 그룹화하면 하나의 단위로 만든 작업들이 언제 끝나는지 알 수 있다.

컴플리션 핸들러를 사용해 작업 하나하나의 끝나고 나서의 액션을 취하는 것이 아니라, 어떠한 단위의 작업들을 끝내고 나서 (예를 들면 앱을 실행하고 나서 서버에서 초기 데이터 전부 받아와 메인에 로딩 완료시키기) 액션을 취할 수 있다는 것이다.

 

이 그룹화한 작업들을 끝나고나서 알려줘 의 기능을 하는것이 DispatchGroup().notify(queue:) {} 이다.

notify 뒤의 코드블럭 내에 작업그룹이 끝난 후 해야할 행동들을 입력할 수 있다.

 

예시는 아래와 같다.

        let group = DispatchGroup()
        let array: [(Int, Int)] = [(0,1), (1,3), (1,2), (55,32), (12,31), (2,5)]
        array.forEach { element in
            group.enter()
            workingQueue.async {
                slowAdd(element)
                group.leave()
            }
        }
        
        group.notify(queue: completionQueue) {
            print("========덧셈이 끝났습니다==========")
        }

 

이제 이 DispatchGroup을 활용해 위에서 작성한 비동기 함수를 그룹화 하는 함수를 더 만들어보자.

추가되는것은 DispatchGroup이고, asyncAddTask 함수를 호출하기전, 후 (completion 클로저)에 그룹에 넣고 빼고 하는 과정만 추가하면 된다.

 

디스패치 그룹을 활용하여 그룹화하는 비동기 함수 코드는 아래와 같다.

func asyncGroupTask(_ tuple: (Int, Int), workingQueue: DispatchQueue, group: DispatchGroup,  completionQueue: DispatchQueue, completion: @escaping (Error?, Int) -> ()) {
    
    group.enter()
    asyncAddTask(tuple, workingQueue: workingQueue, completionQueue: completionQueue) { error, res in
        completion(error, res)
        group.leave()
    }
    
}

 

 

활용 예시는 아래와 같다.

let time = timeCheck {
            tuple.forEach { element in
                asyncGroupTask(element, workingQueue: workingQueue, group: group, completionQueue: completionQueue) { error, res in
                    
                    print("slow add 결과: \(res)")
                }
            }
        }
        
        print("걸린 시간: \(time)")

 

실행 결과