[iOS] GCD (8) - Operations

전성훈·2023년 11월 1일
0

iOS/Concurrency

목록 보기
8/11
post-thumbnail

주제: 비동기의 객체화


Operation이란

  • Operations은 GCD를 기반으로 만들어졌으며, 어떤 단위적인 작업(데이터 다운르도, 압축풀기, 이미지 필터 적용하기) 등을 클래스화 해서 기능을 향상시킨다.
  • Operations은 아래와 같은 기능들을 제공한다.
    • 실행 중인 작업을 취소할 수 있는 능력
    • 순서지정(의존성)
    • 상태 체크(state machine)
    • KVO notifications
    • QoS수준
    • completionBlock 제공
  • Operations은 기본적으로 동기적으로 설정된다.
  • Operation은 추상 클레스로, 상속하여 활용되면서 input data, output data, main() 를 내포하고 있다.
class SomeOperation: Operation { 
	var inputImage: UIImage? 
	var outputImage: UIImage? 
	
	override func main() { 
		// 정의할 메서드 내용
	}
}
let op = SomeOperation

Reusability

  • Operation을 만들어야하는 이유 중 하나는 재사용성 때문이다.
  • 간단한 작업이라면, GCD를 사용하면 되겠지만, 복잡한 작업일 수록 Operation을 사용하는 것이 훨씬 쉬어진다.
  • Operation은 객체화 할 수 있으므로, 입력을 전달하여 작업을 설정하고, helper methods을 실행 하는 등의 다양한 작업을 수행할 수 있다. 따라서 작업 단위를 래핑하여 나중에 언제든지 실행하고 그 작업을 여러번 실행하기에 쉬어진다.

Operation states

  • Operation은 생명 주기를 나타내주는 여러 변수들이 있다.

    isReady
    인스턴스화되어 실행할 준비가 되면, isReady 상태로 전환된다.
    isExecuting
    어느 시점에서 start 메서드가 호출하면, isExecuting 상태로 전환된다.
    isCancelled
    앱이 cancel 메서드를 호출하면, isCancelled 상태로 전환한 후 isFinished 상태로 전환된다.
    isFinished
    취소되지 않으면, isExecuting 상태에서 바로 isFinished 상태로 전환된다.
    operation

  • 위의 각 변수들은 Operation 클래스의 읽기 전용 Boolean 속성이다. 또한 모든 상태 전환을 자동으로 처리한다.

BlockOperation

  • BlockOperation 클래스를 사용하면 초기값을 클로저에 작성함으로써 Operation을 생성할 수 있다.
let operation = BlockOperation { 
	print("2 + 3" = \(2 + 3)")
}

operation.start 
// 2 + 3 = 5 
  • BlockOperation은 default global queue에서 하나 이상의 클로저를 동시에 실행하는 것을 관리한다. 이는 이미 OperationQueue를 사용하고 있는 앱이 따로 DispatchQueue를 생성하고 싶지 않을 경우에 객체지향적인 wrapper를 제공한다.
  • Operation인 만큼 KVO(Key-Value Observing)알림, 종속성 및 Operation이 제공하는 모든 것을 활용할 수 있다.
  • 클래스의 이름만으로는 바로 알기 어렵지만, BlockOperation은 클로저 그룹을 관리한다. 모든 클로저가 완료되면 자체를 완료된 것으로 표시하는 dispatch group과 비슷하게 작동한다.
  • 하지만, BlockOperation 클래스는 여러 개의 클로저를 관리하면서, 이들의 클로저는 동시에 실행된다. 이는 BlockOperation이 내부적으로 concurrent queue를 사용하기 때문이다.
  • 그렇기 때문에 BlockOperation에 추가된 각 클로저는 서로 독립적으로 실행되기 때문에, 그 실행 순서는 보장되지 않는다.

Multiple block operations

  • BlockOperation에 추가적인 클로저를 추가하고 싶을 때는 addExecutionBlock 메서드를 호출하고 새 클로저를 전달하면 된다.
let sentence = "Ray's courses are the best!"
let operation = BlockOperation()

operation.addExecutionBlock { 
	print("Test")
}
for word in sentense.split(separator: " ") { 
	operation.addExecutionBlock { 
		print(word)
	}
}
operation.strat
// Ray's
// Test
// courses
// are 
// the
// best!
  • 위 코드처럼 BlockOperation은 연속적이지 않고 동시에 실행되므로 실행 순서는 계속 바뀔 수 있다.
  • 아래 코드처럼 sleep 코드를 추가했을 때
let sentence = "Ray’s courses are the best!"
let operation = BlockOperation()

operation.addExecutionBlock {
    print("Test")
}

for word in sentence.split(separator: " ") {
    operation.addExecutionBlock {
        print(word)
        sleep(2)
  }
}

duration {
    operation.start()
}

operation.completionBlock = {
    print("End Job!")
}
  • 각 작업이 2초씩 대기해서, 작업 자체의 총 시간이 10초가 걸릴 것이라고 예상하겠지만, BlockOperation은 비동기로 작업되기 때문에 작업이 완료되기 까지 약 2초의 시간이 걸리는 것을 확인 할 수 있다.
  • 또한 completionBlock 클로저를 통해 모든 클로저가 완료될 때 실행되는 코드를 작성할수도있다.

Subclassing operation

  • BlockOperation 클래스는 간단한 작업에 대해서는 좋지만 더 복잡한 작업이나 재사용 가능한 구성요소를 수행할 경우 직접 Operation을 서브 클래스화 해야한다.
  • 강의 내용을 확인해보면 틸트 쉬프트기술을 활용하는데 이는 이미지에서 깊이를 조절하는 기술이다.
  • 예제 프로젝트는 CIFilter의 서브 클래스인 TiltShilftFilter.swift 파일을 제공한다. 코드가 매우 직관적이여서, 교육용으로는 잘 작동하지만 성능 면에서는 최적이 아니다.
  • 이 filter를 활용하여, 주워진 table view에 데이터를 넣어보자.

Tilt shift the wrong way

  • 먼저, Tilt Shift를 초보자가 시도할 때와 같은 단순한 방법으로 구현해보겠다.
import UIKit

class TiltShiftTableViewController: UITableViewController {
  private let context = CIContext()

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 10
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "normal", for: indexPath) as! PhotoCell
    cell.display(image: nil)
    
    let name = "\(indexPath.row).png"
    let inputImage = UIImage(named: name)!
    print("Tilt shifting image \(name)")
    
    guard let filter = TiltShiftFilter(image: inputImage, radius: 3),
          let output = filter.outputImage else {
      print("Failed to generate tilt shift image")
      cell.display(image: nil)
      return cell
    }
    
    print("Generating UIImage for \(name)")
    let fromRect = CGRect(origin: .zero, size: inputImage.size)
    guard let cgImage = context.createCGImage(output, from: fromRect) else {
      print("No image generated")
      cell.display(image: nil)
      return cell
    }
    
    cell.display(image: UIImage(cgImage: cgImage))
    print("Displaying \(name)")

    return cell
  }
}
  • 위의 작업은 tilt shifting 작업이 메인스래드에서 실행되므로 스크롤을 할때마다 버벅이는 현상이 발생한다.
  • 이를 백그라운드에서 실행할 수 있도록 코드를 수정해보자

Tilt shift almost correctly

  • Core Image 연산을 Operation 하위 클래스에 위치시키며, 입력 및 출력 이미지가 필요하므로 두 속성을 만들어야 한다. 입력 이미지는 변경되지 않기 때문에 초기화 시 전달하고 비공개로 해두면 좋다.
import UIKit

final class TiltShiftOperation: Operation { 
	var outputImage: UIImage? 
	
	private let inputImage: UIImage
	
	init(image: UIImage) { 
		inputImage = image 
		super.init()
	}
}
  • TiltShiftTableViewController.swift 파일에서는 클래스 수준의 context 속성을 만들면 되었지만, 이제 operation class로 이동하면 상황이 달라진다.
  • TiltShiftOperation의 단순한 속성으로 만들면 TiltShiftOperation의 모든 인스턴스마다 새로운 context가 생성된다. CIContext는 가능한 경우 재사용해야 하며, Apple의 공식문서에서는 CIContext가 thread-safe하다고 명시하고 있으므로, 이를 static으로 만들 수 있다.
  • TiltShiftOperation 클래스의 시작 부분에 context 속성을 추가하자.
private static let context = CIContext()
  • 이제 수행할 작업은 작업이 시작될 때 호출되는 main메서드를 재정의하는 것 뿐이다.
override func main() { 
	guard let filter = TiltShiftFilter(image: inputImage, radius: 3),
	        let output = filter.outputImage else {
	    print("Failed to generate tilt shift image")
	    return
	  }
	
	  let fromRect = CGRect(origin: .zero, size: inputImage.size)
	  guard let cgImage = TiltShiftOperation
	                          .context
	                          .createCGImage(output, from: fromRect) else {
	    print("No image generated")
	    return
	  }
	
	  outputImage = UIImage(cgImage: cgImage)
	
}
  • TiltShiftTableViewController.swift로 돌아가서 tableView(:cellForRowAt)을 다음과 같이 업데이트한다.
override func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  
  let cell = tableView.dequeueReusableCell(withIdentifier: "normal",
                                           for: indexPath) as! PhotoCell

  let image = UIImage(named: "\(indexPath.row).png")!

  print("Filtering")
  let op = TiltShiftOperation(image: image)
  op.start()

  cell.display(image: op.outputImage)
  print("Done")
  
  return cell
}
  • 직접 Operation 하위 클래스로 코드를 리팩토링했지만, 아직 동시성 기능을 활용하지 않고 start 메서드를 직접 호출하여 메인 스레드에서 동기적으로 호출을 수행하고 있다. 따라서 Operation 클래스로 리팩토링되었지만, 아직 동시성 기능을 활용하고 있지 않으므로 성능 이점을 얻을 수 없다.
    - start 메서드를 직접 호출하면 현재 스레드에서 작동하게 되므로, 작업이 아직 시작 준비가 안 된 경우 예외가 발생할 수 있다. 그러므로 직접 start를 호출하지 않는 것이 좋다.

OperationQueue & Dispatch.main을 활용한 비동기 작업

override func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  
  let cell = tableView.dequeueReusableCell(withIdentifier: "normal",
                                           for: indexPath) as! PhotoCell

  let image = UIImage(named: "\(indexPath.row).png")!

  print("Filtering")
  let op = TiltShiftOperation(image: image)
  let queue = OperationQueue()
  queue.addOperation(op)

  let completion = BlockOperation {
    cell.display(image: op.outputImage)
    print("Done")
  }
  completion.addDependency(op)
  OperationQueue.main.addOperation(completion)
  
  return cell
}
  • 이전과 다르게 직접 TiltShiftOperation을 시작하지 않고, OperationQueue에 추가한다. BlockOperation을 만들어 이전에 실행했던 코드를 completion으로 추가하고, 이전 작업(op)이 끝나야 실행될 수 있도록 completion.addDependency(op)를 추가한다.
  • 앱을 다시 실행해서 보면 이제 테이블 뷰를 스크롤해도 스크롤 뷰가 부드럽게 움직이게 된다. 이것은 TiltShiftOperation이 별도의 스레드에서 실행되어 테이블 뷰의 스크롤 동작과 상호작용하지 않기 때문이다.
  • 다른 방법
let op = TiltShiftOperation(image: image)
let queue = OperationQueue() 

op.completionBlock = { 
	DispatchQueue.main.async { 
		cell.display(image: op.outputImage)
		print("done")
	}
}

queue.addOperation(op)

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글