[Swift] 프로토콜 지향 프로그래밍(Protocol-Oriented Programming)

728x90

프로토콜 지향 프로그래밍(Protocol-Oriented Programming) in Swift에 대해 알아 보겠습니다.

 

 

애플은 2015년 WWDC에서 스위프트를 발표하면서 스위프트 언어는 프로토콜 지향 언어(Protocol-Oriented Language)라고 발표 했습니다.
흔히 알고 있는 객체 지향 프로그래밍(Object-Oriented Programming)이라고 하면, 사물을 객체로 형성하여 공통점을 갖는 모든 곳에서 상속받는 객체 내부의 모든 로직을 캡슐화합니다. 의도 하지 않아도 상속했다는 이유로 모든 속성과 행위를 공유해야하며, 복잡한 상속 구조를 지닌 클래스를 상속했다면 원하는 클래스를 참조해야 할때 다운캐스팅을 해야 합니다. 또한 가장 큰 단점은 단 하나의 SuperClass만 상속할 수 있다는 점입니다. 시간이 흐르면 기능도 확장하기 마련이므로 이에 따라 복잡도가 높아지고 관리도 어려워지게 됩니다.

 

여기서 프로토콜 지향 프로그래밍이 등장하게 된 계기를 찾을 수 있습니다.
POP는 필요한 부분만 프로토콜로 분리해서 만들 수 있고 다중 프로토콜을 구현할 수 있습니다.
게다가 프로토콜 규칙을 class, struct, enum에 적용할 수 있기 때문에 확장 부분에서도 OOP보다 유연합니다.
스위프트는 다른 언어와 달리 클래스로 구현된 타입보다는 대부분 구조체로 기본 타입이 구현되어 있습니다. 상속도 되지 않는 구조체로 공통 기능을 가질 수 있는 방법에는 프로토콜 익스텐션에 있습니다.

 

먼저 프로토콜을 구현해보겠습니다.

protocol Walkable {
    var isBareFoot: Bool { get set }
    
    func walk()
}

struct Person: Walkable {
    var isBareFoot: Bool
    var name: String
    
    func walk() {
        print("\(name)은 걷습니다")
    }
}

struct Animal: Walkable {
    var isBareFoot: Bool
    
    func walk() {
        print("\(name)은 걷습니다")
    }
}

 

Walkable 프로토콜이 있습니다. 이 프로토콜은 맨발인지 아닌지의 구분과 걷을 수 있는 기능이 있습니다.
프로토콜을 채택시 아시다시피 프로토콜이 요구하는 사항을 모두 구현해주어야합니다.
예를 들어 프로토콜에 많은 요구 사항들이 들어있고, 많은 구조체에서 채택을 했다면 많은 중복된 코드를 사용해야 할 겁니다.
이를 방지하기 위해 프로토콜의 요구사항을 구현하지 않더라도 프로토콜의 익스텐션에 미리 프로토콜의 요구사항을 구현해 둘 수 있습니다.

 

protocol Walkable {
    var isBareFoot: Bool { get set }
    var speed: Double { get set }
    
    func walk(name: String)
}

extension Walkable {
    func walk(name: String) {
        if isBareFoot == true {
            print("\(name)은 맨발인 상태에 \(speed)속도로 걷습니다.")
        } else {
            print("\(name)은 신발인 상태에 \(speed)속도로 걷습니다.")
        }
    }
}

struct Person: Walkable {
    var isBareFoot: Bool
    var speed: Double
}

struct Animal: Walkable {
    var isBareFoot: Bool
    var speed: Double
}

let seungjin = Person(isBareFoot: false, speed: 5.0)
seungjin.walk(name: "승진")
let dog = Animal(isBareFoot: true, speed: 10.0)
dog.walk(name: "몽실")

위의 코드에서 Person과 Animal은 Walkable의 요구사항인 walk(name:)을 구현하지 않았음에도 불구하고 오류가 발생하지 않습니다.
프로토콜이 요구하는 사항을 미리 구현해 둘 수 있다면 중복된 코드를 피할 수 있습니다.

구현을 잘 해둔다면 프로토콜 채택만으로도 그 타입의 기능이 추가되어 사용할 수가 있죠.

protocol Walkable {
    func walk()
}

extension Walkable {
    func walk() {
        print("걷다")
    }
}

protocol Flyable {
    func fly()
}

extension Flyable {
    func fly() {
        print("날다")
    }
}

protocol Runable {
    func run()
}

extension Runable {
    func run() {
        print("뛰다")
    }
}

struct Person: Walkable, Runable {}

let seungjin = Person()
seungjin.walk()
seungjin.run()

struct Bird: Flyable, Walkable, Runable {}

let bird = Bird()
bird.fly()
bird.walk()
bird.run()

프로토콜 채택만으로도 그 기능을 사용할수 있게 프로토콜 구현을 잘 해두는게 프로토콜 지향 프로그램에서 가장 중요하다고 볼 수 있습니다.
또한 객체지향프로그래밍의 SuperClass와 달리, 기능들을 분리하여 Protocol extension을 생성할 수 있습니다. 이는 Superclass 단일화 문제점에 대한 해결책이라고 볼 수 있습니다.