프로토콜 : 클래스나 구조체가 어떤 기준을 만족하거나 특수한 목적을 달성하기 위해 구현해야 하는 메소드와 프로퍼티의 목록
🚀 명세 : 프로토콜에 선언된 프로퍼티나 메소드의 형식
🚀 프로토콜을 구현한다 : 이 명세에 맞추어 실질적인 내용을 작성하는 것
프로토콜의 정의
protocol <프로토콜명> {
<구현해야 할 프로퍼티 명세 1>
<구현해야 할 프로퍼티 명세 2>
<구현해야 할 프로퍼티 명세 3>
...
<구현해야 할 메소드 명세 1>
<구현해야 할 메소드 명세 2>
<구현해야 할 메소드 명세 3>
...
}
🚀스위프트에서 프로토콜을 구현할 수 있는 구현체들
- 구조체
- 클래스
- 열거형
- 익스텐션
- 프로토콜 구현을 위해 가장 먼저 할 일은 이 프로토콜을 구현하겠다고 선언하는 것이다.
struct / class / enum / extention 객체명 : 구현할 프로토콜명 {
...
}
프로토콜 프로퍼티
프로토콜에 선언되는 프로퍼티에는 초기값을 할당할 수 없다. 연산 프로퍼티인지 저장 프로퍼티인지도 구분하지 않는다. 프로퍼티의 종류, 이름, 변수/상수 구분, 타입, 익기 전용인지 읽고 쓰기가 가능한지에 대해서마 정의할 뿐이다.
protocol SomePropertyProtocol {
var name: String { get set }
var description: String { get }
}
- get : 읽기 전용 속성 → 저장 프로퍼티 구현할 수 없다.
- get set : 읽고 쓸 수 있는 프로퍼티
프로토콜 메소드
protocol SomeMethodProtocol {
func execute(cmd: String)
func showPort(p: Int) -> String
}
- 프로토콜에 선언되는 메소드는 메소드 종류, 이름, 파라미터 타입, 파라미터 이름, 반환 타입까지는 정의할 수 있지만 실제 실행할 내용을 작성할 수는 없다.
- 메소드의 실질적인 내용 작성을 담당하는 것은 구현체, 즉 프로토콜을 구현하는 구조체나 클래스, 열거형, 혹은 익스텐션의 역할이다.
- 내부 매개변수의 경우에는 프로토콜을 그대로 따르지 않고 필요한 대로 변형하여 사용하는 것이 가능하다. 단, 어떤 경우에도 프로토콜에 정의된 메소드명과 외부 파라미터명은 항상 그대로 따라야한다.
프로토콜에서의 mutating, static 사용
프로토콜에서 선언된 메소드라면 mutating
키워드를 붙이기 위해서는 반드시 프로토콜에 mutating 키워드가 추가되어 있어야 한다.
- 클래스같은 참조 타입은
mutating
키워드를 붙이지 않아도 메소드 내에서 마음대로 프로퍼티를 수정할 수 있지만 구조체나 열거형은 프로토콜의 메소드에 mutating 키워드가 추가되어 있지 않을 경우 프로퍼티의 값을 변경할 수 없다. 프로토콜에 선언되지 않은mutating
키워드를 임의로 구현할 수 없기 때문이다.
- 일반적으로 프로토콜에서 메소드 선언에
mutating
키워드가 붙지 않는 것은 다음 두가지 중 하나로 해석한다.- 구조체나 열거형 등 값 타입의 객체에서 내부 프로퍼티의 값을 변경하기를 원치 않을 때
- 주로 클래스를 대상으로 변경하고 작성된 프로토콜일 때
protocol MService {
mutating func execute(cmd: String)
func showPort(p: Int) -> String
}
- 프로토콜에서
mutating
키워드로 선언된 메소드라 하더라도 실제 구현하는 객체에서 필요 없다면 덧붙이지 않아도 된다. 하지만 프로토콜에서mutating
처리되지 않은 메소드를 구조체에서 임의로mutating
처리하는 것은 구현 명세를 위반하는 오류이다.
- 타입 메소드나 타입 프로퍼티도 프로토콜에 정의할 수 있다. 프로토콜의 각 선언 앞에
static
키워드를 붙여주면 된다. 클래스에서 타입 메소드를 선언할 때 사용할 수 있는 또 다른 키워드인class
는 프로토콜에서 사용할 수 없다.
- 하지만 프로토콜에서
static
키워드로 선언되었더라도 실제로 클래스에서 구현할 때는 필요에 따라static
이나class
키워드를 선택하여 사용할 수 있다.
프로토콜과 초기화 메소드
프로토콜에서는 초기화 메소드도 정의할 수 있다. 작성 방식은 앞에서 다루어 본 일반 메소드와 거의 비슷하다. 실행 블록을 작성하지 않고 단순히 이름과 매개변수명, 그리고 매개변수의 타입만 작성하면 된다. 내부 매개변수명을 따로 추가해도 되지만, 구현체에서 강제되는 요소가 아니기 때문에 큰 의미는 없다. 초기화 메소드인 만큼 반환 타입은 없으며 이름은 init으로 통일한다.
protocol SomeInitProtocol {
init()
init(cmd: String)
}
- 구현되는 초기화 메소드의 이름과 매개변수명은 프로토콜의 명세에 작성된 것과 완전히 일치한다.
- 프로토콜 명세에 선언된 초기화 메소드는 그것이 기본 제공되는 초기화 메소드일지라도 직접 구현해야 한다.
- 클래스에서 초기화 메소드를 구현할 때에는 반드시
required
키워드를 붙여야 한다.
- 상속을 통해 초기화 메소드를 물려받았다 할지라도 구현해야 할 프로토콜 명세에 동일한 초기화 메소드가 선언되어 있다면 이를 다시 구현해야 한다.
// init() 메소드를 가지는 프로토콜
protocol Init {
init()
}
// init() 메소드를 가지는 부모 클래스
class Parent {
init() {
}
// 부모 클래스의 init() + 프로토콜의 init()
class Child: Parent, Init {
override required init() {
}
}
[ 부모 클래스와 프로토콜 양쪽으로부터 모두 init()을 물려받을 때 메소드 표현 방법 ]
부모 클래스 init( ) → override + 프로토콜 init( ) → required
→ 자식 클래스 override required init()
- 단일 상속만 허용되는 클래스의 상속 개념과는 달리 객체에서 구현할 수 있는 프로토콜의 개수는 제한이 없다.
타입으로서의 프로토콜
아래의 상황에서 프로토콜을 사용할 수 있다.
- 상수나 변수, 그리고 프로퍼티의 타입으로 사용할 수 있다.
- 함수, 메소드 또는 초기화 구문에서 매개변수 타입이나 반환 타입으로 포로토콜을 사용할 수 있다.
- 배열이나 사전, 혹은 다른 컨테이너의 타입으로 사용할 수 있다.
델리게이션
특정 기능을 다른 객체에 위임하고, 그에 따라 필요한 시점에서 메소드의 호출만 받는 패턴이다.
확장 구문과 프로토콜
익스텐션에서도 기존 객체의 이름 다음에 콜론을 붙이고, 이어서 구현하고자 하는 프로토콜의 이름을 나열하면 된다. 무론 익스텐션의 중괄호 블록에서는 프로토콜에 대한 실질적인 내용 구현이 있어야 한다.
extension <기존 객체> : <구현할 프로토콜1>, <구현할 프로토콜2> ... {
// 프로토콜의 요소에 대한 구현 내용
}
- 주의할 점은 익스텐션에서 저장 프로퍼티를 정의할 수 없다는 점이다. 만약 프로토콜에 정의된 프로퍼티를 익스텐션에서 구현해야 한다면, 이때는 연산 프로퍼티로 구현해주어야 한다.
프로토콜의 상속
프로토콜은 클래스처럼 상속을 통해 정의된 프로퍼티나 메소드, 그리고 초기화 블록의 선언을 다른 포로토콜에 물려줄 수 있다. 하지만 프로토콜은 클래스와 다르게 다중 상속이 가능하다. 즉, 여러 개의 프로토콜을 한번에 상속하여 각 프로토콜들의 명세를 하나의 프로토콜에 담을 수 있다.
protocol A {
func doA()
}
protocol B {
func doB()
}
protocol C: A, B {
func doC()
}
class ABC: C {
func doA() { }
func doB() { }
func doC() { }
}
이렇게 정의된 클래스 ABC는 다음과 같은 타입의 변수/상수에 할당될 수 있다.
선언된 타입에 따라 사용할 수 있는 메소드의 범위는 제한된다.
let abc: C = ABC()
// abc.doA(), abc.doB(), abc.doC()
let ab: A & B = ABC()
// ab.doA(), ab.doB()
let abc2: A & B & C = ABC()
// abc2.doA(), abc2.doB(), abc2.doC()
클래스 ABC는 이와 마찬가지로 다음 타입으로 선언된 함수나 메소드의 인자값으로 할당될 수 있다.
func foo(abc: C) { }
foo(abc: ABC())
func boo(abc: A & B)
boo(abc: ABC())
- 프로토콜을 상속할 때 부모 프로토콜에서의 선언과 자식 프로토콜에서의 선언이 겹치더라도 클래스에서처럼 override 키워드를 붙여야 하는 제약이 없다.
- 상속 관계가 성립된 프로토콜은 is, as 와 같은 타입 연산자들을 사용하여 타입에 대한 비교와 타입 변환을 할 수 있다.
- is 연산자는 주어진 객체를 비교 대상 타입과 비교하여 그 결과를 반환하는데 이 때 선언된 변수나 상수의 타입이 아니라 할당된 실제 객체의 인스턴스를 기준으로 비교한다.
- as 연산자는 제한된 범위 내에서 타입을 캐스팅할 수 있도록 해준다. 제한된 범위는 아래와 같다.
- 실제로 할당된 인스턴스 타입
- 인스턴스가 구현된 프로토콜 타입
- 클래스가 상속을 받았을 경우 모든 상위 클래스
- 프로토콜 타입이 상속을 받았을 경우 모든 상위 프로토콜
- 인스턴스 객체를 할당한 변수나 상수가 있을 때, 이 변수나 상수가 선언된 타입보다 상위 타입으로 캐스팅하는 것은 아무런 문제가 되지 않으므로 일반 캐스팅 연산자는 as를 사용하여 안전하게 캐스팅할 수 있지만, 선언된 타입보다 하위 타입으로 캐스팅할 때는 주의해야 한다. 실제로 할당된 인스턴스 객체에 따라서 캐스팅이 성공할 수도 실패할 수도 있기 때문이다.
- 실제로 할당된 인스턴스 객체의 타입을 기준으로 일치하거나 상위 타입이면 캐스팅이 잘되겠지만 그렇지 않으면 캐스팅에 실패한다. 이는 캐스팅 결과값으로 nil이 반환될 수도 있다는 의미이다.
- 이 떄문에 하위 캐스팅에서는 일반 캐스팅 연산자를 사용하는 대신 옵셔널 타입으로 캐스팅 결과를 반환하는 옵셔널 캐스팅 (=as?) 연산자와 캐스팅 실패 가능성을 감안하고서라도 일반 타입으로 캐스팅하는 강제 캐스팅 (=as!) 연산자 중에서 선택해서 사용해야 한다.
클래스 전용 프로토콜
protocol SomeClasOnlyProtocol: class {
// 클래스에서 구현할 내용 작성
}
- 클래스만 구현할 수 있도록 제한된 프로토콜
- 클래스 전용 프로토콜에서는 메소드를 정의할 때 mutating 키워드를 붙일 수 없다. static 키워드는 사용할 수 있다.
- 만약 프로토콜이 다른 프로토콜을 상속받는다면, 상속된 프로토콜 이름들을 나열하기 전에 맨 먼저 클래스 전용임을 표시해야 한다.
protocol SomeProtocolOnlyProtocol: class, Wheel, Machine {
// 클래스에서 구현할 내용 작성
}
Optional
프로토콜을 정의할 때 선택적 요청을 개별 요소마다 정의할 수 있는데, 이때 optional 키워드를 사용하여 프로퍼티나 메소드, 초기화 구문 앞에 표시한다. 이 키워드가 붙은 요소들은 프로토콜을 구현할 때 반드시 구현하지 않아도 된다는 것을 의미한다.
프로토콜에서 optional 키워드를 사용하려면 약간의 제약이 있따. 프로토콜 앞에 @objc
를 표시해야 한다.
→ 이 어노테이션이 붙은 코드나 객체를 Objective-C 코드에서도 참조할 수 있도록 노출됨을 의미한다.
정리하자면, optional 키워드가 붙은 선택적 요청 프로토콜은 클래스만 구현할 수 있다는 뜻이다. 이런 의미에서 optional 키워드 역시 클래스 전용 프로토콜임을 뜻하는 것이라고 할 수 있다.