initialization 톺아보기(3) — 2단계 초기화와 클래스 상속
안녕하세요 윤돌이입니다! 다음 글을 예고하고 거의 한 달만에 글을 작성하게 되었네요,,ㅋㅋ
거두절미하고 시작해보겠습니다.
initialization 톺아보기(1) — 값 타입 초기화
initialization 톺아보기(2) — 클래스 초기화
지난글에서는 클래스의 초기화에 대해 정리했습니다. 간단히 요약하자면,
- 클래스는 값 타입과 다르게 memberwise 초기화를 제공하지 않기 때문에 프로퍼티가 초기화되지 않는 경우 반드시 초기화 구문이 필요하다.
- 클래스의 초기화는 designated init과 convenience init 두가지로 나뉜다.
- designated inie은 기본적으로 사용하는 초기화 구문을 의미하고 conveninece init은 필수가 아니다.
정도가 되겠습니다.
오늘 다룰 내용은 클래스의 2단계 초기화와 클래스 상속입니다.
2단계 초기화
클래스의 초기화는 2단계로 구성되어있습니다. 각 단계에서 하는 일은 다음과 같습니다.
1단계: 클래스의 모든 저장된 프로퍼티에 초기값이 할당된다.
2단계: 모든 프로퍼티의 초기값이 결정되면 인스턴스 사용 준비를 완료하기 전에 프로퍼티를 사용자화 할 수 있다.
조금 더 구체적으로 알아봅시다!
1단계
먼저 designated init 또는 convenience init이 클래스에서 호출됩니다. 그러면 클래스의 새로운 인스턴스를 위한 메모리 공간이 할당되고, 이 메모리는 아직 초기화 되기 전입니다.
그리고 클래스의 designated init이 호출되면 모든 지정 프로퍼티가 값을 가지고 있는지 확인하게 됩니다.
자신의 프로퍼티 값이 모두 초기화 된걸 확인하면 슈퍼클래스의 초기화 함수를 호출합니다.
이 작업은 최상위 슈퍼 클래스에 도달할 때까지 반복되고 최상위 클래스도 모든 프로퍼티가 초기화 된 것을 확인하면 비로소 메모리가 완벽히 초기화 되었다고 간주하여 1단계가 완료됩니다.
즉, 내 클래스의 designated init 호출 -> 슈퍼클래스의 init 호출 이 과정이 최상위 클래스까지 반복된다는 말인데요,
.. 너무 긴 것 같지만!! 코드로 알아봅시다.
class Continent {
var name: String
init(name: String) {
self.name = name
}
}
class Asia: Continent {
var direction: String
init(direction: String) {
self.direction = direction
super.init(name: "Asia")
}
}
class Korea: Asia {
var language: String
init(language: String) {
self.language = language
super.init(direction: "East")
}
}
상속 관계를 가지는 세 개의 클래스를 만들어보았습니다. 1단계 초기화를 해봅시다!
- Korea가 자신의 designated init인
init(language:String)
을 호출한다. - 프로퍼티인 language가 초기화 되고 Korea의 모든 프로퍼티가 초기화되었으므로 슈퍼클래스의 init인
super.init(direction:)
을 호출한다. - Asia는 init(direction:)에 의해 프로퍼티 모든 프로퍼티가 초기화된다. 따라서 슈퍼 클래스의 init인
super.init(name:)
을 호출한다. - Continent의 designated init이 호출되어 name이 초기화 된다.
- 최종 슈퍼클래스인 Continent까지 모두 초기화되었으므로 인스턴스의 메모리가 완전히 초기화되며 1단계 초기화가 종료된다.
✅ 추가
만약 클래스의 프로퍼티가 옵셔널 타입의 변수이거나 기본값을 가진 프로퍼티인 경우 init에서 초기화 값을 정해주지 않으면 옵셔널 프로퍼티는 nil로, 기본값을 가진 프로퍼티는 기본값으로 초기화 됩니다!
1단계 초기화를 그림으로 표현하면 아래와 같습니다!
2단계
2단계는 1단계 초기화 완료 이후 진행됩니다.
1단계와 반대로 최상위 ➡️ 하위뷰로 진행되며 각 클래스의 designated init은 인스턴스를 사용자 정의할 수 있습니다.
- 초기화 구문에서
self
로 접근 가능 - 프로퍼티를 수정 가능
- 인스턴스 메서드를 호출할 수 있음
- 모든 클래스에 있는 convenience init은 인스턴스를 사용자화 하고
self
를 사용할 수 있음
2단계 초기화를 그림으로 표현하면
반대로 내려오며 인스턴스를 사용자화하게 됩니다!
즉, 슈퍼클래스의 designated init이 종료되면 서브클래스의 designated init이 추가적인 커스텀을 할 수 있게 되어요. 이때 인스턴스 사용자화는 필수는 아닙니다!
서브클래스의 designated init이 종료되면 원래 호출되었던 convenience init이 인스턴스를 사용자화 할 수 있으며 마찬가지로 필수가 아닙니다.
2단계 초기화를 하는 이유
그렇다면 Swift는 왜 초기화를 2단계로 나누었을까요?
class Korea: Asia {
var language: String
init(language: String) {
self.language = language
print(direction) // Error‼️
super.init(direction: "East")
}
}
해당 코드를 살펴볼게요. Korea의 designated init에서는 Korea의 프로퍼티인 language를 초기화하고 있습니다.
하지만 여기서 super.init을 호출하기 전에 슈퍼클래스의 프로퍼티를 사용하여 문제가 발생합니다.
따라서 2단계 초기화는 서브 클래스가 deisgnated init에서 상속받은 프로퍼티에 접근하기 전에 슈퍼 클래스의 초기화를 먼저 호출(위임)하도록 하여 이처럼 프로퍼티가 초기화 되기 전에 접근하여 사용되는 것을 방지합니다.
convenience init 또한 프로퍼티에 접근하기 전에 다른 init을 먼저 호출해야합니다.(위임)
또한 다른 초기화에 의해 값이 덮어씌워지는 문제도 방지할 수 있습니다.
초기화 구문 상속과 재정의 (Initializer Inheritance and Overriding)
Obj-c와 다르게 Swift는 기본적으로 서브클래스가 슈퍼클래스의 초기화 구문을 상속하지 않습니다.
적절한 경우에 따라 슈퍼클래스의 init을 상속합니다.
이 경우에 대해 알아보기 전에 먼저 override
키워드에 대해 알아보겠습니다!
Override
Swift에서는 슈퍼클래스의 designated init과 일치하는 하위클래스의 초기화 구문을 작성할 때 하위클래스에서 designated init을 재정의할 수 있습니다.
이 경우 하위클래스의 init 구문 앞에 override
키워드를 사용해야 합니다.
초기화 구문 뿐만아니라 재정의 된 프로퍼티, 메소드 또는 서브 스크립트도 마찬가지로 재정의된 경우 override를 작성해주어야 합니다.
서브클래스의 convenience init이더라도 슈퍼클래스의 init을 재정의한 것이라면 override가 필요합니다.
단, 슈퍼클래스의 convenience init은 서브클래스에서 직접적으로 호출할 수 없으므로 슈퍼클래스의 convenience init과 같은 초기화구문을 서브클래스에서 구현할 땐 override를 붙이지 않습니다‼
정리하자면,
- 상위 클래스의 designated init, 프로퍼티, 메소드, 서브 스크립트를 재정의할 때 ➡️
override
필수 - 서브 클래스에서 convenience init으로 슈퍼 클래스의 designated init을 재정의 하는 경우 ➡️
override
필수 - 슈퍼 클래스의 convenience init과 같은 초기화 구문을 서브 클래스에서 재정의 하는 경우 ➡️ 슈퍼클래스의 convenience init은 서브클래스에서 직접적으로 호출 할 수 없으므로
override
사용 X
로 요약할 수 있습니다.
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
이 예시에서 Vehicle 클래스는 모든 프로퍼티에 기본값이 존재하여 초기화되었기 때문에 기본 초기화 구문이 자동으로 생성됩니다.
Vehicle을 상속받은 Bicycle의 init은 슈퍼 기본 초기화 구문과 동일하기 때문에 override
키워드가 붙게 됩니다!
이해가 되셨나요?
super.init으로 Vehicle의 초기화를 호출하게 되면 numberOfWheels = 2
가 실행되어 2단계 초기화 구문으로 프로퍼티를 수정하게 되겠네요!
만약 서브클래스에서 2단계 초기화에 의해 프로퍼티 값이 수정되지 않고 슈퍼 클래스가 동기적이며 Bicycle처럼 인수가 없는 init을 가진다면 super.init을 생략할 수도 있습니다.
추가: 상위 클래스의 초기화 구문이 비동기적이라면 명시적으로 await super.init()을 작성해야 합니다.
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)"
}
}
두 번째 예시는 init 대신 프로퍼티를 재정의하고 있습니다. 여기서는 앞서 말한 것처럼 슈퍼 클래스의 프로퍼티를 수정하지 않기 때문에 super.init()를 생략하여 암묵적으로 호출하고 있네요:)
자동 초기화 구문 상속(Automatic Initializer Inheritance)
다시 본 주제로 돌아와서, Swift에서 자동으로 초기화 구문을 상속하는 2가지 경우에 대해 이야기 해봅시다.
Swift에서 서브 클래스가 상위 클래스의 초기화 구문을 자동으로 상속받는 경우는 다음과 같습니다.
- 하위 클래스가 designated init 구문을 정의하지 않은 경우
- 하위 클래스가 1번에 따라 상속하거나 모든 상위 클래스의 designated init 구현을 제공하는 경우
이 두 가지 규칙을 하위 클래스가 convenience init을 추가할 때도 마찬가지로 적용됩니다.
- 하위 클래스가 designated init 구문을 정의하지 않은 경우
지난 글에서 designated init을 구현하지 않아도 되는 경우는 모든 프로퍼티가 초기화 된 경우라고 했습니다!
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Dog: Animal {
var type: String = "진돗개"
var age: Int?
}
프로퍼티가 init 없이 초기화되는 경우는 프로퍼티가 옵셔널 타입이거나 초기값을 갖는 경우라고 했습니다.
따라서 위 예제에서 Dog 클래스는 Animal의 init을 자동으로 상속받게 됩니다!
Dog.init(name: "멍멍이")
따라서 Dog는 designated init이 없어도 이렇게 사용할 수가 있는 것이지요.
만약 Dog에서 따로 designated init를 생성했다면 Dog의 init()이 사용되겠죠?
2. 하위 클래스가 1번에 따라 상속하거나 모든 상위 클래스의 designated init 구현을 제공하는 경우
1번 조건에 의해 모든 designated init을 상속받은 경우, 또는 모든 designated init을 오버라이딩 한 경우에는 자동으로 슈퍼 클래스의 모든 convenience init이 상속됩니다.
예제로 이해해봅시다! 이번 예제는 Swift.org 내용을 그대로 사용했습니다.
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
Food 클래스는 designated init과 convenience init을 제공하고 있습니다.
클래스는 구조체와 다르게 기본 memeberwise init을 가지고 있지 않기 때문에 name 프로퍼티를 초기화하는 designated init을 구현했습니다.
Food는 convenience init 내부에서도 designated init으로 초기화를 위임하여 name 프로퍼티를 초기화하고 있습니다.
그림으로 도식화 하면 아래와 같겠네요!
let meet = Food()
// name: [Unnamed]
만약 convenience init으로 초기화를 한다면 name 프로퍼티의 상태는 Unnamed가 될거예요.
이번에는 Food 클래스를 상속받는 RecipeIngredient를 만들어봅시다.
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name) // 2단계 초기화 중 1단계 만족
}
override convenience init(name: String) {
self.init(name: name, quatity: 1)
}
}
마찬가지로 2개의 초기화 구문을 제공하고 있습니다.
RecipeIngredient는 인스턴스의 모든 프로퍼티를 초기화할 수 있는 designated init을 가지고 있습니다.
우선 quantity로 서브 클래스의 프로퍼티를 초기화 하고 name을 슈퍼클래스의 init으로 위임하여 슈퍼 클래스의 프로퍼티도 초기화하기 때문이에요.
서브 클래스는 init(name:)이라는 convenience init도 제공합니다. quantity = 1로 기본값을 제공하여 여러 개의 인스턴스가 같은 quantity를 갖는 경우 편리하게 생성할 수 있어요.
이때 convenience init은 슈퍼클래스의 designated init을 재정의하기 때문에 반드시 override 키워드를 붙여주어야 합니다!!
서브 클래스의 두 가지 초기화 구문은 conveniecne init으로 init(name: String)을 제공하지만 슈퍼 클래스의 모든 designated init 구현도 제공했기 때문에 2번째 조건을 만족하게 되어 자동으로 슈퍼 클래스의 convenience init을 상속받게 됩니다.
따라서 RecipeIngredient는 세 가지의 초기화 구문을 사용할 수 있습니다.
let oneMysteryItem = RecipeIngredient() // 슈퍼클래스의 convenience init
let oneBacon = RecipeIngredient(name: "Bacon") // 서브클래스의 convenience init
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6) // 서브클래스의 designated init
정말 마지막으로 RecipeIngredient를 상속받는 클래스를 하나 더 만들어볼게요.
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
purchased와 description의 모든 초기값이 지정되어있기 때문에 초기화 구문을 정의하지 않아도 에러가 발생하지 않습니다.
따라서 1번 조건을 만족하게 되어 자동으로 상위 클래스의 모든 designated init과 convenience init을 상속받게 됩니다!
모두 이해가 잘 되셨나요..?
여기까지 2단계 초기화와 초기화 상속에 관련된 내용을 다루어보았습니다!! 내용이 정말 많네요,, 하ㅏㅏ하
다음 글에서는 실패 가능한 초기화 구문과 필수 초기화 구문에 대해 다뤄보겠습니다!
다음주 토요일에 만나요~
참고
https://bbiguduk.gitbook.io/swift/language-guide-1/initialization#default-initializers