
사실 Swift는 객체지향(Object Oriented Programming)이 아니라 세계 최초의 프로토콜지향(Protocol Oriented Programming)이 되었다는 것을 알고있는가? 이렇게 중요해진 개념인 Protocol을 그냥 훑고 지나가면 안되겠지? 제대로 알아보자.

레시피, 약속 등 많이 생각해봤는데, 가장 와닿는건 클래식하게 설계도(Blueprint)가 가장 어울리는 단어인 것 같다. 설계도에 우주선의 실제 구현이 들어있지 않고 ‘어떤 것들이 필요하다’라고 적혀있는 것처럼 Protocol도 마찬가지로 ‘필요한 것들’만 적혀져있다.
프로토콜을 채택할 수 있는 타입은 Class, Struct, Enum 3가지가 있고, 프로토콜 또한 프로퍼티와 메소드를 가질 수 있다.
Property부터 알아보자. 프로퍼티는 타입 말고도, 어떤 역할을 하는 프로퍼티인지 get, set 키워드(Property Requirement)를 통해서 미리 명시를 해주어야 한다. 이 때, 명시하는 역할은 ‘필수 조건’ 이다. 쉽게 말하면, get으로 명시했을 때, 실제로는 get, set의 역할을 해도 되지만, 반대로 get, set으로 명시했는데, 실제로는 get역할만 하는 것은 안된다.
또한, 저장 프로퍼티, 연산 프로퍼티 상관없이 구현이 가능하다.
protocol Rocket {
var engine: String { get }
}
// Stored Property
class RocketA: Rocket {
let engine: String = "Super Engine"
}
// Computed Property
class RocketB: Rocket {
var engine: String {
return "Super Engine"
}
}
그런데 왜 Protocol Property는 항상 var로 선언해야 할까? let은 안될까??
잘 생각해보면 앞서 서술한 ‘필수조건’과 관련이 있다. var이 ‘필수조건’이기 때문에 모두 var로 선언할 수 밖에 없는 것이다. 정말 그런지 한 번 살펴보자.
저장 프로퍼티의 경우 get이 필수조건이므로 let, var 둘 다 가능하다.
연산 프로퍼티의 경우에도 get이 필수조건이므로 get-only, getter&setter 둘 다 가능하다.(근데 연산프로퍼티는 뭘 하던 var)
class RocketA: Rocket {
let engine: String = "Super Engine"
}
class RocketB: Rocket {
var engine: String = "Super Engine"
}
저장 프로퍼티의 경우 get set이 필수조건이므로 var만 가능하다. (set을 통해 값이 바뀌어야 하므로)
연산 프로퍼티의 경우 get set이 필수조건이므로 getter&setter만 가능하다. (어짜피 var)
class RocketA: Rocket {
// let이면 Error
var engine: String = "Super Engine"
}
그러므로 어떠한 경우에서도 let만 되는 경우가 없으므로, Protocol Property는 항상 var가 되는 것이 맞다.
Optional
Protocol은 항상 선언되어있는 모든 프로퍼티와 메소드를 구현해야하는데.. Protocol을 이곳 저곳에서 재사용하다 보면, 여기서는 이 프로퍼티는 굳이 필요가 없는데.. 라고 생각하는 경우가 있다.
이런 경우를 위해서, 프로퍼티나 메소드를 optional로 선언할 수도 있다!
@objc protocol Rocket: AnyObject {
var engine: String { get set }
@objc optional var wing: String { get set }
}
프로퍼티나 메소드를 optional로 선언하려면 프로토콜 자체와 optional로 선언하려는 부분 둘 다 @objc 선언을 해 주어야 한다.
그런데 이 때, @objc Attribute를 protocol에 붙히게 되면 Class에서만 채택이 가능하다. (AnyObject를 붙힌 것과 동일하게 작동한다.) 왜지???
Objective - C의 언어적 특성
Objective - C의 객체지향 모델은 클래스와 인스턴스 중심이기 때문에. 모든 객체는 클래스로부터 생성되며, 클래스는 특정 Method와 Property를 정의한다. C언어에서의 구조체는 데이터만을 담는 단순한 데이터 구조이기 때문에 Protocol을 채택하기 다소 어렵다.
let rocket: Rocket = ARocket.init()
rocket.wing // String? Type
뒤에서 나올 내용이긴 하지만, Protocol as Type을 통해 Optional Property(Method)에 접근할 경우, 있을지 없을지 알 수 없으므로 타입 또한 Optional이 된다.
Method또한 Property와 별 다를 점은 하나도 없다. 단지, 구조체에서 쓸 Protocol인데 Mutating Method가 필요하다?! 이러면 Protocol 선언부에서부터 Mutating 키워드를 붙혀줘야 한다.
어 근데 아까 프로토콜은 ‘필수 조건’만 선언하면 된다고 하지 않았나요? 그냥 func으로 다 하면 안되나?
mutating func이 func에 포함되어있다고 생각하기 쉬운데, 둘은 포함 관계가 아니라 조금 다른 같은 레벨의 개념이다. 만약 속해있다면 func을 사용하는 모든 부분에서 mutating func을 사용할 수 있어야 하는데 그게 되지 않는다. → Class에서는 mutating func을 사용할 수 없다.
이해가 잘 안 될 수도 있을 것 같아 예시를 하나 들자면 이런 느낌이다.
struct Rocket {
mutating func changeSelf() {
self = Rocket()
}
}
class Rocket {
func changeSelf() {
self = Rocket() //Error: self is immutable
}
}
Class에선 왜 안될까? 값 타입과 참조타입의 문제인데, 근본적으로 self를 통해 자기 자신을 바꾸려고 하면, struct나 enum에서는 mutating 키워드를 통해 가능하지만, class에서는 어떤 짓을 해도 자기 자신을 바꾸는 것이 되지 않는다. 이렇게 설계된 이유에는 다음과 같은 것들이 있다고 추론해볼 수 있다.
다시 돌아와서, 이러한 이유들로 Struct에서 mutating func을 사용할 때는 선언에서부터 mutating키워드를 꼭 붙혀주어야 하고, 이 메소드가 Class에서 쓰일 경우에는 Class에서 mutating func을 사용할 수 없으므로 mutating을 제거하고 사용해주면 된다. (Struct의 경우 mutating이 붙지 않을 경우, 아예 다른 Struct 고유 지정 메소드로 인식하기 때문에 에러가 뜬다.)
protocol Rocket {
mutating func changeEngine(newEngine: String)
}
// Struct
struct RocketA: Rocket {
var engine: String = "Super Engine"
mutating func changeEngine(newEngine: String) {
self.engine = newEngine
}
}
// Class
class RocketB: Rocket {
var engine: String = "Super Engine"
func changeEngine(newEngine: String) {
self.engine = newEngine
}
}
Protocol은 1급 객체(First-Class Citizen)이다. ….. 그게뭔데…
1급 객체도 이름만 어렵지 뭔지 알고 나면 정말 별 것 아니다. 한 마디로 요약하면, 그냥 변수나 상수처럼 사용할 수 있어야 한다는 뜻이다. 전달 인자(매개 변수), 반환값, 변수에 사용만 할 수 있으면 된다. Swift는 함수형 언어 이므로 1급 객체는 당연히 함수, 클로저(이름 없는 함수) 그리고 마지막으로 바로 프로토콜이 1급 객체에 포함된다.
1급객체라는 개념이 Swift에 있는 이유는 Swift가 함수형 프로그래밍 언어이기 때문이다. 함수를 호출, 전달, 반환하는 과정만으로도 프로그램 구현이 가능한 것을 목표로 하기 때문에 1급 객체라는 개념이 중요하고, 그 중 함수가 포함되어있는 것이다. 가장 익숙한 함수부터 한 번 확인해보자
func makeRocket(_ rocket: String) { }
func makeRocket() { }
let rocketA = makeRocket // () -> Void
let rocketB = makeRocket(_:) // (String) -> Void
! 여기서 변수에 함수를 넣는 것과 ‘함수의 반환값’을 넣는 것과 많이 착각을 한다. 잘 보면 return 값을 받는 것이 아니라 함수 실행문이 없고, 타입을 찍어보면 ‘함수 그 자체’를 변수에 담았다는 것을 확인할 수 있다.
어?? 이거 어디서 많이 본 형식인데?
button.addTarget(target: self, action: #selector(buttonClicked))
button.addTarget(target: self, action: #selector(buttonClicked(_:)))
@objc func buttonClicked() {
print(#function)
}
우리가 자주 썼던 addTarget의 #selector를 지정해줄 때 함수를 명시해주던 그 코드에서 썼던 형식이구나!
항상 쓸 때마다 (_:)이 코드랑 그냥 함수 이름만 써주는거랑 차이가 궁금했는데! 함수 Overloading을 할 경우에 매개변수를 통해서 구별해줬던 거구나
(그렇다면 바로 프로토콜의 경우를 살펴보자!)
엥… Protocol은 구현도 안되어있고, 생성자 init()도 없는 BluePrint일 뿐인데 어떻게 상수/변수에 넣죠?
그래서 이럴 때에 아까 잠깐 본 Protocol as Type을 활용할 수 있는 것이다. Protocol을 채택하는 Class, Struct등의 인스턴스를 Protocol Type으로 타입캐스팅 해버려서 Protocol 자체가 하나의 Type으로 취급될 수 있는 것이다.
let rocket: Rocket = RocketA.init()
// 아래와 동작방식이 같다.
let rocket: Rocket = RocketA.init() as Rocket
아하! 이것도 1번과 마찬가지로 Protocol as Type을 통해서 프로토콜 타입으로 Struct, Class, Enum 인스턴스를 전달한다는 거구나.
func makeNewRocket(rocket: Rocket) -> Rocket {
return rocket
}
let rocket = RocketA.init()
makeNewRocket(rocket: rocket)
이 때, 주의해야할 것은 Protocol Type으로 캐스팅된 인스턴스는 인스턴스 내에서 직접 구현한 프로퍼티, 메소드에는 접근을 할 수 없다. 무조건 Protocol에서 선언한 프로퍼티, 메소드에만 접근이 가능하다.
Protocol은 직접 구현할 수 있는 내용은 없이.. 시키는 일만 많은데.. 이러면은 Class를 이용한 상속보다 좋은 점이 뭐지ㅠ 라고 생각할 수 있다. 이 때 사용할 수 있는 것이 Protocol Extension!
Extension을 사용하면 Protocol 자체에서도 기능을 구현할 수 있다. 이렇게 되면 항상 반복되는 작업의 경우도 자체적으로 구현이 가능하고, 또, 예외의 경우에는 따로 구현 또한 당연히 가능하다.
protocol Rocket {
func fly()
}
extension Rocket {
func fly() {
print("Rocket Fly 3..2..1..")
}
}
이렇게 Extension을 통해서 메소드 구현을 미리 해버리게 되면, 채택한 곳에서 직접 구현을 하지 않더라도 바로 메소드를 사용할 수 있다.
struct RocketA: Rocket { }
struct RocketB: Rocket {
func fly() {
print("Rocket didn't fly...")
}
}
RocketA.fly() // "Rocket Fly 3..2..1.."
RocketB.fly() // "Rocket didn't fly..."
이렇게 될 경우에는 extension에 구현된 기본 제공된 fly()보다, 직접 구현한 fly()의 우선순위가 높다.
protocol Weapon { }
protocol Rocket {
func fly()
}
extension Rocket where Self: Weapon {
func fly() {
print("Rocket Fly 3..2..1..")
}
}
struct RocketA: Rocket { }
RocketA().fly() // Impossible Type 'RocketA' does not conform to protocol 'Rocket'
struct RocketB: Weapon, Rocket { }
RocketB().fly() // Possible
또한, where을 통해서 제한된 경우에만 기본 메소드를 제공할 수도 있다. 오잉?? self가 아니라 Self?
self vs Self
self: 일단 self는 앞에서도 살짝 알아봤듯 ‘모든 인스턴스들이 암시적으로 생성하는 자기 자신을 가리키는 프로퍼티’ 이다. init()에서 자주 사용했듯이 같은 이름으로 선언된 변수들이 있으면 좀 더 명확히 명시해주기도 하고, 값 타입 인스턴스 자체를 가리키기도 한다.(참조 타입인 Class는 X)
Self: 타입 프로퍼티와 인스턴스 프로퍼티의 차이이다. self는 타입의 인스턴스를 가리키고, Self는 타입 그 자체를 가리킨다. 아래 예시를 보면 이해가 빠를 것 같다.
class Rocket {
func test() -> Self {
return self
}
}
Int, String등의 타입과 같이 Self는 예시에서 Rocket이라는 class 그 자체를 가리키고 있다. 그리고 리턴값은 항상 Type을 return하는 것이 아닌 그 Type인 인스턴스를 반환하는게 맞으므로 self를 써 주는 것이 바람직하다.
그래서 실제로 타입 프로퍼티를 사용할 때, Self를 사용해서 프로퍼티를 받아줄 수도 있다. (신기함 주의)
class Rocket {
static let engine = "Super Engine"
//1
lazy var engineCopy = Self.engine
//2
var engineCopy: String {
return Self.engine
}
}
//2
Rocket.engine
//3
Rocket.self.engine
1, 2, 3, 4번 모두 똑같이 타입 프로퍼티인 engine을 출력한다.
1번처럼 class내부에서 Self를 통해 타입 프로퍼티를 가져올 수도 있고,(lazy 안붙히면 당연히 안된다 왜?? self와 마찬가지로 프로퍼티 초기화 시점이 class(Self)를 알기 전이므로!)
아니면 2번처럼 lazy붙히기 싫으면 연산프로퍼티로 가져올 수도 있고,
혹은 3번처럼 가장 많이 쓰는 방법으로 가져올 수도 있다.
이건 처음알았는데 사실 3번처럼 사용하면 self 인스턴스가 중간에 숨어져 있다고 한다.(생각해보면 당연한가? 예전에 static 타입 프로퍼티 공부할 때 동작원리가 사실 처음 호출 시점에 instance를 생성한다고 했으므로 그 의미로 봐줘도 괜찮지 않을까 싶다.)
class ViewController {
static func layout() -> UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
return layout
}
//1
var collectionView = UICollectionView(frame: .zero, collectionViewLayout: ViewController.layout())
//2
var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout())
//3
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: Self.layout())
//4
var collectionView = UICollectionView(frame: .zero, collectionViewLayout: ViewController.self.layout())
}
실전에서의 예시는 이런 것들이 있지 않을까 싶다. 1번처럼 명시적으로 Class의 이름을 붙혀서 타입 메소드를 불러줄 수 있다. 또, 2번처럼 같은 스코프 안이니까 Class이름을 생략해줄 수도 있다. 또, 자기 자신의 Class 안이므로 3번처럼 Self를 통해 받아주는 것도 당연히 가능하다. (lazy는 붙혀줘야 한다.) 또, 위에서 봤던 것처럼 4번 형식으로도 가능하다~
헷갈리긴 하지만 이해만 충분히 된다면 모든 self, Self관련 코드를 볼 때 이해도가 많이 올라가지 않을까 싶다 :)
protocol Rocket {
var engine: String { get }
}
extension Rocket {
var engine: String {
return "Super Engine"
}
}
그래서 돌아와서 Extension에서의 구현은 메소드만 가능할까? 프로퍼티도 가능하지 않을까? (일단 Stored Property는 안된다: Extension이기 때문에)
가능하다! 연산 프로퍼티의 경우에는 메소드와 똑같이 extension에서 기본 제공이 가능하다.
Protocol에서 사용하는 Generic이다. Protocol에서는 아래처럼 제네릭을 사용할 수 없다. 에러 메시지 또한 associated type을 정의하라고 나온다.
// Error! An associated type named 'T' must be declared in the protocol 'Rocket' or a protocol it inherits
protocol Rocket<T> {
func makeRocket(rocket: T)
func sendRocket() -> T
}
사용법은 더 간단하다. 그냥 associated type옆에 T같이 사용할 범용 타입을 명시해주면 된다. (보통 associated type에서는 T대신 value를 많이 사용한다.) Generic처럼 Equatable같은 제약조건을 주는 것 또한 가능하다.
protocol Rocket {
associatedtype value: Equatable
func makeRocket(rocket: value)
func sendRocket() -> value
}
사용법은 Protocol을 채택하는 곳에서 typealias를 이용해서 value의 타입을 명시해주면 된다. 혹은, 충분히 추론이 가능한 경우에는 typealias를 생략하는 것도 가능하다.
struct RocketA: Rocket {
typealias value = Int
func makeRocket(rocket: value)
func sendRocket() -> value
}
struct RocketB: Rocket {
func makeRocket(rocket: Int)
func sendRocket() -> Int
}
나는 그래도 기존의 Generic 방식을 써야겠다! 하면 ‘메소드’에 Generic을 걸어주면 되긴 한다. (근데 프로퍼티는 불가능하다. → Protocol자체에 Generic을 못거니까 를 정의해 줄 곳이 없다.)
protocol Rocket {
func makeRocket<T: Equatable>(rocket: T)
}
Protocol 선언부의 Method에서는 Default Value를 가질 수 없다.
protocol Rocket {
// Error! Default argument not permitted in a protocol method
func makeRocket(rocket: String = "Big and Expensive Rocket")
}
근데 Default Value를 지정해주고 싶을 경우에는 extension에서 구현해주면 된다.
extension Rocket {
func makeRocket(rocket: String = "Big and Expensive Rocket") {
print(rocket)
}
}
struct RocketA: Rocket { }
RocketA.makeRocket()
하지만 Protocol이 앞서 배운 associatedtype을 사용하고, Default Value를 주고 싶은 Method또한 그렇다면, 기본값을 할당할 수 없다.
protocol Rocket {
associatedtype value
func makeRocket(rocket: value)
}
extension Rocket {
// Generic parameter 'Self' could not be inferred
func makeRocket(rocket: value = 1)
}
어? 아까 배운 Self 나왔다! 여기서 Self는 그러면 value라는 ‘타입 자체’를 말하는 거겠구나. 쨋든 근데 이런 상황에서 기본값을 할당할 수 없는 것은 어찌보면 당연하다. 범용타입을 사용하려하는데 기본값을 Int로 줘버리면.. 이건 뭐… (하지만 언제나 방법은 있다!)
//1
protocol Rocket {
associatedtype value: BinaryInteger
func makeRocket(rocket: value)
}
//2
protocol Rocket {
associatedtype value
func makeRocket(rocket: value)
}
extension Rocket where value: BinaryInteger {
func makeRocket(rocket: value = 1)
}
//3
protocol Rocket {
func makeRocket<T>(rocket: T)
}
extension Rocket {
func makeRocket<T>(rocket: T = 1) { }
}
첫번째로, associatedtype자체를 BinaryInteger로 제한하거나, (근데 이렇게 되면 범용타입을 사용하는 이유가 없어 ㅠㅠ)
두 번째로, extension에서 where을 통해서 제한된 상황에서만 기본값을 할당할 수도 있다.
세 번째로, 아까 배운대로 단일 메소드에서만 제네릭을 사용하는 경우에는 사용하던 대로 그냥 기본값을 할당하면 된다.
하나하나 짚고 넘어가니까 이해는 잘 되는데.. Protocol에 대한 개념들만 좀 나열한 느낌이라서.. POP 등에서의 활용방법, 예시 같은 것들을 못 쓴 느낌이다. 바로 이어서 작성하면서 보완을 조금 하면 좋을 것 같다!
참조
[iOS/Swift - POP] 프로토콜 지향 프로그래밍(Protocol Oriented Programing) 알아보기 - 1
Swift) Protocol 이해하기 (1/6) - Protocol이 도대체 뭔가요?
프로토콜을 이해하는데 필요한 문법들이 한 번에 정리되어있어서 좋네요! 👍👍