프로그래밍Swift 중급 개발자

Swift에서 프로토콜 컴포지션이란 무엇이며, 어떻게 작동하고 왜 필요한가요? 여러 프로토콜을 동시에 사용할 때의 함정은 무엇인가요?

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

답변.

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를 사용하여 프로토콜 컴포지션의 제한을 설정할 수 있습니다. 예를 들어, 여러 프로토콜을 준수하는 타입에만 적용되는 확장을 생성할 수 있습니다.

전형적인 오류와 안티 패턴

  • 여덟 개에서 아홉 개의 프로토콜로 컴포지션을 사용하는 것은 아키텍처가 과부하되었음을 나타내며, 책임의 분할이 잘못되었음을 나타냅니다.
  • AnyObject 제한이 있는 프로토콜 컴포지션의 변수를 값 타입(구조체)으로 캐스팅하려고 하면 항상 오류가 발생합니다.
  • 애플리케이션의 다른 부분에서 같은 컴포지션을 사용할 때 typealias 없이 사용하면 가독성이 떨어집니다.

실생활 예시

부정적인 케이스

프로젝트에서 5개의 유사한 프로토콜(Drawable, Movable, Resizable, Colorable, Animatable)을 구현했습니다. 여기서 Drawable & Movable & Resizable & Colorable & Animatable와 같은 컴포지션을 사용했습니다. 일반적인 오류는 일부 엔티티가 계약 중 하나를 구현하지 않아 복잡한 버그를 유발했습니다.

장점:

  • 깊은 상속이 필요하지 않습니다.
  • 기능을 쉽게 추가하거나 제거할 수 있습니다.

단점:

  • 불일치를 추적하기 어렵습니다.
  • 복잡한 테스트가 필요합니다.
  • 선언의 가독성이 떨어집니다.

긍정적인 케이스

복잡한 컴포지션 대신 두 개의 주요 프로토콜(예: Actor 및 Viewable)을 도출하고 "DynamicEntity"라는 컴포지션의 typealias를 만들어 사용했습니다. 책임의 구역을 명확하게 구분했습니다.

장점:

  • 코드를 더 쉽게 읽고 유지할 수 있습니다.
  • 테스트가 DynamicEntity의 동작을 명확하게 강조합니다.
  • 요구 사항 리스트를 빠르게 수정할 수 있습니다.

단점:

  • 아키텍처를 재고해야 합니다.
  • 때때로 기존 클래스를 요구 사항에 맞게 분할해야 합니다.