Swift

Swift ) Two-Phase Initialization?

JoonSwift 2022. 5. 27. 15:05

이번 포스팅에서는 Swift의 Two-Phase Initialization에 대해서 알아보겠습니다. 

class SomeSubClass: SomeSuperClass {
  let someProperty: String
  
  init(someProperty: String) {
    super.init()
  }
}

제가 Two-Phase Initialization을 가장 많이 접할 수 있는 코드는 바로 위와 같이 코드를 작성할 때였습니다. 위의 코드를 작성하면 바로 문구가 하나 뜨는데 Property 'self.someProperty' not initialized at super.init call 이런 문구입니다. 

과연 왜 self.someProperty가 super.init 호출 시점에 초기화되어있지 않다고 코드를 빌드조차 못하게 하는것일까?! 한번 알아보겠습니다. 

 

Two-Phase Initialization

Swift의 Class initialization에는 two-phase 과정이 있습니다. 

  • Phase 1 : 어떤 class hierarchy의 아래에서 위까지 모든 저장 프로퍼티들을 초기화 하는 과정이 바로 Phase one입니다. 
  • Phase 2 : 이제 각 클래스는 저장 프로퍼티들을 수정할 수 있는 상태가 되고, 메서드 호출 등 self를 통해 접근하는 프로퍼티, 메서드들을 사용할 수 있게 됩니다. 

이런 Two-Phase initialization 과정은 초기화되지 않은 프로퍼티들에 대한 접근을 막고, 다른 initializer로 부터 예상치 못하게 값이 변경되는 것을 방지하는 안전장치라고 할 수 있습니다. 

또한 Class hierarchy에서 각각의 클래스를 유연하게 유지하면서, initialization 과정을 안전하게 만들어주기도 합니다. 

 

그래서 Swift 컴파일러는 Two-Phase Initialization을 위해 4개의 Safety check를 사용하고 있습니다. 

 

Safety check 1

지정된 이니셜라이저는 해당 클래스에 존재하는 모든 프로퍼티들이 superclass의 이니셜라이저를 위임하기 전에 모두 초기화 해줘야 합니다. 이것이 바로 제일 처음에 보았던 오류입니다. 

super.init을 호출하기 전에 현재 클래스에 존재하는 language, device에 대한 초기화를 먼저 진행해주어야 합니다. 

init(name: String, age: Int, language: String, device: String){
  self.language = language
  self.device = device
  super.init(name: name, age: age)
}

Safety check 2

상속받은 프로퍼티에 대한 접근을 하기 이전에 superclass의 이니셜라이저를 delegate up! 해야합니다. 

class Person {
  let name: String
  let age: Int
  
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

class Developer: Person {
  let language: String
  let device: String
  
  init(name: String, age: Int, language: String, device: String){
    self.language = language
    self.device = device
    self.name // Error! 'self' used in property access 'name' before 'super.init' call
    super.init(name: name, age: age)
  }

위에서 self.name 즉, 상속받은 Person 클래스의 프로퍼티인 name에 superclass의 초기화 이전에 접근하게 된다면, 컴파일러가 이를 발견하고 에러를 보여주게 됩니다. 

Safety check 3

Convenience initializer은 반드시 다른 이니셜라이저에게 delegate한 이후에! 프로퍼티에 접근해야 합니다. 즉, 

  convenience init() {
    self.language // Error! 'self' used before 'self.init' call or assignment to 'self'
    self.init(name: "Joonswift", age: 27, language: "Swift", device: "MacBook Pro 13 M1")
  }

 이렇게 self.init 즉, 다른 이니셜라이저에게 delegate 하기 전에 language 프로퍼티에 대한 접근을 시도할 시 Swift는 그럴수 없다고 말해줍니다. 

Safety check 4

First phase 즉 Phase one이 끝나기 전 까지는 어떤 메서드를 호출하지도, 인스턴스 프로퍼티들에 대한 값을 읽어오지도 못합니다. 

init(name: String, age: Int, language: String, device: String){
    self.language = language
    self.device = device
    someMethod() // Error! 'self' used in method call 'someMethod' before 'super.init' call
    super.init(name: name, age: age)
    //Phase one end
    
    //Phase two start
    print(self.language)
    someMethod() // No error!
  }

위에서 Phase One에 someMethod()를 호출하면 에러가 납니다. 반면에 Phase two에서 someMethod()를 호출하거나 language프로퍼티의 값에 접근하면 정상적으로 실행되는 것을 확인할 수 있습니다. 

 

위의 4가지 Safety check을 바탕으로 한 Two-Phase Initialization의 실행 과정을 보겠습니다. 

Phase 1

  • 클래스의 designated or convenience 이니셜라이저를 호출합니다. 
  • 해당 클래스에 대한 새로운 인스턴스가 생성되면서, 메모리에 할당하는데, 이 메모리는 초기화되지 않은 상태입니다. 
  • designated 이니셜라이저가 해당 클래스에 존재하는 모든 저장 프로퍼티가 초기화 되었고, 값이 있다는 것을 확인한 이후에 이 저장 프로퍼티들을 위한 메모리가 초기화됩니다. 
  • 이 designated 이니셜라이저는 superclass에게 똑같은 작업을 하도록 넘겨주고 superclass 역시 똑같은 과정을 통해서 저장 프로퍼티들을 초기화 하여 메모리에 초기화 시킵니다. 
  • 이것을 계속 반복해서 결국 class hierarchy의 상속 chain의 제일 최상위 class까지 도달하면 마지막 클래스가 모든 저장 프로퍼티들이 값이 있는지, 인스턴스의 메모리가 모두 초기화 되었는지를 확인하고 Phase 1을 끝냅니다. 

Phase 2

  • 이제 다시 위에서 아래로 내려가면서 각각의 designated 이니셜라이저는 인스턴스를 수정할 수 있는 옵션이 생깁니다. 이제 self에 접근하여 프로퍼티를 수정하고, 메서드를 호출하는 등의 권한을 얻게됩니다. 
  • 마지막으로 Convenience 이니셜라이저 역시 인스턴스를 수정할 수 있고, self에 대한 접근이 가능하게 됩니다. 

 

 

정리

swift.org에 정말 그림이랑 잘 그려서 설명을 해 둔 부분인데, 미루고 미루다가 계속 코드를 작성하며 super.init 전에 프로퍼티들을 초기화 하지 않아 생기는 에러들을 그저 아무생각없이 초기화 시켜주고 이것이 Two-Phase initialization이라는 개념 때문에 그런지도 모르고 넘어왔었습니다. 

하지만 이번 기회에 " 아 이게 Two-Phase Initialization 때문에 나오는 에러였구나~" 라는 사실을 알고 속이 뻥 뚫리는 느낌입니다. 

결론 : 기초를 튼튼하게 다지자....

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#:~:text=Two%2Dphase%20initialization%20prevents%20property,value%20by%20another%20initializer%20unexpectedly.&text=Swift's%20two%2Dphase%20initialization%20process,to%20initialization%20in%20Objective%2DC.

 

Initialization — The Swift Programming Language (Swift 5.6)

Initialization Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization t

docs.swift.org