[Swift] 2 단계 초기화(2 Phase Initialization)

초기화에 관한 2번째 포스트입니다.

초기화가 어떻게 이루어 지는지 좀 더 구체적으로 들여다 볼 예정입니다.

먼저 초기화란 저장 프로퍼티에 초기값을 할당하는 것이란걸 상기하며 보자구요!

Swift에서 초기화는 2개의 단계를 거쳐 이루어집니당

첫번째 단계는 모든 저장프로퍼티가 초기값을 할당 받는 과정이구요

두번째 단계는 첫 번째 단계를 거치고 인스턴스 생성이 되기 전에 저장 프로퍼티에 값을 커스터마이징 할 수 있는 단계에요.

4단계 안전검사

컴파일러는 2 단계 초기화가 오류없이 완료될 수 있도록 4가지의 안전 검사를 진행한다고 합니다.

이 4가지의 안전검사를 통과해야 초기화가 이루어질 수 있도록 컴파일 단계에서 오류를 잡아주는 거죠.

차근차근 봐봅시다.

1 단계

부모 클래스의 초기화를 호출 하기 전 먼저 해당 클래스의 저장 프로퍼티를 초기화 해야 한다.

class Parent {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}

class Child: Parent {
    var age: Int
    init(_ name: String, age: Int) {
        self.age = age
        super.init(name)
    }
}

Parent란 클래스를 Child 클래스가 상속 받습니다.

그리고 초기화를 진행하는데 age는 Child 클래스의 저장 프로퍼티죠? 이 저장 프로퍼티에 초기값을 할당해야

부모 클래스의 이니셜라이저를 호출할 수 있습니다. 부모를 초기화 하기 전에 내 프로퍼티 부터 초기화를 진행하고

부모의 프로퍼티를 초기화 한다!

2 단계

부모의 프로퍼티에 값을 할당하기 전 부모의 초기화를 먼저 진행해야 한다.

어떻게 보면 당연한거겠죠? 부모의 프로퍼티에 값을 할당해봤자 부모의 이니셜라이저를 호출하면 그 이니셜라이저에 의해 값이 덮어써질테니까요?

class Parent {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}

class Child: Parent {
    var age: Int
    var description: String { "\(name) \(age)" }
    init(_ name: String,_ age: Int) {
        self.age = age
        self.name = "SweetFood" // 'self' used in property access 'name' before 'super.init' call
        super.init(name)
    }
}

let sweet = Child("dev", 20)
sweet.description

위 코드는 실행조차 할 수 없습니다. 컴파일러에 의해 오류가 나거든요.

부모 이니셜라이저를 호출하기 전 부모의 프로퍼티에 접근하기 때문입니다.

3 단계

편의 이니셜라이저는 프로퍼티에 값을 할당하기 전 다른 이니셜라이저를 호출해야 한다.

이 전 포스트에서 편의 이니셜라이저는 자신의 이니셜라이저를 호출할 수 있고, 궁극적으로는 지정 이니셜라이저를 호출해야 한다고 했었는데 기억나시나요?

그럼 그 전에 내 저장 프로퍼티에 값을 할당하고 지정 이니셜라이저를 호출하면 어떻게 될까요?

값이 덮어써지겠죠? 2단계가 부모 프로퍼티가 대상이였는데 그 대상이 내 프로퍼티로 변경된 것일 뿐이죠.

편의 이니셜라이저 에서는 내 이니셜라이저를 호출한 다음 프로퍼티를 변경합시다.

class Parent {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}

class Child: Parent {
    var age: Int
    var description: String { "\(name) \(age)" }
    init(_ name: String,_ age: Int) {
        self.age = age
        super.init(name)
    }

    convenience init() {
        self.init("sweetfood", 20) // 다른 이니셜라이저를 호출 하고
        name = "customize" // 프로퍼티를 수정하자
    }
}

let sweet = Child()
sweet.description // "customize 20"

4 단계

2 단계 초기화 중 1단계 초기화가 끝나기 전 자신의 프로퍼티나 메소드 호출이 불가하다

모든 프로퍼티에 초기값을 할당하는 게 2단계 초기화 중 1단계라고 하였죠?

그러니까 1단계가 끝나기 전에 메소드 사용과 프로퍼티에 값 할당을 할 수 없다. 사용을 하게 되면 컴파일러가 에러를 분출할 것이다! 라는 말 같습니다.

class Parent {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}

class Child: Parent {
    var age: Int
    init(_ name: String, _ age: Int) {
        sayHello() // 초기화 전에 프로퍼티를 사용하지 않는 메소드라도 사용할 수 없습니다.
        self.age = age
        // 내 프로퍼티를 초기화 하였어도 부모에게서 상속받은 프로퍼티가 
        // 초기화 되지 않았기 때문에 사용할 수 없습니다.
        sayHello() 
        super.init(name)
        sayHello() // 사용 가능!
    }

    func sayHello() {
        print("Hello")
    }
}

2단계 초기화 과정

초기화 1단계가 끝날때 까지 인스턴스는 유효하지 않는다 합니다.

1단계가 끝나야 인스턴스가 유효하고 인스턴스가 유효해야 2단계인 프로퍼티에 값 할당, 메소드의 사용이 가능합니다.

그리고 1단계 2단계는 위의 4단계로 이루어진 안전검사를 기반으로 진행이되는 것이죠.

좀 더 구체적으로 초기화 과정을 살펴 봅시다

1단계 과정

  1. 편의 / 지정 이니셜라이저가 호출되며 시작됩니다
  2. 그 클래스의 인스턴스에 대한 메모리가 할당 됩니다. 이 시점에서는 아직 초기화가 이루어 지지 않습니다 초기화가 이루어지지 않았다는건 저장 프로퍼티에 초기값이 할당 되지 않았다는 거겠죠?
  3. 1단계에서 편의 이니셜라이저가 호출되었다 하더라도 궁극적으로는 지정 이니셜라이저가 호출되고 이 시점에서 모든 저장 프로퍼티에 값이 할당되고, 혹시나 빠진 저장 프로퍼티가 없는지 확인합니다. 이상이 없다면 프로퍼티에 대한 메모리도 할당이 됩니다.
  4. 1 ~ 3 과정을 super(부모)클래스에서도 수행합니다 ( super.init이 호출 되면서 시작하겠죠? )
  5. 부모의 부모클래스가 있다면 쭉쭉 위로 올라가면서 계속 반복 수행합니다.
  6. 최상위 조상, 그러니까 우리나라로 치면 단군 할아버지한테까지 올라갔다면 여기서 모든 저장 프로퍼티에 대한 값이 있는지 확인할 수 있겠죠? 여기서 이상 없이 초기화가 이루어졌다면 이 시점에서 메모리가 완전이 할당 되고 1단계가 완료되었다고 간주합니다

요약하면

결국 1~3번 과정을 최최최고 조상에게 갈때까지 반복하고 최종적으로 인스턴스 메모리를 할당 받는 거에요!

2단계

2단계는 저장 프로퍼티에 대한 값을 할당(커스터마이즈) 할 수 있다고 하였죠?

최상위 루트에서 내려가면서 각 단계별로 지정 이니셜라이저에서는 이제 내 프로퍼티와 조상의 프로퍼티에 값을 수정할 수 있어요. 메소드도 호출 가능하죠. 그리고 해당 단계에서의 편의 이니셜라이저에서도 모든 제약이 풀립니다.

이게 최하단의 자식 까지 쭉쭉 반복되는 거에요.

글만 보면 알쏭달쏭할 수 있으니 애플님 께서 그려주신 그림을 보면서 다시 천천히 봐보자구요

이 그림에서는 편의 이니셜라이저에서 출발하는 걸 예로 들었네용

InitialLevel1.png

주황 : 편의 이니셜라이저, 파랑 : 지정 편의 이니셜라이저

sub클래스의 편의 이니셜라이저에서 지정 이니셜라이저로 이동하고 그다음 super클래스의 지정이니셜라이저로 이동하는게 보이죠? super클래스의 지정 이니셜라이저로 이동할 때는 자신의 저장 프로퍼티를 모두 초기화 하고 이동하겠죠? 제일 위의 지정이니셜라이저에 도착하고 저장프로퍼티가 모두 초기화 되면 1단계 초기화가 끝난 겁니다!

InitialLevel2.png

1단계 초기화가 끝났으니 저장 프로퍼티에 값을 수정할 수 있고 메소드도 호출할 수 있죠.

물론 내가 상속받은 프로퍼티와 내가 선언한 프로퍼티만이겠지만요

class Superclass { 
    var a: String
}

class Subclass {
    var b: String
}

여기서 기준으로 하면 a에 대한 접근만 가능한거죠

자 이제 끝났으니 super의 지정이니셜라이저를 호출한 sub클래스의 지정 이니셜라이저로 돌아갑니다.

여기서는 super의 a, sub의 b 프로퍼티에 접근할 수 있죠, 그리고 편의 이니셜라이저로 넘어갑니다.

편의 이니셜라이저도 a,b 모두에 접근할 수 있죠.

이렇게 2단계 초기화는 연쇄적으로 초기화를 하고 접근할 수 있게 보장을 해줍니다!

정리

  1. 초기화는 저장 프로퍼티에 초기값을 할당한다
  2. 내 프로퍼티를 초기화 하고 부모를 초기화 하자
  3. 초기화가 다 되야지만 프로퍼티 수정 및 메소드 호출이 가능하다
  4. 그러니 프로퍼티 및 메소드 호출을 하려면 지정 이니셜라이저는 부모의 지정이니셜라이저를 먼저 호출하자
  5. 편의 이니셜라이저는 다른 이니셜라이저를 먼저 호출하자 ( 궁극적으로는 지정이니셜라이저를 호출하니까)

댓글남기기