본문 바로가기

Swift

[Swift][번역] 클래스 상속과 초기화 Part.1

 

Swift로 상속관계에 있는 클래스를 다룰 때 항상 init에서 말썽이 일어난다. 제대로 모르는 내 탓이오..

Swift 공식문서Class Inheritance and Initialization 파트가 있는데 이를 번역하면서 제대로 알아가보자.

 

*본 글은 Part.1, 2로 나뉘어 포스팅됩니다.

Part.1

  • Designated Initializers and Convenience Initializers
  • Initializer Delegation for Class Types
  • Two-Phase Initialization

Part.2

  • Initializer Inheritance and Overriding
  • Automatic Initializer Inheritance
  • Designated and Convenience Initializers in Action

Class Inheritance and Initialization

부모 클래스로부터 상속받은 프로퍼티를 포함한 클래스의 모든 저장 프로퍼티(stored property)들은 initialization 과정에서 모두 초기화가 되어야한다.

 

Swift는 저장 프로퍼티가 초기값을 대입받을 수 있도록 보장하기 위해 두 종류의 initializer를 제공하는데, designated initializer, convenience initializer이다.

Designated Initializers and Convenience Initializers

designated initializer는 클래스의 기본(우선되는) initializer이다. designated initializer는 해당 클래스가 가진 모든 프로퍼티들을 완전히(fully) 초기화시키고 필요하다면 부모 클래스의 initializer를 호출하여 inheritance chain(상속 체인)을 타고 올라가며 초기화 과정을 이어갈 수 있도록한다.

 

클래스는 보통 매우 적은 수의 designated initializer를 갖고 있는 경향이 있으며 하나를 가지는 것이 보편적이다. designated initializer는 초기화가 이뤄지고 부모 클래스로의 초기화 과정을 시작하는 일종의 funnel point(깔대기의 입구)라고 표현할 수 있다.

 

모든 클래스는 적어도 하나의 designated initializer를 가져야한다. 어떤 클래스에서는 이 요구사항이 하나 이상의 부모 클래스의 designated initializer를 상속함으로써 충족되기도 한다. (이 내용은 Part.2의 Automatic Initializer Inheritance에서 다루겠다)

Convenience initializer는 지원의 성격을 띄는(secondary, supporting) initializer이다. 한 클래스 내에 있는 designated initializer의 파라미터 중 일부를 기본 값으로 세팅하기 위해 convenience initializer를 정의할 수 있다. 또한 클래스의 인스턴스를 특정한 use case나 input value type을 위해 생성할 때 convenience initializer를 정의할 수 있다.

 

Convenience initializer는 반드시 정의해야하는 것은 아니다. 일반적인(common) initializer으로의 shortcut 패턴을 통해 시간이 절약되거나 클래스 초기화가 더 명료해진다면 convenience initializer를 정의하도록 권장된다.

 

기본적인 문법은 아래와 같다.

init(parameters) {
    statements
}

 

convenience init(parameters) {
    statements
}

Initializer Delegation for Class Types

이 두 initializer 사이의 관계를 좀 간단히 정리하기 위해서 Swift는 initializer 사이에 호출 위임(delegation calls between initializers)에 세 가지 규칙을 적용한다.

 

Rule 1

designated initializer는 반드시 바로 위에 있는 부모 클래스의 designated initializer를 호출해야한다.

 

Rule 2

convenience initializer는 반드시 같은 클래스에 있는 다른 initializer를 호출해야한다.

 

Rule 3

convenience initializer는 반드시 (마지막에는) designated initializer를 호출해야 한다.

 

이 세 가지 룰을 축약하면:

  • designated initializer는 항상 위로 위임해야한다.
  • convenience initializer는 항상 옆으로 위임해야한다.

이 규칙들을 그림으로 나타내면 아래와 같다.

먼저 superclass는 하나의 designated initializer와 두 개의 convenience initializer를 갖고 있다. 한 convenience initializer는 다른 convenience initializer를 호출하고 있고, 그 흐름의 마지막에는 결국 designated initializer를 호출한다. (Rule 2, Rule 3) superclass는 상위에 부모 클래스가 없으므로 Rule 1은 적용되지 않는다.

 

subclass는 두 개의 designated initializer와 하나의 convenience initializer를 갖고 있다. convenience initializer는 같은 클래스 내의 initializer를 호출할 수 밖에 없으므로 반드시 두 designated initializer 중 하나를 호출해야한다. 이는 Rule 2, Rule 3을 만족한다. 두 designated initializer 모두 superclass의 designated initializer를 호출함으로써 Rule 1까지 만족한다.

 

아래는 조금 더 복잡한 경우 (네 개의 클래스)를 표현했다. 이 그림을 보면 위에서 designated initializer가 클래스 초기화의 funnel points라고 표현한 이유를 조금 더 실감할 수 있을 것이다.

 

Two-Phase Initialization

Swift에서의 클래스 상속은 2단계(two-phase) 과정이다. 1단계에서는 각 저장 프로퍼티들이 자신이 속한 클래스에 의해 초기값을 대입받는다. 이 초기화 단계가 끝나면 모든 저장 프로퍼티들의 값이 정해진(determined) 상태일 것이고 2단계가 시작된다. 그러면 각 클래스는 새 인스턴스가 사용되기 전에 저장 프로퍼티를 customize할 수 있는 기회를 갖게된다.

 

2단계 초기화 과정은 클래스 위계(hierarchy)에 속한 각 클래스에게 유연함을 부여하면서도 초기화 과정을 안전하게 한다. 2단계 초기화는 프로퍼티 값들이 초기화 되기 전에 접근되는 것을 막아주고 다른 initializer에 의해 예상치 못하게 값이 결정되는 것도 예방한다.

Swift 컴파일러는 4가지의 safety-check을 진행하여 2단계 초기화 과정이 에러 없이 끝날 수 있도록 한다.

 

Safety check 1

designated initializer는 부모 클래스의 designated initializer를 호출하기 전에 속한 클래스의 모든 프로퍼티가 반드시 초기화 되도록 보장해야한다.

한 객체의 메모리는 모든 저장 프로퍼티의 초기상태(initial state)가 모두 초기화 된 이후에 고려된다. 이 규칙을 만족하기 위해서 designated initializer는 상속 체인으로 순서를 넘기기 전에 자기 클래스의 프로퍼티들이 모두 초기화 되도록해야한다.

 

Safety check 2

designated initializer는 상속받은 프로퍼티에 값을 대입하기 전에 반드시 부모 클래스의 initializer를 호출해야한다. 그렇지 않다면 해당 프로퍼티는 부모 클래스의 initializer에서 초기화된 값으로 덮어씌워질 수 있다.

 

Safety check 3

convenience initializer는 어떤 프로퍼티에든 값을 대입하기 전에 반드시 다른 initializer에게 위임해야한다. 그렇지 않다면 해당 프로퍼티들은 같은 클래스의 designated initializer에서 초기화된 값으로 덮어씌워질 수 있다.

 

Safety check 4

initializer는 1단계 초기화가 끝날 때까지 instance method를 호출할 수 없고 instance property를 읽어올 수 없으며 self에 접근할 수 없다.

 

이 네 가지 safety check를 기반으로 2단계 초기화가 어떻게 이루어지는지 정리하면 아래와 같다.

 

Phase 1

  • 클래스에서 designated 혹은 convenience initializer 가 호출된다.
  • 해당 클래스의 새 인스턴스를 위한 메모리가 할당된다(allocated) 메모리는 아직 초기화(initialized) 되지는 않은 상태
  • 클래스의 designated initializer가 모든 저장 프로퍼티에 값이 있음을 확인(confirm)한다. 이 저장 프로퍼티들을 위한 메모리가 이제 초기화된다.
  • 부모 클래스의 저장 프로퍼티에도 동일한 과정이 수행되도록 designated initializer가 부모 클래스의 initializer에 차례를 넘긴다.
  • 이 과정은 클래스 상속 체인을 타고 끝까지 올라간다.
  • 가장 꼭대기에 있는 클래스까지 닿고, 상속 체인의 마지막 클래스의 모든 저장 프로퍼티에도 값이 들어가면 그제서야 인스턴스의 메모리가 완전히 초기화되고 1단계가 끝난다.

Phase 2

  • 동일한 상속 체인을 따라 내려온다. 각 designated initializer는 인스턴스를 customize하는 옵션을 갖고있다. initializer들은 이제 self에 접근하여 프로퍼티의 값을 바꾸고(modifty) 인스턴스 메소드를 호출할 수 있다.
  • 마침내 상속 체인의 어떤 convenience initializers든 인스턴스를 customize 할 수 있고 self에 접근할 수 있게된다.

Phase 1을 다이어그램으로 그려보면 아래와 같다.

위 예제에서 subclass의 convenience initializer가 호출되면서 초기화 과정이 시작된다. convenience initializer는 아직 어떤 프로퍼티도 변경할 수 없으며 같은 클래스 내의 designated initializer에게 위임한다.

 

designated initializer는 subclass의 모든 프로퍼티가 값을 갖고 있도록 한다. (safety check 1) 그리고 superclass의 designated initializer를 호출하여 초기화 과정이 상속 체인 위로 진행될 수 있도록 한다.

 

superclass의 designated initializer는 superclass의 모든 프로퍼티들이 값을 갖도록 한다. 더 이상 초기화 할 superclass가 없으므로 더 이상 위임은 필요하지 않다.

 

superclass의 모든 프로퍼티들이 초기값을 갖게되면 메모리는 완전히 초기화되고 1단계가 끝난다.

 

아래는 이어서 진행되는 Phase 2의 그림이다.

 

superclass의 designated initializer는 인스턴스를 customize 할 수 있는 기회를 받는다 (필수는 아니다)

superclass의 designated initializer가 이 과정을 마치면 subclass의 designated initializer가 추가적인 customization을 수행할 수 있다 (역시 필수는 아니다)

 

마침내 subclass의 designated initializer가 끝나면 convenience initializer가 추가적인 customization을 수행할 수 있다.

'Swift' 카테고리의 다른 글

[Swift][번역] 클래스 상속과 초기화 Part.2  (0) 2022.03.17