본문 바로가기

정보

performAction<T: Animal>, animal: Animal 차이?

https://explorer89.tistory.com/222

 

상속(Inheritance)과 프로토콜(Protocol)의 차이점은 무엇인가요?

상속(Inheritance)과 프로토콜(Protocol)은 Swift에서 객체지향 프로그래밍(OOP) 및 프로토콜 지향 프로그래밍(POP)을 구현하기 위한 두 가지 주요 개념입니다. 이 둘은 비슷한 목적을 가지고 있지만 사용

explorer89.tistory.com

 

1. 제네릭을 사용하는 경우

func performAction<T: Animal>(on animal: T) {
    animal.makeSound()
    animal.eat()
}

 

  • 장점: 제네릭을 사용하면 함수가 구체적인 타입(T)을 알고 작업할 수 있습니다.
    예를 들어, T가 특정 타입(예: Dog, Cat)이라면 컴파일러가 해당 타입에 대한 추가 정보를 활용할 수 있습니다.
  • 제네릭으로 작성된 함수는 구체적인 타입의 최적화된 코드를 생성할 수 있기 때문에, 런타임 비용이 줄어들거나 더 안전한 동작을 보장할 수 있습니다.
  • T가 Animal을 준수한다는 제약만 있으면 되기 때문에 타입 추론이 가능하고, 함수 호출 시 타입을 명확히 알 수 있습니다.

2. animal: Animal로 하는 경우

func performAction(on animal: Animal) {
    animal.makeSound()
    animal.eat()
}

 

 

  • 단점: animal은 Animal 프로토콜 타입으로만 인식됩니다.
    • 따라서 컴파일러는 구체적인 타입 정보를 잃게 됩니다.
    • 예를 들어, animal의 타입이 Dog인지 Cat인지 구분할 수 없으며, 오직 Animal로서만 동작합니다.
  • 또한, Animal 프로토콜에 정의되지 않은 추가 메서드나 프로퍼티를 사용할 수 없습니다.
  • 런타임 다형성(dynamic dispatch)을 사용하게 되어 약간의 성능 비용이 발생할 수 있습니다.

 

예를 들어, 동물 대신 "박스(Box)"를 생각해봅시다. 박스는 크기와 모양이 다를 수 있죠. 여기에서 제네릭과 프로토콜 타입의 차이를 보겠습니다.

1. 제네릭을 사용하는 경우

protocol Box {
    func open()
}

struct SmallBox: Box {
    func open() {
        print("작은 박스를 엽니다.")
    }

    func wrap() {
        print("작은 박스를 포장합니다.")
    }
}

struct BigBox: Box {
    func open() {
        print("큰 박스를 엽니다.")
    }

    func seal() {
        print("큰 박스를 봉인합니다.")
    }
}

func performAction<T: Box>(on box: T) {
    box.open() // T는 구체적인 타입(SmallBox, BigBox)이기 때문에 이 메서드는 안전하게 호출 가능
}

 

 

이 경우 T가 SmallBox면 SmallBox로, BigBox면 BigBox 컴파일러가 타입을 알고 작업합니다.
만약 performAction 안에서 wrap()이나 seal()을 호출하려면, 해당 메서드가 SmallBox 또는 BigBox에 있는지 확인할 수 있습니다. 예를 들어:

func performAction<T: Box>(on box: T) {
    box.open()
    
    if let smallBox = box as? SmallBox {
        smallBox.wrap() // SmallBox에 있는 추가 메서드
    } else if let bigBox = box as? BigBox {
        bigBox.seal() // BigBox에 있는 추가 메서드
    }
}

 

제네릭은 구체적인 타입 정보를 유지하기 때문에, 타입별로 다른 작업을 할 수 있습니다.

 

2. 그냥 box: Box를 사용하는 경우

func performAction(on box: Box) {
    box.open() // Box 프로토콜에 정의된 메서드만 호출 가능
}

 

여기서 box는 그냥 "Box"라는 프로토콜 타입으로 동작합니다.
구체적인 타입이 SmallBox인지 BigBox인지 모르기 때문에 wrap()이나 seal() 같은 메서드를 호출할 수 없습니다.

func performAction(on box: Box) {
    box.open()
    // box.wrap() ❌ 에러! Box에는 wrap()이 없어요.
    // box.seal() ❌ 에러! Box에는 seal()이 없어요.
}

 

만약 특정 타입으로 동작하게 하고 싶다면 런타임에 타입 확인을 해야 합니다:

func performAction(on box: Box) {
    box.open()
    
    if let smallBox = box as? SmallBox {
        smallBox.wrap()
    } else if let bigBox = box as? BigBox {
        bigBox.seal()
    }
}

 

하지만 이렇게 하면 런타임 비용이 발생하고, 코드도 덜 깔끔해집니다.

왜 제네릭이 더 유용한가?

구체적인 예를 들어, SmallBox와 BigBox를 구분해서 처리해야 한다고 가정해볼게요.

  • 제네릭 사용: performAction 함수가 호출될 때, SmallBox나 BigBox에 맞게 최적화된 코드가 만들어져서 동작합니다.
  • 프로토콜 타입 사용: 함수 안에서 타입을 확인해야 하고, Box에 정의된 메서드 외에는 아무것도 할 수 없습니다.

결론: 제네릭은 "타입별로 다르게 동작하거나, 추가적인 기능을 사용할 수 있는 경우"에 더 적합합니다.

 

 

🔥 제네릭 사용 유무에 따른 함수 호출의 차이점 🔥

1. 프로토콜 타입 (Box 타입) 버전

func performAction(on box: Box) {
    box.open()
    
    if let smallBox = box as? SmallBox {
        smallBox.wrap()
    } else if let bigBox = box as? BigBox {
        bigBox.seal()
    }
}

 

  • 여기서 box는 단순히 Box 프로토콜 타입입니다.
  • 이 상태에서는 컴파일러가 box의 구체적인 타입(SmallBox, BigBox)을 알지 못합니다.
    따라서 if let으로 런타임에 타입 확인(downcasting)을 해야만 SmallBox나 BigBox의 추가 메서드(wrap, seal)를 사용할 수 있습니다.

단점:

  1. 런타임 비용:
    • if let을 사용한 타입 캐스팅(as?)은 런타임에 수행됩니다.
    • box가 SmallBox인지, BigBox인지 확인하려면 실행 시점에 타입을 체크해야 하므로 성능 비용이 발생합니다.
  2. 타입 안정성 부족:
    • 컴파일 타임에는 box의 구체적인 타입을 알 수 없으므로, 실수로 프로토콜 외의 메서드를 호출하려고 하면 런타임 에러로 이어질 가능성이 있습니다.

 

2. 제네릭 버전

func performAction<T: Box>(on box: T) {
    box.open()
    
    if let smallBox = box as? SmallBox {
        smallBox.wrap()
    } else if let bigBox = box as? BigBox {
        bigBox.seal()
    }
}

 

  • 여기서 box는 제네릭 타입 T로, 함수가 호출될 때 구체적인 타입(SmallBox, BigBox)으로 결정됩니다.
  • 호출 시점에 T가 어떤 타입인지 컴파일러가 알고 있기 때문에, 더 많은 최적화를 수행할 수 있습니다.

장점:

  1. 컴파일 타임 타입 정보:
    • 컴파일러는 T가 어떤 타입인지 알기 때문에, performAction 내부에서 구체적인 타입 정보를 사용할 수 있습니다.
    • 이는 코드의 타입 안정성을 보장하며, 잘못된 메서드 호출로 인한 오류를 미리 방지할 수 있습니다.
  2. 성능 최적화:
    • 제네릭 함수는 호출 시점에 구체적인 타입별로 최적화된 코드를 생성합니다.
    • 타입 캐스팅(as?)은 여전히 런타임에 수행되지만, 제네릭으로 작성된 코드는 더 가볍고 효율적으로 작동합니다.
  3. 확장성:
    • 새로운 타입이 추가되어도 제네릭 함수는 확장성이 더 좋습니다.
    • 타입별로 추가적인 처리가 필요할 경우, 제네릭을 사용하면 타입 정보를 활용하기 쉽습니다.

 

3. 결정적인 차이: 타입 정보 유지와 의도

제네릭을 사용하는 이유는 "함수를 호출할 때 타입 정보를 유지하고, 컴파일 타임에 더 안전하게 타입을 다룰 수 있도록 하기 위함"입니다.

  • Box 타입으로 제한: 프로토콜 타입을 사용하는 경우, 모든 동작이 런타임에 의존하고 타입 안정성을 희생해야 합니다.
  • 제네릭: 호출 시점에 타입 정보가 전달되므로, 성능과 안전성에서 이점이 있습니다.

 

요약

  • 프로토콜 타입은 단순히 Box로 동작을 제한하고, 런타임에 타입 확인을 수행해야 합니다.
  • 제네릭은 호출 시점에 타입 정보를 전달받아, 더 안전하고 효율적으로 코드를 작성할 수 있습니다.

결론적으로, 결과가 같아 보이더라도, 제네릭은 컴파일 타임 타입 정보 유지와 최적화라는 중요한 이점을 제공합니다.

'정보' 카테고리의 다른 글

SOLID 원칙이란?  (0) 2024.12.17
디자인 패턴 - 싱글톤 패턴  (0) 2024.12.17
Hashable과 Equatable은 밀접한 관계  (0) 2024.12.16
해시란? Hash, Hashable  (1) 2024.12.15
Static Dispatch vs Dynamic Dispatch  (1) 2024.12.13