[Swift]프로그래밍 패러다임이란? #3 프로토콜 지향 프로그래밍

Eric·2022년 10월 14일
0
post-thumbnail

도입부

애플은 2015년 WWDC에서 Swift 2.0을 출시하고, 이를 프로토콜 지향 프로그래밍 언어(Protocol-Oriented Language)라고 소개했습니다.
하지만 기존의 객체 지향 언어는 많이 들어봤어도, 새로운 패러다임인 프로토콜 지향 언어는 생소할거라 생각합니다.
이 패러다임을 이해하기 위해서는 먼저, 프로토콜에 대한 이해가 필요합니다.

Protocol이란?

애플 공식 문서 Swift Language Guide에서 프로토콜을 아래와 같이 설명하고 있습니다.

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.


프로토콜은 특정 작업에 적합한 메서드, 프로퍼티 또는 다른 요구사항을 정의하는 청사진이다.

The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.


프로토콜은 클래스, 구조체, 열거형의 요구사항 구현을 위해 채택되어질 수 있다.

요약하자면, 프로토콜은 특정 작업에 적합한 메서드, 프로퍼티 등을 정의해놓은 설계도면이고,
이는 클래스, 구조체, 열거형에 채택되어 구현되어집니다.
프로토콜을 구현하는 일은 마치 건축하기 전, 건축물의 설계도면을 구상하는 것 같은 역할을 합니다.

Protocol Syntax

프로토콜은 아래와 같은 문법으로 구현할 수 있습니다.

protocol SomeProtocol {
	// Definition of protocol
}

클래스, 열거형, 구조체이 프로토콜을 채택할 때는 타입의 이름 옆에 프로토콜 이름을 적어주면 됩니다. 또, 여러 프로토콜을 채택할 때는 콤마(,)를 이용해 나열하여 작성하여 구현할 수 있습니다.

struct SomeStruct: SomeProtocol, AnotherProtocol {
	// Definition of Struct
}

Protocol Requirements

Property

  1. 프로토콜 내에서 프로퍼티를 구현할 때, 프로퍼티의 구체적인 성질을 정의할 필요 없습니다. 단지 프로퍼티 이름, 타입, gettable 인지 gettable and settable 인지만 작성하면 됩니다.
  2. 프로퍼티들은 반드시 변수(var)로 선언되어야 합니다.
protocol SomeProtocol {
	var mustBeSettable: String { get set }
    var doesNotNeedToBeSettable: String { get }
}
  1. 타입 프로퍼티를 정의할 때는 프로퍼티 앞에 static 키워드를 붙여줘야합니다.
protocol AnotherProtocol {
	static var typeProperty: String { get set }
    
    
}

적용 예시

간단한 예시로, 아래 프로토콜을 채택하고 있는 구조체를 구현해보겠습니다.

protocol Blossomable {
	var blossomSeason: String { get }
}

Blossomable 프로토콜은 blossomSeason이라는 gettable 프로퍼티를 정의하고 있습니다.

struct Flower: Blossomable {
	var blossomSeason: String
}

let daisy = Flower(blossomSeason: "April")
print(daisy.blossomSeason)
// daisy.blossomSeason = "April"

그리고 이를 채택하고 있는 Flower 구조체는 프로토콜에서 정의하고 있는 blossomSeason프로퍼티를 구현해주었습니다.
이렇게 프로토콜의 요구사항들을 모두 충족한 상태를 프로토콜을 준수(conform)했다.라고 합니다. 위 예시로 말하자면, Flower 구조체는 Blossomable 프로토콜을 준수했다고 말할 수 있습니다.

Method

  1. 프로토콜 내에서도 인스턴스 메서드, 타입 메서드를 정의할 수 있습니다. 타입 메서드를 정의 할 때 메소드 앞에 static 키워드를 붙여줘야합니다.
  2. 일반적인 메서드나 함수와 달리 중괄호({})를 쓰지 않는 것이 특징입니다.
  3. 타입 내의 프로퍼티를 메서드가 변경하는 경우, 메소드 앞에 mutating 키워드를 붙여줘야합니다.
protocol SomeProtocol {
    static func someTypeMethod()
    mutating func someModifyMethod()
}

적용 예시

protocol Blossomable {
	var blossomSeason: String { get }
    
    func blossom() -> String
}
struct Flower: Blossomable {
	var blossomSeason: String
    var presentSeason: String
    
    func blossom() -> String {
    	guard blossomSeason == presentSeason else { return "buds" }
        return "blossom"
    }
}


let daisy = Flower(blossomSeason: "April", presentSeason: "October")
print(daisy.blossom())
// "buds"

앞선 예제에서 사용했던 Blossomable 프로토콜에 blossom이라는 메서드를 추가했고, Flower 구조체에 blossom 메서드를 구현해줌으로써 프로토콜을 준수하였습니다.

Initializer

  1. 프로토콜 내에 이니셜라이저 역시 정의할 수 있습니다. 일반적으로 이니셜라이저를 구현하듯 작성하면 되지만, 중괄호({})는 적지 않습니다.
  2. 단, 클래스에 프로토콜을 채택하는 경우에는 프로토콜을 준수하기 위해 클래스에 정의하는 init 앞에 required 키워드를 붙여줘야 합니다.
protocol SomeProtocol {
	init(someParameter: String)

애플은 이 프로토콜과 구조체를 주로 사용하여 타입들을 구현했습니다.
애플 공식 문서 Swift Standard Library를 보면 실제로 기본 타입들도 구조체로 구현되어있는 것을 볼 수 있습니다.

하지만 아무리 프로토콜을 이용했다 해도, 상속도 안되는 구조체를 이용해서 구현한 타입들이 어떻게 다양한 공통 기능을 가지게 되는지 의문이 들 수 있습니다.

이 때 등장하는 개념이 바로 Extension입니다.

Extension이란?

애플 공식 문서 Swift Language Guide에서 프로토콜을 아래와 같이 설명하고 있습니다.

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you don’t have access to the original source code (known as retroactive modeling).


익스텐션은 이미 존재하는 클래스, 구조체, 열거형, 프로토콜 타입에 새로운 기능을 추가해준다.
이미 구현되어있어 기존 소스코드에 접근할 수 없는 타입들 역시 확장할 수 있다.

Extensions in Swift can:

  • Add computed instance properties and computed type properties
  • Define instance methods and type methods
  • Provide new initializers
  • Define subscripts
  • Define and use new nested types
  • Make an existing type conform to a protocol

스위프트에서 확장할 수 있는 것들

  • 연산 인스턴스 프로퍼티연산 타입 프로퍼티를 추가할 수 있음
  • 인스턴스 메서드타입 메서드를 정의 할 수 있음
  • 이니셜라이저를 제공할 수 있음
  • 서브스크립트를 정의할 수 있음
  • 새로운 중첩 타입들을 정의하고 사용할 수 있음
  • 이미 존재하는 타입을 프로토콜에 준수하게 만들 수 있음

앞서 설명한 프로토콜은 채택한 타입에게 프로토콜이 요구하는 기능들을 강제로 구현하게 만들고,
익스텐션은 프로토콜을 확장하여 프로토콜이 요구하는 기능들을 미리 구현할 수 있게 합니다.

그래서 프로토콜과 익스텐션을 잘 조합하여 초기구현을 잘해놓는다면, 타입에 프로토콜을 채택하는 것만으로 프로토콜이 요구하는 기능을 가지게 할 수 있습니다.
이것이 바로 프로토콜 지향 언어의 프로그래밍 방식입니다.

즉, 프로토콜 지향 프로그래밍은 "프로토콜익스텐션의 결합"으로 이루어집니다.

적용 예시

앞서 예시로 든 Blossomable 프로토콜은 Flower 타입에만 채택되었지만, 만약 다른 타입들에게도 채택하게 된다면 프로토콜에서 요구하는 기능들을 타입마다 전부 구현해주어야 할 것 입니다. 이런 코드의 중복을 피하기 위해 Extension을 이용합니다.

protocol Blossomable {
	var blossomSeason: String { get set }
    var presentSeason: String { get set }
    func blossom() -> String
}

extension Blossomable {
	func blossom() -> String {
    	guard blossomSeason == presentSeason else { return "buds" }
        return "blossom"
    }
}
struct Flower: Blossomable {
	var blossomSeason: String
    var presentSeason: String
}

struct Person: Blossomable {
	var blossomSeason: String
    var presentSeason: String
}

let daisy = Flower(blossomSeason: "April", presentSeason: "November")
print(daisy.blossom())
// "buds"
let eric = Person(blossomSeason: "Young", presentSeason: "Youth")
print(eric.blossom())
// "buds"

Extension을 이용하여 blossom 메서드를 구현하고나니, Blossomable 프로토콜을 채택하고있는 다른 타입들은 요구 메서드를 정의하지 않아도 오류가 나지 않고, blossom 메서드도 사용가능한 걸 볼 수 있습니다.

객체지향 프로그래밍(OOP)이랑 크게 다르지도 않은 것 같은데, 왜 굳이 프로토콜지향 프로그래밍(POP)을 배워야 하나요??

우선 이들은 대립적인 개념이 아닙니다. 단지 OOP가 만능이 아니기에, Swift 개발 설계과정에서 발생하는 OOP의 불편한 점을 POP로 해결하길 애플에서 권장하는 것일 뿐입니다.

간단한 예시를 들어 설명하자면,
게임에 나오는 몬스터들을 OOP로 코딩한다고 가정해봅시다.

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }

    func fight() {
        print("👊")
    }
}

몬스터 종류를 구별해 지상을 돌아다니는 몬스터와 하늘을 돌아다니는 몬스터로 나누어 클래스를 생성해보겠습니다.

class LandCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }

    func run() {
        print("🏃🏻")
    }
}
class SeaCreature: Creature {
    func swim() {
        print("")
    }
}

지상 몬스터 wolf를 만들었을 때는 전혀 이상없어 보이지만 바다 몬스터 killerWhale를 만들었을 때 문제가 발생하게 됩니다.

let wolf = LandCreature(name: "woolf")
wolf.walk() // 🚶🏻‍♀️
wolf.run() // 🏃🏻
wolf.fight() // 👊

let killerWhale = SeaCreature(name: "killer")
killerWhale.walk() // ??
killerWhale.run() // ??

SeaCreture 클래스는 Creature 클래스를 상속받았기 때문에, walk 메서드와 run 메서드를 사용할 수 있는데, 범고래가 뛰고 걷는건 개발자의 의도가 아니기 때문에, 코드 설계에 문제가 있다고 볼 수 있습니다.

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }

    func fight() {
        print("👊")
    }
}
class LandCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }

    func run() {
        print("🏃🏻")
    }
}

그래서 walk, run 메서드를 LandCreature 클래스의 메서드로 두어 문제를 해결했습니다.
하지만 지상에서 걷고 뛰며, 바다에서 헤엄칠 수 있는 스피노사우르스를 추가하려니 또 다른 문제가 발생했습니다.

class Spinosaurus: LandCreature { }
let spinosaurus = Spinosaurus(name: "spinosaurus")
spinosaurus.swim() 
spinosaurus.walk() // error!
spinosaurus.run() // error!
// LandCreature에는 walk, run 메서드가 없다.

스피노사우르스뿐만 아니라 다양한 몬스터들을 추가하다보면 분명 이런 상황을 맞닥드릴 것입니다.
이런 상황에서는 OOP로 접근하는 것이 쉽지 않아 보입니다.
그래서 이번엔 POP로 접근해보겠습니다.

struct Creature {
	let name: String
    init(name: String) {
    	self.name = name
    }
}

protocol Strikeable {
    func fight()
}

extension Strikeable {
    func fight() {
        print("👊")
    }
}

extension Creature: Strikeable { }

Creature 객체의 메서드로 있던 fightStrkieable 프로토콜로 초기구현하여 Creature에 준수시켰고,
다른 메서드들도 마찬가지로 프로토콜을 통해 초기구현 해주었습니다.

protocol Walking {
	func walk()
}

extension Walking {
	func walk() {
    	print("🚶🏻‍♀️")
    }
}
protocol Running {
	func run()
}

extension Running {
	func run() {
		print("🏃🏻")
    }
}
protocol Swimming {
	func swim()
}

extension Swimming {
	func swim() {
    	print("🚶🏻‍♀️")
    }
}

Swift에서는 다중상속이 안되어 하나의 타입이 여러가지 클래스를 상속 받지 못하지만, 프로토콜은 다양하게 채택할 수 있습니다.

class LandCreature: Creature,
                    Walking,
                    Running { }
                    
class Spinosaurus: LandCreature, Swimming { }

let spinosaurus = Spinosaurus(name: "spinosaurus")
spinosaurus.walk()
spinosaurus.run()
spinosaurus.swim()

OOP로 접근했을 때는 복잡해보였던 Spinosaurus 클래스 설계가 POP로 접근하니 쉽게 설계되었습니다.


위 예시처럼 POP를 잘 이용하면 OOP로 접근했을 때의 단점들을 보완해줄 수 있습니다. 이 외에도 OOP의 단점들을 보완해주는 POP의 장점을 나열해보자면,

i) 값 타입(value type)과 참조 타입(reference type)을 자유롭게 사용할 수 있다.

상속을 하기위해서는 참조 타입(reference type)인 class만을 사용함으로써,
값 타입(value type)으로 만들어도 되는 모델을 참조 타입으로만 정의해야한다.
하지만 프로토콜은 struct, class, enum 등의 참조 타입(reference type)과 값 타입 (value type)을 자유롭게 사용할 수 있다.

ii) 여러 개의 프로토콜을 채택할 수 있다.

상속 구조에서는 다중상속이 되지않는다. 즉, 하나의 부모 클래스만을 가질 수 있다. 하지만 프로토콜은 여러 개의 프로토콜을 채택할 수 있어 타입의 기능 확장에 유리하다. 또 프로토콜을 채택하는 각각의 타입이 서로 독립적이기 때문에, 상속의 문제점으로 언급되는 죽음의 다이아몬드에서도 자유롭다.

마무리

객체지향 프로그래밍은 객체 그 자체에 집중하고, 프로토콜지향 프로그래밍은 객체의 기능이나 행동에 집중하는 것을 볼 수 있습니다. 이런 패러다임의 관점 차이를 고려한다면, 어느 패러다임이 더 우월한지를 생각하는 것보다 어느 패러다임이 현재 상황에 더 적합한지를 생각하게 되는 것 같습니다.
그리고 이런 패러다임의 이해를 바탕으로 적재적소에 OOP와 POP를 활용하는게 개발자의 역량이라고 말할 수 있을 것 같습니다.

참고

https://wlaxhrl.tistory.com/77
https://tsh.io/blog/protocol-oriented-programming-swift/
https://blog.yagom.net/529/
https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html

profile
IOS Developer DreamTree

0개의 댓글