본문 바로가기
개린이 이야기

Protocol Composition에 관하여

by iOS 개린이 2023. 6. 6.

Protocol Composition

-Protocol Composition이란 타입이 여러 프로토콜을 묶어서 사용할 수 있는 기능이다.

 

protocol CanFly {
    func fly()
}

protocol CanSwim {
    func swim()
}

struct Duck: CanFly, CanSwim {
    func fly() {
        print("오리 날다.")
    }
    
    func swim() {
        print("오리 수영하다")
    }
}

let duck = Duck()
duck.fly()  //"오리 날다."
duck.swim() //"오리 수영하다"

 

위 코드에서 'Duck' 구조체는 'CanFly', 'CanSwim' 프로토콜 모두 준수한다.

따라서 각 프로토콜에 있는 기능들을 모두 사용할 수 있다.

 

메서드의 매개변수 타입으로 사용하기.

func actionForFlyAndSwimAnimal(animal: CanFly & CanSwim) {
    animal.fly()     
    animal.swim()    
}

let duck = Duck()
actionForFlyAndSwimAnimal(animal: duck)

//"오리 날다."
//"오리 수영하다"

 

'actionForFlyAndSwimAnimal()' 메서드의 매개변수 타입으로 'CanFly', 'CanSwim' 프로토콜을 모두 준수하는 타입을 받을 수 있도록 한다.

매개변수의 타입인 'CanFly & CanSwim' 이 너무 가독성이 떨어진다 싶으면?

 

typealias CanFlyAndSwim = CanFly & CanSwim

func actionForFlyAndSwimAnimal(animal: CanFlyAndSwim) {
    animal.fly()     
    animal.swim()    
}

let duck = Duck()
actionForFlyAndSwimAnimal(animal: duck)

//"오리 날다."
//"오리 수영하다"

 

요렇게 타입별칭을 사용하여 하나의 타입으로 만들어서 사용함으로써 간결한 코드로 만들어 줄 수 있다.

 

Protocol Composition 클래스와 함께 사용하기.

protocol Named {
    var name: String {get}
}

class Location {
    
    var latitude: Double
    var longitude: Double
    
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}

class City: Location, Named {
    
    var name: String
    
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}

typealias LocationAndNamed = Location & Named

func beginConcert(in location: LocationAndNamed) {
    print("Hello, \(location.name)")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)  //"Hello, Seattle"

 

'City' 클래스는 'Location' 클래스를 상속받으면서, 'Named' 프로토콜을 준수하고 있다.

이것을 하나의 타입으로 만들어서 사용하는 'LocationAndNamed' 타입별칭은 

'Location' 클래스를 상속받으면서, 'Named' 프로토콜을 준수하고 있는 타입을 의미한다.

 

이렇게 Protocol Composition 에서는 

프로토콜의 목록 외에도, 필요한 슈퍼 클래스를 지정하는데 사용할 수 있는 클래스 타입이 포함될 수 있다.

 

 

Swift에서 사용되는 Protocol Composition 

-Swift에서 Protocol Composition 은 굉장히 많이 사용되고 있는데,

대표적인 예로 'Codable' 이 있다.

 

-정의

'Decodable' 과 'Encodable' 프로토콜을 Protocol Composition을 사용하여 Codable이라는 타입을 만들었다.

이것은 다중 채택의 복잡성을 줄일 수 있고, 가독성도 좋다.

 

 

Protocol Composition의 이점

1. 코드 재사용

만약 단일 프로토콜만 채택할 수 있다면 해당 타입은 특정 프로토콜에 한정되어 버릴 수 있다.

하지만 Protocol Composition은 해당 타입이 여러 프로토콜을 동시에 채택하여 각 프로토콜에 정의되어 있는 모든 기능을 활용할 수 있게 한다.

 

2. 다중 상속 대체

Swift에서 다중상속을 제한하는 이유

대표적인 이유는 다이아몬드 문제이다. 이것은 두 개 이상의 슈퍼 클래스가 같은 메서드나 프로퍼티를 가지고 있을 때,

서브 클래스는 어느 슈퍼클래스의 메서드나 프로퍼티를 사용할 지 결정하는 데 발생하는 문제이다.

 

class A {
    func hello() { print("Hello from A") }
}

class B: A {
    override func hello() { print("Hello from B") }
}

class C: A {
    override func hello() { print("Hello from C") }
}

class D: B, C {  //다중 상속은 허용되지 않음.
}

 

위 코드에서 만약 'D' 클래스가 'hello()' 메서드를 호출하고 싶다면, 어느 클래스에 있는 메서드를 호출해야 할지 결정하기가 어렵다. 이것이 다이아몬드 문제라고 한다.

이처럼 다중 상속은 클래스의 계층 구조를 복잡하게 만들어 유지보수를 어렵게 하고, 버그를 유발할 수 있다.

 

다중 상속의 장점

첫 번째는 코드 재사용성이다. 하나의 클래스가 여러 슈퍼 클래스로부터 기능을 상속 받을 수 있기 때문에

여러 클래스에서 공통적으로 사용되는 기능을 한 곳에 작성하고 여러 클래스에서 재사용할 수 있다.

 

두 번째는 기능 확장성이다. 다중 상속을 통해 여러 슈퍼 클래스에 있는 특성들을 모두 사용할 수 있게 된다.

 

Protocol Composition

프로토콜의 다중 채택은 다중 상속의 장점을 취하면서, 다중 상속의 문제점을 극복한다.

 

다이아몬드 문제 해결

다이아몬드 문제는 여러 개의 슈퍼클래스로부터 상속을 받았을 때, 동일한 메소드나 프로퍼티가 여러 부모 클래스에 걸쳐 중복 정의되어 어떤 슈퍼클래스의 구현을 따라가야 할 지 모호성이 발생하는 문제다.

 

하지만 프로토콜은 클래스의 상속과 달리 실제 구현체를 포함하지 않는다.

프로토콜은 메소드나 프로퍼티의 요구사항만 제공하고, 실제 구현은 채택한 타입에서 해야 한다.

즉, 하나의 타입이 여러 프로토콜을 채택해도, 각 프로토콜이 요구하는 메소드나 프로퍼티는 채택한 타입에서 구현해야 하고,

이 구현은 각 타입에서 명확하게 정의하기 때문에, 어느 구현을 따라야 할지에 대한 문제가 발생하지 않는다.

 

기능 확장성 

하나의 타입이 여러 프로토콜을 채택할 수 있기 때문에 여러 프로토콜에 있는 특성들을 모두 사용할 수 있다.

예제에서도 보았듯이, 'Duck' 구조체는 'CanFly' 프로토콜 특성과 'CanSwim' 프로토콜의 특성을 Composition 하여 다양한 기능을 사용할 수 있었다.

 

 

 

 

Reference:

https://jusung.gitbook.io/the-swift-language-guide/language-guide/21-protocols