-
Chapter 8: Asynchronous OperationsRaywenderlich/Concurrency by Tutorials 2020. 7. 10. 19:18
지금까지는 operations은 동기화(synchronous)되어 Operation 클래스(class)의 상태(state) 시스템(machine)과 매우 잘 작동했다. operation이 isReady 상태(state)로 전환되면, 시스템(system)은 사용 가능한 스레드(thread)를 검색할 수 있다.
스케줄러(scheduler)가 operation을 실행(run)할 스레드(thread)를 찾으면, operation은 isExecuting 상태(state)로 전환(transition)된다. 이 시점에서 코드가 실행(executes)되어 완료(completes)되고 상태(state)는 isFinished가 된다.
그러나 비동기(asynchronous) operation에서는 조금 다르게 작동한다. operation의 main 메서드(method)가 실행되면, 비동기(asynchronous) 작업이 시작된 다음 main이 종료(exits)된다. 비동기(asynchronous) 메서드(method)가 아직 완료(complete)되지 않았기 때문에, 이 시점에서 operation의 상태(state)를 isFinished로 전환(switch)할 수 없다.
Asynchronous operations
비동기(asynchronous) 메서드(method)를 operation에 래핑(wrap)하는 것이 가능하지만 조금 더 많은 작업이 필요하다. operation 실행이 완료(finished)되어도, operation은 자동으로(automatically) 상태(state)가 결정되지 않으므로 수동으로(manually) 변경해 관리할 필요가 있을 것이다. 그러나 설상가상으로(To make matters worse), 상태(state) 속성(properties)은 모두 읽기 전용(read-only)이다.
하지만 걱정할 필요 없다. 사실 상태(states) 관리는 매우 간단하다. 실제로, 사용하는 모든 비동기(asynchronous) operations이 상속(inherit)할 기본(base) 클래스(class)를 만드므로 다시 수행할 필요가 없다. 이 클래스(class)가 왜 프레임 워크(framework)의 일부가 아닌지 모르겠다.
AsyncOperation
이 장(chapter)의 시작(starter) 폴더(folder)의 AsyncAddOperation.playground를 연다. 코드를 추가하면 잠시 후에 컴파일(compilation) 오류(error)가 해결되므로 무시할 수 있습니다.
State tracking
operation 상태(state)는 읽기 전용(read-only)이므로, 먼저 읽기-쓰기(read-write) 방식으로 변경 사항을 추적(track)할 수 있는 방법을 제공해야한다. 파일 최상단에 State 열거형(enumeration)을 만든다:
extension AsyncOperation { enum State: String { case ready, executing, finished fileprivate var keyPath: String { return "is\(rawValue.capitalized)" } } }
6장(chapter) “Operations”에서 Operation 클래스(class)가 KVO 알림(notifications)을 사용한다고 언급했다. 예를 들어 isExecuting 상태(state)가 변경되면 KVO 알림(notification)이 전송된다. 그러나 위에서 작성한 상태(states)는 'is' 접두사(prefix)로 시작하지 않으며, Swift 스타일(style) 가이드(guide)에 따라 열거형(enum) 항목(entries)을 소문자(lowercased)로 표시한다.
computed property로 작성한 keyPath는 위에서 언급한 KVO 알림(notifications)을 지원하는 데 도움이 된다. 현재 State의 keyPath를 요청하면 상태(state)값의 첫 글자를 대문자(capitalize)로하고 텍스트 앞에 접두사(prefix)를 붙인다. 따라서 상태(state)가 실행(executing)으로 설정되면, keyPath는 isExecuting을 반환(return)하며, 이는 Operation 기본 클래스(base class)의 속성과 일치한다.
fileprivate 수정자(modifier)를 확인한다. Swift4에서 변경(fixes)되면서 사용할 필요가 없어졌다고 생각할 수도 있지만, 여전히 도움이 될 수 있다. keyPath는 이 파일 전체(entire)에서 사용할 수 있어야 하지만, 외부에서는 사용할 수 없다. 만약 이 속성을 private로 하면, 외부에서는 열거형(enum) 자체밖에 볼 수 없을 것이다.
범위(scoping) 지정이 전체(entire) 파일이므로, 클래스(class)가 프로덕션 코드(production code)와 한 파일에 있는지 확인해야한다.
상태(state) 유형(type)을 만들었으므로 상태(state)를 유지(hold)하는 변수(variable)가 필요하다. 값(value)을 변경(change)할 때 적절한 KVO 알림(notifications)을 보내야하므로, 속성 관찰자(property observers)를 속성(property)에 첨부(attach)한다. AsyncOperation 클래스(class)의 // Create state management 아래에 다음 코드를 추가한다:
var state = State.ready { willSet { willChangeValue(forKey: newValue.keyPath) willChangeValue(forKey: state.keyPath) } didSet { didChangeValue(forKey: oldValue.keyPath) didChangeValue(forKey: state.keyPath) } }
기본적(default)으로 상태(state)는 ready이다. 상태(state) 값을 변경하면 실제로 4개의 KVO 알림(notifications)이 전송된다. 잠시 시간을 내어 무슨 일이 일어나고 있는지, 왜 두 개가 아니라 네 개의 항목(entries)이 있는지 알아본다.
현재 상태(state)가 ready이고 executing으로 업데이트하는 경우, isReady는 false가 되고 isExecuting은 true가 된다. 다음과 같은 4가지 KVO 알림(notifications)이 전송된다:
- Will change for isReady.
- Will change for isExecuting.
- Did change for isReady.
- Did change for isExecuting.
Operation의 기본(base) 클래스(class)는 isExecuting과 isReady 속성(properties)이 모두 변경되고 있음을 알아야 한다.
Base properties
이제 상태(state) 변경(changes)을 추적(track)하고 변경(change)이 실제로 수행(performed)되었음을 알릴 수 있으므로, 해당 메서드(methods)의 기본(base) 클래스(class) 인스턴스를 재정의(override)하여 상태(state) 대신 사용해야 한다. 클래스(class)의 // Override properties 아래에 다음 세 가지 속성을 재정의(overrides)한다:
override var isReady: Bool { return super.isReady && state == .ready } override var isExecuting: Bool { return state == .executing } override var isFinished: Bool { return state == .finished }
스케줄러(scheduler)가 operation을 사용할 스레드(thread)를 찾을 준비가 되었는지 여부를 판별하는 동안, 코드가 진행중인 모든 사항을 인식하지 못하므로 기본 클래스의 isReady 메서드(method)에 구현한 점검(check)을 포함시키는 것이 중요하다.
재정의(override)할 마지막 속성(property)은 단순히 비동기(asynchronous) 작업을 지정하는 것이다. 다음 코드를 추가한다:
override var isAsynchronous: Bool { return true }
Starting the operation
이제 남은 것은 start 메서드(method)를 구현하는 것이다. operation을 수동으로(manually) 실행(execute)하든 operation queue가 대신 수행하든, start 메서드(method)가 먼저 호출되고 main을 호출(calling)할 책임(responsible)이 있다. // Override start 바로 아래에 다음 코드를 추가한다:
override func start() { main() state = .executing }
이 코드에서는 super.start()를 호출하지 않는다. 공식 문서(https://apple.co/2YcJvEh)에서는 start을 재정의(overriding)할 때는 super를 호출해서는 안된다고 분명히 언급하고 있다.
이 두 줄의 코드는 아마도 순서가 반대로(backwards) 된 것처럼 보일 것이다. 그러나 실제로는 그렇지 않다. 비동기(asynchronous)로 작업을 수행하기 때문에 main 메서드(method)은 거의 즉시 반환(return)되고, 수동으로(manually) 상태(state)를 .executing으로 되돌려야 operation이 여전히 진행 중(progress)임을 알 수 있다.
위 코드에는 누락된(missing) 부분이 있다. 10장(chapter) "Canceling Operations"에서 취소 가능한(cancelable) operations에 대해 설명하며, 해당 코드는 어떤 start 메서드(method)에서도 사용할 수 있어야 한다. 혼동을 피하기 위해서 여기에는 빠져 있다.
Swift가 직접 인스턴스화(instantiated)할 수없는 추상(abstract) 클래스(class)의 개념이 있는 경우, 해당 클래스(class)를 추상(abstract) 클래스(class)로 표시할 것이다. 다시말해, 이 클래스(class)를 직접 사용하지 말라는 것이다. 항상 AsyncOperation로 서브 클래싱(subclass)해야 한다.
Math is fun!
playground에서 제공된 나머지 코드를 살펴본다. GCD를 이미 학습한 이상, 새로운 것은 없다. 콘솔(console)이 표시(⇧ + ⌘ + Y)된 상태에서 playground를 실행(run)하면 숫자가 올바르게 더해진 것을 볼 수 있다.
주의해야 할 AsyncSumOperation의 주요 세부 사항(detail)은 비동기(asynchronous) 작업이 완료(completes)될 때, 수동으로(manually) operation 상태(state)를 .finished로 설정해야 한다는 것이다. 만약 상태(state)를 변경하는 것을 잊어버린다면, operation은 절대로 완료(complete)로 표시되지 않으며, 이를 무한 루프(infinite loop)라고 한다.
비동기(asynchronous) 메소드(method)가 완료(completes)되면, 항상 상태(state)를 .finished로 설정한다.
Networked TiltShift
이미지 필터링(filtering)을 다시 이어서 구현한다. 지금까지 하드 코딩(hardcoded)된 이미지 목록(list)을 사용했다. 대신 네트워크(network)로 이미지를 가져온다. 네트워크(network) 작업을 수행(performing)하는 operation은 단순한 비동기(asynchronous) operation일 뿐이다. 앞서 비동기(asynchronous) 작업을 opertaion으로 전환하는 방법을 알았으니 그대로 구현하면 된다.
이 장(chapter)의 시작(starter) 프로젝트(project)는 starter/Concurrency 폴더(folder)에 있으며, 이전에 구현하던 프로젝트(project)에 두 개의 새 파일을 포함되어 있다. 이전 Xcode 프로젝트(project)에서 계속하려면 아래 두 파일을 가져와 붙여넣는다.
- AsyncOperation.swift : 이 장(chapter)의 앞부분에서 생성한 비동기(asynchronous) operation이다.
- Photos.plist : 다양한 이미지의 URL 목록(list)이다.
NetworkImageOperation
Concurrency.xcodeproj를 열고 NetworkImageOperation.swift이라는 새 Swift 파일을 만든다. 프로젝트(project)에서 필요한 것보다 더 많은 구현을 할 것이지만, 그로 인해 다른 프로젝트(project)에서 재사용(reusable)할 수 있는 구성 요소(component)를 갖게 된다.
operation에 대한 일반적인 요구 사항은 다음과 같다:
- URL을 나타내는 문자열(String) 또는 실제 URL을 가져와야 한다.
- 지정된 URL에서 데이터를 다운로드(download)해야 한다.
- URLSession 유형(type)의 완료(completion) 처리기(handler)가 제공되는 경우, 디코딩(decoding) 대신 이를 사용한다.
- 다운로드가 성공하면(successful) 완료(completion) 처리기(handler)가 없고, 이미지인 경우 선택적(optional) UIImage 값을 설정해야한다.
처음 두 가지 요구 사항(requirements)은 매우 분명하다. 세 번째와 네 번째는 발신자(caller)에게 최대한의 유연성을 제공하는 것이다. 경우에 따라서는 이 프로젝트(project)와 마찬가지로 완료하면서 디코딩(decoded)된 UIImage를 가져오려 할 수 있다. 그러나 다른 프로젝트(project)에서는 사용자 정의(custom) 처리(processing)가 필요할 수 있다. 예를 들어, HTTP 헤더(header)에 유효한 Content-Type 헤더(header)가 있는지 등의 특정 오류(error)에 대해 신경 쓸 수 있다.
먼저 AsyncOperation을 서브 클래싱(subclassing)하고 클래스(class)에 필요한 변수(variables)를 선언한다. NetworkImageOperation.swift의 내용을 다음과 같이 바꾼다:
import UIKit typealias ImageOperationCompletion = ((Data?, URLResponse?, Error?) -> Void)? final class NetworkImageOperation: AsyncOperation { var image: UIImage? private let url: URL private let completion: ImageOperationCompletion }
완료(completion) 시그니처(signature)은 URLSession 메서드(methods)에서 사용하는 것과 동일한 형식이며, 선택 사항(optional)으로 바뀌었다. 요구 사항(requirements) 1과 2를 충족하려면, 적절한 생성자(initializers)를 정의해야 한다. 새 클래스(class)에 다음 코드를 추가한다:
init(url: URL, completion: ImageOperationCompletion = nil) { self.url = url self.completion = completion super.init() } convenience init?(string: String, completion: ImageOperationCompletion = nil) { guard let url = URL(string: string) else { return nil } self.init(url: url, completion: completion) }
HTTP 반환(return) 데이터 자체를 명시적으로(explicitly) 처리(handle)하는 것이 일반적인 경우는 아니므로, 완료 처리기(completion handler)를 기본값(default)으로 nil로 설정하는 것이 좋다. 실제 URL을 전달하면 "designated initializer"이므로, 문자열을 대신 사용하는 convenience initializer를 선언한다. 해당 생성자(constructor)를 선택 사항(optional)로 선언한 것을 확인해 볼 수 있다. URL로 변환할 수없는 문자열(string)을 전달하면 생성자(constructor)는 nil을 반환(return)한다.
designated init과 convenience init 차이점 https://zeddios.tistory.com/141
이제 operation의 실제 작업을 추가한다. main을 재정의(override)하고, 일반적으로 URLSession을 사용할 때처럼 새 URLSession 작업(task)을 시작한다. 초기화(initializers) 아래에 다음을 추가한다:
override func main() { URLSession.shared.dataTask(with: url) { [weak self] data, response, error in }.resume() }
이 작업은 비동기(asynchronous) operation이므로, 데이터 다운로드(download)가 발생하기 전에 객체(object)가 제거될 가능성이 항상 존재한다. 따라서 약한 캡처 그룹(weak capture group)을 사용하여, self를 유지할 수 있어야 한다.
이제 완료된(completed) 작업을 처리 할 차례이다. 완료 클로저(completion closure) 안에 다음 코드를 추가한다:
guard let self = self else { return } defer { self.state = .finished } if let completion = self.completion { completion(data, response, error) return } guard error == nil, let data = data else { return } self.image = UIImage(data: data)
defer를 사용하면 operation은 완료(complete)된 것으로 표시된다. 현재 메서드(method)에는 이미 3가지의 가능한 종료 분기(paths)가 있으며, 앞으로 어떤 코드 변경이 있을지 모른다. 하지만, 어떤 경우라도 defer문(statement)을 사용하여 확실하게 작업 상태를 완료로 변경할 수 있다.
요구 사항(requirements) 3과 4를 충족시키는 것은 호출자(caller)가 완료 처리기(completion handler)를 제공했는지 여부를 확인하는 것만큼 간단하다. 완료 처리기(completion handler)를 제공 한 경우, 처리(processing) 책임(responsibility)을 전가(pass)하고 종료하면 된다. 그렇지 않으면 이미지를 디코딩(decode)한다.
예외(exceptions)를 발생(throw)시키거나, 오류(error) 조건(condition)을 반환(return)할 필요가 없다. 무언가 실패(fails)하면, image 속성(property)은 nil이 되고, 호출자(caller)는 무언가 잘못되었음을 알게 될 것이다.
Using NetworkImageFilter
TiltShiftTableViewController.swift로 다시 이동한다. 표시할 수 있는 URL 목록(list)을 가져오려면, 뷰 컨트롤러(view controller) 시작 부분에 다음 코드를 추가한다:
private var urls: [URL] = [] override func viewDidLoad() { super.viewDidLoad() guard let plist = Bundle.main.url(forResource: "Photos", withExtension: "plist"), let contents = try? Data(contentsOf: plist), let serial = try? PropertyListSerialization.propertyList(from: contents, format: nil), let serialUrls = serial as? [String] else { print("Something went horribly wrong!") return } urls = serialUrls.compactMap(URL.init) }
.plist 파일의 내용을 읽고 문자열을 실제 URL 객체(objects)로 변환하는 표준 스위프트 메커니즘(standard Swift mechanism)이다. 이전에 compactMap을 사용해 본 적이 없을 수도 있다. 선택적(optional) 값(values)의 배열(array)에 대해서만 map처럼 작동한다. nil 항목(items)은 제외하고 래핑되지 않은(unwrapped) 비 선택적(non-optional) 값(values)만 반환(returns)한다. 이 경우 urls 배열(array)에는 유효한 URL 객체(objects)만 포함된다.
tableView(_:cellForRowAt:)에서 다음 두 줄(line)을:
let image = UIImage(named: "\(indexPath.row).png")! let op = TiltShiftOperation(image: image)
다음으로 바꾼다:
let op = NetworkImageOperation(url: urls[indexPath.row])
TiltShiftOperation 클래스(class)는 outputImage를 결과 변수(result variable)로 사용했지만, NetworkImageOperation은 image로 사용하므로 해당 행(line)을:
cell.display(image: op.outputImage)
다음으로 바꾼다:
cell.display(image: op.image)
이제 앱을 빌드(build)하고 실행(run)하여, 다양한 이미지의 목록(list)을 스크롤(scroll)할 수 있다.
network operation을 사용하여, UI가 중단되지 않으므로 스크롤이 원활(nice)하고 매끄러워(smooth) 진다.
'Raywenderlich > Concurrency by Tutorials' 카테고리의 다른 글
Chapter 10: Canceling Operations (0) 2020.07.12 Chapter 9: Operation Dependencies (0) 2020.07.11 Chapter 7: Operation Queues (0) 2020.07.10 Chapter 6: Operations (0) 2020.07.09 Chapter 5: Concurrency Problems (0) 2020.07.09