Swift의 프로토콜 지향 프로그래밍 소개
게시 됨: 2022-03-11프로토콜은 Swift 프로그래밍 언어의 매우 강력한 기능입니다.
프로토콜은 "특정 작업이나 기능에 적합한 방법, 속성 및 기타 요구 사항의 청사진"을 정의하는 데 사용됩니다.
Swift는 컴파일 타임에 프로토콜 적합성 문제를 확인하므로 개발자는 프로그램을 실행하기 전에도 코드에서 치명적인 버그를 발견할 수 있습니다. 프로토콜을 통해 개발자는 언어의 표현력을 손상시키지 않고도 Swift에서 유연하고 확장 가능한 코드를 작성할 수 있습니다.
Swift는 다른 많은 프로그래밍 언어를 괴롭히는 인터페이스의 가장 일반적인 단점과 제한 사항에 대한 해결 방법을 제공하여 프로토콜 사용의 편의성을 한 단계 더 높입니다.
이전 버전의 Swift에서는 많은 현대 프로그래밍 언어에서와 같이 클래스, 구조 및 열거형만 확장할 수 있었습니다. 그러나 Swift 버전 2부터 프로토콜 확장도 가능하게 되었습니다.
이 기사는 재사용 가능하고 유지보수 가능한 코드를 작성하기 위해 Swift의 프로토콜을 사용하는 방법과 프로토콜 확장을 사용하여 대규모 프로토콜 지향 코드베이스에 대한 변경 사항을 단일 위치로 통합할 수 있는 방법을 조사합니다.
프로토콜
프로토콜이란 무엇입니까?
가장 간단한 형태의 프로토콜은 일부 속성과 메서드를 설명하는 인터페이스입니다. 프로토콜을 준수하는 모든 유형은 프로토콜에 정의된 특정 속성을 적절한 값으로 채우고 필요한 메서드를 구현해야 합니다. 예를 들어:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
대기열 프로토콜은 정수 항목을 포함하는 대기열을 설명합니다. 구문은 매우 간단합니다.
프로토콜 블록 내에서 속성을 설명할 때 속성이 gettable { get }
또는 gettable 및 settable { get set }
모두인지 지정해야 합니다. 우리의 경우 변수 Count( Int
유형)는 gettable만 가능합니다.
프로토콜에서 속성을 가져오고 설정할 수 있어야 하는 경우 해당 요구 사항은 상수 저장 속성이나 읽기 전용 계산 속성으로 충족될 수 없습니다.
프로토콜이 속성을 gettable로 요구하는 경우 요구 사항은 모든 종류의 속성으로 충족될 수 있으며, 이것이 자신의 코드에 유용하다면 속성 설정도 유효합니다.
프로토콜에 정의된 함수의 경우 함수가 mutating
키워드로 내용을 변경할지 여부를 나타내는 것이 중요합니다. 그 외에 함수의 서명은 정의로 충분합니다.
프로토콜을 준수하려면 형식이 모든 인스턴스 속성을 제공하고 프로토콜에 설명된 모든 메서드를 구현해야 합니다. 예를 들어, 아래는 우리의 Queue
프로토콜을 준수하는 struct Container
입니다. 구조체는 기본적으로 푸시된 Int
를 개인 배열 items
에 저장합니다.
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
그러나 현재 Queue 프로토콜에는 큰 단점이 있습니다.
Int
를 처리하는 컨테이너만 이 프로토콜을 따를 수 있습니다.
"연결된 유형" 기능을 사용하여 이 제한을 제거할 수 있습니다. 연결된 형식은 제네릭처럼 작동합니다. 시연하기 위해 연결된 유형을 활용하도록 대기열 프로토콜을 변경해 보겠습니다.
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
이제 대기열 프로토콜을 사용하면 모든 유형의 항목을 저장할 수 있습니다.
Container
구조의 구현에서 컴파일러는 컨텍스트에서 관련 유형(즉, 메서드 반환 유형 및 매개변수 유형)을 결정합니다. 이 접근 방식을 사용하면 일반 항목 유형으로 Container
구조를 만들 수 있습니다. 예를 들어:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
프로토콜을 사용하면 많은 경우에 코드 작성이 간소화됩니다.
예를 들어, 오류를 나타내는 모든 객체는 Error
(또는 LocalizedError
된 설명을 제공하려는 경우 LocalizedError) 프로토콜을 따를 수 있습니다.
그런 다음 코드 전체에서 이러한 오류 개체에 동일한 오류 처리 논리를 적용할 수 있습니다. 결과적으로 오류를 나타내기 위해 특정 객체(Objective-C의 NSError와 같은)를 사용할 필요가 없으며 Error
또는 LocalizedError
프로토콜을 준수하는 모든 유형을 사용할 수 있습니다.
String 유형을 확장하여 LocalizedError
프로토콜을 준수하고 문자열을 오류로 던질 수도 있습니다.
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
프로토콜 확장
프로토콜 확장은 프로토콜의 위대함을 기반으로 합니다. 그들은 우리가 할 수 있습니다:
프로토콜 메서드의 기본 구현과 프로토콜 속성의 기본값을 제공하여 "선택 사항"으로 만듭니다. 프로토콜을 준수하는 유형은 자체 구현을 제공하거나 기본 구현을 사용할 수 있습니다.
프로토콜에 설명되지 않은 추가 방법의 구현을 추가하고 이러한 추가 방법으로 프로토콜을 준수하는 모든 유형을 "장식"합니다. 이 기능을 사용하면 각 유형을 개별적으로 수정할 필요 없이 이미 프로토콜을 준수하는 여러 유형에 특정 메소드를 추가할 수 있습니다.
기본 메소드 구현
프로토콜을 하나 더 만들어 보겠습니다.
protocol ErrorHandler { func handle(error: Error) }
이 프로토콜은 응용 프로그램에서 발생하는 오류 처리를 담당하는 개체를 설명합니다. 예를 들어:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
여기서는 오류에 대한 현지화된 설명을 인쇄합니다. 프로토콜 확장을 사용하여 이 구현을 기본값으로 만들 수 있습니다.
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
이렇게 하면 기본 구현을 제공하여 handle
메서드를 선택 사항으로 만듭니다.
기본 동작으로 기존 프로토콜을 확장하는 기능은 매우 강력하여 기존 코드의 호환성을 깨는 것에 대해 걱정할 필요 없이 프로토콜을 확장하고 확장할 수 있습니다.
조건부 확장
그래서 우리는 handle
메소드의 기본 구현을 제공했지만 콘솔에 인쇄하는 것은 최종 사용자에게 별로 도움이 되지 않습니다.
오류 처리기가 보기 컨트롤러인 경우 현지화된 설명과 함께 일종의 경고 보기를 표시하는 것을 선호할 것입니다. 이를 위해 ErrorHandler
프로토콜을 확장할 수 있지만 특정 경우에만 적용되도록 확장을 제한할 수 있습니다(예: 유형이 보기 컨트롤러인 경우).
Swift를 사용하면 where
키워드를 사용하여 프로토콜 확장에 이러한 조건을 추가할 수 있습니다.
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
위의 코드 조각에서 Self (대문자 "S" 포함)는 유형(구조, 클래스 또는 열거형)을 나타냅니다. UIViewController
에서 상속된 유형에 대해서만 프로토콜을 확장하도록 지정하면 UIViewController
특정 메서드(예: present(viewControllerToPresnt: animated: completion)
)를 사용할 수 있습니다.

이제 ErrorHandler
프로토콜을 준수하는 모든 보기 컨트롤러에는 지역화된 설명과 함께 경고 보기를 표시하는 자체 기본 handle
메서드 구현이 있습니다.
모호한 메서드 구현
동일한 서명을 가진 메서드가 있는 두 가지 프로토콜이 있다고 가정해 보겠습니다.
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
두 프로토콜 모두 이 메서드의 기본 구현을 포함하는 확장을 가지고 있습니다.
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
이제 두 프로토콜을 모두 준수하는 유형이 있다고 가정해 보겠습니다.
struct S: P1, P2 { }
이 경우 모호한 메서드 구현에 문제가 있습니다. 유형은 사용해야 하는 메소드의 구현을 명확하게 나타내지 않습니다. 결과적으로 컴파일 오류가 발생합니다. 이 문제를 해결하려면 유형에 메서드 구현을 추가해야 합니다.
struct S: P1, P2 { func method() { print("Method S") } }
많은 객체 지향 프로그래밍 언어는 모호한 확장 정의의 해결을 둘러싼 제한 사항에 시달리고 있습니다. Swift는 프로그래머가 컴파일러가 부족한 부분을 제어할 수 있도록 함으로써 프로토콜 확장을 통해 이것을 아주 우아하게 처리합니다.
새로운 방법 추가
Queue
프로토콜을 한 번 더 살펴보겠습니다.
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Queue
프로토콜을 준수하는 각 유형에는 저장된 항목 수를 정의하는 count
인스턴스 속성이 있습니다. 이를 통해 무엇보다도 이러한 유형을 비교하여 어느 것이 더 큰지 결정할 수 있습니다. 프로토콜 확장을 통해 이 방법을 추가할 수 있습니다.
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
이 방법은 대기열 기능과 관련이 없기 때문에 Queue
프로토콜 자체에 설명되어 있지 않습니다.
따라서 이것은 프로토콜 메소드의 기본 구현이 아니라 Queue
프로토콜을 준수하는 모든 유형을 "장식"하는 새로운 메소드 구현입니다. 프로토콜 확장이 없으면 이 메서드를 각 유형에 별도로 추가해야 합니다.
프로토콜 확장과 기본 클래스
프로토콜 확장은 기본 클래스를 사용하는 것과 매우 유사해 보일 수 있지만 프로토콜 확장을 사용하면 몇 가지 이점이 있습니다. 여기에는 다음이 포함되지만 반드시 이에 국한되지는 않습니다.
클래스, 구조 및 열거형은 둘 이상의 프로토콜을 따를 수 있으므로 여러 프로토콜의 기본 구현을 사용할 수 있습니다. 이것은 개념적으로 다른 언어의 다중 상속과 유사합니다.
프로토콜은 클래스, 구조 및 열거형에 의해 채택될 수 있지만 기본 클래스 및 상속은 클래스에만 사용할 수 있습니다.
Swift 표준 라이브러리 확장
자신의 프로토콜을 확장하는 것 외에도 Swift 표준 라이브러리에서 프로토콜을 확장할 수 있습니다. 예를 들어, 큐 컬렉션의 평균 크기를 찾으려면 표준 Collection
프로토콜을 확장하여 찾을 수 있습니다.
요소가 인덱싱된 첨자를 통해 탐색 및 액세스할 수 있는 Swift의 표준 라이브러리에서 제공하는 시퀀스 데이터 구조는 일반적으로 Collection
프로토콜을 따릅니다. 프로토콜 확장을 통해 이러한 모든 표준 라이브러리 데이터 구조를 확장하거나 일부를 선택적으로 확장할 수 있습니다.
참고: 이전에 Swift 2.x에서
CollectionType
으로 알려진 프로토콜은 Swift 3에서Collection
으로 이름이 변경되었습니다.
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
이제 큐 모음( Array
, Set
등)의 평균 크기를 계산할 수 있습니다. 프로토콜 확장이 없었다면 이 메서드를 각 컬렉션 유형에 개별적으로 추가해야 했을 것입니다.
Swift 표준 라이브러리에서 프로토콜 확장은 예를 들어 map
, filter
, reduce
등과 같은 메소드를 구현하는 데 사용됩니다.
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
프로토콜 확장 및 다형성
앞서 말했듯이 프로토콜 확장을 통해 일부 메서드의 기본 구현을 추가하고 새로운 메서드 구현도 추가할 수 있습니다. 그러나이 두 기능의 차이점은 무엇입니까? 에러 핸들러로 돌아가서 알아봅시다.
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
결과는 치명적인 오류입니다.
이제 프로토콜에서 handle(error: Error)
메서드 선언을 제거합니다.
protocol ErrorHandler { }
결과는 동일합니다. 치명적인 오류입니다.
프로토콜 메소드의 기본 구현을 추가하는 것과 프로토콜에 새로운 메소드 구현을 추가하는 것 사이에 차이가 없다는 것을 의미합니까?
아니요! 차이점이 존재하며 변수 handler
의 유형을 Handler
에서 ErrorHandler
로 변경하여 볼 수 있습니다.
let handler: ErrorHandler = Handler()
이제 콘솔에 대한 출력은 다음과 같습니다 . 작업을 완료할 수 없습니다. (응용 프로그램 오류 오류 0.)
그러나 핸들(오류: 오류) 메서드의 선언을 프로토콜에 반환하면 결과가 다시 치명적인 오류로 변경됩니다.
protocol ErrorHandler { func handle(error: Error) }
각 경우에 발생하는 순서를 살펴보겠습니다.
프로토콜에 메서드 선언이 있는 경우:
프로토콜은 handle(error: Error)
메서드를 선언하고 기본 구현을 제공합니다. 이 메서드는 Handler
구현에서 재정의됩니다. 따라서 변수 유형에 관계없이 런타임에 메서드의 올바른 구현이 호출됩니다.
프로토콜에 메서드 선언이 없는 경우:
메서드가 프로토콜에 선언되어 있지 않기 때문에 형식이 이를 재정의할 수 없습니다. 이것이 호출된 메소드의 구현이 변수의 유형에 의존하는 이유입니다.
변수가 Handler
유형이면 해당 유형의 메소드 구현이 호출됩니다. 변수가 ErrorHandler
유형인 경우 프로토콜 확장의 메소드 구현이 호출됩니다.
프로토콜 지향 코드: 안전하면서도 표현력이 뛰어남
이 기사에서 우리는 Swift의 프로토콜 확장 기능 중 일부를 시연했습니다.
인터페이스가 있는 다른 프로그래밍 언어와 달리 Swift는 불필요한 제한으로 프로토콜을 제한하지 않습니다. Swift는 개발자가 필요에 따라 모호성을 해결할 수 있도록 하여 이러한 프로그래밍 언어의 일반적인 단점을 해결합니다.
Swift 프로토콜 및 프로토콜 확장을 사용하여 작성하는 코드는 대부분의 동적 프로그래밍 언어만큼 표현력이 풍부할 수 있으며 여전히 컴파일 시간에 유형이 안전합니다. 이를 통해 코드의 재사용성과 유지보수성을 보장하고 보다 자신 있게 Swift 앱 코드베이스를 변경할 수 있습니다.
이 기사가 귀하에게 유용하기를 바라며 피드백이나 추가 통찰력을 환영합니다.