iOS/Swift

Swift) 초기화(Initializers) 이해하기 (4/6) - 클래스의 2단계 초기화 및 상속

소들이 2022. 10. 4. 20:09

 

 

안녕하세요 :) 소들입니당

오늘부터 저는 일찍 일어나는 새가 일찍 졸리다..를 시전할 예정

난 파워 P인데 분명..

인생 계획만큼은 늘 J로 세우는.. 사람 이랄까..?

 

쨌든 이번 포스팅은 지겨운 Initializer 4번 째!!!!

클래스의 Initializer에 대해 좀 더 자세하게 알아볼 것입니당 :)

그래도 점점 끝이 보여가서 기분이 좋네여!!!

한두편 정도 더 늘어날 수.. 도 ..? 내용이 어려워 😥

 

얼른 이거 다쓰고 프로토콜 써야지~~

저는 문법 시리즈를 쓸 때 항상 이전 포스팅을 완벽하게 이해 하고

공부 했단 것을 전제로 깔고 공부 해보도록 하겠습니당 :)

모든 포스팅은 편의 말투로 합니다~!!

 

+

생각해보니 영어로 Designated / Convenience 쓰기 겁나 귀찮은데

난 걍 한글로 쓰면 될 걸 왜.. 그런.. 짓..을 ..?

따라서 앞으론 지정 / 편의 초기화라 하겠음..

 

 

 

 

1. Swift 클래스의 2단계 초기화

 

Swift에서 초기화는 두 단계 과정을 거칩니다

첫 번째 단계에서 저장된 각 프로퍼티는 해당 클래스에 의해 초기값이 할당 됩니다

첫 번째 단계로 인해 모든 프로퍼티의 초기값이 할당되면, 

두 번째 단계로 해당 인스턴스를 사용할 수 있는 상태로 간주하기 전에 프로퍼티 값을 사용자 정의할 수 있는 기회가 제공 됩니다

두 단계 초기화는 초기화를 안전하게 만들도록 진행합니다

 

ㅋㅋ;; 먼데;;

나 빼고 다 이해했냐!?! 나 빼고 다 이해했냐고!!!

라고 생각했는데.. 제가 이해한 내용을 토대로 적어 보겠습니당

틀릴 수 있으니 잘못된 내용은 꼭 피드백 바랍니다 :)

 

 

 

1-1. 1단계 초기화

 

1단계 초기화에선 클래스의 새로운 인스턴스를 위한 메모리가 할당되며, 이때 메모리는 초기화 전입니다

지정 초기화는 해당 클래스의 프로퍼티에 초기값이 있음을 확인하여 그 값으로 초기화 합니다

지정 초기화는 자신의 프로퍼티 값을 모두 초기화 시킨 후, 부모의 초기화 함수에 전달 됩니다

이 작업은 상속 고리의 최상단 부모 클래스에 도달할 때까지 계속 진행됩니다

이 작업으로 인해 최상위 부모의 프로퍼티까지 모두 기본 값으로 초기화 되면

이때 비로소 instance의 메모리가 완전히 초기화 되었다고 할 수 있으며,

1단계 초기화가 완료된 것입니다.

 

길..죠..? 그래서 어려워 보..이죠? 근데 사실 별 거 없음

자, 초기화의 가장 중요한 핵심은 뭐라 했음!?

바로 "모든 프로퍼티가 초기값을 가져야 한다" 였잖음?

 

따라서 지정 초기화를 생성할 경우, super 클래스의 지정 초기화를 반드시 호출해주어야 한다 했음

왜? 내가 내 클래스의 프로퍼티만 다 초기화 할 경우, 

부모 클래스에 선언된 프로퍼티가 초기화 되지 않을 수 있기 때문에!

 

따라서 내 프로퍼티를 모드 초기화 하고,

super.init을 통해 부모 클래스의 초기화까지 모두 시켜버리는 것이

바로 이 1단계 초기화란 말임

 

 

class Human {
    var name: String
    
    init(name: String) {
        self.name = name
        
        print(name)
    }
}
 
class Developer: Human {
    var language: String
    
    init(language: String) {
        self.language = language
        super.init(name: "unknown")
        
        print(language)
    }
}
 
class Sodeul: Developer {
    var nickName: String
    
    init(nickName: String) {
        self.nickName = nickName
        super.init(language: "swift")
        
        print(nickName)
    }
}
 

 

 

 

자, 위와 같은 코드가 있다고 보셈!

Sodeul이란 클래스는 Developer란 클래스를 상속받고,

Developer는 Human 클래스를 상속 받음!!

 

이때 만약 다음과 같이, 

 

 

Sodeul(nickName: "DeulSo")
 

 

 

Sodeul이란 클래스의 인스턴스를 생성하기 위해 생성자를 부르면,

 

 

 

 

이렇게, Sodeul 클래스의 init(nickName: String) 생성자가 호출되고,

이때 Sodeul 클래스 안의 모든 프로퍼티는 초기 값을 지니어야 함

그래야 그 값으로 초기화를 진행할테니까!

(물론 이때 옵셔널 타입의 변수이거나, 기본 값을 지닌 프로퍼티의 경우는 생성자에서 값을 지정해주지 않으면,

옵셔널 타입의 변수는 nil로, 기본 값을 지닌 프로퍼티는 해당 기본 값으로 알아서 초기화 됨!)

 

하여튼 이렇게 초기값을 갖지 않는 프로퍼티에 한해서 초기값을 지정해주고,

부모 클래스의 생성자를 호출하는 이 당연한 과정이 바로 

Swift의 초기화 1단계임!!

 

이 과정을 거쳐 최상위 클래스에 도달해서 더이상 슈퍼 클래스의 생성자를 호출할 수 없을 때,

이때 1단계 초기화가 끝나면서, 비로소 인스턴스의 메모리가 완전히 초기화 되었다! 라고 볼 수 있음

당연히 인스턴스의 메모리가 완전히 초기화 되기 전엔,

해당 프로퍼티를 읽고 쓰기가 불가능하기 때문에(초기화 외에)

 

 

 

 

이렇게 슈퍼 클래스의 init을 호출하기 전에(1단계 초기화가 끝나지 않은 경우)

이때 이미 내 생성자에서 초기화가 진행된 self.nickName의 값을 읽어오는 것은 가능하지만,

아직 초기화가 되지 않은 슈퍼 클래스의 프로퍼티 name, language는 접근할 수 없는 것!!

 

 

 

1-2. 2단계 초기화

 

최상위 부모 프로퍼티의 생성자까지 모두 호출 되어 1단계 초기화가 완료된 경우,

이때부턴 지정 초기화로부터 인스턴스를 구체화 할 기회를 부여 받습니다

이 단계부터 sefl에 접근할 수 있으며, 프로퍼티의 값 수정 및 메소드 호출 등등의 작업이 가능합니다

부모 클래스의 저장 프로퍼티 값을 변경할 수 있다

 

모~든 프로퍼티가 기본 값을 가지게 되어 초기화가 완료 되었다면,

이제부턴 가장 마지막으로 호출됐던 제일 상위 클래스의 남은 작업부터 차례로 아래로 실행

(당연히 메서드 호출은 스택으로 이루어지니!!)

 

 

 

 

따라서 이때 만약 Human 클래스에서 name을 "sodeul"로 설정 했더라도,

이는 Human 클래스의  2차 초기화 과정에서 "deulso"로 덮어씌어지고,

최종적으로 Sodeul 클래스의 2차 초기화 가정에서 "unknown"으로 덮어 씌어짐

따라서 Sodeul의 초기화가 끝난 후 name의 값을 찍으면 가장 마지막에 덮어씌어진 "unknown"이 나옴!!

따라서 부모, 조상 클래스의 저장 프로퍼티 값을 변경하는 것이 2차 초기화 과정에서 가능한 것임!!

 

앞서 말 했듯

1차 초기화가 끝난 이후, 인스턴스가 생성되기 때문에

 

 

 

위처럼, sayHello란 메서드를 1차 초기화가 끝나기 전엔 호출하지 못하지만,

2차 초기화 지점에 호출할 수 있음!!

 

 

 

1-3. 2단계 초기화는 왜 있는 걸까?

 

지정 초기화는 상속받은 프로퍼티에 접근하기 전에, 부모의 초기화를 먼저 호출(위임) 해야 함

이로 인해 부모의  초기화에 의해 내가 설정한 겂이 덮어지는 문제를 방지할 수 있음

 

편의 초기화 또한 어떤 프로퍼티든 접근하기 전에 다른 초기화를 먼저 호출(위임) 해야 함

전에 편의 초기화 공부할 때 말했잖음!?

 

Convenience Initializers에서는 프로퍼티 값 설정 못하나여?ㅠ

설정하려고 하면 'self' used before self.init call or assignment to 'self' 에러 뜨는데염 ㅠ

 

 

 

 

위 처럼 에러가 뜰 때는 다른 Initializer가 불린 후에 접근해주면 됨!!

 

 

 

 

이런 식으루!! :)

 

다른 초기화를 부르기 전에 값을 설정하면,

다른 초기화에 의해 값이 덮어씌어지는 문제가 발생할 수 있기 떄문에 이를 방지하는 것임!!!

이제는 이해할 수 있겠군요!!!!!!!! XD

 

 

이처럼, 초기화를 보다 "안전"하게 진행하기 위해

(초기 값이 설정되기 전에 해당 프로퍼티에 접근하거나, 부모 생성자에 의해 값이 덮어씌여지거나 하는 일이 없게!!)

2단계 초기화를 통해 초기화를 진행 함 :)

 

 

 

 

2. Initializer의 상속

 

우린 클래스의 생성자가 총 2단계를 걸쳐 초기화를 진행한다는 것을 알게 됐잖음!? 

근데 이번엔 이 클래스의 생성자의 상속에 대해 알아볼 것임

클래스의 Initializer는 총 두 가지 조건에 의해 상속이 됨

 

Swift 서브클래스는 기본적으로 슈퍼클래스 이니셜라이저를 상속받지 않기 때문에 생긴 조건임!!

옵젝씨와 다르니 헷갈리지 마셈!!

 

 

 

2-1. 서브 클래스에서 지정 초기화를 직접 구현하지 않았다면, 슈퍼 클래스의 모든 지정 초기화가 상속 된다

 

자, 열어분 생각을 해보셈

클래스에서 지정 초기화를 직접 구현하지 않아도 되는 경우는 언제였음!??!!

바로 생성자를 작성하지 않아도 모든 프로퍼티가 기본 값을 가질 경우에 한해서였음

 

어떻게?

옵셔널 타입의 변수로 설정되어 자동으로 nil로 초기화 되거나,

아니면 생성과 동시에 초기값을 갖거나!!!

 

 

class Human {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}
 
class Sodeul: Human {

    // 1. 서브 클래스의 모든 속성이 기본 값으로 초기화
    var nickName: String = "Sodeul"
    var age: Int?
    
    // 2. Designated Initializers를 직접 구현 X
}
 

 

 

요롷게!!! 따라서 첫번 째 상속 조건인,

서브 클래스에서 지정 초기화를 구현하지 않았다는 건

당연히 먼저 

생성자를 작성하지 않아도 모든 프로퍼티가 기본 값을 가질 경우

란 전제 조건이 따름!!!

 

따라서, 이렇게 지정 초기화를 직접 구현하지 않을 경우엔,

이때는 슈퍼 클래스인 Human의 지정 초기화가 모두 상속

 

 

 

 

이렇게!!

Sodeul 클래스는 생성자를 정의해주지 않았으나,

Human에 정의된 Initializers를 상속받아서 쓸 수 있음!!

 

자, 생각을 해보자 :)

우리는 앞서 공부할 때  모든 프로퍼티가 기본 값을 가질 경우,

그 경우에 한해서만 Initializers를 필수로 작성하지 않아도 된다고 했음!

 

자, 그러면 만약 기본 값을 갖지 않는 프로퍼티가 있어서 생성자를 만들어야 한다면??

혹은 난 모든 프로퍼티가 기본값을 갖지만, 생성자를 굳이 굳이 만들어야 겠어!!! 한다면!?!?

 

 

 

 

이땐 당연히 지정 초기화를 직접 선언 했으니,

부모 클래스의 init(name:) 생성자가 상속되지 않음!!!

 

 

 

2-2. 서브 클래스가 1번 조건에 의해 모든 지정 초기화를 상속 받거나 / 또는 모든 지정 초기화를 오버라이딩 했다면, 자동으로 부모의 모든 편의 초기화가 상속 된다

 

1번 조건에 의해 모든 지정 초기화를 상속 받는단 말까진 이해 했잖음?

근데 오버라이딩이라니?ㅎㅎ;;

 

슈퍼 클래스의 지정 이니셜라이저와 일치하는 이니셜라이저를 만약 서브 클래스에서 작성해야 된다고 해보셈,

이때 우린 당연하게 override를 생각할 거잖음??

 

 

class Human {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}
 
class SodeulHuman {
    var nickName: String = "Sodeul"
    var age: Int?
    
    override init(name: String) {
        super.init(name: "")
    }
}
 

 

 

이땐 당연히 생성자 앞에 override를 사용해서 오버라이딩을 사용할 수도 있음 :)

물론 이때도 당연히 super.init을 호출해줘야 에러 안남^^;;;;

 

쨌든 이런 식으로 부모 클래스의 모든 지정 초기화를 상속받거나,

위처럼 부모 클래스의 모든 지정 초기화를 오버라이딩 해서 구현 했다면

이땐 부모의 모든 편의 초기화 메서드가 상속됨!!!

 

 

class Human {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init(unknown: String) {
        self.init(name: unknown)
    }
}
 
class SodeulHuman {
    var nickName: String
    
   override init(name: String) {
      nickName = name
       super.init(name: name)
    }
}
 

 

 

예로,

위 코드처럼 Sodeul이란 클래스는 슈퍼 클래스의 모든 지정 초기화를 오버라이딩 했잖음!?

근데 이런 경우엔, 지정 초기화 외에도

Human 클래스의 편의 초기화인 init(unknown:) 도 상속받는단 말임!!

 

 

 

 

이렇게!!! 편의 초기화도 상속된 것을 볼 수 있음 :)

 

참고로, 편의 초기화는 오버라이딩 개념이 적용되지 않음

어차피 편의 초기화 자체는 동일 계층으로 수평으로 호출되기 때문

super.편의 초기화를 호출할 수 없기 떄문

 

 

 

 

편의 초기화는 상속은 되지만, 오버라이딩은 안 된 다아아아아아아

 

 

 

 

 

 

 

 

 

 

.

.

.

.

호..호.. 저는 이해했는데.. 여러분은 이해.. 하 셨나 ..?

틀린 게 있다면 대왕 지송.;;

피드백 대환영 ;;;..