프로토콜 패턴

프로토콜이란 사전적 의미로 조약 또는 규약이라는 뜻으로, 특정 작업이나 기능의 일부분을 구현하기 위해 필수, 선택적인 유형을 정의한 인터페이스를 지칭합니다. 프로토콜을 실제로 정의라는 부분을 담당하기 때문에 인터페이스로만 사용이 되며, 구체적인 세부 프로세스가 없기 때문에 이는 추상체라고도 불립니다.

프로토콜에 정의된 인터페이스를 실제로 구현하는 것을 구현체라고 말합니다, 구현체는 클래스, 구조체 또는 열거형, 확장 등 모든 데이터 유형에서 사용할 수 있습니다.

유형의 프로토콜을 준수하면 Swift는 프로토콜의 모든 요구 사항을 충족하는지 확인 합니다. 여기에는 몇 가지 필수 메소드, 일부 선택적 메소드 또는 일부 인스턴스 변수가 포함될 수 있습니다.

프르토콜은 넥스트 시절 이후 애플 개발에 필수적인 부분이었지만, 애플 OSX로 병합되면서 부터는 사용방식이 급격하게 변하게 됩니다. Objective-C 초창기의 NSObjects는 소위 “비공식 프로토콜”라고 불리는 카테고리로 작성되었으며, 런타임시에 NSObject를 참조한 객체는 해당 메소드가 프로토콜과 같이 준수검사를 통해서 적합할 경우 호출하도록 구현되어있습니다.

요즘 Objective-C에서 정의한 프로토콜은 대부분 공식 프로토콜이라고 불립니다. 프로토콜의 구체적 유형이 준수할 수 있도록 메서드 및 인스턴스 변수가 명명된 컬렉션입니다. 스위프트에서의 프로토콜은 Objective-C의 프로토콜 보다 더욱 광범위하게 사용합니다.


이름 지정 프로토콜

프르토콜 이름 지정에는 두 가지 측면이 있습니다. 첫째는 프로토콜의 이름 지정(예: Equatable 및 Hashable)과 둘째 프로토콜 내부의 메소드 이름 지정입니다. 규칙은 복잡하지 않지만 코드를 Apple 플랫폼의 나머지 부분에 원활하고 밀접하게 적용시키려면 다음과 같은 규칙을 준수 해야합니다.

  • 해당 프로토콜이 무언가를 설명하는 경우 명사를 사용하십시오: 예) Collaction, Sequence, UITableDataSource
  • 해당 프로토콜의 능력을 기술할 경우 형용사를 사용하십시오: 예) Equatable, Comparable, UITableDataSourceDelegating.

Apple의 Swift 가이드 라인은 능력이 기술된 프로토콜을 위해 “…able, …ible, …ing” 이라는 접미사를 특별히 제안하는건 아니지만, 참고하여 이름을 지정 하시기 바랍니다.

프로토콜 내부의 메소드의 이름을 지정하는 경우 원하는 이름을 사용할 수 있습니다. 그러나 프로토콜이 델리게이트를 구현하도록 설계된 경우, 다른 코드와 함께 사용할 수 있도록 Apple의 명명 규칙을 따라야합니다. 특히, 해당 메서드를 사용 되는 시점인, will, did, should라는 방법으로 트리거 될 때 해당 이름의 규칙적인 일관성을 유지해야만 합니다.

예를 들어봅시다. 나는 델리게이트로 사용할 프로토콜을 작성하였습니다.

protocol CalendarDelegate {
  func willDisplay(year: Int)
  func didSelect(date: Date)
  func shouldChangeYear() -> Bool
}

해당 델리게이트가 메소드를 트리거 한 객체을 알리는 것이 중요하다고 판단되어, 메서드의 이름을 변경해보겠습니다.

func calendar(_ calendar: Calendar, willDisplay year: Int)
func calendar(_ calendar: Calendar, didSelect date: Date)
func calendar(_ calendar: Calendar, shouldYear) -> Bool /// ???

마지막 메서드가 어색하게 느껴지지 않으신가요? 그렇습니다. 이것은 유효하지 않는 이름입니다. 스위프트의 – shouldChangeYear은 데이터 유형에 연결되어 있지 않기 때문에 매개변수가 존재하지 않으므로, 아무런 의미가 없습니다.

이러한 방식때문에 보통 애플 개발을 배울 때 사람들을 혼란스럽게 만드는 부분입니다. 해당 부분의 유효성을 해결하기 위해서는 다음 두가지 규칙을 따라야 합니다.

첫번째, 딜리게이트에 하나의 매개 변수만 허용하면 다음과 같은 규칙을 따라야 합니다.

func textFieldShouldClear(UITextField)

그리고 둘 이상의 매개변수가 필요한 경우 다음과 같이 구조화되어야 합니다.

func textField(UITextField, shouldChangeCharactersIn: NSRange, replacement: String)

반환하는 속성이 있을 경우는 다음과 같이 지정하는 것이 올바른 방법입니다.

func calendarShouldChangeYear(_ calendar: Calendar) -> Bool

순수(Pure) 스위프트 프로토콜

순수 스위프트 프로토콜과(non @objc) Objective-C프로토콜(@objc)는 미묘하게 다르며, 그 차이점이 무엇인지 알아봅시다. 먼저 순수 스위프트 프로토콜입니다.

예를 들어, 다음과 같은 간단한 프로토콜을 만들어봅니다.

protocol MoviePlayerDelegate {
  func movieDidLoad()
  func movieShouldPause() -> Bool
  func movieWillEnd()
}

해당 프로토콜은 모든 ViewController에서 사용될 수 있도록, 영상이 로드, 일시중지, 종료 되는 시점에 이벤트 알림을 줄수 있도록 인터페이스를 작성합니다.

순수 스위프트 프로토콜은 반드시 명시된 모든 메서드들을 구현해야합니다, 만약, 해당 선택적인 메서드를 구현하는 경우 다음과 같이 두개로 프로토콜을 나누어 작업을 해야합니다.

protocol MoviePlayerStatusDelegate {
  func movieDidLoad()
}

protocol MoviePlayerPlayBackDelegate {
  func movieShouldPause() -> Bool
  func movieWillEnd()
}

Objective-C 베이스 프로토콜

Objective-C의 프로토콜 방식으로 스위프트에서 사용하려고 할 경우, 다음 세가지 사항을 고려해야합니다.

  • Objective-C 베이스 프로토콜을 사용하기 위해서 protocol 앞에 반드시 @objc를 붙여야됩니다.
  • 프로토콜의 메서드를 선택 사항으로 표시 할 수 있습니다.
  • 프로토콜과 함께 Swift의 구조체 및 enum을 사용할 수 없습니다.
  • 프로토콜 확장을 사용할 수 없습니다.

선택 사항은 Swift 프로토콜에서 선택적 유형과 달리 메서드에 선택 사항을 명시할 수 있습니다. 따라서 프로토콜을 준수하는 유형은 방법을 구현할 수 있지만 필수는 아닙니다.

Objective-C 언어의 프로토콜과 마찬가지로 해당 메서드의 선택 사항 또한 스위프트에서도 선택사항으로 바뀌게 됩니다. 해당 메서드가 존재할 수 있거나, 없을 수도 있기 때문에 이를 적절하게 확인, 배제, 또는 선택적 체인을 사용해야합니다. 이 구문은 Swift에서 자연스럽지않게 느껴질 수 있겠습니다만, 해당 프로토콜에 대해서는 알아볼 가치는 있다고 생각됩니다. 왜냐하면 @objc라고 쓰여진 Apple 플랫폼에는 많은 프로토콜들이 Objective-C로 되어있기 때문입니다.

다음 예는 동영상플레이어 델리게이트입니다.

@objc protocol MoviePlayerDelegate {
  @objc optional func movieShouldPause() -> Bool
  func movieWillEnd()
}

작성된 코드는 하나의 선택적인 메서드와 또하나는 필수 메서드로 정의했습니다.

우리는 델리게이트가 struct라는 새로운 구조체에서 프로토콜을 사용하려고 합니다.

struct MoviePlayer {
  var delegate: MoviePlayerDelegate?
  func pausePressed() -> Bool {
    /// ...?
  }
}

작성된 코드에서는 delegate라는 속성에 배정 된 대리인의 속성은 선택 사항입니다. 그리고 MoviePlayerDelegate 프로토콜에서 내부에서 정의한 movieShouldPause()옵션 요구사항을 구현하지 않았습니다.결과적으로 값을 반환하기위해서 pausePressed() 메서드를 사용하려면 선택적 체인을 사용해야합니다.

struct MoviePlayer {
  var delegate: MoviePlayerDelegate?
  func pausePressed() -> Bool {
    return delegate?.movieShouldPause?() ?? true
  }
}

여기서 두 개의 선택적 체인을 사용하여, 해당 델리게이트의 메서드가 구현되어있는지, 항상 확인하고 구현체가 없을 경우 또한 고려해야합니다.

그외에, 프로토콜에서는 구조체 또는 enum과 함께 사용할 수 없고, 또한 해당 프로토콜에 대한 확장을 만들 수 없다는 것입니다. 이러한 제한의 이유는 Objective-C에서 사용할수 없기 때문입니다. 따라서 이 둘을 결합하는 합리적인 방법을 모색하기 보다는 둘 중 하나를 선택하여 사용하는걸 추천드립니다.


클래스 전용 프로토콜

스위프트는 ARC(Auto Reference Counting)이라는 메모리 자동관리 시스템으로 내부의 메모리를 관리하게 됩니다. 레퍼런스 카운트는 기본적으로 객체 생성(+1), 객체 해제(-1)이 되는데, 자동으로 0으로 되는 시점에 시스템에서 메모리를 해제 관리 대상으로 이관되어 집니다. 이러한 메모리 관리에서 때때로 프로토콜은 느슨한 참조라고 불리는 방식을 사용하게 됩니다. 이러한 느슨한 참조에서 카운팅을 증가를 피하기 위해서 사용하는 방법 중 한가지는 delegate라는 속성을 참조 타입으로 사용하는 방법이 있습니다.

예를 들어 쇼핑 목록을 만드는 앱이 있습니다. 사용자는 구매가능한 항목의 목록을 탐색 한 다음 하나를 눌러 세부 정보 화면이 나오게 되며, 그 세부 화면에서 장바구니 버튼을 탭하여, 몇가지 아이템을 추가하고 다시 목록으로 다시 돌아가는 시나리오를 구현해야합니다.

객체와 객체간의 관계의 결합도(Coupling)을 낮추고 응집도(Cohesion)을 높히는 방법 중 하나인 델리게이트패턴을 활용해서 작성해봅시다.

protocol ListDelegate {
  func item(_ item: String, didUpdate quantity: Int)
}

그리고 해당 델리게이트를 사용하여 ItemDetailController를 만들어 델리게이트와 연동하도록합니다

struct ItemDetailController {
  var delegate: ListDelegate?
}

위의 ARC의 수명주기를 고려하게 될 경우, 아무것도 지정되지않을 경우 Strong (+1)이 되어집니다. 해당 강한 참조를 사용한다면, 메모리 누수로 이어지게 됩니다. 이러한 문제를 해결하기 위해서는 weak이라고 불리는 느슨한 참조를 사용하여, 예제 코드를 수정 해 봅시다.

struct ItemDetailController {
  weak var delegate: ListDelegate?
}

그러나 컴파일이 되지 않는 새로운 문제가 발생하게 됩니다. weak이라는 키워드는 Data유형이기 때문에 구조체 또는 열거형에서 사용될 수 없기 때문입니다. weak은 항상 정확하게 하나의 소유자를 가지고 있기 때문에 해당 키워드는 의미가 없어지게 되죠.

클래스 프로토콜은 이러한 문제를 해결하기 위해 설계 되었으며, 구현하기도 쉽습니다. 다음과 같이 AnyObject라는 프로토콜을 준수하도록 추가해봅시다.

protocol ListDelegate: AnyObject {
  func item(_ item: String, didUpdate quantity: Int)
}

이제 프로토콜은 ListDelegate와 AnyObject를 준수해야 하므로, Struct에서 사용할 수 없게 되었습니다.

class ItemDetailController {
  weak var delegate: ListDelegate?
}

이제 우리는 약한 참조로 프로토콜을 사용할 수 있게 되었습니다.

클래스 전용 프로토콜에는 AnyObject와 class 프로토콜은 사용할 수 있습니다. 다만 둘 간의 네이밍만 동일한 의미입니다. ( 발췌: SE-0156 )


프로토콜 상속과 결합

프로토콜은 다른 프로토콜과 결합 (Protocol Composite)하여 더욱 포괄적인 프로토콜을 만들 수 있습니다.

예를 들자면, Swift4에 업데이트 된, Codable이 좋은 예입니다.

typealias Codable = Decodable & Encodable

프로토콜 상속을 사용하여 다음과 같이 더 큰 프로토콜을 작성할 수 있습니다.

protocol Payable { }
protocol NeedsTrainning { }
protocol HasRestTime { }
protocol Employee: Payable, NeedsTrainning, HasRestTime { }

여기서 이점은 Employee 상속 된 프로토콜은 기존에 요구사항 + 추가 요구사항을 작성 할 수 있게 됩니다.


관련 유형 ( Associated types )

프로토콜 관련 유형은 Swift의 가장 진보 되고, 복잡한 기능 중 하나이며, 제대로 이해하고 사용할 경우, 많은 코드량과 시간을 절약시킬 수 있습니다.

관련유형이 있는 프로토콜은 불완전한 프로토콜 또는 구멍이 있는 프로토콜이라고 부릅니다. 해당 불완전완 요소를 완전한 요소로 바꾸는 작업은 구현체가 담당하게 됩니다.

관련 유형은 실제로 다소 복잡하게 느낄 수 있기 때문에, 간단한 예제부터 살펴 봅시다.

protocol Identifiable {
 var id: Int { get set }
}

이 예제 코드는 Identifiable 이라는 프로토콜을 생성하고 정수 타입의 id 속성을 제공하게 되어 있습니다.

우리는 다음과 같은 프로토콜에 맞는 두 가지 유형을 구현체로 만들 수 있습니다.

struct Person: Identifiable {
  var id: Int
}

struct WebPage: Identifiable {
  var id: Int
}

두개의 구현체는 각각 id를 정수로 구현하였으므로, 프로토콜을 준수하게 되었습니다.

그러나 요구사항이 변경된다면, 더이상 사람과 웹 페이지는 정수로 식별되지 않습니다. 예를 들어 Person 구조체는 0000-00-000 의 문자열 된 유니크한 키값(예: 고객 식별 코드)으로 식별하고, 또한 WepPage는 URL로 식별 ID를 붙여 페이지를 식별할 수 있습니다.

그렇다면 두개의 조건을 사용할 경우 다음과 같이 유형부분을 수정해야 합니다.

struct Person: Identifiable {
  var id: String
}

struct WebPage: Identifiable {
  var id: URL
}

하지만, 더이상 id의 유형은 정수가 아니므로, 프로토콜을 준수하지 않기 때문에 오류가 발생합니다.

해당 방법을 해결하기 위해서는 관련 유형을 사용하여 이를 해결해 봅시다. 프로토콜에서 정수 유형을 관련 유형으로 변경하여 블 완전한 프로토콜을 만든 다음, 구현체인 Person과 WebPage에 프로토콜을 관련 유형을 사용할 유형으로 변경 해봅시다.

protocol Identifiable {
  associatedtype IDType
  var id: IDType
}

struct Person: Identifiable {
  typealias IDType = String
  var id: IDType
}

struct WebPage: Identifiable {
  typealias IDType = URL
  var id: IDType
}

여기서 Swift의 타입추론은 매우 똑똑하기 때문에, 구현체에서 typealist IDType을 명시 하지 않아도, 스위프트에서 변수 이름이 일치한 경우, 생략이 가능합니다.

대부분의 객체 지향 언어와 마찬가지로 Swift는 객체가 다른 형태로 나타내는 방법으로 다형성을 지원합니다. 예를 들어, Animal이라는 상위클래스에가 있고 Dog와 Cat을 각각 하위 클래스를 만들었습니다. 이를 배열로 상위클래스인 Animal로 배열을 만들어 Dog와 Cat을 자유롭게 추가할 수 있습니다. 스위프트의 프로토콜은 이와 같은 방법을 프로토콜에서도 사용할 수 있게 하는 방법이 프로토콜에서 관련 유형 추가입니다.

그러나 스위프트는 관련 유형의 의미를 이해하지 못하기 때문에 혼동하여 일반적으로 사용할 수 없습니다. 관련 유형을 사용한 프로토콜은 불완전한 프로토콜이라는 것을 기억하세요. 이 프로토콜에는 준수하는 모든 내용을 구현체에서 채워야 완전한 프로토콜이 되어, 서로간의 규약과 준수라는 규칙을 제대로 이행할 수 있습니다.

앞서 만든 예제를 사용해 봅시다.

let taylor = Person(id: "555-55-5555")

결과적으로 해당 코드는 잘 작동하고, 아무런 문제가 없습니다.

하지만, 이와 같은 코드는 그렇지 않습니다.

let taylor: Identifiable = Person(id: "555-55-5555")

위와 같은 코드를 시도하려고 한다면, 개발자에게 잠못드는 밤을 보낼 있는 메시지가 나타납니다.

- Xcode 원문
 "error: protocol 'Identifiable' can only be used as a generic constraint because it has Self or associated type requirements"

- 번역
 "오류 : 프로토콜 'Identifiable'은 자체 또는 관련 유형 요구 사항이 있으므로 일반 제약 조건으로만 사용할 수 있습니다." 

분명한 반복을 피하기 위해 제네릭 제약 조건을 사용해봅시다. 예를 들어, checkIndentification() 이라는 메서드를 모든 종류의 객체에서 호출할 수 있는 메소드를 작성한 경우, Identifiable 프로토콜로 제공할 수 있습니다.

func checkIndentification<T>(object: T)

제네릭 제약조건을 사용하면 어떤 유형을 사용할 수 있는지 제한 할 수 있습니다. 제한할 조건은 Identifiable와 같은 유형만 허용으로 제한해 봅시다.

func checkIndentification<T: Identifiable>(object: T)

위의 오류 메시지를 위의 제네릭 함수와 연결지어 해석해보겠습니다. 관련 유형의 요구 사항은 위의 코드처럼 제네릭 제한 조건을 추가한것과 동일합니다. Self 오류메시지의 일부는 저장된 특성 또는 메서드 매개 변수에 대한 프로토콜의 구현체에서 현재 유형을 구체적으로 나타내는 특수 연관 유형을 사용하는 것을 의미합니다.

참고: Self 대문자 S는 프로토콜 내에서 “구현 된 구체적인 유형”을 의미하는 반면에, self의 소문자 s는 유형 및 확장 내에서 “현재 인스턴스”를 의미합니다.

다음 Self와 self 같이 사용된 코드를 흔히 볼 수 있습니다.

extension Numeric {
  func squared() -> Self {
    return self * self
  }
}

추가로 작성된 확장에서 제곱의 반환을 Self로 되어있습니다. ( 물론 사용방법에 따라 Int, Float, Double등을 사용하여 다를 수 있습니다.) 이와 같이 관련 유형에 문제가 발생되는 주된 이유는 Self 라는 요구 사항이 있는 프로토콜을 사용하려고 할 때 입니다. 인스턴스 유형을 필요로 하는 프로토콜 Self 또는 매개변수로 허용되는 Self 메소드임을 기억하십시오

예를 들어, Equatable 프로토콜에는 다음과 같은 메소드가 필요로 합니다.

static func == (lhs: Self, rhs: Self) -> Bool

Self 라는 매개변수로 2번 볼 수 있습니다. 즉, 프로토콜로 상속 받게되면 Equatable은 즉시 다형성을 사용할 수 없게 됩니다.


개요

프로토콜은 Apple 플랫폼 개발의 필수 요소 중 하나이며, Swift와 Objective-C 프로토콜 간의 주요 차이점과 유사점이 있으며, 제안 된 지침은 다음과 같습니다.

  • 사물로 사용되는 프로토콜은 명사로, 능력이 포함된 프로토콜은 형용사로 사용해야 합니다. (“able”, “ible”, “ing”)
  • will, did, should라는 메서드를 사용하는 델리게이트 또는 데이터 소스 프로토콜은 첫 번째 매개 변수로 이벤트를 트리거한 객체를 전달해야합니다.
  • 순수 스위프트 프로토콜은 스위프트의 모든 기능과 표현성을 사용할 수 있지만, 선택적인 요건은 가질 수 없습니다.
  • @objc 프로토콜은 선택적 요구 사항을 가질 수 있지만, 구조체 또는 열거형(enum)에서는 작동하지 않으며 프로토콜 또한 확장할 수 없습니다
  • 프로토콜을 준수하는 유형을 weak(느슨한) 참조 유형으로 사용하여 클래스에서 사용할 수 있게 하려면 클래스 전용 프로토콜을 사용하세요
  • 프로토콜은 다른 프로토콜과 결합이 가능합니다. 즉, 모든 것을 포함하는 하나의 초대형 프로토콜을 작성하려는 것은 의미가 없습니다. 대신 작은 것으로 시작하여 더 큰 것을 만들도록 구성하세요
  • 관련 유형을 사용하면 강력한 기능이 될 수 있지만, 실수로 우연히 오류를 발견 될 수 있습니다. 몇 가지 간단한 단계를 거치면 해당 오류를 해결할 수 있습니다.
10 Shares:
You May Also Like
Read More

초기화 패턴

스위프트의 초기화는 다른 언어들에 비해 유연하고, 기발하며, 강력하다고 생각합니다. 매우 구체적인 규칙을 따르고, 몇몇은 다른 주요 프로그래밍 언어들과 완전히 다른 방식으로 작동하며 그 규칙을 조금이라도 변형 할 수 있는 범위 또한 없습니다. 스위프트의 아버지라고 할 수 있는 Objective-C 의 초기화도 다른 언어들과의 독특하게 작동하기 때문에 스위프트 또한 그대로 유지되었을까 하는 추측입니다.
Read More

확장 패턴

스위프트의 확장(Extension) 기능을 사용하면 기존 데이터 유형의 기능을 추가하거나 수정할 수 있습니다.