: 타입이 따라야 할 규약(약속, 인터페이스)
'이 메서드/프로퍼티는 반드시 구현해야 한다'고 강제하는 문서와 같다.타입이 가져야 할 프로퍼티&메서드를 정의한다.
// 프로토콜 선언
protocol Animal {
var name: String { get } // 읽기 전용 프로퍼티
func makeSound() // 메서드 요구사항
}
// 프로토콜 채택
struct Dog: Animal {
var name: String
func makeSound() {
print("멍멍!")
}
}
struct Cat: Animal {
var name: String
func makeSound() {
print("야옹~")
}
}
서로 다른 구체 타입을 같은 타입처럼 다룰 수 있다.
// 프로토콜을 타입처럼 사용
func greet(_ animal: Animal) {
print("Hello, \(animal.name)")
animal.makeSound()
}
let dog = Dog(name: "바둑이")
let cat = Cat(name: "나비")
greet(dog) // Hello, 바둑이 \n 멍멍!
greet(cat) // Hello, 나비 \n 야옹~
"무엇을 할 수 있다"만 정의하고, "어떻게 한다"는 구현체가 책임진다.
라이브러리/프레임워크 코드를 바꾸지 않고도 새로운 타입이 프로토콜을 채택하면 기능을 연결할 수 있다.
공통 메서드에 대한 기본 구현을 제공할 수 있다.
extension Animal {
func greet() {
print("Hi, I'm \(name)")
}
}
struct Dog: Animal {
let name: String
func makeSound() { print("멍멍") }
}
Dog(name: "바둑이").greet() // "Hi, I'm 바둑이"
여러 프로토콜을 동시에 요구하여 사용할 수 있다.
func describe(_ object: CustomStringConvertible & Equatable) {
print(object.description)
}
제네릭처럼 프로토콜 안에서 타입 파라미터를 선언하여 사용할 수 있다.
protocol Container {
associatedtype Item
func add(_ item: Item)
func getAll() -> [Item]
}
다른 프로토콜을 상속받아 요구사항을 확장할 수 있다.
protocol Readable {
func read()
}
protocol Writable: Readable {
func write()
}
@objc protocol로 선언하면 옵셔널 프로퍼티(구현 필수 ❌, 선택적으로 원하는 경우에만 구현해도 됨)를 선언할 수 있다.@objc protocol TableViewDelegate {
@objc optional func numberOfRows() -> Int
@objc optional func didSelectRow(index: Int)
}
class MyDelegate: TableViewDelegate {
// 둘 중 원하는 것만 구현 가능
func numberOfRows() -> Int { 10 }
}
let delegate: TableViewDelegate = MyDelegate()
delegate.didSelectRow?(index: 2) // ?로 안전하게 호출
: AnyObject 제약이 암묵적으로 추가된 것과 동일하다. (컴파일러 내부적으로 취급하는 방식)➕ Swift스러운 선택적 구현 활용 방법 (@objc optional을 사용하지 않는 방법)
1. 프로토콜 확장 기본 구현
extension에 기본 동작을 넣고, 필요한 타입만 override한다.
2. 옵셔널 클로저 프로퍼티
var onTap: (() -> Void)?같은 콜백을 두고, 필요할 때만 세팅한다.
위의 조건을 만족한다면, 프로그래밍 언어에서 해당 요소를 1급 객체로 간주한다.
Swift에서 프로토콜은 타입으로 취급되기 때문에, 변수 타입, 파라미터 타입, 반환 타입으로 사용이 가능하다.
➡️ 즉, 1급 객체로 동작
프로토콜을 타입으로 사용하여 변수에 담는 것은 해당 프로토콜을 채택한 구조체/클래스/열거형의 인스턴스를 프로토콜로 "타입 캐스팅" 해서 사용한다는 의미이다.
protocol Shape {
func area() -> Double
}
struct Square: Shape {
var length: Double
func area() -> Double { length * length }
}
struct Circle: Shape {
var radius: Double
func area() -> Double { .pi * radius * radius }
}
// 프로토콜 타입 변수
var shape: Shape
shape = Square(length: 5)
print(shape.area()) // 25
shape = Circle(radius: 3)
print(shape.area()) // 28.27...
프로토콜은 자체적으로 인스턴스를 생성할 수 없기 때문에 해당 프로토콜을 준수하는 구조체/클래스/열거형 인스턴스를 "프로토콜 타입으로 캐스팅"해서 전달한다.
func printArea(of shape: Shape) {
print("Area:", shape.area())
}
printArea(Square(length: 4)) // Area: 16
printArea(Circle(radius: 2)) // Area: 12.566...
func makeRandomShape() -> Shape {
Bool.random() ? Square(length: 3) : Circle(radius: 5)
}
let randomShape = makeRandomShape()
print(randomShape.area())
Swift에서 위와 같이 Shape를 변수/인자/반환에 직접 사용하는 것을 존재 타입이라고 한다.
let s: Shape = Square(length: 5) // Shape 타입 변수
print(s.area())
Shape를 만족하는 어떤 것일 뿐, 정확히 무슨 타입인지 모름.let shapes: [Shape] = [Square(length: 5), Circle(radius: 3)]
for shape in shapes { print(shape.area()) }
func doubleArea<T: Shape>(_ shape: T) -> Double {
shape.area() * 2
}
func makeShape() -> some Shape {
Square(length: 10)
}
Shape를 반환한다는 것만 알 수 있음.위 세 가지 경우의 차이를 이해하고 적절한 상황에 적용해야 한다.
| 방식 | 타입 결정 시점 | 내부 동작 | 장점 | 단점 |
|---|---|---|---|---|
Shape (Existential) | 런타임 | Existential Container 생성 + 동적 디스패치 | 다양한 타입을 한 컨테이너에 담기 가능 | 런타임 오버헤드(성능 손해) |
<T: Shape> (Generic) | 컴파일 타임 | 구체 타입별로 코드가 생성됨 | 성능 최적화, 강한 타입 안정성 | 서로 다른 타입을 한 배열에 담기 어려움 |
some Shape (Opaque) | 컴파일 타임 | 구체 타입 확정, 외부에는 숨김 | 성능 최적화 + API 캡슐화 | 반환 타입은 하나로 고정해야 함 |