본문 바로가기
아이폰 개발/Swift

swift DispatchGroup 과 DispatchSemaphore 실무에 사용+ 실험

by 인생여희 2021. 12. 7.

swift DispatchGroup 과 DispatchSemaphore  실무에 사용+ 실험

 

 

ios 앱 개발할때 DispatchGroup과 semaphore 를 사용할 기회가 많이 없는데, 이번에 둘다 사용해 보게되었다.

먼저, 개발 중인 앱에는 결제 프로세스가 있다. 첫번째 요구사항 정의는 모든 결제는 한번에 하나만 할 수 있다 였다. 즉, 장바구니 개념이 없고, 결제 한번에 하나의 품목만 결제를 할 수 있다. 

 

결제 api 는 1. 결제 api 호출 , 2. 결제 확인 api 호출 순으로 총 2번 순서대로 호출해야했다. 호출이 완료되면 다음 화면을 push 해주면 되었다.

 

그래서 아래와 같이 1.결제처리 함수, 2. 결제확인함수를 만들어 주었다.

 

import UIKit
import Foundation

/// [1] 결제 처리 api
func doPay(_ data: Int, delay: Double, completionHandler: @escaping (String)->()) {
    print("doPay - 진입 :\(data) - delay:\(delay)")
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        print("doPay - 처리[**] :\(data) ")
        completionHandler("\(data)")
    }
    print("doPay - 종료 ")
}

/// [2] 결제 처리 확인응답 api
func payResponse(_ data: Int, delay: Double, completionHandler: @escaping (String)->()) {
    print("payResponse - 진입 :\(data) - delay:\(delay)")
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        print("payResponse - 처리[***] :\(data)\n")
        completionHandler("\(data)")
    }
    print("payResponse - 종료 :\(data)")
}

 

 

그리고 1.결제 api 호출 후 2.결제 확인 api 를 호출해 주어야 해서 1.결제 api 가 응답되고 호출 되는 콜백 함수 안에서 2. 결제 확인 api 를 호출 해 주었다. 그리고 DispatchGroup을 사용해서 두 api 가 모두 호출되면 notify 함수를 이용해서 다음 화면을 push 해주었다.

 

let group = DispatchGroup()
var text = ""

group.enter()

/// [1] 결제처리
doPay(0, delay: Double.random(in: 0.1...3)) {_ in
    
    text += "💶pay0- "
    
    ///[2] 확인처리
    payResponse(0, delay: Double.random(in: 0.1...3)) {_ in
        text += "📡res0- "
        
        group.leave()
    }
}


group.notify(queue: DispatchQueue.main) {
    print("📲 끝- 다음화면으로 : \(text)")
}

 

결과는 아래와 같이 순서대로 작동이 된다.

doPay - 진입 :0 - delay:0.4925298720140009
doPay - 종료 
doPay - 처리[**] :0 
payResponse - 진입 :0 - delay:0.3396354870342088
payResponse - 종료 :0
payResponse - 처리[***] :0

📲 끝- 다음화면으로 : 💶pay0- 📡res0-

 

🧨 문제는 얼마지나지 않아 발생했다. 결제에 장바구니 기능이 추가되었다. 사용자는 1건이 아니라 여러건 결제를 할 수 있다. 나는 단순하게 for 문을 사용하면 되지 않나라고 너무나 너무나 황당한 생각을 해버리고 만다. 일단 실험을 해보았다. for 문을 사용해서 결제 3건, 즉 3번을 돌렸다.

 

let group = DispatchGroup()
var text = ""
// 결제 3건
for i in 0..<3 {
    
    group.enter()
    
    /// [1] 결제처리
    doPay(i, delay: Double.random(in: 0.1...3)) {_ in
        
        text += "💶pay\(i)- "
        
        ///[2] 확인처리
        payResponse(i, delay: Double.random(in: 0.1...3)) {_ in
            text += "📡res\(i)- "

            group.leave()
        }
    }
}

group.notify(queue: DispatchQueue.main) {
    print("📲 끝- 다음화면으로 : \(text)")
}

 

 

결과는 아래와 같이 💶pay n- 📡res n .... 순으로 출력되지 않았다.  그리고 💶pay api 만 먼저 3번 호출되었다.!

당연한 결과였다. for문이 엄청 빠르게 , 1번 결제 api를 호출하고(2-3초걸림)....  ... 호출한 api 응답을 안기다리고... 또 1번결제 api를 호출해버리고(2-3초걸림)... 즉 비동기 방식으로 연속적으로 처리되기 때문이었다.  이건 요구사항에 어긋난다.

doPay - 진입 :0 - delay:2.862823393883068
doPay - 종료 
doPay - 진입 :1 - delay:2.7782999587612998
doPay - 종료 
doPay - 진입 :2 - delay:1.5717905658122133
doPay - 종료 
doPay - 처리[**] :2 
payResponse - 진입 :2 - delay:2.2812485210527207
payResponse - 종료 :2
doPay - 처리[**] :1 
payResponse - 진입 :1 - delay:0.5266285057299884
payResponse - 종료 :1
doPay - 처리[**] :0 
payResponse - 진입 :0 - delay:1.6996043597976123
payResponse - 종료 :0
payResponse - 처리[***] :1

payResponse - 처리[***] :2

payResponse - 처리[***] :0

📲 끝- 다음화면으로 : 💶pay2- 💶pay1- 💶pay0- 📡res1- 📡res2- 📡res0-

 

 요구사항은 결제가 한건이든 두건이던 여러건이든 1.결제 api 가 호출되고, 그 후에 2.결제확인 api 가 순서대로 호출되어야 한다. 순서대로! 이 순서대로를 만족시킬 수 있는 녀석은 뭘까? 바로 세마포어다. 세마포어는 첫번재 for문 안의 내용이 다 끝날때 까지 기다린다. 그 후 두번째 for 문을 실행한다. 그럼 세마포어는 어디다 넣어야 할까? for 문 끝나는 지점에서 wait를 시키고, 2.결제 확인 api 가 종료되는 지점에서 signal을 보내면 된다. 

 

var text = ""
let semaphore = DispatchSemaphore(value: 0)
let group = DispatchGroup()

/// semaphore 처리는 async 안에서
DispatchQueue.global().async {
    
    for i in 0..<3 {

        group.enter()
        
        /// [1] 결제처리
        doPay(i, delay: Double.random(in: 0.1...3)) {_ in
            
            text += "💶pay\(i) - "

            ///[2] 확인처리
            payResponse(i, delay: Double.random(in: 0.1...3)) {_ in
                text += "📡res\(i) - "
                
                /// [!!!] 아래서 기다리고 있는 semaphore 에게 종료되었다고 알림 [!!!]
                semaphore.signal()
                group.leave()
            }
            
        }
        
        ///[!!!]다음 for문 넘어가서 api call 하기전까지 이전에 payResponse 가 응답되도록 기다림[!!!]
        semaphore.wait()
    }
    
    group.notify(queue: DispatchQueue.main) {
        print("📲 끝- 다음화면으로 : \(text)")
    }
}

 

위의 코드를 실행시키면 어떤 결과가 나올까? 의도했던 대로 순서대로 출력이 되었다.

 

doPay - 진입 :0 - delay:1.3257324019562897
doPay - 종료 
doPay - 처리[**] :0 
payResponse - 진입 :0 - delay:0.3535576416484567
payResponse - 종료 :0
payResponse - 처리[***] :0

doPay - 진입 :1 - delay:2.9804089428142873
doPay - 종료 
doPay - 처리[**] :1 
payResponse - 진입 :1 - delay:2.1649215839054174
payResponse - 종료 :1
payResponse - 처리[***] :1

doPay - 진입 :2 - delay:0.24332565453172345
doPay - 종료 
doPay - 처리[**] :2 
payResponse - 진입 :2 - delay:0.10224742295913322
payResponse - 종료 :2
payResponse - 처리[***] :2

📲 끝- 다음화면으로 : 💶pay0 - 📡res0 - 💶pay1 - 📡res1 - 💶pay2 - 📡res2 -

 

DispatchGroup과 DispatchSemaphore 에 대한 개념과 코드는 아래 포스트에 잘 설명해 놓았다.

 

swift DispatchGroup 과 DispatchSemaphore - 1

swift DispatchGroup 과 DispatchSemaphore - 2