iOS

[Swift] POP(Protocol Oriented Programming) 한번쯤 들어봤을거고, 어쩌면 나도 모르게 하고 있었던거

Peppo 2025. 2. 1. 21:11
728x90

오늘은 POP (Protocol Oriented Programming)에 대해 공부해보려고 합니다.

 

그동안 Protocol이란 

1. 채택하면 해당 protocol 내부의 구현을 해야하고

2. struct, enum에도 채택을해서 구현할 수 있고

3. extension으로 구현부를 작성할 수 있고

4. delegate로 대리자에게 기능을 위임하도록 할 수 있고 

등등 생각보다 protocol이란 개념은 알겠지만

'POP 프로토콜 지향 프로그래밍을 하고 있나?'

라는 질문에는 섣불리 대답을 못할것 같다..

 

이에 어떤 장점이 있고 

어떤 상황에써야 좋으며, 

왜 써야하는지에 대해 정리를 해보고자 합니다.

 


프로토콜을 알고 있다는 가정하에 아래 내용을 작성합니다.

약간의 개념과 예시를 참고 하고싶다면 여기를 읽고 와주세요.

 

POP (Protocol Oriented Programming)

 

Swift에서 강조하는 프로그래밍 패러다임으로, 프로토콜을 중심으로 설계하는 방법 입니다.

OOP(객체 지향 프로그래밍)에서는 상속을 통해 기능을 확장하는 방식이 일반적이었지만,

상속은 하나밖에 할 수 없으며, 애플에서 구조체 사용을 권장하는데 구조체(struct)는 상속을 받지 못합니다.

이를 보완하기 위해 등장한 개념이 POP 입니다.

 


OOP, POP의 차이점

보완하기 위해 나왔다는 POP.

그럼 비교도 한번 해보겠습니다.

  OOP (객체 지향) POP (프로토콜 지향)
개념 클래스(Class) 중심 프로토콜 중심
기능 확장 상속(Inheritance) 사용 프로토콜 준수(Conformance) 사용
다중 기능 적용 단일 상속(다중 상속 불가) 다중 프로토콜  채택 가능

 


 

설명을 쓰는것보단 예시로 작성하는게 이해가 잘 될것 같아 바로 예시로 넘어가겠습니다.

 

예시

자전거, 자동차는 모두 탈것(Vehicle)에 속하고 공통으로 사용되는 프로퍼티, 메서드가 있을겁니다.

아래 예시에서는 공통 프로퍼티를 speed, 메서드를 가속(accelerate)으로 예를 들어볼게요.

 

먼저 공통으로 사용되는 부분은 protocol로 선언해주고 (Vehicle)

protocol Vehicle {
    var speed: Double { get set }
    func accelerate()
}

 

Car, Bicycle 클래스를 만들어 각각 Vehicle 프로토콜을 채택해주면 Vehicle을 따르지 않았다고 에러가 뜹니다. 

 

해당 프로토콜을 구현해줍니다.

class Car: Vehicle {
	var speed: Double = 0.0
    
    func accelerate() {
        self.speed += 10.0
        print("Car is accelerating. Current speed: \(self.speed) km/h")
    }
}

class Bicycle: Vehicle {
    var speed: Double = 0.0
    
    func accelerate() {
        self.speed += 5.0
        print("Bicycle is accelerating. Current speed: \(self.speed) km/h")
    }
}

let myCar = Car()
myCar.accelerate()
myCar.accelerate()
myCar.accelerate()

let myBicycle = Bicycle()
myBicycle.accelerate()
myBicycle.accelerate()

/* 출력
Car is accelerating. Current speed: 10.0 km/h
Car is accelerating. Current speed: 20.0 km/h
Car is accelerating. Current speed: 30.0 km/h
Bicycle is accelerating. Current speed: 5.0 km/h
Bicycle is accelerating. Current speed: 10.0 km/h
*/

 

 

초기구현(Default Implementation)

여기서 멈췄을때를 공통적으로 구현해주고 싶을때 extension으로 따로 구현해줍니다.

protocol Vehicle {
    var speed: Double { get set }
    func accelerate()
}

extension Vehicle {
    func stop(vehicle: String) {
        print("\(vehicle) is stopped")
    }
}

 

Car에서도, Bicycle에서도 stop() 메서드를 사용할 수 있게 됩니다.

let myCar = Car()
myCar.accelerate()
myCar.accelerate()
myCar.accelerate()
myCar.stop(vehicle: "Car")

let myBicycle = Bicycle()
myBicycle.accelerate()
myBicycle.accelerate()
myBicycle.stop(vehicle: "bicycle")

/* 출력
 Car is accelerating. Current speed: 10.0 km/h
 Car is accelerating. Current speed: 20.0 km/h
 Car is accelerating. Current speed: 30.0 km/h
 Car is stopped.
 Bicycle is accelerating. Current speed: 5.0 km/h
 Bicycle is accelerating. Current speed: 10.0 km/h
 bicycle is stopped.
 */

 

 

위와 같이 extension으로 프로토콜을 구현하는 방식을 초기구현(Default Implementation) 이라고 합니다.

 

오케이, 근데 아직 '와.. 꼭 써야겠는데?' 하는 포인트가 없는것 같은데.

좀 더 가보죠

 

프로토콜은 프로토콜끼리 상속도 가능하다.

만약 여기서 전기차, 전기자전거가 추가 된다면?

탈것이긴한데, 배터리 관련해서 정보가 있어야할거고, 충전도 해야할겁니다.

이에 아래와 같이 batteryLevel, charge() 메서드를 추가해보겠습니다.

 

'프로토콜은 프로토콜끼리 상속이 가능합니다.'

이전에 만들어뒀던 Vehicle을 상속시켜보면

protocol ElectricVehicle: Vehicle {
    var batteryLevel: Double { get set }
    func charge()
}

 

사실상 아래와 같은 코드겠죠.

protocol ElectricVehicle: Vehicle {
//  Vehicle 
//  var speed: Double { get set }
//  func acclerate()

    var batteryLevel: Double { get set }
    func charge()
}

 

Tesla라는 Class를 만들어서 ElectricVehicle을 채택시켜보면 아래와 같은 에러가 납니다.

ElectricVehicle, Vehicle을 준수해라

 

그리고 ElectricVehicle을 채택했으니 준수(따라줘야)해줘야겠죠.

구현해줍시다.

protocol ElectricVehicle: Vehicle {
    var batteryLevel: Double { get set }
    func charge()
}

class Tesla: ElectricVehicle {
    var batteryLevel: Double = 60.0
    
    func charge() {
        self.batteryLevel += 20.0
        if self.batteryLevel >= 100 {
            print("Tesla is fully charged")
        }
        else {
            print("Tesla is charging.. batteryLevel: \(self.batteryLevel)")
        }
    }
    
    // Vehicle
    var speed: Double = 0.0
    
    func accelerate() {
        self.speed += 20
        print("Tesla is accelerating. Current speed: \(self.speed) km/h")
    }
}

let myTesla = Tesla()
myTesla.charge()
myTesla.charge()
myTesla.accelerate()
myTesla.stop(vehicle: "Tesla")

/* 출력
 Tesla is charging.. batteryLevel: 80.0
 Tesla is fully charged
 Tesla is accelerating. Current speed: 20.0 km/h
 Tesla is stopped.
 */

 

 

정리

 

위의 예제를 통해 느꼈던점은 아래와 같습니다.

 

protocol의 역할을 세분화하게 되면서 필요한 기능들만 채택하게 할 수 있고 (여러 프로토콜 상속)

extension을 통해 기본 구현을 제공함으로써 중복될 코드들을 줄일 수 있고,

Swift에서 구조체사용을 권장하듯, 구조체(값 타입)에도 protocol을 통해 상속이 가능하도록 할 수 있다는 장점이 있다. 

 

이전에 protocol 관련 세션을 들었을때 

protocol 네이밍을 대부분 ~able (~할 수 있는)로 지어주는게 좋다고 했던것도,

'가능한 하나의 프로토콜이 하나의 기능을 담당하도록 세분화 하라'는 의미가 이번 글을 정리하면서 이해가 됐었습니다.

처음 들었을땐 메서드 하나당 프로토콜을 새로 하나 만드는 모습을 보고 '저렇게 까지 세분화 할 필요가 있나?' 라는 생각이

기능별로 protocol을 만들어두고 필요한것만 쏙쏙 골라 채택해주고 준수해주기만하면 되니 

추후 확장성을 고려하면 '해야겠다'는 생각으로 고쳐지게된것 같습니다.

 


 

 

참고

    1. https://velog.io/@haanwave/iOS-Swift-Protocol-Oriented-Programming-POP
    2. https://medium.com/nsistanbul/protocol-oriented-programming-in-swift-ad4a647daae2
    3. https://www.swiftanytime.com/blog/protocol-oriented-programming-in-swift

 

 

 

728x90