Swift의 다형성과 Protocol

Wonbi·2024년 4월 2일
0

Swift 뿌수기

목록 보기
11/12

💎 Swift의 다형성과 Protocol

✏️ 다형성이란

  • 하나의 객체에 여러 가지 타입을 대입할 수 있다는 것
class Animal {
    func eat() {
        fatalError("Should be implemented")
    }
}

class Horse: Animal {
    override func eat() {
        print("말이 당근을 먹습니다.")
    }
}

class Cat: Animal {
    override func eat() {
        print("고양이가 츄르를 먹습니다.")
    }
}

var animal: Animal = Horse()
animal.eat() // 말이 당근을 먹습니다.
animal = Cat()
animal.eat() // 고양이가 츄르를 먹습니다.
  • animal 지역변수는 컴파일 타임에 Animal 타입에 의존하고 있다.
  • 하지만 Animal을 상속하는 Horse, Cat 타입(구체 타입)의 인스턴스를 주입할 수 있다.
  • 즉, 컴파일 타임에서의 의존성 (Animal)과 런타임에서의 의존성 (Horse, Cat)이 다르다.
  • 또한, 컴파일러가 Animal 타입에 eat() 메서드가 있는 것을 알고 있기 때문에, Animal을 상속하는 어떤 타입이 주입되더라도 eat() 메서드를 호출할 수 있다.
  • 똑같은 메서드를 호출하지만, 결과는 런타임에서 어떤 타입이 주입되는가에 따라 달라진다.
  • 이것이 객체지향의 큰 특징중 하나인 다형성(Polymorphism)이다.

✏️ Protocol

  • 다형성을 구현하는 또 다른 방법은 바로 스위프트의 프로토콜(Protocol)이다.
  • 프로토콜은 프로토콜을 채택하는 타입들이 공통적으로 구현해야 할 기능들을 정의한 설계도 같은것이다.
protocol Animal {
    var name: String { get set }
    func eat()
}

struct Horse: Animal {
    var name: String
    
    func eat() {
        print("\(name)가 당근을 먹습니다.")
    }
}

struct Cat: Animal {
    var name: String
    
    func eat() {
        print("\(name)가 츄르를 먹습니다.")
    }
}

var animal: Animal = Horse(name: "적토마")
animal.eat() // 적토마가 당근을 먹습니다.
animal = Cat(name: "개냥이")
animal.eat() // 개냥이가 츄르를 먹습니다.
  • HorseCatAnimal을 채택 하고 있기 때문에 name 프로퍼티와 eat() 메서드를 반드시 가지고 있어야 한다.
  • 프로토콜은 어떤 타입이든 채택할 수 있으므로, 프로토콜은 값타입에서도 다형성을 구현할 수 있다.
  • 또, 하나의 타입이 여러개의 프로토콜을 채택할 수 있다.
  • 프로토콜은 상속이라는 고질적인 문제점을 해결할 수 있다. 상속은 객체의 특징들이 다양해질수록 자식 클래스가 계속해서 늘어나는 문제가 있지만, 프로토콜은 합성이라는 방법을 통해 이를 간단하게 해결할 수 있다.

✏️ Associated Type: 연관 타입

  • Associated Type은 프로토콜에서 제네릭과 같이 하나의 타입 매개변수로 사용할 수 있는 기능이다.
  • 우리가 자주 사용하는 ArrayCollection프로토콜을 채택하는 타입이다.
public protocol Collection: Sequence {
    associatedtype Element
    associatedtype Index: Comparable
    
    var startIndex: Self.Index { get }
    var endIndex: Self.Index { get }
    public var first: Self.Element? { get }
}
  • 그리고 이 CollectionSequence를 채택하고 있다. 이렇게 여러 프로토콜을 채택할 수 있다는 걸 확인해볼 수 있다.
  • 무튼, 여기서 associatedtype은 Generic과 마찬가지로 타입 파라미터의 역할을 한다. 그리고 이 associatedtype은 이 프로토콜을 구현하는 타입에 의해 결정된다.
  • Element는 associatedtype을 통해 선언된 임의의 타입으로 구체적으로 어떤 타입인지 모르지만, 이 프로토콜을 채택한 타입이 구현할 때 이 값을 구현한다. 그리고 그 때 associatedtype이 어떤 타입인지 결정된다.
  • 스위프트의 Collection은 컬렉션에 담을 원소인 Element와 그 Element의 위치를 나타내는 Index를 연관 타입으로 선언했기 때문에, Int, String, 기타 struct등 스위프트의 모든 타입을 담는 Array를 만들 수 있는 것이다.
var animals: [Animal] = [
    Cat(name: "길냥이"),
    Horse(name: "천리마")
]
  • animalsElementAnimal 이라는 타입으로 대체되고, IndexInt가 된다.
  • 그럼 이 연관 타입을 사용해보자.
protocol Animal {
		associatedtype Feed

    var feed: Feed { get }
    var name: String { get set }

    func eat()
}
  • Animal프로토콜에 Feed라는 연관 타입을 추가했다. 이 Feed는 다른 어떤 타입으로도 대체가 가능하다.
struct Carrot {}
struct Churu {}
  • FeedCarrot이 될 수도 있고, Churu가 될 수도 있다. 이 두가지 뿐만 아니라 어떤 타입이든 이 Feed로 선언될 수 있다.
struct Horse: Animal {
    typealias Feed = Carrot
    
    var feed: Feed = Carrot()
    var name: String
    
    func eat() {
        print("\(name)\(feed)을 먹습니다.")
    }
}

struct Cat: Animal {
    typealias Feed = Churu
    
    var feed: Feed = Churu()
    var name: String
    
    func eat() {
        print("\(name)\(feed)를 먹습니다.")
    }
}
  • 이렇게 각각의 feed프로퍼티에 알맞은 사료를 넣어주었다.
  • 하지만, 이코드의 문제는 feed아무 타입으로 대체가 가능하다는 것이다.
  • 즉, 타이어나 컴퓨터 타입을 만들어 feed를 이 타입으로 대체해버릴 수 있는 것이다. 타이어를 씹는 말이라..
  • Feed라는 타입이 어떤 프로퍼티와 메서드를 가질 지 모르기 때문에, 어떤 정보를 String으로 표시할 수 있는지도 알 수 없다. eat()메서드에서 \(feed)가 무엇을 프린트할지 우리도 모르고 컴파일러도 모른다.
  • 이를 한번 해결해보자.
protocol Eatable {
    var name: String { get }
    var expiration: Date { get }
}

protocol Animal {
    associatedtype Feed: Eatable
    
    var feed: Feed { get }
    var name: String { get set }
    func eat()
}

struct Carrot: Eatable {
    var name: String = "드럽게 비싼 무농약 유기농 당근"
    var expiration: Date = Date()
}

struct Churu: Eatable {
    var name: String = "고양이 눈돌아가는 대존맛 츄르"
    var expiration: Date = Date()
}

struct Horse: Animal {
    typealias Feed = Carrot
    
    var feed: Feed = Carrot()
    var name: String
    
    func eat() {
        print("\(name)\(feed.name)을 먹습니다.")
    }
}

struct Cat: Animal {
    typealias Feed = Churu
    
    var feed: Feed = Churu()
    var name: String
    
    func eat() {
        print("\(name)\(feed.name)를 먹습니다.")
    }
}
  • 이제 associatedtypeFeedEatable을 채택한다. 이 말은 이 연관 타입은 Eatable을 채택한 타입만 대체될 수 있다는 것이다.
  • 타이어는 Eatable을 채택하지 않기 때문에 더이상 들어올 수 없다.
  • 때문에, feed프로퍼티는 컴파일러가 nameexpiration을 프로퍼티로 가지고 있다는 것을 알 수 있고, eat()메서드에서 feed.name으로 접근이 가능해진다.

✏️ associatedtype이 초래한 문제

  • 이제 말이 타이어를 씹어먹거나 컴파일러와 우리도 모르는 \(feed)프린트 문제를 해결했다.
  • 하지만 이로인해 새로운 문제가 발생하는데..

  • 사실, 이 문제는 이전에 Animal 프로토콜에 Feed라는 연관 타입을 넣는 순간부터 생긴 문제이다.
  • 이전 글에서도 말했듯, 연관 타입은 프로토콜을 구현하는 타입에 의해 결정된다. (매우 중요)
typealias Feed = Carrot
  • 다음과 같이 typealias를 이용해 Feed가 무슨 타입인지 컴파일러에게 알려주는 과정이 필요하다.
var animal: Animal = Horse(name: "적토마") // Error !!
  • 하지만, animalAnimal 타입이다. 이는 프로토콜에 선언된 feed, name프로퍼티와 eat()메서드에 접근이 가능한 변수라는 뜻이다.
  • 여기서 문제가 생기는 것이다. 컴파일러는 Animal프로토콜을 타입처럼 사용한다. 그럼 feed프로퍼티에 접근해야 하는데.. 이 Animal은 프로토콜이고.. 그럼 feed의 구체 타입이 뭔지 전혀 모르네? 컴파일러는 구체 타입으로 뭔가 들어올 것이라는 확실한 명분이 필요한 것이다.

✏️ some

  • 그래서 swift 5.1에서 some 키워드가 등장하였다.
  • 이 some키워드는 구체적인 타입 정보를 숨겨준다. 예를 들어 어떤 구체타입인지.. 어떤 연관 타입을 가지고 있는지.. 등등
var animal: some Animal = Horse(name: "적토마")
  • 다음과 같이 타입 어노테이션 앞에 써서 사용한다.
  • some 키워드는 구체적인 타입 정보를 숨기는 대신, Animal 프로토콜을 구현하는 단 하나의 구체 타입이라는 것을 보장해준다.
  • 여기서 단 하나라는 것이 중요한데, 위 예시처럼 하나의 구체 타입으로 선언된 이후에는 이 타입이라는 것을 보장해야 하기 때문에 다른 타입의 인스턴스를 넣어줄 수 없다.
var animal: some Animal = Horse(name: "적토마")
animal = Cat(name: "개냥이") // Error !!
  • 다음과 같이 고양이를 넣으려고 하면 에러가 난다. 하나의 타입임을 보장하기 때문이다.

✏️ any

  • 하지만 이걸론 부족하다.. 그래서 swift 5.7에서 any 키워드가 등장하였다.
  • 런타임시에 여러가지 구체타입이 프로토콜 타입에 들어가야 할 때 사용하는 키워드이다.
var animals: [any Animal] = [
    Cat(name: "길냥이"),
    Horse(name: "천리마")
]

for animal in animals {
    animal.eat()
}

//길냥이가 고양이 눈돌아가는 대존맛 츄르를 먹습니다.
//천리마가 드럽게 비싼 무농약 유기농 당근을 먹습니다.
  • 사실, 연관 타입을 사용하면서 any 키워드가 없다면, 그 연관 타입의 크기가 모두 다르기 때문에 배열에 넣어줄 수 없는 문제도 존재한다.
  • 배열은 RendomAccessCollection 프로토콜을 채택하고 있는데, 이는 임의의 인덱스가 어느 위치에 있던 그 접근을 O(1)의 시간복잡도로 수행하도록 하는 프로토콜이다. 이런 접근이 가능한 이유는 배열의 내부 엘리먼트가 모두 같은 크기임을 보장하기 때문이다.
  • 하지만 연관 타입을 사용해버리면 이 연관타입의 크기가 구체타입에서 어떻게 구현되는가에 따라 달라지기 때문에 문제가 생기는 것이다.
  • 하지만 이 any키워드를 사용하면 이 구체타입을 any로 한번 감싼다. 쉽게말해 구체타입을 똑같은 크기의 박스에 담아 이를 런타임 단에서 열어보며 확인한다.
  • 이렇게 any를 사용하여 런타임시에 여러가지 구체타입을 넣어줄 수 있다.

0개의 댓글