프로토콜

Gooreum·2021년 11월 11일
0

Swift

목록 보기
14/16
post-custom-banner

프로토콜이란

  • 프로토콜은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의한다.
  • 어떤 프로토콜의 요구사항을 모두 따르는 타입은 '해당 프로토콜을 준수한다'고 표현한다.
  • 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 한다.
  • 즉, 프로토콜은 정의를 하고 제시를 할 뿐이지 스스로 기능을 구현하지는 않는다.
  • protocol 키워드를 사용하여 정의하며, 프로토콜을 채택하는 클래스, 열거형, 구조체는 자신의 이름 뒤에 콜론(:)을 붙여준 후 채택할 프로토콜 이름을 쉼표(,)로 구분하여 명시해준다.

프로토콜 요구사항

  • 프로토콜이 자신을 채택한 타입에 요구하는 사항은 프로퍼티나 메서드와 같은 기능들이다.

(1) 프로퍼티 요구

  • 프로토콜은 자신을 채택한 타입이 어떤 프로퍼티를 구현해야 하는지 요구할 수 있다.
  • 그렇지만 프로퍼티의 종류(연산 / 저장)는 따로 신경쓰지 않는다.
  • 프로토콜을 채택한 타입은 프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞도록 구현해주면 된다.
  • 다만 프로퍼티를 읽기 전용으로 할지 혹은 읽고 쓰기가 모두 가능하게 할지는 프로토콜이 정해야 한다.
  • 만약 프로토콜이 읽고 쓰기가 가능한 프로퍼티를 요구한다면 읽기만 가능한 상수 저장 프로퍼티 또는 읽기 전용 연산 프로퍼티를 구현할 수 없다.
  • 만약 프로토콜이 읽기 가능한 프로퍼티를 요구한다면 타입에 프로퍼티를 구현할 때 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티를 포함해서 어떤 식으로든 프로퍼티를 구현할 수 있다.
  • 쓰기만 가능한 프로퍼티는 없으니 타입에 구현해주는 프로퍼티는 무엇이 되어도 상관없다.
  • 프로토콜의 프로퍼티 요구사항은 항상 var 키워드를 사용한 변수 프로퍼티로 정의해야 한다.
  • 타입 프로퍼티를 요구하려면 static 키워드를 사용한다.
    • 클래스의 타입 프로퍼티에는 상속 가능한 프로퍼티인 class 타입 프로퍼티와 상속 불가능한 static 타입 프로퍼티가 있지만 이 두 타입 프로퍼티를 따로 구분하지 않고 모두 static 키워드를 사용하여 타입 프로퍼티를 요구하면 된다.

(2) 메서드 요구

  • 프로토콜이 요구할 메서드는 프로토콜 정의에서 작성하며, 특정 인스턴스 메서드나 타입 메서드를 요구할 수도 있다.
  • 다만 메서드의 실제 구현부인 중괄호 {} 부분은 제외하고 메서드의 이름 매개변수 반환 타입 등만 작성하며 가변 매개변수도 허용한다.
  • 프로토콜의 메서드 요구에서는 매개변수 기본값을 지정할 수 없다.
  • 타입 메서드를 요구할 때는 타입 프로퍼티 요구와 마찬가지로 앞에 static 키워드를 명시한다.
    • static 키워드를 사용하여 요구한 타입 메서드를 클래스에서 실제 구현할 때는 static 키워드나 class 키워드 어느 쪽을 사용해도 무방하다.

💡 타입으로서의 프로토콜
프로토콜은 요구만 하고 스스로 기능을 구현하지는 않는다. 그렇지만 프로토콜은 코드에서 완전한 하나의 타입으로 사용되기에 여러 위치에서 프로토콜을 타입으로 사용할 수 있다.

  • 함수, 메서드, 이니셜라이저에서 매개변수 타입이나 반환 타입으로 사용될 수 있다.
  • 프로퍼티, 변수, 상수 등의 타입으로 사용될 수 있다.
  • 배열, 딕셔너리 등 컨테이너 요소의 타입으로 사용될 수 있다.

(3) 가변 메서드 요구

  • 값 타입(구조체와 열거형)의 인스턴스 메서드에서 자신 내부의 값을 변경하고자 할 때는 메서드의 func 키워드 앞에 mutating 키워드를 적어 메서드에서 인스턴스 내부의 값을 변경한다.
  • 프로토콜이 어떤 타입이든 간에 인스턴스 내부의 값을 변경해야 하는 메서드를 요구하려면 프로토콜의 메서드 정의 앞에 mutating 키워드를 명시해야 한다.
  • 참조 타입인 클래스의 메서드라면 명시하지 않아도 상관 없지만 값 타입인 구조체와 열거형의 메서드 앞에는 mutating 키워드를 붙인 가변 메서드 요구가 필요하다.

(4) 이니셜라이저 요구

  • 프로토콜은 이니셜라이저를 요구할 수 있다.
  • 물론 프로토콜 내에서 정의만 하지 중괄호를 포함한 이니셜라이저 구현은 하지 않는다.
  • 구조체는 상속을 할 수 없기 때문에 이니셜라이저 요구에 대해 크게 신경쓸 필요가 없지만 클래스의 경우라면 조금 다르다.
    • 클래스 타입에서 프로토콜의 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 이니셜라이저가 지정 이니셜라이저인지 편의 이니셜라이저인지는 중요하지 않다.
    • 그러나 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 required 식별자를 붙인 요구 이니셜라이저로 구현해야 한다.
    • 만약 클래스 자체가 상속받을 수 없는 final 클래스라면 required 식별자를 붙여줄 필요 없다.
protocol Named { 
	var name: String { get }
	init(name: String)
}

class Person: Named { 
	var name: String
	
	required init(name: String) { 
		self.name = name 
	}
}
  • 만약 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있는 상황에서 그 클래스를 상속받은 클래스가 있다면, requiredoverride 식별자를 모두 명시하여 프로토콜에서 요구하는 이니셜라이저를 구현해주어야 한다. (순서는 상관없다.)
class School { 
	var name: String
	init(name: String) { 
		self.name = name
	}
}

class MiddleSchool: School, Named { 
		required override init(name: String) {
			super.init(name: name)
		}
}
  • 프로토콜은 실패가능한 이니셜라이저를 요구할 수도 있다.
    • 해당 프로토콜을 준수하는 타입은 해당 이니셜라이저를 구현할 때 실패 가능한 이니셜라이저로 구현해도, 일반적인 이니셜라이저로 구현해도 무방하다.

프로토콜의 상속과 클래스 전용 프로토콜

  • 프로토콜의 상속 리스트에 class 키워드를 추가해 프로토콜이 클래스 타입에만 채택될 수 있도록 제한할 수도 있다.
  • 클래스 전용 프로토콜로 제한을 주기 위해서는 프로토콜의 상속 리스트의 맨 처음에 class 키워드가 위치해야 한다.
protocol ClassOnlyProtocol: class, Readable, Writable { 
	//추가 요구사항
}

프로토콜 조합과 프로토콜 준수 확인

  • 하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 한다면 하나의 매개변수에 여러 프로토콜을 한 번에 조합하여 요구할 수 있다.
  • 프로토콜을 조합하여 요구할 때는 SomeProtocol & AnotherProtocol과 같이 표현한다.
  • 또 하나의 매개변수가 프로토콜을 둘 이상 요구할 수도 있다.
    • 이때도 마찬가지로 앰퍼샌드(&)를 여러 프로토콜 이름 사이에 써주면 된다.
  • 더불어 특정 클래스의 인스턴스 역할을 할 수 있는지 함께 확인할 수 있다.
  • 구조체나 열거형 타입은 조합할 수 없고, 조합 중 클래스 타입은 한 타입만 조합할 수 있다.
protocol Named { 
	var name: String { get }
}

protocol Aged { 
	var age: Int { get }
}

struct Person: Named, Age {
	var name: String
	var age: Int
}

class Car: Named {
	var name: String 
	init(name: String) { 
		self.name = name
	}
}

class Truck: Car, Aged { 
	var age: Int
	init(name: String, age: Int) { 
		self.age = age
		super.init(name: name)
	}
}

func celebrateBirthday(to celebrator: Named & Aged) { 
	print("Happy birthday \(celebrator.name)!! Now you are \(celebrator.age)")
}

let yagom: Person = Person(name: "yagom", age: 90)
celebrateBirthday(to: yagom) // Happy birthday yagom!! Now you are 90

let myCar: Car = Car(name: "Boong Boong")
//celebrateBirthday(to: myCar) //Aged를 충족하지 못하므로 오류발생

//Car 클래스의 인스턴스 역할도 수행할 수 있고,
//Aged 프로토콜을 준수하는 인스턴스만 할당할 수 있다.
var someVariable: Car & Aged

//Truck 인스턴스는 Car 클래스 역할도 할 수 있고 Aged 프로토콜도 준수하므로 할당할 수 있다.
someVariable = Truck(name: "Truck", age: 5)

//Car의 인스턴스인 myCar는 Aged 프로토콜을 준수하지 않으므로 할당할 수 없다.
//오류발생!
//someVariable = myCar
  • 타입캐스팅에 사용하던 is와 as 연산자를 통해 대상이 프로토콜을 준수하는지 확인할 수도 있고, 특정 프로토콜로 캐스팅할 수 있다.
    • is 연산자를 통해 해당 인스턴스가 특정 프로토콜을 준수하는지 확인 가능.
    • as? 다운캐스팅 연산자를 통해 다른 프로토콜로 다운캐스팅을 시도할 수 있다.
    • as! 다운캐스팅 연산자를 통해 다른 프로토콜로 강제 다운캐스팅 할 수 있다.
print(yagom is Named) // True
print(yagom is Aged) // True

print(myCar is Named) // True
print(myCar is Aged) // False

if let castedInstance: Named = yagom as? Named { 
	print("\(castedInstance) is Named")
} //Person is Named

프로토콜의 선택적 요구

  • objc 속성이 부여된 프로토콜은 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있다.
  • objc 속성이 부여되는 프로토콜은 Objective-C 클래스(NSObject)를 상속받은 클래스에서만 채택할 수 있다. 즉, 열거형이나 구조체 등에서는 objc 속성이 부여된 프로토콜은 아예 채택이 불가하다.
  • objc 속성을 사용하려면 Foundation 프레임워크 모듈을 임포트해야 한다.
  • 선택적 요구사항은 optional 식별자를 요구사항의 정의 앞에 붙여주면 된다.
    • 만약 메서드나 프로퍼티를 선택적 요구사항으로 요구하게 되면 그 요구사항의 타입은 자동적으로 옵셔널이 된다.
    • 메서드의 매개변수나 반환 타입이 옵셔널이 된 것이 아니라 메서드(함수) 자체의 타입이 옵셔널이 된 것이라는 점을 놓치지 말자!
  • 선택적 요구사항은 그 프로토콜을 준수하는 타입에 구현되어 있지 않을 수 있기 때문에 옵셔널 체이닝을 통해 호출할 수 있다.
@objc protocol Moveable {
    func walk()
    @objc optional func fly()
}

class Tiger: NSObject, Moveable {
    func walk() {
        print("Tiger walks")
    }
}

class Bird: NSObject, Moveable {
    func walk() {
        print("Bird walks")
    }
    
    func fly() {
        print("Bird flys")
    }
}

let tiger: Tiger = Tiger()
let bird: Bird = Bird()

tiger.walk()
bird.walk()

bird.fly()

var moveableInstance: Moveable = tiger
moveableInstance.fly?() //응답 없음

moveableInstance = bird
moveableInstance.fly?()

--result--
Tiger walks
Bird walks
Bird flys
Bird flys
  • Moveable 프로토콜 변수에 할당된 moveableInstance는 인스턴스의 타입에 실제로 fly() 메서드가 구현되어 있는지 알 수 없으므로 옵셔널 체인을 이용하여 fly() 메서드 호출을 시도해본다. 옵셔널 체인을 사용할 때는 메서드 이름 뒤에 물음표를 붙여 표현한다.

프로토콜 변수와 상수

  • 프로토콜은 자기 스스로 인스턴스를 생성하고 초기화 할 수는 없지만, 프로토콜 변수나 상수를 생성하여 특정 프로토콜을 준수하는 타입의 인스턴스를 할당할 수는 있다.

위임을 위한 프로토콜

  • 위임(Delegation)은 클래스나 구조체가 자신의 책임이나 임무를 다른 타입의 인스턴스에게 위임하는 디자인 패턴이다.
  • 위임은 사용자의 특정 행동에 반응하기 위해 사용되기도 하며, 비동기 처리에도 많이 사용한다.
  • 위임 패턴(Delegation Pattern)은 애플의 프레임워크에서 사용하는 주요한 패턴 중 하나이다. ex) UITableViewDelegate는 UITableView의 인스턴스가 해야 하는 일을 대신 처리해줄 수 있다.
profile
하루하루 꾸준히
post-custom-banner

0개의 댓글