[Swift 프로그래밍] 상속

이정훈·2022년 6월 24일
0

Swift 기본

목록 보기
17/22
post-thumbnail

본 내용은 스위프트 프로그래밍 3판 (야곰 지음) 교재를 공부한 내용을 바탕으로 작성 하였습니다.

상속


상속은 클래스의 프로퍼티나 메서드, 서브스크립트 등을 다른 클래스에서 사용 가능하도록 한다. 상속은 클래스구조체 사이의 차별화되는 가장 큰 특징으로 구조체는 상속을 할 수 없다.

A라는 클래스에서 B의 상속을 받으면 A를 자식 클래스(SubClass)라고 하며 B를 부모 클래스(SuperClass)라고 한다.

스위프트에서 클래스 상속은 콜론(:) 뒤에 상속 할 클래스 이름을 써서 사용하며 형태는 다음과 같다.

class A: B {
	//프로퍼티, 메서드
}

위의 형태는 클래스 A가 클래스 B의 상속 받겠다는 의미이다.

클래스 상속


다음은 클래스의 상속을 구현한 코드이다.

//부모 클래스
class Person {
    var name: String = ""   // 저장 프로퍼티
    var age: Int = 0
    
    var greet: String {     // 연산 프로퍼티(읽기 전용)
        get {
            return "나의 이름은 \(name)이고, 나이는 \(age)입니다. 반갑습니다!"
        }
    }
    
    func today() -> Void {     // 메서드
        print("오늘도 힘찬 하루를 보냅니다!")
    }
}

//상속 받은 자식 클래스
class Student: Person {
    var major: String = ""  //새로운 프로퍼티
    
    func study() -> Void {
        print("열심히 공부합니다.")
    }
}

let personLee: Person = Person()
personLee.name = "이철수"
personLee.age = 50
print(personLee.greet)  //나의 이름은 이철수이고, 나이는 50입니다. 반갑습니다!
personLee.today()       //오늘도 힘찬 하루를 보냅니다!

let personKim: Student = Student()
personKim.name = "김영희"
personKim.age = 20
personKim.major = "Computer Science"
print(personKim.greet)  //나의 이름은 김영희이고, 나이는 20입니다. 반갑습니다!
personKim.today()       //오늘도 힘찬 하루를 보냅니다!
personKim.study()       //열심히 공부합니다.

Person 클래스를 상속 받은 Student 클래스는 Person 클래스의 프로퍼티와 메서드를 사용한 것을 확인 할 수 있으며, Student 클래스 자신만이 가지는 프로퍼티와 메서드도 사용 하는 것을 확인 할 수 있다.

다시 말해, 자식 클래스는 부모 클래스의 프로퍼티, 메서드, 서브 스크립트 등 모든 요소를 가지며 활용 가능하다는 것이다.(접근 수준에 따라 달라질 수 있음)
또한 상속을 받은 자식 클래스는 또 다른 클래스의 부모 클래스가 될 수 있다.

재정의


재정의(override)자식 클래스부모 클래스의 요소를 가져와 자신만의 것으로 만들어 사용 할 수 있다.

부모 클래스의 요소를 재정의 하기 위해서 새로운 정의 앞에 override 키워드를 사용한다.

만약 재정의 했지만 부모 클래스의 요소를 그대로 사용 하고 싶다면 super 키워드를 사용하여 부모 클래스에서 구현한 내용을 그대로 가져온다.
타입 메서드 내부에서 super를 사용할 경우 부모 클래스의 타입 프로퍼티와 타입 메서드에 접근할 수 있으며, 인스턴스 메서드 내부에서 super를 사용하면 부모 클래스의 인스턴스 프로퍼티와 인스턴스 메서드에 접근할 수 있다.

1. 메서드 재정의

다음은 재정의한 메서드를 구현한 코드이다.

class Person {
    var name: String = ""   // 저장 프로퍼티
    var age: Int = 0
    
    var greet: String {     // 연산 프로퍼티(읽기 전용)
        get {
            return "나의 이름은 \(name)이고, 나이는 \(age)입니다. 반갑습니다!"
        }
    }
    
    func today() -> Void {     // 메서드
        print("오늘도 힘찬 하루를 보냅니다!")
    }
    
    class func goal() -> String {   //재정의가 가능한 타입 메서드
        return "목표를 달성하기 위하여 노력합니다."
    }
}

//상속 받은 자식 클래스
class Student: Person {
    var major: String = ""  //새로운 프로퍼티
    
    func study() -> Void {
        print("열심히 공부합니다.")
    }
    
    override func today() -> Void {     //부모 클래스의 today 메서드 재정의
        super.today()   //부모 클래스를 호출하는 super 키워드
        print("오늘은 프로젝트 발표가 있는 날입니다.")
    }
    
    override class func goal() -> String {  //타입 메서드 재정의
        return "좋은 성적을 받기 위하여 항상 노력합니다."
    }
}

let personLee: Person = Person()
personLee.name = "이철수"
personLee.age = 50
print(personLee.greet)  //나의 이름은 이철수이고, 나이는 50입니다. 반갑습니다!
personLee.today()       //오늘도 힘찬 하루를 보냅니다!
print(Person.goal())    //목표를 달성하기 위하여 노력합니다.

let personKim: Student = Student()
personKim.name = "김영희"
personKim.age = 20
personKim.major = "Computer Science"
print(personKim.greet)  //나의 이름은 김영희이고, 나이는 20입니다. 반갑습니다!
personKim.today()       //오늘도 힘찬 하루를 보냅니다!
                        //오늘은 프로젝트 발표가 있는 날입니다.
personKim.study()       //열심히 공부합니다.
print(Student.goal())   //좋은 성적을 받기 위하여 항상 노력합니다.

Student 클래스에서 부모 클래스의 메서드인 today() 메서드를 재정의 하였고, 재정의한 메서드 내부에는 부모 클래스를 호출하는 super 키워드를 사용하여 부모 클래스의 today() 메서드를 그대로 가져 오도록 구현하였다.

타입 메서드에는 두 가지 종류가 존재하는데 static 키워드를 사용한 타입 메서드는 자식 클래스에서 재정의가 불가능하다. 반대로 class 키워드를 사용한 타입 메서드는 자식 클래스에서 재정의가 가능한 타입 메서드이다.(재정의가 불가능하다는 것이지 상속은 가능하다.)

위의 코드는 부모 클래스에서 class 키워드를 사용한 타입 메서드를 선언하고 자식 클래스에서 타입 메서드를 재정의하는 코드를 구현하였다.

여기서 주의 할 것은 스위프트는 메서드 이름이 같아도 매개변수 이름과 반환 값의 타입이 다르면 다른 메서드로 취급한다. 따라서 메서드를 재정의 할 때 매개변수 이름과 반환 타입이 부모 클래스의 매개변수와 반환 타입이 같은지 반드시 확인해야 한다.

2. 프로퍼티 재정의

조상 클래스에 선언된 인스턴스 프로퍼티와 타입 프로퍼티를 자식 클래스에서 재정의하여 사용이 가능하다.
(여기서 말하는 조상 클래스는 부모 클래스를 포함한 상위 클래스를 말한다.)

하지만 저장 프로퍼티로의 재정의는 불가능하다. 프로퍼티를 재정의 한다는것은 프로퍼티의 값을 재정의 하는것이 아니라 프로퍼티의 접근자(getter), 설정자(setter), 프로퍼티 감시자 를 재정의 하는 것이기 때문이다.

따라서 접근자(getter), 설정자(setter)를 모두 구현한 프로퍼티를 재정의 하는 경우 접근자(getter), 설정자(setter)를 모두 재정의 해야 한다. (읽기 전용으로 구현한 프로퍼티를 읽기, 쓰기 모두 구현하여 재정의 하거나 혹은 읽기 전용을 읽기 전용으로 재정의하는 것은 당연히 가능하다.)

다음은 프로퍼티 재정의를 구현한 코드이다.

 //부모 클래스
class Person {
    var name: String = ""   // 저장 프로퍼티
    var age: Int = 0
    
    var born: Int {
        return 2022 - self.age + 1
    }
    
    var greet: String {     // 연산 프로퍼티(읽기 전용)
        get {
            return "나의 이름은 \(name)이고, 나이는 \(age)입니다. 반갑습니다!"
        }
    }
    
    func today() -> Void {     // 메서드
        print("오늘도 힘찬 하루를 보냅니다!")
    }
    
    class func goal() -> String {   //재정의가 가능한 타입 메서드
        return "목표를 달성하기 위하여 노력합니다."
    }
}

//상속 받은 자식 클래스
class Student: Person {
    var major: String = ""  //새로운 프로퍼티
    
    override var born: Int {
        get {
            return 2022 - self.age + 1
        }
        
        set {
            self.age = 2022 - newValue + 1
        }
    }
    
    override var greet: String {    //연산 프로퍼티 재정의
        get {
            return "나의 이름은 \(name)이고, \(major)를 공부하고 있습니다. 반갑습니다!"
        }
    }
    
    func study() -> Void {
        print("열심히 공부합니다.")
    }
    
    override func today() -> Void {     //부모 클래스의 today 메서드 재정의
        super.today()   //부모 클래스를 호출하는 super 키워드
        print("오늘은 프로젝트 발표가 있는 날입니다.")
    }
    
    override class func goal() -> String {  //타입 메서드 재정의
        return "좋은 성적을 받기 위하여 항상 노력합니다."
    }
}

let personLee: Person = Person()
personLee.name = "이철수"
personLee.age = 50
print(personLee.greet)  //나의 이름은 이철수이고, 나이는 50입니다. 반갑습니다!
personLee.today()       //오늘도 힘찬 하루를 보냅니다!
print(Person.goal())    //목표를 달성하기 위하여 노력합니다.
print(personLee.born)   //1973

let personKim: Student = Student()
personKim.name = "김영희"
personKim.major = "Computer Science"
print(personKim.greet)  //나의 이름은 김영희이고, 나이는 20입니다. 반갑습니다!
personKim.today()       //오늘도 힘찬 하루를 보냅니다!
                        //오늘은 프로젝트 발표가 있는 날입니다.
personKim.study()       //열심히 공부합니다.
print(Student.goal())   //좋은 성적을 받기 위하여 항상 노력합니다.
personKim.born = 2003
print(personKim.age)    //20

위의 코드에서는 Person 클래스의 greet라는 읽기 전용 연산 프로퍼티를 Person 클래스를 상속 받은 Student 클래스에서 읽기 전용 프로퍼티로 재정의한 코드를 구현하였다.

Person 클래스의 born 연산 프로퍼티는 접근자만 구현이 되어 있지만 Student 클래스에서 재정의한 born 연산 프로퍼티는 접근자와 생성자를 모두 구현하여 재정의 하였다.

3. 프로퍼티 감시자 재정의

프로퍼티 감시자를 재정의 할때 주의 할 점은 상수 프로퍼티읽기 전용 프로퍼티는 프로퍼티 감시자를 재정의 할 수 없다는 것이다. (당연하겠지만 상수 프로퍼티와 읽기 전용 프로퍼티는 willSet과 didSet를 구현 할 수 없기 때문)

그리고 프로퍼티 감시자를 재정의하면 조상 클래스에 정의 되어 있는 프로퍼티 감시자도 함께 동작 한다.

또한 접근자와 생성자 그리고 프로퍼티 감시자는 함께 사용이 불가능하다.

다음은 프로퍼티 감시자 재정의한 코드이다.

//부모 클래스
class Person {
    var name: String = ""   // 저장 프로퍼티
    var age: Int = 0 {
        willSet {
            print("변수의 값이 \(newValue)로 변경될 예정입니다.(Person)")
        }
        
        didSet {
            print("변수의 값이 \(self.age)로 변경 되었습니다.(Person)")
        }
    }
    
    var born: Int {
        return 2022 - self.age + 1
    }
    
    var greet: String {     // 연산 프로퍼티(읽기 전용)
        get {
            return "나의 이름은 \(name)이고, 나이는 \(age)입니다. 반갑습니다!"
        }
    }
    
    func today() -> Void {     // 메서드
        print("오늘도 힘찬 하루를 보냅니다!")
    }
    
    class func goal() -> String {   //재정의가 가능한 타입 메서드
        return "목표를 달성하기 위하여 노력합니다."
    }
}

//상속 받은 자식 클래스
class Student: Person {
    var major: String = ""  //새로운 프로퍼티
    
    override var age: Int {		//프로퍼티 감시자 재정의
        didSet {	
            print("변수의 값이 \(self.age)(으)로 변경 되었습니다.(Student)")
        }
    }
    
    override var born: Int {
        get {
            return 2022 - self.age + 1
        }
        
        set {
            self.age = 2022 - newValue + 1
        }
    }
    
    override var greet: String {    //연산 프로퍼티 재정의
        get {
            return "나의 이름은 \(name)이고, \(major)를 공부하고 있습니다. 반갑습니다!"
        }
    }
    
    func study() -> Void {
        print("열심히 공부합니다.")
    }
    
    override func today() -> Void {     //부모 클래스의 today 메서드 재정의
        super.today()   //부모 클래스를 호출하는 super 키워드
        print("오늘은 프로젝트 발표가 있는 날입니다.")
    }
    
    override class func goal() -> String {  //타입 메서드 재정의
        return "좋은 성적을 받기 위하여 항상 노력합니다."
    }
}

let personLee: Person = Person()
personLee.name = "이철수"
personLee.age = 50      //나이가 50로 변경될 예정입니다.(Person)
                        //나이가 50로 변경 되었습니다.(Person)
print(personLee.greet)  //나의 이름은 이철수이고, 나이는 50입니다. 반갑습니다!
personLee.today()       //오늘도 힘찬 하루를 보냅니다!
print(Person.goal())    //목표를 달성하기 위하여 노력합니다.
print(personLee.born)   //1973

let personKim: Student = Student()
personKim.name = "김영희"
personKim.major = "Computer Science"
print(personKim.greet)  //나의 이름은 김영희이고, 나이는 20입니다. 반갑습니다!
personKim.today()       //오늘도 힘찬 하루를 보냅니다!
                        //오늘은 프로젝트 발표가 있는 날입니다.
personKim.study()       //열심히 공부합니다.
print(Student.goal())   //좋은 성적을 받기 위하여 항상 노력합니다.
personKim.born = 2003   //나이가 20로 변경될 예정입니다.(Person)
                        //나이가 20로 변경 되었습니다.(Person)
                        //나이가 20(으)로 변경 되었습니다.(Student)
print(personKim.age)    //20

추가적으로 위의 코드에서 볼 수 있듯이 age 프로퍼티 감시자를 재정의 했더라도 조상 클래스의 프로퍼티 감시자가 먼저 동작 후 재정의 된 프로퍼티 감시자가 동작하는 것을 볼 수 있다.

4. 서브스크립트 재정의

위에서 설명한 것과 같이 서브스크립트 또한 재정의 할 수 있는데 구현은 다음과 같다.

class Customer {
    var guest: [Person] = [Person]()
    
    subscript(num: Int) -> Person {
        print("일반 고객입니다.")
        return guest[num]
    }
}

class VIPCustomer: Customer {
    var VIPguest: [Person] = [Person]()
    
    override subscript(index: Int) -> Person {
        print("VIP 고객입니다.")
        return VIPguest[index]
    }
}

let customerInfo: Customer = Customer()
customerInfo.guest.append(Person())
customerInfo[0] //일반 고객입니다.

let VIPInfo: VIPCustomer = VIPCustomer()
VIPInfo.VIPguest.append(Person())
VIPInfo[0]  //VIP 고객입니다.

메서드 재정의에서 설명한 것과 같이 서브스크립트 또한 매개변수와 반환 타입이 다르면 다른 서브스크립트로 취급한다. 따라서 서브스크립트를 재정의 할때 매개변수와 반환 타입이 동일해야 한다.

5. 재정의 방지

만약 부모 클래스의 요소를 자식 클래스로의 재정의를 제한하고 싶다면 제한하고자 하는 요소 앞에 final 키워드를 사용하여 재정의를 제한한다.

또한 클래스 자체의 상속을 제한하고자 한다면 class 선언 부에 final 키워드를 사용하여 상속을 제한한다.

다음은 final 키워드를 사용하여 name 프로퍼티를 재정의 방지 프로퍼티를 구현한 코드이다.

class Person {
    final var name: String = ""   // final로 선언된 저장 프로퍼티
}

위의 설명에서 타입 메서드는 상속이 불가능한 static 키워드와 상속이 가능한 class 키워드 두 가지 종류가 있다고 하였다. 근데 만약 class 키워드 앞에 final 키워드를 사용하여 다음과 같이 선언하면 어떻게 될까?

class Person {
	 final class func goal() -> String {   
        return "목표를 달성하기 위하여 노력합니다."
    }
}

상속이 가능한 타입 메서드 앞에 final 키워드를 사용하였으니 static과 같지 않을까? 라고 생각 할 수 있다.

정확한 답변이다!

따라서 본인이 편한데로 static만 쓰던지 final class로 쓰던지 하면 될꺼 같다.

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글