포스트

이니셜라이저 알아보기(2)

지난 편에 이어 클래스의 이니셜라이저를 알아보겠습니다.

클래스 이니셜라이저

클래스는 구조체와 다르게 상속을 지원하기 때문에 초기화 로직이 조금 더 복잡하고, 지켜야 할 규칙도 몇 가지 있습니다. 추가되는 키워드도 있죠. 그래도 한 가지만 잘 기억해두신다면 어렵지 않게 사용하실 수 있습니다.

편의 이니셜라이저

구조체의 이니셜라이저에서 이니셜라이저 위임은 동일 구조체 내 다른 이니셜라이저를 호출함으로써 구현할 수 있었습니다. 하지만, 클래스의 이니셜라이저는 구조체의 이니셜라이저처럼 동일 객체 내 다른 이니셜라이저를 호출하려면 특별한 키워드가 필요합니다.

클래스의 이니셜라이저는 크게 두 종류로 구분됩니다. 하나는 지정 이니셜라이저(Designated Intializer)이고, 다른 하나는 편의 이니셜라이저(Convenience Intailizer)입니다.

지정 이니셜라이저는 클래스 내 모든 프로퍼티를 초기화하는 목적으로 사용됩니다. 그리고 상위 클래스의 지정 이니셜라이저도 호출해 상위 클래스의 프로퍼티도 초기화할 수 있습니다. 지정 이니셜라이저는 초기화 과정에서 뼈대와 같은 역할을 하므로 클래스 내 최소 하나 이상 필요합니다. 지정 이니셜라이저는 구조체의 이니셜라이저와 동일한 키워드로 생성할 수 있습니다.

편의 이니셜라이저는 지정 이니셜라이저를 도와주는 보조 이니셜라이저입니다. 초기화 로직에 살을 덧붙이거나, 과정을 단순하게 만들어주는 역할을 합니다. 편의 이니셜라이저는 지정 이니셜라이저와는 다르게 무조건 정의할 필요는 없습니다. 편의 이니셜라이저는 convenience 키워드로 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {
    var name: String
    var age: Int
    // 지정 이니셜라이저
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    // 편의 이니셜라이저
    convenience init() {
        self.init(name: "unknown", age: 19)
    }
    convenience init(name: String) {
        self.init(name: name, age: 19)
    }
}

class Student: Person {
    var grade: Int
    // 지정 이니셜라이저
    init(name: String, age: Int, grade: Int) {
        self.grade = grade
        super.init(name: name, age: age)
    }
    override init(name: String, age: Int) {
        self.grade = 1
        super.init(name: name, age: age)
    }
    // 편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, age: 19)
    }
}

위 예제 코드는 지정 이니셜라이저와 편의 이니셜라이저를 생성하는 방법을 보여줍니다.

지정 이니셜라이저와 편의 이니셜라이저를 사용하려면 아래 3가지 규칙을 무조건 따라야 합니다.

번호내용
편의 이니셜라이저는 동일한 클래스 내 다른 지정 혹은 편의 이니셜라이저를 반드시 호출해야 합니다.
편의 이니셜라이저는 궁극적으로 동일한 클래스 내 다른 지정 이니셜라이저를 반드시 호출해야 합니다.
하위 클래스의 지정 이니셜라이저는 상위 클래스의 지정 이니셜라이저를 반드시 호출해야 합니다.

위 예제 코드에서 Student 클래스 내 편의 이니셜라이저는 동일한 클래스 내 지정 이니셜라이저를 호출하고 있습니다. 그리고 해당 클래스 내 지정 이니셜라이저는 상위 클래스인 Person 클래스 내 지정 이니셜라이저를 호출하고 있습니다. 따라서 위 예제 코드는 클래스의 이니셜라이저 위임 규칙을 모두 충족한다고 볼 수 있습니다.

위 예제 코드의 초기화 흐름을 도식화하면 아래 그림과 같습니다.

1

2단계 초기화

클래스의 초기화는 2단계(Two-Phase)로 구분됩니다.

단계내용
①단계・ 각 클래스 내 프로퍼티를 초기화합니다.
・ 인스턴스 프로퍼티에 접근하거나, 인스턴스 메서드를 호출할 수 없습니다. 그리고 self 키워드도 사용할 수 없습니다.
・ 최상위 클래스의 프로퍼티까지 모두 초기화가 완료되면 2단계로 넘어갑니다.
②단계・ 각 클래스 내 프로퍼티에 임의로 값을 할당할 수 있습니다.
・ 인스턴스 프로퍼티에 접근하거나, 인스턴스 메서드를 호출할 수 있습니다. 그리고 self 키워드도 사용할 수 있습니다.
・ 최하위 클래스의 지정 혹은 편의 이니셜라이저까지 모두 실행을 마치면 초기화 과정이 종료됩니다.

아래 예제 코드를 살펴보면 2단계 초기화 단계를 명확히 구분할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
    var name: String
    var age: Int
    // 지정 이니셜라이저
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        // ⬆️ 1단계 초기화
    }
    func sayHello(to name: String) {
        print("Hello, \(name)!")
    }
}

class Student: Person {
    var grade: Int
    // 지정 이니셜라이저
    init(name: String, age: Int, grade: Int) {
        self.grade = grade
        // ⬆️ 1단계 초기화
        super.init(name: name, age: age)
        // ⬇️ 2단계 초기화
        self.age = 19
        sayHello(to: "김문어")
    }
}

Student 클래스의 지정 이니셜라이저에서 super.init(name: name, age: age) 구문을 기점으로 단계가 구분되어 있는 모습을 볼 수 있습니다. 그리고 Swift는 이러한 초기화 과정을 조금 더 안정하게 처리하기 위해 4가지 안전 제약(Safety-Check)을 두고 있습니다.

번호내용
하위 클래스의 지정 이니셜라이저는 상위 클래스의 지정 이니셜라이저를 호출하기 전에 반드시 하위 클래스의 모든 프로퍼티를 초기화해야 합니다.
하위 클래스의 지정 이니셜라이저는 상위 클래스의 프로퍼티에 임의로 값을 할당하기 전 반드시 상위 클래스의 지정 이니셜라이저를 호출해야 합니다.
편의 이니셜라이저는 상위 혹은 동일 클래스의 프로퍼티에 임의로 값을 할당하기 전 반드시 동일 클래스의 지정 혹은 편의 이니셜라이저를 호출해야 합니다.
1단계 초기화를 끝마치기 전까지 모든 이니셜라이저는 인스턴스 프로퍼티에 접근하거나, 인스턴스 메서드를 호출할 수 없습니다. 그리고 self 키워드도 사용할 수 없습니다.

각 안전 제약을 위반하는 경우를 하나씩 살펴보겠습니다.

안전 제약 ①

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int
    init(name: String, age: Int, grade: Int) {
        super.init(name: name, age: age)
    }
}

위 예제 코드는 하위 클래스의 grade 프로퍼티를 초기화하지 않고, 슈퍼 클래스의 지정 이니셜라이저를 호출하려고 시도해 제약을 위반하였습니다.

안전 제약 ②

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int
    init(name: String, age: Int, grade: Int) {
        self.grade = grade
        self.name = "김문어"
        super.init(name: name, age: age)
    }
}

위 예제 코드는 하위 클래스의 지정 이니셜라이저가 슈퍼 클래스의 지정 이니셜라이저를 호출하기 전에 name 프로퍼티에 임의의 값을 할당하려고 시도해 제약을 위반하였습니다.

안전 제약 ③

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int
    init(name: String, age: Int, grade: Int) {
        self.grade = grade
        super.init(name: name, age: age)
    }
    override init(name: String, age: Int) {
        self.grade = 1
        super.init(name: name, age: age)
    }
    convenience init(name: String) {
        self.grade = 1
        self.init(name: name, age: 19)
    }
}

위 예제 코드는 하위 클래스의 편의 이니셜라이저가 동일한 클래스의 다른 지정 혹은 편의 이니셜라이저를 호출하기 전에 grade 프로퍼티에 임의의 값을 할당하려고 시도해 제약을 위반하였습니다.

안전 제약 ④

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int
    
    init(name: String, age: Int, grade: Int) {
        self.sayHello(to: "김문어")
        print("GoodBye, \(self.name)!")

        self.grade = grade
        super.init(name: name, age: age)
    }
    func sayHello(to name: String) {
        print("Hello, \(name)!")
    }
}

위 예제 코드는 1단계 초기화를 완료하기 전에 인스턴스 프로퍼티 혹은 인스턴스 메서드를 호출하려고 시도해 제약을 위반하였습니다.

이니셜라이저 상속

Swift는 기본적으로 하위 클래스가 상위 클래스의 이니셜라이저를 상속하지 않습니다. 다만, 아래 조건을 충족한다면 하위 클래스는 상위 클래스로부터 이니셜라이저를 자동으로 상속받을 수 있습니다.

번호내용
하위 클래스에 별도 지정 이니셜라이저를 정의하지 않은 경우
하위 클래스에 프로퍼티를 추가로 정의하지 않거나, 추가로 정의한 프로퍼티가 이미 초기값을 가지고 있는 경우

즉, 하위 클래스에서 추가로 초기화를 해줘야 할 프로퍼티가 없다면 상위 클래스의 지정 이니셜라이저를 자동으로 상속받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int = 1
    //override init(name: String, age: Int) {
    //    self.name = name
    //    self.age = age
    //}
}

var student = Student(name: "김문어", age: 15)

위 예제 코드는 하위 클래스에 아무런 지정 이니셜라이저를 정의하지 않았음에도 상위 클래스로부터 상속 받은 지정 이니셜라이저로 인스턴스를 생성하고 있는 모습을 보여주고 있습니다.

아래 조건을 충족한다면 지정 이니셜라이저뿐만 아니라 편의 이니셜라이저도 상속받을 수 있습니다.

번호내용
하위 클래스에서 상위 클래스의 지정 이니셜라이저를 자동으로 상속받은 경우
하위 클래스가 상위 클래스의 모든 지정 이니셜라이저를 오버라이딩을 하는 경우

이때, 상위 클래스의 이정 이니셜라이저를 하위 클래스에서 편의 이니셜라이저로 오버라이딩을 하는 경우에도 위 조건을 충족할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    convenience init(name: String) {
        self.init(name: name, age: 1)
    }
}

class Student: Person {
    var grade: Int = 1
    override init(name: String, age: Int) {
        super.init(name: name, age: age)
    }
    //convenience init(name: String) {
    //    self.init(name: name, age: 1)
    //}
}

var student = Student(name: "김문어")

위 예제 코드는 하위 클래스가 상위 클래스의 모든 지정 이니셜라이저를 오버라이딩을 한 결과로, 하위 클래스는 상위 클래스로부터 상속 받은 편의 이니셜라이저로 인스턴스를 생성하고 있는 모습을 보여주고 있습니다.

추가로, 하위 클래스는 상위 클래스의 실패 가능한 이니셜라이저도 상속받을 수 있습니다. 하위 클래스는 실패 가능한 이니셜라이저 혹은 실패하지 않는 이니셜라이저로 모두 오버라이딩이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person {
    var name: String
    var age: Int
    // 실패 가능한 이니셜라이저
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
        self.age = 1
    }
    // 실패 가능한 이니셜라이저
    init?(name: String, age: Int) {
        if name.isEmpty, age < 0 {
            return nil
        }
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int = 1
    override init(name: String) {
        super.init(name: name)!
    }
    // 실패 가능한 이니셜라이저
    override init?(name: String, age: Int) {
        super.init(name: name, age: age)
    }
}

필수 이니셜라이저

상위 클래스에서 필수 이니셜라이저(Required Intializer)를 정의하면, 해당 클래스를 상속 받는 하위 클래스는 해당 이니셜라이저를 반드시 오버라이딩해야 합니다. 지정 이니셜라이저와 다르게 필수 이니셜라이저를 하위 클래스에서 오버라이딩을 한다면 override가 아닌 required 키워드를 사용해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
    var name: String
    var age: Int
    // 필수 이니셜라이저
    required init() {
        self.name = "(알 수 없음)"
        self.age = 1
    }
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var grade: Int = 1
    // 필수 이니셜라이저
    required init() {
        super.init()
    }
    override init(name: String, age: Int) {
        super.init(name: name, age: age)
    }
}

출처

참고 자료

■ 야곰, ⌜스위프트 프로그래밍⌟, 한빛미디어, P.342~360, 20230909 ■ 애플 공식 개발자 문서, ⌜The Swift Programming Language⌟, 공식 페이지, 20230909

이미지

■ 애플 공식 문서, ⌜The Swift Programming Language, 공식 홈페이지

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.