ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DispatchQueue와 DispatchWorkItem 비교하기
    Apple🍎/Swift 2025. 4. 17. 18:50

    DispatchQueue란?

    DispatchQueue는 작업들을 FIFO(First-In-First-Out) 순서로 관리하고 실행하는 객체입니다. 주로 비동기 작업을 관리하고 백그라운드 스레드에서 코드를 실행할 때 사용합니다.

    // 글로벌 큐를 사용한 비동기 작업
    DispatchQueue.global().async {
        // 백그라운드에서 실행할 코드
        print("백그라운드 작업 실행 중...")
    }
    
    // 메인 큐를 사용한 UI 업데이트
    DispatchQueue.main.async {
        // UI 업데이트 코드
        print("메인 스레드에서 UI 업데이트 중...")
    }
    

    DispatchWorkItem이란?

    DispatchWorkItem은 실행 가능한 코드 블록을 캡슐화한 객체입니다. 작업을 객체화함으로써 취소, 지연 실행, 완료 통지 등 더 세밀한 제어가 가능합니다.

    let workItem = DispatchWorkItem {
        print("DispatchWorkItem이 실행되었습니다.")
    }
    
    // 디스패치 큐에서 워크아이템 실행
    DispatchQueue.global().async(execute: workItem)
    
    // 또는 직접 실행
    workItem.perform()
    

    기본 사용법 비교

    1. 간단한 비동기 작업

    DispatchQueue만 사용할 경우

    func fetchData() {
        DispatchQueue.global().async {
            // 데이터 가져오기
            let data = self.performNetworkRequest()
            
            DispatchQueue.main.async {
                // UI 업데이트
                self.updateUI(with: data)
            }
        }
    }
    

    DispatchWorkItem을 사용할 경우

    func fetchData() {
        let workItem = DispatchWorkItem {
            // 데이터 가져오기
            let data = self.performNetworkRequest()
            
            DispatchQueue.main.async {
                // UI 업데이트
                self.updateUI(with: data)
            }
        }
        
        DispatchQueue.global().async(execute: workItem)
    }
    

    이 간단한 예시에서는 큰 차이가 없어 보이지만, 좀 더 복잡한 시나리오에서 DispatchWorkItem의 장점이 드러납니다.

    DispatchWorkItem 활용하기 

    1. 작업 취소

    DispatchWorkItem의 가장 큰 장점 중 하나는 작업을 취소할 수 있다는 점입니다.

    var dataFetchWorkItem: DispatchWorkItem?
    
    func fetchDataWithCancellation() {
        // 이전 작업이 있다면 취소
        dataFetchWorkItem?.cancel()
        
        // 새 작업 생성
        let newWorkItem = DispatchWorkItem {
            // 작업 중간에 취소 여부 확인
            if !Thread.current.isCancelled {
                let data = self.performLongNetworkRequest()
                
                // 다시 한번 취소 여부 확인
                if !Thread.current.isCancelled {
                    DispatchQueue.main.async {
                        self.updateUI(with: data)
                    }
                }
            }
        }
        
        // 작업 참조 저장
        dataFetchWorkItem = newWorkItem
        
        // 작업 실행
        DispatchQueue.global().async(execute: newWorkItem)
    }
    
    func cancelFetch() {
        dataFetchWorkItem?.cancel()
    }
    

    이러한 기능은 사용자가 화면을 빠르게 전환하거나, 검색어를 연속해서 입력하는 경우 등 이전 작업을 취소해야 할 때 매우 유용합니다.

    2. 작업 완료 통지

    작업이 완료되었을 때 알림을 받을 수 있습니다

    func fetchDataWithNotification() {
        let workItem = DispatchWorkItem {
            let data = self.performNetworkRequest()
        }
        
        // 작업 완료 시 실행될 코드
        workItem.notify(queue: .main) {
            print("데이터 가져오기 완료!")
            self.hideLoadingIndicator() // 작업 완료시 메인쓰레드에 해당 작업 요청
        }
        // 백그라운드에 비동기 작업 실행하고 
        DispatchQueue.global().async(execute: workItem)
        self.showLoadingIndicator() // 로딩 인디케이터 보여주고 
    }

    3. 지연 실행과 취소

    특정 시간 후에 작업을 실행하고, 필요시 취소하는 기능을 구현할 수 있습니다.

    var searchWorkItem: DispatchWorkItem?
    
    func searchWithDebounce(query: String) {
        // 이전 검색 작업 취소
        searchWorkItem?.cancel()
        
        // 새 검색 작업 생성
        let newWorkItem = DispatchWorkItem {
            self.performSearch(query: query)
        }
        
        // 작업 참조 저장
        searchWorkItem = newWorkItem
        
        // 0.5초 후에 검색 실행
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: newWorkItem)
    }
    

    이 패턴은 사용자가 타이핑을 멈추고 일정 시간이 지난 후에만 검색을 실행하는 "디바운싱(debouncing)" 구현에 매우 유용합니다.

    실용적인 예제

    예제 1: 이미지 로더 구현

    DispatchQueue만 사용한 경우

    class ImageLoader {
        func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
            DispatchQueue.global().async {
                guard let data = try? Data(contentsOf: url),
                      let image = UIImage(data: data) else {
                    DispatchQueue.main.async {
                        completion(nil)
                    }
                    return
                }
                
                DispatchQueue.main.async {
                    completion(image)
                }
            }
        }
    }
    

    DispatchWorkItem을 사용한 개선된 버전

    class ImageLoader {
        private var loadTasks: [URL: DispatchWorkItem] = [:]
        
        func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
            // 이전 작업 취소
            cancelLoad(for: url)
            
            let loadTask = DispatchWorkItem {
                guard let data = try? Data(contentsOf: url),
                      let image = UIImage(data: data) else {
                    DispatchQueue.main.async {
                        completion(nil)
                    }
                    return
                }
                
                DispatchQueue.main.async { [weak self] in
                    self?.loadTasks[url] = nil
                    completion(image)
                }
            }
            
            // 작업 저장 및 실행
            loadTasks[url] = loadTask
            DispatchQueue.global().async(execute: loadTask)
        }
        
        func cancelLoad(for url: URL) {
            loadTasks[url]?.cancel()
            loadTasks[url] = nil
        }
        
        func cancelAllLoads() {
            loadTasks.values.forEach { $0.cancel() }
            loadTasks.removeAll()
        }
    }
    

    개선된 버전은 다음과 같은 이점이 있습니다

    • 중복 요청 관리: 동일한 URL에 대한 이전 요청을 취소
    • 메모리 관리: 작업이 완료되면 참조를 제거
    • 일괄 취소: 모든 로드 작업을 한 번에 취소할 수 있음

    예제 2: 타임아웃 구현

    DispatchWorkItem으로 타임아웃 구현

    func performTaskWithTimeout(timeoutSeconds: Double, completion: @escaping (Result<Data, Error>) -> Void) {
        let workItem = DispatchWorkItem {
            // 네트워크 요청 등 시간이 오래 걸릴 수 있는 작업
            do {
                let data = try self.performLongRunningTask()
                
                // 작업이 취소되지 않았는지 확인
                if !workItem.isCancelled {
                    DispatchQueue.main.async {
                        completion(.success(data))
                    }
                }
            } catch {
                if !workItem.isCancelled {
                    DispatchQueue.main.async {
                        completion(.failure(error))
                    }
                }
            }
        }
        
        // 작업 실행
        DispatchQueue.global().async(execute: workItem)
        
        // 타임아웃 설정
        DispatchQueue.main.asyncAfter(deadline: .now() + timeoutSeconds) {
            if !workItem.isCancelled {
                workItem.cancel()
                let timeoutError = NSError(domain: "com.example", code: -1, userInfo: [NSLocalizedDescriptionKey: "작업 시간 초과"])
                completion(.failure(timeoutError))
            }
        }
    }
    

    이 패턴은 네트워크 요청이나 오래 걸리는 작업에 타임아웃을 설정할 때 매우 유용합니다.

    예제 3: 검색 자동완성 구현

    class SearchController {
        private var searchWorkItem: DispatchWorkItem?
        
        func search(query: String) {
            // 이전 검색 취소
            searchWorkItem?.cancel()
            
            // 검색어가 비어있으면 결과 초기화
            if query.isEmpty {
                self.updateSearchResults([])
                return
            }
            
            // 새 검색 작업 생성
            let workItem = DispatchWorkItem { [weak self] in
                guard let self = self else { return }
                
                // 네트워크 요청으로 자동완성 결과 가져오기
                let results = self.fetchAutocompleteSuggestions(for: query)
                
                // 메인 스레드에서 UI 업데이트
                if !workItem.isCancelled {
                    DispatchQueue.main.async {
                        self.updateSearchResults(results)
                    }
                }
            }
            
            // 작업 참조 저장
            searchWorkItem = workItem
            
            // 0.3초 지연 후 검색 실행 (디바운싱)
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.3, execute: workItem)
        }
        
        private func fetchAutocompleteSuggestions(for query: String) -> [String] {
            // 실제 구현에서는 네트워크 요청 또는 로컬 데이터베이스 검색
            return ["결과 1", "결과 2", "결과 3"]
        }
        
        private func updateSearchResults(_ results: [String]) {
            // UI 업데이트 로직
            print("검색 결과 업데이트: \(results)")
        }
    }
    

    이 예제는 사용자가 검색창에 타이핑할 때마다 이전 검색을 취소하고 짧은 지연 후에 새 검색을 실행하는 디바운싱 패턴을 구현합니다. 이는 UX 개선과 서버 부하 감소에 도움이 됩니다.

    언제 DispatchWorkItem을 사용해야 할까?

    일반적으로 다음과 같은 경우에 DispatchWorkItem이 유용합니다:

    1. 작업 취소가 필요한 경우
      • 사용자 인터랙션에 의해 진행 중인 작업을 취소해야 할 때
      • 화면 전환이나 뷰 컨트롤러 해제 시 백그라운드 작업을 취소해야 할 때
    2. 작업 완료 알림이 필요한 경우
      • 여러 작업이 완료된 후 특정 동작을 수행해야 할 때
      • 작업 완료 시점을 정확히 알아야 할 때
    3. 지연 실행과 타임아웃이 필요한 경우
      • 디바운싱이나 쓰로틀링을 구현할 때
      • 작업에 시간 제한을 설정할 때
    4. 작업 상태 추적이 필요한 경우
      • 작업의 취소 여부를 코드 내에서 확인해야 할 때
    5. QoS(서비스 품질) 설정이 필요한 경우
      • 작업 우선순위를 세밀하게 제어해야 할 때
    // QoS 설정 예시
    let highPriorityTask = DispatchWorkItem(qos: .userInitiated) {
        // 사용자 응답성에 중요한 작업
    }
    
    let lowPriorityTask = DispatchWorkItem(qos: .background) {
        // 백그라운드 처리 작업
    }
    

    댓글

Designed by Tistory.