[Swift] 프로토콜 지향 프로그래밍이란?
Swift 언어를 학습하다보면 새롭게 접하는 프로그래밍 패러다임이 있다.
오늘은 새로운 프로그래밍 패러다임인 POP(Protocol Oriented Programming)에 대하여 알아볼까 한다.
🤔 프로토콜이란?
POP를 이해하기 위해선 우선 프로토콜이라는 개념을 명확히 잡고갈 필요가 있다.
프로토콜은 특정 작업이나 기능에 맞는 메서드, 속성 및 기타 요구 사항의 청사진을 정의한다. 그런 다음 프로토콜은 클래스, 구조체 또는 열거형에서 채택되어 해당 요구사항의 실제 구현을 제공한다.
문서에서 얘기하고 있는 프로토콜은 우리가 OOP에서 본 Super class와 매우 유사한 부분이 있다.
그렇다면 이 둘은 어떤 부분이 같으며 또 어떻게 다를까?
🆚 부모 클래스 vs 프로토콜
부모 클래스(Super class)
슈퍼 클래스는 다른 클래스가 상속받는 기본 클래스이다. 상속을 통해 서브 클래스는 슈퍼 클래스의 속성과 메서드를 물려받아 재사용할 수 있다.
특징
- 단일 상속: 대부분의 OOP 언어에서는 한 클래스가 하나의 부모 클래스를 가질 수 있다.
- 재사용성: 슈퍼 클래스에 구현된 메소드와 속성을 서브 클래스가 그대로 사용할 수 있다.
- 상속 계층 구조: 클래스 간의 계층적인 관계를 형성하여, 공통된 기능을 슈퍼 클래스에서 정의하고, 특화된 기능은 서브 클래스에서 추가한다.
프로토콜(Protocol)
정의는 위에서 설명됐고, 특징에 대해 살펴보자
특징
- 다중 채택: 하나의 클래스나 구조체가 여러 개의 프로토콜을 채택할 수 있다.
- 행동 규약: 프로토콜은 특정한 동작이나 기능을 정의하며, 이를 채택한 타입이 해당 기능을 구현하도록 강제한다.
- 유연성: 상속과 달리 프로토콜은 다양한 타입에 동일한 기능을 부여할 수 있어, 코드의 재사용성과 유연성을 높인다.
예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protocol Drivable {
func drive()
}
protocol Flyable {
func fly()
}
class Vehicle: Drivable {
func drive() { print("Driving...") }
}
class FlyingCar: Drivable, Flyable {
func drive() { print("Driving a flying car...") }
func fly() { print("Flying a car!") }
}
let car = Vehicle()
car.drive() // 출력: Driving...
let flyingCar = FlyingCar()
flyingCar.drive() // 출력: Driving a flying car...
flyingCar.fly() // 출력: Flying a car!
주요 차이점
위에서 언급한 특성에서 보이듯 이 둘은 상속 방식(단일 vs 다중), 기능 제공 방식(메서드 물려줌 vs 청사진 제공), 목적(재사용성과 확장 vs 기능 구현 강제) 등 다양하게 존재한다.
👍🏻 적절한 사용방법
그렇다면 이 둘을 어떻게 구분해서 사용해야될까?
당연하지만 명확히 구분된 방법은 없고, 본인이 구현하려는 것을 기반으로 생각해서 사용해야된다.
슈퍼 클래스
- 여러 클래스가 공통된 기능과 속성을 공유해야 할 때
- 상속 계층을 통해 자연스러운 계층 구조를 형성하고자 할 때
- 기본적인 구현을 서브 클래스에서 재사용하고, 필요에 따라 오버라이딩을 할 때
프로토콜
- 여러 타입이 특정 기능을 구현해야 하지만, 공통의 상위 클래스가 필요 없을 때
- 다중 상속이 필요할 때
- 특정 기능을 구현하는 다양한 타입을 통일된 방식으로 다루고자 할 때.
🖨️ 프로토콜 지향 프로그래밍이란?
OOP에서 보이는 여러 제약(단일 상속, 결합도, 상속 계층 구조 등)과 같은 한계를 보완하고, 코드의 유연성과 재사용성을 높이기 위해 고안된 프로그래밍 패러다임이다.
프로토콜 지향 프로그래밍은 프로토콜을 중심으로 설계된 프로그래밍 패러다임이다. POP는 OOP의 상속 구조 대신 프로토콜 확장을 이용하여 구현해준다.
🔧 POP의 주요 특징
유연성과 재사용성 향상
프로토콜 중심의 코드를 구성함으로써, 다양한 타입이 공통된 인터페이스를 채택하고 구현할 수 있게 한다. 이는 상속 기반의 OOP보다 더 유연한 코드 재사용성을 제공
다중 채택
클래스는 단일 상속만 가능하지만, 여러 프로토콜을 채택할 수 있다. 이를 통해 다양한 기능을 조합하여 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol Drivable {
func drive()
}
protocol Flyable {
func fly()
}
struct FlyingCar: Drivable, Flyable {
func drive() {
print("Driving a flying car...")
}
func fly() {
print("Flying a car!")
}
}
또한 POP는 상속보다 구성을 우선시하여, 더 작은 단위의 프로토콜을 조합함으로써 복잡한 기능을 구현할 수 있다.
이렇게 다양한 특성을 통해 POP는 OOP보다 코드의 모듈화를 가능하게 하고 유지보수성을 높이는 데 기여를 한다.
Value Types의 활용
Swift에서 Value types인 struct와 enum이 존재한다. POP는 이러한 값 타입을 지원해준다.
값 타입을 지원해줌으로써 얻게 되는 이득은
- 안정성 및 예측 가능성: 값 타입은 복사가 발생할 때 원본과 복사본이 독립적으로 존재하므로, 데이터의 무결성과 안정성을 보장
- 성능 최적화: 값 타입은 힙 대신 스택 메모리에 할당되기에, 성능 면에서 이점을 가질 수 있다.
상속의 한계 극복
OOP에서 상속은 강력한 도구이다. 하지만 단일 상속의 제한과 타이트한 결합으로 인해 코드의 유연성이 떨어질 수 있다.
애플은 POP를 통해 이러한 한계를 극복하려 했다.
- 타이트한 결합 감소: 프로토콜을 사용하면 클래스 간의 결합도를 낮추어, 더 느슨한 결합을 구현할 수 있다.
- 다중 상속의 대안: 다중 프로토콜 채택을 통해 여러 기능을 결합할 수 있어, 다중 상속의 필요성을 줄인다.
Swift의 설계 철학과 일치
Swift는 안정성, 성능, 모던한 프로그래밍 패러다임을 목표로 설계되었다. POP는 이러한 Swift의 설계 철학과 잘 맞아 떨어진다.
- 명확한 열학 분리: 프로토콜이 특정한 기능이나 역할을 담당함으로써 코드의 책임이 명확하게 분리된다.
- 확장성: Swift의 Extension 기능과 결합하여, 프로토콜에 기본 구현을 제공하거나 기존 타입에 새로운 기능을 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protocol Drivable {
func drive()
}
extension Drivable {
func startEngine() {
print("Engine started.")
}
}
struct Car: Drivable {
func drive() {
print("Driving a car...")
}
}
let car = Car()
car.drive() // 출력: Driving a car...
car.startEngine() // 출력: Engine started.
테스트 용이성 및 유지보수성 향상
프로토콜을 사용하면 DI(의존성 주입, Dependency injection)가 용이해져 단위 테스트 작성이 쉬워진다. 또한, 프로토콜을 통해 인터페이스를 명확히 정의함으로써 코드의 유지보수성과 확장성이 향상된다.
🧑🏻💻 결론
애플은 POP를 도입한 가장 큰 이유는 Swift 언어의 기능을 최대한 활용하고, 더 유연하고 재사용 가능한 코드를 작성하기 위함이라 볼 수 있을것 같다.
POP가 상속의 한계를 극복하고, Value type의 장점을 살려주며, 코드의 모듈화와 유지보수성을 높이는데 큰 역할을 한다.
물론 POP가 정답은 아니지만 POP의 개념을 명확히 이해하고 적절히 활용하는것은 프로그램의 전반적인 차원에서 긍정적이라 느껴진다.