Swift 제네릭에 대하여

Benedicto·4일 전
0

iOS

목록 보기
29/29

Generics

제네릭특정 타입에 종속되지 않고 다양한 타입에 대해 타입 안정성을 유지하면서, 다양한 타입에 대해 유연하게 동작할 수 있게 해주는 문법.

  • 함수 (Functions)
  • 타입: 구조체 / 클래스 / 열거형 (Structs, Classes, Enums)
  • 프로토콜 (protocol)

등에 적용이 가능.


제네릭 함수 (Functions)

func swapTwoValue<T>(_ value1: inout T, _ value2: inout T) -> Void {
    let temp = value1
    value1 = value2
    value2 = temp
}
var num1 = 0
var num2 = 1

swapTwoValue(&num1, &num2)

swapTwoValues<T>(_:_:)는 제네릭 함수로, T는 타입 매개변수.
T는 특정 타입에 구애받지 않고 함수가 호출될 때, 구체적인 타입으로 결정된다.

💡 즉, 함수가 실제로 호출되는 시점에 전달된 인자의 타입에 맞게 자동으로 타입이 결정된다.


제네릭 타입: 구조체 / 클래스 / 열거형 (Structs, Classes, Enums)

class Container<T> {
    var items: [T] = []
    
    func getItems() -> [T] {
        return items
    }

    func appendItem(_ value: T) {
        self.items.append(value)
    }
}
let intContainer = Container<Int>()
intContainer.items = [10, 20, 30]
intContainer.appendItem(0)

print(intContainer.getItems())]
//	Prints: [10, 20, 30, 0]
let stringContainer = Container<String>()
stringContainer.items = ["Hello"]
stringContainer.appendItem("World")

print(stringContainer.getItems())
//	Prints: ["Hello", "World"]

Container<T>는 타입 매개변수 T를 사용하여 어떤 타입이든 담을 수 있는 클래스.
메서드에서 제네릭 타입 T를 활용해 다양한 타입의 데이터를 처리할 수 있다.


제네릭 프로토콜 (protocol)

protocol ContainerType {
    associatedtype Item
    var items: [Item] { get }
    
    func getItems() -> [Item]
    func appendItem(_ value: Item) -> Void
}

프로토콜 또한 제네릭화 할 수 있으며, 프로토콜 내부에서 associatedtype 키워드를 사용.
associatedtype 키워드를 통해 연관 타입을 정의하며, 연관 타입은 프로토콜을 채택하는 타입이 나중에 구체적으로 지정할 수 있는 타입 자리 표시자.

💡 즉, associatedtype: 프로토콜 안에서 사용할 타입을 미리 정하지 않고!, 프로토콜을 채택하는 쪽에서 나중에 정하게 하는 일종의 Placeholder.

class IntContainer: ContainerType {
    typealias Item = Int
    var items: [Item] = []
    
    func getItems() -> [Item] {
        return items
    }
    
    func appendItem(_ value: Item) -> Void {
        self.items.append(value)
    }
}

일반적으로 typealias 키워드를 사용하여 associatedtype의 타입을 명시

let intContainer = IntContainer()
intContainer.items = [10, 20]
intContainer.appendItem(30)

print(intContainer.getItems())
//	Prints: [10, 20, 30]

또는 아래와 같이 사용

class StringContainer: ContainerType {
    var items: [String] = []
    
    func getItems() -> [String] {
        return items
    }
    
    func appendItem(_ value: String) -> Void {
        self.items.append(value)
    }
}

associatedtype의 타입이 사용되는 곳에서 typealias 없이도 어떤 타입인지 충분히 추론이 가능한 경우, typealias를 생략 가능.

💡 보통은 추론 가능하게끔 하고, typealias 생략

let strContainer = StringContainer()
strContainer.items = ["Hello", "World"]
strContainer.appendItem("!")

print(strContainer.getItems())
//	Prints: ["Hello", "World", "!"]

🔄 제네릭 클래스와 함께 사용하는 경우 (더 일반적인 형태)

protocol ContainerType {
    associatedtype Item
    
    var items: [Item] { get }
    var count: Int { get }
    
    func getItem(at index: Int) -> Item
    func getItems() -> [Item]
    func appendItem(_ value: Item) -> Void
}
class Container<T>: ContainerType {
	//	typealias Item = T	//	생략해도 무방
    var items: [T] = []
    var count: Int { return items.count }
    
    func getItem(at index: Int) -> T {
        return items[index]
    }

    func getItems() -> [T] {
        return items
    }
    
    func appendItem(_ value: T) -> Void {
        items.append(value)
    }
}
let intContainer = Container<Int>()
intContainer.items = [10, 20]

print(intContainer.items.count)
//	Prints: 2
print(intContainer.getItem(at: 1))
//	Prints: 20

intContainer.appendItem(30)

print(intContainer.getItems())
//	Prints: [10, 20, 30]

제네릭 차이점

제네릭 사용으로 다양한 타입을 다룰 수 있다는 점에서는 공통점이 있으나, 접근 방식유연성의 수준에는 차이가 있음.

1. 일반 제네릭 클래스 (Container<T> 예제)

  • 특징: 하나의 클래스 정의로 여러 타입을 저장 가능
  • 장점: 간단하고 직관적이며, 타입이 명확하게 클래스 생성 시 결정됨
  • 제한: 특정 프로토콜이나 기능 요구 사항을 정의하지 않음

적합한 상황:

  • 단순히 다양한 타입을 안전하게 담아야 할 때.
  • 별도의 기능 요구사항(프로토콜)이 없을 때.
  • 데이터만 보관하거나 포장(wrapping)하는 구조가 필요할 때.

예: Container<Int>, Container<String> 등 → 타입을 직접 지정해서 객체 생성

2. 제네릭 프로토콜 + 구체 타입 구현 (IntContainer: ContainerType 예제)

  • 특징: associatedtype을 사용하여 프로토콜 정의,
    실제 구현체(IntContainer / StringContainer)는 특정 타입(Int)에 고정됨
  • 장점: 해당 타입에 특화된 로직 구현이 쉬움
  • 제한: 타입이 고정되어 있으므로 재사용성이 떨어짐

적합한 상황:

  • 특정 타입에 대해서만 작동하는 기능을 구현할 때.
  • 단순한 프로토콜 구조를 사용하면서도 명확한 타입을 고정하고 싶을 때.
  • 타입에 따라 로직을 분기하거나 최적화할 필요가 있을 때.

예: 오직 Int 값과 String 값만 다루는 IntContainer / StringContainer

3. 제네릭 프로토콜 + 제네릭 클래스 구현 (Container<T>: ContainerType 예제)

  • 특징: 제네릭 클래스가 프로토콜을 채택하면서 어떤 타입도 다룰 수 있음
  • 장점: 매우 유연하고 재사용 가능. 프로토콜의 요구사항도 만족
  • 활용도: 제네릭 프로그래밍에서 가장 널리 사용되는 형태

적합한 상황:

  • 타입에 관계없이 동작해야 하는 자료구조나 알고리즘이 필요할 때.
  • 다양한 타입의 객체에서 동일한 인터페이스를 사용하고 싶을 때.
  • 재사용성과 확장성을 최대화해야 하는 라이브러리나 프레임워크 수준의 코드.

예: Container<String>, Container<Double> 등 → 타입 유연성과 구조적 설계 가능


방식타입 유연성프로토콜 사용재사용성복잡도
class Container<T>높음높음낮음
class IntContainer: ContainerType낮음 (Int 고정)낮음낮음
class Container<T>: ContainerType높음높음중간

상황에 따라 다르지만, 확장성과 유연성이 중요한 경우 associatedtype + 제네릭 가장 강력.

profile
 Developer

0개의 댓글