티스토리 뷰
개발을 하다 보면, 객체지향 프로그래밍(OOP)의 한계를 느낄 때가 많다. 특히 "이걸 꼭 상속으로 만들어야 할까?" 하는 고민이 들 때가 있다. 클래스 상속은 강력하지만, 단일 상속만 지원하다 보니 확장이 어렵고, 잘못 설계하면 결국 유지보수가 힘들어진다.
Swift는 이런 문제를 해결하기 위해 프로토콜 지향 프로그래밍(POP, Protocol-Oriented Programming)을 강력하게 밀고 있다. 사실상 Swift는 OOP보다 POP를 더 권장한다고 봐도 될 정도다.
그럼, POP가 대체 뭐길래 그렇게 강조하는 걸까? 오늘은 이 개념을 내 나름대로 정리해보고, 직접 코드로 구현하면서 이해해보려 한다.
객체지향(OOP)과 프로토콜 지향(POP), 뭐가 다를까?
객체지향 프로그래밍(OOP)에서는 클래스를 상속해서 기능을 확장하는 게 일반적이다.
예를 들어 강아지를 나타내는 Dog 클래스를 만든다고 해보자.
OOP 방식 (클래스 상속)
class Animal {
func makeSound() {
print("Some sound")
}
}
class Dog: Animal {
override func makeSound() {
print("Bark!")
}
}
let dog = Dog()
dog.makeSound() // "Bark!"
이 코드만 보면 문제가 없어 보인다. 그런데, 만약 Dog가 Mammal(포유류) 클래스를 상속받고 싶다면?
Swift는 다중 상속을 지원하지 않기 때문에 한 개의 부모 클래스밖에 가질 수 없다.
이제 고민이 생긴다.
"포유류 기능을 따로 빼야 하나?"
"근데 그럼 또 Bird나 Fish 같은 애들은 어떻게 하지?"
"기능이 많아질수록 상속 관계가 복잡해지는 거 아냐?"
OOP는 이런 문제에서 자유롭지 못하다.
POP 방식 – 유연한 프로토콜 조합
POP에서는 상속 대신 프로토콜(Protocol)을 조합해서 기능을 만든다.
Dog가 꼭 Animal 클래스를 상속받을 필요 없이, 그냥 "소리를 낼 수 있다"는 SoundMaking 프로토콜만 채택하면 된다.
protocol SoundMaking {
func makeSound()
}
extension SoundMaking {
func makeSound() {
print("Some sound")
}
}
struct Dog: SoundMaking {
func makeSound() {
print("Bark!")
}
}
struct Cat: SoundMaking {
func makeSound() {
print("Meow!")
}
}
let dog = Dog()
dog.makeSound() // "Bark!"
let cat = Cat()
cat.makeSound() // "Meow!"
이 방식이 뭐가 좋냐면?
- Dog, Cat, Bird 등 다양한 타입이 필요한 기능만 채택할 수 있다.
- SoundMaking 프로토콜의 기본 구현을 extension으로 제공할 수도 있다.
- 필요하면 각 타입별로 맞춤 구현을 할 수도 있다.
"상속 없이도 이렇게 깔끔하게 기능을 나눌 수 있구나!"
이걸 처음 알았을 때 좀 신선한 충격이었다.
POP의 조합형 설계 – OOP보다 유연하다!
만약 오리(Duck)는 물에서 헤엄칠 수도 있고, 하늘을 날 수도 있다고 해보자.
이걸 OOP로 설계하려면 Duck이 Bird와 Swimmer 두 개의 클래스를 동시에 상속받아야 할 수도 있는데, Swift에서는 다중 상속이 안 된다.
하지만 POP 방식이라면?
protocol Flyable {
func fly()
}
protocol Swimmable {
func swim()
}
// 기본 동작 정의
extension Flyable {
func fly() {
print("날 수 있어!")
}
}
extension Swimmable {
func swim() {
print("헤엄칠 수 있어!")
}
}
// Duck은 두 개의 프로토콜을 조합해서 사용
struct Duck: Flyable, Swimmable {}
let duck = Duck()
duck.fly() // "날 수 있어!"
duck.swim() // "헤엄칠 수 있어!"
오... 이렇게 하면 기능 조합이 엄청 유연해진다!
- Duck은 Flyable과 Swimmable을 동시에 채택하면 된다.
- Fish는 Swimmable만 채택하면 되고,
- Eagle은 Flyable만 채택하면 된다.
OOP에서는 단일 상속이 문제였는데, POP에서는 조합만 잘하면 해결된다.
이거 꽤 혁신적인 패러다임 아닌가?
프로토콜 확장(Extension)이 POP를 더 강력하게 만든다
사실 Swift에서 POP가 강력한 이유는 extension을 통해 프로토콜에 기본 구현을 줄 수 있기 때문이다.
protocol Identifiable {
var id: String { get }
}
extension Identifiable {
func displayID() {
print("My ID is \(id)")
}
}
struct User: Identifiable {
let id: String
}
let user = User(id: "12345")
user.displayID() // "My ID is 12345"
- Identifiable을 채택한 모든 타입이 displayID()를 자동으로 사용할 수 있다!
- 필요하면 오버라이딩해서 직접 구현할 수도 있다.
이걸 보면 "Swift가 왜 OOP보다 POP를 더 권장하는지" 알 것 같다.
객체지향에서는 이런 걸 위해 기본 클래스를 만들고 상속해야 하는데, Swift에서는 그냥 프로토콜 + 익스텐션 조합이면 끝이다.
그럼 언제 POP를 써야 할까?
솔직히 말해서 모든 걸 다 POP로 구현할 필요는 없다.
클래스 상속이 더 적절한 경우도 있을꺼다
하지만 이런 상황에서는 POP가 빛을 발한다.
✅ 서로 다른 타입이 동일한 기능을 가져야 할 때 → (예: SoundMaking, Flyable)
✅ 기능을 확장하면서도 코드 중복을 피하고 싶을 때 → (extension을 활용)
✅ 구조체(Struct)를 주로 사용하면서 기능을 공유하고 싶을 때
정리 – POP는 Swift에서 필수 개념이다!
Swift는 OOP보다 POP를 더 추천하는 언어다.
클래스 상속의 단점을 보완하고, 확장성을 높이기 위해 프로토콜 + 익스텐션 조합을 적극적으로 활용하는 게 좋다.
이제 POP 개념을 이해했으니,
실제 프로젝트에서도 "이걸 꼭 상속으로 만들어야 할까?" 한 번 더 고민해 보면 좋을 것 같다.
그냥 습관적으로 클래스를 상속하는 게 아니라, 프로토콜을 활용해서 더 깔끔하고 유연한 구조를 만들 수 있을지도 모르니까!
'프로그래밍 공부' 카테고리의 다른 글
| ARC(Automatic Reference Counting) – Swift는 어떻게 메모리를 관리할까? (1) | 2025.02.20 |
|---|---|
| Swift – iOS 개발자가 알아야 할 모든 것 (2) | 2025.02.19 |
| 객체 지향 설계의 SOLID 원칙 – 제대로 알고 써보자! (0) | 2025.02.18 |
| 함수형 프로그래밍(FP) – 진짜 왜 써야 할까? (0) | 2025.02.17 |
| 반응형 프로그래밍(React Programming, RP) – 데이터를 흐름처럼 다루는 방식 (0) | 2025.02.15 |