Protocol Oriented Programming에 대한 얕은 공부

Swift는 프로토콜 지향 언어라고 한다. 아직도 대부분 클래스를 사용하고 구조체, 열거형 등은 특별한 경우에만 쓰는 나로서는 Swift의 특징적인 면을 제대로 이해하지 못 하고 사용하고 있다는 느낌을 받았다. 그래서 이번 주제로 잡고 조사하게 되었다.

POP가 나오게 된 배경

기존의 객체 지향 프로그래밍에서는 공통된 기능을 Class의 상속을 통해 구현한다. Swift도 많은 부분이 Class로 이루어져 있다. 대표적인 것이 UIViewController일 것이다. 그런데 참조 타입의 경우 다중 스레드 환경 같은 곳에서 원본 데이터가 바뀔 수 있기 때문에 값 타입을 사용하는 것이 권장되고 있다. 그렇지만 Struct나 Enum의 경우 상속이 불가능하기 때문에 공통적인 기능을 어떻게 구현할지 의문이 생긴다.

이 의문에 대한 답으로 2015년 애플은 Swift 2의 Extension을 제시하였다. Protocol과 Extension의 조합으로 Protocol은 메서드, 프로퍼티 등 구현해야 할 사항들을 모아놓은 껍데기에서 구현까지 할 수 있게 되었다. 이것을 Protocol Default Implementation, 프로토콜 초기 구현이라고 하고 이를 통해 Class의 상속과 같이 공통된 기능을 구현할 수 있게 되어 POP가 가능하게 되었다.

프로토콜 초기 구현(Protocol Default Implementation)

프로토콜의 한계

프로토콜은 위에서 언급했듯이 일종의 설계도일 뿐 그 속에 실제 값들을 담지 못 한다. 그래서 다음과 같은 일이 일어난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protocol Person {
var name: String { get }
var age: Int { get }

func getAge() -> Int
}

struct FireFighter: Person {
var name: String
var age: Int

// 중복 구현
func getAge() -> Int {
return age
}

func extinguish() {
}
}

struct PolieMan: Person {
var name: String
var age: Int
var jail: [Criminal] = []

// 중복 구현
func getAge() -> Int {
return age
}

mutating func arrest(criminal: Criminal) {
jail.append(criminal)
}
}

getAge() 메서드는 하는 일이 같은데도 불구하고 구현부를 따로 둬야 하기 때문에 중복으로 구현해야하는 문제가 발생한다.

Extension 적용

여기서 사용할 수 있는 것이 Extension이다. 다음과 같이 구현하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protocol Person {
var name: String { get }
var age: Int { get }

func getAge() -> Int
}

// 프로토콜 초기구현
extension Person {
func getAge() -> Int {
return self.age
}
}

struct FireFighter: Person {
var name: String
var age: Int

func extinguish() {
}
}

struct PolieMan: Person {
var name: String
var age: Int
var jail: [Criminal] = []

mutating func arrest(criminal: Criminal) {
jail.append(criminal)
}
}

Extension을 통해 Person 프로토콜의 getAge() 메서드를 미리 구현하여 Person 프로토콜을 준수하는 구조체가 따로 구현할 필요없이 사용할 수 있다. 그런데 만약 변형해서 사용하고 싶다면 사용하는 클래스 내애서 재정의하여 사용하면 된다.

Protocol과 Generic

프로토콜은 GenericAssociatedType을 적용하면 더욱 더 재사용성을 높일 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protocol Box {
// 연관값으로 선언
associatedtype Item

var items: [Item] { get set }
mutating func addItem(item: Item)
}

extension Box {
mutating func addItem(item: Item) {
items.append(item)
}
}

// 제네릭으로 선언
struct StructBox<Element>: Box {
typealias Item = Element

var items: [Element]
}

let intBox: StructBox<Int> = StructBox(items: [0, 1])
let stringBox: StructBox<String> = StructBox(items: ["Alpaca", "Cattle"])

프로토콜 추가

여기서 새로운 기능을 추가할 경우 Extension으로 새로운 프로토콜을 따르게 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol Printable {
func printSelf()
}

// where로 Self가 Box 프로토콜을 따르는 경우에만 초기 구현이 되도록 제약.
extension Printable where Self: Box {
func printSelf() {
print(items)
}
}

extension StructBox: Printable {
}

// StructBox는 Box 프로토콜을 따르므로 초기 구현된 것을 바로 사용 가능.
stringBox.printSelf()
// ["Alpaca", "Cattle"]

POP의 이점

  • 가볍고 안전하다.
    굳이 알려주지 않아야 할 것은 알려주지 않아도 되고 그로 인해 필요한 것만 쓸 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protocol Car {
func drive()
}

struct Sedan: Car {
let brand: String

func drive() {
print("drive")
}
}

// Struct로
let sedan1: Sedan = Sedan(brand: "BMW")
sedan1.brand // 접근 가능
// Protocol로
let sedan2: Car = Sedan(brand: "Audi")
// sedan2.brand // 접근 불가
  • 값 타입의 상속 효과
    위에서 보듯이 값 타입도 공통된 기능을 쉽게 구현할 수 있다.

  • 수평적인 기능 확장
    Class는 하나의 상속만 가능하고 수직적인 구조를 고려하여야 하지만 Protocol은 마치 블럭처럼 기능을 추가할 수 있다.

  • 제네릭의 활용
    자료형의 구애를 받지 않는 제네릭을 활용하였을 때 POP의 힘은 훨씬 강력해진다고 한다. Swift의 Array 타입을 예로 들 수 있겠다. 수많은 프로토콜을 준수하는 Array 타입은 타입에 관계없이 만들 수 있으며 그에 따른 메서드들도 다양하게 지원이 된다.


이번 공부를 하면서 느낀 것은 Swift 언어가 깊이 들어갈수록 배울 것이 많아지고 그만큼 활용성이 커진다는 것이다. POP를 당장 활용할 수는 없겠지만 좀 더 유연한 사고를 가지고 이것을 직접 적용해보는 시간을 점차 늘려야겠다.

Reference