본 글은 야곰의 스위프트 프로그래밍: Swift5 교재를 토대로 공부한 내용과 찾아본 내용을 요약한 것입니다.
스위프트에서 프로토콜 지향 프로그래밍이 큰 화두로 떠올랐습니다.
스위프트에서 프로토콜은 특정 작업이나 기능을 수행하기 위한 메서드, 프로퍼티 및 다른 요구사항의 청사진(blueprint)입니다. 프로토콜은 추상화를 통해 객체 간의 통신을 정의하고, 코드의 유연성과 재사용성을 증가시키는데 사용됩니다. 어떤 프로토콜의 요구사항을 모두 따르는 타입은 '해당 프로토콜을 준수한다'고 표현하며 프로토콜은 정의를 하고 제시를 할 뿐이지 스스로 기능을 구현하지는 않습니다.
프로토콜은 구조체, 클랫, 열거형의 모양과 비슷하게 정의할 수 있으며 protocol 키워드를 사용합니다.
// 프로토콜 정의
protocol Animal {
func makeSound()
}
// 클래스가 프로토콜을 채택
class Dog: Animal {
func makeSound() {
print("멍멍")
}
}
// 구조체가 프로토콜을 채택
struct Cat: Animal {
func makeSound() {
print("야옹")
}
}
// 열거형이 프로토콜을 채택
enum Bird: Animal {
case crow
case sparrow
func makeSound() {
switch self {
case .crow:
print("까악")
case .sparrow:
print("짹짹")
}
}
}
// 채택된 타입의 인스턴스 생성 및 메서드 호출
let dog = Dog()
let cat = Cat()
let crow = Bird.crow
dog.makeSound() // 출력: 멍멍
cat.makeSound() // 출력: 야옹
crow.makeSound() // 출력: 까악
위 예시에서 Dog, Cat, Bird 클래스 및 구조체는 모두 Animal 프로토콜을 채택하고 있습니다. 각각의 인스턴스는 makeSound() 메서드를 구현하고 있으며, 이를 호출하여 각각의 동물이 나는 소리를 출력할 수 있습니다.
프로토콜은 프로퍼티 요구, 메서드 요구, 가변 메서드 요구 및 이니셜라이저 요구와 같은 다양한 종류의 요구사항을 정의할 수 있습니다. 이들 각각을 설명하고 예시를 제시하겠습니다.
프로토콜에서 특정 프로퍼티를 요구할 수 있습니다. 프로퍼티 요구는 gettable 프로퍼티(읽기 전용) 또는 gettable 및 settable 프로퍼티(읽기/쓰기 모두 가능)로 선언될 수 있습니다.
protocol Vehicle {
var numberOfWheels: Int { get }
var color: String { get set }
}
프로토콜은 특정 메서드를 요구할 수 있습니다. 이러한 메서드는 구현을 포함하지 않고 단지 시그니처만을 선언합니다. 프로토콜을 채택하는 타입은 요구된 메서드를 반드시 구현해야 합니다.
protocol Drawable {
func draw()
}
프로토콜은 값 형식(구조체 또는 열거형)에서 상태를 변경하는 메서드를 요구할 때 mutating 키워드를 사용합니다.
protocol Vehicle {
mutating func accelerate()
}
프로토콜은 특정 이니셜라이저를 요구할 수 있습니다. 요구된 이니셜라이저는 클래스에서만 구현될 수 있으며, 반드시 required 키워드를 사용하여 필수로 구현되어야 합니다.
protocol FullyNamed {
init(firstName: String, lastName: String)
}
프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있습니다.
스위프트에서는 프로토콜도 클래스처럼 다른 프로토콜로부터 상속받을 수 있습니다. 상속을 받는 프로토콜은 부모 프로토콜에 정의된 모든 요구사항을 상속받게 됩니다. 이를 통해 코드의 중복을 피하고 프로토콜 간의 계층 구조를 만들 수 있습니다.
protocol Animal {
func speak()
}
protocol Pet: Animal {
func play()
}
class SomeClass: Pet {
func speak() {
print("Speak")
}
func play() {
print("play")
}
}
클래스 전용 프로토콜은 클래스 타입에서만 프로토콜을 채택할 수 있도록 지정하는 것입니다. 이를 위해 AnyObject 프로토콜을 상속받습니다. 클래스 전용 프로토콜은 주로 클래스에서만 사용될 수 있는 기능을 정의할 때 유용합니다.
protocol Drawable: AnyObject {
func draw()
}
위의 예시에서 Pet 프로토콜은 Animal 프로토콜을 상속받고 있습니다. 이는 Pet 프로토콜을 채택하는 타입은 speak() 메서드를 구현해야 함을 의미합니다. 또한, Drawable 프로토콜은 AnyObject를 상속받고 있으므로 클래스 전용 프로토콜이 되며, 이를 채택하는 타입은 클래스여야 합니다.
프로토콜 조합(Protocol Composition)은 두 개 이상의 프로토콜을 결합하여 새로운 프로토콜을 생성하는 것을 말합니다. 프로토콜 조합을 사용하면 하나의 타입이 여러 프로토콜을 동시에 준수하도록 지정할 수 있습니다. 이는 다형성을 더욱 유연하게 활용하고 코드의 재사용성을 높일 수 있습니다.
프로토콜 조합은 & 연산자를 사용하여 수행됩니다. 이 연산자는 여러 프로토콜을 결합하여 새로운 프로토콜을 생성합니다. 결합된 프로토콜을 채택하는 타입은 모든 프로토콜의 요구사항을 모두 충족해야 합니다.
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
// 아래와 같이 여러개의 프로토콜을 준수할 수 있음
struct Person: Named, Aged {
var name: String
var age: Int
}
protocol Drawable {
func draw()
}
protocol Soundable {
func makeSound()
}
// 프로토콜 조합은 & 연산자를 사용하여 수행
// Drawable과 Soundable 프로토콜을 조합한 새로운 프로토콜
typealias DrawableAndSoundable = Drawable & Soundable
// Animal은 Drawable과 Soundable 프로토콜을 모두 준수하는 프로토콜
protocol Animal: DrawableAndSoundable { }
// Dog 타입은 Animal 프로토콜을 준수함으로써 Drawable과 Soundable 프로토콜을 모두 준수함
class Dog: Animal {
func draw() {
print("Drawing a dog")
}
func makeSound() {
print("Barking")
}
}
타입캐스팅에 사용하던 is와 as 연산자를 통해 대상이 프로토콜을 준수하는지 확인할 수도 있고, 특정 프로토콜로 캐스팅할 수 있습니다.
// is 키워드는 객체가 특정 클래스의 인스턴스인지 또는 특정 프로토콜을 준수하는지 여부를 확인하는데 사용됩니다. 결과는 불리언(Boolean) 값으로 반환됩니다.
protocol Drawable {
func draw()
}
class Circle: Drawable {
func draw() {
print("Drawing a circle")
}
}
let circle = Circle()
if circle is Drawable {
print("Circle is drawable")
} else {
print("Circle is not drawable")
}
// as 키워드는 타입 캐스팅을 수행합니다. 객체를 다른 타입으로 캐스팅하거나, 다운캐스팅을 수행할 때 사용됩니다. 프로토콜을 채택한 타입으로 캐스팅할 때 사용할 수 있습니다.
protocol Drawable {
func draw()
}
class Circle: Drawable {
func draw() {
print("Drawing a circle")
}
}
let circle = Circle()
if let drawableCircle = circle as? Drawable {
drawableCircle.draw()
}
프로토콜의 선택적 요구(Optional Requirements)는 특정 프로토콜을 채택한 타입이 해당 요구사항을 구현할지 여부를 선택할 수 있도록 하는 기능입니다. 선택적 요구사항을 가진 프로토콜은 @objc 속성을 가져야 하며, Objective-C와의 상호 운용성을 위해 사용됩니다.
선택적 요구사항은 프로토콜 내부의 메서드나 프로퍼티에 @objc 키워드를 사용하여 선언됩니다. 또한 이러한 요구사항을 채택하는 타입에서는 필요한 경우 해당 요구사항을 구현할 수 있습니다.
@objc protocol Greetable {
@objc optional func greet()
}
class Person: Greetable {
func greet() {
print("Hello")
}
}
class Animal: Greetable {
// greet() 메서드를 구현하지 않음
}
let person = Person()
person.greet() // 출력: Hello
let animal = Animal()
animal.greet() // 아무런 동작도 하지 않음
위의 예시에서 Greetable 프로토콜은 @objc optional 키워드를 사용하여 greet() 메서드를 선택적 요구사항으로 선언합니다. 그리고 Person 클래스에서는 greet() 메서드를 구현하고 있지만, Animal 클래스에서는 구현하지 않았습니다. 이는 선택적 요구사항의 특징으로, 해당 메서드를 구현하지 않아도 되는 것을 보여줍니다.
프로토콜 변수와 상수는 스위프트에서 프로토콜을 사용하여 선언된 요구사항을 채택하는 인스턴스를 저장하는 데 사용됩니다. 이러한 변수와 상수는 해당 프로토콜을 준수하는 어떤 타입의 인스턴스도 저장할 수 있습니다. 이를 통해 코드의 유연성을 높이고 다형성을 구현할 수 있습니다.
protocol Drawable {
func draw()
}
class Circle: Drawable {
func draw() {
print("Drawing a circle")
}
}
class Square: Drawable {
func draw() {
print("Drawing a square")
}
}
// Drawable 프로토콜을 준수하는 어떤 타입의 인스턴스도 저장할 수 있는 변수
var shape: Drawable
// Circle 인스턴스를 저장
shape = Circle()
shape.draw() // 출력: Drawing a circle
// Square 인스턴스를 저장
shape = Square()
shape.draw() // 출력: Drawing a square
// 프로토콜 변수에는 해당 프로토콜을 준수하는 어떤 타입의 인스턴스도 저장할 수 있음
위의 예시에서 Drawable 프로토콜을 채택한 Circle 및 Square 클래스를 정의하고 있습니다. 그리고 Drawable 프로토콜을 준수하는 어떤 타입의 인스턴스도 저장할 수 있는 shape 변수를 선언하고 있습니다. shape 변수에는 Circle 또는 Square 인스턴스를 저장할 수 있으며, 각각의 경우에 해당하는 draw() 메서드가 호출됩니다.
프로토콜 변수와 상수를 사용하면 타입에 상관없이 해당 프로토콜을 준수하는 어떤 타입의 인스턴스도 저장할 수 있으므로, 코드의 유연성을 높이고 다형성을 구현할 수 있습니다.
위임을 위한 프로토콜은 한 객체가 다른 객체에게 특정 동작 또는 데이터 처리를 위임할 수 있도록 하는 디자인 패턴입니다. 이 패턴은 객체 간의 결합도를 낮추고 코드 재사용성을 높이는 데 도움이 됩니다.
위임을 위한 프로토콜은 보통 해당 동작이나 데이터 처리를 처리할 수 있는 메서드를 정의합니다. 이 메서드들은 일반적으로 프로토콜을 채택한 객체에 의해 구현되며, 이후에 위임을 받는 객체에서 호출됩니다.
위임을 위한 프로토콜을 사용하는 가장 일반적인 예시는 델리게이트(Delegate) 패턴입니다. 델리게이트 패턴에서는 주로 프로토콜을 사용하여 다음과 같은 작업을 처리합니다:
델리게이트 패턴은 보통 다음과 같은 구성 요소로 이루어집니다.
델리게이트 패턴(Delegation Pattern)을 사용하면 객체 간의 결합도를 낮추고 코드의 모듈화 및 재사용성을 높일 수 있으며, 객체의 역할을 분리하여 코드를 더욱 관리하기 쉽게 만듭니다.