본문 바로가기

학습 노트/iOS (2021)

171. Operation & Operation Queue

Operation

Operation은 하나의 작업을 나타내는 객체이다.
일반적으로 Operation Class를 상속한 Block Operation을 사용하지만
Operation Class를 SubClassing 하고 Custom Operation을 생성해 사용하기도 한다.

Operation의 장점

  • Operation 사이의 의존성 추가(Interoperation Dependencies)
    Operation들 사이에 의존성을 추가해 실행 순서를 제어할 수 있다.
  • 실행 취소(Cancellation)
  • Completion Handler API 제공
    t Value Observing을 사용해 상태를 감시하고, 우선순위 설정에 필요한 API를 함꼐 제공한다.

Operation은 Single-shot Object이다.
실행이 완료된 인스턴스는 다시 실행할 수 없다.
동일한 작업을 반복적으로 실행해야 한다면 매번 새로운 인스턴스를 실행해야한다.

Operation은 4가지 상태를 갖는다.

  • Ready
    작업을 실행할 수 있는 상태이며,
    실행되면 Executing 상태로 전환된다.
  • Executing
    작업이 진행중인 상태이다.
    완료되면 Finished로 전환되고, 취소되면 Cancelled 상태로 전환된다.
  • Finished
  • Cancelled

Finished나 Cancelled로 전환된 이후 Ready로 복귀하는 것은 불가능하다.

Operation 상태 감시

  • Operation Class가 제공하는 속성 사용하기
  • T Vlaue Observing 활용하기

Operation은 API를 통해 직접 실행하는 것이 가능하다.
하지만 다른 코드와 동시에 실행되는 것을 보장하지 않는다.
이는 Operation이 기본적으로 동기 방식으로 실행되기 때문이다.
비동기 방식으로 전환할 수 있지만, 권장되지 않는다.
즉, Operation을 직접 실행하기 보다는 Queue에 올리는 것이 추천된다.

Operation Queue

이름 그대로 Opration을 관리하는 Queue이다.
실행할 작업을 Operation으로 구현한 다음, Queue에 추가하면 이후는 자동으로 진행된다.
실행 순서는 우선순위나 의존성에 의해 결정된다.

사용 가능한 CPU Core와 System Resource를 사용해 동시에 실행할 수 있는 작업 수를 결정하고, 실행한다.

정상적으로 완료된 작업은 Queue에서 제거되고, 시작되지 않은 작업은 언제든 취소할 수 있지만,
실행중인 경우에는 직접 구현해야만 취소할 수 있다.

따라서 Custom Operation을 구현하는 경우 항상 취소 여부를 고려해 구현하는 것이 중요하다.

Operation의 우선순위

두 가지 속성으로 설정한다.

Queue Priority
동일한 Que에 추가돼있는 Operation 사이의 상대적 우선순위를 나타낸다.

  • veryHigh
  • high
  • normal
  • low
  • veryLow

높은 우선순위를 가진 Operation이 먼저 실행된다.
직접 설정하지 않으면 normal로 설정된다.

Quality of Service
Resource 사이의 우선순위를 나타낸다.

  • userInteractive
  • userInitiated
  • utility
  • background

높은 우선순위를 가질수록 Resource를 더 오랫동안 사용할 수 있고, 더 빠르게 실행된다.
기본 값은 backgorund이다.

 

Operation Queue

Operation Queue 생성하기

//main에서 작동하는 Operation Queue
//UI를 업데이트 하는 경우
let queue = OperationQueue.main

//background에서 작동하는 Operation Queue
let queue = OperationQueue()

 

  • addOperation, addOperation(block:)
    인스턴스를 추가하지 않고 직접 block으로 추가하는 경우
  • addOperation(op:)
    개벌 Operation을 추가하는 경우
  • addOperations(waitUntilFinished:)
    의존성을 추가하거나 여러개를 동시에 추가할 때 사용

기본형태

@IBAction func start(_ sender: Any) {
	queue.addOperation {
		for _ in 1..<100 {
			print("😆")
			Thread.sleep(forTimeInterval: 0.3)
		}
	}
}

파라미터나 반환이 존재하지 않는다.
이모티콘을 일정한 지연을 통해 반복적으로 출력한다.

Operation은 메모리 관리를 자동으로 하지 않는다.
따라서 Block이나 Custom Operation을 구현하는 경우 autoReleasPool을 직접 추가해야한다.

@IBAction func start(_ sender: Any) {
	queue.addOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("😆")
				Thread.sleep(forTimeInterval: 0.3)
			}
		}
	}
}

즉시 Block에 존재하는 코드가 실행된다.

인스턴스 생성하고 Operation 추가하기

@IBAction func start(_ sender: Any) {
	let op = BlockOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥳")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	queue.addOperation(op)
}

Block Operation은 하나의 Operation에 두개 이상의 Block을 추가할 수 있다는 점이다.

@IBAction func start(_ sender: Any) {
	let op = BlockOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥳")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	queue.addOperation(op)
	
	op.addExecutionBlock {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥺")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
}

addExcutionBlock을 사용해 새로운 Block을 추가했다.
이렇게 추가한 Block은 다른 Block과 함께 동시에 실행된다.
지금처럼 아직 실행되지 않은 Operation에 추가하는 것은 문제가 없지만 실행이 완료된 경우 주의해야한다.

Custom Operation

Custom Operation을 구현할 때는 Operation Class를 Subclassing 해야한다.

class CustomOperation : Operation {
	let type: String
	
	init(type: String) {
		self.type = type
	}
}

위와같이 Operation Class를 상속하고,
출력할 문자열을 선언한 뒤 생성자를 통해 초기화한다.

class CustomOperation : Operation {
	let type: String
	
	init(type: String) {
		self.type = type
	}
	
	override func main() {
		autoreleasepool {
			for _ in 1..<100 {
				print(type)
				Thread.sleep(forTimeInterval: 0.9)
			}
		}
	}
}

이후 main 함수를 오버라이딩해 실제 코드를 작성한다.

@IBAction func start(_ sender: Any) {
	let op = BlockOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥳")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	queue.addOperation(op)
	
	op.addExecutionBlock {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥺")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	
	let op2 = CustomOperation(type: "🥸")
	queue.addOperation(op2)
}

이후 새로운 인스턴스로 생성해 이를 Queue에 전달한다.

Operation Class에는 completionBlock 속성이 선언되어있다.
해당 속성에 저장된 코드는 Operation이 완료된 다음 호출된다.

@IBAction func start(_ sender: Any) {
	queue.addOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("😆")
				Thread.sleep(forTimeInterval: 0.3)
			}
		}
	}
	
	let op = BlockOperation {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥳")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	queue.addOperation(op)
	
	op.addExecutionBlock {
		autoreleasepool {
			for _ in 1..<100 {
				print("🥺")
				Thread.sleep(forTimeInterval: 0.6)
			}
		}
	}
	
	let op2 = CustomOperation(type: "🥸")
	queue.addOperation(op2)
	
	op.completionBlock = {
		print("done")
	}
}

모두 합쳐진 코드는 위와 같다.

정리하면 Queue에 추가된 Operation은

0.3초마다 😆를 출력하는 Operation

0.6초마다 🥳를 출력하는 Operation

0.6초마다 🥺를 출력하는 Operation

0.9초마다 🥸를 출력하는 Custom Operation

Operation 취소하기

Operation을 취소할 때는 Operation Class의 Cancel 메소드를 사용한다.

Queue의 모든 Operation을 취소할 때는 cancelAllOperation 메소드를 사용한다.
이 의미는 Operation의 상태를 isCancelled 상태로 전환한다는 것을 의미한다.
따라서 해당 상태를 감지하면 동작을 취소하도록 구현된 경우에만 작업을 취소하게 된다.

class CustomOperation : Operation {
	let type: String
	
	init(type: String) {
		self.type = type
	}
	
	override func main() {
		autoreleasepool {
			for _ in 1..<100 {
				guard !isCancelled else { return }
				print(type)
				Thread.sleep(forTimeInterval: 0.9)
			}
		}
	}
}

위와 같은 형태로 중지를 원하는 시점에서 감지 하면 종료하는 방식으로 구현한다.

하지만 생성자나 메서드로 전달하는 코드들의 경우 해당 코드에서 Operation에 직접 접근할 수 없기 때문에,
같은 방식으로 isCancelled 상태를 감지하도록 구현하는 것은 불가능하다.
따라서 만약 취소 기능이 필요하다면 직접 Operation Class를 SubClassing 하는 방식으로 구현하는 방식이 보편적이다.

하지만 지금의 상황에서 취소 기능이 필요하다면 Class에 속성을 직접 추가하고, 해당 속성을 사용해 기능을 구현하는 방법이 있다.

class OperationQueueViewController: UIViewController {
	
	let queue = OperationQueue()
	
	var isCancelled = false
	
	@IBAction func start(_ sender: Any) {
		
		isCancelled = false
		
		queue.addOperation {
			autoreleasepool {
				for _ in 1..<100 {
					guard !self.isCancelled else { return }
					print("😆")
					Thread.sleep(forTimeInterval: 0.3)
				}
			}
		}
		
		let op = BlockOperation {
			autoreleasepool {
				for _ in 1..<100 {
					guard !self.isCancelled else { return }
					print("🥳")
					Thread.sleep(forTimeInterval: 0.6)
				}
			}
		}
		queue.addOperation(op)
		
		op.addExecutionBlock {
			autoreleasepool {
				for _ in 1..<100 {
					guard !self.isCancelled else { return }
					print("🥺")
					Thread.sleep(forTimeInterval: 0.6)
				}
			}
		}
		
		let op2 = CustomOperation(type: "🥸")
		queue.addOperation(op2)
		
		op.completionBlock = {
			print("done")
		}
	}
	
	@IBAction func cancel(_ sender: Any) {
		isCancelled = true
		queue.cancelAllOperations()
	}
}

Class 내에 Flag로 사용할 isCancelled 변수를 생성하고, 해당 속성을 감지하도록 구현한다.
cancelAllOperations 메서드를 호출하는 시점에 해당 속성을 동일하게 바꿔 주면 된다.

이렇게 되면 모든 Operation을 일시에 취소할 수 있다.
단, 취소되는 경우에도 Operation이 완료된 것으로 간주되며, 따라서 Completion Handler를 실행하고,
해당 코드를 실행하게 된다.

Timer와 마찬가지로 Operation도 Scene을 벗어난다고 자동으로 종료되지 않고,
돌아온다고 다시 사용할 수도 없다.
따라서 Scene을 이동하는 경우엔 반드시 모든 Operation들을 종료하도록 구현해야한다.