본문 바로가기

다우 & iOS/[Swift] Design Pattern

스레드 안전한 객체 구현

스레드 안전성을 구현하는 방법은 크게 두가지가 있다.

  1. 임계영역을 제거한다.
  2. 임계영역을 참조하는 연산을 동기화한다.

이 글에서는 객체지향 프로그래밍 시에 스레드 안전성을 구현하는 방법에 대해 서술하고자 한다. 첫 번째 방법은 함수형 프로그래밍에 적합하기에 두 번째 방식을 위주로 서술한다.

객체지향 프로그래밍에서 임계영역이 발생할 가능성이 존재하는 위치는 단 두 곳밖에 존재하지 않는다. 바로 클래스 변수와 인스턴스 변수이다. 따라서 스레드 안전성을 확보하기 위해서는 이들에 직접접근을 허용하지 않고 여기에 접근하는 모든 함수, 예를 들면 게터, 세터 등에 동기화 처리를 해주어야 한다.

class Cat {
  private let semaphore: DispatchSemaphore = .init(value: 1)

  private var _name: String = ""
  var name: String? {
    get {
      var result: String = ""
      self.semaphore.wait()
      result = self._name
      self.semaphore.signal()
      return result
    }
    set {
      self.semaphore.wait()
      self._name = newValue
      self.semaphore.signal()
    }
  }
}

위의 코드는 세마포어를 사용해 임계영역인 _name에 락을 걸었다. 여기까지만 해주더라도 이미 스레드 안전성을 구현할 수 있다. 그러나 코드가 쓸데없이 장황해지는 문제가 발생한다. 변수를 이름 하나만 가진 고양이 클래스를 만드는 데에도 동기화를 위해 많은 코드가 추가되었다. 만약 변수가 더 늘어난다면 동기화를 위해 많은 코드가 추가될 것임을 어렵지 않게 예상할 수 있다.

고양이 클래스에 인스턴스 변수로 나이와 자녀를 추가해보자.

class Mutex {
  private let semaphore: DispatchSemaphore = .init(value: 1)

  func run(_ block: () -> Void) {
    self.semaphore.wait()
    block()
    self.semaphore.signal()
  }
}

class Cat {
  private let mutex: Mutex = .init()

  private var _name: String = ""
  var name: String {
    get {
      var result: String = ""
      self.mutex.run {
        result = self._name
      }
      return result
    }
    set {
      self.mutex.run {
        self._name = newValue
      }
    }
  }

  private var _age: Int = 0
  var age: Int {
    get {
      var result: Int = 0
      self.mutex.run {
        result = self._age
      }
      return result
    }
    set {
      self.mutex.run {
        self._age = newValue
      }
    }
  }

  private var _children: [Cat] = []
  var children: [Cat] {
    get {
      var result: [Cat] = []
      self.mutex.run {
        result = self._children
      }
      return result
    }
    set {
      self.mutex.run {
        self._children = newValue
      }
    }
  }
}

나름 별도의 Mutex 클래스를 구현해 동기화 코드를 단순화시켰음에도 불구하고 코드가 길어진다. 그렇다면 이 변수들을 구조체로 묶어서 하나의 변수로 만든다면 동기화 코드를 상당히 줄일 수 있을 것 같다. 한번 시도해보자.

class Mutex {
  private let semaphore: DispatchSemaphore = .init(value: 1)

  func run(_ block: () -> Void) {
    self.semaphore.wait()
    block()
    self.semaphore.signal()
  }
}

class Cat {
  struct State {
    let name: String = ""
    let age: Int = 0
    let children: [Cat] = []
  }

  private let mutex: Mutex = .init()

  private var _state: State = .init()
  var state: State {
    get {
      var result: State = .init()
      self.mutex.run {
        result = self._state
      }
      return result
    }
    set {
      self.mutex.run {
        self._state = newValue
      }
    }
  }
}

아까 보다 훨씬 코드가 짧아졌다. 마지막으로 외부에서 내부의 구현을 알지 못하게 하고 구조체로 감싸기 전과 동일하게 사용할 수 있게 해보자.

class Mutex {
  private let semaphore: DispatchSemaphore = .init(value: 1)

  func run(_ block: () -> Void) {
    self.semaphore.wait()
    block()
    self.semaphore.signal()
  }
}

class Cat {
  struct State {
    let name: String = ""
    let age: Int = 0
    let children: [Cat] = []

    func clone(name: String? = nil, age: Int? = nil, children: [Cat]? = nil) {
      return .init(name: name ?? self.name, age: age ?? self.age, children: children ?? self.children)
    }
  }

  private let mutex: Mutex = .init()

  private var _state: State = .init()
  private var state: State {
    get {
      var result: State = .init()
      self.mutex.run {
        result = self._state
      }
      return result
    }
    set {
      self.mutex.run {
        self._state = newValue
      }
    }
  }

  var name: String {
    get {
      return self.state.name
    }
    set {
      self.state = self.state.clone(name: newValue)
    }
  }

  var age: Int {
    get {
      return self.state.age
    }
    set {
      self.state = self.state.clone(age: newValue)
    }
  }

  var children: [Cat] {
    get {
      return self.state.children
    }
    set {
      self.state = self.state.clone(children: newValue)
    }
  }
}

코드의 길이는 다시 늘어났지만 동기화는 state 한 곳에서만 발생한다. 따라서 멤버 변수의 값을 이용하는 어떤 메서드던지 state를 이용해 연산을 진행한다면 추가적으로 동기화에 신경쓰지 않고도 스레드 안전성을 구현할 수 있다.

'다우 & iOS > [Swift] Design Pattern' 카테고리의 다른 글

스위프트에서 빌더 패턴 구현  (0) 2019.11.19