본문 바로가기
Swift 공식 가이드/Swift 3

Initialization (2/3)

by 토끼찌짐 2017. 3. 13.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Initialization 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.


이니셜라이저 2편입니다. 이번 시간에는 클래스의 이니셜라이저와 초기화에 대하여 중점적으로 다뤄보려고 합니다. 1편을 아직 안 읽으셨다면 먼저 1편을 읽어보세요.

Initialization 1편 링크 > http://wlaxhrl.tistory.com/47



들어가며

클래스의 모든 stored 프로퍼티(슈퍼 클래스의 Stored 프로퍼티도 포함)는 초기화 과정에서 반드시 초기값initial value이 할당되어야 한다. 이것을 보장하기 위해 Swift는 클래스 타입에 대한 이니셜라이저 두 종류를 제공한다. 지정 이니셜라이저designated initializaer와 편의 이니셜라이저convenience initializer다.



지정 이니셜라이저 & 편의 이니셜라이저

(1) 지정 이니셜라이저란?

  • 클래스의 주 이니셜라이저

  • 모든 프로퍼티가 이 안에서 전부 초기화된다

  • 슈퍼 클래스의 이니셜라이저를 호출하여 초기화 과정을 슈퍼클래스로 연쇄

  • 클래스는 매우 적은 수의 지정 이니셜라이저를 가지는 것이 좋고, 반드시 1개는 있어야 한다. 웬만하면 1개만 가지는 것을 추천

  • 어떤 경우에는, 슈퍼클래스의 지정 이니셜라이저를 자동으로 상속받음으로써 위 조건을 만족시킬 수 있다 (나중에 자세히 설명)

  • 초기화 과정의 "깔때기" 역할을 함. 슈퍼클래스로 이어지는 초기화 과정 연쇄에서도 지정 이니셜라이저는 깔때기가 되어줌


(2) 편의 이니셜라이저란?

  • 초기화 패턴을 단축하거나 클래스의 의도를 명확하게 만들기 위해 정의한다

  • 필요한 셋팅을 끝낸 뒤 지정 이니셜라이저를 호출하며 끝나야 한다

  • 필요할 때만 정의하면 됨



Initializer Delegation for Class Types

1편에서 value 타입의 이니셜라이저 딜리게이션에 대해 알아봤었다. 이번에는 class 타입의 이니셜라이저 딜리게이션에 대해 살펴보자.


<Swift가 정한 3가지 규칙 - 무조건 해야 함. 필수>

  1. 지정 이니셜라이저 안에서, 바로 위 슈퍼클래스의 지정 이니셜라이저를 호출

  2. 편의 이니셜라이저 안에서, 같은 클래스 내의 지정 이니셜라이저를 호출

  3. 편의 이니셜라이저 안에서, 마지막에는 지정 이니셜라이저를 호출


<규칙을 쉽게 기억하는 법>

지정 이니셜라이저 -> must always delegate up

편의 이니셜라이저 -> must always delegate across

화살표 방향을 보자. 지정 이니셜라이저는 위를 가리키고 (슈퍼 클래스의 지정 이니셜라이저 호출) 편의 이니셜라이저는 옆을 가리킨다 (같은 클래스의 지정 이니셜라이저 호출).


상속 관계가 4단계인 경우에도 이런 식으로 구현할 수 있다. 살펴보면 지정 이니셜라이저가 깔때기 역할을 한다는 것이 어떤 의미인지 알 수 있다.



Two-Phase Initialization

Swift에서는 두 단계에 걸쳐서 클래스가 초기화된다.

  • 1단계 : stored 프로퍼티 모두에게 각각 초기값이 할당된다.

  • 2단계 : 상속 트리 상의 각각의 클래스가 stored 프로퍼티를 커스터마이징할 기회를 얻는다. 

이렇게 두 단계를 거쳐 초기화하기 때문에 클래스의 초기화는 안전하게, 그리고 유연하게 이뤄질 수 있다.

<note> Objective-C에서도 비슷한 방식으로 클래스가 초기화된다. Objective-C에서는 1단계에서 초기값으로 0이나 nil이 할당되는 반면 Swift에서는 커스텀 초기값을 정할 수 있게 해준다. (따라서 0이나 nil이 초기값으로 적합하지 않은 경우에도 대응할 수 있다)



두 단계 초기화 과정을 위해 Swift  컴파일러는 4가지의 안전점검safety-check를 한다.

  1. 지정 이니셜라이저는, 슈퍼클래스의 이니셜라이저에게 초기화 과정을 위임delegate하기 전에, 일단 자기 클래스에서 도입된 모든 프로퍼티가 초기화 되었음을 보장해야 한다.
    (* why? 객체의 메모리는 해당 객체의 stored 프로퍼티가 전부 초기화되어야만 초기화 되었다고 간주한다. 따라서 초기화 연쇄를 위로 전달하기 앞서 이것을 보장해야 하는 것이다)

  2. 지정 이니셜라이저는, 상속받은 프로퍼티에 값을 할당하기 전에, 슈퍼 클래스의 이니셜라이저에게 초기화 과정을 위임해야 한다.
    (* why? 슈퍼 클래스의 이니셜라이저를 먼저 거쳐야만 자신이 새로 할당한 값으로 프로퍼티의 값을 덮어쓸 수 있으므로 당연한 얘기다)

  3. 편의 이니셜라이저는, 프로퍼티에 값을 할당하기 전에, 다른 이니셜라이저에게 초기화 과정을 위임해야 한다.
    (* why? 여기서의 다른 이니셜라이저는 지정 이니셜라이저 혹은 다른 편의 이니셜라이저가 될 수 있는데, 편의 이니셜라이저의 경우에도 결국에는 지정 이니셜라이저를 거치게 된다. 따라서 지정 이니셜라이저를 먼저 거쳐야만 나중에 자신이 새로 할당한 값으로 프로퍼티의 값을 덮어쓸 수 있으므로 당연한 얘기다)

  4. 초기화 1단계가 끝나기 전까지는, 이니셜라이저는 인스턴스 메서드를 호출할 수 없고, 인스턴스 프로퍼티의 값을 읽을 수도 없고, self를 참조할 수도 없다.
    (* why? 클래스 인스턴스는 초기화 1단계가 끝나기가 전까지는 valid하지 않기 때문이다. 참고로 초기화 1단계가 끝나기 전까지 그렇다는 것이지, 이니셜라이저 구현코드 안에서 인스턴스 메서드를 호출할 수 없다는 그런 뜻이 아니다!)



이제 두 단계의 초기화 과정에서 이 안전점검 4가지가 어떤 식으로 쓰이는지를 그림과 함께 살펴보자.

<Phase 1>

  • 클래스의 이니셜라이저가 호출되며 Phase1 시작 (위 그림 예제에서는 편의 이니셜라이저가 지정 이니셜라이저를 호출하고 있음)

  • 클래스 인스턴스를 위한 메모리가 할당됨. 메모리는 아직 초기화 되지 않음

  • 지정 이니셜라이저에서, 자기 클래스에서 도입된 모든 stored 프로퍼티가 값을 가지는 것을 확인받음 -> stored 프로퍼티를 위한 메모리들 초기화됨

  • 지정 이니셜라이저는 슈퍼클래스의 이니셜라이저에게 이제 너도 똑같은 짓을 하라고 순서를 건네줌

  • 클래스의 상속 계층 맨 위에 도달할 때까지 위 과정이 반복됨

  • 연쇄의 맨 위에 있는 클래스에 도달하면 이 클래스 역시 자신의 stored 프로퍼티들이 값을 가지는 것을 확인받는다. 그때 비로소 인스턴스의 메모리가 초기화 되었다고 간주된다

  • Phase 1 끝


<Phase2>

  • Phase1이 끝나면 Phase2가 시작됨

  • 이제 연쇄를 반대로 내려오게 되는데, 각각의 클래스에 정의된 지정 이니셜라이저에서 인스턴스를 커스터마이징할 수 있게 된다. 지정 이니셜라이저 안에서 인스턴스 메서드를 호출할 수도 있고 self에도 접근할 수 있고 등등

  • 위 연쇄가 끝나면 이번에는 편의 이니셜라이저가 인스턴스를 커스터마이징 할 기회를 얻게 된다. (* 편의 이니셜라이저로 인스턴스 생성을 했을 때 해당되는 얘기)

  • Phase 2 끝



이니셜라이저의 상속과 오버라이딩

Swift에서는 슈퍼클래스의 이니셜라이저를 자동으로 상속받지 않는다는 점이 Objective-C와 다르다. 슈퍼 클래스의 이니셜라이저를 사용했다가 자식 클래스의 인스턴스를 잘못 생성하는 일이 없도록 하기 위함이다. (* 안전하고 적합한 상황에서는 상속받기도 한다. 나중에 나온다)


만약 슈퍼클래스와 같은 이니셜라이저를 서브클래스에서도 가지게 하고 싶다면

  • 슈퍼클래스에서 그게 지정 이니셜라이저인 경우 : 
    override 수식어를 붙여서 서브클래스 안에서 오버라이드 & 커스텀 구현해야 한다. 그리고 그 안에서 슈퍼클래스의 지정 이니셜라이저를 불러준다. 이때의 슈퍼클래스의 지정 이니셜라이저가 자동으로 만들어진 디폴트 이니셜라이저인 경우에도 예외는 아니다.

  • 슈퍼클래스에서 그게 편의 이니셜라이저인 경우 :
    앞에서 알아봤던 딜리게이션 룰을 생각해보자. 이니셜라이저 안에서 슈퍼클래스의 편의 이니셜라이저는 부르지 못한다(위임하지 못한다). 위임이 이루어지는 방향을 생각해보면, 지정 이니셜라이저는 슈퍼클래스의 지정 이니셜라이저로, 편의 이니셜라이저는 같은 클래스 내의 지정 이니셜라이저로 위임이 되어야 하기 때문이다. 따라서 이 경우에는 그냥 위임의 룰에 맞게 정의하면 된다. 엄격히 말해서 오버라이드가 아니며 override 수식어를 쓸 필요도 없다.


class Vehicle {
    let hasWheel = true
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
    // 디폴트 이니셜라이저를 가짐
}

class Bicycle: Vehicle {
    override init() { // 디폴트 이니셜라이저를 오버라이드한 케이스
        super.init() // 슈퍼클래스의 지정 이니셜라이저 호출
        numberOfWheels = 2 // 상속받은 변수 프로퍼티 값 변경
        
        hasWheel = false
       // error! 상속받은 상수 프로퍼티는 초기화 중에도 바꾸지 못함
    }
}



자동 이니셜라이저 상속(Automatic Initializer Inheritance)

위에서 언급했듯이 Swift에서는 슈퍼클래스의 이니셜라이저를 서브클래스에서 자동으로 상속받지 않는다. 그러나 특정 조건이 만족된다면, 자동으로 상속받아도 안전하다고 판단하여 이니셜라이저를 자동으로 서브클래스에 상속시켜준다. 

<가정> 서브클래스에서 도입된 새로운 프로퍼티들은 디폴트 값들이 있다.

위와 같은 상황에서, 다음과 같은 두 가지 룰이 적용된다.

  1. 서브클래스에 어떤 지정 이니셜라이저도 없다면 -> 자동으로 슈퍼클래스의 지정 이니셜라이저들을 상속받는다

  2. 서브클래스가 슈퍼클래스의 모든 지정 이니셜라이저를 구현한다면 (1번 룰에 의해 상속을 받았든가, 혹은 커스텀 구현을 다 해주든가) -> 자동적으로 슈퍼클래스의 편의 이니셜라이저들을 상속받는다

즉 서브클래스에서 새롭게 도입된 프로퍼티에 디폴트 값만 다 제공해주고, 지정 이니셜라이저를 따로 만들지 않으면 -> 슈퍼클래스의 모든 이니셜라이저를 자동 상속받을 수 있다.

<추가1> 위 룰은 서브클래스에 추가적인 편의 이니셜라이저를 정의했을 때에도 유효하다.

<추가2> 슈퍼클래스의 지정 이니셜라이저를 서브클래스에서 편의 이니셜라이저로 구현하더라도 룰2를 만족했다고 여긴다.



지정/편의 이니셜라이저 실제로 써보기

지정 이니셜라이저, 편의 이니셜라이저, 자동 이니셜라이저 상속 등을 예제를 통해 알아보는 시간을 갖겠다.

  • 사용할 클래스 : Food, RecipeIngredient, ShoppingListItem

  • Food 클래스 : Base 클래스로 사용. 식품 이름을 캡슐화하고 있는 클래스.

class Food {
    var name: String
    
    init(name: String) {
        // 지정 이니셜라이저
        // 모든 stored 속성의 초기화 보장
        // 슈퍼클래스 없으므로 슈퍼클래스 이니셜라이저 불러줄 필요X
        self.name = name
    }
    
    convenience init() {
        // 편의 이니셜라이저
        // 같은 클래스 내의 지정 이니셜라이저를 불러줌
        self.init(name: "[이름없음]")
    }
}

let namedMeat = Food(name: "베이컨") // name : 베이컨
let mysteryMeat = Food() // nmae : [이름없음]


  • RecipeIngredient 클래스 : Food를 상속받는 클래스. 요리 만들기에 필요한 재료를 모델링함. 재료가 몇 개 필요한지를 나타내기 위해 quantity라는 Int형 프로퍼티 도입

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[이름없음]")
    }
}

class RecipeIngredien: Food {
    var quantity: Int
    
    init(name: String, quantity: Int) {
        // 지정 이니셜라이저
        // 이 클래스에서 새로 도입된 stored 프로퍼티의 초기화 보장
        // 부모클래스가 있으므로 부모클래스의 지정 이니셜라이저 불러주기
        self.quantity = quantity
        super.init(name: name)
    }
    
    override convenience init(name: String) {
        // 부모클래스의 지정 이니셜라이저를 편의 이니셜라이저로 구현했다
        // 이 경우에도, 부모클래스의 지정 이니셜라이저를 전부 구현했다고 취급
        // 따라서 룰2에 따라, 부모클래스의 편의 이니셜라이저를 전부 자동상속
        self.init(name: name, quantity: 1)
    }
}

// 부모클래스 Food의 편의 이니셜라이저 init()을 자동상속하기 때문에 아래가 가능
let oneMysteryItem = RecipeIngredien()

let oneBacon = RecipeIngredien(name: "베이컨")
let sixEggs = RecipeIngredien(name: "계란", quantity: 6)


  • ShoppingListItem 클래스 : RecipeIngredient 클래스를 상속받는 클래스. 요리 만들기에 필요한 재료가 쇼핑리스트에서 어떻게 보여야할지를 모델링함. 샀는지 안샀는지를 나타내기 위해 purchased 라는 Bool stored 프로퍼티를 도입. 필요한 수량 등을 표시해주기 위해 description이라는 computed 프로퍼티를 도입

class ShoppingListItem: RecipeIngredien {
    var purchased = false
    
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
    
    // 이 클래스에서 도입된 모든 프로퍼티에 초기값이 있고
    // 따로 이니셜라이저를 정의하지도 않았기 때문에
    // 부모 클래스로부터 모든 지정/편의 이니셜라이저를 상속받는다
}

// 슈퍼클래스의 이니셜라이저들을 잘 쓰고 있다
var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "베이컨"),
    ShoppingListItem(name: "계란", quantity: 6)
]

breakfastList[0].name = "오렌지 주스"
breakfastList[0].purchased = true

for item in breakfastList {
    print(item.description)
}


지금까지 클래스의 이니셜라이저와 초기화에 대하여 알아보았다. 이니셜라이저 3탄에서는 Failable Initializers, Required Initializers 등에 대해 다루겠다.


 3편을 작성하였습니다 > http://wlaxhrl.tistory.com/49

'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Deinitialization  (0) 2017.03.13
Initialization (3/3)  (0) 2017.03.13
Initialization (1/3)  (0) 2017.03.12
Inheritance  (0) 2017.03.01
Subscripts  (0) 2017.03.01