Chapter 7: Operation Queues
Operations의 진정한 힘은 OperationQueue에서 작업을 처리할 때 나타난다. GCD의 DispatchQueue와 마찬가지로 OperationQueue 클래스(class)는 Operation의 작업 예약(manage the scheduling)과 동시에 실행할 수 있는 최대 operations 수를 관리하기 위해 사용하는 클래스(class)이다.
OperationQueue를 사용하면 다음 세 가지 방법으로 작업을 추가 할 수 있다:
- Pass an Operation.
- Pass a closure.
- Pass an array of Operations.
이전 장(chapter)에서 프로젝트(project)를 구현하면서 operation 자체는 동기(synchronous) 작업이라는 것을 알게 되었다. 기본 스레드(main thread)에서 GCD 대기열(queue)로 비동기(asynchronously) 전송(dispatch)할 수도 있지만, operations의 완전한 동시성(concurrency) 이점을 누리기 위해서는 OperationQueue에 추가하는 것이 좋다.
OperationQueue management
operation 대기열(queue)은 작업이 가지고 있는 서비스 품질(quality of service)과 종속성(dependencies)에 따라 준비(ready)된 작업을 실행한다. 대기열(queue)에 Operation을 추가하면, 작업이 완료(completed) 또는 취소(canceled)될 때까지 실행된다. 이후의 장(chapter)에서 종속성(dependencies)과 operations 취소(canceling)에 대해 배우게 된다.
OperationQueue에 Operation을 추가 한 후에는 다른 OperationQueue에 동일한 Operation을 추가 할 수 없다. Operation 인스턴스(instances)는 한 번만 실행되어 완료되는(once and done) 작업이므로, 필요한 경우 여러 번 실행할 수 있도록 서브 클래스(subclasses)로 만든다.
Waiting for completion
OperationQueue를 살펴보면, waitUntilAllOperationsAreFinished라는 메서드(method)가 있다. 이는 이름 그대로 수행된다. 해당 메서드(method)를 호출하려고 할 때마다, 메서드(method) 이름의 wait를 block으로 바꿔 생각한다. 호출하면 현재 스레드(current thread)가 차단되므로, 기본 UI 스레드(thred)에서 절대로 이 메서드(method)를 호출하면 안된다.
이 메서드(method)가 필요한 경우, 이 차단(blocking) 메서드(method)를 안전하게(safely) 호출(call)할 수 있는 private 직렬(serial) DispatchQueue를 설정해야 한다. 모든 operations이 완료(complete)될 때까지 기다릴 필요없이, 일련의 operations(set of operations)만 수행하는 경우에는 OperationQueue에서 addOperations(_:waitUntilFinished:) 메서드(method)를 대신 사용할 수 있다.
Quality of service
OperationQueue는 다른 서비스 품질(quality of service)을 가진 operations을 추가 할 수 있고, 해당 우선 순위(priority)에 따라 실행된다는 점에서 DispatchGroup과 유사하게 작동한다. 서비스 품질(quality of service) 수준(levels)에 대한 복습(refresher)이 필요한 경우 3장(chapter) "Queues & Threads"를 참조한다.
operation queue의 기본(default) 서비스 품질(quality of service) 수준(level)은 .background이다. operation queue에서 qualityOfService 속성(property)을 설정할 수 있지만, 대기열(queue)에서 관리하는 개별 작업에서 설정한 서비스 품질(quality of service)에 의해 이 속성이 재정의(overridden)될 수 있다는 것을 유의해야 한다.
Pausing the queue
isSuspended 속성(property)을 true로 설정하여 dispatch queue를 일시 정지(pause)할 수 있다. 실행 중(In- flight)인 operations은 계속 실행(run)되지만, isSuspended를 다시 false로 변경할 때까지 새로 추가 된 operations은 예약(scheduled)되지 않는다.
Maximum number of operations
때로는 한 번에 실행되는 operations 수를 제한하려고 할 수도 있다. 기본적(default)으로 dispatch queue는 장치(device)가 한 번에 처리 할 수 있는 만큼의 작업을 실행한다. 해당 숫자를 제한(limit)하려면 dispatch queue의 maxConcurrentOperationCount 속성(property)을 설정하면 된다. maxConcurrentOperationCount를 1로 설정하면, 효과적으로 직렬(serial) 대기열(queue)을 생성할 수 있다.
Underlying DispatchQueue
OperationQueue에 operations을 추가하기 전에, 기존 DispatchQueue를 underlyingQueue(기본 대기열)로 지정할 수 있다. 이 경우, dispatch queue의 서비스 품질(quality of service)이 operation queue에 설정한 서비스 품질(quality of service)보다 우선(override)한다.
기본 대기열(main queue)을 underlying queue로 지정해선 안 된다.
Fix the previous project
이전(previous) 장(chapter)에서 틸트 시프트(tilt shift)를 처리하는 operation을 설정했지만, 동기적(synchronously)으로 실행되었다. 이제 OperationQueue에 익숙해 졌으므로 해당 프로젝트(project)가 제대로 작동하도록 수정한다. 기존 프로젝트(project)를 계속 진행하거나, 이 장(chapter)의 시작(starter)에서 Concurrency.xcodeproj를 사용해도 된다.
UIActivityIndicator
가장 먼저 변경해야 할 것은 UIActivityIndicator를 추가하여 사용자에게 어떤 일이 일어나고 있다는 것을 알리는 것이다. Main.storyboard를 열고 Tilt Shift Table View Controller Scene을 선택한다. 십자형(crosshairs) 포인터가 양방향으로 나타나도록 activity indicator를 이미지의 중앙으로 드래그(drag)한다.
이후, activity indicator에서 이미지 뷰(image view)로 대각선 Control-drag 한다. 팝업(pop-up)이 나타나면 Shift키를 누른 상태에서 Center Vertically과 Center Horizontally를 모두 선택한다.
속성 관리자(Attributes inspector)에서 behaviors 섹션(section)의 Animating과 Hides When Stopped을 선택한다.
PhotoCell.swift를 연다. activityIndicator라는 새 @IBOutlet을 추가하고, 스토리 보드(storyboard)에서 새로 추가된 activity indicator와 연결한다:
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
다음으로 computed proeprty을 PhotoCell에 추가한다:
var isLoading: Bool {
get { return activityIndicator.isAnimating }
set {
if newValue {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
}
activityIndicator 속성(property)을 public으로 하고 메서드(methods)를 직접 호출할 수도 있지만, UIKit 관련 논리(logic)가 추상(abstraction) 상위 계층(higher layers)으로 유출되는 것을 피하기 위해, UI 요소 및 outlets를 노출시키지 않는 것이 좋다. 예를 들어, 이 indicator를 다운로드의 어느 시점에서 사용자 지정(custom) 구성 요소(component)로 교체할 수 있다.
Updating the table
TiltShiftTableViewController.swift을 연다. 대기열(queue)에 operations을 추가하려면 이를 생성해야 한다. 클래스(class)의 가장 위에 다음 속성(property)을 추가한다:
private let queue = OperationQueue()
다음으로, tableView(_:cellForRowAt:)에서 image 선언(declaring)과 return cell 사이의 모든 것을 바꾼다:
let op = TiltShiftOperation(image: image)
op.completionBlock = {
DispatchQueue.main.async {
guard let cell = tableView.cellForRow(at: indexPath) as? PhotoCell else { return }
cell.isLoading = false
cell.display(image: op.outputImage)
}
}
queue.addOperation(op)
operation에서 start를 수동(manually)으로 호출(calling)하는 대신, 시작(starting)하고 완료(completing)할 operation을 대기열(queue)에 추가한다. 또한 대기열(queue)은 기본적으로 background에서 실행되므로, 더 이상 기본 스레드(main thread)를 차단하지 않는다.
operation이 완료(completes)되면, completeBlock이 인수(arguments)없이 호출(called)되며 반환 값(return value)은 없다. 즉시 기본 UI 스레드(main UI thread)로 변경해 코드를 완성한다. 테이블 뷰 셀(table view cell)에 대한 참조를 얻을 수있는 경우(스크롤되지 않은 경우), activity indicator를 끄고(turning off) 이미지를 업데이트하면 된다.
operation을 operation queue에 추가하자마자 작업이 예약된다. 더 이상 op.start()를 호출할 필요가 없다.
앱을 빌드(build)하고 실행(run)하여 테이블 뷰를 다시 스크롤해 본다.
코드가 비동기(asynchronous) 방식으로 실행(running)되고 있으므로, 테이블 뷰의 스크롤(scrolling)이 훨씬 매끄럽다. 이러한 변경은 operation을 사용해 실행되는 코드의 성능을 향상시키기는 데에 아무런 도움이 되지 않지만, UI가 잠기(locked)거나 끊기지(choppy) 않도록 한다.
GCD와 다른 점이 무엇인지 궁금할 수 있다. 지금까지는 차이가 없다. 그러나 다음 두 장(chapter)에서 operations의 활용을 확장하다 보면, 그 이유가 명확해질 것이다.