티스토리 뷰

개발을 하다 보면, 객체지향 프로그래밍(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로 설계하려면 DuckBirdSwimmer 두 개의 클래스를 동시에 상속받아야 할 수도 있는데, 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 개념을 이해했으니,
실제 프로젝트에서도 "이걸 꼭 상속으로 만들어야 할까?" 한 번 더 고민해 보면 좋을 것 같다.
그냥 습관적으로 클래스를 상속하는 게 아니라, 프로토콜을 활용해서 더 깔끔하고 유연한 구조를 만들 수 있을지도 모르니까! 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
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
글 보관함