Swift에서는 많은 OOP 언어의 경험이 일반화되고 개선되어 프로토콜을 상속하는 것뿐만 아니라 결합(컴포지션)하는 기능이 추가되었습니다. 프로토콜 컴포지션을 통해 변수, 함수 매개변수 또는 제네릭을 선언할 때 여러 프로토콜을 동시에 준수해야 한다는 요구사항을 지정할 수 있습니다. 이 메커니즘은 여러 계약(인터페이스)의 동작을 가진 객체를 다루어야 할 때 유용하며, 다중 상속의 단점을 유연하게 피할 수 있습니다. 컴포지션이 해결하는 문제는 "객체가 요구사항 그룹을 만족해야 한다"는 필요성입니다.
Swift의 솔루션에서는 특별한 구문이 사용됩니다: 프로토콜의 결합은 & (앰퍼샌드) 기호로 표시됩니다. 예를 들어, protocolA & protocolB와 같이 사용합니다. 내부적으로는 런타임 체크가 수행되며(예를 들어, 타입 캐스팅 및 제네릭 컨텍스트에서의 캐스팅), 타입 수를 최소화하고 "책임 분리" 패턴을 유연하게 구현합니다.
코드 예:
protocol Drawable { func draw() } protocol Movable { func move() } struct Sprite: Drawable, Movable { func draw() { print("Sprite 그리기") } func move() { print("Sprite 이동") } } func animate(object: Drawable & Movable) { object.draw() object.move() } let s = Sprite() animate(object: s)
핵심 특징:
특정 구조체나 클래스에 묶이지 않고 protocolA & protocolB 타입의 변수를 생성할 수 있나요?
네, 변수를 여러 프로토콜을 동시에 준수하는 것으로 선언할 수 있습니다. 예:
var obj: protocolA & protocolB
하지만 중요합니다: 이러한 변수는 반드시 객체를 참조해야 하며(값 타입이 아님) 컴포지션 내에서 적어도 하나의 프로토콜이 클래스 타입에 제한될 경우에 해당합니다 (protocol: AnyObject).
클래스 타입을 조합에 포함할 수 있나요, 예를 들면 SomeClass & Drawable?
네, 그러나 주의가 필요합니다: SomeClass & Protocol 형태의 컴포지션은 값이 반드시 해당 클래스의 인스턴스(또는 그 하위 클래스)여야 하며, 프로토콜을 구현해야 합니다. 이러한 접근 방식은 제네릭 타입을 제한하는 데 사용됩니다.
프로토콜 확장에서 관련 타입의 타입으로 프로토콜 컴포지션을 사용할 수 있나요?
네, 그러나 제한이 있습니다: associatedtype을 컴포지션으로 선언할 수는 없지만, 확장에서 where를 사용하여 프로토콜 컴포지션의 제한을 설정할 수 있습니다. 예를 들어, 여러 프로토콜을 준수하는 타입에만 적용되는 확장을 생성할 수 있습니다.
프로젝트에서 5개의 유사한 프로토콜(Drawable, Movable, Resizable, Colorable, Animatable)을 구현했습니다. 여기서 Drawable & Movable & Resizable & Colorable & Animatable와 같은 컴포지션을 사용했습니다. 일반적인 오류는 일부 엔티티가 계약 중 하나를 구현하지 않아 복잡한 버그를 유발했습니다.
장점:
단점:
복잡한 컴포지션 대신 두 개의 주요 프로토콜(예: Actor 및 Viewable)을 도출하고 "DynamicEntity"라는 컴포지션의 typealias를 만들어 사용했습니다. 책임의 구역을 명확하게 구분했습니다.
장점:
단점: