🔥 프로토콜 조합

614자
8분

때로는 하나의 타입이 여러 프로토콜을 동시에 준수하도록 요구하는 것이 유용할 수 있습니다. 이런 경우 프로토콜 조합을 사용하여 여러 프로토콜을 하나의 요구사항으로 결합할 수 있습니다. 프로토콜 조합은 조합에 포함된 모든 프로토콜의 요구사항을 결합한 임시 로컬 프로토콜을 정의한 것처럼 동작합니다. 프로토콜 조합 자체는 새로운 프로토콜 타입을 정의하지 않습니다.

프로토콜 조합은 SomeProtocol & AnotherProtocol과 같은 형태를 가집니다. 필요한 만큼의 프로토콜을 나열할 수 있으며, 앰퍼샌드(&)로 구분합니다. 프로토콜 목록 외에도 프로토콜 조합에는 하나의 클래스 타입을 포함할 수 있는데, 이를 통해 필수 상위 클래스를 지정할 수 있습니다.

다음은 NamedAged라는 두 개의 프로토콜을 함수 매개변수의 단일 프로토콜 조합 요구사항으로 결합하는 예제입니다:

swift
protocol Named {
    var name: String { get }
}
 
protocol Aged {
    var age: Int { get }
}
 
struct Person: Named, Aged {
    var name: String
    var age: Int
}
 
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
 
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// "Happy birthday, Malcolm, you're 21!"이 출력됩니다.
swift
protocol Named {
    var name: String { get }
}
 
protocol Aged {
    var age: Int { get }
}
 
struct Person: Named, Aged {
    var name: String
    var age: Int
}
 
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
 
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// "Happy birthday, Malcolm, you're 21!"이 출력됩니다.

이 예제에서 Named 프로토콜은 name이라는 가져올 수 있는 String 속성에 대한 단일 요구사항을 가집니다. Aged 프로토콜은 age라는 가져올 수 있는 Int 속성에 대한 단일 요구사항을 가집니다. 두 프로토콜 모두 Person이라는 구조체에서 채택되었습니다.

또한 이 예제에서는 wishHappyBirthday(to:) 함수를 정의합니다. celebrator 매개변수의 타입은 Named & Aged로, 이는 "NamedAged 프로토콜을 모두 준수하는 모든 타입"을 의미합니다. 함수에 전달되는 구체적인 타입은 중요하지 않으며, 필요한 두 프로토콜을 모두 준수하기만 하면 됩니다.

그 다음, 예제에서는 birthdayPerson이라는 새로운 Person 인스턴스를 생성하고 이를 wishHappyBirthday(to:) 함수에 전달합니다. Person은 두 프로토콜을 모두 준수하므로 이 호출은 유효하며, wishHappyBirthday(to:) 함수는 생일 축하 메시지를 출력할 수 있습니다.

다음은 이전 예제의 Named 프로토콜과 Location 클래스를 결합하는 예제입니다:

swift
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)
    }
}
 
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}
 
let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// "Hello, Seattle!"이 출력됩니다.
swift
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)
    }
}
 
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}
 
let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// "Hello, Seattle!"이 출력됩니다.

beginConcert(in:) 함수는 Location & Named 타입의 매개변수를 받습니다. 이는 "Location의 하위 클래스이면서 Named 프로토콜을 준수하는 모든 타입"을 의미합니다. 이 경우 City는 두 요구사항을 모두 만족합니다.

birthdayPersonbeginConcert(in:) 함수에 전달하는 것은 유효하지 않은데, 그 이유는 PersonLocation의 하위 클래스가 아니기 때문입니다. 마찬가지로, Location의 하위 클래스를 만들었지만 Named 프로토콜을 준수하지 않는다면 해당 타입의 인스턴스로 beginConcert(in:)을 호출하는 것도 유효하지 않습니다.

프로토콜 조합을 사용하면 여러 프로토콜의 요구사항을 결합하여 더욱 구체적이고 제한적인 타입 요구사항을 표현할 수 있습니다. 이를 통해 코드의 안전성과 명확성을 높일 수 있죠.

다음은 프로토콜 조합의 활용 예시를 보여주는 코드입니다:

swift
protocol Flyable {
    func fly()
}
 
protocol Swimmable {
    func swim()
}
 
struct Duck: Flyable, Swimmable {
    func fly() {
        print("오리가 날아갑니다!")
    }
 
    func swim() {
        print("오리가 수영합니다!")
    }
}
 
func performActions(on creature: Flyable & Swimmable) {
    creature.fly()
    creature.swim()
}
 
let donald = Duck()
performActions(on: donald)
// "오리가 날아갑니다!"와 "오리가 수영합니다!"가 출력됩니다.
swift
protocol Flyable {
    func fly()
}
 
protocol Swimmable {
    func swim()
}
 
struct Duck: Flyable, Swimmable {
    func fly() {
        print("오리가 날아갑니다!")
    }
 
    func swim() {
        print("오리가 수영합니다!")
    }
}
 
func performActions(on creature: Flyable & Swimmable) {
    creature.fly()
    creature.swim()
}
 
let donald = Duck()
performActions(on: donald)
// "오리가 날아갑니다!"와 "오리가 수영합니다!"가 출력됩니다.

이 예제에서는 FlyableSwimmable이라는 두 개의 프로토콜을 정의하고, Duck 구조체에서 이들을 채택합니다. performActions(on:) 함수는 Flyable & Swimmable 타입의 매개변수를 받아 해당 객체의 fly()swim() 메서드를 호출합니다.

donald라는 Duck 인스턴스를 생성하고 performActions(on:) 함수에 전달하면, 오리가 날아가고 수영하는 동작이 수행됩니다.

이처럼 프로토콜 조합을 활용하면 특정 기능들의 조합을 요구하는 함수나 메서드를 유연하게 정의할 수 있습니다. 이는 코드의 재사용성을 높이고 타입 안정성을 보장하는 데 도움이 됩니다.

프로토콜 조합은 Swift의 강력한 기능 중 하나로, 프로토콜 지향 프로그래밍(Protocol-Oriented Programming)의 핵심 개념입니다. 프로토콜 조합을 적절히 활용하면 코드의 모듈화와 유연성을 크게 향상시킬 수 있습니다.

Swift에서 프로토콜은 매우 중요한 역할을 하며, 프로토콜 조합은 이를 더욱 확장하여 다양한 요구사항을 조합할 수 있게 해줍니다. 프로토콜 조합을 사용하면 클래스 상속 계층과는 별개로 타입에 대한 제약 조건을 명시할 수 있어 코드의 유연성과 재사용성이 높아집니다.

앞으로도 Swift 프로그래밍에서 프로토콜과 프로토콜 조합을 적극 활용하여 더욱 간결하고 표현력 있는 코드를 작성해 보시기 바랍니다!

YouTube 영상

채널 보기
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
Trie 자료구조 완전 정복 - 개념부터 시각화까지 | Trie 자료구조 이야기
트라이(Trie)를 이용한 자동 완성 알고리즘 | Trie 자료구조 이야기
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기
마지막편, 10억 개 데이터 검색이 0.3ms면 끝나는 이유와 LSM-Tree의 등장