문제의 역사:
프로토콜 추상화는 OOP의 전통적인 객체 상속에 반하여 스위프트에서 등장했습니다. Objective-C 및 기타 OOP 언어에서는 "일반에서 특별로" 향하는 상속 접근 방식이 지배적이었지만, 스위프트는 상속보다 합성을 강조하며 프로토콜을 추상화의 주요 방법으로 발전시켰습니다.
문제:
전통적인 상속은 엄격한 계층 구조를 가정합니다: 재정의를 통해 의무적으로 확장해야 하는 서브클래스의 트리. 이는 유연성을 제한하고, "부서지기 쉬운" 코드, 복잡한 리팩토링 및 기본 조상 클래스의 과도한 팽창을 초래합니다. 또한, 스위프트는 클래스의 다중 상속을 지원하지 않으므로, 기능성을 재사용하기 위해서는 다른 메커니즘을 통해 가능합니다.
해결책:
프로토콜 추상화는 타입이 구현해야 하는 "요구 사항 집합"을 선언할 수 있게 해줍니다. 프로토콜은 공통 논리를 주입하기 위해 확장(extension)할 수 있으며, 이는 "믹스인" 개념에 더 가깝습니다:
코드 예:
protocol Drawable { func draw() } extension Drawable { func draw() { print("Default drawing") } } struct Circle: Drawable {} let c = Circle() c.draw() // "Default drawing" 출력
주요 특징:
프로토콜의 확장과 클래스의 일반 구현 간 차이는 무엇입니까?
프로토콜을 확장합니다. extension은 사용자가 자신의 타입에서 이 메소드를 구현하지 않은 경우에만 default 구현을 추가합니다. 타입에서 명시적으로 구현된 메소드가 있다면, 그 메소드가 호출됩니다.
예:
protocol Demo { func foo() } extension Demo { func foo() { print("default") } } struct X: Demo { func foo() { print("custom") } } X().foo() // "custom"
프로토콜과 해당 extension을 구현하는 타입이 프로토콜 데이터로 참조되면 어떻게 됩니까?
프로토콜이 메소드를 필수(요구 사항)으로 선언하면, 프로토콜 타입으로 변환하더라도 특정 타입의 구현이 사용됩니다. 그러나, extension에 새로운 (프로토콜에서 이전에 선언되지 않은) 속성이 추가되면, 이는 extension을 통해서만 접근할 수 있고 프로토콜 타입을 통해서는 접근할 수 없습니다.
하나의 프로토콜을 구현하는 서로 다른 struct 인스턴스를 배열에 저장할 수 있습니까?
네 — "존재론적" 타입 덕분에 (예: [Drawable]) 이형 컬렉션을 저장할 수 있습니다:
struct Tri: Drawable { func draw() { print("Triangle") } } let arr: [Drawable] = [Circle(), Tri()] arr.forEach { $0.draw() }
회사에는 모든 도형(Circle, Square, Polygon)이 상속받는 기본 슈퍼클래스 Shape가 있었습니다. 새로운 도형을 추가할 때마다 기본 클래스가 성장했습니다. 시스템 확장이 점점 더 어려워졌고, 새로운 타입이 기존 코드의 ABI를 깨뜨리곤 했습니다.
장점:
단점:
여러 프로토콜 Drawable, Colorable, Animatable을 사용하기 시작했습니다. 이제 각 도형을 다른 구조를 변경하지 않고도 "애니메이션 효과와 색상"이 동시에 적용 가능하게 되었습니다. 새로운 기능은 extension을 통해 추가됩니다.
장점:
단점: