동시성 프로그래밍은 무거운 작업을 효율적으로 처리할 수 있는 아주 강력한 기술입니다.

하지만, 비동기 코드를 작성할 때 가장 어려운 점은,

  1. 디버깅
  2. Data Race(데이터 경합)

위 두 가지 때문에 비동기 코드는 개발자가 의도하지 못한 버그를 만들어 냅니다.

이러한 문제가 발생하는 근본적인 이유는,

첫 번째는, 다중 쓰레드가 하나의 변경 가능한 데이터를 공유하기 때문입니다. 즉, 데이터가 shared mutable state이기 때문

두 번째는, Data Race가 발생해도 컴파일러는 에러라고 인지하지 못하는 것입니다.

import Foundation

class Counter {
  var count = 0
  
  func increment() {
    count += 1
    print(count)
  }
}

let counter = Counter()

Task {
  counter.increment()
}

Task {
  counter.increment()
}

RunLoop.main.run()

위와 같이 Data Race가 발생하도록 코드를 작성했습니다. 위 코드는 문제가 있음에도 정상적으로 실행이 됩니다. 문법적으로 문제가 없어 컴파일러가 에러를 발생시키지 않기 때문입니다.

위 코드는 increment()가 호출될 때 마다 count를 1씩 증가 시키고, 곧 바로 현재 값을 출력하는 코드입니다.

개발자는 콘솔에 1 2가 출력될 것은 기대하고 코드를 실행시켰지만,

image.png

결과는 예상하지 못한 2 2가 출력되었습니다. 이는 Data Race가 발생했다는 증거입니다.

개발자가 어렵게 디버깅을 통해 Data Race가 발생했다는 것을 인지하고, 이를 해결하기 위해 Locks, Serial dispatch queue 등을 사용해 문제 해결을 시도할 것입니다.

위와 같은 방법으로 문제를 해결할 수 있지만, 주의해서 사용하지 않으면 **교착상태(DeadLock)**과 같은 추가적인 버그를 발생시킬 수 있습니다. 이 또한 컴파일러는 알려주지 않죠..

위와 같은 문제를 한번에 해결할 수 있도록 Swift 5.5에서 Actor라는 개념이 등장하게 됩니다.

Actor