-
프로토콜 지향언어, Swift (2)iOS/Swift 2020. 3. 14. 11:34
타입으로서의 프로토콜
프로토콜은 그 자체로는 아무런 기능을 구현하고 있지 못하므로 인스턴스를 만들 수 없을뿐더러 프로토콜만으로 할 수 있는 일도 거의 없다.
하지만 프로토콜은 특별히 기능을 부여하지 않더라도 코드 내에서 자료형으로 사용하기에 부족함이 없는 객체이다.
따라서 때로는 타입으로서 중요한 역할을 하기도 한다.
- 상수나 변수, 그리고 프로퍼티의 타입으로 사용할 수 있다.
- 함수, 메소드 또는 초기화 구문에서 매개변수 타입이나 반환 타입으로 프로토콜을 사용할 수 있다.
- 배열이나 사전, 혹은 다른 컨테이너의 타입으로 사용할 수 있다.
특정 프로토콜을 구현한 구조체나 클래스들이 있을 때, 이들의 인스턴스를 각각의 타입이 아닌 프로토콜 타입으로 정의된 변수나 상수에 할당할 수 있다.
프로토콜 타입으로 정의된 변수나 상수에 할당된 객체는 프로토콜 이외에 구현체에서 추가한 프로퍼티나 메소드들을 컴파일러부터 감춰준다.
protocol Wheel { func spin() func hold() } class Bicycle: Wheel { var moveState = false func spin() { self.pedal() } func hold() { self.pullBreak() } func pedal() { self.moveState = true } func pullBreak() { self.moveState = false } }
바퀴를 정의하는 Wheel이라는 프로토콜을 선언하고, 두 개의 메소드를 통해 바퀴가 움직이고 멈추는 기능을 만들었다.
그리고 Wheel을 구현하는 자전거를 의미하는 Bicycle 클래스를 만들었다.
let trans = Bicycle() trans.moveState trans.pedal() trans.pullBreak() trans.spin() trans.hold()
위와 같이 사용하게 된다면 초기화된 객체를 대입한 상수 trans는 타입 추론에 의하여 Bicycle 타입으로 결정된다.
따라서 사용할 수 있는 프로퍼티나 메소드는 Bicycle 클래스에 선언된 모든 프로퍼티와 메소드이다.
let trans: Wheel = Bicycle() trans.spin() trans.hold()
하지만 상수 trans의 타입 어노테이션에 프로토콜 타입을 사용하여 위와 같이 Wheel 타입 상수에 할당한다면, 해당 상수가 사용할 수 있는 프로퍼티와 메소드는 Wheel에 선언된 프로퍼티와 메소드뿐이다.
컴파일러가 읽어 들이는 trans 상수의 타입은 Wheel 프로토콜 타입이므로 trans 상수가 사용할 수 있는 메소드 역시 Wheel 프로토콜에 선언된 spin(), hold() 메소드로 제한되어 나머지 Bicycle에 선언되었던 프로퍼티와 메소드는 은닉된다.
이처럼 객체 본래 타입이 아니라 프로토콜 타입으로 선언한 변수나 상수에 할당받아 사용하는 것은 특정 프로토콜을 구현한 모든 클래스나 구조체를 변수나 상수에 할당할 수 있다는 장점이 있다.
실질적으로 정의된 객체가 무엇이든지 특정 프로토콜을 구현하기만 했다면 모두 할당받을 수 있다.
클래스는 AnyObject 타입으로 변수나 상수를 선언하면 모든 클래스를 할당받을 수 있지만, 이는 클래스로 제한될 뿐만 아니라 프로토콜에 정의된 프로퍼티나 메소드를 전혀 사용할 수 없다.
반면에 프로토콜 타입으로 선언하여 할당받으면 프로토콜에서 선언된 메소드나 프로퍼티는 모두 이용할 수 있다.
하지만 필요에 따라 두 개 이상의 특정 프로토콜들 타입을 모두 사용해야 할 때도 있다.
protocol A { func doA() } protocol B { func doB() } class Impl: A, B { func doA() { } func doB() { } func desc() -> String { return "Class instance method" } } var ipl: A & B = Impl() ipl.doA() ipl.doB()
ipl 변수의 타입으로 사용된 A & B 는 두 개의 프로토콜을 모두 포함하는 객체 타입이라는 뜻이다.
이 타입으로 정의된 변수는 두 프로토콜을 모두 구현한 객체만 할당받을 수 있다.
(ipl은 클래스 Impl의 인스턴스를 할당받았지만, 클래스에서 정의된 메소드인 desc()는 사용할 수 없다.)
함수나 메소드, 초기화 구문 등의 매개변수를 프로토콜 타입으로 정의할 경우에도 동일하다.
입력된 값의 실제 타입에 상관없이, 인자 값은 프로토콜 타입에서 정의된 메소드나 프로퍼티만 사용할 수 있다.
델리게이션
프로토콜 타입으로 선언된 값을 사용한다는 것은, 여기에 할당된 객체가 구체적으로 어떤 기능을 갖추고 있는지는 상관없다는 뜻이기도 하다.
단순히 할당된 객체를 사용하여 프로토콜에 정의된 프로퍼티나 메소드를 호출하겠다는 의미가 된다.
"네가 누군지 난 알 필요 없다. 다만 너는 내가 호출할 메소드를 구현하고 있기만 하면 된다."
코코아 터치 프레임워크에서는 이러한 프로토콜 타입의 특성을 이용하여 델리게이션이라는 기능을 구현한다.
델리게이션(Delegation)이란 특정 기능을 다른 객체에 위임하고, 그에 따라 필요한 시점에서 메소드의 호출만 받는 패턴이다.
protocol FuelPumpDelegate { func lackFuel() func fullFuel() }
FuelPumpDelegate 프로토콜은 연료의 양에 따라 필요한 알림을 전달하는 프로토콜이다.
이 프로토콜은 두 개의 메소드로 이루어져 있는데, 하나는 연료가 부족할 때 호출되는 메소드이며 또 다른 하나는 연료가 가득 찼을 때 호출되는 메소드이다.
이 메소드들을 자동차, 오토바이 등 각 객체에서 나름대로 구현하여 연료펌프는 이 객체들의 메소드만 호출하여 연료를 보충하거나 보충을 중단한다.
class FuelPump { var maxGage: Double = 100.0 var delegate: FuelPumpDelegate? = nil var fuelGage: Double { didSet { if oldValue < 10 { // 연료가 부족해지면 델리게이트의 lackFuel 메소드를 호출한다. self.delegate?.lackFuel() } else if oldValue == self.maxGage { // 연료가 가득차면 델리게이트의 fullFuel 메소드를 호출한다. self.delegate?.fullFuel() } } } init(fuelGage: Double = 0) { self.fuelGage = fuelGage } // 연료펌프를 가동한다. func startPump() { while (true) { if (self.fuelGage > 0) { self.jetFuel() } else { break } } } // 연료를 엔진에 분사한다. 분사할 때마다 연료 게이지의 눈금은 내려간다. func jetFuel() { self.fuelGage -= 1 } }
연료펌프 클래스이다. 이 클래스는 FuelPumpDelegate 프로토콜을 구현한 객체의 정보를 delegate 프로퍼티에 저장해두었다가, 필요한 시점에 프로토콜의 메소드를 호출하는 대상으로 사용한다.
또한, 연료 눈금을 의미하는 프로퍼티 fuelGage에 대한 프로퍼티 옵저버를 작성하여 연료 눈금이 변화할 때마다 적정 수치를 검사하고 10 미만으로 떨어지면 델리게이트 프로퍼티에 저장된 객체에 lackFuel() 메소드를, 연료가 가득 차면 fullFuel() 메소드를 각각 호출한다.
(프로퍼티 옵저버는 프로퍼티 값이 변경될 때마다 호출된다.)
이때 delegate 프로퍼티에 저장되는 객체는 FuelPumpDelegate 프로토콜 타입으로 선언된다. delegate 프로퍼티는 선언된 타입으로 인해, 실제 그 객체가 어떤 타입이든지 관계없이 FuelPumpDelegate 프로토콜에 정의된 lackFuel()과 fullFuel() 메소드만을 사용할 수 있다.
할당된 인스턴스가 나머지 프로퍼티나 메소드들도 분명 구현하고 있겠지만, 여기에서는 그 정보를 알 필요는 없다. 그냥 필요한 시점에 lackFuel()과 fullFuel() 메소드들을 호출할 수 있으면 된다.
class Car: FuelPumpDelegate { var fuelPump = FuelPump(fuelGage: 100) init() { self.fuelPump.delegate = self } // fuelPump가 호출하는 메소드이다. func lackFuel() { // 연료를 보충한다. } // fuelPump가 호출하는 메소드이다. func fullFuel() { // 연료 보충을 중단한다. } // 자동차에 시동을 건다. func start() { fuelPump.startPump() } }
fuelPump라는 프로퍼티에 앞에서 작성한 연료펌프 클래스의 인스턴스를 할당한다.
초기화 구문을 통해 클래스가 만들어질 때 연료펌프에 연료를 100으로 채우고, 연료펌프의 델리게이트 프로퍼티를 자신으로 설정한다.
이제 Car 클래스를 인스턴스로 생성하여 start 메소드를 호출하면 연료펌프 역시 작동되면서 연료가 부족해지는 시점이 오면 delegate 객체를 대상으로 lackFuel() 메소드를 호출한다.
delegate 프로퍼티에는 Car의 인스턴스가 할당되어 있으므로 Car 클래스에서 작성한 lackFuel() 메소드가 실행된다.
즉, 델리게이트 참조를 통해 메소드를 호출할 인스턴스 객체를 전달받고, 이 인스턴스 객체가 구현하고 있는 프로토콜에 선언된 메소드를 호출하는 것이 델리게이션이다.
왜 굳이 프로토콜을 사용해서 델리게이션을 적용할까?
그것은 클래스가 단일 상속만을 지원하기 때문에 구현 개수에 제한이 없는 프로토콜을 이용하여 필요한 기능 단위별 객체를 작성하는 것이다.
프로토콜의 활용
확장 구문과 프로토콜
익스텐션에서 프로토콜을 구현할 수도 있다.
익스텐션은 별도의 타입으로 존재하는 객체라기보다 기존에 정의되었던 객체 자체를 확장하여 새로운 기능을 추가하는 역할이므로 익스텐션에서 프로토콜을 구현한다는 것은 일반적으로 구조체나 클래스, 열거형에서 프로토콜을 구현하는 것과 차이가 거의 없다.
이때 확장하기 전 본래의 객체에서는 프로토콜을 구현하지 않았더라도 익스텐션에서 프로토콜을 구현한다면 이후의 객체들은 프로토콜을 구현한 것으로 처리된다.
class Man { var name: String? init(name: String = "홍길동") { self.name = name } } protocol Job { func doWork() }
위의 코드에서 Man은 Job 프로토콜을 구현하지 않기 때문에 doWork() 메소드 역시 사용할 수 없다.
하지만 익스텐션을 사용한다면 클래스를 수정하지 않고도 Job 프로토콜을 구현할 수 있다.
extension Man: Job { func doWork() { print("\(self.name!)님이 일을 합니다") } } let man = Man(name: "개발자") man.doWork()
주의할 점은 익스텐션에서 저장 프로퍼티를 정의할 수 없다는 점이다.
만약 프로토콜에 정의된 프로퍼티를 익스텐션에서 구현해야 한다면, 이때에는 연산 프로퍼티로 구현해야 한다.
'iOS > Swift' 카테고리의 다른 글
[Swift] ARC 알아보기 (Auto Reference Counting) (0) 2020.09.04 Closure (2) (0) 2020.04.17 Closure (0) 2020.04.11 프로토콜 지향언어, Swift (3) (0) 2020.03.21 프로토콜지향 언어, Swift (1) (0) 2020.03.07