iOS/Common

[Swift] protocol에 관하여

TDCIAN 2023. 4. 10. 16:19

교보문고 링크 걸어놨습니다!

 

 

야곰, ⟪스위프트 프로그래밍: Swift 5

p.371

프로토콜이란

프로토콜(protocol)은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의합니다. 구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 실행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있습니다. 어떤 프로토콜의 요구사항을 모두 따르는 타입은 '해당 프로토콜을 준수(Conform)한다'고 표현합니다. 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 합니다. 즉, 프로토콜은 정의를 하고 제시를 할 뿐이지 스스로 기능을 구현하지는 않습니다.

 

 

 

 

Why always me, human?

 

Q. Swift에서 protocol의 정의와 특징을 설명해 줘

 

A. Swift에서 protocol은 메서드, 속성 및 기타 기능을 나열하는 일종의 추상 인터페이스입니다.

이러한 프로토콜을 정의함으로써 클래스, 구조체 또는 열거형에서 일관성 있는 기능을 제공할 수 있습니다.

 

프로토콜의 주요 특징은 다음과 같습니다.

 

1. 추상 인터페이스

- 프로토콜은 실제 구현을 갖지 않는 추상 인터페이스입니다. 대신, 프로토콜을 준수하는 클래스, 구조체 또는 열거형에서 해당 기능을 구현해야 합니다.

 

다음은 Animal이라는 프로토콜의 예시입니다. Animal 프로토콜은 makeSound라는 메서드를 선언하고 있습니다.

이 프로토콜을 준수하는 클래스나 구조체에서는 이 메서드를 반드시 구현해야 합니다.

protocol Animal {
	func makeSound()
}

class Dog: Animal {
	func makeSound() {
    	print("멍멍")
    }
}

 

 

2. 다중 상속

- 클래스는 일반적으로 하나의 상위 클래스만 상속할 수 있지만, 프로토콜은 여러 개를 동시에 채택할 수 있습니다. 이를 통해 클래스나 구조체가 여러 프로토콜에서 요구하는 기능을 모두 구현할 수 있습니다.

 

다음은 CanFly와 CanSwim이라는 두 개의 프로토콜을 채택하는 Bird라는 클래스의 예시입니다.

Bird 클래스에서는 이 두 프로토콜에서 요구하는 기능을 모두 구현해야 합니다.

protocol CanFly {
	func fly()
}

protocol CanSwim {
	func swim()
}

class Bird: CanFly, CanSwim {
	func fly() {
    	print("날다")
    }
    
    func swim() {
    	print("헤엄치다")
    }
}

 

 

3. 선택적 구현

- 프로토콜은 필수 구현과 선택적 구현을 나눌 수 있습니다. 필수 구현은 반드시 구현되어야 하지만, 선택적 구현은 구현해도 되고 구현하지 않아도 됩니다.

 

야곰, 스위프트 프로그래밍: Swift 5

p.388 프로토콜의 선택적 요구 (ChatGPT에서 알려준 예시가 잘못되어 책으로 갈음합니다)

 

프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있습니다. 다만 먼저 고려해야 할 사항이 있습니다.

선택적 요구사항을 정의하고 싶은 프로토콜은 objc 속성이 부여된 프로토콜이어야 합니다.

objc 속성은 해당 프로토콜을 Objective-C 코드에서 사용할 수 있도록 만드는 역할을 합니다.

그렇지만 해당 프로토콜을 Objective-C 코드와 공유하고 싶지 않더라도, 혹은 프로젝트를 Objective-C 코드와 공유하지 않더라도 objc 속성이 부여되어야만 선택적 요구사항을 정의할 수 있습니다.

 

여기서 더 생각해보아야 할 것은 objc 속성이 부여되는 프로토콜은 Objective-C 클래스를 상속받은 클래스에서만 채택할 수 있다는 것입니다. 즉, 열거형이나 구조체 등에서는 objc 속성이 부여된 프로토콜은 아예 채택할 수 없습니다.

 

선택적 요구를 하면 프로토콜을 준수하는 타입에 해당 요구사항을 필수로 구현할 필요가 없습니다. 선택적 요구사항은 optional 식별자를 요구사항의 정의 앞에 붙여주면 됩니다. 만약 메서드나 프로퍼티를 선택적 요구사항으로 요구하게 되면 그 요구사항의 타입은 자동적으로 옵셔널이 됩니다. 예를 들어 (Int) -> String 타입의 메서드는 ((Int) -> String)? 타입이 됩니다. 메서드의 매개변수나 반환 타입이 옵셔널이 된 것이 아니라 메서드(함수) 자체의 타입이 옵셔널이 된 것이라는 점을 놓치지 마세요!

 

선택적 요구사항은 그 프로토콜을 준수하는 타입에 구현되어 있지 않을 수 있기 때문에 옵셔널 체이닝을 통해 호출할 수 있습니다.

프로퍼티뿐만 아니라 메서드도 마찬가지입니다.

 

프로토콜의 선택적 요구

import Foundation

@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() // Tiger walks
bird.walk() // Bird walks
bird.fly() // Bird flys

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

moveableInstance = bird
moveableInstacne.fly?() // Bird flys

 

 

 

4. 제네릭

- 프로토콜은 제네릭으로 정의할 수 있습니다. 이를 통해 동일한 프로토콜을 사용하면서도 다양한 유형의 데이터를 처리할 수 있습니다.

 

다음은 Stack이라는 프로토콜의 예시입니다. Stack 프로토콜은 제네릭으로 정의되어 있습니다.

Stack 프로토콜을 준수하는 모든 유형은 제네릭 타입으로 요소의 유형을 정의할 수 있습니다.

protocol Stack {
	associatedtype Element
    mutating func push(_ element: Element)
    mutating func pop() -> Element?
}

struct InsStack: Stack {
	typealias Element = Int
    
    private var elements = [Element]()
    
    mutating func push(_ element: Element) {
    	elements.append(element)
    }
    
    mutating func pop() -> Element? {
    	return elements.popLast()
    }
}

 

 

 

5. 익스텐션

- 프로토콜은 익스텐션을 사용하여 기본 구현을 제공할 수 있습니다. 이를 통해 프로토콜을 준수하는 모든 유형에서 공통된 동작을 구현할 수 있습니다.

 

다음은 Printable이라는 프로토콜의 예시입니다. Printable 프로토콜은 description이라는 메서드를 선언하고 있습니다.

Printable 프로토콜을 준수하는 모든 유형에서는 이 메서드를 구현해야 합니다.

하지만, 이 메서드는 이미 String, Int 등의 내장된 Swift 유형에서 구현되어 있습니다.

따라서 Printable 프로토콜을 준수하는 유형에서 이미 구현된 메서드를 다시 구현할 필요가 없습니다.

대신, 익스텐션을 사용하여 이러한 유형에서 description 메서드를 사용할 수 있도록 확장할 수 있습니다.

 

protocol Printable {
	func description() -> String
}

extention Int: Printable {
	func description() -> String {
    	return "\(self)"
    }
}

print(123.description()) // 출력 결과: "123"

위 예시에서 Int 타입에 Printable 프로토콜을 익스텐션으로 추가하여 description 메서드를 구현하였습니다.

이제 Int 타입에서도 description 메서드를 사용할 수 있게 되었습니다. 출력 결과는 "123"이 됩니다.

 

프로토콜은 Swift에서 매우 중요한 개념 중 하나이며, 코드의 재사용성과 유지보수성을 향상시키는 데 큰 역할을 합니다.

 

 

 

 

Q. Swift에서 protocol을 사용했을 때 특별한 장점이 있어?

 

A. Swift에서 protocol을 사용하는 것에는 다음과 같은 장점이 있습니다.

 

1. 다형성 구현

protocol은 다형성(polymorphism)을 구현하기 위해 사용됩니다.

protocol을 준수하는 여러 개의 클래스나 구조체가 있을 때, 동일한 protocol 타입으로 이들을 모두 다 다룰 수 있습니다.

이는 코드의 유연성을 높이고, 유지보수를 쉽게 만들어 줍니다.

 

protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    var width: Double
    var height: Double
    
    func area() -> Double {
    	return width * height
    }
}

class Circle: Shape {
    var radius: Double
    
    init(radius: Double) {
    	self.radius = radius
    }
    
    func area() -> Double {
    	return Double.pi * radius * radius
    }
}

위 코드에서 Shape 프로토콜은 면적을 구하는 area() 메서드를 가지고 있습니다. Rectangle과 Circle 구조체는 Shape 프로토콜을 준수하며, 각각의 면적을 구하는 area() 메서드를 구현하고 있습니다.

 

이제 다음과 같이 Shape 타입으로 이들을 다룰 수 있습니다.

let rectangle: Shape = Rectangle(width: 10, height: 20)
let circle: Shape = Circle(radius: 5)

print(rectangle.area()) // 출력 결과: 200
print(circle.area()) // 출력 결과: 78.53981633974483

Shape 타입으로 이들을 다룰 수 있으므로, 다형성을 구현할 수 있습니다.

 

 

 

2. 코드 재사용

protocol을 사용하여 유형 간에 공통된 메서드나 속성을 추상화하고, 재사용 가능한 코드를 작성할 수 있습니다.

이는 개발 생산성을 높이고, 중복 코드를 제거하여 코드의 품질을 향상시킵니다.

 

protocol Geetable {
    var name: String { get }
    func greet() -> String
}

extension Greetable {
	func greet() -> String {
    	return "Hello, \(name)!"
    }
}

위 코드에서 Greetable 프로토콜은 name 속성과 greet() 메서드를 가지고 있습니다. 이들은 이름을 가진 객체를 인사하는 기능을 구현하기 위한 것입니다. Greetable 프로토콜을 준수하는 구조체나 클래스에서는 name 속성을 구현해주어야 합니다. greet() 메서드는 기본 구현이 포함되어 있으므로, 구조체나 클래스에서 구현할 필요가 없습니다.

 

이제 다음과 같이 Greetable 프로토콜을 준수하는 구조체를 만들 수 있습니다.

struct Person: Greetable {
    var name: String
}

let person = Person(name: "Swift")

print(person.greet()) // 출력 결과: "Hello, Swift!"

Person 구조체는 Greetable 프로토콜을 준수하므로, name 속성과 greet() 메서드를 구현해주어야 합니다.

name 속성은 Person 구조체에서 구현되어 있습니다.

greet() 메서드는 Greetable 프로토콜에서 기본으로 구현된 greet() 메서드를 사용하였습니다. 이것은 Greetable 프로토콜을 준수하는 모든 구조체에서 사용 가능한 메서드입니다. 따라서, 코드 재사용성이 증가하였습니다.

 

 

3. 확장성

protocol을 사용하여 새로운 유형이나 기능을 추가하는 것이 쉬워집니다.

프로토콜을 확장하거나 새로운 프로토콜을 작성하여 기존 코드에 영향을 미치지 않고도 새로운 기능을 추가할 수 있습니다.

 

protocol Printable {
    var printableDescription: String { get }
}

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

extension Person: Printable {
    var printableDescription: String {
        return "Name: \(name), Age: \(age)"
    }
}

위 코드에서, Printable 프로토콜은 printableDescription 속성을 선언합니다. Person 타입은 Printable 프로토콜을 준수하기 위해 확장(extension)됩니다. 이제 Person 타입은 printableDescription 속성을 갖게 되며, 이 속성을 사용하여 객체를 출력할 수 있습니다.

 

다음은 Person 객체를 생성하고 printableDescription 속성을 출력하는 코드입니다.

let person = Person(name: "John", age: 30)
print(person.printableDescription) // Name: John, Age: 30

위 코드에서, Person 객체는 Printable 프로토콜을 준수하는 것으로 간주됩니다.

이제 Printable 프로토콜에서 정의된 다른 속성과 메서드를 사용할 수 있습니다.

이를 통해 Person 객체에 새로운 기능을 추가하거나, Printable 프로토콜을 준수하는 다른 객체와 함께 사용할 수 있습니다.

 

다음은 Car 타입에 Printable 프로토콜을 적용하는 코드입니다.

struct Car {
    var make: String
    var model: String
    var year: Int
}

extension Car: Printable {
    var printableDescription: String {
        return "Make: \(make), Model: \(model), Year: \(year)"
    }
}

위 코드에서, Car 타입은 Printable 프로토콜을 준수하기 위해 확장(extension)됩니다.

이제 Car 타입도 printableDescription 속성을 갖게 되며, 이 속성을 사용하여 객체를 출력할 수 있습니다.

 

 

 

4. 유연성

protocol은 유형 간의 강한 종속성을 줄여주어 코드의 유연성을 높입니다.

protocol을 사용하여 유형 간의 결합도를 낮추면, 코드를 변경할 때 다른 유형에 영향을 미치지 않는 범위에서 변경할 수 있습니다.

 

예를 들어, 프로토콜을 사용하여 객체의 속성에 접근하는 방법을 정의할 수 있습니다.

이를 통해 객체의 속성을 더 유연하게 처리할 수 있습니다.

protocol Ageable {
    var age: Int { get set }
}

class Person: Ageable {
    var age: Int = 0
}

class Dog: Ageable {
    var age: Int = 0
}

func printAge(of object: Ageable) {
    print("The age of the object is \(object.age)")
}

let person = Person()
person.age = 30
printAge(of: person) // "The age of the object is 30"

let dog = Dog()
dog.age = 5
printAge(of: dog) // "The age of the object is 5"

위 코드에서, Ageable 프로토콜을 정의하고, age 속성을 포함합니다.

Person과 Dog 클래스는 Ageable 프로토콜을 준수하며, 각각 age 속성을 가지고 있습니다.

printAge 함수는 Ageable 타입의 인자를 받아 해당 객체의 age 속성을 출력합니다.

이를 통해 Person과 Dog 객체의 age 속성을 모두 출력할 수 있으며, 코드의 유연성이 높아집니다.

 

또 다른 예시로는, 프로토콜을 사용하여 여러 타입에서 공통적으로 사용하는 함수나 메서드를 정의할 수 있습니다.

이를 통해 코드의 중복을 방지하고, 코드의 유지 보수성을 높일 수 있습니다.

protocol Playable {
    func play()
}

class Video: Playable {
    func play() {
        print("Playing video...")
    }
}

class Music: Playable {
    func play() {
        print("Playing music...")
    }
}

func playAll(media: [Playable]) {
    for item in media {
        item.play()
    }
}

let video = Video()
let music = Music()
playAll(media: [video, music]) // "Playing video..." "Playing music..."

위 코드에서, Playable 프로토콜을 정의하고, play 함수를 포함합니다.

Video와 Music 클래스는 Playable 프로토콜을 준수하며, 각각 play 함수를 가지고 있습니다.

playAll 함수는 Playable 타입의 배열을 인자로 받아 각각의 객체에서 play 함수를 호출합니다.

이를 통해 Video와 Music 객체에서 공통적으로 사용하는 play 함수를 프로토콜을 통해 정의할 수 있으며, 코드의 유지 보수성이 높아집니다.

 

 

 

5. 테스트 용이성

protocol을 사용하여 의존성 주입(dependency injection)을 구현하면, 테스트 용이성을 높일 수 있습니다.

의존성을 주입함으로써, 테스트 케이스에서 프로토콜을 준수하는 가짜(mock) 객체를 만들어 테스트할 수 있습니다.

이는 테스트의 정확성과 안정성을 높여줍니다. 객체를 모의(mock)하는 것은 해당 객체의 동작을 대신하여 테스트용 객체를 생성하는 것을 의미합니다. 이를 통해 테스트를 더 쉽게 수행할 수 있으며, 코드의 안정성과 신뢰성을 높일 수 있습니다.

 

다음은 Printable 프로토콜을 준수하는 Person 객체를 모의(mock)하여 테스트하는 코드입니다.

class MockPerson: Printable {
    var printableDescription: String {
        return "Name: Mock Person, Age: 0"
    }
}

func testPersonPrintable() {
    let person = Person(name: "John", age: 30)
    XCTAssertEqual(person.printableDescription, "Name: John, Age: 30")
    
    let mockPerson = MockPerson()
    XCTAssertEqual(mockPerson.printableDescription, "Name: Mock Person, Age: 0")
}

위 코드에서, MockPerson 클래스는 Printable 프로토콜을 준수합니다. 이를 통해 Person 객체를 모의(mock)하여 테스트할 수 있습니다. testPersonPrintable 함수에서는 Person 객체와 MockPerson 객체를 각각 생성하고, printableDescription 속성을 비교하여 테스트합니다. 이를 통해 프로토콜을 사용하여 테스트 용이성을 높일 수 있다는 것을 알 수 있습니다.

 

 

 

 

자료 출처

- 야곰, ⟪스위프트 프로그래밍: Swift 5

- ChatGPT(https://chat.openai.com/chat)