상속(Inheritance)
- 상속(Inheritance) 은 클래스가 다른 클래스의 특성을 물려받아 사용하는 것을 의미한다
- 새로운 클래스를 정의할 때, 기존 클래스의 속성(저장 속성)과 메서드(기능) 를 그대로 물려받아 사용할 수 있다
- 상속은 Swift에서 클래스(Class) 에서만 사용 가능하며, 구조체(Struct) 나 열거형(Enum) 에서는 사용할 수 없다
상속의 기본 개념
| 용어 | 의미 |
|---|
| Base Class | 가장 기본적인 클래스로 다른 클래스가 상속받을 수 있는 클래스이다 |
| Parent Class / Super Class | 다른 클래스에게 상속을 제공하는 기존 클래스이다 |
| Child Class / Sub Class | 다른 클래스를 상속받아 기능을 확장하는 새로운 클래스이다 |
상속의 개념: 저장 속성의 추가
- 상속은 기존 클래스(Base Class, Super Class)의 저장 속성과 메서드를 재사용하면서 새로운 저장 속성을 추가하는 것이다
- 즉, 기존 클래스의 데이터(저장 속성)을 확장하거나 기능(메서드)을 변형시키는 것이 상속의 주요 목적이다
상속의 기본 문법
class SuperClass {
var name = "기본 이름"
}
class SubClass: SuperClass {
var age = 0
}
SubClass 는 SuperClass 를 상속하여 name 속성을 물려받고, 새로운 저장 속성(age) 을 추가한다
상속의 예
상위 클래스 - Person 클래스 (기본 클래스 - Base Class)
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("안녕하세요, 저는 \(name)이고, 나이는 \(age)살입니다")
}
}
Person 클래스는 가장 기본적인 클래스이며, 이름과 나이를 저장하는 저장 속성(데이터) 을 가지고 있다
introduce() 메서드는 자신의 이름과 나이를 출력하는 기능(메서드) 이다
- 이 클래스는 다른 클래스들이 상속하여 사용할 수 있는 기본 클래스(
Base Class) 또는 부모 클래스(Parent Class) 역할을 한다
하위 클래스 - Employee 클래스 (Person을 상속받음)
class Employee: Person {
var employeeID: Int
init(name: String, age: Int, employeeID: Int) {
self.employeeID = employeeID
super.init(name: name, age: age)
}
}
Employee 클래스는 Person 클래스를 상속하여 기존 속성(name, age)을 그대로 사용한다
- 새로운 저장 속성(
employeeID)을 추가하여 기능을 확장한다
super.init() 을 사용하여 부모 클래스의 초기화 메서드를 호출한다
상속 금지된 클래스 - Manager 클래스 (final 키워드 사용)
final class Manager: Employee {
var department: String
init(name: String, age: Int, employeeID: Int, department: String) {
self.department = department
super.init(name: name, age: age, employeeID: employeeID)
}
}
Manager 클래스는 Employee 클래스를 상속받지만, final 키워드를 사용하여 상속이 불가능하게 만든다
- 이 클래스는 더 이상 다른 클래스가 상속받을 수 없다
상속 예제 사용하기 (코드 실행 예시)
let person = Person(name: "Royce", age: 20)
person.introduce()
let employee = Employee(name: "Steve", age: 28, employeeID: 101)
employee.introduce()
let manager = Manager(name: "Charlie", age: 35, employeeID: 201, department: "Engineering")
manager.introduce()
final 키워드의 특징
final 클래스:
- 클래스 선언 앞에
final 키워드를 붙이면 더 이상 상속할 수 없다
- 예:
final class Manager { ... }
final 메서드:
- 메서드 선언 앞에
final 키워드를 붙이면 해당 메서드를 하위 클래스에서 재정의할 수 없다
- 예:
final func introduce() { ... }
- 사용 이유:
- 특정 클래스를 상속하여 잘못 수정되는 것을 방지하기 위해 사용한다
- 보안적인 이유로 외부에서 클래스를 확장하지 못하도록 막을 때 사용한다
요약
- 상속: 클래스가 다른 클래스의 속성과 메서드를 물려받아 사용하는 기능이다
- 상속의 본질: 기존 클래스의 저장 속성(데이터) 을 확장하거나 기능(메서드) 을 추가하는 것이다
- 상속의 문법:
class SubClass: SuperClass { }
- 상속 방지(
final): 클래스 또는 메서드를 상속하지 못하도록 제한한다
super.init(): 부모 클래스의 초기화 메서드를 호출하는 필수 코드이다
재정의(Overriding)
오버로딩(Overloading) vs 오버라이딩(Overriding)
오버로딩(Overloading)
- 함수 이름은 같지만, 매개변수의 타입 또는 개수가 다른 여러 함수를 정의하는 것을 의미한다
- 함수 이름이 동일해도 다양한 방식으로 사용할 수 있게 하는 기능이다
- 오버로딩은 함수의 이름이 동일해도 파라미터의 종류나 개수로 구분하여 처리할 수 있게 만들어준다
func printInfo(name: String) {
print("이름: \(name)")
}
func printInfo(name: String, age: Int) {
print("이름: \(name), 나이: \(age)")
}
func printInfo(name: String, address: String) {
print("이름: \(name), 주소: \(address)")
}
- 동일한 이름의 함수
printInfo()가 매개변수의 타입 또는 개수에 따라 다르게 정의되어 있다
- 오버로딩을 통해 함수 이름을 통일하면서 다양한 기능을 제공할 수 있다
오버라이딩(Overriding)
- 클래스 상속에서 부모 클래스의 메서드 또는 속성을 재정의하여 새로운 기능으로 변경하는 것을 의미한다
- 하위 클래스에서 상위 클래스의 기능을 수정하거나 확장할 때 사용한다
override 키워드를 사용하여 선언한다
- 오버로딩과는 다르게 함수의 이름과 파라미터 구조가 동일할 때 발생한다
메서드 오버라이딩 예제
부모 클래스 (Animal)
class Animal {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func makeSound() {
print("동물이 소리를 낸다")
}
func sleep() {
print("\(name)가 잠을 잔다")
}
}
자식 클래스 (Dog)
class Dog: Animal {
var breed: String
init(name: String, age: Int, breed: String) {
self.breed = breed
super.init(name: name, age: age)
}
override func makeSound() {
print("멍멍!")
}
func fetch() {
print("\(name)가 공을 물어온다")
}
}
자식 클래스 (Cat)
class Cat: Animal {
var color: String
init(name: String, age: Int, color: String) {
self.color = color
super.init(name: name, age: age)
}
override func makeSound() {
print("야옹!")
}
override func sleep() {
super.sleep()
print("특히 낮잠을 좋아한다")
}
}
Dog 와 Cat 클래스는 Animal 클래스를 상속받아 기존 메서드를 변경하거나 확장하였다
makeSound() 메서드는 각각 멍멍!과 야옹! 이라는 소리를 내도록 재정의되었다
Cat 클래스의 sleep() 메서드는 부모의 기능을 확장하여 특히 낮잠을 좋아한다 라는 메시지를 추가로 출력하도록 하였다
super.sleep() 를 사용하여 부모의 기능을 그대로 사용하고 확장하는 방식을 보여준다
저장 속성과 메서드의 메모리 저장 방식의 차이
저장 속성의 저장 방식
- 저장 속성(Stored Property) 는 실제로 메모리의 Heap 영역에 저장되며, 고유한 메모리 주소값을 가진다
- 각 인스턴스마다 별도의 메모리 공간을 가지므로, 다른 인스턴스에서 동일한 속성을 변경하더라도 서로 영향을 미치지 않는다
- 하위 클래스에서 저장 속성을 재정의하려면 메모리 주소 자체를 변경해야 하는데, 이것은 허용되지 않는다 (Swift의 안전성을 위한 설계)
메서드의 저장 방식
- 메서드(Method) 는 실제로 메모리의 Code 영역에 함수의 주소값(포인터)을 배열 형태로 저장한다
- 인스턴스가 생성될 때, 클래스의 메서드는 Code 영역에 미리 저장되어 있는 메서드 테이블(Method Table)에 등록된다
- 이 테이블은 상속 관계에서 메서드를 재정의하면, 기존 주소값이 새로운 메서드 주소값으로 바뀌게 된다 (동적 디스패치)
- 하위 클래스에서 부모 클래스의 메서드를 변경할 수 있는 이유는, 메서드가 고유한 메모리 주소값을 가지는 것이 아니라 테이블에서 참조하는 방식이기 때문이다
저장 속성과 메서드의 메모리 저장 구조
Heap 영역 (저장 속성)
────────────────────
[Dog 인스턴스]
- name (고유 메모리 주소값)
[Cat 인스턴스]
- name (고유 메모리 주소값)
Code 영역 (메서드 테이블)
────────────────────
[Animal]
- makeSound() -> "동물이 소리를 낸다"
[Dog]
- makeSound() -> "멍멍!" (테이블의 주소값이 변경됨)
[Cat]
- makeSound() -> "야옹!"
저장 속성의 재정의 예제 (계산 속성으로 변환)
부모 클래스 (Vehicle)
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "현재 속도는 \(currentSpeed) km/h 이다"
}
}
자식 클래스 (SportsCar)
class SportsCar: Vehicle {
override var description: String {
get {
return "이 스포츠카의 속도는 \(currentSpeed) km/h 이다"
}
set {
print("설명 변경: \(newValue)")
}
}
}
Vehicle 클래스는 currentSpeed 라는 저장 속성을 가지고 있으며, description 은 읽기 전용 속성으로 정의되었다
SportsCar 클래스는 description 속성을 오버라이딩하여 읽기 / 쓰기 가능한 계산 속성으로 변환하였다
- 기존 저장 속성의 값을 활용하여, 새로운 기능을 추가할 수 있는 구조로 변경한 예제이다
- 저장 속성을 계산 속성으로 변환하여 재정의하는 것은 가능하지만, 반대는 불가능하다
속성 감시자 오버라이딩 예제 (계산 속성과의 관계)
부모 클래스 (Person)
class Person {
var name: String
var age: Int {
willSet {
print("나이가 \(age)에서 \(newValue)로 변경될 예정입니다.")
}
didSet {
print("나이가 \(oldValue)에서 \(age)로 변경되었습니다.")
}
}
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
자식 클래스 (Student)
class Student: Person {
override var age: Int {
willSet {
print("학생의 나이가 \(age)에서 \(newValue)로 바뀌려고 합니다.")
}
didSet {
print("학생의 나이가 \(oldValue)에서 \(age)로 바뀌었습니다.")
}
}
}
Person 클래스는 age 라는 저장 속성에 속성 감시자(willSet, didSet)를 사용하여 값의 변화를 감지한다.
Student 클래스는 Person 클래스를 상속받아 age 속성을 오버라이딩하고, 새로운 감시자 구현을 추가하였다
- 부모 클래스에서 정의된 감시자 기능을 확장하거나 변경할 수 있다
- 계산 속성에는 속성 감시자를 추가할 수 없다
- 하지만, 계산 속성을 오버라이딩하여 기능을 변경하는 것은 가능하다
오버라이딩 예제 사용하기
let person = Person(name: "Royce", age: 25)
person.age = 26
let student = Student(name: "Steve", age: 20)
student.age = 21
let dog = Dog(name: "바둑이", age: 3, breed: "포메라니안")
dog.makeSound()
dog.sleep()
dog.fetch()
let cat = Cat(name: "나비", age: 2, color: "검정")
cat.makeSound()
cat.sleep()
let car = Vehicle()
car.currentSpeed = 80
print(car.description)
let sportsCar = SportsCar()
sportsCar.currentSpeed = 150
print(sportsCar.description)
sportsCar.description = "빠른 스포츠카"
Person & Student 클래스 사용 예시 (속성 감시자 오버라이딩)
Person 클래스의 age 속성은 부모 클래스의 감시자가 실행된다
Student 클래스의 age 속성은 감시자가 오버라이딩되어 새롭게 정의된 방식으로 실행된다
Dog & Cat 클래스 사용 예시 (메서드 오버라이딩)
Dog 클래스의 makeSound() 메서드는 멍멍! 으로 재정의되었다
Cat 클래스의 makeSound() 메서드는 야옹! 으로 재정의되었다
Cat 클래스의 sleep() 메서드는 super.sleep() 를 호출하여 부모 클래스의 기능을 확장하였다
Vehicle & SportsCar 클래스 사용 예시 (저장 속성을 계산 속성으로 변환)
SportsCar 클래스의 description 속성은 계산 속성으로 변환되었다
- 기존의 속성을 유지하면서 새로운 기능을 추가할 수 있게 되었다
오버라이딩의 특징 정리
- 저장 속성은 재정의할 수 없다 (메모리 주소값이 고유하기 때문이다)
- 저장 속성은 인스턴스마다 고유의 메모리 주소값을 가지므로 변경할 수 없다
- 하위 클래스에서 기존 저장 속성을 그대로 사용하거나, 추가적인 저장 속성을 정의할 수는 있지만 수정은 불가능하다
- 하지만, 저장 속성을 계산 속성으로 변환하여 기능을 추가하는 것은 가능하다
- 예시:
Vehicle 클래스의 description 속성을 SportsCar 클래스에서 계산 속성으로 변환한 예제
- 메서드는 재정의할 수 있다 (코드 영역의 주소값을 변경할 수 있기 때문이다)
- 메서드는 코드 영역의 메서드 테이블(Method Table)에 저장되며, 상속받은 메서드를 하위 클래스에서 변경할 수 있다
- 부모 클래스의 메서드를 완전히 새로운 기능으로 변경할 수도 있고, 기존 기능을 유지하면서 추가 기능을 더할 수도 있다
super 키워드를 사용하여 부모 클래스의 기능을 호출하고 확장할 수 있다
- 예시:
Dog 클래스와 Cat 클래스에서 Animal 클래스의 makeSound() 메서드를 재정의하여 각각 멍멍!, 야옹! 으로 변경하였다
- 속성 감시자의 재정의는 가능하다 (단, 계산 속성에는 적용할 수 없다)
- 부모 클래스의 저장 속성에 대한 속성 감시자는 하위 클래스에서 재정의할 수 있다
willSet과 didSet 감시자를 오버라이딩하여 새로운 기능을 추가하거나 변경할 수 있다
- 계산 속성에는 속성 감시자를 직접 추가할 수 없지만, 계산 속성을 오버라이딩하여 기능을 변경하는 것은 가능하다
- 예시:
Person 클래스의 age 속성을 Student 클래스에서 감시자 기능을 변경한 예제
- 속성 재정의의 규칙 (읽기/쓰기 여부 관련)
- 부모 클래스의 저장 속성은 하위 클래스에서 계산 속성으로 재정의할 수 있다
- 부모 클래스의 읽기 전용 계산 속성은 하위 클래스에서 읽기 / 쓰기 가능한 계산 속성으로 확장할 수 있다
- 하지만, 읽기 / 쓰기 가능한 계산 속성을 읽기 전용으로 축소하는 것은 불가능하다
- 읽기 전용으로 변경하면 부모 클래스의 기능을 제한하는 것이기 때문에 허용되지 않는다
override 키워드 사용
- 부모 클래스의 메서드, 계산 속성, 속성 감시자를 재정의할 때 반드시 사용해야 한다
- Swift에서는 오버라이딩을 안전하게 하기 위해
override 키워드를 사용하여 의도를 명확히 해야 한다
- 사용하지 않으면 컴파일 오류가 발생한다 (Swift의 안전성을 보장하기 위한 규칙)
super 키워드 사용
- 부모 클래스의 기능을 호출하고, 그 기능을 기반으로 추가 기능을 구현하거나 확장할 때 사용한다
- 주로 메서드 오버라이딩에서 사용되지만, 속성 오버라이딩에서도 사용될 수 있다
- 예시:
Cat 클래스의 sleep() 메서드에서 super.sleep() 호출하여 부모의 기능을 확장
요약
- 오버로딩: 같은 이름의 함수가 매개변수의 타입이나 개수로 구분되는 기능이다
- 오버라이딩: 상속받은 클래스의 메서드 또는 속성을 재정의하여 기능을 수정하거나 확장하는 기능이다
- 저장 속성은 재정의할 수 없다 (메모리 주소값이 고유하기 때문이다)
- 저장 속성을 계산 속성으로 변환하여 재정의하는 것은 가능하다 (저장 속성 자체는 수정할 수 없지만, 계산 속성으로 변경하여 기능을 추가할 수 있다)
- 메서드는 재정의할 수 있다 (코드 영역의 주소값을 변경하여 새로운 기능을 정의할 수 있다)
- 속성 감시자의 재정의는 가능하다 (계산 속성에는 적용할 수 없다)
override 키워드: 부모 클래스의 기능을 재정의할 때 사용한다
super 키워드: 부모 클래스의 기능을 호출하거나 확장할 때 사용한다
- 오버라이딩은 기존 기능을 확장하거나 수정할 때 유용하게 사용된다