Swift – 프로토콜 지향 프로그래밍

오늘의 주제

  • 프로토콜 지향 프로그래밍

안녕하세요, 야곰입니다. 
지난 포스팅에서는 스위프트의 프로토콜과 익스텐션에 대해 알아봤습니다.

이번에는 스위프트와 함께 대두된 프로토콜 지향 프로그래밍 디자인 패턴에 대해 알아보겠습니다 🙂

프로토콜 지향 프로그래밍

애플은 2015년 9월, WWDC에서 스위프트 버전 2.0을 발표하면서 스위프트는 프로토콜 지향 언어(Protocol-Oriented Language)라고 발표했습니다.

프로토콜 지향 언어는 도대체 무슨 뜻일까요?
스위프트의 표준 라이브러리에서 타입과 관련된 것을 살펴보면 대부분이 구조체로 구현되어 있습니다.

객체지향 프로그래밍 패러다임에 기반을 둔 언어는 대부분 클래스의 상속을 사용해 타입에 공통된 기능을 구현합니다. 그런데 스위프트는 클래스로 구현된 타입은 별로없고, 대부분 구조체로 기본 타입이 구현되어 있습니다.

상속도 되지 않는 구조체로 어떻게 그렇게 다양한 공통 기능을 가질 수 있는 걸까요?
해답은 프로토콜과 익스텐션에 있습니다.

프로토콜 초기구현

지난 포스팅에서 프로토콜(Protocol)특정 역할을 수행하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진이라고 말씀드렸습니다.

프로토콜을 채택(Adopted)한 타입은 프로토콜이 요구하는 기능을 구현하여 프로토콜을 준수해야(Conform)합니다. 

익스텐션은 기존 타입의 기능을 확장하며, 프로토콜은 프로토콜을 채택한 타입이 원하는 기능을 강제로 구현한다는 점을 우리는 알고 있습니다.

그런데 특정 프로토콜을 정의하고 여러 타입에서 이 프로토콜을 준수하게 만들어 타입마다 똑같은 메서드, 똑같은 프로퍼티, 똑같은 서브스크립트 등을 구현해야 한다면...?
얼마나 많은 코드를 중복 사용해야 하며 또 유지보수는 얼마나 힘들어질지 생각만 해도 머리가 아플 겁니다.

이때 필요한 게 바로 익스텐션과 프로토콜의 결합입니다. 
프로토콜을 채택한 타입의 정의부에 프로토콜의 요구사항을 구현하지 않더라도 프로토콜의 익스텐션에 미리 프로토콜의 요구사항을 구현해 둘 수 있습니다.
이를 프로토콜 초기구현이라고 합니다. 

protocol Talkable {
    var topic: String { get set }
    func talk(to: Self)
}

struct Person: Talkable {
    var topic: String
    var name: String

    func talk(to: Person) {
        print("\(topic)에 대해 \(to.name)에게 이야기합니다")
    }
}

여기 지난 포스팅에서 예제로 보았던 Talkable 프로토콜이 있습니다.
이 프로토콜은 Person이라는 구조체 타입에만 채택이 되었으므로 여러 프로퍼티와 메서드를 구현하더라도 Person에만 구현하면 되므로 큰 문제가 없었습니다.

그런데, Talkable이라는 프로토콜을 Person 뿐만 아니라 다른 타입에서도 채택하고 싶다면?
아마도 그 타입에서도 Talkable 프로토콜이 요구하는 사항을 모두 구현해 주어야 할 것입니다.

protocol Talkable {
    var topic: String { get set }
    func talk(to: Self)
}

struct Person: Talkable {
    var topic: String
    var name: String

    func talk(to: Person) {
        print("\(topic)에 대해 \(to.name)에게 이야기합니다")
    }
}

struct Monkey: Talkable {
    var topic: String

    func talk(to: Monkey) {
        print("우끼끼 꺄꺄 \(topic)")
    }
} 

그런데 프로토콜이 요구하는 사항을 미리 모두 한꺼번에 구현해 둘 수 있다면 중복된 코드를 피할 수 있을 것입니다.

protocol Talkable {
    var topic: String { get set }
    func talk(to: Self)
}

// 익스텐션을 사용한 프로토콜 초기 구현
extension Talkable {
    func talk(to: Self) {
        print("\(to)! \(topic)")
    }
}

struct Person: Talkable {
    var topic: String
    var name: String
}

struct Monkey: Talkable {
    var topic: String
}

let yagom = Person(topic: "Swift", name: "yagom")
let hana = Person(topic: "Internet", name: "hana")

yagom.talk(to: hana)
hana.talk(to: yagom) 

위의 코드에서는 PersonMonkeyTalkable의 요구사항인 talk(to:) 메서드를 구현하지 않았음에도 전혀 오류가 발생하지 않습니다.

이렇게 하나의 프로토콜을 만들어두고, 초기 구현을 해둔다면 여러 타입에서 해당 기능을 사용하고 싶을 때 프로토콜을 채택하기만 하면 됩니다.

만약에 프로토콜 초기 구현과 다른 동작을 해야한다면, 그저 그 타입에 프로토콜의 요구사항을 재정의해주면 됩니다.

struct Monkey: Talkable {
    var topic: String
    func talk(to: Monkey) {
        print("\(to)! 우끼기기기끼기기")
    }
}

let sunny = Monkey(topic: "바나나")
let jack = Monkey(topic: "나무")

sunny.talk(to: jack) 

프로토콜 초기 구현을 잘 해둔다면 여러 프로토콜을 그저 채택만 하기만하면 그 타입에 기능이 추가되는 것이죠.

protocol Flyable { func fly() }

extension Flyable {
    func fly() {
        print("푸드득 푸드득")
    }
}

protocol Runable { func run() }

extension Runable {
    func run() {
        print("후다닥 후다닥")
    }
}

protocol Swimable { func swim() }
extension Swimable {
    func swim() {
        print("어푸 어푸")
    }
}

protocol Talkable { func talk() }
extension Talkable {
    func talk() {
        print("재잘재잘 쪼잘쪼잘")
    }
}

struct Bird: Flyable, Talkable { }

let bird = Bird()
bird.fly()
bird.talk()

struct Person: Runable, Swimable, Talkable { }

let person = Person()
person.run()
person.talk()
person.swim() 

위 코드처럼 프로토콜 초기 구현을 잘 해두면 이렇게 프로토콜 채택만으로도 그 기능을 사용할 수 있게 됩니다.
프로토콜 초기 구현이 프로토콜 지향 프로그래밍의 핵심이라고 볼 수 있습니다.

프로토콜 지향 프로그래밍을 추구하는 이유

그렇다면 프로토콜 지향 프로그래밍을 하는 이유는 무엇일까요?

구조체, 클래스, 열거형 등 구조화된 타입 중에 상속은 클래스 타입에서만 가능합니다.

클래스는 참조 타입이므로 참조 추적에 비용이 많이 발생합니다.
비교적 비용이 적은 값 타입을 활용하고 싶어도, 상속을 할 수 없으므로 때마다 기능을 다시 구현해 주어야 했지만, 프로토콜 지향 프로그래밍은 그 한계를 없앴습니다.

기능의 모듈화가 더욱 명확해 집니다.

클래스가 상속을 할 수 있도록 설계되어 있다고 하더라도 다중상속을 지원하는 언어는 많지 않습니다. 다중상속을 지원하지 않는다는 뜻은 하나의 상속체계에서 다른 상속체계에 속해있는 기능을 끌어다 쓸 수 없다는 뜻입니다.

그런데 프로토콜 지향 프로그래밍은 기능을 프로토콜이라는 단위로 묶어 표현하고 초기 구현을 해 둘 수 있으니 상속이라는 한계점을 탈피할 수 있습니다.


어떤가요? 프로토콜 지향 프로그래밍! 정말 매력적이지 않나요?

익스텐션을 통한 각 프로토콜의 초기구현은 구현코드를 볼 수 없기 때문에 어떻게 구현되었는지는 확실히 볼 수 없지만 Array의 정의만 보더라도 제네릭, 프로토콜을 다양하게 사용한 것을 볼 수 있습니다.
아마도 각 타입별로 공유하는 초기구현은 익스텐션으로 구현되어 있을 것입니다. 

스위프트의 주요 기능을 하나하나 알아갈수록 스위프트 표준 라이브러리의 코드가 눈에 잘 들어올거예요.
하나의 기능을 알아갈 때마다 스위프트 표준 라이브러리를 살펴보면서 어떤 기능을 통해 구현 되었는지, 어떻게 연관이 되는지 읽어보고, 해석해보고, 상상해보는 것도 언어를 이해하는 데 도움이 됩니다. 

이번 포스팅에서는 프로토콜 지향 프로그래밍(Protocol Oriented Programming, POP) 대해 알아보았습니다. 

앞으로 또 무슨 주제를 써 나갈까요? 
그 동안 많은 도움이 되었길 빕니다!고맙습니다 😀

본 글의 일부내용은 필자의 저서 [스위프트 프로그래밍](2017, 한빛미디어)(http://www.hanbit.co.kr/store/books/look.php?p_code=B5682208459)에서 요약, 발췌하였음을 알립니다.

스위프트의 문법에 대해 더 알아보고 싶다면 애플의 Swift Language Guide[https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html]를 참고해도 많은 도움이 됩니다.

한글로 프로토콜 지향 프로그래밍에 대해 소개된 내용은 (https://realm.io/kr/news/protocol-oriented-programming-in-swift/)에서도 확인해 볼 수 있습니다.

by yagom


facebook : https://www.facebook.com/yagompage
facebook group : https://www.facebook.com/groups/yagom/

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.RSS Feed 받기   

This Post Has 3 Comments

  1. 와… 곰센세 감사합니다!

  2. 옛날 닌텐도 DS에 게임 칩셋을 끼워서 해당 게임을 플레이 하던게
    스위프트의 프로토콜과 무척 비슷해 보이지 않나요?

    라고 이야기하려다가 닌텐도DS에서 R4칩이 나온 것처럼, 기술의 발달이 받쳐준다면 모든 구조체가 모든 프로토콜을 선택적으로 사용할 수 있게 되는 순간이 오지 않을까 싶기도 하네요. 야곰은 어떻게 생각해요?

    1. R4 칩에 관한 이야기는 이해하지 못해서 잘 모르겠지만, 닌텐도에 여러 칩을 끼울수 있는 이유는 닌텐도 칩이 따라야하는 프로토콜을 따르고 있기 때문이겠죠?ㅎㅎ

댓글 남기기

Close