TIL: 클래스(class)의 상속과 재정의

Royce·2025년 3월 22일

Swift 문법

목록 보기
33/63

상속(Inheritance)

  • 상속(Inheritance)클래스가 다른 클래스의 특성을 물려받아 사용하는 것을 의미한다
  • 새로운 클래스를 정의할 때, 기존 클래스의 속성(저장 속성)과 메서드(기능) 를 그대로 물려받아 사용할 수 있다
  • 상속은 Swift에서 클래스(Class) 에서만 사용 가능하며, 구조체(Struct) 나 열거형(Enum) 에서는 사용할 수 없다

상속의 기본 개념

용어의미
Base Class가장 기본적인 클래스로 다른 클래스가 상속받을 수 있는 클래스이다
Parent Class / Super Class다른 클래스에게 상속을 제공하는 기존 클래스이다
Child Class / Sub Class다른 클래스를 상속받아 기능을 확장하는 새로운 클래스이다

상속의 개념: 저장 속성의 추가

  • 상속은 기존 클래스(Base Class, Super Class)의 저장 속성과 메서드를 재사용하면서 새로운 저장 속성을 추가하는 것이다
  • 즉, 기존 클래스의 데이터(저장 속성)을 확장하거나 기능(메서드)을 변형시키는 것이 상속의 주요 목적이다

상속의 기본 문법

class SuperClass {  // 부모 클래스 (Base Class / Parent Class / Super Class)
    var name = "기본 이름"
}

class SubClass: SuperClass {  // 자식 클래스 (Child Class / Sub Class)
    var age = 0  // 새로운 저장 속성 추가
}
  • SubClassSuperClass 를 상속하여 name 속성을 물려받고, 새로운 저장 속성(age) 을 추가한다

상속의 예

상위 클래스 - Person 클래스 (기본 클래스 - Base Class)

class Person {  // Base Class, Parent Class, Super Class
    var name: String       // 저장 속성 (이름)
    var age: Int           // 저장 속성 (나이)
    
    // 기본 생성자(Initializer)
    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 {  // 하위 클래스 (Child Class, Sub Class)
    var employeeID: Int  // 새로운 저장 속성 추가 (직원 번호)
    
    // 초기화 메서드(Initializer)
    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 {  // 상속이 불가능한 클래스 (final 사용)
    var department: String  // 새로운 저장 속성 추가 (부서 이름)
    
    // 초기화 메서드(Initializer)
    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()   
// 출력: 안녕하세요, 저는 Royce이고, 나이는 20살입니다

let employee = Employee(name: "Steve", age: 28, employeeID: 101)
employee.introduce()  
// 출력: 안녕하세요, 저는 Steve이고, 나이는 28살입니다  (부모 클래스의 메서드 사용)

let manager = Manager(name: "Charlie", age: 35, employeeID: 201, department: "Engineering")
manager.introduce()  
// 출력: 안녕하세요, 저는 Charlie이고, 나이는 35살입니다  (부모 클래스의 메서드 사용)

final 키워드의 특징

  1. final 클래스:
    • 클래스 선언 앞에 final 키워드를 붙이면 더 이상 상속할 수 없다
    • 예: final class Manager { ... }
  2. final 메서드:
    • 메서드 선언 앞에 final 키워드를 붙이면 해당 메서드를 하위 클래스에서 재정의할 수 없다
    • 예: final func introduce() { ... }
  3. 사용 이유:
    • 특정 클래스를 상속하여 잘못 수정되는 것을 방지하기 위해 사용한다
    • 보안적인 이유로 외부에서 클래스를 확장하지 못하도록 막을 때 사용한다

요약

  • 상속: 클래스가 다른 클래스의 속성과 메서드를 물려받아 사용하는 기능이다
  • 상속의 본질: 기존 클래스의 저장 속성(데이터) 을 확장하거나 기능(메서드) 을 추가하는 것이다
  • 상속의 문법: 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("특히 낮잠을 좋아한다")
    }
}
  • DogCat 클래스는 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 속성을 오버라이딩하고, 새로운 감시자 구현을 추가하였다
  • 부모 클래스에서 정의된 감시자 기능을 확장하거나 변경할 수 있다
  • 계산 속성에는 속성 감시자를 추가할 수 없다
  • 하지만, 계산 속성을 오버라이딩하여 기능을 변경하는 것은 가능하다

오버라이딩 예제 사용하기

// Person 클래스를 상속받은 Student 클래스 사용하기
let person = Person(name: "Royce", age: 25)
person.age = 26
/*
출력:
나이가 25에서 26로 변경될 예정입니다.
나이가 25에서 26로 변경되었습니다.
*/

let student = Student(name: "Steve", age: 20)
student.age = 21
/*
출력:
학생의 나이가 20에서 21로 바뀌려고 합니다.
학생의 나이가 20에서 21로 바뀌었습니다.
*/

// Animal 클래스를 상속받은 Dog 클래스 사용하기
let dog = Dog(name: "바둑이", age: 3, breed: "포메라니안")
dog.makeSound()   // 출력: 멍멍!
dog.sleep()       // 출력: 바둑이가 잠을 잔다
dog.fetch()       // 출력: 바둑이가 공을 물어온다

// Animal 클래스를 상속받은 Cat 클래스 사용하기
let cat = Cat(name: "나비", age: 2, color: "검정")
cat.makeSound()   // 출력: 야옹!
cat.sleep()       // 출력: 나비가 잠을 잔다. 특히 낮잠을 좋아한다

// Vehicle 클래스를 상속받은 SportsCar 클래스 사용하기
let car = Vehicle()
car.currentSpeed = 80
print(car.description)  // 출력: 현재 속도는 80.0 km/h 이다

let sportsCar = SportsCar()
sportsCar.currentSpeed = 150
print(sportsCar.description)  // 출력: 이 스포츠카의 속도는 150.0 km/h 이다
sportsCar.description = "빠른 스포츠카"  // 출력: 설명 변경: 빠른 스포츠카
  1. Person & Student 클래스 사용 예시 (속성 감시자 오버라이딩)
    • Person 클래스의 age 속성은 부모 클래스의 감시자가 실행된다
    • Student 클래스의 age 속성은 감시자가 오버라이딩되어 새롭게 정의된 방식으로 실행된다
  2. Dog & Cat 클래스 사용 예시 (메서드 오버라이딩)
    • Dog 클래스의 makeSound() 메서드는 멍멍! 으로 재정의되었다
    • Cat 클래스의 makeSound() 메서드는 야옹! 으로 재정의되었다
    • Cat 클래스의 sleep() 메서드는 super.sleep() 를 호출하여 부모 클래스의 기능을 확장하였다
  3. Vehicle & SportsCar 클래스 사용 예시 (저장 속성을 계산 속성으로 변환)
    • SportsCar 클래스의 description 속성은 계산 속성으로 변환되었다
    • 기존의 속성을 유지하면서 새로운 기능을 추가할 수 있게 되었다

오버라이딩의 특징 정리

  1. 저장 속성은 재정의할 수 없다 (메모리 주소값이 고유하기 때문이다)
    • 저장 속성은 인스턴스마다 고유의 메모리 주소값을 가지므로 변경할 수 없다
    • 하위 클래스에서 기존 저장 속성을 그대로 사용하거나, 추가적인 저장 속성을 정의할 수는 있지만 수정은 불가능하다
    • 하지만, 저장 속성을 계산 속성으로 변환하여 기능을 추가하는 것은 가능하다
    • 예시: Vehicle 클래스의 description 속성을 SportsCar 클래스에서 계산 속성으로 변환한 예제
  2. 메서드는 재정의할 수 있다 (코드 영역의 주소값을 변경할 수 있기 때문이다)
    • 메서드는 코드 영역의 메서드 테이블(Method Table)에 저장되며, 상속받은 메서드를 하위 클래스에서 변경할 수 있다
    • 부모 클래스의 메서드를 완전히 새로운 기능으로 변경할 수도 있고, 기존 기능을 유지하면서 추가 기능을 더할 수도 있다
    • super 키워드를 사용하여 부모 클래스의 기능을 호출하고 확장할 수 있다
    • 예시: Dog 클래스와 Cat 클래스에서 Animal 클래스의 makeSound() 메서드를 재정의하여 각각 멍멍!, 야옹! 으로 변경하였다
  3. 속성 감시자의 재정의는 가능하다 (단, 계산 속성에는 적용할 수 없다)
    • 부모 클래스의 저장 속성에 대한 속성 감시자는 하위 클래스에서 재정의할 수 있다
    • willSetdidSet 감시자를 오버라이딩하여 새로운 기능을 추가하거나 변경할 수 있다
    • 계산 속성에는 속성 감시자를 직접 추가할 수 없지만, 계산 속성을 오버라이딩하여 기능을 변경하는 것은 가능하다
    • 예시: Person 클래스의 age 속성을 Student 클래스에서 감시자 기능을 변경한 예제
  4. 속성 재정의의 규칙 (읽기/쓰기 여부 관련)
    • 부모 클래스의 저장 속성은 하위 클래스에서 계산 속성으로 재정의할 수 있다
    • 부모 클래스의 읽기 전용 계산 속성은 하위 클래스에서 읽기 / 쓰기 가능한 계산 속성으로 확장할 수 있다
    • 하지만, 읽기 / 쓰기 가능한 계산 속성을 읽기 전용으로 축소하는 것은 불가능하다
    • 읽기 전용으로 변경하면 부모 클래스의 기능을 제한하는 것이기 때문에 허용되지 않는다
  5. override 키워드 사용
    • 부모 클래스의 메서드, 계산 속성, 속성 감시자를 재정의할 때 반드시 사용해야 한다
    • Swift에서는 오버라이딩을 안전하게 하기 위해 override 키워드를 사용하여 의도를 명확히 해야 한다
    • 사용하지 않으면 컴파일 오류가 발생한다 (Swift의 안전성을 보장하기 위한 규칙)
  6. super 키워드 사용
    • 부모 클래스의 기능을 호출하고, 그 기능을 기반으로 추가 기능을 구현하거나 확장할 때 사용한다
    • 주로 메서드 오버라이딩에서 사용되지만, 속성 오버라이딩에서도 사용될 수 있다
    • 예시: Cat 클래스의 sleep() 메서드에서 super.sleep() 호출하여 부모의 기능을 확장

요약

  • 오버로딩: 같은 이름의 함수가 매개변수의 타입이나 개수로 구분되는 기능이다
  • 오버라이딩: 상속받은 클래스의 메서드 또는 속성을 재정의하여 기능을 수정하거나 확장하는 기능이다
  • 저장 속성은 재정의할 수 없다 (메모리 주소값이 고유하기 때문이다)
  • 저장 속성을 계산 속성으로 변환하여 재정의하는 것은 가능하다 (저장 속성 자체는 수정할 수 없지만, 계산 속성으로 변경하여 기능을 추가할 수 있다)
  • 메서드는 재정의할 수 있다 (코드 영역의 주소값을 변경하여 새로운 기능을 정의할 수 있다)
  • 속성 감시자의 재정의는 가능하다 (계산 속성에는 적용할 수 없다)
  • override 키워드: 부모 클래스의 기능을 재정의할 때 사용한다
  • super 키워드: 부모 클래스의 기능을 호출하거나 확장할 때 사용한다
  • 오버라이딩은 기존 기능을 확장하거나 수정할 때 유용하게 사용된다
profile
iOS 개발자 지망생

0개의 댓글