[swift] Protocol 프로토콜 (2/2)

이은수, Lee EunSoo·2024년 10월 5일
0

Swift Basic

목록 보기
19/24
post-thumbnail

개요

지난 시간엔 프로토콜의 형태와 사용방법에 채택/구현방법에 대해 알아 보았다 이번시간에는 프로토콜로 할 수 있는 추가적인 기능에 대해서 알아볼것이다.

1. 프로토콜 타입의 컬렉션

프로토콜으로 배열이나 set같은 컬렉션으로 만들 수 있다.

// TextRepresentable protocol 정의
protocol TextRepresentable {
    var textualDescription: String { get }
}

// Game 구조체
struct Game: TextRepresentable {
    var name: String
    
    var textualDescription: String {
        return "Game: \(name)"
    }
}

// Dice 구조체
struct Dice: TextRepresentable {
    var sides: Int
    
    var textualDescription: String {
        return "A dice with \(sides) sides"
    }
}

// Hamster 구조체
struct Hamster: TextRepresentable {
    var name: String
    
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}

// 프로토콜을 준수하는 인스턴스 생성
let game = Game(name: "Chess")
let d12 = Dice(sides: 12)
let simonTheHamster = Hamster(name: "Simon")

// TextRepresentable 프로토콜을 준수하는 타입의 컬렉션 생성
let things: [TextRepresentable] = [game, d12, simonTheHamster]

// 컬렉션 순회하며 textualDescription 호출
for thing in things {
    print(thing.textualDescription)
}

여기서 아래의 코드에 집중해보자

let things: [TextRepresentable] = [game, d12, simonTheHamster]

이처럼 프로토콜을 타입으로 이용해서 인스턴스 컬렉션을 만들 수 있다.

이렇게 하면 타입(클래스/구조체/열거형)이 달라도 같은 프로토콜을 준수하는 인스턴스 끼리 묶을 수 있다.

2. 프로토콜의 상속

프로토콜도 다른 타입들처럼 상속을 할 수 있다.

기존의 상속한 요구사항 위에 요구사항을 더 추가할 수 있다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 프로토콜 제약사항은 여기에...
}

만약 이처럼 SomeProtocol, AnotherProtocol을 상속받는다면 InheritingProtocol을 채택하고 구현하는 타입은 InheritingProtocol과 SomeProtocol, AnotherProtocol의 제약사항을 모두 지켜야 한다.

3. 클래스 전용 프로토콜

클래스에서만 사용할 수 있게 프로토콜을 설계할 수 있다.

바로 AnyObject프로토콜을 상속받는 프로토콜을 만들면 된다.

protocol SomeClassOnlyProtocol: AnyObject{
    // 클래스 전용 프로토콜 제약사항은 여기에...
}

4. 프로토콜의 혼합

// 두 개의 프로토콜 정의
protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

// 이 두 프로토콜을 혼합해서 사용
struct Person: Named, Aged {
    var name: String
    var age: Int
}

// 함수에서 프로토콜 혼합을 이용하여 여러 프로토콜을 동시에 요구
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy Birthday, \(celebrator.name)! You are \(celebrator.age) years old!")
}

let person = Person(name: "Alice", age: 30)
wishHappyBirthday(to: person)
// 출력: Happy Birthday, Alice! You are 30 years old!
  1. Named와 Aged라는 두 프로토콜을 정의
  2. Person 구조체는 Named와 Aged 프로토콜을 모두 준수함
    • name과 age 속성을 제공하여 두 프로토콜을 충족
  3. wishHappyBirthday(to:) 함수는 인자로 Named & Aged 타입을 요구하는데 즉, 이 함수는 Named와 Aged 프로토콜을 모두 준수하는 객체만 받을 수 있다.
  4. Person은 두 프로토콜을 모두 준수하기 때문에, wishHappyBirthday 함수의 인자로 전달할 수 있음

프로토콜 혼합을 쓰는 이유

프로토콜 혼합은 함수나 메서드에서 다양한 타입을 처리할 때 유용하게 쓰일 수 있다.

예를 들어, 어떤 객체가 특정한 여러 기능을 제공해야 할 때, 클래스를 상속하지 않고 여러 프로토콜을 혼합하여 타입을 제약하게 할 수 있다.

5. 프로토콜 검사

타입캐스팅에서 사용하는 is, as 연산자를 이용해서 인스턴스의 타입이 특정 프로토콜을 따르고 있는지 확인 할 수 있다.
(자세한 내용은 타입캐스팅(링크x)에서)

is
인스턴스가 프로토콜을 준수한다면 true 를 반환하고 그렇지 않으면 false 를 반환합니다.

as?
프로토콜의 타입의 옵셔널 값을 반환하고 인스턴스가 프로토콜을 준수하지 않으면 nil을 반환한다.

as!
프로토콜 타입으로 강제로 다운 캐스팅 하고 다운 캐스트가 성공하지 못하면 런타임 에러를 발생시킨다.

// 프로토콜 정의
protocol HasName {
    var name: String { get }
}

protocol CanRun {
    func run()
}

// 클래스가 프로토콜을 구현
class Person: HasName, CanRun {
    var name: String
    init(name: String) {
        self.name = name
    }
    
    func run() {
        print("\(name) is running!")
    }
}

class Dog: HasName {
    var name: String
    init(name: String) {
        self.name = name
    }
}

// 객체 생성
let person = Person(name: "Alice")
let dog = Dog(name: "Buddy")

// 배열에 넣기
let things: [Any] = [person, dog]

// 타입 검사: is 연산자 사용
for thing in things {
    if thing is HasName {
        print("This thing has a name.")
    }
    
    if thing is CanRun {
        print("This thing can run.")
    }
}

// 타입 캐스팅: as? 연산자 사용
for thing in things {
    if let nameable = thing as? HasName {
        print("This thing's name is \(nameable.name).")
    }
    
    if let runnable = thing as? CanRun {
        runnable.run()
    }
}

출력결과

This thing has a name.
This thing can run.
This thing has a name.
This thing's name is Alice.
Alice is running!
This thing's name is Buddy.

6. 프로토콜의 확장

extension을 이용해서 프로토콜의 확장또한 가능하다. 이때 프로토콜을 확장하면 요구사항의 구현을 할 수 있다.

새로운 요구사항을 추가하는것은 불가능하다.

// Protocol 정의
protocol Describable {
    var description: String { get }
}

// Protocol 확장: 새로운 메서드 추가
extension Describable {
    func printDescription() {
        print(description)
    }
}

// Struct가 프로토콜을 채택하고 기본 속성 구현
struct Car: Describable {
    var description: String
}

let car = Car(description: "A red sports car")
car.printDescription()  // 출력: A red sports car

이처럼 기존의 요구사항으로 정의했던 description 메소드를 extension부분에서 구현할 수 있다.
(계산 프로퍼티도 가능)

이렇게 하면 타입에서는 해당 타입을 채택 하는것 만으로도 새로운 메소드를 사용할 수 있게 된다.

이를 기본구현 이라고 한다.

6.1 프로토콜 확장에 제약 추가

프로토콜확장에 where키워드를 이용해서 특정 제약을 추가 할 수 있다 이해가 어렵다면 제네릭을 공부하고 다시 보도록 하자.

// Protocol 정의
protocol Summable {
    static func +(lhs: Self, rhs: Self) -> Self
}

// Protocol 확장: 제약 있는 확장
extension Summable where Self: Numeric {
    func doubled() -> Self {
        return self + self
    }
}

// Int와 Double은 Numeric 프로토콜을 준수하므로 Summable도 자동으로 채택함
extension Int: Summable {}
extension Double: Summable {}

let intValue: Int = 10
let doubleValue: Double = 5.5

print(intValue.doubled())     // 출력: 20
print(doubleValue.doubled())  // 출력: 11.0

정리

프로토콜은 Swift에서 강력한 기능을 제공한다. 잘사용한다면 코드가 한단계 더 발전 할 기회를 제공한다.

  1. 프로토콜끼리 상속이 가능하다
  2. 클래스 전용 프로토콜을 만들 수 있다.
  3. 프로토콜을 타입처럼 이용해 컬렉션에도 사용할 수 있다.
  4. 프로토콜을 채택했는지 확인가능하다.
  5. extension을 이용해서 프로토콜자체를 확장 할 수 있다.
profile
iOS 개발자 취준생, 천 리 길도 한 걸음부터

0개의 댓글