프로토콜은 메서드, 프로퍼티, 특정 작업이나 기능의 청사진을 의미한다.
청사진(靑寫眞) 또는 블루프린트
는 아키텍처 또는 공학 설계를 문서화한 기술 도면을 인화로 복사하거나 복사한 도면을 말한다. 은유적으로 "청사진"이라는 용어는 어떠한 자세한 계획을 일컫는 데에 쓰인다. - 위키백과
프로토콜은 프로그래밍 내에서 흔히 약속, 규칙이라고 불린다.
프로토콜은 자신을 채택하는 타입들에게 지켜야 할 최소한의 요구사항을 전달할 수 있다.
단, 이에 대한 직접적인 구현은 하지 않는다.
프로토콜은 요구사항을 전달하고,
해당 프로토콜을 채택하는 타입은 해당 요구사항들을 구체화한다.
Swift는 Protocol Oriented Programming, 프로토콜 지향 프로그래밍으로써 프로토콜은 매우 중요한 요소 중 하나이다.
클래스, 구조체, 열거형과 같은 방법으로 프로토콜을 정의
protocol SomeProtocol {
}
나는 이 프로토콜을 채택하겠다!
하는 타입은 타입의 이름 뒤에 프로토콜의 이름을 적으면 된다.
여러 프로토콜을 채택할 수도 있다.
상위 클래스가 존재한다면, 프로토콜 전에 적어주기
class Student: Human, SomeProtocol1, SomeProtocol2 {
}
클래스, 구조체 등과 마찬가지로 프로토콜도 하나의 타입이므로, 대문자로 시작해야 한다.
프로퍼티는 타입들에게 인스턴스 프로퍼티, 타입 프로퍼티를 준수하도록 요구할 수 있다.
요구하는 프로퍼티는 3가지 사항을 지켜야 하는데,
프로토콜을 채택한 타입은 저장 프로퍼티, 연산 프로퍼티 모두를 사용해 구현할 수 있다.
ex.
protocol FullyNamed {
var fullName: String { get }
}
타입들이 모두 풀네임을 제공할 수 있도록 하기 위해
FullyNamed
프로토콜을 만들고, fullName
이라는 프로퍼티를 만들었다.
이 프로토콜을 채택한 타입은 모두 fullName
프로퍼티를 가져야 한다.
protocol FullyNamed {
var fullName: String { get }
}
struct Student: FullyNamed {
var fullName: String
}
let min = Student(fullName: "Kim Min")
FullyNamed
프로토콜을 채택하여 fullName
프로퍼티를 가지는 Student
구조체
프로퍼티와 마찬가지로, 해당 프로토콜을 준수하는 타입이 구현해야 하는 특정한 메서드를 요구할 수 있다.
프로토콜을 채택하는 타입은 메서드 본문을 구현하여 구체화해야 한다.
ex.
protocol SomeProtocol1 {
func someMethod()
}
class SomeClass: SomeProtocol1 {
func someMethod() {
print("someMethod 구현하기")
}
}
프로퍼티와 같이 프로토콜을 채택했지만 요구사항을 구현하지 않으면 에러가 난다.
구조체와 열거형은 value type.
value type의 프로퍼티(== 구조체나 열거형 내의 프로퍼티)는 기본적으로
인스턴스 메서드 내에서 수정될 수 없음.
하지만mutating
을 앞에 선언해 주면 메서드 내에서 수정이 가능하다.
프로토콜에서도 mutating
를 메서드 앞에 선언하여, 인스턴스를 변경할 수 있음을 나타낼 수 있다.
이때 구조체와 열거형이 프로토콜을 채택하고 요구사항을 구현한다.
프로토콜의 메서드 요구사항이 mutating
을 선언했을지라도, 클래스에서는 표시할 필요 없다.
ex.
protocol Togglable {
mutating func toggle() // 타입의 상태를 전환하는 메서드
}
struct TestStruct: Togglable {
var num = 0
mutating func toggle() {
num = 1
print("num:", num)
}
}
var ts = TestStruct()
ts.toggle() // num: 1
Togglable
을 채택하여 toggle()
구현
class TestClass: Togglable {
var num = 0
func toggle() {
num = 4
print("num:", num)
}
}
let tc = TestClass()
tc.toggle() // num: 4
class에서는 mutating
키워드를 따로 선언해 주지 않아도 에러 없이 작동한다.
마찬가지로 프로토콜을 준수하는 타입에게 초기화 구문도 구현해 달라고 요구할 수 있음.
protocol SomeProtocol {
init(param: Int)
}
class SomeClass: SomeProtocol {
required init(param: Int) {
print("initialized...")
}
}
let sc = SomeClass(param: 3) // initialized...
SomeProtocol
을 채택한 SomeClass
는 요구된 초기화 구문을 구현해야 하는데,
이때는 required
키워드를 함께 표시해야 한다.
init background
init을 반드시 재정의해 줘야 하는 경우required
수식어를 붙여주고,
required
초기화 구문을 가진 클래스의 하위 클래스들은 해당 초기화 구문을 반드시 구현해야 함
required
를 붙임으로써 해당 클래스를 상속하는 다른 클래스들이 해당 초기화 구문을 구현해야 함을 나타냄.
따라서 SomeClass
를 상속하는 하위 클래스들도 init을 지정할 시에 required
키워드를 통해 프로토콜을 준수할 수 있도록 함.
protocol SomeProtocol {
init(param: Int)
}
class SomeClass: SomeProtocol {
required init(param: Int) {
print("SomeClass initialize paramter", param)
}
}
class AnotherClass: SomeClass {
required init(param: Int) {
print("AnotherClass initialize paramter", param)
super.init(param: param)
}
}
let ac = AnotherClass(param: 3)
/*
AnotherClass initialize paramter 3
SomeClass initialize paramter 3
*/
반대로 이야기하면,final
키워드는 재정의를 막는 키워드였음.
따라서 final
키워드가 선언된 클래스는 상속이 불가능하게 되므로, required
를 선언할 필요가 없다.
error: AboutProtocol.xcplaygroundpage:82:5: error: initializer requirement 'init(param:)' can only be satisfied by a 'required' initializer in non-final class 'SomeClass'
init(param: Int) {
^
required
required
붙여줘… :( 이런 오류가 뜨지만
final
을 선언해 준 경우, 오류 없이 잘 작동한다.
final class SomeClass: SomeProtocol {
init(param: Int) {
print("SomeClass initialize paramter", param)
}
}
let sc = SomeClass(param: 5) // SomeClass initialize paramter 5
만약에 상위 클래스의 init을 재정의하고, 프로토콜도 일치하는 init을 요구한다면
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
required override init() {
}
}
required
와 override
키워드를 모두 적어줘야 함.
또한 실패 가능한 초기화 구문도 프로토콜에 선언할 수 있다.
프로토콜은 그 자체로 타입이 될 수 있다.
protocol SomeProtocol {
}
class SomeClass: SomeProtocol {
init() {
print("initialized...")
}
}
let sc1: SomeClass = SomeClass() // initialized...
let sc2: SomeProtocol = SomeClass() // initialized...
delegate 패턴을 한마디로 이야기하자면,
자신이 해야 할 일을 다른 객체한테 위임할 수 있도록(넘겨주도록) 하는 디자인 패턴이다.
넘겨받은 일을 해야 하는 타입을 대리자라고 하면,
이 대리자가 내가 넘겨준 일을 모두 할 수 있게 보장되도록 할 일을 캡슐화 함(하나로 묶음).
-> 이런 작업을 프로토콜을 선언함으로써 할 수 있음.
그렇다면 대리자는 내가 넘겨준 일을 하기 위해 해당 프로토콜을 채택하고 구현을 하면 되는 것.
처음에는 위임이라는 단어가 너무 어려워서 delegate pattern을 이해하기 어려웠는데,
여러번 생각해 보니 개념 자체는 생각보다 간단하다!
예를 들어, UIViewController에서 UITableView를 구현할 때를 생각해보자.
tableView.dataSource = self
vc가 자신이 대리자가 되겠다고 선언 필요
UITableViewDataSource
채택하기
tableView(_:numberOfRowsInSection:)
/ tableView(_:cellForRowAt:)
구현하기
UITableViewDataSource
프로토콜은 실제로 이렇게 구성되어 있다.
@MainActor public protocol UITableViewDataSource : NSObjectProtocol {
@available(iOS 2.0, *)
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
@available(iOS 8.0, *)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
// ...
}
타입이 프로토콜을 채택한 뒤, 프로토콜의 요구사항을 모두 구현할 때, 이를 프로토콜을 준수했다고 표현한다.
protocol SomeProtocol {
func someMethod()
}
class SomeClass {
var someProtperty: Int
init(someProtperty: Int) {
self.someProtperty = someProtperty
}
}
let someClass = SomeClass(someProtperty: 3)
someClass.someMethod()
extension SomeClass: SomeProtocol {
func someMethod() {
print("채택했다!")
}
}
class SomeClass {
var someProtperty: Int
init(someProtperty: Int) {
self.someProtperty = someProtperty
}
func someMethod() {
print("채택했다!")
}
}
extension SomeClass: SomeProtocol {}
기존 타입의 코드에서 상속하거나 채택하는 프로토콜이 많은 경우, 이렇게 빈 extension에 채택을 하면 가독성이 좋아질 것 같다.
where
절을 활용하여 프로토콜을 조건적으로 준수할 수 있도록 할 수 있다.for i in 0..<10 where i % 2 == 0 {
print(i)
}
/*
0
2
4
6
8
*/
ex.
protocol SomeProtocol {
var description: String { get }
}
class SomeClass: SomeProtocol {
var description: String {
return "💦"
}
}
extension Array: SomeProtocol where Element == Int {
var description: String {
return "✅"
}
}
let arr = [1, 2, 3]
arr.description // ✅
프로토콜은 하나 이상의 다른 프로토콜을 상속할 수 있다.
클래스 상속 구문과 유사하다.
protocol SomeProtocol {
func someMethod()
}
protocol AnotherProtocol: SomeProtocol {
func anotherMethod()
}
class SomeClass: AnotherProtocol {
func anotherMethod() {
print("another method...")
}
func someMethod() {
print("some method...")
}
}
프로토콜이 다른 프로토콜을 상속한다면, 해당 프로토콜을 채택한 타입은 모든 요구사항을 충족해야 한다.
AnyObject
프로토콜을 상속하는 프로토콜은 클래스 타입만 해당 프로토콜을 채택할 수 있도록 한다.
protocol SomeClassOnlyProtocol: AnyObject {
}
// ✅
class TestClass: SomeClassOnlyProtocol {
}
// 🚨 error
struct TestStruct: SomeClassOnlyProtocol {
}
AnyObject
는 모든 클래스가 암시적으로 준수하는 프로토콜이라,
프로토콜의 요구사항이 reference type 체계에만 의미 있을 경우 AnyObject
를 채택하여 class-only 프로토콜을 사용하면 된다.
이런 케이스들에는 어떤 게 있을까 하다 클래스의 특징들과 관련될까? 싶었음.
대표적인 클래스의 특징에는 상속, 메모리 관리 등이 있을 거라 생각했고…
아무리 생각해도 정확히 모르겠어서 서치 결과를 퍼왔씁니당…
Restricting a Protocol to Classes in Swift
delegate pattern
delegate protocol을 정의할 때, 위임받는 대리자가 class인지 보장할 수 있음.
subclass requirements
상속에서 하위 클래스가 상위 클래스의 특정 메서드를 구현하도록 보장하기 위해서 class-only protocol을 사용하여 요구사항을 정의할 수 있음!
Interoperability(상호 운용성)
Swift ↔ Objective-C의 참조 기반 메모리 관리와의 호환성을 유지하기 위해서는 클래스 활용이 필요하다. class-only protocol을 통해 해당 프로토콜을 준수하는 타입이 클래스임을 보장할 수 있다.
여러 프로토콜의 요구사항을 하나로 합쳐 임시적인 프로토콜로 정의된 것처럼 동작하게 할 수 있다.
하지만 진짜로 새로운 프로토콜 타입을 정의한 것은 아님!
&
를 통해 많은 프로토콜을 리스트화 가능
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func celebrateBirthday(to celebrator: Named & Aged) {
print("HBD!", celebrator.name)
print("Happy", celebrator.age)
}
let mini = Person(name: "Mini", age: 25)
celebrateBirthday(to: mini)
/*
HBD! Mini
Happy 25
*/
Person
이 Named
, Aged
중 둘 중 하나라도 채택하지 않는다면, celebrator에 인수로 들어갈 수 없다.
옵셔널 요구사항을 정의할 수 있는데, 이것으로 Objectice-C와 상호운용되는 코드 작성 가능할 수 있게 한다.
따라서 옵셔널 요구사항은 class에서만 사용이 가능.
옵셔널 요구사항은 반드시 구현될 필요는 없다.
optional
수식어 앞에 붙여야 하고,프로토콜과 옵셔널 요구 사항 앞에 @objc
로 선언되어야 함.
@objc protocol Living {
func breath()
@objc optional func eat()
}
옵셔널 요구사항에서 메서드나 프로퍼티의 타입은 자동으로 옵셔널이 된다.
프로토콜을 준수하는 타입이 요구사항을 구현하지 않았을 경우에 대비해, 옵셔널 체이닝으로 호출될 수 있음.
class June: Living {
func breath() {
print("숨 쉬는 중...")
}
func eat() {
print("먹는 중...")
}
}
class Min: Living {
func breath() {
print("숨 쉬는 중...")
}
}
let j: Living = June()
j.breath() // 숨 쉬는 중...
j.eat?() // 먹는 중...
let m: Living = Min()
m.breath() // 숨 쉬는 중...
m.eat?() // 아무것도 출력되지 않음.
eat()
은 호출 시에 옵셔널 체이닝으로 호출된다. → eat?()
클래스 생성 시에 타입을 클래스 타입 자체로 선언하게 되면,
let june = June()
june.breath() // 숨 쉬는 중...
june.eat() // 먹는 중...
let min = Min()
min.breath()
min.eat?() // 🚨
min.eat() // 🚨
메서드 자체가 존재하지 않으니 eat?()
이든 eat()
이든 호출 불가능.
프로토콜을 준수하는 타입에게 메서드, 초기화 구문, 서브스크립트, 연산 프로퍼티(저장 프로퍼티는 불가)의 구현을 확장해서 프로토콜 자체에 동작을 정의하여 제공할 수 있음.
protocol RandomNumberGenerator {
func random() -> Double
}
extension RandomNumberGenerator {
func hi() {
print("hi")
}
func randomBool() -> Bool {
return random() > 0.5
}
}
class SomeClass: RandomNumberGenerator {
func random() -> Double {
return 0.3
}
}
let sc = SomeClass()
sc.random() // 0.3
sc.randomBool() // false
sc.hi() // hi
extension에서 프로토콜의 기본 구현을 제공할 수도 있는데, (🙀!!!!!)
protocol RandomNumberGenerator {
func random() -> Double
}
class SomeClass: RandomNumberGenerator {
func random() -> Double {
return 0.3
}
}
프로토콜을 채택한 타입은 반드시 프로토콜의 요구사항을 구현해야 하는 것은 이제 당연하고,
protocol RandomNumberGenerator {
func random() -> Double
}
extension RandomNumberGenerator {
func random() -> Double {
return 0.5
}
}
class SomeClass: RandomNumberGenerator {
}
let sc = SomeClass()
sc.random() // 0.5
이렇게 extension에 기본 구현을 제공해 놓을 수도 있다.
이때는 따로 구현을 하지 않으면 기본 구현이 제공된다.
(🙀!!!!!!!! 공부하면서 처음 알았다…)
protocol RandomNumberGenerator {
func random() -> Double
}
extension RandomNumberGenerator {
func random() -> Double {
return 0.5
}
}
class SomeClass: RandomNumberGenerator {
func random() -> Double {
return 0.8
}
}
let sc = SomeClass()
sc.random() // 0.8
구현을 한 경우에는 물론 구현된 메서드가 작동된다!