클래스와 구조체의 구분되는 특성 중 하나로 상속(Ingeritance)이라는 것을 들 수 있다. 이는 말 그대로 하나의 클래스가 다른 클래스에게 무엇인가를 물려줄 수 있다는 말이다.
일반 사회 현상에서의 상속과 프로그래밍에서의 상속을 보면 아래와 같다.
일반 사회 - "아들이 아버지로부터 재산을 물려받았다."
프로그래밍 - "Child클래스가 Father클래스를 상속한다."
이 관계를 통해서 객체지향에서 상속을 정의하면 "한 클래스가 다른 클래스에서 정의된 프로퍼티나 메소드를 물려받아 사용하는 것" 과 같다. 상속을 사용하면 이미 만들어진 다른 클래스의 기능이나 프로퍼티를 직접 구현하지 않고도 사용할 수 있다. 다만, 추가로 필요한 기능이나 프로퍼티만 정의하여 사용하면 된다.
이때 기능이나 프로퍼티를 물려주는 클래스와 이를 상속받는 클래스의 관계는 아래와 같다.
프로퍼티와 메소드를 물려주는 클래스 = 부모 클래스 = 상위 클래스 = 슈퍼 클래스 = 기본 클래스
프로퍼티와 메소드를 물려받은 클래스 = 자식 클래스 = 하위 클래스 = 서브 클래스 = 파생 클래스
클래스를 상속하는 과정에 대한 예제를 살펴보자.
우선 어떤 클래스도 상속받지 않는 클래스를 정의하자. 이 클래스를 기본 클래스라고 한다.
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")
}
}
[서브클래싱]
이제 우리는 클래스 A를 상속받아 새로운 클래스를 정의해보자. 이러한 과정을 우리는 서브클래싱이라고 한다.
즉, 서브클래싱이란 기존에 있는 클래스를 기반으로 하여 새로운 클래스를 작성하는 과정을 의미한다.
서브클래싱의 문법구조는 아래와 같다.
class <클래스 이름> : <부모 클래스> {
// 추가로 구현할 내용
}
다중상속이 지원되는 c++과 달리 Swift에서는 단일 상속만 지원한다. 이는 다중 상속에서 발생되는 메소드나 프로퍼티의 중첩및 충돌을 방지하기 위해서이다.
하지만 코코아 터치 프레임워크의 클래스 정의 구문을 보면 콜론 다음에 나열된 여러가지 클래스를 보게 될 때가 있는데 이는 대부분 가장 첫 번째가 상속이고 나머지는 구현(Implements)라고 하는데 또 다른 기능이다. -> 프로토콜 학습에 배울 예정
이제 앞서 작성한 클래스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 프로퍼티와 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() -> String {
return "Class B prop = \(self.prop)"
}
}
이처럼 상속을 이용한다면 기능을 직접 구현하지 않고도 이미 만들어둔 기존 클래스를 통해 손쉽게 기능을 확장할 수 있다.
[오버라이딩]
자식 클래스는 일반적으로 부모 클래스로부터 상송받은 프로퍼티나 메소드를 그대로 사용하기도 하지만, 필요에 의해 이를 다시 구현하거나 재정의하여 사용하기도 한다. 자식 클래스에서 재정의된 메소드나 프로퍼티는 부모 클래스로부터 물려받은 내용을 덮어쓰게 되는데 이 과정을 오버라이딩(Overriding)이라고 한다.
오버라이딩한 내용은 자기 자신또는 자신을 서브클래싱한 하위 클래스에만 적용된다. 즉, 자식 클래스에서 값을 변경했다고해서 부모 클래스에까지 적용되지 않는다는 뜻이다.
Swift에서는 오버라이팅하려는 메소드나 프로퍼티의 선언 앞에는 override 키워드를 붙여야한다. 만약 상위 클래스에서 이미 정의된 기존 메소드나 프로퍼티를 오버라이딩하면서 override 키워드를 붙이지 않았다면 컴파일러는 오류를 발생시킨다.
override 키워드가 붙으면 컴파일러는 이 프로퍼티 또는 메소드가 상위 클래스(부모 클래스 이상의 클래스까지 모두)에서 정의 된 것인지 검사를 한다. 검사했음에도 정의된 내역을 발견하지 못했다면 override 키워드가 잘못되었음을 오류로 알려준다.
프로퍼티를 오버라이딩할 때 저장 프로퍼티를 저장 프로퍼티로 오버라이딩하거나 연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것은 허용되지 않는다.
저장 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것은 아무런 의미가 없다. 오버라이딩 대신 값만 다시 할당하는 것이 좋다.
연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것은 연산 프로퍼티 자체를 오버라이딩 하는 것으로도 충분하다.
조금 복잡하기 때문에 아래의 사항들을 잘 보면서 오버라이딩을 하자.
<프로퍼티 오버라이딩 시 허용되는 것>
1. 저장 프로퍼티를 get, set 구문이 모두 있는 연산 프로퍼티로 오버라이딩 하는 것
2. get,set 구문이 모두 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것
3. get 구문만 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것
4. get 구문만 제공되는 연산 프로퍼티를 get 구문만제공되는 연산 프로퍼티로 오버라이딩 하는 것
<프로퍼티 오버라이딩 시 허용되지 않는 것>
1. 저장 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
2. get, set 구문과 관계없이 연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
3. 저장 프로퍼티를 get 구문만제공되는 연산 프로퍼티로 오버라이딩 하는 것
4. get, set 구문을 모두 제공하는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩하는 것
위 규칙을 보면 복잡해 보이지만 원칙은 단순하다. 프로퍼티 오버라이딩은 상위 클래스의 기능을 하위 클래스가 확장 또는 변경하는 방식으로 진행되어야지, 제한하는 방식으로 진행되어서는 안되는다는 것이다.
이제 이 내용들을 바탕으로 프로퍼티의 오버라이딩 예제를 학습해보자.
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "시간당 \(self.currentSpeed)의 속도로 이동하고 있습니다."
}
func mackNoise() {
// 임의의 교통수단 자체는 경적을 울리는 기능이 필요없다.
}
}
class Car: Vehicle {
var gear = 0
var engineLevel = 0
override var currentSpeed: Double {
get {
return Double(self.engineLevel * 50)
}
set{
//nothing
}
}
override var description: String {
get{
return "Car: engineLevel = \(self.engineLevel), so currentSpeed = \(self.currentSpeed)"
}
set {
print("New Value is \(newValue)")
}
}
}
기본 클래스 Vehicle을 상속받는 새로운 클래스 Car를 정의하였다. Car 클래스는 두 개의 프로퍼티를 오버라이딩하고 있는데, 각각
currentSpeed, description이다.
currentSpeed는 부모 클래스에서 저장 프로퍼티로 정의되었던 것을 오버라이딩하여 연산 프로퍼티의 형태로 바꾸고 있다.
원래 저장 프로퍼티였기 때문에 get 뿐만 아니라 set 구문도 제공하여 읽기 쓰기가 모두 가능하게 만들어 주어야 한다.
description는 부모 클래스에서 get만 제공하는 연산 프로퍼티를 오버라이딩하여 get,set 구문을 모두 제공하는 연산 프로퍼티로 바꾸고 있다.
이제 메소드 오버라이딩을 살펴보자. 메소드 오버라이딩은 조금 까다롭다. 오버라이딩 대상이 되는 메소드의 매개변수 개수나 타입, 그리고 반환 타입은 변경할 수 없다. 물론 순서도 변경이 되면 안된다.
메소드 오버라이딩을 통해서 변경할 수 있는 것은 오로지 내부 구문들 뿐이다.
예제를 살펴보자.
class Bike: Vehicle {
override func mackNoise() {
print("띠링띠링")
}
}
let bk = Bike()
bk.mackNoise() // 띠링띠링
메소드의 경우 오버라이딩의 제약 조건으로 매개변수 타입이나 반환 타입을 그대로 유지해야 하는 것은 스위프트가 메소드 오버라이딩을 지원하기 때문이다. 오버라이팅이란 차곡차곡 쌓는다는 뜻이다. 즉, 하나의 메소드 이름으로 여러가지 메소드를 만들어 쌓는 것이다.
이때 기준이 되는것이 바로 매개변수의 타입과 종류이다. 즉, 같은 이름의 메소드라도 정의된 매개변수의 타입이 다르면 서로 다른 메소드로 처리하는 것이 오버라이딩이라고 할 수 있다.
아래와 같이 하나의 이름으로 여러 개의 메소드를 정의할 수 있다.
func mackNoise()
func mackNoise(param : Int)
func mackNoise(param : String)
func mackNoise(param : Double) -> String
func mackNoise(param : Double, append: String)
func mackNoise(param : Double, appendix : String)
스위프트에서 메소드는 이름뿐만 아니라 매개변수의 개수와 타입을 기준으로 하여 유일성 여부를 구분한다. 따라서 이름이 같고 매개변수의 개수까지 일치하더라도 타입이 다르면 서로 다른 메소드로 간주한다. (매개변수의 이름만 달라도 새로운 메소드로 정의)
오버라이딩과 오버로딩 구분
오버라이딩은 덮어쓰기라는 개념으로 정의하는 것이 좀 더 직관적인데 덮어쓰기의 영어 표현이 overwriting이라 오버라이딩과 발음이 비슷한데에 착안하여 오버라이딩은 덮어쓰기 문법이라고 생각하고 이외의 다른 것은 오버로딩이라고 생각하면된다.
그러면 프로퍼티나 메소드를 오버라이딩하면 더는 본래의 값이나 기능을 사용할 수 없는 것일까?
아니다. 스위프트에서는 상속받은 부모 클래스의 인스턴스를 참조할 수 있도록 super라는 객체를 제공한다. 이 객체를 이용하여 점구문을 함께 사용하면 부모 클래스의 프로퍼티나 메소드를 호출할 수 있다.
super.프로퍼티명 or super.메소드명()으로 부모 클래스이 프로퍼티나 메소드를 참조할 수 있다.
이제 오버라이딩을 막는 방법에 대해서 알아보자.
오버라이딩은 상위 클래스의 프로퍼티나 메소드를 수정할 수 있다는 점에서 매우 강력한 생산성을 가진다. 하지만 예를 들어 상위 클래스에서 중요한 인증코드를 처리하는 메소드를 작성하고 그에 대한 성공여부를 bool값으로 반환해준다고 생각했을 경우 메소드 오버라이딩을 통해서 인증을 성공했다는 true를 반환하면 아무런 검사 없이 성공으로 처리되는 문제가 발생된다.
따라서, swift에서는 이러한 문제를 해결하기 위해서 final 키워드를 제공한다. 만약 정의한 메소드나 프로퍼티가 하위 클래스에서 오버라이딩되는 것을 원치 않는다면 프로퍼티나 메소드를 정의하는 var, func등의 키워드 앞에 final 키워드를 붙이면된다.
예제로 좀더 자세하게 알아보자.
class Vehicle {
final var currentSpeed = 0.0
final var description: String {
return "시간당 \(self.currentSpeed)의 속도로 이동하고 있습니다."
}
final func mackNoise() {
// 임의의 교통수단 자체는 경적을 울리는 기능이 필요없다.
}
}
class Car: Vehicle {
var gear = 0
var engineLevel = 0
override var currentSpeed: Double { // 에러 발생 - var ovverides a 'final' var
get {
return Double(self.engineLevel * 50)
}
set{
//nothing
}
}
override var description: String { // 에러 발생 - var ovverides a 'final' var
get{
return "Car: engineLevel = \(self.engineLevel), so currentSpeed = \(self.currentSpeed)"
}
set {
print("New Value is \(newValue)")
}
}
}
이렇게 에러가 발생되어 오버라이딩을 할 수 없게된다.
final 키워드는 클래스 자체에도 사용할 수 있는데 이렇게 되면 클래스의 상속 자체가 차단 되어 어떤 클래스도 이 클래스를 서브 클래싱할 수 없게 된다.
상속은 객체지향 프로그래밍에서 매우 큰 비중을 차지하는 개념이다. 상속 기능 덕분에 우리는 새로운 객체를 만들 때 반복적으로 코드를 작성하는 일을 덜 수 있고 한번 정의해둔 객체를 여러 곳에서 사용할 수 있다. 즉, 재사용성이 높아지고 전반적으로 코드를 효율적으로 사용할 수 있게되었다.
'문법관련 > Swift' 카테고리의 다른 글
초기화 구문 (꼼꼼한 재은 씨의 Swift : 문법편) (0) | 2022.04.09 |
---|---|
타입 캐스팅 (꼼꼼한 재은 씨의 Swift : 문법편) (0) | 2022.04.09 |
메소드 (꼼꼼한 재은 씨의 Swift : 문법편) (0) | 2022.04.02 |
프로퍼티 (꼼꼼한 재은 씨의 Swift : 문법편) (0) | 2022.03.31 |
구조체와 클래스의 기본 개념 (꼼꼼한 재은 씨의 Swift : 문법편) (0) | 2022.03.29 |