Dispatch Queue(GCD) 사용 시 주의사항

Dispatch Queue(GCD)를 사용할 때는 4가지 주의사항이 존재합니다.

📌 1. 반드시 메인큐에서 처리해야하는 작업(UI 관련 작업)

iOS에서 업데이트 사이클(Update Cycle: 이벤트 결과를 화면에 출력) 작업을 수행할 수 있는 쓰레드는 오직 1번 쓰레드(Main Thread)뿐입니다.

때문에 다른 쓰레드에서 작업을 수행하던 중 UI 관련 작업이 시작되면, 해당 작업을 메인 큐(Main Queue)를 통해 1번 쓰레드로 보내야 합니다.

즉, UI 관련 작업은 1번 쓰레드(Main Thread)만 할 수 있으며, 다른 쓰레드에서 UI 작업을 만나면 1번 쓰레드로 보내야 합니다.


✅ 해결 코드

DispatchQueue.global().async {
    // 다양한 작업 (UI 제외)
    DispatchQueue.main.async {
        // UI 작업
    }
}

✅ UI 관련 작업을 다른 쓰레드에서 작업 (UI 작업 작동 X)

✅ UI 관련 작업을 1번 쓰레드(main thread)에서 작업 (UI 작업 작동 O)


📌 2. 컴플리션 핸들러(Completion Handler)의 존재이유 - 올바른 콜백함수의 사용 ⭐️⭐️⭐️

iOS에서 비동기(Async) 처리 방식은 "해당 작업을 기다리지 않고 다음 작업을 진행"하는 방식입니다.

이러한 방식은 작업을 분산 처리하여 성능을 높인다는 장점을 가지고 있지만, 특정 작업의 함수 결과물을 의존/사용하는 다른 작업이 존재할 경우 에러가 발생할 수 있습니다.

위 그림처럼 서로 다른 쓰레드에서 task1과 task2가 동시에 진행 하려 하면 에러가 발생합니다.
(task2의 작업 진행은 task1의 결과물에 의존하기 때문)


🤔 이처럼 특정 함수의 작업 결과를 의존/사용하는 다른 함수가 존재할 때는 어떡하지??

  • 특정 함수의 작업 결과를 의존하는 다른 함수가 존재할 경우 특정 함수의 작업 결과를 반환(return)하는 방식으로 할 수를 설계해서는 안 됩니다.

  • 에러(nil) 없는 작동을 위해 작업 결과를 반환(return)하지 않는 형태(Void) + 클로저를 호출할 수 있는 형태로 설계해야 합니다.

    즉, 비동기 작업의 끝나는 시점을 파악하여 해당 작업 결과를 클로저로 전달하는 형태로 설계해야 합니다.

✅ 잘못된 함수 설계

비동기적인 작업을 해야 하는 함수를 설계할 때 return을 통해서 데이터를 전달하려면 항상 nil이 반환됩니다.

var resultName: String?

func myName(name: String) -> String?{  // n번 쓰레드에서 작업
    DispatchQueue.global().async {
        sleep(2)
        resultName = name
    }
    return resultName
}

print(myName(name: "김철수")) // print() 함수는 n번 쓰레드의 함수 결과를 사용
/* 출력 결과
 nil  // myName() 함수의 작업이 끝나기 전에 print() 함수의 작업이 진행하기 때문에 nil(에러) 출력
 */

✅ 올바른 함수 설계

비동기적인 작업을 해야 하는 함수는 항상 클로저를 호출할 수 있도록 함수를 설계해야 합니다.

var resultName: String?

func myName(name: String, completionHandler: @escaping (String?) -> Void){  // n번 쓰레드에서 작업
    DispatchQueue.global().async {
        sleep(2)
        resultName = name
        completionHandler(resultName)
    }
}

myName(name: "김철수") { XXX in
    print(XXX)
}

/* 출력 결과
 Optional("김철수")
 */

📌 3. weak, strong 캡처의 주의

객체 내에서 비동기 코드를 사용할 때는 약한(weak)/강한(strong) 참조를 생각하면서 코드를 작성해야 합니다.

참고: Swift의 캡처(Capture)와 캡처 리스트(Capture List)

✅ 강한 참조 사이클(Strong Reference Cycle)

캡처 리스트 안에서 weak self로 선언하지 않으면 강한 참조 발생

class Man{
    var name: String
    var run: (()->Void)?
  
    init(name: String){
        self.name = name
    }
    func runClosure(){
        DispatchQueue.global().async {
            self.run = {
                print("\(self.name)이 달리고 있습니다.")
            }
        }
    }
  
    deinit{
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething(){
    var kim: Man? = Man(name: "김철수")  // kim 인스턴스 생성 (kim RC 1증가)
    kim?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething()  // 아무 출력 없음

✅ 캡처 리스트(Capture List) + 약한 참조(Weak Reference)

대부분의 경우, 캡처 리스트 안에서 weak self로 선언하여 약한 참조 하게끔 하는 것을 권장합니다.

class Man{
    var name: String
    var run: (()->Void)?
  
    init(name: String){
        self.name = name
    }
      
    func runClosure(){
        DispatchQueue.global().async {
            self.run = { [weak self] in
                print("\(self?.name)이 달리고 있습니다.")
            }
        }
    }
  
    deinit{
        print("\(self.name) 메모리에서 제거되었습니다.")
    }
}

func doSomething(){
    let kim: Man? = Man(name: "김철수")  // kim 인스턴스 생성 (kim RC 1증가)
    kim?.runClosure()  // 클로저(run)가 메모리의 Heap 영역에 생성
}

doSomething()  // 김철수 메모리에서 제거되었습니다.

📌 4. 동기(Sync)함수를 비동기(Async)적으로 동작하는 함수로 변형

작업 시간이 긴 함수들을 동기함수로 만들면 1번 쓰레드(Main Thread)에 과부하가 걸립니다.

이러한 이유로 작업 시간이 긴 함수를 내부에 비동기적 처리 하여 비동기로 동작하는 함수로 변형해야 합니다.

✅ 동기함수

myTask() 함수처럼 긴 작업시간을 가지고 있는 함수를 1번 쓰레드(Main Thread)에서 작업하게 되면 버벅임 등과 같은 현상(과부하)이 발생할 수 있습니다.

func myTask(programmingLanguage: String) -> String{
    print("수학 숙제 시작")
    sleep(2)
    print("수학 숙제 종료")
   
    print("\(programmingLanguage) 코딩 공부 시작")
    sleep(2)
    print("코딩 공부 종료")
   
    print("영어 숙제 시작")
    sleep(2)
    print("영어 숙제 종료")
   
    return "공부 종료"
}

myTask(programmingLanguage: "Swift")

✅ 동기함수를 비동기적으로 동작하는 함수로 변형

동기함수를 비동기적으로 동작하는 함수로 변형하는 함수를 생성하여 만들면 버벅임 등과 같은 현상(과부하)을 예방할 수 있습니다.

func myTask(programmingLanguage: String) -> String{
    print("수학 숙제 시작")
    sleep(2)
    print("수학 숙제 종료")
   
    print("\(programmingLanguage) 코딩 공부 시작")
    sleep(2)
    print("코딩 공부 종료")
   
    print("영어 숙제 시작")
    sleep(2)
    print("영어 숙제 종료")
   
    return "공부 종료"
}

func asyncMyTask(programmingLanguage: String, completionHandler: @escaping (String) -> Void){
    DispatchQueue.global().async {
        let function = myTask(programmingLanguage: programmingLanguage)
        completionHandler(function)
    }
}

asyncMyTask(programmingLanguage: "Swift") { XXX in
    print(XXX)
}
profile
응애 나 코린이(비트코인X 코딩O)

0개의 댓글