Protocol

이원희·2020년 12월 29일
1

 🐧 Swift

목록 보기
14/32
post-thumbnail

오늘은 protocol(프로토콜)에 대해서 알아보자.
protocol은 Swift로 코드를 짜다보면 정말 많이 마주치고, 사용하게 된다.
이전에는 프로토콜은 이런거다! 보다는 이럴때 사용하나..?라는 느낌으로 사용했다.
더이상은 이런 애매모호한 이유가 아니라 명확한 이유를 가지고 사용하고 싶어서 정리해본다.

Protocol이 뭐야?!

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 등을 어떻게 정의하는지 알아보자.

Property

프로퍼티의 종류는 신경 쓰지 않지만 읽기 전용인지 읽기 쓰기가 모두 가능한지는 정해줘야한다.
프로토콜을 채택한 타입은 프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞도록 구현해주면 된다.

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에서는 velocitydirection 모두 읽기, 쓰기가 모두 가능하다.
프로토콜에서 direction은 읽기 전용으로 정의되어 있는데 SomeOnedirection은 읽기 쓰기인데도 오류가 안 나오지?!
읽기 쓰기가 가능한 프로퍼티지만 프로토콜에 정의해둔 읽기 기능은 할 수 있기 때문이다.
즉, 프로토콜에서 지정해둔 최소의 사양은 준수하고 있기 때문에 해당 class는 프로토콜을 준수한다고 볼 수 있다.


Method

메서드의 실제 구현부를 제외하고 정의한다.
타입 메서드를 정의할때는 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)")
    }
}

가변 Method

value Type인스턴스 메서드에서 자신 내부의 값을 변경하고자 할때 func 앞에 mutating을 붙여 인스턴스 내부의 값을 변경한다는 것을 확실히 해줘야 한다.

프로토콜이 어떤 타입이든 간에 인스턴스 내부의 값을 변경해야 하는 메서드를 정의하려면 mutating 키워드를 명시해줘야 한다.
reference Type의 메서드 앞에는 mutating 키워드를 명시하지 않아도 되지만 value Type은 반드시 필요하다.

프로토콜에 mutating 키워드를 사용한 메서드여도 reference Type 구현시에는 mutating 키워드를 명시하지 않아도 된다.


initializer

메서드와 마찬가지로 이니셜라이저를 정의하지만 구현은 하지 않는다.

struct

protocol Person {
    var name: String { get }
    init(name: String)
}

struct Wonhee: Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}

struct는 상속할 수 없기 때문에 이니셜라이저 요구에 크게 신경 쓸 것이 없다.

class

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 프로토콜에서 요구하는 이니셜라이저가 이미 구현되어 있다.
CrowBird를 상속하고 Animal을 채택하고 있다.
이럴때는 해당 이니셜라이저에 requiredoverride 키워드를 모두 써주고, 프로토콜에서 요구하는 이니셜라이저를 구현해준다.


추가

프로토콜과 상속

프로토콜은 하나 이상의 프로토콜을 상속 받을 수 있다.

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! 연산자로 다른 프로토콜로 강제 다운캐스팅 할 수 있다.

Optional Protocol

프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있다.
선택적 요구사항을 정의할 프로토콜은 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 패턴을 알아보기 전에 프로토콜에 대한 이해가 필요할거 같아서 알아봤는데 재밌었다.
이것 저것 정리하다보니 프로토콜을 사용한 여러 아이디어가 떠올랐다.
그 부분도 조만간 정리해둬야겠다.
그럼 이만👋

0개의 댓글