정보/레벨 1

상속(Inheritance)과 프로토콜(Protocol)의 차이점은 무엇인가요?

밤새는 탐험가89 2024. 11. 14. 06:16

상속(Inheritance)과 프로토콜(Protocol)은 Swift에서 객체지향 프로그래밍(OOP) 및 프로토콜 지향 프로그래밍(POP)을 구현하기 위한 두 가지 주요 개념입니다. 이 둘은 비슷한 목적을 가지고 있지만 사용 방식과 목적에서 몇 가지 차이점이 있습니다.

 

1. 상속(Inheritance)

  • 정의: 상속은 클래스가 다른 클래스의 속성과 메서드를 물려받는 기능입니다. Swift에서는 단일 상속만 지원되며, 하나의 클래스는 하나의 부모 클래스만 가질 수 있습니다.
  • 사용 목적: 상속을 통해 코드 재사용이 가능하며, 부모 클래스의 기능을 확장하거나 수정하기 위해 사용됩니다.
  • 예시: 부모 클래스에 있는 기본 속성이나 메서드를 자식 클래스에서 사용하거나 오버라이딩하여 고유한 기능을 추가할 수 있습니다.
class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func makeSound() {
        print("Some sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Woof!")
    }
}

let myDog = Dog(name: "Buddy")
myDog.makeSound() // "Woof!" 출력

 

  • 특징:
    • 재사용성: 부모 클래스의 속성과 메서드를 자식 클래스에서 그대로 재사용할 수 있습니다.
    • 오버라이딩(Overriding): 자식 클래스에서 부모 클래스의 메서드 또는 속성을 재정의할 수 있습니다.
    • 단일 상속: Swift는 단일 상속만 허용하므로, 하나의 클래스가 여러 부모 클래스를 상속받을 수 없습니다.
    • 클래스 타입에서만 사용 가능: 상속은 클래스에만 적용되며, 구조체나 열거형에는 사용할 수 없습니다.

 

2. 프로토콜(Protocol)

  • 정의: 프로토콜은 특정 기능이나 속성의 요구 사항을 정의하는 틀입니다. 클래스, 구조체, 열거형이 프로토콜을 채택하고 구현할 수 있습니다.
  • 사용 목적: 여러 타입이 공통적으로 구현해야 하는 기능을 정의하는 데 사용됩니다. 다중 프로토콜 채택이 가능하므로, 클래스나 구조체, 열거형이 여러 프로토콜을 채택하여 다양한 기능을 구현할 수 있습니다.
  • 예시: Animal이라는 공통 기능을 여러 타입에 정의하고, 각 타입에 맞게 구현할 수 있습니다.
protocol Animal {
    var name: String { get set }
    func makeSound()
}

struct Dog: Animal {
    var name: String
    
    func makeSound() {
        print("Woof!")
    }
}

struct Cat: Animal {
    var name: String
    
    func makeSound() {
        print("Meow!")
    }
}

let myDog = Dog(name: "Buddy")
myDog.makeSound() // "Woof!" 출력

let myCat = Cat(name: "Whiskers")
myCat.makeSound() // "Meow!" 출력

 

특징:

  • 다중 채택: 한 타입이 여러 프로토콜을 채택할 수 있습니다.
  • 유연성: 클래스뿐만 아니라 구조체와 열거형에서도 프로토콜을 채택할 수 있어 다양한 타입에서 공통 기능을 정의할 수 있습니다.
  • 추상적인 요구 사항 정의: 프로토콜은 실제 구현을 포함하지 않고, 구현해야 할 메서드와 속성의 청사진만 제공합니다.
  • 확장 가능성: 프로토콜 익스텐션을 사용해 모든 프로토콜을 채택한 타입에 기본 구현을 제공할 수 있습니다.

 

3. 상속과 프로토콜의 차이점 비교

특성 상속 프로토콜
주요 사용 대상 클래스(Class) 클래스, 구조체, 열거형
상속 관계 단일 상속만 가능 다중 프로토콜 채택 가능
목적 코드 재사용 및 클래스 계층 구조 정의 특정 기능 요구 사항 정의, 다형성 구현
오버라이딩 메서드 오버라이딩 가능 프로토콜 자체에 구현이 없고 채택한 타입이 구현
확장성 클래스 상속 체계 내에서만 확장 가능 프로토콜 익스텐션을 통해 모든 타입에 확장 가능
추상성 구체적인 구현 포함 가능 요구 사항만 정의, 구현은 타입에서 수행

 

 

4. 상속과 프로토콜의 선택 기준

  • 상속은 클래스 계층이 명확하게 정의되어 있고, 자식 클래스가 부모 클래스의 속성과 메서드를 상속받아야 할 때 사용합니다. 상속은 코드 재사용이 필요한 경우와 기본적인 동작을 자식 클래스에서 확장할 때 유용합니다.
  • 프로토콜은 클래스뿐만 아니라 구조체와 열거형에서도 사용할 수 있어, 특정 기능을 여러 타입에 걸쳐 공통적으로 구현해야 할 때 적합합니다. 다중 프로토콜 채택이 가능하므로 특정 기능 요구 사항을 여러 타입에서 유연하게 구현할 수 있습니다.

 

5. 상속과 프로토콜을 함께 사용하는 경우

Swift에서는 상속과 프로토콜을 함께 사용할 수도 있습니다. 클래스 상속을 통해 공통 동작을 물려받으면서, 프로토콜을 채택해 추가적인 기능 요구 사항을 정의할 수 있습니다.

protocol SoundMaking {
    func makeSound()
}

class Animal {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Dog: Animal, SoundMaking {
    func makeSound() {
        print("Woof!")
    }
}

let myDog = Dog(name: "Buddy")
myDog.makeSound() // "Woof!" 출력

 

 

 

클래스 상속을 사용할 때의 장단점은 무엇인가요?

클래스 상속의 장점

  1. 코드 재사용:
    • 부모 클래스의 속성과 메서드를 자식 클래스에서 그대로 상속받을 수 있어 코드 중복을 줄이고, 코드 재사용성을 높입니다.
    • 공통 기능을 부모 클래스에 구현함으로써 여러 자식 클래스에서 공유할 수 있습니다.
  2. 확장성:
    • 상속을 통해 기존 클래스에 기능을 추가하거나, 기존 메서드를 오버라이드하여 특정 동작을 변경할 수 있어, 유지 보수와 확장이 용이합니다.
    • 자식 클래스에서 부모 클래스의 기본 동작을 활용하면서 필요한 기능을 추가할 수 있습니다.
  3. 계층 구조 형성:
    • 상속을 사용하면 객체 계층 구조를 정의할 수 있습니다. 이 계층 구조는 개념적으로 연관된 클래스 간의 관계를 명확히 하여, 프로그램의 구조를 이해하기 쉽게 만듭니다.
    • 상위 클래스와 하위 클래스 간의 계층을 통해 특정 유형의 객체를 그룹화하고 관리하기 쉽습니다.
  4. 다형성(Polymorphism) 활용:
    • 부모 클래스 타입의 참조를 통해 자식 클래스의 인스턴스를 다룰 수 있어, 다형성을 활용한 유연한 코드를 작성할 수 있습니다.
    • 이를 통해 자식 클래스의 고유 기능을 통해 동일한 부모 클래스 타입을 확장할 수 있습니다.

 

클래스 상속의 단점

  1. 강한 결합(Coupling):
    • 상속은 자식 클래스가 부모 클래스에 의존하게 만들어, 부모 클래스의 변경이 자식 클래스에 영향을 미칠 수 있습니다.
    • 부모 클래스의 수정이 필요할 때, 해당 클래스를 상속하는 모든 자식 클래스에서 예상치 못한 오류가 발생할 가능성이 있어 유지보수에 부담이 될 수 있습니다.
  2. 복잡한 계층 구조:
    • 상속 계층이 깊어질수록 클래스 구조가 복잡해져서 가독성과 관리가 어려워집니다. 또한, 다수의 상속 계층이 있는 경우, 클래스 간 관계를 파악하기가 까다로울 수 있습니다.
    • 불필요하게 깊은 계층 구조는 설계의 유연성을 떨어뜨리며, 코드의 이해도와 유지 보수를 어렵게 만듭니다.
  3. 다중 상속 제한:
    • Swift는 단일 상속만 허용하기 때문에, 하나의 클래스가 여러 부모 클래스를 상속받을 수 없습니다.
    • 여러 부모 클래스로부터 상속이 필요할 경우, 상속 대신 프로토콜을 활용하는 것이 필요합니다.
  4. 유연성 부족:
    • 상속을 통해 자식 클래스는 부모 클래스의 특성을 물려받지만, 이는 때로는 제한적이어서 모든 상황에 적합하지 않을 수 있습니다. 자식 클래스에서 불필요한 부모 클래스의 기능까지 물려받는 경우도 발생할 수 있습니다.
    • 이로 인해 상속보다는 프로토콜과 컴포지션(구성)을 사용하는 것이 더 유연하고 적합한 경우가 많습니다.
  5. 캡슐화 문제:
    • 상속을 사용하면 부모 클래스의 내부 구현이 자식 클래스에 노출될 수 있어 캡슐화가 깨질 수 있습니다.
    • 자식 클래스가 부모 클래스의 세부 구현에 의존하게 되면, 부모 클래스의 내부 구현을 수정할 때 자식 클래스에서 부작용이 발생할 가능성이 있습니다.

 

클래스 상속의 적절한 사용 예와 대안

  • 상속은 클래스 간 명확한 'is-a' 관계가 있을 때 적합합니다. 예를 들어, Bird는 Animal의 한 종류이므로 Bird 클래스가 Animal 클래스를 상속하는 것은 적절합니다.
  • 그러나 여러 기능을 조합하여 객체를 구성해야 하는 경우, 상속 대신 프로토콜이나 **컴포지션(구성)**을 사용하는 것이 더 유리할 수 있습니다. 예를 들어, Flyable 프로토콜을 정의하고 여러 클래스가 채택하여 fly() 메서드를 구현하게 하면 상속을 통해 깊은 계층을 형성하지 않고도 객체에 특정 기능을 부여할 수 있습니다.

 

요약

  • 장점: 코드 재사용, 확장성, 계층 구조 형성, 다형성 활용이 가능해 클래스 간의 관계를 명확히 하고, 공통 동작을 재사용할 수 있습니다.
  • 단점: 강한 결합으로 인한 유지보수 문제, 깊은 계층 구조로 인한 복잡성, 단일 상속 제한, 유연성 부족, 캡슐화 문제로 인해 설계의 유연성을 저해할 수 있습니다.

상속은 강력하지만, 모든 상황에서 사용하기보다는 적절한 상황에 맞춰 사용하고, 필요에 따라 프로토콜과 컴포지션을 활용하는 것이 더 유연하고 유지보수에 적합한 설계를 만드는 데 도움이 됩니다.

 

 

다중 상속(Multiple Inheritance)이 불가능한 이유는 무엇인가요?

  • 다이아몬드 문제(Diamond Problem):
    • 다중 상속을 허용하면 동일한 상위 클래스가 두 개 이상의 경로로 상속되는 상황이 발생할 수 있습니다. 예를 들어, 클래스 A가 B와 C의 부모 클래스이고, D 클래스가 B와 C를 동시에 상속할 때 다이아몬드 형태의 상속 구조가 생깁니다.
    • 이 경우 D 클래스가 A 클래스의 어떤 인스턴스를 상속받을지 명확하지 않아, A 클래스의 속성과 메서드가 중복되거나 모호해집니다.
    • 이러한 문제를 해결하려면 복잡한 규칙을 정의해야 하며, 이는 코드의 복잡성과 오류 가능성을 증가시킵니다.
    A
   / \
  B   C
   \ /
    D
  • 모호성 문제:
    • 다중 상속을 통해 부모 클래스들이 동일한 이름의 메서드나 속성을 정의하고 있을 때, 자식 클래스에서 어떤 부모 클래스의 메서드 또는 속성을 사용할지 모호해질 수 있습니다.
    • 이로 인해 개발자가 의도하지 않은 동작이 발생할 수 있으며, 다중 상속이 허용된 언어에서는 이를 해결하기 위해 복잡한 규칙이나 키워드(예: super)를 추가로 정의해야 합니다. 이는 코드의 가독성과 유지보수를 어렵게 만듭니다.
  • 복잡한 상속 구조:
    • 다중 상속을 허용하면 상속 관계가 복잡해지기 쉽습니다. 다수의 부모 클래스를 상속받는 클래스는 여러 부모 클래스에서 가져온 메서드와 속성을 모두 포함하므로, 클래스 간 관계가 복잡해지고 코드의 이해가 어려워집니다.
    • 상속 구조가 복잡해지면 클래스 간의 결합도가 높아져 유지보수와 확장성이 떨어질 수 있습니다.
  • 객체 지향 원칙 위반 가능성:
    • 객체 지향 프로그래밍의 원칙 중 하나는 "상속보다는 구성(Composition over Inheritance)"을 사용하는 것입니다. 상속은 강한 결합을 만들기 때문에, 다중 상속을 허용할 경우 결합도가 더 높아지고, 객체 지향 설계의 유연성을 해칠 수 있습니다.
    • Swift는 상속보다 프로토콜을 통한 다형성을 지원함으로써 이러한 결합도를 낮추고, 더 유연한 설계를 유도합니다.

 

프로토콜 준수(Conformance)를 통해 다형성을 구현하는 방법은 무엇인가요?

프로토콜 준수를 통해 다형성을 구현하는 방법은 프로토콜 지향 프로그래밍에서 자주 사용됩니다. Swift에서 프로토콜 다형성은 특정 프로토콜을 채택한 객체가 해당 프로토콜에서 정의된 요구 사항을 준수하여, 일관된 방식으로 다양한 객체를 처리할 수 있게 해줍니다. 이를 통해 코드의 유연성과 재사용성을 높일 수 있습니다.

 

1. 프로토콜 정의하기

먼저, 여러 타입이 공통적으로 준수해야 할 요구 사항을 정의하는 프로토콜을 만듭니다. 예를 들어, 모든 동물은 makeSound()라는 메서드를 구현해야 한다고 가정해봅시다.

protocol Animal {
    func makeSound()
}

 

이제 Animal 프로토콜은 makeSound() 메서드를 요구하게 됩니다. 이 프로토콜을 채택하는 모든 타입은 makeSound() 메서드를 반드시 구현해야 합니다.

 

2. 다양한 타입이 프로토콜을 준수하도록 구현하기

Animal 프로토콜을 준수하는 여러 타입을 정의할 수 있습니다. 각각의 타입은 makeSound() 메서드를 각기 다르게 구현할 수 있습니다.

struct Dog: Animal {
    func makeSound() {
        print("Woof!")
    }
}

struct Cat: Animal {
    func makeSound() {
        print("Meow!")
    }
}

struct Bird: Animal {
    func makeSound() {
        print("Chirp!")
    }
}

 

이렇게 Dog, Cat, Bird는 Animal 프로토콜을 준수하며, 각기 다른 방식으로 makeSound()를 구현합니다.

 

3. 프로토콜 타입을 사용하여 다형성 구현하기

Animal 프로토콜을 채택한 여러 타입의 인스턴스를 Animal 타입으로 선언하여 다형성을 구현할 수 있습니다. Animal 타입 변수는 Dog, Cat, Bird 인스턴스를 참조할 수 있고, 각각의 makeSound() 메서드를 호출할 때 다형적으로 동작합니다.

let animals: [Animal] = [Dog(), Cat(), Bird()]

for animal in animals {
    animal.makeSound() // 각각의 타입에 맞는 메서드가 호출됩니다.
}

 

위의 코드에서 animals 배열은 Animal 프로토콜을 준수하는 객체들로 구성됩니다. 반복문을 통해 makeSound()를 호출하면, 각 객체는 자신만의 구현에 따라 다른 소리를 출력합니다. 이를 통해 다형성을 구현할 수 있습니다.

 

4. 프로토콜 확장(Protocol Extension)을 활용한 기본 구현 제공

Swift에서는 프로토콜 확장을 통해 프로토콜을 준수하는 모든 타입에 대해 기본 구현을 제공할 수도 있습니다. 이는 다형성을 구현하면서도 코드 중복을 줄이는 방법입니다.

extension Animal {
    func eat() {
        print("Eating food.")
    }
}

 

이제 Animal 프로토콜을 준수하는 모든 타입은 eat() 메서드를 자동으로 사용할 수 있습니다. Dog, Cat, Bird 타입에 추가적으로 구현하지 않아도 됩니다.

for animal in animals {
    animal.eat()       // "Eating food." 출력
    animal.makeSound() // 각 동물에 맞는 소리를 출력
}

 

 

5. 프로토콜 타입을 함수의 파라미터로 사용하기

프로토콜을 함수의 파라미터 타입으로 지정함으로써 다양한 객체를 동일한 함수에서 처리할 수 있습니다.

func describe(animal: Animal) {
    animal.makeSound()
}

describe(animal: Dog()) // "Woof!"
describe(animal: Cat()) // "Meow!"
describe(animal: Bird()) // "Chirp!"

 

이 예시는 describe 함수가 Animal 프로토콜을 준수하는 어떤 타입의 객체도 인수로 받아서 처리할 수 있음을 보여줍니다.

 

6. 제네릭(Generic)과 프로토콜을 함께 사용하여 다형성 구현하기

Swift에서는 제네릭을 사용하여 다형성을 좀 더 유연하게 구현할 수 있습니다. 제네릭을 사용하면서 특정 프로토콜을 준수하도록 제한하여 다형성을 구현할 수 있습니다.

func performAction<T: Animal>(on animal: T) {
    animal.makeSound()
    animal.eat()
}

performAction(on: Dog()) // "Woof!", "Eating food."
performAction(on: Cat()) // "Meow!", "Eating food."

 

요약

  • 프로토콜 정의: 공통 기능을 요구하는 프로토콜을 정의합니다.
  • 타입 구현: 여러 타입이 프로토콜을 채택하여 요구 사항을 구현하고, 각기 다른 방식으로 메서드를 정의합니다.
  • 다형성 활용: 프로토콜 타입을 사용하여 다양한 객체를 동일한 인터페이스로 처리하고, 다형적인 동작을 구현합니다.
  • 프로토콜 확장: 기본 메서드 구현을 제공하여 코드 중복을 줄이고, 프로토콜 채택 타입에 공통 동작을 추가할 수 있습니다.
  • 제네릭과 결합: 제네릭을 통해 프로토콜 준수 타입에 대해 유연한 다형성을 구현합니다.