이번 포스트에서는 프로토콜을 타입으로써 사용하는 것과, 어떻게 프로토콜을 타입으로써 사용할 수 있는지 그 원리를 알아보도록 하겠습니다.

  • 타입으로써의 프로토콜
    프로토콜을 타입으로 사용할 때는 ‘Existential Type(실존 타입)’이라고도 불립니다. 이는 ‘이 프로토콜을 채택한 어떤 타입 T가 존재한다(there exists a type T such that T conforms to the protocol)’ 라는 구문에서 유래되었습니다. 이 실존 타입은 프로토콜이 실제 구현을 제공하지 않기 때문에 인스턴스화는 불가능하지만, 여러 프로토콜을 채택한 타입들을 가리킬 수 있어서 다른 타입들과 거의 동일하게 사용할 수 있습니다. 이러한 기능이 주로 사용되는 곳은 Delegate 패턴입니다. 다음 코드를 보겠습니다.

          protocol DiceGame { // 주사위를 이용한 게임을 나타내는 프로토콜
              var dice: Dice { get }
              func play()
          }
          protocol DiceGameDelegate: AnyObject { // 주사위 게임의 delegate 객체를 위한 프로토콜
              func gameDidStart(_ game: DiceGame)
              func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
              func gameDidEnd(_ game: DiceGame)
          }
    

    그리고 이를 이용해서 다음과 같은 뱀사다리 게임을 만들었다 생각해보겠습니다.

          class SnakesAndLadders: DiceGame {
              let finalSquare = 25
              let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
              var square = 0
              var board: [Int]
              init() {
                  // 보드 초기화
              }
              weak var delegate: DiceGameDelegate? // 프로토콜 타입 사용
    
              func play() { // 게임 루프 시작 
                  square = 0
                  delegate?.gameDidStart(self)
                  gameLoop: while square != finalSquare {
                      let diceRoll = dice.roll()
                      delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
                      switch square + diceRoll {
                      case finalSquare:
                          break gameLoop
                      case let newSquare where newSquare > finalSquare:
                          continue gameLoop
                      default:
                          square += diceRoll
                          square += board[square]
                      }
                  }
                  delegate?.gameDidEnd(self)
              }
          }
    

    delegate가 프로토콜 타입으로 되어있어서, 실제 타입이 무엇인가에 관계없이 프로토콜이 노출한 인터페이스만으로 작업을 수행할 수 있게 됩니다. 즉, 다형성을 제공해줍니다.

  • Existential Type의 원리
    프로토콜은 값타입, 참조타입을 가리지 않고 다형성을 제공해줍니다. 그러면 어떻게 이러한 것이 가능할까요? 답은 프로토콜 타입이 사용하는 특별한 자료구조에 있습니다. 이 자료구조의 이름은 Existential Container입니다.

    Existential Conatiner

    Value Buffer는 3 Word 크기를 기진, 프로토콜 타입의 실제 값이 들어가기 위한 자리입니다.

    Word는 프로세서가 데이터를 처리하는 기본 단위 입니다. 보통 이를 지원하는 OS의 종류 (32비트, 64비트)로 친숙한데, 현재 애플 플랫폼은 64비트를 기본으로 사용합니다.

    값의 크기가 작다면 그대로 들어가지만, 값이 커서 버퍼에 다 안들어간다면 값타입, 참조 타입 여부와 무관하게 힙 할당이 일어나게 됩니다. 이는 특히 복사할 때 문제가 되는데, 값 타입은 매번 복사가 일어나기 때문에 힙 할당이 계속해서 일어나게 됩니다. 따라서 값 타입의 크기가 너무 크다면 오히려 값타입을 참조 타입으로 바꿔주면 전체 복사보다는 덜 비싼 RC(Reference Counting)를 하는 것만으로 복사를 수행할 수 있으므로 고려해볼만 합니다.

    Value Witness Table은 Existential Container의 생성과 복사, 삭제 등을 담당하는 메소드들의 위치를 가리키는 테이블입니다. 그리고 그 아래에 프로토콜이 요구하는 함수들을 제대로 호출하기 위한 Protocol Witness Table이 존재합니다.

    프로토콜 타입을 사용하면, 메소드와 프로퍼티는 Protocol Witness Table을 통해서 참조가 일어납니다. 따라서 어떤 타입의 값을 가지던, 올바른 메소드를 호출할 수 있게 됩니다.


참고자료
Understanding Swift Perfomance