본문 바로가기

Swift

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

 

*[Swift] 클래스 상속과 초기화 Part.1에 이어지는 내용입니다.

Initializer Inheritance and Overriding

Objective-C의 subclass와 달리 Swift의 subclass는 부모 클래스의 initializer를 기본적으로 상속받지 않는다. 이러한 Swift의 접근은 간단한 부모 클래스의 initializer가 더 구체화된(specialized) 서브 클래스의 initializer에 의해 상속되어 서브 클래스의 인스턴스가 완전히/제대로 초기화되지 않은 채 생성되는 문제를 예방한다.

 

만약 부모 클래스의 initializer와 동일한 서브 클래스에서 제공하기 원한다면 부모 클래스의 initializer들을 서브 클래스에서 커스텀하게 구현하여 제공할 수 있다.

 

부모 클래스의 designated initializer에 매칭되는 서브 클래스의 initializer를 작성할 때, 해당 designated initializer의 override를 효과적으로 제공하는 것이다. (응?) 그러므로 서브 클래스의 initializer 정의 앞에 override를 붙여야한다. 이는 자동으로 제공되는 기본 initializer(default initializer)를 작성할 때에도 동일하다.

 

overriden 프로퍼티, 메소드, 서브스크립트와 같이 override 수식어는 Swift로 하여금 부모 클래스가 override 되어야하는 매칭되는 designated initializer가 있는지, 오버라이딩하는 initializer의 파라미터들이 제대로 명시되었는지 검사하도록한다.

 

거꾸로, 부모 클래스의 convenience initializer와 매치되는 initializer를 서브 클래스에 작성한다면 부모 클래스의 convenience initializer는 서브 클래스로부터 절대 직접적으로(directly) 불릴 수 없다. (Initializer Delegation for Class Types Rule, Part.1 참고) 그러므로 이런 경우 서브 클래스가 부모 클래스의 initializer를 override하고 있지 않은 것이다. 결과적으로 부모 클래스의 convenience initializer에 매칭되는 구현을 제공할 때는 override를 붙이지 않는다.

 

아래의 예제는 Vehicle이라는 베이스 클래스를 정의한다. 이 베이스 클래스는 numberOfWheels라는 저장 프로퍼티를 선언하고있고, 기본 값은 Int 값 0이다. numberOfWheels 프로퍼티는 String 타입의 description이라는 계산 프로퍼티에 의해 사용된다.

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheel(s)"
    }
}

 

Vehicle 클래스는 저장 프로퍼티에 대해서만 기본값를 제공하고 custom initializer를 제공하지 않는다. 결과적으로, 이 클래스는 default initializer를 받게된다. default initializer는 항상 해당 클래스의 designated initializer이며 numberOfWheels가 0인 Vehicle 인스턴스를 생성하는데 사용된다.

let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)

 

다음 예제는 Vehicle의 서브 클래스인 Bicyle을 정의한다.

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

Bicycle 서브클래스는 custom designated initializer인 init()을 정의하고 있다. 이 designated initializer는 Bicycle의 부모 클래스인 Vehicle의 designated initializer와 매치되므로 앞에 override 지시어가 붙는다.

 

Bicycle의 init()은 super.init()을 호출하는 것으로 시작된다. 이 메소드 호출은 Bicycle 클래스의 부모 클래스인 Vehicle의 default initializer를 호출한다. 이는 상속된 numberOfWheels 프로퍼티에 대해 Bicycle이 custom 하기 전에 (위 예제의 경우 2로 초기화하기 전에) Vehicle에 의해 초기화되도록 보장한다. super.init()을 호출한 이후에 numberOfWheels의 원본 값은 새로운 값인 2로 대체된다.

 

만약 Bicycle 인스턴스를 생성하면 상속받은 계산 프로퍼티인 description를 호출할 수 있고, numberOfWheels가 어떻게 업데이트 되었는지 확인할 수 있다:

let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)

 

서브 클래스 initializer가 초기화 과정의 Phase 2에서 customization을 수행하지 않고 부모클래스의 designated initializer가 zero-argument라면 서브 클래스의 저장 프로퍼티를 모두 초기화한 이후에 super.init()을 생략할 수 있다.

 

이 예제는 Vehicle의 또 다른 서브 클래스 Hoverboard를 정의한다. Hoverboard의 initializer에서는 color 프로퍼티에 대해서만 초기화를 진행한다. super.init()를 명시적으로 호출하지 않고 이 initializer는 암시적인 호출에 의존하여 부모 클래스의 초기화 과정이 완료되도록한다.

(여기서 super.init()을 생략해도 상속 체인 위로 초기화 과정이 위임 될 수 있는 이유가 직전에 나온 "서브 클래스 initializer가 초기화 과정의 Phase 2에서 customization을 수행하지 않으며", " 부모클래스의 designated initializer가 zero-argument"이기 때문으로 보인다.)

class Hoverboard: Vehicle {
    var color: String
    init(color: String) {
        self.color = color
        // super.init() implicitly called here
    }
    override var description: String {
        return "\(super.description) in a beautiful \(color)"
    }
}

 

Hoverboard의 인스턴스는 Vehicle initializer에 의해 wheels를 기본 값으로 사용하게된다.

let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver

 

Automatic Initializer Inheritance

위에서 언급되었듯, 서브 클래스는 부모 클래스의 initializer를 기본(default)으로 상속하지 않는다. 하지만 특정 조건이 갖춰지면 부모 클래스의 initializer가 자동으로 상속된다. 이는 initializer override를 일반적인 시나리오에서는 쓸 일이 많이 없음을 의미하며 안전하다면 언제든 최소한의 노력으로 부모 클래스의 initializer를 상속받을 수 있다는 뜻이다.

 

서브 클래스에서 새로운 프로퍼티에 기본 값을 제공했다고 가정하면 아래의 두 규칙이 적용된다

 

Rule 1

서브 클래스가 designated initializer를 정의하지 않는다면 자동으로 부모 클래스의 designated initializer를 상속받는다

 

Rule 2

서브 클래스가 부모 클래스의 모든 designated initializer에 대한 구현을 제공한다면, (rule 1에 의해서든, custom implementation을 제공하든) 자동으로 부모 클래스의 모든 convenience initializer를 상속받는다.

 

이 두 규칙은 서브 클래스에 convenience initializer를 추가해도 적용된다.

 

Designated and Convenience Initializers in Action

아래 예제는 designated initializers, convenience initializers, 그리고 automatic initializer inheritance까지 보여준다. 이 예제는 세 클래스 Food, RecipeIngredient, ShoppingListItem의 hierarchy를 정의하며 그것들의 initializer가 어떻게 상호작용하는지 증명한다.

 

이 위계의 베이스 클래스는 Food이다. foodstuff의 이름을 캡슐화하는 간단한 클래스이다. Food 클래스는 name이라는 String 타입 프로퍼티를 가지며 인스턴스 생성을 위한 두 initializer를 제공한다.

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

 

아래 그림은 Food 클래스의 initializer chain을 보여준다.

클래스는 default memberwise initializer를 갖고있지 않으므로 Food 클래스는 name이라는 인자를 갖는 designated initializer를 제공한다. 이 initializer는 구체적인(specific) 이름을 갖는 Food 인스턴스를 생성하는데 사용될 수 있다.

let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"

Food 클래스의 init(name: String) initializer는 새 Food 인스턴스의 모든 저장 프로퍼티를 초기화함을 보장하기 때문에 designated initializer로 제공되었다. Food 클래스는 부모 클래스를 갖지 않으므로 init(name: String) initializer는 super.init()를 호출하지 않는다.

 

Food 클래스는 convenience initializer도 제공하는데, init()이라는 이름의 인자를 받지 않는 형태이다. init() initializer는 name 프로퍼티에 대해 기본값(default placeholder)를 제공하고 designated initializer로 초기화 과정을 위임한다.

let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"

 

두번째 클래스는 RecipeIngredient이다. RecipeIngredient 클래스는 요리 레시피에 적힌 재료를 모델링하는 클래스다. quantity라는 Int형 프로퍼티를 갖고 있으며 (Food 클래스로부터 상속받은 name 프로퍼티도 갖고 있다) 두 initilizer를 정의한다.

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

 

아래 그림은 RecipeIngredient 클래스의 initializer chain을 보여준다.

RecipeIngredient 클래스는 init(name: String, quantity: Int)라는 하나의 designated initializer를 갖는다. 이 initializer는 RecipeIngredient 클래스의 모든 프로퍼티를 채울(populate) 수 있다. 이 initializer는 인자로 전달받은 quantity 값을 quantity 프로퍼티에 대입함을 시작으로 한다. 그 후에 Food 클래스의 init(name: String) initializer로 위임한다. 이 프로세스는 safety check 1을 만족한다.

(Safety check 1: designated initializer는 부모 클래스의 designated initializer를 호출하기 전에 속한 클래스의 모든 프로퍼티가 반드시 초기화 되도록 보장해야한다., Part.1 참고)

 

RecipeIngredient 클래스는 또한 init(name: String)이라는 convenience initializer를 정의하며 이는 name만으로 RecipeIngredient 인스턴스를 생성할 때 사용된다. 이 convenience initializer는 명시적으로 지정되지 않았다면 어떤 RecipeIngredient 인스턴스든 quantity를 1로 가정(assume)한다. 이 convenience initializer의 정의는 RecipeIngredient 인스턴스를 더 빠르고 편하게 생성할 수 있도록 해주며 single-quantity RecipeIngredient 인스턴스를 여러개 만들어야할 때 코드 중복을 방지해준다. 이 initializer는 같은 클래스 내에 있는 designated initializer에게 quantity를 1이라는 값으로 넘기며 초기화 과정을 위임한다.

 

RecipeIngredient 클래스의 init(name: String) convenience initializer는 Food 클래스의 designated initializer init(name: String)과 동일한 파라미터를 받는다. 이 convenience initializer는 부모 클래스의 designated initializer를 오버라이드하기 때문에, override 지시어가 표기되어야한다.

 

RecipeIngredient 클래스가 init(name: String) initializer를 convenience initializer로 제공한다고해도 RecipeIngredient는 부모 클래스의 designated initializer의 구현을 제공하지는 않는다. 그러므로 RecipeIngredient는 자동으로 부모 클래스의 convenience initializer들까지 모두 상속한다.

 

이 예제에서는 RecipeIngredient의 부모 클래스인 Food가 init()이라는 하나의 convenience initializer밖에 갖고있지 않다. 이 initializer는 그러므로 RecipeIngredient에게 상속된다. 상속된 버전의 init() 함수는 RecipeIngredient의 init(name: String)으로 위임한다는 사실을 제외하면 Food 버전의 init()과 완벽하게 동일하게 작동한다.

 

이 세 initializer는 모두 새 RecipeIngredient 인스턴스를 생성하는데 사용될 수 있다.

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

 

RecipeIngredient의 서브 클래스이자 마지막 클래스는 ShoppingListItem이다. ShoppingListItem 클래스는 쇼핑 리스트에 있는 레시피 재료들을 모델링한 클래스다.

 

쇼핑 리스트의 모든 아이템은 unpurchased 상태에서 시작한다. 이 사실을 표현하기 위해 ShoppingListItem 클래스는 purchased라는 bool 타입의 프로퍼티를 가지며 기본값은 false이다. ShoppingListItem는 또한 description 프로퍼티를 더해 ShoppingListItem 인스턴스의 textual description을 제공한다.

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

 

모든 프로퍼티의 기본 값을 제공하기 때문에 아예 initializer를 정의하지 않았다. 그리고 ShoppingListItem은 자동적으로 부모 클래스의 모든 designated, convenience initializer를 상속한다.

 

새 ShoppingListItem 인스턴스를 생성하기위해 세 개의 상속한 initializer를 모두 사용할 수 있다.

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘

여기서, 새 ShoppingListItem 인스턴스 세 개를 포함(contain)하는 breakfastList라는 새로운 배열이 생성되었다. 이 배열의 타입은 [ShoppingListItem]이다. 이 배열이 생성된 이후에 첫번째 ShoppingListItem의 이름은 "[Unnamed]"에서 "Orance juice"로 바뀌었고 구매되었다고(purchased) 표시되었다. 각 아이템의 description을 출력하면 각 아이템의 default state를 확인할 수 있다.

'Swift' 카테고리의 다른 글

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