Dispatch Queue(GCD)를 사용할 때는 4가지 주의사항이 존재합니다.
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)
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("김철수") */
객체 내에서 비동기 코드를 사용할 때는 약한(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() // 김철수 메모리에서 제거되었습니다.
작업 시간이 긴 함수들을 동기함수로 만들면 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) }