Protocol(프로토콜)이란?
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/
Documentation
docs.swift.org
공식문서에 프로토콜을 다음과 같이 정의하고 있습니다.
`프로토콜(Protocol)` 은 특정 역할을 수행하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의한다.
프로토콜이라는 문법을 처음봐서 생소하기도 했고 어려웠습니다. 이번 시간에 차근차근 알아가보겠습니다.
프로토콜은 클래스, 구조체, 열거형에 의해 채택될 수 있다고 합니다. 채택된 타입에서 요구사항을 모두 충족한다면 해당 프로토콜을 '준수'한다고 말할 수 있습니다.
Protocol 기본 문법
Protocol은 3단계로 나누어집니다.
- 프로토콜 정의
- 프로토콜 채택
- 프로토콜 구현(준수)
단계를 순서대로 한번 알아보겠습니다.
1. Protocol 정의
일반적으로 타입을 정의하는 방식과 유사합니다. 하지만 프로토콜에서는 실제 구현을 하지않고 구현부(속성, 메서드)의 뼈대만 남겨 놓습니다.
protocol SomeProtocol {
// protocol definition goes here
var name: String { get }
func getName() -> String
}
2. Protocol 채택
- Struct(구조체)에서 채택
구조체는 상속의 개념이 없기 때문에 첫번째 자리부터 바로 프로토콜로 인식하면 되겠습니다.
struct SomeStructure: SomeProtocol {
// structure definition goes here
}
- Class(클래스) 채택
클래스에서는 상속의 개념이 있기 때문에 첫번째 자리에 슈퍼클래스가 오고 뒤에 쉼표로 프로토콜이 이어서 채택됩니다.
하지만 Swift에서는 클래스 다중 상속 개념이 없기 때문에 첫번째 자리 이후에는 프로토콜로 인식하면 되겠습니다.
class SomeClass: SomeSuperclass, SomeProtocol {
// class definition goes here
}
3. Protocol 구현(준수)
struct SomeStructure: SomeProtocol {
}
Protocol을 채택하고 아무것도 작성하지 않으면 컴파일 에러가 발생합니다.
Type 'SomeStructure' does not conform to protocol 'SomeProtocol'
해석하자면 "타입 'SomeStructure'는 프로토콜 'SomeProtocol'을 준수하지 않았다고 합니다."
네 프로토콜에 구현사항을 명시해 뒀는데 왜 안따르냐 이겁니다. 컴파일 에러부분을 클릭해 Fix를 누르면 자동으로 필수 프로토콜 구현사항의 뼈대가 작성되게 됩니다. 그렇게 하셔도 되고 직접 구현하셔도 상관없습니다.
struct SomeStructure: SomeProtocol {
// structure definition goes here
var name: String
func getName() -> String {
return "철수"
}
}
지금까지 프로토콜의 기본 문법을 알아봤습니다. 근데 이걸 도대체 언제 쓸까요? 여러가지 이유가 있겠지만 대표적으로는 상속의 단점을 보완하기 위해 사용한다와 프로젝트에서는 어떤 타입에 관습적으로 또는 항상 쓰이는 프로퍼티나 메서드가 있다면 프로토콜로 채택하게 만들어 자동으로 구현되게 하는 것입니다.
- 상속의 단점 보완
- 프로젝트에서 어떤 타입에서 강제하고 싶은 프로퍼티나 메서드가 있을 경우
Class의 상속이 왜 단점이 될까?
우선 상속이 무엇인지 간단하게 알아보겠습니다.
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
print("makeNoise")
// do nothing - an arbitrary vehicle doesn't necessarily make a noise
}
}
Vehicle이라는 클래스 타입을 만들어 주었고 속성과 메서드를 기본적으로 넣어주었습니다. Vehicle 클래스를 부모클래스로 하여 상속해보겠습니다.
class Bicycle: Vehicle {
var hasBasket = false
}
Bicycle에서 Vehicle을 상속받았기 때문에 속성 및 메서드 와 같은 Vehicle의 모든 특성을 자동으로 얻을 수 있습니다.
let bicycle = Bicycle()
bicycle.makeNoise() // "makeNoise"
이것이 바로 상속의 장점이자 단점입니다. Bicycle 클래스에서 부모클래스의 currentSpped와 같은 속성만 상속받고 싶은데 메서드까지 자동으로 상속받아버려 불필요하게 상속된다는 것입니다. 물론 사용하지 않아도 되지만 메모리에서 사용하지 않는 데이터가 남아있다고 생각하니 비효율적인게 맞는것 같습니다.
그럼 다시 프로토콜을 하나 선언해보겠습니다.
protocol VehicleProtocol {
func run()
}
그냥 run 메서드만 선언되어 있습니다. 이것은 실제 구현한 것이 아닙니다. ⭐️
자금 위에서 상속관계에 있는 Vehicle과 Bicycle에 프로토콜을 채택해보겠습니다.
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
print("makeNoise")
// do nothing - an arbitrary vehicle doesn't necessarily make a noise
}
}
class Bicycle: Vehicle, VehicleProtocol {
func run() {
// code
}
var hasBasket = false
}
불필요하게 부모클래스의 메서드를 상속받지 않아도 되고 정말 사용하고 싶은 클래스에서만 프로토콜을 채택해 사용하게 되니깐 상속의 단점을 보완한 것을 볼 수 있습니다.
다음으로는 프로젝트에서 예시를 한번 보겠습니다.
프로젝트 예시
기본적으로 ViewController 하나 생성해보겠습니다. 여기서 모든 뷰컨트롤러 마다 매번 함수를 통해 View의 디자인을 하는 코드를 작성하고 있다고 가정해보겠습니다.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
designView()
}
func designView() {
self.view.layer.cornerRadius = 10
self.view.clipsToBounds = true
}
}
간단하게 예를 들어 ViewController, ViewController2, ViewController3 에 모두 designView를 작성해 view를 디자인한다고 가정했을 때 함수 호출안하고 그냥 viewDidLoad에 구현할 수도 있고 같은 로직인데 다른 함수명으로 똑같은 로직을 작성할 수도 있고 그렇게 되면 코드가 중구난방이 될 확률이 높아질겁니다. 그래서 designView 함수를 프로토콜로 만들어 채택해서 사용해보겠습니다.
- 프로토콜 작성
protocol ViewControllerDesign {
func designView()
}
- 프로토콜 채택
class ViewController: UIViewController, ViewControllerDesign {
override func viewDidLoad() {
super.viewDidLoad()
designView()
}
}
- 프로토콜 준수(구현)
class ViewController: UIViewController, ViewControllerDesign {
override func viewDidLoad() {
super.viewDidLoad()
designView()
}
func designView() {
// code
}
}
그렇게되면 view를 디자인해야하는 ViewController마다 취사선택해 해당 프로토콜을 채택해 일관성있는 메서드명으로 사용가능하게 될 것입니다.
또한 프로토콜은 UIKit으로 app을 만들 때 이미 알고 사용중일 수도 있고 그냥 관습적으로 사용했었을 수도 있습니다. 대표적인 TableView 예시를 한번 살펴보겠습니다.
TableView를 만들기 위해 UIViewController에서 tableView에 대한 프로토콜을 채택해서 사용하셨을겁니다.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
}
}
그럼 또 컴파일 에러가 발생합니다.
Type 'ViewController' does not conform to protocol 'UITableViewDataSource'
한번 봤던 에러네요 프로토콜을 준수하지 않았으니 내용을 준수해달라는 겁니다. 관습적으로 Fix나 개념상 알고 있어 다음 두 메서드를 추가하셨을겁니다.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
<#code#>
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
<#code#>
}
그러면 TableView를 사용할 준비가 되었습니다. 근데 왜 꼭 이 2개의 메서드를 구현해야할까요? 그것은
UITableViewDelegate, UITableViewDataSource가 프로토콜이기 때문에 이미 위 2개의 메서드를 채택하면 구현하게끔 선언이 되어있을 겁니다.
그렇다면 여기서 궁금하네요 진짜 프로토콜로 구현되어있는지 한번 확인해보겠습니다. 해당 타입에 cmd+click하면 jump to definition으로 구현코드를 볼 수 있습니다.
UITableViewDataSource가 정말 protocol로 선언되어 있네요. 그렇기 때문에 채택하면 무조건 빨간색으로 표시된 2개의 메서드를 무조건 채택해 구현해야됐습니다. 애플에서 테이블뷰를 구현할 때 최소한 필수적으로 2개의 메서드는 있어야 테이블뷰를 나타낼 수 있게 만들었던 겁니다.
그런데 밑에 보면 numberOfSections라고 Section의 개수를 나타는 메서드가 있습니다. 필요할 때 쓰지만 강제하지는 않고 있습니다. 왜그럴까요? 살펴보면 2개의 필수 메서드말고는 func 앞에 optional 키워드가 붙어있습니다. 네 프로토콜에서는 필수적으로 구현해야 하는 부분과 선택적으로 구현해야하는 부분을 나눠둘 수 있게 되어 있습니다.
Protocol(프로토콜)의 선택적 요구사항
위 예시에서 봤듯이 프로토콜에 선택적 요구사항을 남길 수 있습니다.
@objc protocol ViewControllerDesign {
func designView()
@objc optional func designButton()
}
class ViewController: UIViewController, ViewControllerDesign {
func designView() {
<#code#>
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
프로토콜에서 선언되어 있던 'designView' 메서드를 구현하지 않아도 에러가 나지 않습니다. @objc 어트리뷰트를 사용해 선택적 요구사항 멤버앞에 작성하여 나타낼 수 있습니다. 옵젝씨의 잔재...ㅠ
- 구조체, 열거형 사용 불가(objective-c에서 클래스에서 채택되도록 구현되어 있음)
- objective-c에서 클래스밖에 없었기 때문...
속성 요구 사항
프로토콜을 정의할 때는 형식을 지정해줘야합니다. 실제 값을 저장하지 않고 타입을 지정하거나 하는 등의 형식을 지정해줘야합니다.
속성이 저장속성인지 계산속성인지 필요한 속성 이름과 타입만 지정합니다.
각 속성이 gettable인지 gettable & settable이어야 하는지 여부를 지정합니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
타입 저장 속성도 마찬가지로 타입 키워드인 static 만 붙이고 똑같이 지정합니다.
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
메서드 요구 사항
메서드의 헤드부분(input, output)을 정의 합니다.
protocol SomeProtocol {
func someMethod()
func randomNumber() -> Int
mutating toggle()
static func reset()
}
return값이 따로 없다면 생략이 가능합니다.
만약 class에서 채택 시 static/class 키워드로 메서드 구현가능합니다. 또한 매개변수를 지정하는 방법도 가능합니다.
protocol SomeProtocol {
func randomNumber(_ num: Int) -> Int
}
하지만 가변 매개변수의 기본값을 지정하는 것은 불가능합니다.
생성자 요구 사항
생성자 또한 메서드와 마찬가지고 {} 중괄호는 사용하지 않고 매개변수를 작성하여 지정할 수 있습니다.
protocol SomeProtocol {
init(someParameter: Int)
}
생성자에는 조금 유의해서 봐야할 규칙이 있습니다.
- 상속을 고려해야하는 클래스에서 필수 생정자로 구현해야한다.
- 프로토콜을 채택한 클래스에서 final class로 상속을 제한한다면 필수 생성자로 구현하지 않아도 된다.
- 프로토콜을 채택하더라도 반드시 지정 생성자로 구현할 필요는 없다.
위 3가지 정도의 생성자 규칙이 존재합니다. 하나씩 살펴보겠습니다.
1) 규칙같은 경우 프로토콜로 생성자를 강제했는데 필수 생성자가 아니게 되면 상속을 했을 경우 상위클래스의 모든 속성이 초기화되지 않기 때문에 반드시 필수 생성자로 지정해야합니다.
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
<#code#>
}
}
2) 만약 final 키워드로 class의 상속을 제한한다면 굳이 필수 생성자로 구현하지 않더라도 상속 관계에서의 초기화를 신경쓰지 않아도 되므로 필수 생성자로 구현하지 않아도 됩니다.
final class SomeClass: SomeProtocol {
init(num: Int) {}
}
3) 말 그대로 프로토콜을 채택하더라도 반드시 지정 생성자로 구현할 필요는 없고 편의 생성자로도 구현 가능합니다.
class SomeClass: SomeProtocol {
required convenience init(num: Int) {
self.init()
}
init() {}
}
실패 가능 생성자, 실패 불가능 생성자
실패 가능 생성자와 실패 불가능 생성자를 프로토콜로 지정하고 채택하는 경우 다음과 같이 정리할 수 있습니다.
실패 가능 생성자 init?()
- init(), init?(), init!()
protocol SomeProtocol {
init?(someParameter: Int)
}
class SomeClass: SomeProtocol {
// required init?(someParameter: Int) {}
// required init!(someParameter: Int) {}
// required init(someParameter: Int) {}
}
실패 불가능 생성자 init()
- init()
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
// required init?(someParameter: Int) {} // 불가능
// required init!(someParameter: Int) {} // 불가능
// required init(someParameter: Int) {} // 가능
}
메서드 요구사항 - 서브스크립트 요구사항
- get(읽기), set(쓰기)를 요구사항에 구현
- get(읽기): 필수사항, 최소한의 요구사항
- set(쓰기): 선택사항
protocol DataList {
subscript(idx: Int) -> Int { get }
}
struct DataStructure: DataList {
subscript(idx: Int) -> Int {
get {
return 0
}
set {
// 구현은 선택
}
}
}
프로토콜의 상속
프로토콜도 상속이 가능합니다. 하지만 클래스와 다르게 다중 상속이 가능하다는 점이 다릅니다. 예시를 바로 한번 살펴보겠습니다.
protocol SomeProtocol {
func randomNumber() -> Int
}
protocol AnotherProtocol {
func changeIndex()
}
protocol ChildProtocol: SomeProtocol, AnotherProtocol {
func doSomething()
}
struct AStruct: ChildProtocol {
func doSomething() {
<#code#>
}
func randomNumber() -> Int {
<#code#>
}
func changeIndex() {
<#code#>
}
}
ChildProtocol에서 SomeProtocol, AnotherProtocol을 상속받고 구조체에서 채택을 하였더니 ChildProtocol에서 지정하지 않았던 메서드까지 필수 구현 내용으로 채택되었습니다.
클래스 전용 프로토콜(AnyObject)
AnyObject 키워드를 본 적이 있을겁니다. 배열 같은 곳에 타입을 선언할 때 AnyObject 타입으로 선언하게 되면 클래스만 배열에 담을 수 있게 해주는 키워드였습니다. 이것을 프로토콜에도 적용할 수 있는데 프로토콜을 선언할 때 Anyobject를 사용하게 되면 해당 프로토콜을 클래스에서만 채택할 수 있게 됩니다. 구조체에서 채택하려고 하면 컴파일 에러가 나게됩니다.
protocol SomeProtocol: AnyObject {
func randomNumber() -> Int
}
class SomeClass: SomeProtocol {
func randomNumber() -> Int {}
}
struct SomeStruct: SomeProtocol {}
Non-class type 'SomeStruct' cannot conform to class protocol 'SomeProtocol'
프로토콜 합성(Protocol Composition) 문법
프로토콜은 다중 채택이 가능하다고 하였습니다.
protocol SomeProtocol {
func randomNumber() -> Int
}
protocol AnotherProtocol {
func changeIndex()
}
class SomeClass: SomeProtocol, AnotherProtocol {
func randomNumber() -> Int {}
func changeIndex() {}
}
원래는 쉼표(,)를 기준으로 여러 프토토콜을 채택하여하는데 &로 다중 채택을 가능하게 할 수 있습니다.
protocol SomeProtocol {
func randomNumber()
}
protocol AnotherProtocol {
func changeIndex()
}
class SomeClass: SomeProtocol & AnotherProtocol {
func randomNumber() {}
func changeIndex() {}
}
프로젝트에서 적용해볼 수 있는 곳이 딱 있을 것 같습니다. 바로 테이블뷰에서 프로토콜 채택을 항상 2개 해주는데 이곳에 적용시켜 보겠습니다.
extension ViewController: UITableViewDataSource & UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "")!
return cell
}
}
다중 채택이 가능하기 때문에 계속 &로 이어서 채택하면 될 것 같네요
프로토콜의 확장(Protocol Extension)
프로토콜에서도 확장이 가능합니다. 프토토콜의 확장은 여러 타입에서 같은 프로토콜을 채택했을 경우 코드가 중복되는 상황의 단점을 보완할 수 있습니다.
프로토콜에서 메서드의 구현부가 작성되면 Witness Table이라는 가지게 되며 이 곳에 메서드 이것은 참조 타입의 메서드의 포인터를 저장하는 방식인 vtable(Virtual Dispatch Table) 을 가지는 것과 유사한 방식입니다. 여기서는 간단하게 이 정도만 알고 넘어가고 메서드 디스패치(Method Dispatch) 글에서 좀 더 자세히 다뤄보도록하겠습니다.
본론으로 넘어가서 프로토콜을 채택시 중복된 경우를 살펴보겠습니다.
protocol Language {
func sayEnglish()
func sayKorean()
}
class SomeClass: Language {
func sayEnglish() {}
func sayKorean() {}
}
class AnotherClass: Language {
func sayEnglish() {}
func sayKorean() {}
}
각각의 클래스에서 같은 프로토콜을 채택하고 있어 메서드가 중복되어 구현되어있습니다. 이것을 직접 구현하지 않고 확장을 통해 구현된 메서드를 호출하는 방식을 사용해보겠습니다.
protocol Language {
func sayEnglish()
func sayKorean()
}
class SomeClass: Language {
// func sayEnglish() {}
// func sayKorean() {}
}
class AnotherClass: Language {
func sayEnglish() {}
func sayKorean() {}
}
extension Language {
func sayEnglish() {
print("Hello")
}
func sayKorean() {
print("안녕하세요")
}
}
let ko = SomeClass()
ko.sayEnglish() // "Hello"
ko.sayKorean() // "안녕하세요"
SomeClass에서 채택한 프로토콜의 메서드를 직접구현하지 않았지만 컴파일 에러가 발생하지 않았습니다.
이후 인스턴스를 생성해 채택한 프로토콜에서 선언한 메서드를 호출해 보았더니 프토토콜 확장에서 구현된 메서드가 실행되었습니다.
여기서 2가지 사실을 알 수 있었습니다.
- 프로토콜은 확장할 시 직접 구현이 가능하다.
- 채택한 타입에서 직접 구현하지 않아도 확장한 프로토콜의 메서드를 사용가능하다.
근데 여기서 확장에서 구현된 메서드를 사용하지 않고 직접 구현한 메서드를 사용하고 싶을 수 있습니다. 그럴 땐 그냥 직접 구현하면 됩니다.
class SomeClass: Language {
func sayEnglish() {
print("Hi")
}
func sayKorean() {
print("반갑습니다")
}
}
let ko = SomeClass()
ko.sayEnglish() // "Hi"
ko.sayKorean() // "반갑습니다"
정리하자면 다음과 같습니다.
💡 프로토콜 확장으로 기본 구현한 메서드를 채택한 타입(클래스, 구조체, 열거형)에서 직접 구현하지 않았다면 프로토콜 확장의 메서드가 실행되고, 직접 구현하였다면 구현한 메서드가 실행된다.
- 채택한 프로토콜 메서드 직접 구현 O
- 기본 메서드 실행 (우선순위 1)
- 채택한 프로토콜 메서드 직접 구현 X
- 프로토콜 확장 메서드 실행 (우선순위 2)
프로토콜 확장 제한
where문을 통해서 확장을 제한할 수 있습니다.
protocol Nation {}
protocol Language {
func sayEnglish()
func sayKorean()
}
class SomeClass: Language {
}
extension Language where Self: Nation {
func sayEnglish() {
print("Hello")
}
func sayKorean() {
print("안녕하세요")
}
}
let ko = SomeClass()
ko.sayEnglish()
ko.sayKorean()
Extension부분의 코드를 해석하면 Nation 프로토콜을 채택하지 않는다면 확장 기능을 제공하지 않는다는 의미입니다.
위 코드를 실행하면 컴파일 에러가 발생합니다.
Type 'SomeClass' does not conform to protocol 'Language'
아래와 같이 Nation 프로토콜을 채택하면 확장에서 구현된 메서드가 호출됩니다.
class SomeClass: Language, Nation {
}
'iOS > Swift' 카테고리의 다른 글
Swift - Swift에서는 왜 문자열을 Subscript로 접근할 수 없을까? (1) | 2023.12.12 |
---|---|
Swift - Method Swizzling (1) | 2023.12.01 |
ARC(Automatic Reference Counting) (0) | 2023.09.04 |
Swift - Type Casting (0) | 2023.07.29 |
Swift - @Property Wrapper (1) | 2023.07.27 |