[2019.03.13]

원문 : https://www.raywenderlich.com/5371-grand-central-dispatch-tutorial-for-swift-4-part-2-2

Grand Centrial Dispatch Tutorial for Swift 4: Part 2/2

Grand Central Dispatch 튜토리얼 시리즈의 두번째이자 마지막 부분에 온걸 환영합니다.

이 시리즈의 첫번째 부분에서 동시성(concurrency), 쓰레딩(threading)과, GCD 동작 방법에 대해서 배웠었습니다. dispatch barriers와 synchronous dispatch queue의 조합을 사용해서, 읽고 쓰는 싱글톤을 스레드에 안전하도록 만들었습니다. 또한 뷰 컨트롤러를 인스턴화 할때 프롬프트(prompt)에 표시되는 것을 지연시키고 CPU 집중적인(CPU-intensive) 작업을 비동기적으로 바꾸기 위해, dispatch queues를 사용해서 앱의 UX를 향상 시켰습니다.

두번째 Grand Central Dispatch 튜토리얼에서, 이전 부분에서 여러분이 알고있고 사랑하는 동일한 googlyPuff 앱으로 작업할 것입니다. 고급 GCD 개념은 dispatch groups, canceling dispatch blocks, asynchronous testing techniques, dispatch sources 등을 포함합니다.

더 많은 GCD를 탐험할 시간입니다!

시작하기(Getting Started)

샘플 프로젝트를 따라했었다면, 이전 부분에서 중단하고 남겨둔 부분부터 다시 시작할 수 있습니다. 또는 이 튜토리얼의 Download로 시작 프로젝트를 다운로드 받아 사용할 수 있습니다.

앱을 실행해서, +버튼을 탭하고 인터넷으로 사진을 추가하기 위해 Le Internet를 선택합니다. 이미지 다운로드가 완료되기 전에 다운로드 완료 경고 메시지가 나타날수도 있습니다.

고치기 위해 해야할 첫번째 입니다.

Dispatch Groups

PhotoManager.swift을 열고 downloadPhotos(withCompletion:)을 보세요.

func downloadPhotos(
    withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError?
  for address in [PhotoURLString.overlyAttachedGirlfriend,
                  PhotoURLString.successKid,
                  PhotoURLString.lotsOfFaces] {
                    let url = URL(string: address)
                    let photo = DownloadPhoto(url: url!) { _, error in
                      if error != nil {
                        storedError = error
                      }
                    }
                    PhotoManager.shared.addPhoto(photo)
    }
    
    completion?(storedError)
}

메소드에 전달된 completion 클로져는 경고를 띄웁니다. 사진을 다운로드하는 for 반복문 뒤에 호출합니다. 클로져를 호출하기 전에 다운로드가 완료되었다고 잘못 가정했습니다.

DownloadPhoto(url:) 호출해서 사진 다운로드를 시작합니다. 이 호출은 즉시 반환하지만 실제 다운로드는 비동기적으로 발생합니다. 따라서 completion을 실행할때, 모든 다운로드가 완료되었는지를 보장하지 않습니다.

원하는 것은 downloadPhotos(withCompletion:)이 모든 사진 다운로드 작업이 완료된 이후(after)에 완료 클로져를 호출하는 것입니다. 이를 위해서 동시성(concurrent) 비동기 이벤트를 어떻게 감시할 수 있을까요? 핸재 방법론(methodology)으로, 작업이 완료될때와 어떤 순서로 끝나는지를 알지 못합니다.

좋은 소식입니다! 이것이 dispatch groups가 존재하는 이유 입니다. dispatch groups는 여러개의 작업들을 함께 그룹화하고 완료될때까지 기다릴수 있으며, 또는 그것들이 완료될때 알림을 받을 수 있습니다. 작업은 비동기 또는 동기로 할 수 있으며, 심지어는 다른 큐에서도 실행할 수 있습니다.

DispatchGroup는 dispatch groups를 관리합니다. wait 메소드를 먼저 보게 될 것입니다. 이는 그룹의 큐에 추가되어 있는 모든 작업이 완료될때까지 현재 스레드를 차단합니다.

PhotoManager.swift에서 downloadPhotos(withCompletion:)에 있는 코드를 다음에 오는 것으로 교체합니다.

// 1
DispatchQueue.global(qos: .userInitiated).async {
  var storedError: NSError?

  // 2
  let downloadGroup = DispatchGroup()
  for address in [PhotoURLString.overlyAttachedGirlfriend, 
                  PhotoURLString.successKid,
                  PhotoURLString.lotsOfFaces] {
    let url = URL(string: address)

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url!) { _, error in
      if error != nil {
        storedError = error
      }   

      // 4
      downloadGroup.leave()
    }   
    PhotoManager.shared.addPhoto(photo)
  }   

  // 5      
  downloadGroup.wait()

  // 6
  DispatchQueue.main.async {
    completion?(storedError)
  }   
}

다음은 이 코드가 단계별로 무엇을하는지 입니다.

  1. 현재 스레드를 차단하는, 동기적인(synchronous) wait 메소드를 사용하며, 전체 메소드를 백그라운드 큐에 배치해서 메인스레드를 차단하지 않도록 하기 위해 async를 사용합니다.
  2. 새로운 dispatch group을 만듭니다.
  3. 작업이 시작되었음을 그룹에 수동적으로 알리기 위해 enter()를 호출합니다. enter() 호출 갯수와 leave() 호출 갯수를 맞춰야 하며 그렇지 않으면 앱은 크래쉬가 발생할 것입니다.
  4. 작업이 완료된것을 그룹에 알립니다.
  5. 작업이 완료되는 것을 기다리는 동안 현재 스레드를 차단하기 위해 wait()를 호출합니다. 사진 만드는 작업이 항상 완료되기 때문에 영원히 기다리는 것도 괜찮습니다. 지정한 시간을 지정하고 지정된 시간동안 기다린후에 벗어나기(bail out) 위해 wait(timeout:)을 사용할 수 있습니다.
  6. 이 시점에, 모든 이미지 작업이 완료되거나 타임아웃이 발생하는 것을 보장합니다. 그리고나서 완료 클로져를 실행하기 위해 메인 큐에서 다시 호출하도록 만듭니다.

빌드하고 앱을 실행합니다. Le Internet 옵션으로 사진을 다운로드하고 모든 이미지가 다운로드 될때까지 경고가 표시되지 않는지 확인합니다.

주의
네트워크가 너무 빨라서 완료 클로져를 언제 호출해야 하는지 식별할수 없고, 기기에서 앱을 실행하는 경우에, iOS Settings 앱의 Developer 섹션에 있는 네트워크 설정을 토글(toggling)해서, 진짜 동작하는지 확인 할 수 있습니다. Network Link Conditioner 섹션으로 가고, 프로파일을 Very Bad Netwrok로 설정하는 것은 좋은 선택입니다.

시뮬레이터에서 실행하는 경우에, 네트워크 속도를 변경하기 위해 Network Link Conditioner inclouded in Advanced Tools for Xcode를 사용할 수 있습니다. 연결 속도가 최적일때보다 낮을때 어떤 일이 일어나는지 인식할수 있어야 하기 때문에, 이것은 여러분이 가지고 있는 좋은 도구입니다.

Dispatch groups은 모든 타입의 큐에 적합합니다. 모든 작업이 완료될때까지 동기적으로 기다리는 경우에 메인스레드를 잡아두고 싶지 않기 때문에, 메인 큐에서 dispatch groups를 사용하는 것에 주의해야 합니다. 하지만, 비동기 모델은 네트워크 호출과 같이 길게 실행하는 몇개의 작업이 완료되면 UI를 업데이트하기 위한 매력적인(attractive) 방법입니다.

현재 해결책이 좋지만, 일반적으로 가능하다면 스레드를 차단하는 것을 피하는 것이 가장 좋습니다. 다음 작업은 모든 다운로드가 완료될때 비동기적으로 알리기 위해, 일부 메소드를 다시 작성하는 것입니다.

Dispatch Groups, Take 2

다른 큐에 비동기적으로 전달하고나서 wait으로 작업을 차단하고 있는 것은 어색(clumsy)합니다. 다행히, 더 좋은 방법이 있습니다. DispatchGroup은 그룹의 모든 작업이 완료 될때 대신 알릴 수 있습니다.

PhotoManager.swift에서, downloadPhotos(withCompletion:) 안쪽 코드를 다음에 오는 것으로 교체합니다.

// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [PhotoURLString.overlyAttachedGirlfriend,
                PhotoURLString.successKid,
                PhotoURLString.lotsOfFaces] {
  let url = URL(string: address)
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url!) { _, error in
    if error != nil {
      storedError = error
    }   
    downloadGroup.leave()
  }   
  PhotoManager.shared.addPhoto(photo)
}   

// 2    
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

다음은 무엇을 하는지 입니다.

  1. 새로운 구현에서, 메인 스레드를 차단하지 않기에 메소드를 async 호출로 감쌀 필요가 없습니다.
  2. notify(queue:work:)가 비동기 완료 클로져 역할을 합니다. 그룹에 더 이상 남은 항목이 없을때 실행합니다. 또한 메인 큐에서 실행하도록 완료 작업을 스케쥴링하도록 지정할 수 있습니다.

이것은 스레드를 차단하지 않으므로 특정 작업을 처리하기 위한 훨씬 더 깔끔한 방법입니다.

빌드하고 앱을 실행합니다. 인터넷으로 모든 사진이 완료된 후에 다운로드 완료 경고가 여전히 보이는지 확인합니다.

동시성 반복하기(Concurrency Looping)

새로운 도구를 마음대로 사용한다면, 스레드를 사용할 것입니다. 그렇죠?

PhotoManager에서 downloadPhotos(withCompletion:)을 살펴봅니다. for 반복문이 3번 반복되고 3개의 개별 이미지가 다운로드 되는 것을 알수도 있습니다. 여러분이 해야할 일은 for 반복문을 동시에 실행해서 속도를 높일수 수 있는지 확인하는 것입니다.

이는 DispatchQueue.concurrentPerform(iterations:execute:)에 대한 작업입니다. 그 작업은 동시에 실행한다는 점에서 for반복문과 비슷합니다. 그것은 동기식(synchronous)이고 모든 작업이 완료될때만 반환됩니다.

주어진 작업량에 따라 최적의 반복횟수를 계산할때 주의해야 합니다. 많은 반복과 반복할때마다 작은 양의 작업은 과부하(overhead)가 발생해서 동시 호출을 만듬으로써 얻는 이익이 없어질 수 잇습니다. striding으로 알려진 기술이 여러분을 도울것입니다. Striding은 반복할때마다 여러가지 작업을 할 수있게 해줍니다.

DispatchQueue.concurrentPerform(iterations: execute:)을 언제 사용해야 하나요? 여기에서 이득이 없으면 직렬 큐를 배제(rule out) 할 수 있습니다 - 일반 for 반복문을 사용할 수도 있습니다. 특히 진행 상황을 추적해야 하는 경우에, 반복문이 포함된 동시성 큐(concurrent queues)는 좋은 선택입니다.

PhotoManager.swift에서 downloadPhotos(withCompletion:) 코드 안쪽을 다음에 오는 것으로 교체합니다.

var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [PhotoURLString.overlyAttachedGirlfriend,
                 PhotoURLString.successKid,
                 PhotoURLString.lotsOfFaces]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
  let address = addresses[index]
  let url = URL(string: address)
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url!) { _, error in
    if error != nil {
      storedError = error
    }
    downloadGroup.leave()
  }
  PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

동시성 반복문으로 처리하기 위해서 DispatchQueue.concurrentPerform(iterations:execute:)로 이전 for 반복문을 교체했습니다.

이 구현에는 흥미로운 코드가 포함되어 있습니다: let _ = ispatchQueue.global(qos: .userInitiated). 이를 호출해서 GCD를 동시 호출에 대한 .userInitiated 서비스 품질(Qos)로된 큐를 사용할 수 있습니다.

빌드하고 앱을 실행합니다. 인터넷 다운로드 기능이 여전히 올바르게 동작하는지 확인합니다.

기기에서 새로운 코드를 실행하면 때때로 결과가 조금 더 빨라질 것입니다. 하지만 모든 작업이 가치(worth)가 있나요?

실제로, 이럴때에는 가치(worth)가 없습니다. 다음은 왜 그런지 입니다:

  • 첫번째 위치에서 for반복문을 실행하는 것보다 병렬로 스레드를 실행하는데 더 많은 과부하(overhead)가 걸릴 수도 있습니다. 적절한 걸음걸이(stride length)로 매우 큰 설정을 반복하기 위해, DispatchQueue.concurrentPerform(iterations:execute:)를 사용해야 합니다.
  • 알지 못하는 것을 최적화하는 코드로 앱을 만드는데는 시간이 제한적입니다(미리 낭비하지 마세요). 무언가를 최적화하려는 경우에, 눈에 띄고 시간을 쓸 가치가 있는 것을 최적화 합니다. Instruments에서 앱을 프로파일링해서 가장 긴 실행시간인 메소드를 찾습니다. 더 자세히 배우기위해 How to Use Instruments in Xcode를 확인하세요.
  • 일반적으로, 코드를 최적화 하는것은 자신과 다른 개발자들에게 코드를 더 복잡하게 만듭니다. 복잡합이 추가된것이 가치가 있는지 확인합니다.

최적화에 미쳐서는 안되는 것을 기억하세요. 자신과 다른 누군가가 여러분의 코드를 살펴봐야 하는경웨 어려워질 수 있습니다.

Canceling Dispath Blocks

지금까지(Thus far), 큐에 포함된 작업을 취소할 수 있는 코드를 보지 못했습니다. 이는 DispatchWorkItem 에 중점을 두고 dispatch block objects를 표현되었습니다. 큐의 처음에 도착해서 실행하기 전에(before)DispatchWorkItem으로만 취소할 수 있습니다.

Le Internet으로 여러개의 이미지를 다운로드 하는 작업을 하고 이를 취소해 봅시다.

PhotoManager.swift에서 downloadPhotos(withCompletion:)에 있는 코드를 다음에 오는 것으로 교체합니다.

var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [PhotoURLString.overlyAttachedGirlfriend,
                 PhotoURLString.successKid,
                 PhotoURLString.lotsOfFaces]

// 1
addresses += addresses + addresses

// 2
var blocks: [DispatchWorkItem] = []

for index in 0..<addresses.count {
  downloadGroup.enter()

  // 3
  let block = DispatchWorkItem(flags: .inheritQoS) {
    let address = addresses[index]
    let url = URL(string: address)
    let photo = DownloadPhoto(url: url!) { _, error in
      if error != nil {
        storedError = error
      }
      downloadGroup.leave()
    }
    PhotoManager.shared.addPhoto(photo)
  }
  blocks.append(block)

  // 4
  DispatchQueue.main.async(execute: block)
}

// 5
for block in blocks[3..<blocks.count] {

  // 6
  let cancel = Bool.random()
  if cancel {

    // 7
    block.cancel()

    // 8
    downloadGroup.leave()
  }
}

downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

다음은 위 코드를 단계별로 살펴봅니다.

  1. 각 이미지의 복사본을 3개를 저장하기 위해 address 배열을 늘려줍니다.
  2. 나중에 사용하기 위해 dispatch block객체들을 blocks 배열로 초기화 합니다.
  3. 새로운 DispatchWorkItem을 만듭니다. 블럭이 전달(dispatch)하는 큐(queue)에서 서비스 품질(Quality of Services) 클래스를 상속하도록 지정하기 위해서, flags 매개변수로 전달합니다. 그리고나서 클로져에 수행해야 할 작업을 정의합니다.
  4. 메인 큐에 비동기적으로 블럭을 전달합니다. 이 예제에서, 메인 큐를 사용하면 직렬 큐이기에 취소할 블럭을 쉽게 선택할 수 있습니다. 전달 블럭을 설정하는 코드는 이미 메인 큐에서 실행중이므로 다운로드 블럭이 나중에 실행될 것이라는 것을 보장합니다.
  5. blocks 배열을 분할해서 처음 다운로드하는 3개의 블럭을 건너뜁니다.
  6. 여기에서 true와 false 간에 무작위로 고르기 위해 Bool.random()을 사용합니다. 이는 동전 던지기와 비슷합니다.
  7. 무작위 값이 true인 경우에, 블럭을 취소합니다. 큐에 남아있고 실행하기 전에만 블럭을 취소할 수 있습니다. 실행중에는 블럭을 취소할 수 없습니다.
  8. 여기에서 취소된 블럭을 dispatch group에서 제거해야 하는 것을 기억합니다.

빌드하고 앱을 실행하고나서, Le Internet로 이미지를 추가합니다. 3개 이상의 이미지들을 다운로드하는 것을 보게 될 것입니다. 추가 이미지는 앱을 다시 실행할 때마다 변경됩니다. 큐에서 추가적인 이미지 다운로드가 시작되기 전에 일부를 취소합니다.

이것은 부자연스러운(contrived) 예제이지만, dispatch blocks을 사용하고 취소하는 방법을 보여주는 좋은 에제입니다.

Dispatch blocks는 더 많은 것을 할수 있으며, Apples' documentation을 보세요.

그외 GCD(Miscellaneous GCD Fun)

기다려요! 조금 더 있습니다! 다음은 주제에서 조금은 벗어난 몇가지 추가 기능들이 있습니다. 이러한 도구들은 거의 사용되지 않지만, 적절한 상황에서 엄청나게(tremendously) 도움이 될 수 있습니다.

비동기 코드 테스트하기(Testing Asynchronous Code)

미친 생각으로 들릴지 모르지만, Xcode에 테스트 기능이 있다는 것을 알고 있었나요? :] 알고 있습니다. 가끔은 없는 것처럼하는 것을 좋아하지만, 테스트를 작성하고 테스트하는 것은 코드에서 복잡한 관계를 만들때 중요합니다.

Xcode 테스트는 XCTestCase의 하위클래스에 모두 포함되어 있고 모두 Test로 시작하는 메소드 입니다. 테스트는 메인스레드에서 실행되므로, 모든 테스트가 순차적(serial)으로 발생한다고 가정할 수 있습니다.

주어진 테스트 메소드가 완료되자마자, Xcode는 테스트가 완료된것으로 간주하고 다음 테스트로 넘어갑니다. 이는 이전 테스트의 비동기 코드가 다음 테스트가 실행되는 동안 계속 실행되는 것을 의미합니다.

네트워크 코드는 네트워크로 가져오는 동안에 메인 스레드를 차단하길 원치 않으므로 일반적으로 비동기입니다. 테스트 메소드가 끝났을때 사실상 테스트가 완료된것과 네트워킹 코드를 함께 테스트 하는 것이 어려울 수 있습니다.

비동기 코드를 테스트하기 위해 semaphores를 사용하는 방법을 간단히 살펴봅시다.

Semaphores

세마포어(Semaphores)는 Edsger W. Dijkstra가 세상에 선보인 구식 쓰레딩 개념입니다. 세마포어는 복잡한 운영체제 기능에 기반하기 때문에 복잡한 주제입니다.

세마포어(Semaphores)에 대해 더 자세히 배우길 원하는 경우, 세마포어 이론에 대해 detailed discussion를 보세요. 학문적(academic type)인 경우에는 세마포어를 사용하는 고전적인 소프트웨어 개발 문제인, Dining Philosophers Problem를 보세요.

GooglyPuffTests.swift를 열고 downloadImageURL(withString:) 안쪽을 다음에 오는 것으로 교체합니다.

let url = URL(string: urlString)

// 1
let semaphore = DispatchSemaphore(value: 0)
let _ = DownloadPhoto(url: url!) { _, error in
  if let error = error {
    XCTFail("\(urlString) failed. \(error.localizedDescription)")
  }

  // 2
  semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)

// 3
if semaphore.wait(timeout: timeout) == .timedOut {
  XCTFail("\(urlString) timed out")
} 

다음은 위 코드에서 세마포어로 작업하는 방법입니다.

  1. 세마포어를 만들고 시작값을 설정합니다. 이는 세마포어를 증가시키지 않고 세마포어를 사용할 수 있는 것들의 갯수를 나타냅니다(세마포어를 증가하는 것을 signaling이라고 합니다)
  2. 완료 클로져에서 세마포어 신호를 보냅니다. 세마포어 갯수를 증가하고 세마포어를 다른 리소스에서 사용할수 있는 신호입니다.
  3. 주어진 타임아웃 시간으로 세마포어를 기다립니다. 이 호출은 세마포어에 신호가 올때까지 현재 스레드를 차단합니다. 이 함수로부터 0이 아닌 코드를 반환하는 것은 타임아웃 기간이 만료되었음을 의미합니다. 이 경우에, 테스트는 네트워크가 반환되는데 10초이상 걸리지 않아야 하기 때문에 실패합니다.

Command-U를 사용하거나 메뉴의 Product > Test를 선택해서 테스트를 실행합니다. 기본 키 바인딩이 있는 경우에, 모두 적시에 성공해야 합니다.

접속을 끊고 다시 테스트 합니다. 기기에서 실행하는 경우, 비행기 모드로 설정합니다. 시뮬레이터에서 실행하는 경우에는 간단하게 연결을 해제(turn off) 합니다. 테스트는 10초 후에 결과가 실패로 완료됩니다. 훌륭해요, 효과가 있습니다!

이것은 사소한 테스트이지만, 서버 팀과 작업하는 경우에, 이런 기본 테스트는 네트워크 문제에 대해 누구의 책임이 있는지 손가락질(finger-pointing) 하는 것을 막을 수 있습니다.

주의
코드에서 비동기 테스트 구현할때, 저수준 APi를 사용하기 전에 먼저 XCTWaiter를 보세요. XCTWaiter API는 비동기 테스트에 대해 훨씬 강력한 기능을 많이 제공합니다.

Dispatch Sources

Dispatch Sources는 GCD의 특히 흥미로운 기능입니다. 어떤 타입의 이벤트를 보기(monitor)위해서 dispatch source를 사용할 수 있습니다. 이벤트는 Unix signals, file descriptors, Mach ports, VFS node, 다른 애매한 것들(other obscure stuff)을 포함할 수 있습니다.

dispatch source를 설정할때, 모니터할 이벤트 타입과 해당 이벤트 핸들러 블럭을 실행하는 dispatch queue를 원하는지 알려줍니다. 그리고나서 dispatch source에 이벤트 처리기(event handler)를 할당합니다.

생성시, dispatch sources는 일시정지된(suspended) 상태에서 시작합니다. 이벤트를 설정하는 것처럼 필요한 추가 구성을 수행하는 것을 허용합니다. dispatch source를 구성하고나서, 이벤트 처리를 시작하려면 다시 시작해야 합니다.

이 튜토리얼에서, 약간 특이한 방법을 사용해서 dispatch sources로 동작하는 것을 맛보기할 것입니다: 디버그 모드에서 앱을 실행할때 모니터하기 위함.

PhotoCollectionViewController.swift를 열고 backgroundImageOpacity 전역 프로퍼티를 바로 아래에 다음에 오는 것들을 추가합니다.

// 1
#if DEBUG

  // 2
  var signal: DispatchSourceSignal?

  // 3
  private let setupSignalHandlerFor = { (_ object: AnyObject) in
    let queue = DispatchQueue.main

    // 4
    signal =
      DispatchSource.makeSignalSource(signal: SIGSTOP, queue: queue)
        
    // 5
    signal?.setEventHandler {
      print("Hi, I am: \(object.description!)")
    }

    // 6
    signal?.resume()
  }
#endif

이 코드는 약간 섞여 있으니 단계별로 살펴봅니다.

  1. DEBUG 모드에서만 이 코드를 컴파일합니다. DEBUG는 Project Settings -> Build Settings -> Swift Compiler - Custom Flags -> Other Swift Flags -> Debug에서 -D DEBUG를 추가해서 정의됩니다. 시작 프로젝트에 이미 설정되어 있습니다.
  2. Unix signals을 모니터하는데 사용하는 DispatchSourceSignal 타입의 signal 변수를 선언합니다.
  3. dispatch source를 한 번 설정하는데 사용하는 setupSignalHandlerFor 전역 변수에 할당된 블럭을 만듭니다.
  4. 여기에 signal을 설정합니다. SIGSTOP Unix signal을 모니터하는데 관심있는 것을 나타내고 메인 큐에서 받은 이벤트를 처리합니다 - 이유를 곧 알게 될것입니다.
  5. dispatch source가 성공적으로 생성되는 경우, SIGSTOP 신호를 받을때마다 호출하는 이벤트 처리기 클로져를 등록합니다. 처리기는 클래스 설명을 포함하는 메시지를 출력합니다.
  6. 기본적으로 모든 소스는 일지정지된 상태에서 시작됩니다. 여기에서 dispatch source를 다시시작하므로, 이벤트 모니터링을 시작할수 있습니다.

super.viewDidLoad()를 호출하는 바로 아래에 viewDidLoad() 코드를 추가합니다.

#if DEBUG
  setupSignalHandlerFor(self)
#endif

이 코드는 dispatch source의 초기화 코드를 호출합니다.

빌드하고 앱을 실행합니다. Xcode의 디버거에 있는 pause를 탭하고 play를 탭해서, 프로그램 실행을 일시정지(Pause)하고 앱을 바로 재개(resume)합니다.

콘솔을 확인합니다. 다음과 같이 보여야 합니다.

Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10>

앱이 디버깅을 인식합니다! 그것은 꽤 엄청나지만, 실제로 어떻게 사용할까요?

객체를 디버깅하기 위해 사용할 수 있고 앱을 재개할때마다 데이터를 보여줄수 있습니다. 또한 악의적으로 앱에 디버거를 연결해서 공격할때, 앱 자체를 보호(또는 사용자 데이터)하기 위해 사용자정의된 보안 로직을 적용할 수도 있습니다.

흥미로운 아이디어는 디버거에서 객체를 조작하기 위해 스택 추적도구로 사용하는 것입니다.

그 상황을 잠깐 생각해 보세요. 파란색으로 디버거를 멈출때, 원하는 스택 프레임에 도달하지 못했었습니다. 이제 언제든지 디버거를 중지할 수 있고 원하는 위치에서 코드를 실행할 수 있습니다. 이는 디버거가 사용하기 짜증나는(tedious) 앱의 한 지점에서 코드를 실행하고자할때 매우 유용합니다. 시도해 보세요!

방금 추가된 setupSignalHanderFor 블럭 안쪽에 print() 구문에 중단점(breakpoint)를 넣습니다.

디버거에서 일시정지하고 다시 시작합니다. 앱은 추가된 중단점(breakpoint)에서 도달합니다. 이제 PhotoCollectionViewController 메소드의 깊숙한 곳에 있습니다. PhotoCollectionViewController의 인스턴스를 마음껏 사용할 수 있습니다. 꽤 편리합니다!

주의
디버거에 어떤 스레드가 있는지 아직 알지못하는 경우, 지금 찾아봅니다. 메인 스레드는 항상 첫번째 스레드일 것이며, 그 다음에는 두번째 스레드로 libdispatch, GCD 코디네이터가 옵니다. 그 후에, 앱이 중단점에 도착할때, 스레드 갯수와 남아있는 스레드는 하드웨어가 무엇을하느냐에 달려있습니다.

디버거 콘솔에서, 다음을 입력합니다.

expr object.navigationItem.prompt = "WOOT!"

Xcode 디버거는 가끔씩 비협조적일수 있습니다. 메시지를 받는 경우:

error: use of unresolved identifier 'self'

LLDB로 버그를 해결하는 것은 어려운 방법입니다. 먼저 디버기 영역에 있는 object의 주소를 기록합니다.

po object

그리고나서 디버거에서 다음 명령을 실행해서 원하는 타입으로 값을 직접 형변환해서, 출력된 주소로 0xHEXADDRESS를 교체합니다.

expr let $vc = unsafeBitCast(0xHEXADDRESS, to: GooglyPuff.PhotoCollectionViewController.self)
expr $vc.navigationItem.prompt = "WOOT!"

작동하지 않으면, 운이 좋습니다 - LLDB에서 다른 버그가 발생됩니다. 이 경우에, 앱을 다시 빌드하고 실행해야 할 수도 있습니다.

이 명령을 성공적으로 실행하면, 앱의 실행을 재개합니다. 다음과 같이 보일것입니다.

이 메소드로, UI를 업데이트 할 수 있으며, 클래스의 프로퍼티에 대해 조회하고 심지어는 메소드를 실행합니다 - 특별한 워크플로우 상태로 들어가기 위해 앱을 다시시작할 필요가 없습니다.

여기에서 어디로 가야하나요(Where to Go From Here?)

Downlaod를 이용해서 완성된 프로젝트를 다운로드 할 수 있습니다.

GCD외에도(beyond), Operation and OperationQueue Tutorial in Swift 보기를 추천하며, 동시성(concurrency) 기술은 GCD를 기반으로 합니다. 일반적으로, 간단한 시작하고 잊어버리는(fire-and-forget) 작업을 사용하는 경우에, GCD를 사용하는것이 가장 좋습니다. Operation은 더 나은 제어와 최대로 동시에 운영하는 것을 처리하는 구현을 제공하고, 속도를 희생해서 좀 더 객체지향 패러다임이 되게 만듭니다.

또한, 이 튜토리얼을 포함한 몇가지 더 많은 주제를 다루는 iOS Concurrency with GCD and Operations 비디오 튜토리얼을 보세요.

구체적인 이유가 없는한 항상 상위 수준의 API를 사용하는 것을 기억하세요.

Posted by 까칠코더