오늘은 protocol(프로토콜)
에 대해서 알아보자.
protocol
은 Swift로 코드를 짜다보면 정말 많이 마주치고, 사용하게 된다.
이전에는 프로토콜은 이런거다! 보다는 이럴때 사용하나..?라는 느낌으로 사용했다.
더이상은 이런 애매모호한 이유가 아니라 명확한 이유를 가지고 사용하고 싶어서 정리해본다.
protocol
은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진
네트워크를 공부하다보면 프로토콜이라는 단어를 정말 많이 보게된다.
TDP/IP Protocol도 있고, 우리가 흔히 말하는 HTTP도 Hyper Text Transfer Protocol의 약자이다.
네트워크에서는 보통 프로토콜을 하나의 약속, 규약이라고 표현한다.
위에서 protocol
은 "특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진"라고 했다.
그럼 이렇게 볼 수 있지 않을까?
protocol
은 특정 역할을 하기 위한 청사진이다.
protocol
은 청사진을 준수해 나가라는 약속 혹은 규약이다.
struct
, class
, enum
은 프로토콜을 채택할 수 있다.
프로토콜의 요구사항을 모두 따르는 타입은 프로토콜을 준수한다고 얘기한다.
즉, 프로토콜이 제시한 청사진의 기능을 모두 구현했을때 프로토콜을 준수한다고 얘기할 수 있다.
프로토콜에 대해서 알아봤으니 어떻게 사용하는지 알아보자.
protocol 프로토콜 이름 {
프로토콜 정의
}
protocol
키워드로 프로토콜을 정의할 수 있다.
struct exStruct: walkProtocol, runProtocol {
}
class exClass: walkProtocol, runProtocol {
}
enum exEnum: walkProtocol, runProtocol {
}
:
을 통해 프로토콜을 채택할 수 있다.
,
으로 다수의 프로토콜을 채택할 수 있다.
프로토콜에 프로퍼티, func 등을 어떻게 정의하는지 알아보자.
프로퍼티의 종류는 신경 쓰지 않지만 읽기 전용인지 읽기 쓰기가 모두 가능한지는 정해줘야한다.
프로토콜을 채택한 타입은 프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞도록 구현해주면 된다.
enum Direction {
case west
case north
}
protocol walkProtocol {
var velocity: Int { get set }
var direction: Direction { get }
}
protocol runProtocol {
static var velocity: Int { get set }
static var direction: Direction { get }
}
또한, 프로토콜의 프로퍼티는 항상 var
키워드를 사용해서 정의한다.
읽기 쓰기 모두 가능하다면 { get set }
, 읽기 전용이라면 { get }
을 써준다.
runProtocol
처럼 static
키워드를 사용해 타입 프로퍼티를 정의할수도 있다.
enum Direction {
case west
case north
}
protocol walkProtocol {
var velocity: Int { get set }
var direction: Direction { get }
}
class Wonhee: walkProtocol {
var wonheeDirection: Direction = .west
var wonheeVelocity = 10
var velocity: Int {
get {
return self.wonheeVelocity
}
set {
wonheeVelocity += 10
}
}
var direction: Direction {
return wonheeDirection
}
}
class SomeOne: walkProtocol {
var someOneDirection: Direction = .west
var someOneVelocity = 10
var velocity: Int {
get {
return self.someOneVelocity
}
set {
someOneVelocity += 10
}
}
var direction: Direction {
get {
return someOneDirection
}
set {
someOneDirection = .north
}
}
}
walkProtocol
을 채택하는 Wonhee
, SomeOne
class를 정의했다.
walkProtocol
에서 velocity
는 읽기, 쓰기가 모두 가능하고 direction
은 읽기 전용 프로퍼티이다.
Wonhee
에서는 velocity
는 읽기, 쓰기가 모두 가능하고, direction
은 읽기 전용으로 프로토콜을 준수하고 있다.
SomeOne
에서는 velocity
와 direction
모두 읽기, 쓰기가 모두 가능하다.
프로토콜에서 direction
은 읽기 전용으로 정의되어 있는데 SomeOne
의 direction
은 읽기 쓰기인데도 오류가 안 나오지?!
읽기 쓰기가 가능한 프로퍼티지만 프로토콜에 정의해둔 읽기 기능은 할 수 있기 때문이다.
즉, 프로토콜에서 지정해둔 최소의 사양은 준수하고 있기 때문에 해당 class는 프로토콜을 준수한다고 볼 수 있다.
메서드의 실제 구현부를 제외하고 정의한다.
타입 메서드를 정의할때는 static
키워드를 사용한다.
프로토콜에 static
키워드로 정의된 메서드는 프로토콜을 채택한 타입 즉, 메서드를 실제 구현할 때에는 static
, class
키워드 모두를 사용할 수 있다.
enum Direction {
case west
case north
}
protocol walkProtocol {
var velocity: Int { get set }
var direction: Direction { get }
}
protocol runable {
func run(velocity: Int, direction: Direction)
}
class Wonhee: walkProtocol, runable {
var wonheeDirection: Direction = .west
var wonheeVelocity = 10
var velocity: Int {
get {
return self.wonheeVelocity
}
set {
wonheeVelocity += 10
}
}
var direction: Direction {
return wonheeDirection
}
func run(velocity: Int, direction: Direction) {
print("velocity: \(velocity), direction: \(direction)")
}
}
value Type
의 인스턴스 메서드에서 자신 내부의 값을 변경하고자 할때 func
앞에 mutating
을 붙여 인스턴스 내부의 값을 변경한다는 것을 확실히 해줘야 한다.
프로토콜이 어떤 타입이든 간에 인스턴스 내부의 값을 변경해야 하는 메서드를 정의하려면 mutating
키워드를 명시해줘야 한다.
reference Type
의 메서드 앞에는 mutating
키워드를 명시하지 않아도 되지만 value Type
은 반드시 필요하다.
프로토콜에 mutating
키워드를 사용한 메서드여도 reference Type
구현시에는 mutating
키워드를 명시하지 않아도 된다.
메서드와 마찬가지로 이니셜라이저를 정의하지만 구현은 하지 않는다.
protocol Person {
var name: String { get }
init(name: String)
}
struct Wonhee: Person {
var name: String
init(name: String) {
self.name = name
}
}
struct
는 상속할 수 없기 때문에 이니셜라이저 요구에 크게 신경 쓸 것이 없다.
required
키워드가 이니셜라이저에 붙으면 해당 클래스의 모든 클래스는 이니셜라이저를 구현해야 한다.
required
키워드를 이니셜라이저에 붙이면 해당 클래스를 상속한 서브클래스에서도 해당 이니셜라이저를 필수로 구현해야 한다.
protocol Animal {
var age: Int { get }
init(age: Int)
}
class Bird: Animal {
var age: Int
required init(age: Int) {
self.age = age
}
}
Bird
클래스를 상속받는 모든 클래스는 Animal
프로토콜을 준수해야 하며, 상속받는 클래스에 해당 이니셜라이저를 모두 구현해야한다는 뜻이다.
만약 클래스 자체가 상속받을 수 없는 final 클래스라면 required
키워드를 붙여줄 필요가 없다.
protocol Animal {
var age: Int { get }
init(age: Int)
}
class Bird {
var age: Int
init(age: Int) {
self.age = age
}
}
class Crow: Bird, Animal {
required override init(age: Int) {
super.init(age: age)
}
}
Bird
class에서 Animal
프로토콜에서 요구하는 이니셜라이저가 이미 구현되어 있다.
Crow
는 Bird
를 상속하고 Animal
을 채택하고 있다.
이럴때는 해당 이니셜라이저에 required
와 override
키워드를 모두 써주고, 프로토콜에서 요구하는 이니셜라이저를 구현해준다.
프로토콜은 하나 이상의 프로토콜을 상속 받을 수 있다.
protocol Animal {
func sleep()
}
protocol Person {
func eat()
}
protocol Walkable: Animal, Person {
func walk()
}
class Wonhee: Walkable {
func sleep() {
print("sleep")
}
func eat() {
print("eat")
}
func walk() {
print("walk")
}
}
프로토콜의 상속 리스트에 class
키워드를 추가해 프로토콜이 class
타입에서만 채택될 수 있도록 제한할 수 있다.
이때 class
키워드는 상속 리스트 맨 처음에 위치해야한다.
protocol Walkable: class, Animal, Person {
func walk()
}
class Wonhee: Walkable {
func sleep() {
print("sleep")
}
func eat() {
print("eat")
}
func walk() {
print("walk")
}
}
struct errorHuman: Walkable {
}
-is
연산자로 해당 인스턴스가 특정 프로토콜을 준수하는지 확인할 수 있다.
as?
연산자로 다른 프로토콜로 다운캐스팅 해볼 수 있다.as!
연산자로 다른 프로토콜로 강제 다운캐스팅 할 수 있다.프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있다.
선택적 요구사항을 정의할 프로토콜은 objc
속성이 부여된 프로토콜여야 한다.
objc
속성은 프로토콜을 Objective-C
코드에서 사용할수 있도록 만드는 역할이다.
선택적 요구사항을 정의하기 위해서는 해당 프로토콜을 Objective-C
와 공유하고 싶지 않아도 objc
속성을 추가해야한다.
또한, objc
속성이 부여된 프로토콜은 Objective-C
클래스를 상속받은 클래스에서만 채택할 수 있다.
(enum
혹은 struct
에서는 objc
속성이 부여된 프로토콜을 채택할 수 없다.)
선택적 요구사항은 프로토콜을 준수할 때 해당 요구사항은 필수로 구현할 필요가 없어진다.
optional
키워드를 요구사항 정의 앞에 붙여주면 선택적 요구사항이다.
메서드나 프로퍼티를 선택적 요구사항으로 정의한다면 요구사항의 타입은 자동으로 optional이 된다.
(Int) -> String 타입의 메서드는 ((Int) -> String)? 타입이 된다.
메서드의 매개변수나 반환 타입이 옵셔널이 되는 것이 아니라 메서드 자체의 타입이 옵셔널이 된다.
선택적 요구사항은 프로토콜을 준수하는 타입에 구현되어 있지 않을 수도 있어서 옵셔널 체이닝을 통해 호출할 수 있다.
@objc protocol Movable {
func walk()
@objc optional func run()
}
class Duck: NSObject, Movable {
func walk() {
print("오리는 걸을 수 있다.")
}
}
class Dog: NSObject, Movable {
func walk() {
print("강아지는 걸을 수 있다.")
}
func run() {
print("강아지는 뛰는걸 좋아한다.")
}
}
let duck = Duck()
let dog = Dog()
duck.walk()
dog.walk()
dog.run()
위에서 살펴본 바에 따르면 프로토콜은 요구만 하고 스스로 기능을 구현하지 않는다.
프로토콜은 코드에서 하나의 타입으로 사용되기에 여러 위치에서 프로토콜을 타입으로 사용할 수 있다.
함수, 메서드, 이니셜라이저에서 매개변수 타입이나 반환 타입으로 사용될 수 있다.
프로퍼티, 변수, 상수 등의타입으로 사용될 수 있다.
배열, 딕셔너리 등 컨테이너 요소의 타입으로 사용될 수 있다.
오늘은 프로토콜에 대해 알아봤다.
delegate 패턴을 알아보기 전에 프로토콜에 대한 이해가 필요할거 같아서 알아봤는데 재밌었다.
이것 저것 정리하다보니 프로토콜을 사용한 여러 아이디어가 떠올랐다.
그 부분도 조만간 정리해둬야겠다.
그럼 이만👋