Swift에서 제네릭(Generic)과 프로토콜(Protocol)은 각각 강력한 추상화 도구입니다.
제네릭은 타입에 의존하지 않는 유연한 코드를 가능하게 하며, 프로토콜은 공통된 인터페이스를 정의함으로써 다양한 타입을 하나로 묶을 수 있게 합니다.
이 둘을 함께 사용하는 경우, 더 강력하고 확장성 있는 코드를 작성할 수 있습니다.
이 글에서는 제네릭 함수나 타입에서 프로토콜 제약을 걸거나, associatedtype
과 함께 사용하는 다양한 패턴을 소개합니다.
1. 제네릭에 프로토콜 제약을 거는 기본 문법
func printDescription<T: CustomStringConvertible>(_ value: T) {
print(value.description)
}
위 함수는 CustomStringConvertible
프로토콜을 채택한 타입만 허용합니다.
printDescription("Swift") // Swift
printDescription(123) // 123
printDescription([1, 2, 3]) // [1, 2, 3]
2. 여러 개의 프로토콜 제약
func compare<T: Equatable & Comparable>(_ a: T, _ b: T) -> Bool {
return a == b
}
compare(3, 3) // true
compare("a", "b") // false
이러한 방식은 where
절을 통해 더 유연하게 확장할 수 있습니다.
3. where
절을 통한 고급 제약
func areAllEqual<T>(_ items: [T]) -> Bool where T: Equatable {
guard let first = items.first else { return true }
return items.dropFirst().allSatisfy { $0 == first }
}
areAllEqual([1, 1, 1]) // true
areAllEqual([1, 2, 1]) // false
where
절은 프로토콜 준수 외에도 연관 타입의 제약 등 다양한 조건 설정에 활용됩니다.
4. 프로토콜 내부에서 제네릭을 사용하는 방법
protocol Transformer {
associatedtype Input
associatedtype Output
func transform(_ input: Input) -> Output
}
이러한 형태는 일반적으로 타입 지우기(type erasure)와 함께 사용됩니다.
5. 프로토콜 타입과 제네릭의 구분
간혹 다음과 같은 형태를 혼동하는 경우가 있습니다.
func logItems(_ items: [CustomStringConvertible]) {
items.forEach { print($0.description) }
}
이 함수는 CustomStringConvertible
을 타입으로 사용합니다. 하지만 모든 요소가 동일한 구체 타입일 필요는 없습니다.
반면 다음 제네릭 함수는 모든 요소가 동일한 타입이면서, 해당 프로토콜을 채택해야 합니다.
func logItems<T: CustomStringConvertible>(_ items: [T]) {
items.forEach { print($0.description) }
}
제네릭은 더 구체적이고, 타입 안정성을 확보하는 데 유리합니다.
마무리
제네릭과 프로토콜을 함께 사용하는 것은 Swift의 타입 시스템을 최대한 활용하는 방식입니다.
제네릭을 통해 유연성을 확보하고, 프로토콜을 통해 일관된 인터페이스를 정의할 수 있습니다.
특히 where
절과 associatedtype
을 함께 활용하면 복잡한 추상화도 간결하게 표현할 수 있으며,
실무에서 재사용성과 유지 보수성이 뛰어난 코드를 작성하는 데 큰 도움이 됩니다.