Swift/개념 & 응용

[Swift] memberwise 초기화, convenience 초기화

유정주 2023. 8. 7. 14:29
반응형

서론

Swift에서 구조체와 클래스는 많은 차이가 있고, 초기화도 그중 하나입니다.

구조체는 멤버와이즈(memberwise) 초기화를, 클래스는 편의(convenience) 초기화를 가지고 있죠.

 

초기화에 대해 할 말은 굉장히 많지만,

이번 포스팅에서는 멤버와이즈(memberwise) 초기화와 convenience 초기화에 집중해서 알아보겠습니다.

 

 

초기화(Initializer)

초기화란 구조체, 열거형, 클래스의 인스턴스를 생성하는 것입니다.

초기화의 역할은 모든 프로퍼티를 기본값으로 초기화하는 것입니다.

만약 초기화가 끝나는 시점에 모든 프로퍼티가 기본값을 가지고 있지 않다면 초기화가 실패됩니다.

(여기서 언급한 초기화의 역할은 아래에서도 꾸준히 언급되므로 알아두시면 좋겠습니다.)

 

 

Default Initializers

Default 초기화는 모든 프로퍼티에 기본값이 존재하면서 초기화가 하나도 없는 경우 제공됩니다.

Default 초기화는 모든 프로퍼티에 기본값을 설정해서 새로운 인스턴스를 생성합니다.

class ClassA {
    var num: Int = 10
    var name: String = "jeong"
}

struct StructA {
    var num: Int = 10
    var name: String = "jeong"
}

초기화의 역할은 모든 프로퍼티를 초기화하는 것인데,

모든 프로퍼티가 기본값이 존재하면 이 역할이 완수된 것이므로 편하게 인스턴스 생성을 할 수 있게 제공하는 것이죠.

 

 

memberwise Initializers

구조체는 멤버와이즈 초기화를 제공합니다.

 

멤버와이즈 초기화는 새로운 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 방법입니다.

구조체는 멤버와이즈 초기화 덕분에 기본값이 없는 프로퍼티가 있더라도 컴파일 에러가 발생하지 않습니다.

반면 클래스는 초기화가 없다는 컴파일 에러가 발생합니다.

 

단, 어떠한 커스텀 초기화도 존재하면 안 됩니다.

커스텀 초기화가 하나라도 존재한다면 멤버와이즈 초기화를 사용할 수 없습니다.

위 스크린샷을 보면 커스텀 초기화를 정의했기 때문에 멤버와이즈 초기화가 없어진걸 볼 수 있습니다.

 

그래도 아예 방법이 없는건 아니고,

extension 안에 커스텀 초기화를 작성하면 두 종류의 초기화 모두 사용할 수 있습니다.

위 스크린샷을 보면 멤버와이즈 초기화와 커스텀 초기화 모두 사용 가능한 걸 볼 수 있습니다.

 

멤버와이즈 초기화 특성

멤버와이즈 초기화 특성을 좀 더 알아봅시다.

멤버와이즈 초기화는 프로퍼티에 "직접 접근"하여 값을 할당합니다.

이 동작 원리때문에 아래 특성들이 생기는 것이므로 연계해서 이해해 주세요.

 

1. 초기화 순서

멤버와이즈 초기화는 프로퍼티가 선언된 순서와 개수에 맞춰 생성됩니다.

따라서 순서와 개수를 맞추지 않으면 컴파일 에러가 발생합니다.

 

 

2. 기본값이 있는 var 초기화

let a1 = StructA(num: 5, name: "jeong") //OK
let a2 = StructA(name: "jeong") //OK

기본값이 있는 var 프로퍼티는 초기화가 선택입니다.

이미 초기화가 되었으니 추가로 초기화를 안 해도 괜찮고,

var니까 값이 바뀔 수 있기 때문에 다시 초기화를 해도 괜찮습니다.

 

 

 

3. 기본값이 있는 let 초기화

기본값이 있는 let 프로퍼티는 초기화를 할 수 없습니다. (기본값이 없으면 초기화 가능)

let은 값을 바꿀 수 없기 때문에 초기화에서 값을 다시 할당할 수 없는 것이죠.

 

 

4. private var/let을 포함한 초기화

private 프로퍼티가 하나라도 있다면 멤버와이즈 초기화를 사용할 수 없습니다.

private에는 외부에서 직접 접근을 할 수 없기 때문에 멤버와이즈 초기화도 불가능한 것입니다.

 

이걸 보고 궁금했던건 "private let 프로퍼티에 이미 기본값이 있어도 안 될까?" 입니다.

기본값이 있는 let 프로퍼티는 멤버와이즈 초기화에서 제외되니까 private이어도 되지 않을까? 싶었던거죠.

그래서 직접 해보니

제 생각대로 멤버와이즈 초기화를 제공하였습니다. (이게 되네 ㅋㅋ)

 

혹시나해서 var로도 해봤는데 역시 안 되더라고요.

좋은 거 배웠습니다 ㅎㅎ

 

 

클래스 초기화

이제 클래스 초기화에 대해 알아보겠습니다.

 

클래스는 멤버와이즈 초기화를 지원하지 않습니다.

그래서 직접 초기화를 해줘야 하는데요.

클래스의 초기화는 designated 초기화와 convenience 초기화로 나뉩니다.

 

두 초기화 모두 초기화의 주요 역할인

"새로운 인스턴스의 모든 프로퍼티를 올바르게 초기화한다"를 지키기 위해 몇 가지 규칙이 존재합니다.

이것도 아래에서 꾸준히 언급되므로 연계해서 생각해 주세요.

 

 

designated Initializer

먼저 designated 초기화에 대해 알아보겠습니다.

 

designated 초기화는 클래스의 기본 초기화입니다.

우리가 일반적으로 init 함수라고 부르는 초기화가 바로 designated 초기화입니다.

 

class ClassA {
    var num: Int
    var name: String
    
    init(num: Int, name: String) {
        self.num = num
        self.name = name
    }
}

designated 초기화는 클래스에서 정의된 모든 프로퍼티를 초기화하고,

 

class ChildA: ClassA {
    var age: Int
    
    init(age: Int) {
        self.age = age
        super.init(num: 10, name: "jeong")
    }
}

적절한 부모 클래스의 이니셜라이저를 호출하여 부모 클래스와 연관된 초기화 작업을 수행합니다

 

designated 초기화는 아래 두 가지 규칙을 지켜야 합니다.

  1. 해당 초기화가 종료되기 전까지, 초기화 안의 모든 프로퍼티가 초기화 돼야 함
  2. 자식 클래스인 경우, 반드시 부모 클래스의 designated 초기화를 호출해야 함

1번, 2번 모두 초기화의 역할을 수행하기 위해 필요한 규칙입니다.

 

특히 2번 규칙은 클래스가 상속이 가능하기 때문에 정해진 규칙인데요.

만약 부모 클래스의 designated 초기화를 호출하지 않는다면, 부모 클래스의 프로퍼티가 초기화되지 않을 가능성이 있습니다.

designated 초기화를 호출하면 모든 프로퍼티가 초기화 되었다는 보장이 가능하므로, 2번 규칙이 생긴거죠.

 

심지어 안전한 초기화를 위해 초기화 순서도 중요한데요.

자신의 모든 프로퍼티 초기화 후 부모 클래스의 초기화를 진행해야 합니다.

위 코드는 순서가 반대여서 컴파일 에러가 발생한 거에요.

(이와 관련된 2-Phase 초기화에 대해서는 아래에서 아주 간단하게 다루겠습니다.)

 

 

convenience Initializer

다음은 convenience 초기화입니다.

 

class ClassA {
    var num: Int
    var name: String
    
    init(num: Int, name: String) {
        self.num = num
        self.name = name
    }
    
    convenience init(num: Int) {
        self.init(num: num, name: "jeong")
    }
}

convenience 초기화는 designated 초기화의 파라미터 중 일부를 기본값으로 설정해서 호출할 수 있습니다.

convenience 초기화를 사용하면 초기화의 중복 코드를 줄일 수 있다는 장점이 있습니다.

 

convenience 초기화는 내부에서 반드시 다른 초기화를 호출해야 합니다.

그리고 최종적으로는 반드시 같은 클래스 내의 designated 초기화를 호출해야 합니다.

designated 초기화를 호출해야 모든 프로퍼티가 초기화된다는 보장이 가능하기 때문에

convenience 초기화는 반드시! 최종적으로 designated 초기화를 호출해야 합니다.

 

 

initializer delegation

이걸 그림으로 표현하면 아래와 같은데요.

타고 타고 올라가서 초기화가 진행되는 것을 initializer delegation이라고 합니다.

 

initializer delegation은 아래 두 가지 규칙을 지켜야 합니다.

  1. designated 초기화는 반드시 부모 클래스를 delegation 해야한다.
  2. convenience 초기화는 반드시 같은 레벨에서 delegate 해야한다.

1번 규칙은 부모 클래스로 화살표가 가는 것이고,

2번 규칙은 convenience 초기화가 수평으로 화살표를 보내는 것입니다.

 

형태는 좀 달라지더라도 이 규칙을 지켜야 합니다.

 

 

2-Phase 초기화

initializer delegation은 2-Phase 초기화에서 중요하게 사용됩니다.

 

Swift는 안전한 초기화를 위해 2단계로 나눠서 초기화를 진행하는데요.

1단계에서는 자식 클래스에서 부모 클래스로 올라가면서 값을 초기화하고,

2단계에서는 부모 클래스에서 자식 클래스로 내려오면 값을 커스텀합니다.

 

바로 위에서 super.init의 순서가 달라진다고 컴파일 에러가 발생했던 거 기억하시나요?

자식 클래스에서 부모 클래스로 올라가면서 모든 프로퍼티의 값을 초기화 해야 하는데,

super.init이 먼저 되면 자식 클래스의 프로퍼티는 초기화가 안 되겠죠.

그래서 안전하지 않다고 판단해서 컴파일 에러가 발생하는 것입니다.

 

 

구조체는 convenience 초기화가 없는 이유

구조체는 내부에서 다른 초기화를 호출해도 에러가 발생하지 않습니다.

같은 초기화를 호출해도 Warning이 뜰 뿐이지 에러는 나지 않아요.

 

하지만 클래스는 내부에서 다른 초기화를 호출하면 convenience 키워드를 붙이라는 컴파일 에러가 발생합니다.

 

이 차이는 상속 가능성때문에 발생합니다.

구조체는 상속을 할 수 없고, 클래스는 상속을 할 수 있죠.

 

위에서 알아본 것처럼 클래스는 연속적으로 init을 호출하면서 부모 클래스까지 올라갑니다.

근데 designated 초기화 안에서 또다른 designated 초기화를 호출하면 프로퍼티가 두 번 이상 초기화 될 위험이 있습니다.

그래서 안전한 초기화를 위해 Swift 내부적으로 convenience를 붙여야 init 안에서 또다른 init을 호출 할 수 있도록 컴파일 에러를 발생시키는 것입니다.

 

 

final 클래스도 convenience가 필요할까

이번에도 작은 궁금증이 생겼는데요.

final 클래스는 상속이 불가능하므로 convenience 키워드가 필요 없지 않을까요?

근데 이건 1차원적으로 생각한 거였어요.

 

final 클래스도 다른 클래스를 상속할 수 있기 때문에 부모 클래스의 init을 호출할 수 있습니다.

그러면 initializer delegation과 2-Phase 초기화가 발생하기 때문에 convenience 키워드가 필요합니다.

 

이렇게 보니 말도 안 되는 궁금증이었죠? ㅋㅋ

 

 

마무리

오늘은 멤버와이즈 초기화와 convenience 초기화에 집중해서 초기화에 대해 알아봤습니다.

초기화에 대해 더 많은 내용을 학습하고 싶으신 분은 공식 문서를 참고하면 유용할 듯 합니다.

 

감사합니다!

 

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

https://jeong9216.tistory.com/486#two-phase%C2%A0initialization

https://ios-development.tistory.com/1457

https://www.hackingwithswift.com/forums/100-days-of-swiftui/why-do-memberwise-initializers-change-to-a-private-access-level-when-a-property-of-its-struct-changes-to-private/15243


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형