iOS에서 프로토콜을 공부해보자:)

dqQQQ

·

2023. 11. 30. 09:09

개요

애플은 WWDC2015에서 스위프트를 소개하면서 프로토콜지향 프로그래밍 언어라고 했다.

그러나 프로토콜 지향 언어라고 해서 프로토콜만 있는 것이 아니라 더 많은 것을 포함하고 있다.

스위프트의 표준 라이브러리는 프로토콜에 기반을 둔다.

이 프로토콜은 스위프트에서 새로나온 개념이 아니라 옵젝시에서도 많이 사용하던 개념이다.

옵젝시에서의 사용법, 스위프트에서의 사용법을 알아보겠다.


프로토콜이란?

프로토콜이란 객체의 동작을 나타내는 메서드 선업 집합이라고 정의된다.

프로토콜은 보통 네트워크에서 인터넷 같은 통신 규약같은 의미로 널리 사용된다.

그러나 iOS 에서는 객체가 담당하고 있는 역할의 청사진 역할을 하는 것이라고 볼 수 있다. 자바나 C++의 인터페이스의 개념과 유사하다.

역할과 개념이 다른 객체여도 같은 동작을 할 때가 있다. 그런 경우는 상속관계로 설계하기엔 억지스러운 면이 있고 그럴 때 프로토콜을 사용한다.

예를들어 배열, 리스트, 트리를 구현할 때에 이 3개의 자료구조는 다른 개념의 객체이지만 요소 추가, 삭제와 같은 공통된 동작을 한다.

이럴 때 프로토콜을 하나 만들고 그것을 채택하는 방식으로 구현한다.

스위프트와 옵젝시 모두 클래스의 다중상속을 지원하지 않는다. 따라서 다중상속이 필요할 때는 프로토콜을 사용해야한다.


옵젝시에서의 프로토콜 선언과 채택

프로토콜 선언

@protocol DataStructure
- (void) add;
- (void) remove;
@end

프로토콜 채택

@interface myLinkedList: NSObject <DataStructure>
{
    인스턴스 변수 선언; //count 같은 것들
}

메서드 선언;

@end

이렇게 프로토콜을 채택하면 메서드를 다시 선언할 필요가 없고 프로토콜에서 정의한 것들은 모두 구현해야한다.


스위프트에서의 프로토콜 요구 사항

프로토콜에서 프로퍼티를 정의할 때에는 get과 set 키워드를 사용해 명시해줘야한다.

프로토콜에서는 타입 추론을 사용할 수 없기 때문에 프로퍼티의 타입을 명시해줘야한다.

메소드는 구현부가 없는 점을 제외하고는 클래스와 구조체에서 정의했던 것과 같다.

static을 사용할 수 있지만 기본 값을 세팅하지는 못한다.

구조체 같은 값 타입일 경우 메소드 앞에 mutating을 추가해야한다. 따라서 프로토콜에서도 붙여줘야한다.

@objc로 프로토콜 속성을 부여하면 클래스만 채택이 가능하다.


프로토콜 상속

프로토콜은 상속이 가능하고 요구사항들을 추가할 수가 있다.

protocol Name {
  var firstName: String {get set}
  var lastName: String {get set}

  func getFullName() -> String
}

protocol Person: Name {
  var age: Int {get set}
}

struct Student: Person {
  var firstName = ""
  var lastName = ""
  var age = 0

  func getFullName() -> String {
    return "\(firstName) \(lastName)"
  }
}

프로토콜 컴포지션

프로토콜 컴포지션은 타입이 여러 프로토콜을 채용할 수 있게 해준다.

클래스는 한 개의 슈퍼클래스만 상속할 수 있지만 프로토콜을 사용하면 해결할 수 있다.

프로토콜 컴포지션은 모든 요구사항을 단일 프로토콜이나 단일 클래스에서 상속하지 않고 요구사항을 여러 작은 컴포넌트로 나눌 수 있게 해준다.


프로토콜 타입

프로토콜에 아무런 기능이 구현돼 있지 않다고 하더라도 스위프트에서는 여전히 하나의 완벽한 타입으로 간주하고 사용할 수 있다.

쉽게말해 함수의 매개변수나 반환형으로 사용할 수 있다는 것이다.

protocol Person {
  var firstName: String {get set}
  var lastName: String {get set}
  var age: Int {get set}

  func getFullName() -> String
}

func updatePerson(person: Person) -> Person {
  var newPerson: Person
  ...//newPerson 구현
  return newPerson
}

프로토콜의 다형성

다형성은 polymorphism으로 많다는 뜻의 poly와 형태를 뜻하는 morphe에 어원을 두고 있다.

protocol Person {
  var firstName: String {get set}
  var lastName: String {get set}
  var age: Int {get set}

  func getFullName() -> String
}

struct SwiftProgrammer: Person {
  ...
}

struct CProgrammer: Person {
  ...
}

var swift = SwiftProgrammer(...)
var c = CProgrammer(...)

var programmer: [Person] = [ ]
programmer.append(swift)
programmer.append(c)

만약에 programmer로 c의 다른 기능을 접근하고 싶다면 형변환을 해야한다.

이때는 as?로 하면 된다.


프로토콜의 연관타입

프로토콜에 associated type을 정의할 수가 있다.

연관타입이란 프로토콜 내에서 타입을 대신해 사용할 수 있는 이름을 제공한다.

연관타입에서 사용하는 실제 타입은 프로토콜이 채택되기 전까지는 정의되지 않는다.

연관타입을 사용한다는 것은 사용할 타입을 정확이 모르니깐 프로토콜을 채택하는 애한테 물어봐라고 할 수 있다.

associatedtype 키워드를 사용하면 된다.

protocol Queue {
  associatedtype QueueType
  mutating func addItem(item: QueueType)
  func count() -> Int
}

제네릭인것같은디


델리게이션

델리게이션은 코코아와 코코아 터치 프레임워크에서 광범위하게 사용된다.

동작을 델리겟(위임)하는 인스턴스는 델리게이트 인스턴스의 참조를 저장하고 있다가 어떠한 동작이 발생하면 델리게이팅 인스턴스는 계획된 함수를 수행하기 위해 델리게이트를 호출한다.

스위프트에서는 델리게이트가 해야할 일을 정의한 프로토콜을 생성하는 방식으로 델리게이션 디자인 패턴을 구현한다.

아래 예제를 보면 동작을 delegate한다는 것이 뭔지 감이 온다.

protocol DisplayNameDelegate {
  func displayName(name: String)
}

struct MyDelegate: DisplayNameDelegate {
  func displayName(name: String) {
    print("Name: \(name)")
  }
}

struct Person {
  var displayNameDelegate: DisplayNameDelegate

  var firstName = "" {
    didset {
      displayNameDelegate.displayName(name: getFullName())
    }
  }

  init(displayNameDelegate: DisplayNameDelegate) {
    self.displayNameDelegate = displayNameDelegate
  }

  func getFullName() -> String {
    return "\(firstName)"
  }
}