프로그래밍스위프트 중급/리드

스위프트에서 프로토콜 컴포지션이란 무엇이며, 실제로 어떻게 작동하며, 다중 상속이나 일반 프로토콜 사용 대신 언제 사용해야 하나요?

Hintsage AI 어시스턴트로 면접 통과

답변.

프로토콜 컴포지션은 스위프트에서 여러 프로토콜을 동시에 준수해야 하는 타입을 생성할 수 있는 메커니즘입니다. 이는 스위프트에서 클래스에 없는 다중 상속을 대체하는 대안적인 방법입니다.

문제의 역사

Objective-C는 클래스에 대해서는 다중 상속을 지원하지 않지만, 프로토콜에 대해서는 지원했습니다. 스위프트는 새로운 추상화를 구축하는 데 있어 프로토콜과 그 조합에 초점을 맞추며 이 전통을 이어갑니다.

문제

프로그래머는 종종 여러 추상화가 정의하는 동작을 가진 타입을 만들어야 합니다. 다중 상속은 계층 간의 충돌을 불가피하게 일으키며, 스위프트에서는 프로토콜과 프로토콜 컴포지션을 통해 안전하게 해결됩니다.

해결책

스위프트에서는 '&' 연산자를 사용하여 프로토콜을 조합할 수 있습니다. 이는 동시에 여러 프로토콜을 준수해야 하는 변수나 함수 매개 변수를 생성할 수 있게 합니다.

코드 예제:

protocol Drivable { func drive() } protocol Flyable { func fly() } struct FlyingCar: Drivable, Flyable { func drive() { print("Driving") } func fly() { print("Flying") } } func testVehicle(_ vehicle: Drivable & Flyable) { vehicle.drive() vehicle.fly() } testVehicle(FlyingCar())

주요 특징:

  • '&' 연산자는 함수의 변수나 매개 변수를 위해 여러 프로토콜을 조합할 수 있게 합니다.
  • 클래스, 구조체, 열거형 모두에서 작동합니다.
  • 중간 타입을 생성하지 않고 필요한 동작을 명확하게 설명할 수 있습니다.

함정 질문.

프로토콜 컴포지션 매개 변수로 하나의 프로토콜만 구현한 객체를 전달할 수 있나요?

아니요, 객체는 컴포지션에 참여하는 모든 프로토콜을 구현해야 하며, 그렇지 않으면 컴파일러가 오류를 발생시킵니다.

코드 예제:

// struct Car: Drivable {} — testVehicle에 전달할 수 없습니다, 왜냐하면 fly()가 구현되지 않았기 때문입니다.

프로토콜 컴포지션이 값이 아닌 타입にも 적용되나요?

프로토콜 컴포지션은 값(변수, 함수 매개 변수)에 적용되지만 객체의 타입 정의에는 사용되지 않습니다 (예: 새로운 타입을 어떤 "프로토콜의 조합"으로 선언할 수 없습니다 — 변수에 대해서만 가능합니다).

코드 예제:

var obj: SomeProtocol & AnotherProtocol // 가능 // typealias MyType = SomeClass & AnotherProtocol // 오류

&를 사용하여 클래스와 프로토콜을 조합할 수 있나요?

네, 그러나 한 가지 제한이 있습니다: 왼쪽에는 하나의 클래스 타입(클래스 또는 그 자식 클래스)만 올 수 있으며, 나머지는 모두 프로토콜이어야 합니다. 그렇지 않으면 컴파일러가 오류를 발생시킵니다.

코드 예제:

class A {} protocol B {} // func f(obj: A & B) {} // 가능 // func f(obj: A & AnotherClass & B) {} // 오류! 하나의 클래스 타입만 허용됩니다.

일반적인 오류와 안티 패턴

  • 클래스의 다중 상속처럼 작동할 것 기대
  • 하나의 프로토콜로 충분할 때 불필요하게 컴포지션 사용
  • 외부 라이브러리 및 프로토콜 컴포지션 사용 시 계약 위반

실제 사례

부정적인 사례

프로젝트에서 층 간 데이터를 전송하기 위해 프로토콜 컴포지션을 사용하는 객체를 사용했지만, 하나의 프로토콜만으로도 충분했습니다:

func present(item: Displayable & Serializable) { ... }

장점:

  • 유연하고 확장 가능한 인터페이스 단점:
  • 대부분의 전달 객체가 모든 기능을 요구하지 않는 경우 복잡성과 혼란

긍정적인 사례

명백한 경우에만 프로토콜 컴포지션을 사용하는 것 — 예를 들어, 일반화된 직렬화를 위해 Codable과 Identifiable을 동시에 지원하는 객체 처리:

func save<T: Codable & Identifiable>(_ item: T) { ... }

장점:

  • 타입에 대한 요구 사항이 명확하게 설명됨
  • 오류 최소화 단점:
  • 함수 시그니처가 약간 복잡해질 수 있음