개요
초기화에 관해서 전체 내용을 정리하기 전에, two phase initialization에 대해서 먼저 보고 들어가는 것이 다른 개념들을 이해하는데 도움이 될 거라고 생각이 들었다.
그래서 two phase initialization에 대해 먼저 정리를 하고 나중에 초기화에 관해 전체적으로 보기로 했다.
두 단계로 진행되는 초기화
Swift의 클래스 초기화는 두 단계로 진행이 된다.
첫번째 단계에서는 클래스에 명시된 값에 따라 각 stored property들이 초기화가 된다.
모든 stored property에 대한 초기 상태가 정해지고 나면(예를 들면 메모리 할당), 두번째 단계가 실행이 된다.
두번째 단계에서는 클래스에게 stored property들의 값을 사용자가 설정 할 수 있는 기회가 주어진다.
이 두 단계는 클래스 인스턴스를 사용할 수 있다고 판단하기 전에 행해진다.
이 말은 즉, 이 두 단계가 모두 진행이 되어야 클래스 인스턴스를 사용할 수 있다고 판단하는 것이다.
Safety check
Two phase initialization 과정은 각 클래스의 계층 구조에 유연성을 유지하면서 안전하게 초기화하기 위해 고안된 것이다.
Two phase initialization는 초기화되기 전에 클래스 속성의 값에 접근하는 것을 방지해주고, 다른 initializer에 의해 속성 값이 예상치 못하게 다른 값으로 설정되는 것을 방지해준다.
그리고 이 안정성을 지키기 위해 Swift의 컴파일러는 4가지 safety-check를 통해 Two phase initialization가 오류 없이 마무리 되게 도운다.
Safety check 1
Designated initializer는 상위 클래스 이니셜라이저로 위임하기 전에 해당 클래스에 도입된 모든 속성들이 초기화되도록 보장해야 한다.
위에서 언급된 대로, 객체의 메모리는 모든 stored property의 초기 상태(initial state)가 알려져있어야 완전히 초기화 된 것으로 간주된다.
이 규칙을 만족하기 위해, Designated initializer는 자신의 프로퍼티가 상속 체인을 따라 전달되기 전에 모든 자체 속성들이 초기화되도록 해야 한다.
Safety check 2
Designated initializer는 상속된 프로퍼티에 값을 할당하기 전에 상위 클래스 이니셜라이저로 위임해야 한다.
그렇지 않으면 Designated initializer가 할당하는 새로운 값은 상위 클래스의 초기화 과정 중에 덮어쓰여질 수 있다.
Safety check 3
Convenience initializer는 속성(동일한 클래스에서 정의된 속성 포함)에 값을 할당하기 전에 다른 이니셜라이저로 위임해야 한다.
그렇지 않으면 Convenience initializer가 할당하는 새 값은 해당 클래스의 Designated initializer에 의해 덮어쓰여질 수 있다.
Safety check 4
Initializer는 초기화의 첫 번째 단계가 완료되기 전까지는 인스턴스 메서드를 호출하거나 인스턴스의 속성값을 읽거나 self를 값으로 참조할 수 없다.
첫 번째 단계가 종료되고 나서야 클래스 인스턴스가 유효한 상태인 것으로 알려질 때에만 프로퍼티에 접근하고 메서드를 호출할 수 있다.
위의 네 가지 안전 점검을 기반으로 이니셜라이저의 두 단계 초기화는 다음과 같이 진행된다.
Phase 1
Phase 1의 진행 단계
Phase 1은 아래와 같이 진행된다.
- 클래스에서 Designated initializer 또는 Convenience initializer가 호출된다.
- 해당 클래스의 새 인스턴스를 위한 메모리가 할당된다.
이 메모리는 아직 초기화되지 않은 상태이다. - 해당 클래스의 Designated initializer는 해당 클래스에 도입된 모든 stored property가 값을 갖는지 확인한다.
이로 인해 stored property의 메모리가 초기화된다. - Designated initializer는 상위 클래스 initializer에게 자신의 stored property에 대해 동일한 작업을 수행하도록 위임한다.
- 이 과정은 클래스의 상속 체인을 따라 계속 진행된다.
상속 체인의 맨 위에 도달할 때까지 계속한다. - 상속 체인의 맨 위에 도달하고, 체인의 마지막 클래스가 모든 stored property에 값을 갖도록 보장하면 인스턴스의 메모리는 완전히 초기화된 것으로 간주되며, 1단계가 완료된다.
이를 도식화하면 아래와 같이 간단하게 볼 수 있다. ▼
과정을 순서대로 보자.
가장 먼저 Convenience Initializer가 호출된다고 해보자. ▼
Convenience Initializer는 아직 아무 속성도 변경할 수 없기에 같은 클래스에 있는 Designated initializer에게 초기화 작업을 위임시킨다. ▼
초기화 작업을 위임받은 Designated initializer는 앞에서 보았던 Safety check의 첫번째 항목을 확인한다.
Designated initializer는 상위 클래스 initializer로 위임하기 전에 해당 클래스에 도입된 모든 속성들이 초기화되도록 보장해야 한다.
현재 클래스에서 새롭게 생긴 속성들, 상속받지 않은 속성들에 대한 초기화를 확인한다. ▼
Safety check를 통과했다면, 초기화 과정을 상위 클래스에 위임한다.
이를 위임받은 상위 클래스는 앞의 과정을 더 이상 위임할 상위 클래스가 없을 때 까지 반복한다. ▼
최상위 클래스까지 올라가게 되면 phase 1은 종료된다.
Phase 2
Phase 2의 진행 단계
- 상속 체인의 맨 위에서부터 다시 작업을 시작하면, 체인의 각 Designated initializer는 인스턴스를 추가로 사용자 정의(Customize)할 수 있는 옵션을 갖는다.
Initializer는 이제 self에 접근하고 그의 프로퍼티를 수정하고 인스턴스 메서드를 호출할 수 있다. - 마지막으로, 체인에 있는 모든 Convenience initializer는 인스턴스를 사용자 정의하고 self와 함께 작업할 수 있는 옵션을 갖는다.
이를 도식화하면 아래와 같이 간단하게 볼 수 있다. ▼
Customize 한다는 것
위에서부터 아래로 내려오며 값을 사용자 정의, Customize 한다고 되어있는데 조금 의아할 수 있다.
“아래에서 위로 올라가는 phase 1에서 이미 값을 다 설정할텐데, Customize할 상황이 있는 것일까?”
그래서 이를 명쾌하게 설명해주는 예제를 하나 가져왔다.
Deer
클래스와 Deer
클래스를 상속받는 Gorani
클래스가 있다.
Deer
의 age
와 weight
는 옵셔널 값이기에 Deer
의 init()
에서 값을 설정하지 않아도 nil
로 초기화가 된다.▼
class Deer {
var age: Int?
var weight: Int?
init() {
age = 1
}
}
class Gorani: Deer {
var goraniTrait: Int?
override init() {
super.init()
weight = 70
goraniTrait = 4242
}
}
그래서 age
에 대해서면 Deer
의 init()
에서 초기화를 하는 게 이상한 상황은 아니게 된다.
이렇게 되면, phase1에서 age
, weight
, goraniTrait
그 어느 것에 대해서도 초기화하지 않고 nil
로 설정 한 후, phase2에서 상위 클래스인 Deer
부터 값을 다시 초기화할 수 있게 된다. ▼
Deer의 속성인 weight의 값을 Gorani의 initializer에서 바꿨다.
self 접근
앞의 Safety check 4를 다시 한 번 보자.
Safety check 4
Initializer는 초기화의 첫 번째 단계가 완료되기 전까지는 인스턴스 메서드를 호출하거나 인스턴스의 속성값을 읽거나 self를 값으로 참조할 수 없다.
초기화가 끝나기 전까지는 self를 값으로 참조할 수 없다고 명시가 되어있다.
이는 초기화의 흐름을 보면 말이 안된다는 것을 알 수 있다.
아래의 코드를 보자.
Country
클래스와 City
클래스는 초기화 단계에서 서로를 속성의 값으로 가진다. ▼
class Country {
let name: String
var capitalCity: City
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
이 코드는 얼핏보면 정상적인 코드같지만, 동작하지 않는 코드다.
그 이유는 이 코드의 흐름을 보면 명확히 알 수 있다.
가장 먼저 Country의 initializer가 호출이 된다.
Country의 고유 속성인 name과 capitalCity에 대해서 초기화가 진행이 되는데, capitalCity에 값을 넣기 위해 City의 initializer가 호출된다. ▼
호출된 City의 initializer는 고유 속성인 name과 country를 초기화하는데, country의 값으로 self가 들어왔다. ▼
self가 들어왔기에, 아까 초기화하던 Country의 인스턴스를 전달해주면 되지만 그렇게 할 수 없다.
아까 초기화하던 Country 인스턴스는 아직 safety check1도 수행되지 않은 상태이고, Phase 2도 수행되지 않은 상태이기에 사용할 수 없는 인스턴스이기 때문이다. ▼
그렇기에 위의 예제에서 self를 사용할 수 없는 것이다.
아직 전부 초기화가 되지 않았는데 인스턴스를 사용하려고 했기 때문이다.
“그러면 저렇게 하나의 initializer로 두 클래스를 초기화하는 매커니즘은 아예 구현할 수 없는 건가?”
이에 대한 해답은 옵셔널이다.
옵셔널 값으로 선언하게 되면, 아무것도 없는 것도 nil이라는 표현으로 값이 있는 것으로 판별이 된다.
그렇기에 safety check1 에서 값이 모두 초기화 된 것으로 판별이 되어 위의 매커니즘을 구현할 수 있게 된다. ▼
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
마치며
간단하게 Swift의 Two phase initialization에 대해서 알아보았다.
그동안에 self를 사용할 수 없다고 IDE에서 경고나 오류를 띄울 때 왜 그런건지 잘 모른채로 권장하는 코드로 바꾸기만 했는데, 이제는 그 원인을 명확히 짚어낼 수 있게 되었다.
두루뭉술하게, 약간은 안개가 낀 듯한 느낌이었는데 조금은 뚜렷하게 볼 수 있게 되었다.
아직은 정말 깨끗하게 그 시야가 보이지는 않지만, 앞으로 나아갈 수 있을 정도는 됐다고 생각이 든다.
이제는 본격적으로 예제 어플을 만들어보면서 다른 부족한 부분들에 대해서 공부를 해야겠다.
'Develop > iOS' 카테고리의 다른 글
[iOS][Swift] Swift의 메모리 관리 : ARC 2 (0) | 2024.02.11 |
---|---|
[iOS][Swift] 클로져 캡쳐에 대한 이해 (0) | 2024.02.11 |
[iOS][Swift] Swift의 메모리 관리 : ARC 1 (0) | 2024.02.09 |
[iOS][UI] Auto Layout에 대해 (0) | 2024.01.23 |
[iOS][Swift] MetaType이란 (0) | 2024.01.23 |