[Swift 프로그래밍] 프로토콜 (protocol)

이정훈·2022년 7월 5일
0

Swift 기본

목록 보기
19/22
post-thumbnail

본 내용은 스위프트 프로그래밍 3판 (야곰 지음) 교재를 공부한 내용을 바탕으로 작성 하였습니다.

Swift는 프로토콜 지향 프로그래밍 언어이다.

이번에는 스위프트의 프로토콜에 대하여 알아 보고자 한다.

프로토콜 (protocol) 이란?


프로토콜의 사전적 정의는 다음과 같다.

프로토콜은 특정한 기능을 수행하기 위한 요구 사항의 청사진(blue print)이다.

쉽게 말하면 어떠한 기능을 수행하기 위해 특정 타입(클래스, 구조체, 열거형 등)에게 프로토콜이 요구하는 요구 사항을 반드시 타입 내부에 구현해야 한다고 알려주는 역할이라고 보면 될꺼 같다.

필자는 프로토콜의 정의를 보고 JAVA의 인터페이스와 유사하다고 생각했다.

또한 타입 내부에 프로토콜이 요구하는 요구사항들을 모두 빠짐 없이 구현 되었을때 타입이 프로토콜을 준수한다(conform) 라고 할 수 있다.

여기서 중요한 것은 프로토콜은 기능을 직접 구현하지 않고 정의만 한다는 것이다.

이제 프로토콜의 형태를 살펴보자

protocol 프로토콜 이름 {
	프로토콜 정의
}

프로토콜은 protocol 키워드를 사용하여 프로토콜임을 명시한다.

프로토콜을 채택하기 위한 방법은 타입명 뒤에 콜론(:)을 붙인뒤에 채택하고자하는 프로토콜명을 뒤에 써준다.

또한 프로토콜은 클래스처럼 상속이 가능하고 다중 상속을 지원하기 때문에 프로토콜명 뒤에 쉼표(,)를 사용하여 다중 상속을 표현한다.

클래스의 상속과 프로토콜 채택을 구분하는 방법은 상속 받을 클래스을 먼저 콜론(:) 뒤에 명시하고 그 뒤에 쉼표(,)를 사용하여 프로토콜을 명시하여 구분한다.

AClass: BClass, SomeProtocol, AnotherProtocol {
	//클래스 정의
}

위의 형태는 AClass 클래스가 BClass의 상속을 받으며 SomeProtocol과 AnotherProtocol을 채택한다는 의미이다.

프로토콜 요구사항


1. 프로퍼티 요구

프로토콜은 특정 타입에게 프로퍼티를 구현하도록 요구할수 있다.

다음은 프로토콜에 프로퍼티를 요구하도록 작성된 코드이다.

protocol info {
    var name: String { get }    //읽기 전용 프로퍼티 요구
    var age: Int { get }    //읽기 전용 프로퍼티 요구

struct Person: info {
    var age: Int
    var name: String
}

프로토콜 내부의 프로퍼티는 실제로 프로퍼티 내용을 구현하지 않으며 읽기 전용으로 할지 읽기, 쓰기가 모두 가능하도록 할지 정하는 역할을 한다.

위의 코드에서 프로토콜 내부의 fullName 변수는 읽기 전용인 { get }으로 정의 되어있고 읽기 쓰기 모두 구현하기 위해서는 { get set }으로 정의하면된다.

프로토콜을 정의 하였으면 프로토콜을 채택한 타입을 만들고 내부에 요구하는 프로퍼티의 타입과 이름이 일치하게 구현하면 된다.

여기서 중요한 점은 읽고 쓰기가 모두 가능한 프로퍼티의 요구를 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티로 구현할 수 없다.

하지만 읽기 전용으로 요구한 프로퍼티는 상수 저장 프로퍼티, 읽기 전용 프로퍼티를 포함하여 읽고 쓰기가 모두 가능한 프로퍼티로 구현할 수 있다.

가령 다음과 같은 프로토콜이 있다고 가정하자

protocol SomeProtocol {
    var settableProperty: String { get set }    //읽기, 쓰기 전용 프로퍼티 요구
    var notNeedToBeSettableProperty: String { get }    //읽기 전용 프로퍼티 요구
    
    //타입 프로퍼티는 static으로 선언
    //구현부에서 class 혹은 static으로 선언해도 상관 없음
    static var someTypeProperty: Int { get set }
    static var anotherProperty: Int { get }
}

SomeProtocol은 타입 프로퍼티를 요구하고 있으며, static 키워드로 선언되어 있다.
하지만 타입 프로퍼티를 구현하는 구현부에서는 static 혹은 class 키워드 모두 사용하여 타입 프로퍼티를 구현할 수 있다.

2. 메서드 요구

프로토콜은 특정 타입에게 메서드를 구현하도록 요구할 수 있다.

다음과 같이 프로토콜에서 메서드를 요구하고 프로토콜을 채택한 타입에서 구현할 수도 있다.

protocol info {
    var name: String { get }    //읽기 전용 프로퍼티 요구
    var age: Int { get }    //읽기 전용 프로퍼티 요구
    
    //메서드 요구
    func greet(to: Any)
}

struct Person: info {
    var age: Int
    var name: String
    
    //메서드 구현
    func greet(to: Any) {
        if let person: info = to as? info {    //info protocol를 준수하는 인스턴스인지 확인
            print("\(person.name)씨, 안녕하세요! 저의 이름은 \(self.name)이고 나이는 \(self.age)입니다. 잘 부탁합니다!")
        } else {
            print("대화 상대가 없습니다.")
        }
    }
}

위와 같이 프로토콜에서 메서드를 요구할때는 함수의 몸통 부분을 제외한 함수의 이름과 매개변수, 반환 타입만 작성하면 된다.(매개변수 기본값 지정 불가)

가령 다음과 같은 프로토콜이 있다고 가정하자

protocol SomeProtocol {
    //타입 메서드 또한 static으로 선언
    static func someTypeMethod(_ parameter: Any) -> Any
}

SomeProtocol은 타입 메서드를 요구하고 있다.

프로토콜에서 타입 메서드를 요구할때는 타입 프로퍼티와 마찬가지로 static 키워드를 사용한다.
하지만 실제 구현부에서 타입 메서드를 구현할때는 static 혹은 class 키워드로 모두 구현 가능하다.

3.이니셜라이저 요구


프로토콜을 프로퍼티와 메서드와 같이 이니셜라이저 또한 요구할 수 있다.

protocol info {
    var name: String { get }    //읽기 전용 프로퍼티 요구
    var age: Int { get }    //읽기 전용 프로퍼티 요구
    
    //이니셜라이저 요구
    init(name: String, age: Int)
    
    //메서드 요구
    func greet(to: Any)
}

class Person: info {
    var age: Int
    var name: String
    
    //이니셜라이저 요구 구현
    required init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    //메서드 구현
    func greet(to: Any) {
        if let person: info = to as? info {    //info protocol를 준수하는 인스턴스인지 확인
            print("\(person.name)씨, 안녕하세요! 저의 이름은 \(self.name)이고 나이는 \(self.age)입니다. 잘 부탁합니다!")
        } else {
            print("대화 상대가 없습니다.")
        }
    }
}

여기서 주의할 점은 프로토콜이 요구하는 이니셜라이저를 구현 할때는 required 키워드를 사용하여 요구 이니셜라이저 형태로 구현해야 한다.

해당 class를 상속 받는 모든 자식 클래스는 부모 클래스가 준수하는 프로토콜을 준수해야 하며, 따라서 이니셜라이저를 필수적으로 구현하도록하는 요구 이니셜라이저로 구현해야 한다.

하지만 다음과 같이 특정 클래스에서 이미 프로토콜에서 요구하는 이니셜라이저를 가지고 있는 상태에서 다른 클래스가 그 클래스를 상속 받았다면 requiredoverried 키워드를 모두 사용하여 구현해야 한다.

class SomeClass {
	var name: String
    var age: Int
    
    init(name: String, age: Int) {
    	self.name = name
        self.age = age
    }
}

class AnotherClass: SomeClass, info {
	var name: String
    var age: Int
    
    required overried init(name: String, age: Int) {
    	self.name = name
        self.age = age
    }
}

프로토콜 상속


프로토콜은 다른 하나 이상의 프로토콜을 상속 받아 더 많은 요구 사항을 추가 할 수 있다.

protocol SwiftProgramming {
    func swift()
}

protocol PythonProgramming {
    func python()
}

protocol Programming: SwiftProgramming, PythonProgramming {
    func coding()
}

class Programmer: Programming {
    func coding() {
        print("I can Coding")
    }
    
    func swift() {
        print("I can use Swift!")
    }
    
    func python() {
        print("I can use Python!")
    }
}

위의 코드 처럼 Programming 프로토콜은 SwiftProgramming 프로토콜과 PythonProgramming 프로토콜을 상속 받았다.
따라서 Programming 프로토콜을 준수하는 Programmer 클래스는 세개의 프로토콜 요구사항을 모두 구현하여야 한다.

만약 위의 예시에서 프로토콜 상속 리스트에 class 키워드를 함께 넣으면 해당 프로토콜을 class에서만 준수 가능한 프로토콜이 된다.

protocol Programming: class, SwiftProgramming, PythonProgramming {
    ...
}

프로토콜 준수 확인


isas 연산자를 사용하여 대상이 특정 프로토콜을 준수하고 있는지 확인하거나 프로토콜로 타입 캐스팅을 할 수 있다.

  • is 연산자는 대상이 특정 프로토콜을 준수하고 있는지 확인할 수 있다.
  • as? 연산자는 대상을 다른 프로토콜로 다운캐스팅을 시도 할 수 있다.
  • as! 연산자는 대상을 다른 프로토콜로 강제 다운캐스팅을 할 수 있다.
var personLee: Programmer = Programmer()
print(personLee is Programming)     //true

if let test: Programming = personLee as? Programming {
    print("Programming 프로토콜을 준수합니다.")   //Programming 프로토콜을 준수합니다.
}

personLee 인스턴스는 Programmer 클래스 타입으로 Programmer 클래스는 Programming 프로토콜을 준수하기 때문에

personLee is Programming

is 연산자의 연산 결과로 true를 반환한다.

또한 Programming 프로토콜도 하나의 타입이며 프로토콜을 준수하는 타입을 프로토콜로 타입 캐스팅 할 수 있다.

따라서 personLee 인스턴스를 as? 연산자를 사용하여 Programming 프로토콜로 다운 캐스팅 할 수 있다.

if let test: Programming = personLee as? Programming {
    print("Programming 프로토콜을 준수합니다.")  
}

값 바인딩의 결과가 참이되어 내부 수행문이 실행되게 된다.

선택적 protocol 요구


프로토콜의 요구사항 중 일부를 선택적으로 요구할 수 있다. 그렇게 되면 프로토콜을 준수하는 클래스는 해당 요구를 실제로 구현해도 되고 안해도 된다.

프로토콜의 요구사항을 선택적으로 요구하기 위해서는 먼저 해당 프로토콜이 @objc 속성을 부여해야 한다.

@objc 속성의 본 목적은 swift에서 정의한 프로토콜을 Objective-C에서 사용하기 위함이나, Objective-C 코드와 siwft 코드를 공유하지 않더라도 선택적 protocol을 구현하기 위해서는 @objc 속성을 부여 해야 한다.

더 나아가 @objc 속성을 부여한 프로토콜은 Objective-C의 클래스를 상속받은 클래스만이 해당 프로토콜을 준수 할 수 있다.(보통 Objective-C의 최상위 클래스인 NSObject 클래스를 상속 받는다.)

또한 선택적 요구사항 앞에 optional 키워드를 사용하여 정의 하며 해당 요구사항을 구현하며 선택적 요구사항의 프로퍼티나 메서드는 자동적으로 옵셔널 타입이 된다.

따라서 해당 프로퍼티나 메서드에 접근하기 위해서는 옵셔널 체이닝을 통해 접근 및 호출이 가능하다.

import Foundation

@objc protocol Car {
    func drive()
    @objc optional func driveWithElectricity()    //optional type
}

class GasolineCar: NSObject, Car {
    func drive() {
        print("gasoline car driving")
    }
}

class ElectricityCar: NSObject, Car {
    func drive() {
        print("electricity car driving")
    }
    
    func driveWithElectricity() {
        print("it works with electricity")
    }
}

var myCar: GasolineCar = GasolineCar()
var yourCar: ElectricityCar = ElectricityCar()

myCar.drive()
yourCar.drive()

var car: Car = myCar
car.driveWithElectricity?()    //nil

car = yourCar
car.driveWithElectricity?()
profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글