[iOS] Concurrency Programming (동시성 프로그래밍) 3 : Operation

2023. 3. 16. 21:17iOS/iOS

728x90
반응형

👉 이전 글

 

[iOS] Concurrency Programming (동시성 프로그래밍) 2 : GCD, DispatchGroup, DispatchWorkItem

👉 시작 2023.03.15 - [iOS/iOS] - [iOS] Concurrency Programming (동시성 프로그래밍) 1 : Sync (동기), Async (비동기) [iOS] Concurrency Programming (동시성 프로그래밍) 1 : Sync (동기), Async (비동기) 👉 시작 프로그래밍

nlestory.tistory.com

 

 

👉 시작

 

 

동시성 프로그래밍을 하기 위해서 iOS에서 제공하는 기술인 GCD와 Operation 중

오늘은 Operation 에 대한 공부한 내용이다.

 

 


👉 Operation

  • 단일 작업과 관련된 코드 및 데이터를 나타내는 추상클래스이다.
  • Operation은 추상 클래스이므로 직접 사용하지 않고 하위 클래스나 시스템에서 정의된 하위 클래스를 대신 사용하여 실제 작업을 수행한다.
  • 추상적임에도 기본 구현에는 작업의 안전한 실행을 조정하는 로직이 포함되어 있다.
  • operation queue를 사용하지 않으려면 start() 코드에서 직접 메소드를 호출하여 작업을 직접 실행할 수 있다.
    준비 상태가 아닌 작업을 시작하면 예외 트리거가 되기 때문에 작업을 수동으로 실행하면 코드에 많은 부담이 가해진다.
  • 작업을 대기열에 추가하는 대신 수동으로 실행할 계획이라면 동기나 비동기 방식으로 작업을 실행하도록 설계할 수 있다.
    디폴트값은 동기 방식이다.
    동기 작업에서는 작업을 실행할 별도의 쓰레드를 만들지 않는다.
    start() 코드에서 직접 동기 작업의 메소드를 호출하면 작업이 현재 쓰레드에서 즉시 실행된다.
    이러한 메소드가 호출자에게 제어를 반환할 때까지 작업 자체가 완료된다.
  • 비동기 작업의 메소드를 호출하면 start() 작업이 완료되기 전에 해당 메소드가 반환될 수 있다.
    비동기 작업 개체는 별도의 쓰레드에서 해당 작업을 예약한다.
    새 쓰레드를 직접 시작하거나 비동기식 메소드를 호출하거나 실행을 위해 블록을 디스패치큐에 제출하여 수행한다.

 

오퍼레이션은 단일 작업을 하는 개체이기 때문에 실행이 완료된 인스턴스는 다시 실행할 수 없다.

동일한 작업을 반복해야하는 경우에는 새로운 인스턴스를 생성해서 실행해야 한다.

오퍼레이션은 디폴트 값이 동기 방식이기 때문에 다른 코드와 동시에 실행되는 것을 보장하지 않는다. 비동기 방식으로 실행되도록 구현할 수 있디만 권장하지 않고 오퍼레이션 큐에 추가하는 것이 좋다.

 

 

👉 Operation Queue

  • 작업 실행을 규제하는 큐이다.
  • 오퍼레이션큐는 우선 순위와 준비 상태에 따라 대기중인 개체를 호출한다.
    작업을 오퍼레이션큐에 추가하면 해당 작업이 완료할 때까지 작업이 큐에 남아있다.
    작업을 추가한 후에 큐에서 작업을 직접 제거할 수 없다.
    완료되지 않은 작업으로 큐를 일시 중단하면 메모리 누수가 발생할 수 있다.

 

오퍼레이션의 실행 순서는 우선순위나 의존성에 따라서 자동으로 결정된다. 사용 가능한 CPU코어와 시스템 리소스를 활영해서 동시에 실행할 수 있는 작업의 수를 결정하고 최대한 빠르게 실행한다.

완료된 작업은 큐에서 자동으로 삭제되고 아직 시작되지 않은 작업은 언제든지 취소할 수 있다. 현재 실행중인 작업은 취소 기능을 구현한 경우 취소할 수 있다. 취소 구현이 되지 않은 경우 계속 작업이 실행된다. 구현할 때 항상 취소 기능을 구현하는 것이 좋다.

 

✔️  QueuePriority

동일한 큐에 추가되어 있는 오퍼레이션에 우선순위를 정할 수 있다.

높은 우선순위를 가진 오퍼레이션이 먼저 실행된다.

veryHigh, high, normal, low, veryLow 설정이 가능하며 디폴트 값은 normal 이다.

 

✔️  Quality of Service

우선순위가 높을 수록 CPU, 네트워크 등 더 오랜 시간을 사용할 수 있고 먼저 실행한다.

userInteractive, userInitiated, utility, background 설정이 가능하면 디폴트 값을 background 이다.

 

 

👉 Operation Queue 종류

✔️  main

  • 메인 쓰레드와 관련된 오퍼레이션 큐를 반환한다.
  • 반환된 큐는 앱의 기본 쓰레드에서 한 번에 하나의 작업을 실행한다.
  • 기본 쓰레드에서의 작업 실행은 이벤트 서비스 및 앱의 UI 업데이트와 같이 기본 쓰레드에서 실행해야하는 작업을 진행한다.
  • 큐의 속성값은 기본 쓰레드의 디스패치 큐이다. 이 속성은 다른 값으로 설정할 수 없다.

 

✔️  current

  • 현재 작업을 시작한 오퍼레이션 큐를 반환한다.
  • 작업을 시작한 오퍼레이션 큐 또는 큐를 확인할 수 없는 경우 nil 값을 반환한다.
  • 실행 중인 작업 개체 내에서 이 메소드를 사용하여 이를 시작한 오퍼레이션 큐에 대한 참조를 가져올 수 있다.
  • 실행 중인 작업 외부에서 이 메소드를 호출하면 일반적으로 nil이 반환된다.

 

 

✔️  사용 방법 1

1. OperationQueue 생성

let mainQueue = OperationQueue.main
let currentQueue = OperationQueue.current

main = 메인 쓰레드와 관련된 오퍼레이션큐

current = 현재 작업이 시작된 오퍼레이션큐

let queue = OperationQueue()

비동기 작업을 실행하는 오퍼레이션 큐

 

2. Operation 추가

mainQueue.addOperation {
    for i in 1...1000 {
        print("\(i)")
    }
}

오퍼레이션을 작성하여 큐에 추가한다.

main은 메인 쓰레드와 관련된 오퍼레이션 큐이므로 UI와 관련된 작업을 해준다.
디스패치큐 main 과 같은 역할을 한다.

 

 

✔️  사용 방법 2

1. Operation  Class 생성

class CustomOperation: Operation {
    override func main() {
        for i in 1...10 {
            print("\(i)")

        }
    }
}

커스텀된 오퍼레이션 큐는 main을 꼭 오버라이딩해줘야한다.

오퍼레이션을 main에 작성한다.

 

2. Operation 추가

let queue = OperationQueue()
let customOP = CustomOperation()
queue.addOperation(customOP)

 

3. 종료 시점

customOP.completionBlock = {
    print("끝")
}

오퍼레이션의 종료 시점을 알 수 있다.

 

 

✔️  사용 예제

let queue = OperationQueue()

queue.addOperation {
    for i in 1...10 {
        print("\(i) 💚 ")
        self.mainQueue.addOperation {
            self.label.text = "\(i)"
        }
    }
}

UILabel의 텍스트를 변경하는 작업은 UI가 변경되는 작업이므로 main에서 변경해줘야한다.

 

✔️ 커스텀 오퍼레이션 추가

queue.addOperation {
    for i in 1...10 {
        print("\(i) 💚 ")
        self.mainQueue.addOperation {
            self.label.text = "\(i)"
        }
    }
}

//MARK: - 커스텀 오퍼레이션
class CustomOperation: Operation {
    override func main() {
        for i in 1...10 {
            print("\(i) 💛 ")
        }
    }
}
let customOP = CustomOperation()
queue.addOperation(customOP)
customOP.completionBlock = {
    print("커스텀 오퍼레이션 작업 완료")
}

두 가지의 작업을 큐에 추가하여 실행한 결과이다. 두 가지가 동시에 진행되는 것을 알 수 있다.

커스텀 오퍼레이션은 종료시점을 알리는 컴플리션블록으로 출력을 해주었다. 

 

✔️ 최대 실행 오퍼레이션 수 지정

큐의 오퍼레이션 수를 제한할 수도 있다.

queue.maxConcurrentOperationCount = 1

maxConcurrentOperationCount 를 이용하여 작성하면 한 번에 실행되는 오퍼레이션을 제한한다.

1로 지정하였을 때 동시에 1개의 오퍼레이션만 작동이 된다.

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1

queue.addOperation {
    for i in 1...10 {
        print("\(i) 💚 ")
        self.mainQueue.addOperation {
            self.label.text = "\(i)"
        }
    }
}

//MARK: - 커스텀 오퍼레이션
class CustomOperation: Operation {
    override func main() {
        for i in 1...10 {
            print("\(i) 💛 ")
        }
    }
}
let customOP = CustomOperation()
queue.addOperation(customOP)
customOP.completionBlock = {
    print("커스텀 오퍼레이션 작업 완료")
}

전의 예제와 같은 코드에서 제한만 추가해준 경우이다.

제한을 두지 않은 경우를 A, 제한을 둔 경우를 B라고 하자.

A에서는 2가지의 오퍼레이션을 추가하여 실행하였기에 2가지가 동시에 실행이 되어 둘의 진행이 섞여있는 반면,

B에서는 최대 오퍼레이션을 1로 지정하여 1가지의 오퍼레이션을 실행하게 제한을 두어 1가지가 끝나야 다음 1가지를 실행하게 된다.

이렇게 Serial큐 같은 실행을 할 수 있게 해준다.

 

✔️ 오퍼레이션 취소

취소상태를 계속 체크하면서 작업을 중지시킨다.

var isCancelled: Bool = true

변수를 선언하여 상태에 따라 취소를 할 수 있게 한다.

queue.addOperation {
    for i in 1...10 {
        sleep(1)
        guard self.isCancelled else {
            return
        }
        print("\(i) 💚 ")
        self.mainQueue.addOperation {
            self.label.text = "\(i)"
        }
    }
}
@IBAction func touchUpButton(_ sender: Any) {
    print("버튼클릭")
    label.text = "작업 취소"
    isCancelled = false
}

1초에 1씩 증가하게 하고 버튼을 만들어서 버튼을 클릭하면 작업이 취소되게 하였다.

 

 

 

👉 BlockOperation 

  • 하나 이상의 블록을 동시에 실행하는 것을 관리하는 오퍼레이션이다.
  • 각각에 대해 별도의 오퍼레이션을 만들지 않고도 한 번에 여러 블록을 실행할 수 있다.
  • 둘 이상의 블록을 실행할 때 모든 블록이 실행을 완료한 경우에만 작업 자체가 완료된 것으로 간주한다.
  • 블록오퍼레이션에 추가된 블록은 적절한 작업 큐에 기본 우선 순위로 발송된다.

 

✔️  addExecutionBlock

  • 블록 목록에 지정된 블록을 추가한다.
  • 이미 실행 중이거나 완료된 이후에 이 메소드를 추가하면 예외가 발생한다.

블록오퍼레이션은 이 메소드를 통해서 하나의 오퍼레이션에 두 개 이상의 블록을 추가할 수 있다.

추가된 블록은 나머지 블록과 동시에 실행된다.

 

✔️  사용 방법

1. BlockOperation 생성

let blockOperation = BlockOperation {
    for i in 1...1000 {
        print("\(i)")
    }
}

 

2. BlockOperation 실행

blockOperation.start()

start 메소드를 이용하여 작업을 수동으로 실행한다.

오퍼레이션은 디폴트가 동기 방식으로 진행되기 때문에 하나씩 실행된다. 비동기 방식을 이용할 때에는 오퍼레이션큐에 추가해야한다.

OperationQueue().addOperation(blockOperation)

큐에 넣어서 실행할 경우에는 비동기 방식으로 실행된다.

 

3. addExecutionBlock 새로운 블록 추가

blockOperation.addExecutionBlock {
    for i in 1...10 {
        print("\(i)")
    }
}

블록 오퍼레이션에 새로운 블록을 추가하여 두 가지 블록을 같이 실행한다.

 

 

✔️  사용 예제

✔️ 동기 방식

let RedblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) ❤️ ")
    }
}

let OrangeblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) 🧡")
    }
}
RedblockOperation.start()
OrangeblockOperation.start()

먼저 실행한 Red가 먼저 끝까지 출력된 후에 Orange가 다음으로 실행되어 출력된다.

 

✔️ 비동기 방식

let RedblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) ❤️ ")
    }
}

let OrangeblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) 🧡")
    }
}

let queue = OperationQueue()
queue.addOperation(RedblockOperation)
queue.addOperation(OrangeblockOperation)

두 가지의 블록을 큐에 추가하여 비동기 방식으로 실행된다.

큐에 추가하여 종속성을 부여하여 순서를 정해줄 수 있다.

 

✔️ 종속성 추가

let queue = OperationQueue()
//종속성 추가
RedblockOperation.addDependency(OrangeblockOperation)
queue.addOperation(RedblockOperation)
queue.addOperation(OrangeblockOperation)

종속성을 추가하게 되면 하나의 작업이 끝나면 다음 작업이 실행된다.

Red의 의존성을 추가해준 것인데 현재 Red개체가 실행을 시작하기전에 완료해야되는 작업을 추가해주는 것이다.

시작하기전에 완료해야되는 작업을 뒤에 작성하므로 먼저 작성한 것이 나중에 실행되는 것이다.

의존성을 추가하는 작업은 큐에 추가하기 전에 해줘야한다.
큐에 추가하고 의존성을 추가하는 것은 실행한 뒤에 이뤄지기 때문에 종속성이 적용되지 않는다.

 

✔️ 새로운 블록 추가

let RedblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) ❤️ ")
    }
}

let OrangeblockOperation = BlockOperation {
    for i in 1...10 {
        print("\(i) 🧡")
    }
}

OrangeblockOperation.addExecutionBlock {
    for i in 1...10 {
        print("\(i) 💛 ")
    }
}
RedblockOperation.start()
OrangeblockOperation.start()

start() 를 사용하여 수동으로 실행하였고 Orange 블록에 새로운 블록을 추가해주었다.

그러면 Red를 실행하고 Orange 블록에 기존 블록과 새로운 블록이 함께 실행된다.

이 때 주의할 점은 완료된 이후에 새로운 블록을 추가하게 되면 예외가 발생한다.

RedblockOperation.start()
OrangeblockOperation.start()

sleep(3)
print("휴식")

OrangeblockOperation.addExecutionBlock {
    for i in 1...10 {
        print("\(i) 💛 ")
    }
}

오퍼레이션을 실행시킨 후 완료한 후 3초 기다렸다가 Orange 블록에 새로운 블록을 추가해준 것이다.

이렇게 하면 블록이 완료된 후에 추가되었다는 에러가 나오게 된다.

 

 

 

 

 

 


📂 정리

OperationQueue

  • UI 작업은 main 에서 진행해야 한다.
  • 커스텀 오퍼레이션을 만들 수 있다.
  • 두 가지 이상의 오퍼레이션을 큐에 추가하여 실행하면 비동기 방식으로 실행되고
    start를 이용하여 실행하면 동기 방식으로 실행된다.
  • Serial 큐처럼 구현할 수 있는 방식은 maxConcurrentOperationCount 을 지정하는 방법과
    addDependency 을 이용하여 종속성을 추가하는 방법이 있다.

 

 

 

 

 

 

 

 

 

 

 

[예제 소스코드 깃허브 링크]

https://github.com/HANLeeeee/PracticeTest/tree/main/OperationTest

 

GitHub - HANLeeeee/PracticeTest

Contribute to HANLeeeee/PracticeTest development by creating an account on GitHub.

github.com

[참고자료]

Apple_Developer_Documentation_Operation

Apple_Developer_Documentation_OperationQueue

Apple_Developer_Documentation_BlockOperation

 

 

 

 

728x90
반응형