본문 바로가기
iOS/SWIFT

[Swift] Chapter 18. 상속

by 원만사 2021. 10. 10.
반응형

클래스는 메서드나 프로퍼티 등을 다른 클래스로부터 상속받을 수 있다. 어떤 클래스로부터 상속을 받으면 상속받은 클래스는 그 어떤 클래스의 자식클래스라고 표현한다. 자식클래스에게 자신의 특성을 물려준 클래스를 부모클래스라고 표현한다.

스위프트의 클래스는 부모클래스로부터 물려받은 메서드를 호출할 수 있고 프로퍼티에 접근할 수 있으며 서브스크립트도 사용할 수 있다. 또, 부모클래스로부터 물려받은 메서드, 프로퍼티, 서브스크립트 등을 자신만의 내용으로 재정의할 수도 있다. 스위프트는 부모클래스의 요소를 자식클래스에서 재정의할 때 자식클래스가 부모클래스의 요소들을 재정의한다는 것을 명확히 확인해주어야 한다.

상속받은 프로퍼티에 프로퍼티의 값이 변경되었을 때 알려주는 프로퍼티 감시자도 구현할 수 있다. 연산 프로퍼티를 정의해준 클래스에서는 연산 프로퍼티에 프로퍼티 감시자를 구현할 수 없지만, 부모클래스에서 연산 프로퍼티로 정의한 프로퍼티든 저장 프로퍼티로 정의한 프로퍼티든 자식클래스에서는 프로퍼티 감시자를 구현할 수 있다.

다른 클래스로부터 상속을 받지 않은 클래스를 기반클래스(Base Class)라고 부른다.

 

/*
	코드 18-1. 기반클래스 Person
*/

class Person {
    var name: String = ""
    var age: Int = 0
    
    var introduction: String {
        return "이름 : \(name). 나이 : \(age)"
    }
    
    func speak() {
        print("가나다라마바사")
    }
}

let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 99
print(yagom.introduction) // 이름 : yagom. 나이 : 99
yagom.speak() // 가나다라마바사

[코드 18-1]의 Person 클래스는 다른 클래스를 상속받지 않으므로 기반클래스로 부를 수 있다. 상속을 통해 기반클래스인 Person 클래스보다 세분화된 특징이 있고, 더 많은 기능을 실행할 수 있는 새로운 자식클래스를 만들어 줄 수 있다.

 

18.1 클래스 상속


상속은 기반클래스를 다른 클래스에서 물려받는 것을 말한다. 부모클래스의 메서드, 프로퍼티 등을 재정의하거나, 기반 클래스의 기능이나 프로퍼티를 물려받고 자신의 기능을 추가할 수 있다.

클래스 이름 뒤에 콜론을 붙이고 다른 클래스 이름을 써주면 뒤에 오는 클래스의 기능을 앞의 클래스가 상속받을 것임을 뜻한다.

class 클래스 이름: 부모클래스 이름 {
	프로퍼티와 메서드들
}

 

/*
	코드 18-2. Person 클래스를 상속받은 Student 클래스
*/

class Student: Person {
    var grade: String = "F"
    
    func study() {
        print("Study hard...")
    }
}

let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 99
print(yagom.introduction) // 이름 : yagom. 나이 : 99
yagom.speak() // 가나다라마바사


let jay: Student = Student()
jay.name = "jay"
jay.age = 10
jay.grade = "A"
print(jay.introduction) // 이름 : jay. 나이 : 10
jay.speak()  // 가나다라마바사
jay.study()  // Study hard...

[코드 18-2]에서 Student 클래스는 [코드 18-1] Person 클래스를 상속받았기 때문에 부모 클래스가 물려준 프로퍼티와 메서드를 사용할 수 있으며 자신이 정의한 프로퍼티와 메서드도 사용할 수 있다.

[코드 18-3]처럼 Person 클래스를 상속받은 Student 클래스는 다시 다른 클래스가 상속할 수 있다. 즉, 어떤 클래스의 자식클래스가 다른 클래스의 부모클래스가 될 수 있다.

 

/*
	코드 18-3. Student 클래스를 상속받은 UniversityStudent 클래스
*/

class UniversityStudent: Student {
    var major: String = ""
}

let jenny: UniversityStudent = UniversityStudent()
jenny.major = "Art"
jenny.speak() // 가나다라마바사
jenny.study() // Study hard...

 

Person 클래스를 상속받은 Student 클래스는 Person의 인스턴스 메서드, 타입 메서드, 인스턴스 프로퍼티, 타입 프로퍼티, 서브스크립트 등 모든 특성을 포함하며 Student를 상속받은 UniversityStudent 클래스는 Person과 Student가 갖는 모든 특성을 포함한다.

 

다른 클래스를 상속받으면 똑같은 기능을 구현하기 위하여 코드를 다시 작성할 필요가 없으므로 코드를 재사용하기 용이하고 더불어 기능을 확장할 때 기존 클래스를 변경하지 않고도 새로운 추가 기능을 구현한 클래스를 정의할 수 있다.

 

18.2 재정의


자식클래스는 부모클래스로부터 물려받은 특성(인스턴스 메서드, 타입 메서드, 인스턴스 프로퍼티, 타입 프로퍼티, 서브스크립트 등)을 그대로 사용하지 않고 자신만의 기능으로 변경하여 사용할 수 있다. 이를 재정의(Override)라고 한다.

상속받은 특성들을 재정의하려면 새로운 정의 앞에 override라는 키워드를 사용한다. override 키워드는 스위프트 컴파일러가 조상클래스(부모를 포함한 그 상위 부모클래스)에 해당 특성이 있는지 확인한 후 재정의하게 된다. 만약 조상클래스에 해당 특성이 없는데 override 키워드를 사용하면 컴파일 오류가 발생한다.

 

만약 자식클래스에서 부모클래스의 특성을 재정의했을 때, 부모클래스의 특성을 자식클래스에서 사용하고 싶다면 super 프로퍼티를 사용하면 된다. super 키워드를 타입 메서드 내에서 사용한다면, 부모클래스의 타입 메서드와 타입 프로퍼티에 접근할 수 있으며 인스턴스 메서드 내에서 사용한다면, 부모클래스의 인스턴스 메서드와 인스턴스 프로퍼티, 서브스크립트에 접근할 수 있다.

 

18.2.1 메서드 재정의

부모클래스로부터 상속받은 인스턴스 메서드나 타입 메서드를 자식클래스에서 용도에 맞도록 재정의할 수 있다.

 

/*
	코드 18-4. 메서드 재정의
*/

class Person {
    var name: String = ""
    var age: Int = 0
    
    var introduction: String {
        return "이름 : \(name). 나이 : \(age)"
    }
    
    func speak() {
        print("가나다라마바사")
    }
    
    class func introduceClass() -> String {
        return "인류의 소원은 평화입니다."
    }
}

class Student: Person {
    var grade: String = "F"
    
    func study() {
        print("Study hard...")
    }
    
    override func speak() {
        print("저는 학생입니다.")
    }
}

class UniversityStudent: Student {
    var major: String = ""
    
    class func introductClass() {
        print(super.introduceClass())
    }
    
    override class func introduceClass() -> String {
         return "대학생의 소원은 A+입니다."
    }
    
    override func speak() {
        super.speak()
        print("대학생이죠.")
    }
}

let yagom: Person = Person()
yagom.speak() // 가나다라마바사

let jay: Student = Student()
jay.speak() // 저는 학생입니다.

let jenny: UniversityStudent = UniversityStudent()
jenny.speak() // 저는 학생입니다. 대학생이죠.

print(Person.introduceClass()) // 인류의 소원은 평화입니다.
print(Student.introduceClass()) // 인류의 소원은 평화입니다.
print(UniversityStudent.introduceClass() as String) // 대학생의 소원은 A+입니다.
//UniversityStudent.introduceClass() as Void // 인류의 소원은 평화입니다.

 

18.2.2 프로퍼티 재정의

부모클래스로부터 상속받은 인스턴스 프로퍼티나 타입 프로퍼티를 자식 클래스에서 용도에 맞게 재정의할 수 있다. 프로퍼티를 재정의할 때는 저장 프로퍼티로 재정의할 수는 없다. 프로퍼티를 재정의한다는 것은 프로퍼티 자체가 아니라 프로퍼티의 접근자(Getter), 설정자(Setter), 프로퍼티 감시자(Property Observer)등을 재정의하는 것을 의미한다.

조상클래스에서 저장 프로퍼티로 정의한 프로퍼티는 물론이고 연산 프로퍼티로 정의한 프로퍼티도 접근자와 설정자를 재정의할 수 있다. 재정의하려는 프로퍼티는 조상클래스 프로퍼티의 이름과 타입이 일치해야 한다. 만약 조상클래스에 없는 프로퍼티를 재정의하려고 한다면 메서드와 마찬가지로 컴파일 오류가 발생한다.

조상클래스에서 읽기 전용 프로퍼티였더라도 자식클래스에서 읽고 쓰기가 가능한 프로퍼티로 재정의해줄 수도 있다. 그러나, 읽기 쓰기 모두 가능했던 프로퍼티를 읽기 전용으로 재정의해줄 수는 없다.

읽기 쓰기가 모두 가능한 프로퍼티를 재정의할 때 설정자만 따로 재정의할 수는 없다. 즉, 접근자와 설정자를 모두 재정의해야 한다. 만약 접근자에 따로 기능 변경이 필요 없다면 super.someProperty와 같은 식으로 부모클래스의 접근자를 사용하여 값을 받아와 반환해주면 된다.

 

/*
	코드 18-5. 프로퍼티 재정의
*/

class Person {
    var name: String = ""
    var age: Int = 0
    var koreanAge: Int {
        return self.age + 1
    }
    
    var introduction: String {
        return "이름 : \(name). 나이 : \(age)"
    }
}

class Student: Person {
    var grade: String = "F"
    
    override var introduction: String {
        return super.introduction + " " + "학점 : \(self.grade)"
    }
    
    override var koreanAge: Int {
        get {
            return super.koreanAge
        }
        
        set {
            self.age = newValue - 1
        }
    }
}

let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 55
//yagom.koreanAge = 56 // 오류 발생
print(yagom.introduction) // 이름 : yagom. 나이 : 55
print(yagom.koreanAge) // 56

let jay: Student = Student()
jay.name = "jay"
jay.age = 14
jay.koreanAge = 15
print(jay.introduction) // 이름 : jay. 나이 : 14 학점 : F
print(jay.koreanAge) // 15

[코드 18-5]의 Student 클래스에서는 Person 클래스에서 상속받은 introduction과 koreanAge라는 연산 프로퍼티를 재정의했다. 읽기 전용이었던 koreanAge 프로퍼티는 읽기와 쓰기가 모두 가능하도록 재정의했고, introduction은 학생의 학점 정보를 추가하도록 재정의했다.

 

 

18.2.3 프로퍼티 감시자 재정의

프로퍼티 감시자도 재정의할 수 있다. 또 조상클래스에 정의한 프로퍼티가 연산 프로퍼티인지 저장 프로퍼티인지는 상관없다. 다만 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티는 프로퍼티 감시자를 재정의할 수 없다. 왜냐하면 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티는 값을 설정할 수 없으므로 willSet이나 didSet 메서드를 사용한 프로퍼티 감시자를 원천적으로 사용할 수 없기 때문이다. 또, 프로퍼티 감시자를 재정의하더라도 조상클래스에 정의한 프로퍼티 감시자도 동작한다는 점을 잊으면 안된다.

프로퍼티의 접근자와 프로퍼티 감시자는 동시에 재정의할 수 없다. 만약 둘 다 동작하길 원한다면 재정의하는 접근자에 프로퍼티 감시자의 역할을 구현해야 한다.

 

/*
	코드 18-6. 프로퍼티 감시자 재정의
*/

class Person {
    var name: String = ""
    var age: Int = 0 {
        didSet {
            print("Person age : \(self.age)")
        }
    }
    
    var koreanAge: Int {
        return self.age + 1
    }

    var fullName: String {
        get {
            return self.name
        }
        
        set {
            self.name = newValue
        }
    }
}

class Student: Person {
    var grade: String = "F"
    
    override var age: Int {
        didSet {
            print("Student age : \(self.age)")
        }
    }
    
    override var koreanAge: Int {
        get {
            return super.koreanAge
        }
        
        set {
            self.age = newValue - 1
        }
        
//        didSet { } // 오류 발생
    }
    
    override var fullName: String {
        didSet {
            print("Full Name : \(self.fullName)")
        }
    }
}

let yagom: Person = Person()
yagom.name = "yagom"
yagom.age = 55 // Person age : 55
yagom.fullName = "Jo yagom"
print(yagom.koreanAge) // 56

let jay: Student = Student()
jay.name = "jay"
jay.age = 14
// Person age : 14
// Student age : 14
jay.koreanAge = 15
// Person age : 14
// Student age : 14
jay.fullName = "Kim jay" // Full Name : Kim jay
print(jay.koreanAge) // 15

 

18.2.4 서브스크립트 재정의

서브스크립트도 매개변수와 반환 타입이 다르면 다른 서브스크립트로 취급하므로, 자식클래스에서 재정의하려는 서브스크립트라면 부모클래스 서브스크립트의 매개변수와 반환 타입이 같아야 한다.

/*
	코드 18-7. 서브스크립트 재정의
*/

class School {
    var students: [Student] = [Student]()
    
    subscript(number: Int) -> Student {
        print("School subscript")
        return students[number]
    }
}

class MiddleSchool: School {
    var middleStudents: [Student] = [Student]()
    
    // 부모클래스(School)에게 상속받은 서브스크립트 재정의
    override subscript(index: Int) -> Student {
        print("MiddleSchool subscript")
        return middleStudents[index]
    }
}

let university: School = School()
university.students.append(Student())
university[0] // School subscript

let middle: MiddleSchool = MiddleSchool()
middle.middleStudents.append(Student())
middle[0] // MiddleSchool subscript

 

18.2.5 재정의 방지

몇몇 특성을 재정의할 수 없도록 제한하고 싶다면 재정의를 방지하고 싶은 특성 앞에 final 키워드를 명시하면 된다. 재정의를 방지한 특성을 자식클래스에서 재정의하려고 하면 컴파일 오류가 발생한다.

만약 클래스를 상속하거나 재정의할 수 없도록 하고 싶다면 class 키워드 앞에 final 키워드를 명시해주면 된다. 그렇게 하면 더 이상 자식 클래스를 가질 수 없다. 상속이 방지된 클래스를 다른 클래스가 상속받으려고 하면 컴파일 오류가 발생한다.

 

/*
	코드 18-8. final 키워드의 사용
*/

class Person {
    final var name: String = ""
    
    final func speak() {
        print("가나다라마바사")
    }
}

final class Student: Person {
    // 오류! Person의 name은 final을 사용하여
    // 재정의를 할 수 없도록 했기 때문
    override var name: String {
        set {
            super.name = newValue
        }

        get {
            return "학생"
        }
    }
    
    // 오류! Person의 speak()는 final을 사용하여
    // 재정의를 할 수 없도록 했기 때문
    override func speak() {
        print("저는 학생입니다.")
    }
}


// 오류!
// Student 클래스는 final을 사용하여
// 상속할 수 없도록 했기 때문
class UniversityStudent: Student { }

 

18.3 클래스의 이니셜라이저 - 상속과 재정의


값 타입의 이니셜라이저는 이니셜라이저 위임을 위해 이니셜라이저끼리 구분할 필요가 없었지만 클래스에서는 지정 이니셜라이저와 편의 이니셜라이저로 역할을 구분한다. 또, 값 타입의 이니셜라이저는 상속을 고려할 필요가 없었지만 클래스는 상속이 가능하므로 상속받았을 때 이니셜라이저를 어떻게 재정의하는지도 큰 관건이다.

 

18.3.1 지정 이니셜라이저와 편의 이니셜라이저

지정 이니셜라이저(Designated Initializer)는 클래스의 주요 이니셜라이저다. 필요에 따라 부모클래스의 이니셜라이저를 호출할 수 있으며, 이시녈라이저가 정의된 클래스의 모든 프로퍼티를 초기화해야 하는 임무를 갖고 있다. 클래스의 이니셜라이저 중 기둥과 같은 역할을 하므로 클래스에 하나 이상 정의한다.

모든 클래스는 하나 이상의 지정 이니셜라이저를 갖는 다. 만약 조상클래스에서 지정 이니셜라이저가 자손클래스의 지정 이니셜라이저 역할을 충분히 할 수 있다면, 자손클래스는 지정 이니셜라이저를 갖지 않을 수도 있다. 이런 경우는 조상클래스로부터 물려받은 프로퍼티를 제외하고 옵셔널 저장 프로퍼티 외에 다른 저장 프로퍼티가 없을 가능성이 크다.

편의 이니셜라이저(Convenience Initializer)는 초기화를 좀 더 손쉽게 도와주는 역할을 한다. 편의 이니셜라이저는 지정 이니셜라이저를 자신 내부에서 호출한다. 지정 이니셜라이저의 매개변수가 많아 외부에서 일일이 전달인자를 전달하기 어렵거나 특정 목적에 사용하기 위해서 편의 이니셜라이저를 설계할 수도 있다.

편의 이니셜라이저는 필수 요소는 아니다. 다만 클래스 설계자의 의도대로 외부에서 사용하길 원하거나 인스턴스 생성 코드를 작성하는 수고를 덜 때 유용하게 사용할 수 있다.

지정 이니셜라이저는 값 타입 이니셜라이저를 정의할 때와 같은 형식으로 정의할 수 있다.

init(매개변수들) {
	초기화 구문
}

편의 이니셜라이저는 앞에 convenience 지정자를 init 키워드 앞에 명시해주면 된다.

convenience init(매개변수들) {
	초기화 구문
}

 

18.3.2 클래스의 초기화 위임

지정 이니셜라이저와 편의 이니셜라이저 사이의 관계를 정리해보기 위해 세 가지 규칙을 적용해 볼 수 있다.

  1. 자식클래스의 지정 이니셜라이저는 부모클래스의 지정 이니셜라이저를 반드시 호출해야 한다.
  1. 편의 이니셜라이저는 자신을 정의한 클래스의 다른 이니셜라이저를 반드시 호출해야 한다.
  1. 편의 이니셜라이저는 궁극적으로는 지정 이니셜라이저를 반드시 호출해야 한다.

 

다음처럼 생각해볼 수도 있다. '누군가'는 다른 지정 이니셜라이저 또는 편의 이니셜라이저를 뜻한다.

  • 누군가는 지정 이니셜라이저에게 초기화를 반드시 위임한다.
  • 편의 이니셜라이저는 초기화를 반드시 누군가에게 위임한다.

 

18.3.3 2단계 초기화

스위프트의 클래스 초기화는 2단계를 거친다. 1단계는 클래스에 정의한 각각의 저장 프로퍼티에 초깃값이 할당된다. 모든 저장 프로퍼티의 초기 상태가 결정되면 2단계로 돌입해 저장 프로퍼티들을 사용자 정의할 기회를 얻는다. 그 후 비로소 새로운 인스턴스를 사용할 준비가 끝난다.

2단계 초기화는 프로퍼티를 초기화하기 전에 프로퍼티 값에 접근하는 것을 막아 초기화를 안전하게 할 수 있도록 해준다. 또, 다른 이니셜라이저가 프로퍼티의 값을 실수로 변경하는 것을 방지할 수도 있다.

스위프트 컴파일러는 2단계 초기화를 오류 없이 처리하기 위해 다음과 같은 네 가지 안전확인(Safety-check)을 실행한다.

  1. 자식클래스의 지정 이니셜라이저가 부모클래스의 이니셜라이저를 호출하기 전에 자신의 프로퍼티를 모두 초기화했는지 확인한다.
  1. 자식클래스의 지정 이니셜라이저는 상속받은 프로퍼티에 값을 할당하기 전에 반드시 부모클래스의 이니셜라이저를 호출해야 한다.
  1. 편의 이니셜라이저는 자신의 클래스에 정의한 프로퍼티를 포함하여 그 어떤 프로퍼티라도 값을 할당하기 전에 다른 이니셜라이저를 호출해야 한다.
  1. 초기화 1단계를 마치기 전까지는 이니셜라이저는 인스턴스 메서드를 호출할 수 없다. 또, 인스턴스 프로퍼티의 값을 읽어들일 수도 없다. self 프로퍼티를 자신의 인스턴스를 나타내는 값으로 활용할 수도 없다.

클래스의 인스턴스는 초기화 1단계를 마치기 전까지는 아직 유효하지 않다. 프로퍼티는 읽기만 가능하며, 메서드는 호출될 수 있을 뿐이다. 클래스의 인스턴스가 초기화 1단계를 마쳤을 때 비로소 유효한 인스턴스가 되는 것이다.

네 가지 안전확인에 근거하여 어떻게 2단계 초기화가 이루어지는지 살펴보자.

 

1단계

  1. 클래스가 지정 또는 편의 이니셜라이저를 호출한다.
  1. 그 클래스의 새로운 인스턴스를 위한 메모리가 할당된다. 메모리는 아직 초기화되지 않은 상태이다.
  1. 지정 이니셜라이저는 클래스에 정의된 모든 저장 프로퍼티에 값이 있는지 확인한다. 현재 클래스 부분까지의 저장 프로퍼티를 위한 메모리는 이제 초기화되었다.
  1. 지정 이니셜라이저는 부모클래스의 이니셜라이저가 같은 동작을 행할 수 있도록 초기화를 양도한다.
  1. 부모클래스는 상속 체인을 따라 최상위 클래스에 도달할 때까지 이 작업을 반복한다.

 

최상위 클래스에 도달했을 때, 최상위 클래스까지의 모든 저장 프로퍼티에 값이 있다고 확인하면 해당 인스턴스의 메모리는 모두 초기화된 것이다. 이로써 1단계가 완료되었다.

 

2단계

  1. 최상위 클래스로부터 최하위 클래스까지 상속 체인을 따라 내려오면서 지정 이니셜라이저들이 인스턴스를 제 각각 사용자 정의하게 된다. 이 단계에서는 self를 통해 프로퍼티 값을 수정할 수 있고, 인스턴스 메서드를 호출하는 등의 작업을 진행할 수 있다.
  1. 마지막으로 각각의 편의 이니셜라이저를 통해 self를 통한 사용자 정의 작업을 진행할 수 있다.
/*
	코드 18-9. Person 클래스를 상속받은 Student 클래스
*/

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Student: Person {
    var major: String
    
    init(name: String, age: Int, major: String) {
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 7, major: "")
    }
}

 

[코드 18-9] Student 클래스의 지정 이니셜라이저(init(name:age:major))는 부모클래스의 지정 이니셜라이저를 호출하기 전에 자신의 self 프로퍼티를 이용해 major 프로퍼티의 값을 할당한다. 그렇게 하면 안전확인 중 1번의 조건에 만족한다. 그리고 super.init(name: name, age: age)를 통해 부모클래스의 이니셜라이저를 호출했으며 그 외에 상속받은 프로퍼티가 없으므로 부모의 이니셜라이저 호출 이후에 값을 할당해줄 프로퍼티가 없다. 따라서 2번의 조건을 갖추었다. 또, 편의 이니셜라이저인 convenience init(name:)은 따로 차후에 값을 할당할 프로퍼티가 없고, 다른 이니셜라이저를 호출했으므로 3번 조건에 부합한다. 마지막으로 이니셜라이저 어디에서도 인스턴스 메서드를 호출하거나 인스턴스 프로퍼티의 값을 읽어오지 않았으므로 4번 조건도 충족한다.

안전확인 후 super.init(name: name, age: age)를 통해 1단계와 2단계의 초기화까지 마치게 된다.

 

18.3.4 이니셜라이저 상속 및 재정의

기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않는다. 부모클래스로부터 물려받은 이니셜라이저는 자식클래스에 최적화되어 있지 않아서, 부모클래스의 이니셜라이저를 사용했을 때 자식클래스의 새로운 인스턴스가 완전하고 정확하게 초기화 되지 않는 상황을 방지하고자 함이다. 안전하고 적절하다고 판단되는 특정한 상황에서는 부모클래스의 이니셜라이저가 상속되고 한다.

보통 부모클래스의 이니셜라이저와 똑같은 이니셜라이저를 자식클래스에서 사용하고 싶다면 자식클래스에서 부모의 이니셜라이저와 똑같은 이니셜라이저를 구현해주면 된다.

부모클래스와 동일한 지정 이니셜라이저를 자식클래스에서 구현해주려면 재정의하면 된다. 그러려면 override 수식어를 붙여야 한다. 클래스에 주어지는 기본 이니셜라이저를 재정의 할 때도 마찬가지다. 자식클래스의 편의 이니셜라이저가 부모클래스의 지정 이니셜라이저를 재정의하는 경우에도 override 수식어를 붙여준다.

반대로 부모클래스의 편의 이니셜라이저와 동일한 이니셜라이저를 자식클래스에 구현할 때는 override 수식어를 붙이지 않는다. 자식클래스에서 부모클래스의 편의 이니셜라이저는 절대로 호출할 수 없기 때문이다. 즉, 재정의할 필요가 없다.

 

/*
	코드 18-10. 클래스 이니셜라이저의 재정의
*/

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: 0)
    }
}

class Student: Person {
    var major: String
    
    override init(name: String, age: Int) {
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 7)
    }
}

 

[코드 18-10]을 보면 Person 클래스를 상속받은 Student 클래스에서 부모클래스의 편의 이니셜라이저와 동일한 편의 이니셜라이저를 정의할 때 override 수식어를 붙이지 않은 것을 볼 수 있다. 반대로 지정 이니셜라이저는 재정의를 위해 override 수식어를 사용한 것을 볼 수 있다. 기본 이니셜라이저 외에 지정 이니셜라이저를 자식클래스에서 동일한 이름으로 정의하려면 재정의를 위한 override 수식어를 명시해주어야 한다.

부모클래스의 실패 가능한 이니셜라이저를 자식클래스에서 재정의하고 싶을 때는 실패 가능한 이니셜라이저로 재정의해도 되고 필요에 따라서 실패하지 않는 이니셜라이저로 재정의해줄수도 있다.

 

/*
	코드 18-11. 실패 가능한 이니셜라이저의 재정의
*/

class Person {
    var name: String
    var age: Int
    
    init() {
        self.name = "Unknown"
        self.age = 0
    }
    
    init?(name: String, age: Int) {
        
        if name.isEmpty {
            return nil
        }
        
        self.name = name
        self.age = age
    }
    
    init?(age: Int) {
        if age < 0 {
            return nil
        }
        
        self.name = "Unknown"
        self.age = age
    }
}

class Student: Person {
    var major: String
    
    override init?(name: String, age: Int) {
        self.major = "Swift"
        super.init(name: name, age: age)
    }
    
    override init(age: Int) {
        self.major = "Swift"
        super.init()
    }
}

[코드 18-11]과 같이 부모클래스에서는 실패 가능한 이니셜라이저였더라도 자식클래스에서는 필요에 따라 실패하지 않는 이니셜라이저로 재정의해줄 수 있다.

 

18.3.5 이니셜라이저 자동 상속

기본적으로 스위프트의 이니셜라이저는 부모클래스의 이니셜라이저를 상속받지 않으나 특정 조건에 부합한다면 부모클래스의 이니셜라이저가 자동으로 상속된다. 사실, 대부분의 경우 자식클래스에서 이니셜라이저를 재정의해줄 필요가 없다.

자식클래스에서 프로퍼티 기본값을 모두 제공한다고 가정할 때, 다음 두 가지 규칙에 따라 이니셜라이저가 자동으로 상속된다.

  • 규칙 1 : 자식클래스에서 별도의 지정 이니셜라이저를 구현하지 않는다면, 부모클래스의 지정 이니셜라이저가 자동으로 상속된다.
  • 규칙 2 : 만약 규칙 1에 따라 자식클래스에서 부모클래스의 지정 이니셜라이저를 자동으로 상속받은 경우 또는 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스와 동일한 지정 이니셜라이저를 모두 사용할 수 있는 상황이라면 부모클래스의 편의 이니셜라이저가 모두 자동으로 상속된다.

 

/*
	코드 18-12. 이니셜라이저 자동 상속
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "Unknown")
    }
}

class Student: Person {
    var major: String = "Swift"
}

// 부모클래스의 지정 이니셜라이저 자동 상속
let yagom: Person = Person(name: "yagom")
let hana: Student = Student(name: "hana")
print(yagom.name) // yagom
print(hana.name) // hana

// 부모클래스의 편의 이니셜라이저 자동 상속
let wizplan: Person = Person()
let jinSung: Student = Student()
print(wizplan.name) // Unknown
print(jinSung.name) // Unknown

[코드 18-12]를 보면 Student의 major 프로퍼티에 기본값이 있으며, 따로 지정 이니셜라이저를 구현해주지 않았으므로 부모클래스인 Person 클래스의 지정 이니셜라이저가 자동으로 상속된다. 이는 규칙 1에 부합한다. 또, 부모클래스의 지정 이니셜라이저를 모두 자동으로 상속받았으므로 편의 이니셜라이저도 자동으로 상속되었다.

 

/*
	코드 18-13. 편의 이니셜라이저 자동 상속
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "Unknown")
    }
}

class Student: Person {
    var major: String
    
    override init(name: String) {
        self.major = "Unknown"
        super.init(name: name)
    }
    
    init(name: String, major: String) {
        self.major = major
        super.init(name: name)
    }
}

// 부모클래스의 편의 이니셜라이저 자동 상속
let wizplan: Person = Person()
let jinSung: Student = Student()
print(wizplan.name) // Unknown
print(jinSung.name) // Unknown

[코드 18-13]을 보면 Student 클래스의 major 프로퍼티에 기본값이 없더라도 이니셜라이저에서 적절히 초기화했고, 부모클래스의 지정 이니셜라이저를 모두 재정의하여 부모클래스의 지정 이니셜라이저와 동일한 이니셜라이저를 모두 사용할 수 있는 상황이므로 규칙 1에 부합한다. 따라서 부모클래스의 편의 이니셜라이저가 자동으로 상속되었다.

자동 상속 규칙은 자식클래스에 편의 이니셜라이저를 추가한다고 하더라도 유효하다. 또, 부모클래스의 지정 이니셜라이저를 자식클래스의 편의 이니셜라이저로 구현하더라도 규칙 2를 충족한다.

 

/*
	코드 18-14. 편의 이니셜라이저 자동 상속2
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "Unknown")
    }
}

class Student: Person {
    var major: String
    
    convenience init(major: String) {
        self.init()
        self.major = major
    }
    
    override convenience init(name: String) {
        self.init(name: name, major: "Unknown")
    }
    
    init(name: String, major: String) {
        self.major = major
        super.init(name: name)
    }
}

// 부모클래스의 편의 이니셜라이저 자동 상속
let wizplan: Person = Person()
let jinSung: Student = Student(major: "Swift")
print(wizplan.name) // Unknown
print(jinSung.name) // Unknown
print(jinSung.major) // Swift

[코드 18-14]에서는 Student 클래스에서 부모클래스의 지정 이니셜라이저인 init(name:)을 편의 이니셜라이저로 재정의했지만 부모의 지정 이니셜라이저를 모두 사용할 수 있는 상황인 규칙 2에 부합하므로 부모클래스의 편의 이니셜라이저를 사용할 수 있다. 또, 자신만의 편의 이니셜라이저인 convenience init(major:)를 구현해주었지만 편의 이니셜라이저 자동 상속에는 아무러 영향을 미치지 않았다.

 

/*
	코드 18-15. 편의 이니셜라이저와 자동 상속3
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    convenience init() {
        self.init(name: "Unknown")
    }
}

class Student: Person {
    var major: String
    
    convenience init(major: String) {
        self.init()
        self.major = major
    }
    
    override convenience init(name: String) {
        self.init(name: name, major: "Unknown")
    }
    
    init(name: String, major: String) {
        self.major = major
        super.init(name: name)
    }
}

class UniversityStudent: Student {
    var grade: String = "A+"
    var description: String {
        return "\(self.name) \(self.major) \(self.grade)"
    }
    
    convenience init(name: String, major: String, grade: String) {
        self.init(name: name, major: major)
        self.grade = grade
    }
}

let nova: UniversityStudent = UniversityStudent()
print(nova.description) // Unknown Unknown A+

let raon: UniversityStudent = UniversityStudent(name: "raon")
print(raon.description) // raon Unknown A+

let joker: UniversityStudent = UniversityStudent(name: "joker", major: "Programming")
print(joker.description) // joker Programming A+

let chope: UniversityStudent = UniversityStudent(name: "chope", major: "Computer", grade: "C")
print(chope.description) // chope Computer C

[코드 18-15]를 보면 Student 클래스를 상속받은 UniversityStudent 클래스는 grade 프로퍼티에 기본값이 있으며, 별도의 지정 이니셜라이저를 구현해주지 않았으므로 규칙 1에 부합한다. 따라서 부모클래스의 이니셜라이저를 모두 자동 상속받는다. 게다가 자신만의 편의 이니셜라이저를 구현했지만 자동 상속에는 영향을 미치지 않았다. 결과적으로 UniversityStudent 클래스는 상속받은 이니셜라이저와 자신의 편의 이니셜라이저들을 모두 사용할 수 있다.

 

18.3.6 요구 이니셜라이저

required 수식어를 클래스의 이니셜라이저 앞에 명시해주면 이 클래스를 상속받은 자식클래스에서 반드시 해당 이니셜라이저를 구현해주어야 한다. 다시 말하면 상속받을 때 반드시 재정의해야 하는 이니셜라이저 앞에 required 수식어를 붙여준다. 다만 자식클래스에서 요구 이니셜라이저를 재정의할 때는 override 수식어 대신에 required 수식어를 사용한다.

 

/*
	코드 18-16. 요구 이니셜라이저 정의
*/

class Person {
    var name: String
    
    // 요구 이니셜라이저 정의
    required init() {
        self.name = "Unknown"
    }
}

class Student: Person {
    var major: String = "Unknown"
}

let miJeong: Student = Student()

[코드 18-16]을 살펴보면 Person 클래스에 init() 요청 이니셜라이저를 구현해주었지만, Person 클래스를 상속받은 Student 클래스에는 요구 이니셜라이저를 구현하지 않았다. 이는 Student 클래스의 major 프로퍼티에 기본값이 있으며 별다른 지정 이니셜라이저가 없기 때문에 이니셜라이저가 자동으로 상속된 것이다.

만약 Student 클래스에 새로운 지정 이니셜라이저를 구현한다면 부모클래스로부터 이니셜라이저가 자동으로 상속되지 않으므로 요구 이니셜라이저를 구현해주어야 한다.

 

/*
	코드 18-17. 요구 이니셜라이저 재구현
*/

class Person {
    var name: String
    
    // 요구 이니셜라이저 정의
    required init() {
        self.name = "Unknown"
    }
}

class Student: Person {
    var major: String = "Unknown"
    
    // 자신의 지정 이니셜라이저 구현
    init(major: String) {
        self.major = major
        super.init()
    }
    
    required init() {
        self.major = "Unknown"
        super.init()
    }
}

class UniversityStudent: Student {
    var grade: String
    
    // 자신의 지정 이니셜라이저 구현
    init(grade: String) {
        self.grade = grade
        super.init()
    }
    
    required init() {
        self.grade = "F"
        super.init()
    }
}

let jiSoo: Student = Student()
print(jiSoo.major) // Unknown

let yagom: Student = Student(major: "Swift")
print(yagom.major) // Swift

let juHyun: UniversityStudent = UniversityStudent(grade: "A+")
print(juHyun.grade) // A+

[코드 18-17]을 보면 Student와 UniversityStudent 클래스는 자신만의 지정 이니셜라이저를 구현했다. 그래서 부모클래스의 이니셜라이저를 자동 상속받지 못한다. 그래서 Person 클래스에 정의한 요구 이니셜라이저를 이니셜라이저 자동 상속 규칙에 부합하지 않는 자식클래스인 Student에도 구현해주고, 그 자식 클래스인 UniversityStudent 클래스에도 구현해주어야 한다.

만약 부모클래스의 일반 이니셜라이저를 자신의 클래스로부터 요구 이니셜라이저로 변경할 수도 있다. 그럴 때는 required override를 명시해주어 재정의됨과 동시에 요구 이니셜라이저가 될 것임을 명시해주어야 한다. 또, 편의 이니셜라이저도 요구 이니셜라이저로 변경될 수 있다. 마찬가지로 required convienience를 명시해주어 편의 이니셜라이저가 앞으로 요구될 것임을 명시해주면 된다.

 

/*
	코드 18-18. 일반 이니셜라이저의 요구 이니셜라이저 변경
*/

class Person {
    var name: String
    
    init() {
        self.name = "Unknown"
    }
}

class Student: Person {
    var major: String = "Unknown"
    
    init(major: String) {
        self.major = major
        super.init()
    }
    
    // 부모클래스의 이니셜라이저를 재정의함과 동시에
    // 요구 이니셜라이저로 변경됨을 알린다.
    required override init() {
        self.major = "Unknown"
        super.init()
    }
    
    // 이 요구 이니셜라이저는 앞으로 계속 요구한다.
    required convenience init(name: String) {
        self.init()
        self.name = name
    }
}

class UniversityStudent: Student {
    var grade: String
    
    init(grade: String) {
        self.grade = grade
        super.init()
    }
    
    // Student 클래스에서 요구했으므로 구현해주어야 한다.
    required init() {
        self.grade = "F"
        super.init()
    }
    
    // Student 클래스에서 요구했으므로 구현해주어야 한다.
    required convenience init(name: String) {
        self.init()
        self.name = name
    }
}

let yagom: UniversityStudent = UniversityStudent()
print(yagom.grade) // F

let juHyun: UniversityStudent = UniversityStudent(name: "JuHyun")
print(juHyun.name) // JuHyun

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

반응형

'iOS > SWIFT' 카테고리의 다른 글

[Swift] Chapter 20. 프로토콜  (0) 2021.10.11
[Swift] Chapter 19. 타입캐스팅  (0) 2021.10.11
[Swift] Chapter 17. 서브스크립트  (0) 2021.10.10
[Swift] Chapter 16. 모나드  (0) 2021.10.10
[Swift] Chapter 15. 맵, 필터, 리듀스  (0) 2021.10.10

댓글