[Swift] Operation과 OperationQueue를 사용했던 이유


Swift에는 Concurrency관련 문법이 여럿 있는데, 그 중에서 OperationQueue에 대해 최근 사용한 경험이 있어, 내용을 정리하게 되었습니다.

Operation, OperationQueue 간단 정의 및 쓰임

OperationQueue

Operation 객체들을 관리하여, 병렬 또는 직렬로 작업을 실행할 수 있게 하는 클래스입니다.

  • 작업을 병렬 또는 직렬로 실행할 수 있음
  • 큐에 추가된 작업의 우선순위 결정
  • 작업을 취소
  • 작업 최대 동시 작업 갯수를 제한할 수 있음

Operation

단일 Task를 정의하고 관리하는 추상 클래스

  • 단일 Task의 로직을 모듈화하여, 관리할 수 있음

간단한 로직이라면, 굳이 모듈화할 필요없이, 아래와 같이 코드블럭내에 직접 선언하여 사용

let queue = OperationQueue()
...
queue.addOperation {
    // 작업            
}

굳이 OperationQueue를? async-await, Task, TaskGroup사용하면 되지 않나?

Operation과 OperationQueue는 iOS 2.0부터 지원된 기능이며, async-await는 비교적(?) 최근에 도입된 문법입니다. 대부분의 경우 비동기작업을 처리할 때, async-await 및 Task, TaskGroup을 사용합니다. 때문에 굳이 OperationQueue를 사용할 필요가 있을까 생각이 듭니다.

하지만 저의 경우 다음 상황에서 Operation/OperationQueue를 사용한 경험이 있습니다.

[1]최대 작업 갯수 제한이 필요한 경우

물론 TaskGroup이나 DispatchSemaphore를 사용하여 최대 작업 수를 제한하고, 병렬로 비동기 작업을 처리할 수 있습니다.
하지만 아래의 예시 코드와 같이 로직을 직접 구현해야 합니다.

// TaskGroup 이용
await withTaskGroup(of: Void.self) { taskGroup in
    var activeTasks = 0

    for task in tasks {
        if activeTasks >= maxConcurrentTasks {
            await taskGroup.next() // 하나의 작업이 완료되기를 기다림
            activeTasks -= 1
        }

        taskGroup.addTask {
            await task()
        }
        activeTasks += 1
    }
        
    // 다른 작업 끝날 때까지 기다림
    while activeTasks > 0 {
        await taskGroup.next()
        activeTasks -= 1
    }
}

// Semaphore 이용
let semaphore = DispatchSemaphore(value: maxConcurrentTasks) // 최대 작업 갯수 지정
...
semaphore.wait() // 다른 작업 끝날 때까지 기다림
task()
semaphore.signal()

반면, OperationQueue를 이용하면 간단하게 최대 작업 갯수를 지정할 수 있습니다.

// OperationQueue 이용
let operationQueue = OperationQueue()
...
operationQueue.maxConcurrentOperationCount = maxConcurrentTasks // 최대 작업 갯수 지정

DispatchSemaphore 방식도 비교적 간단해 보이지만, 작업 취소나 순서관리와 같이 복잡한 작업 흐름을 처리하기에는 OperationQueue방식이 더 쉬울 것 같습니다.

[2] async-await 미지원 기능일 경우

async-await를 지원하지 않는 기능이라도 메서드를 async 메서드로 랩핑하여 사용할 수 있지만, 이 경우 예상치 못한 에러가 발생할 수 있습니다.
저의 경우 iOS17미만 버전에서 CoreML 추론기능을 사용할 때, 동시에 5개 이상의 작업을 동시에 진핼할시 크러쉬 나는 현상이 발생했습니다. (iOS17 버전 부터 추론메서드의 async-await 기능 지원)
이 경우에는 테스트를 통해 디바이스의 동시에 진행할 수 있는 테스크의 수를 파악한 후, OperationQueue를 이용해 최대 작업 갯수를 제한하여 처리하였습니다.

[3] 특정 작업 취소 로직을 구현하기가 간편

Operation 객체로 모듈화가 되어 있다보니, id: UUID과 같은 Hashable한 변수만을 이용해서 특정 작업을 취소할 수 있습니다.
TaskGroup에서도 특정 작업을 취소시키게 할 수 있지만, OperationQueue보다 로직이 복잡 했습니다. (Task전체를 딕셔너리형태로 보관하여 취소 작업을 컨트롤하도록 로직을 짜야됨, 아직 TaskGroup관련 문법을 잘 몰라서 일 수도 있음…)


Operation, OperationQueue 사용 예시

다음은 OperationQueue 사용의 간단한 예시입니다.

// Operation 채택 클래스
final class CalculateCountOperation: Operation {
    let id: UUID
    var result: Int?
    private let image: UIImage
    
    init(id: UUID, image: UIImage) {
        self.id = id
        self.image = image
    }
    
    override func main() {
        guard !isCancelled, // 취소됐는지 확인
              let cgImage = image.cgImage,
              let calculatedCount = calculateCount(from: cgImage)
        else { return }
        
        result = calculatedCount
    }
    
}

// OperationQueue
let operationQueue = OperationQueue()
...
operationQueue.maxConcurrentOperationCount = 3

// 작업 추가
func add operation(id: id, image: CGImage) {}
    let operation = CalculateCountOperation(id: id, image: image)
            
    operation.completionBlock = {
        if let result = operation.result {
            print("결과: ", result)
        } else {
            print("작업 실패 id: ", id)
        }
    }
            
    operationQueue.addOperation(operation)
}

// 작업 취소
func cancelOperation(by id: UUID) {
    for operation in operationQueue.operations {
        if let detectFaceOperation = operation as? CalculateCountOperation,
           detectFaceOperation.id == id {
            detectFaceOperation.cancel()
        }
    }
}

// 작업 전체 취소
func cancelAllOperation() {
    operationQueue.cancelAllOperations()
}

그래서 OperationQueue vs TaskGroup ?

OperationQueue는 iOS 2.0부터 지원되는 오랜 역사를 가진 기능으로, 안정성과 신뢰성이 높습니다.

TaskGroup은 최신 비동기 프로그래밍 모델인 async/await과 함께 발전하고 있으며, 계속해서 기능 개선과 업데이트가 이루어지고 있습니다. 이러한 발전 덕분에 언젠가는 OperationQueue를 완전히 대체할 수 있지 않을까 생각합니다.

하지만 현재로서는 앱의 iOS 지원 버전 혹은 Swift버전에 따라 TaskGroup을 사용하는게 비효율적일 수 있습니다. 따라서, 앱이 지원하는 iOS 버전과 프로젝트의 요구 사항에 따라 적절한 도구를 선택하는 것이 중요하다고 생각합니다.

결론적으로, 비동기 작업을 처리할 때 어떤 기능을 사용하든 각 기능의 본질을 이해하고, 상황에 맞게 적절히 선택하여 사용하는 것이 좋다고 생각합니다.


참고 링크

Operation | Apple Developer Documentation
OperationQueue | Apple Developer Documentation




© 2021.02. by kirim

Powered by kkrim