클래스가 구조체와 구분되는 특성 중 하나는 상속(Inheritance)이다. 일반적으로 사회에서 사용하는 상속의 의미와 유사하다. 이를 객체지향에서 상속을 정의하자면 '한 클래스가 다른 클래스에서 정의된 프로퍼티나 메소드를 물려받아 사용하는 것'이라고 할 수 있다. 기능이나 프로퍼티를 물려주는 클래스와 이를 상속받는 클래스 사이에서는 다음과 같은 관계가 성립된다.
- 프로퍼티와 메소드를 물려준 클래스는 부모 클래스 = 상위 클래스 = 슈퍼 클래스 = 기본 클래스
- 프로퍼티와 메소드를 물려받은 클래스는 자식 클래스 = 하위 클래스 = 서브 클래스 = 파생 클래스
아래는 아무것도 상속받지 않은 기본 클래스를 만든 예제 코드이다.
class A {
var name = "Class A"
var description: String {
return "This class name is \(self.name)"
}
func foo() {
print("\(self.name)'s method foo is called")
}
}
이제 인스턴스를 만들기 위해 클래스를 초기화 한다.
let a = A()
a.name // Class A
a.description // This is class name is Class A
a.foo()
// 실행 결과
// Class A's method foo is called
서브 클래싱
클래스 A를 상속받아 새로운 클래스를 정의해 볼 것이다. 이것을 서브클래싱 이라고 한다. 즉 서브클래싱이란 기존에 있는 클래스를 기반으로 하여 새로운 클래스를 작성하는 과정을 의미한다.
class <클래스 이름>: <부모 클래스> {
// 추가로 구현할 내용
}
주의할 점은 스위프트는 단일 상속만 가능하다. 즉 하나의 클래스만 상속받을 수 있다. 아래 코드는 앞에서 작성한 A 클래스를 사용해 B 클래스를 서브 클래싱하는 예제코드이다.
class B : A {
var prop = "Class B"
func boo() -> String {
return "Class B prop = \(self.prop)"
}
}
let b = B()
b.prop // Class B
b.boo() // Class B prop = Class B
b.name // Class A
b.foo() // Class A's method foo is called
b.name = "Class C"
b.foo() // Class C's method foo is called
위 구문을 보면 클래스 B 에는 분명 name 프로퍼티와 foo 메소드가 정의되어 있지 않음에도 불구하고, 인스턴스 b는 이들을 사용하고 있다. 심지어 프로퍼티에 값을 대입한다. 이는 클래스 B 가 A를 상속받음으로써 모든 프로퍼티와 메소드를 물려받았기 때문이다. 현재 B의 상태는 아래와 같다.
class B {
var name = "Class A"
var prop = "Class B"
var description: String {
return "This class name is \(self.name)"
}
func foo() {
print("\(self.name)'s method foo is called")
}
func boo() -> {
return "Class B prop = \(self.prop)"
}
}
이 중 name, description 프로퍼티와 foo 메소드는 직접 정의한 것이 아니다. 클래스 A로부터 물려받은 것이다. 이처럼 상속을 이용하면 기능을 직접 구현하지 않아도 이미 만들어진 기존 클래스를 통해 기능을 확장할 수 있다. 이 때 클래스 A를 B의 부모 클래스 또는 슈퍼 클래스라고 하고, 클래스 B를 A의 자식 클래스 또는 서브 클래스라고 한다.
오버라이딩
자식 클래스는 일반적으로 부모 클래스로부터 상속받은 프로퍼티나 메소드를 그대로 사용하지만, 필요에 의해 이를 다시 구현하거나 재정의하여 사용하기도 한다. 자식 클래스에서 재정의된프로퍼티는 부모 클래스로부터 물려받은 내용을 덮어쓰게 되는데, 이 과정을 오버라이딩(Overriding)이라고 한다. 우리말로는 재정의 라고 번역된다.
스위프트에서는 오버라이딩하려는 메소드나 프로퍼티의 선언 앞에 override 키워드를 붙여야 한다. 만약 상위 클래스에서 이미 정의된 기존 메소드나 프로퍼티를 오버라이딩하면서 override 키워드를 붙이지 않았다면 컴파일러는 잘못된 선언으로 간주하고 오류를 발생시킨다. override 키워드가 붙으면 컴파일러는 이 프로퍼티 또는 메소드가 상위 클래스에서 정의된 것인지 검사한다. 굳이 상위 클래스라고 한 것은 부모 클래스의 부모 클래스, 그 부모 클래스까지 계속 탐색하기 때문이다. 하지만 상위 전체를 탐색해도 정의된 내역을 발견하지 못하면 override 키워드가 붙은 선언이 잘못되었음을 오류로 알려준다. 오버라이딩이 아님에도 override 키워드를 붙이는 경우에도 마찬가지로 오류가 발생한다.
프로퍼티 오버라이딩이 허용되는 것과 허용되지 않는 것은 다음과 같다.
프로퍼티 오버라이딩 시 허용되는 것
- 저장 프로퍼티를 get, set 구문이 모두 있는 연산 프로퍼티로 오버라이딩 하는 것
- get, set 구문이 모두 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것
- get 구문만 제공되는 연산 프로퍼티를 get, set 구문이 모두 허용되는 연산 프로퍼티로 오버라이딩 하는 것
- get 구문만 제공되는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩 하는 것
프로퍼티 오버라이딩 시 허용되지 않는 것
- 저장 프로퍼티를 저장 프로퍼티로 오버라이딩 하는 것
- get, set 구문과 상관 없이 연산 프로퍼티를 저장 프로퍼티로 오버라이딩 하는 것
- 저장 프로퍼티를 get 구문만 제공되는 연산 프로퍼티(= 읽기 전용)로 오버라이딩 하는 것
- get, set 구문을 모두 제공하는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩 하는 것
이 내용을 바탕으로 프로퍼티 오버라이딩 예제 코드를 작성했다.
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "시간당 \(self.currentSpeed)의 속도로 이동하고 있습니다."
}
func makeNoise() {
// 임의의 교통수단 자체는 경적을 울리는 기능이 필요 없다.
}
}
class Car: Vehicle {
var gear = 0
var engineLevel = 0
override var currentSpeed: Double {
get {
return Double(self.engineLevel * 50)
}
set {
// 아무것도 하지 않음
}
}
override var description: String {
get {
return "Car: engineLevel = \(engineLevel), so currentSpeed = \(self.currentSpeed)"
}
set {
print("New Value is \(newValue)")
}
}
}
기본 클래스 Vehicle을 상속받는 새로운 클래스 Car를 정의했다. currentSpeed, description을 오버라이딩하여 연산 프로퍼티의 형태로 바꾸었다. 자동차의 엔진 등급이 올라가면 속도가 올라간다고 가정하여 엔진 등급에 50을 곱한 값을 반환하고 있다. set 구문은 아무것도 하지 않지만 제거하면 안된다. currentSpeed가 원래 저장 프로퍼티였으므로 오버라이딩할 때도 읽고 쓰기가 모두 가능해야 한다.
description 프로퍼티에 값을 할당하면 set 구문이 실행되어 내부에서 작성된 출력 구문이 실행된다.
let c = Car()
c.engineLevel = 5
c.currentSpeed // 250
c.description = "New Class Car"
print(c.description)
// 실행 결과
// New Value is New Class Car
// Car: engineLevel = 5, so currentSpeed = 250.0
상속받은 프로퍼티를 오버라이딩하는 과정에서 우리는 필요에 따라 프로퍼티 옵저버를 붙일 수 있다. 부모 클래스에서 프로퍼티가 상수로 선언되었거나 읽기 전용 프로퍼티로 선언되지만 않았다면 우리는 프로퍼티를 오버라이딩할 때 프로퍼티 옵저버 구문을 추가하면 된다. 이 때 오버라이드 되는 프로퍼티도 역시 읽기/쓰기용으로 선언되어야 한다.
class AutomaticCar : Car {
override var currentSpeed: Double {
didSet {
self.gear = Int(currentSpeed / 10.0) + 1
}
}
}
위 예제는 현재 속도에 따라 기어 단수가 함께 변경되어 바뀔 수 있는 기능을 하는 코드이다.
메소드 오버라이딩은 조금 까다롭다. 오버라이딩 대상이 되는 메소드의 매개변수나 타입, 그리고 반환 타입은 변경할 수 없다.
아래는 예제 코드이다.
class Bike : Vehicle {
override func makeNoise() {
print("빠라빠라빠라밤")
}
}
let bk = Bike()
bk.makeNoise()
// 실행 결과
// 빠라빠라빠라밤
기본 클래스에서는 아무것도 하지 않던 빈 메소드인 makeNoise를 오버라이딩하여 오토바이의 경적을 만들어주고 있다. 메소드의 경우 오버라이딩의 제약 조건으로 매개변수 타입이나 반환 타입을 그대로 유지해야 하는 것은 사실 스위프트가 메소드 오버로딩(Overloading)을 지원하기 때문이다. 하나의 메소드 이름으로 여러가지 메소드를 만들 수 있다. 이 때 기준이 되는 것이 매개변수의 타입과 종류이다. 메소드 이름이 같아도 매개변수 타입이 다르거나 종류가 다르면 다른 메소드로 처리된다.
- func makeNoise()
- func makeNoise(param: Int)
- func makeNoise(param: String)
- func makeNoise(param: Double) -> String
- func makeNoise(param: Double, append: String)
- func makeNoise(param: Double, appendix: Striing)
오버라이딩하는 대상 메소드의 매개변수 타입이 달라지거나 매개변수의 개수가 달라지만 오버로딩 문법에 의해 새로운 메소드로 인식하므로 이는 오버라이딩 대상에 포함되지 않는다. 이 때는 오히려 override 키워드를 붙이지 않아야 한다.
그런데 프로퍼티나 메소드를 오버라이딩하면 더는 본래의 값이나 기능을 사용할 수 없다고 생각할 수 있다. 하지만 그렇지 않다. 스위프트에서는 상속받은 부모 클래스의 인스턴스를 참조할 수 있도록 super라는 객체를 제공하는데, 이 객체를 이용하면 부모 클래스의 프로퍼티나 메소드를 호출할 수 있다. 예를 들어 위 bike 클래스에서 부모 클래스의 makeNoise()를 호출하려먼 super.makeNoise()로 호출할 수 있다.
마지막은 오버라이딩을 막는 방법이다. 상위 클래스의 입장에서 하위 클래스가 자신의 기능을 재정의하는 것으 막아야 할 때가 있다. 이 때 final 키워드를 사용한다. 프로퍼티나 메소드를 정의하는 var, func 등의 키워드 앞에 final을 붙이면 된다.
lass Vehicle {
final var currentSpeed = 0.0
final var description: String {
return "시간당 \(self.currentSpeed)의 속도로 이동하고 있습니다."
}
final func makeNoise() {
// 임의의 교통수단 자체는 경적을 울리는 기능이 필요 없다.
}
}
이제 vehicle을 상속한 서브 클래스에서 final키워드가 붙은 프로퍼티나 메소드를 override할 경우 컴파일러는 오류를 표시한다.
final 키워드는 프로퍼티나 메소드 뿐 아니라 클래스 자체에도 붙일 수 있다. final 키워드가 붙은 클래스는 상속 자체가 차단되어, 어떤 클래스도 이 클래스를 서브클래싱할 수 없게 된다. 단지 인스턴스로만 사용할 수 있다.
final class X {
}'swift' 카테고리의 다른 글
| 스위프트 초기화 구문 (1) | 2024.04.16 |
|---|---|
| 스위프트 타입 캐스팅 (1) | 2024.04.16 |
| 스위프트 메소드 (0) | 2024.04.16 |
| 스위프트 프로퍼티 (0) | 2024.04.15 |
| 스위프트 구조체와 클래스 (0) | 2024.04.15 |