ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Chapter 9: Operation Dependencies
    Raywenderlich/Concurrency by Tutorials 2020. 7. 11. 16:45

    이 장(chapter)에서는 operations간의 의존성(dependencies)에 대해 설명한다. 한 operation을 다른 operation에 의존(dependent)시키면, operations간 상호 작용(interactions)에 대한 두 가지 이점을 제공한다:

    1. 전제 조건(prerequisite) operation이 완료(completed)되기 전에, 종속(dependent) operation이 시작되지 않도록 한다.
    2. 첫 번째 operation에서 두 ​​번째 operation으로 데이터를 자동으로 전달할 수 있는 깔끔한 방법을 제공한다.

    operations간의 의존성(dependencies)을 활성화(enabling)하는 것이 GCD대신 Operation을 사용하는 주된 이유 중 하나이다.

     

    Modular design

    지금까지 작업한 틸트 시프트(tilt shift) 프로젝트(project)를 생각해 본다. 이제 네트워크(network)에서 다운로드(download)하는 operation과 틸트 시프트(tilt shift)를 수행하는 operation이 있다. 두 작업을 모두 수행하는 단일 operation을 만들 수 있지만, 이는 좋은 아키텍처(architectural) 설계(design)가 아니다.

    클래스(Classes)는 프로젝트(projects) 내부 또는 프로젝트(projects)간에 재사용(reuse)할 수 있도록 단일 작업(single task)을 수행하는 것이 이상적이다. 네트워크(networking) 코드를 틸트 시프트(tilt shift) operation에 직접(directly) 구축(built)한다면, 이미 번들로 제공되는 이미지(already-bundled image)에는 사용할 수 없을 것이다. 이미지를 네트워크에서 제공(provided)할지 또는 다운로드(downloaded)될지 여부를 지정하는 많은 초기화(initialization) 매개 변수(parameters)를 추가 할 수 있지만, 이는 클래스(class)를 비대화(bloats)시킨다. 장기적인(long-term) 클래스(class) 유지 관리(maintenance) 측면에서 URLSession를 Alamofire로의 전환(switching)하거나 테스트 사례의 수도 늘릴 수 있다.

     

    Specifying dependencies

    의존성(dependency)을 추가(adding)하거나 제거(removing)하려면, 의존(dependent) operation에 대한 단일(single) 메서드(method) 호출(call)만 있으면 된다. 이미지를 다운로드(download)하고 디코드(decrypt)한 다음 틸트 시프트(tilt shift)로 결과 이미지를 변환하는 가상(fictitious)의 예(example)를 생각해 본다:

    let networkOp = NetworkImageOperation()
    let decryptOp = DecryptOperation()
    let tiltShiftOp = TiltShiftOperation()
    
    decryptOp.addDependency(op: networkOp) 
    tiltShiftOp.addDependency(op: decryptOp)

    어떤 이유로 의존성(dependency)을 제거(remove)해야하는 경우에는 removeDependency(op:) 메서드(method)를 호출하면 된다:

    tiltShiftOp.removeDependency(op: decryptOp)

    또한 Operation 클래스(class)는 읽기 전용(read-only) 속성(property)인 dependencies을 제공한다. 이 속성(property)은 지정된 operation에 대한 의존성(dependencies)으로 표시된 일련의 Operations 배열(array)을 반환(return)한다.

     

    Avoiding the pyramid of doom

    의존성(dependencies)은 코드를 훨씬 더 읽기 쉽게 한다. GCD를 사용하여 세 개의 연쇄(chained) operations을 함께 작성하려 한다면 결국 파멸의 피라미드(pyramid of doom)가 될 것이다. GCD로 이전 예제를 구현한다고 할때, 의사 코드(pseudo-code)는 다음과 같이 작성될 것이다:

    let network = NetworkClass()
    network.onDownloaded { raw in
        guard let raw = raw else { return }
        
        let decrypt = DecryptClass(raw)
        decrypt.onDecrypted { decrypted in
            guard let decrypted = decrypted else { return }
            
            let tilt = TiltShiftClass(decrypted)
            tilt.onTiltShifted { tilted in
                guard let tilted = tilted else { return }
            }
        }
    }

    프로젝트의 규모가 커지고 이를 담당하는 새로운 개발자 참여하게 될 때 어느 쪽이 더 이해(understand)하고 유지(maintain)하기가 쉬운지는 명확하다. 또한 제공된 예제는 실제 코드에서 올바르게 처리되어야 하는 retain cycles 또는 오류 검사(error checking)를 고려하지 않았다는 점을 생각해야 한다.

     

    Watch out for deadlock

    5장(chapter), "Concurrency Problems"에서 교착 상태(deadlock)에 대해 배웠다. 작업이 다른 작업에 의존될(dependent) 때마다, 주의하지 않으면 교착 상태(deadlock)가 발생할 수 있다. 의존성(dependency) 체인(chain)을 마음 속으로 그려본다. 그래프(graph)가 직선을 그리면 교착 상태(deadlock)가 발생할 가능성이 없다.

     

    한 operation queue의 operations이 다른 operation queue의 operation에 의존(depend on)되도록하는 것은 완전히 유효하다. 그렇게 해도 루프(loops)가 없는 한, 교착 상태(deadlock)로 부터 안전하다.

     

    그러나 루프(loops)가 있다면, 거의 확실하게(almost certainly) 교착 상태(deadlock)에 빠지게 될 것이다.

     

    위의 이미지에서 어디에 문제가 있는지 확인할 수 있다:

    • Operation 2는 Operation 5가 완료(done)될 때까지 시작할 수 없다.
    • Operation 5는 Operation 3이 완료(done)될 때까지 시작할 수 없다.
    • Operation 2가 완료(done)될 때까지 Operation 3을 시작할 수 없다.

    사이클(cycle)에서 동일한 Operation 번호로 시작하고 종료하면, 교착 상태(deadlock)에 빠진 것이다.

    어떤 operations도 실행되지 않는다. 교착 상태(deadlock) 상황을 해결하기 위한 완벽한(silver-bullet) 해결책(solution)은 없으며, 의존성(dependencies)을 파악하지 않으면 찾기 어려울 수 있다. 이러한 상황이 발생하면(run into) 다시 설계(re-architect)할 수밖에 없다.

     

    Passing data between operations

    하나의 operation을 다른 operation에 안전하게 의존(depend on)시킬 수있으므로, 그들 사이에 데이터를 전달할 수 있도록 프로토콜(protocol)을 활용할 수 있다. NetworkImageOperation에는 image라는 출력(output) 속성(property)이 있다. 그러나 그 속성(property)을 다른 이름으로 부르는 경우도 있다.

    operations의 이점 중 하나는 이들이 제공하는 캡슐화(encapsulation) 및 재사용성(reusability)이다. operation을 작성하는 모든 작업이 출력(output) 속성(property)을 image로 사용하는 것은 아니다. 예를 들어, 내부적으로는 클래스(class)에서 foodImage라고 부르는 것이 더 타당한 경우가 있을 수도 있다.

     

    Using protocols

    "이 operation이 완료(finishes)되었을 때, 모든 것이 제대로 진행되었다면 UIImage 유형(type)의 이미지를 제공한다." 라고 생각하면 된다.

    평소와 같이 이 장(chapter)의 시작(starter) 프로젝트(project) 폴더(folder)에서 Concurrency.xcodeproj를 열고, ImageDataProvider.swift라는 새 Swift 파일을 만든다. 파일에 다음 코드를 추가한다:

    import UIKit
    
    protocol ImageDataProvider {
        var image: UIImage? { get }
    }

    UIImage 출력(output)이 있는 모든 operation은 해당 프로토콜(protocol)을 구현해야 한다. 이 경우 속성(property) 이름이 일대일로(one-to-one) 일치하므로, 더 편해진다. TiltShiftOperation에 대해 생각해 보면, CIFilter 명명 규칙(naming conventions)에 따라 하나의 outputImage를 호출(called)했다. 두 클래스(classes) 모두 ImageDataProvider를 준수해야 한다.

     

    Adding extensions

    NetworkImageOperation.swift를 열고 파일의 맨 아래에 코드를 추가한다:

    extension NetworkImageOperation: ImageDataProvider {}

    클래스(class)에는 이미 프로토콜(protocol)에서 정의한 속성(property)이 포함되어 있으므로 따로 추가할 필요가 없다. 단순히 ImageDataProvider를 클래스(class) 정의(definition)에 추가 할 수도 있지만, Swift Style Guide는 대신 확장(extension) 방식을 권장(recommends)한다.

    TiltShiftOperation의 경우에는 해야할 일이 조금 더 있다. output image가 이미 있지만 속성(property) 이름은 프로토콜(protocol)에서 정의한 image가 아니다.

    TiltShiftOperation.swift의 끝에 다음 코드를 추가한다:

    extension TiltShiftOperation: ImageDataProvider {
        var image: UIImage? { return outputImage }
    }

    확장(extension)은 어느 파일의 어디에서나 작성할 수 있다. 두 가지 operations을 모두 만들었으니, 당연히 확장(extension)을 클래스(class) 바로 옆에 두는 것이 타당하다. 그러나 소스(source)를 편집할 수 없는 서드 파티(third-party) 프레임 워크(framework)를 사용하고 있을 수 있다. operation이 image를 제공하는 경우, ThirdPartyOperation+Extension.swift와 같이, 프로젝트 내의 파일에 직접 확장(extension)을 추가한다.

     

    Searching for the protocol

    TiltShiftOperation에는 입력(input)으로 UIImage가 필요하다. 단순히 inputImage 속성(property)만 설정하는 것이 아니라, UIImage를 의존(dependencies) 출력(output)으로 제공하는지 여부를 확인할 수 있다.

    TiltShiftOperation.swift의 main()에서 첫 번째 guard 문(e.g. 첫 번째 줄)을 다음으로 변경한다:

    let dependencyImage = dependencies
        .compactMap{ ($0 as? ImageDataProvider)?.image }
        .first
        
    guard let inputImage = inputImage ?? dependencyImage else {
        return
    }

    위의 코드에서 operation에 직접 제공된 입력 이미지 혹은 nil이 아닌(non-nil) 의존성(dependency) 체인(chain)의 이미지를 가져온다.

    둘 다 nil이라면, 작업을 수행하지 않고 단순히 반환(return)된다.

    모든 것을 제대로 수행하기 위한 마지막 조각이 남아있다. 이제 의존성(dependency) 체인(chain)에 이미지가 있는지 확인하기 때문에, 입력(input) 이미지를 제공하지 않고 TiltShiftOperation을 초기화(initialize)하는 방법이 있어야 한다. 입력(input)을 처리(handle)하지 않는 가장 간단한 방법은 현재 생성자(constructor)의 입력(input) 이미지 기본값(default)을 nil로 하는 것이다.

    다음과 같이 초기화(initializer)를 변경한다:

    init(image: UIImage? = nil) {
        inputImage = image
        super.init()
    }

     

    Updating the table view controller

    TiltShiftTableViewController.swift로 이동하여, 이미지를 다운로드하고 틸트 시프트(tilt shift)한 다음 테이블 뷰 셀(table view cell)에 할당할 수 있는지 확인한다.

    이 작업을 수행하려면 틸트 시프트(tilt shift) operation의 의존성(dependency)으로 다운로드(download) operation을 추가해야 한다. 다시 말해, 틸트 시프트(tilt shift)는 이미지를 얻기 위해 다운로드(download) operation에 의존(depends on)한다.

    다음 코드를 사용하여 op를 설정하고 선언하는 tableView(_:cellForRowAt:)의 행을 바꾼다:

    let downloadOp = NetworkImageOperation(url: urls[indexPath.row])
    let tiltShiftOp = TiltShiftOperation()
    tiltShiftOp.addDependency(downloadOp) //의존성 설정

    단일(single) operation을 수행하는 대신, 이제 두 개의 operations과 이들 간의 종속성(dependency)을 설정해 준다.

    다음으로 이미지가 제공(provide)되므로 tiltShiftOp에 completeBlock을 설정한다.

    전체(entire) 완료(completion) 블록(block)을 다음으로 교체한다:

    tiltShiftOp.completionBlock = {
        DispatchQueue.main.async {
            guard let cell = tableView.cellForRow(at: indexPath) as? PhotoCell else { return }
            
            cell.isLoading = false
            cell.display(image: tiltShiftOp.image)
        }
    }

    마지막으로 대기열(queue)에 op를 추가하는 줄을 다음 두 줄로 바꾼다:

    queue.addOperation(downloadOp) 
    queue.addOperation(tiltShiftOp)

    틸트 시프트(tilt shift)는 다운로드(download)에 의존(depends on)하지만, 여전히 두 operations을 모두 대기열(queue)에 추가해야 한다. 대기열(queue)은 의존성(dependencies)을 추적(track)하고 다운로드(download)가 완료된 후에만 ​​틸트 시프트(tilt shift) operation을 시작한다.

    앱을 빌드(build)하고 실행(run)한다. 틸트 시프트(tilt shifted)된 이미지 목록(list)이 나타난다.

     

    Custom completion handler

    현재 작성된 코드는 Operation 클래스(class)에서 제공하는 기본(default) completionBlock을 사용하고 있다. 이미지를 가져(grab)와서 기본 대기열(main queue)로 다시 보내려면, 약간의 추가 작업이 필요하다. 이와 같은 경우, 사용자 지정(custom) 완료 블록(completion block)을 추가하는 것을 고려할 수 있다.

    TiltShiftOperation.swift로 돌아가, 클래스(class) 상단에 새로운 선택적(optional) 클래스 레벨(class-level) 속성(property)을 추가하여 사용자 지정(custom) 완료(completion) 처리기(handler)를 할당한다:

    /// Callback which will be run *on the main thread*
    /// when the operation completes.
    var onImageProcessed: ((UIImage?) -> Void)?

    그런 다음 main() 메서드(method)의 끝에서 outputImage를 할당(assigning)한 후, 기본 스레드(main thread)에서 해당 완료(completion) 처리기(handler)를 호출한다:

    if let onImageProcessed = onImageProcessed {
        DispatchQueue.main.async { [weak self] in
            onImageProcessed(self?.outputImage)
        }
    }

    위 코드를 추가한 뒤, TiltShiftTableViewController.swift로 돌아가 tableView(_:cellForRowAt:)에서 전체(entire) 완료(completion) 블록(block) 코드를 다음과 같이 바꿀 수 있다:

    tiltShiftOp.onImageProcessed = { image in
        guard let cell = tableView.cellForRow(at: indexPath) as? PhotoCell else {
            return
        }
          
        cell.isLoading = false
        cell.display(image: tiltShiftOp.image)
    }

    이러한 변경 사항은 기능(functional) 또는 성능(performance)의 차이는 없지만, 호출자(caller)에게 operation 처리를 조금 더 좋게 만든다. 가능한 유지 주기(retain cycle)에 대한 혼란(confusion)을 제거하고, 기본(main) UI 스레드(thread)에서 올바르게 작동하는지 확인한다.

    operation queue에 제공된 스레드(thread)가 아닌 기본(main) UI 스레드(thread)에서 완료 처리기(completion handler)가 실행되고 있다는 사실을 문서화(document)하는 것은 매우 중요하다.

    다른 개발자(end user)가 애플리케이션에 영향을 줄 수 있는 작업을 하지 않도록, 스레드(threads)를 전환하고 있다는 것을 알려야 한다.

    주석(comment)에 세 개의 / 문자(characters)가 표시된 것을 주목한다. 이 구문을 사용하면 Xcode는 해당 주석(comment)을 빠른 도움말 관리자(Quick Help Inspector)에서 속성(property) 요약(summary)으로 표시한다(해당 속성을 Option + 클릭했을 때 작성한 주석이 도움말로 팝업된다). Xcode는 제한된(limited) 스타일링(styling)도 지원하며, main thread라는 텍스트는 실제로 도움말에서 기울임꼴(italicized)로 표시된다.

    'Raywenderlich > Concurrency by Tutorials' 카테고리의 다른 글

    Chapter 11: Core Data  (0) 2020.07.12
    Chapter 10: Canceling Operations  (0) 2020.07.12
    Chapter 8: Asynchronous Operations  (0) 2020.07.10
    Chapter 7: Operation Queues  (0) 2020.07.10
    Chapter 6: Operations  (0) 2020.07.09
Designed by Tistory.