Swift + iOS/Swift

[Swift] Protocol [02]

군옥수수수 2017. 10. 2. 00:11

Protocol Basic To Advanced


저번 포스팅에서는 프로토콜의 기본적인 개념과 문법들을 살펴보았습니다. 이번 포스팅에서는 스위프트에서 프로토콜에 대한 심화된 내용과 이를 사용하는 이유, 더 나아가 POP (Protocol Oriented Programming)에 대해서도 살펴보는 시간을 갖도록 하겠습니다.


글을 읽기전에 먼저 숙지하고 계셔야할 주제들입니다.


  1. protocol - basic
  2. extension (추후 업데이트 예정)


만일 당신이 레이싱 게임을 개발한다고 상상해보세요. 당신은 자동차를 운전할 수도 있고 오토바이를 운전할 수도 있으며 심지어는 비행기도 조종할 수 있습니다.


객체지향적인 설계로 이러한 종류의 어플리케이션을 만들게 된다면 공통적인 기능들을 빼서 클래스를 만들고 이를 상속하는 클래스들을 작성하는 방법으로 설계를 하실 것입니다. 아마 Vehicle이라는 클래스를 만들어 각기 상속하는 방법을 사용하시겠죠.

class Vehicle {
    func needToChangeWheels(){
        /*
            code
        */
    }
    
    func needToFillFuel(){
        /*
            code
        */
    }
}

class Airplane: Vehicle {
    func fly(){
        /*
            code
        */
    }
}

얼핏보면 납득이 되실지 모르지만 이러한 객체지향적인 접근법은 부작용도 따르게 됩니다.

만일 당신이 위와 같이 공통적인 기능들(메소드)들을 묶은 클래스의 특정 기능을 필요로 하는 전혀 다른 요소를 추가하게 된다면 어떻게 해야할까요?

게임 속 캐릭터가 연료가 필요한 기계들을 만들 수 있고 게임 속 배경에 날아다니는 새들이 추가된다면 어떻게 설계해야할까요?


“음…날아다니는 것은 새와 비행기의 공통점인데… 기계들도 연료를 필요로하고 탈 것들도 연료가 필요한데… 기능이 겹치기는 한데 전혀 다른 객체인데… 상속하자니 불필요한 것들도 같이 상속되고…메소드를 똑같이 사용하자니 중복이 되고…“


이러한 상황에서 프로토콜은 강력한 능력을 보입니다. 프로토콜은 보다 좋은 확장성과 재사용성을 제공합니다.

그럼 본격적으로 프로토콜에 대해 살펴보는 시간을 갖도록 하겠습니다.



Protocol as Type

기본적으로 프로토콜은 일종의 타입입니다. 또한 프로토콜간의 채택도 가능합니다. 이는 클래스 간 상속이 가능한 것과 마찬가지입니다.

protocol Readable{
    func read()
}

protocol Writeable {
    func write()
}

protocol ReadWriteable: Readable, Writeable{ }

protocol ReadWriteTalkable: ReadWriteable {
    func talk()
}

class CanRead: Readable {
    func read(){
        print("I can read!")
    }
}

class CanWrite: Writeable {
    func write() {
        print("I can write!")
    }
}

struct CanReadWrite: ReadWriteable {
    func read() {
        print("I can raad!")
    }
    func write() {
        print("I can write!")
    }
}

struct CanReadWriteTalk: ReadWriteTalkable {
    func read() {
        print("I can raad!")
    }
    func write() {
        print("I can write!")
    }
    func talk() {
        print("I can talk!")
    }
}

print("string" is String) // true
print(CanRead() is Readable) // true
print(CanReadWrite() is Writeable) // true
print(CanReadWriteTalk() is ReadWriteable) // true

감이 오시나요?! 위의 예제를 통해 프로토콜을 따르는 것은 클래스 상속은 클래스끼리만 가능했던 것과 달리 Value Typestruct, enum에도 적용할 수 있습니다. 그리하여 보다 넓은 확장성과 재사용성을 보장합니다.



본격적으로 프로토콜의 강력함을 느껴보자 !

다음의 코드를 바탕으로 프로토콜의 강력함을 알아보는 시간을 갖겠습니다.

protocol Bird {
    var name: String { get }
    var canFly: Bool { get }
}

protocol Flyable {
    var airspeedVelocity : Double { get }
}

왜 두가지 기능을 나누어 놓았을까요?
프로토콜이 존재하기 전에는 Flyablebase class로 관련된 클래스들이 이를 상속하여 새, 비행기와 같은 날 수 있는 클래스들을 만들었을 것입니다.


이러한 프로토클을 세분화하는 것은 몇몇 언어들만 제공하는 클래스의 다중 상속과 유사한 기능을 스위프트에서는 프로토콜의 다중 채택으로 제공합니다. 이러한 기능은 재사용성을 월등히 높혀 시스템 전체의 유연성을 높이는 효과를 가져옵니다.


예제 코드를 보며 프로토콜에 대해서 더 알아보도록 하겠습니다.

struct FlappyBird: Bird, Flyable {
    let name: String
    let flappyAmplitude: Double
    let flappyFrequency: Double
    let canFly = true
    
    var airspeedVelocity: Double {
        return 3 * flappyFrequency * flappyAmplitude
    }
}

FlappyBird 구조체는 BirdFlyable 프로토콜을 준수합니다. 두 개의 구조체를 더 만들어보도록 하겠습니다.

struct Pengiun: Bird {
    let name: String 
    let canFly = false
}

struct SwiftbBird: Bird, Flyable {
    var name: String { return "Swift \(version)" }
    var version: Double
    let canFly = true
    
    var airspeedVelocity: Double { return version*1000.0 }
}

Pengiun은 새이지만 날지 못하는 새입니다. Bird이지만 Flyable하지는 않습니다. 만일 위에서 언급했던 것 처럼 Flyablebase class로 하여 Bird라는 클래스를 만들었다면 이와 같은 상황은 굉장히 난처로운 상황입니다.


하지만 프로토콜을 사용한다면 기능적인 요소들을 정의하여 이와 연관된 객체들이 이러한 프로토콜들을 준수하도록 합니다.


하지만 위의 코드에서도 몇몇 불필요하게 반복되는 코드를 발견할 수 있습니다. 모든 Bird 타입들은Flyable이라는 개념이 존재함에도 불구하고 canFly인지 아닌지를 명시해주고 있습니다.



Extending Protocols

스위프트에서는 위와 같은 불필요하게 반복되는 코드를 protocolextension을 통해 해결할 수 있습니다. 바로 Default Implementation 입니다. 코드로 살펴보도록 하겠습니다.

extension Bird {
    var canFly: Bool { return self as Flyable } 
} 

다음과 같이 extension을 통해 Bird를 확장시킬 수 있습니다.
위의 코드는 만일 Bird 프로토콜을 준수하는 객체가 Flyable 프로토콜도 준수한다면 canFlytrue 값을 반환하게끔 하는 코드입니다.


이러한 Default Implementation을 통해 디폴트 행위에 대한 것을 미리 정의하여 불필요한 코드가 반복되는 것을 막습니다.


이러한 문법은 얼핏보면 base classabstract classes를 사용하든 다른 언어들과 크게 다를 것이 없어보입니다. 하지만 스위프트에서는 위에서 언급했던 것과 같이 두가지의 장점이 있습니다.


  1. 여러개의 프로토콜을 준수할 수 있기 때문에 확장성과 재활용성이 좋습니다.
  2. 프로토콜은 class 타입만 가능한 상속과는 다르게 struct, enum에서도 준수할 수 있습니다.

Overriding Default Behavior

만일 들어본적 없는 새가 있다면 이 새가 날수 있는지 날지 못하는 새인지 정확히 알지 못할 수도 있습니다. 그렇다면 비행의 여부를 판단하여 Flyable을 나중에 준수하는 것은 코드 자체를 고쳐야하기 때문에 옳지 않은 행위입니다.


이를 위해서는 Flyable을 준수하되 canFlyfalse로 할당해주어야 합니다. 여기서 우리는 Default Behavior에 대한 overriding이 필요합니다. 방법은 어렵지 않습니다!

class UnknownBird: Bird, Flyable{
    var name:String 
    var canFly = false
    
    var airspeedVelocity = 0.0
}

다음과 같이 원하는 값을 직접 대입하므로써 overriding을 할 수 있고 혹은 다음과 같이 사용할 수 있습니다.

class UnknownBird: Bird, Flyable {
    var name:String=""
    var airspeedVelocity = 0.0
}

extension UnknownBird {
    var canFly: Bool {
        if self.airspeedVelocity > 0{
            return true
        }
        return false
    }
}

var unknown = UnknownBird()

print(unknown.canFly) // false
unknown.name = "NailerBird" 
unknown.airspeedVelocity = 11.11
// 새가 날 수 있다는 것이 판명난 뒤
print(unknown.canFly)  // true

마무리

지금까지 우리는 프로토콜에 기본에 이어 프로토콜의 심화된 내용을 살펴보았습니다. 프로토콜은 반드시 익혀두셔야 많은 디자인 패턴들을 이해하고 이를 실제 ios 개발에 접목하실 수 있습니다. 이 글이 스위프트를 공부하시는 분들에게 도움이 되셨길 바랍니다. 감사합니다.




참고자료