본문으로 건너뛰기

프로토콜을 사용해 설계

프로토콜지향 프로그래밍에서의 프로토콜은 어떻게 설계할 수 있을까? 객체지향 프로그래밍에서는 서브클래스를 위한 모든 기본적인 요구 사항을 포함하는 슈퍼클래스를 갖는다. 프로토콜 설계 방식은 이와는 다르다.

프로토콜지향 프로그래밍에서는 슈퍼클래스 대신 프로토콜을 사용하며, 이는 요구사항을 작고 매우 구체적인 프로토콜로 나누기에 매우 적절하다.

Robots 모델링을 예제로 활용해보자. 모든 로봇은 움직임을 갖고 있으므로 이러한 움직임에 대한 요구 사항을 정의할 프로토콜을 만드는 것으로 시작한다.

protocol RobotMovement {
func forward(speedPercent: Double)
func reverse(speedPercent: Double)
func left(speedPercent: Double)
func right(speedPercent: Double)
func stop()
}

로봇이 2차원상에서 움직이는 경우 위 프로토콜 만으로 요구 사항을 충족할 수 있다. 만약 3차원상에서 움직이게 하기 위한 요구 사항이 추가된다면 프로토콜 상속을 활용할 수 있다.

protocol RobotMovementThreeDimensions: RobotMovement {
func up(speedPercent: Double)
func down(speedPercent: Double)
}

RobotMovement 인스턴스가 RobotMovementThreeDimensions 프로토콜을 따르는지를 확인하고자 할 경우 is 키워드를 사용할 수 있다.

이제 로봇 설계를 위한 센서를 추가해보자.

protocol Sensor {
var sensorType: String {get} // 센서타입 정의
var sensorName: String {get set} // 센서 구분을 위함

init(sensorName: String)
func pollSensor() // 센서의 기본동작 수행
}

이제 위 프로토콜을 활용해 구체적인 센서 타입에 대한 요구 사항을 만들어보자.

protocol EnvironmentSensor: Sensor {
func currentTemperature() -> Double // 마지막 온도 반환
func currentHumidity() -> Double // 마지막 습도 반환
}

센서 타입을 더 만들어보자.

protocol RangeSensor: Sensor {
func setRangeNotification(rangeCentimeter: Double, rangeNotification: () -> Void)
func currentRange() -> Double
}

protocol DisplaySensor: Sensor {
func displayMessage(message: String)
}

protocol WirelessSensor: Sensor {
func setMessageReceivedNotification(messageNotification: (String) -> Void)
func messageSend(message: String)
}

위 프로토콜 중 노티피케이션을 설정하는 메서드는 메서드 매겨변수로 클로저를 받으며, 클로저는 어떠한 일이 발생했을 경우 로봇 코드에 즉시 알려주기 위해 pollSensor() 메서드 내에서 사용될 것이다.

이와 같은 프로토콜지향 설계에서 얻을 수 있는 장점에는 두 가지가 있다. 첫 번째로 각 프로토콜은 특정 센서 타입에서 필요한 구체적인 요구 사항만을 포함한다. 두 번째는 프로토콜 컴포지션을 사용해 단일 타입이 다중 프로토콜을 따르게 할 수 있다.

예를 들어 와이파이가 내장된 Display 센서를 가진 경우 DisplaySensorWirelessSensor 프로토콜을 모두 준수하는 타입을 생성할 수 있다.

이제 로봇 타입을 위한 요구 사항을 정의할 프로토콜을 생성해보자.

protocol Robot {
var name: String {get set}
var robotMovement: RobotMovement {get set}
var sensors: [Sensor] {get}

init(name: String, robotMovement: RobotMovement)
func addSensor(sensor: Sensor)
func pollSensors()
}

위 프로토콜들을 활용하면 다음과 같은 객체를 생성하고 활용할 수 있다.

class MyTinyRobot: Robot {
var name: String

var robotMovement: RobotMovement

var sensors: [Sensor] = []

required init(name: String, robotMovement: RobotMovement) {
self.name = name
self.robotMovement = robotMovement
}

func addSensor(sensor: Sensor) {
sensors.append(sensor)
}

func pollSensors() {
for sensor in sensors {
sensor.pollSensor()
}
}
}

class RobotMovement2D: RobotMovement {
var position: CGPoint = .init()

func forward(speedPercent: Double) {
position.y += speedPercent
}

func reverse(speedPercent: Double) {
position.y -= speedPercent
}

func left(speedPercent: Double) {
position.x += speedPercent
}

func right(speedPercent: Double) {
position.x -= speedPercent
}

func stop() {
print(position)
}
}

struct TinyDisplaySensor: DisplaySensor {
func displayMessage(message: String) {
print(message)
}

var sensorType: String

var sensorName: String

init(sensorName: String) {
self.sensorName = sensorName
self.sensorType = sensorName
}

func pollSensor() {
displayMessage(message: "called!")
}
}

let myTinyRobot = MyTinyRobot(name: "my tiny robot", robotMovement: RobotMovement2D())
let myTinyDisplaySensor = TinyDisplaySensor(sensorName: "tinyDisplaySensor")

myTinyRobot.addSensor(sensor: myTinyDisplaySensor)

myTinyRobot.robotMovement.stop() // print((0.0, 0.0))
myTinyRobot.pollSensors() // print(called!)
myTinyRobot.robotMovement.forward(speedPercent: 2)
myTinyRobot.robotMovement.left(speedPercent: 2)
myTinyRobot.robotMovement.stop() // print((2.0, 2.0))