Swift 제네릭 | Generic 함수, 타입, 제약
이 글의 핵심
Swift 제네릭에 대해 정리한 개발 블로그 글입니다. func swap<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp }
들어가며
제네릭은 Array<Element>처럼 틀은 하나로 두고, 구체 타입은 호출부에서 정하는 방식입니다. 연관 타입(associatedtype)으로 프로토콜과 조합하기도 합니다.
1. 제네릭 함수
기본 제네릭 함수
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swap(&x, &y)
print("x: \(x), y: \(y)") // x: 20, y: 10
var str1 = "Hello"
var str2 = "World"
swap(&str1, &str2)
print("\(str1), \(str2)") // World, Hello
제네릭 함수 예제
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, item) in array.enumerated() {
if item == value {
return index
}
}
return nil
}
let numbers = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: numbers) {
print("인덱스: \(index)") // 인덱스: 2
}
let strings = ["a", "b", "c"]
if let index = findIndex(of: "b", in: strings) {
print("인덱스: \(index)") // 인덱스: 1
}
2. 제네릭 타입
Stack 구현
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
func peek() -> Element? {
return items.last
}
var isEmpty: Bool {
return items.isEmpty
}
var count: Int {
return items.count
}
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.pop()) // Optional(3)
print(intStack.peek()) // Optional(2)
var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
Pair 구현
struct Pair<T, U> {
let first: T
let second: U
}
let pair1 = Pair(first: 1, second: "one")
let pair2 = Pair(first: "name", second: 25)
print("\(pair1.first): \(pair1.second)")
3. 제네릭 제약
프로토콜 제약
func largest<T: Comparable>(_ array: [T]) -> T? {
guard !array.isEmpty else { return nil }
var largest = array[0]
for item in array {
if item > largest {
largest = item
}
}
return largest
}
print(largest([1, 5, 3, 9, 2])) // Optional(9)
print(largest(["a", "z", "m"])) // Optional("z")
where 절
func allEqual<T: Equatable>(_ array: [T]) -> Bool {
guard let first = array.first else { return true }
for item in array {
if item != first {
return false
}
}
return true
}
print(allEqual([1, 1, 1])) // true
print(allEqual([1, 2, 1])) // false
4. 연관 타입 (Associated Type)
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
5. 실전 예제
예제: 제네릭 캐시
class Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
func set(_ value: Value, forKey key: Key) {
storage[key] = value
}
func get(_ key: Key) -> Value? {
return storage[key]
}
func remove(_ key: Key) {
storage.removeValue(forKey: key)
}
func clear() {
storage.removeAll()
}
}
let cache = Cache<String, Int>()
cache.set(100, forKey: "score")
cache.set(25, forKey: "age")
if let score = cache.get("score") {
print("점수: \(score)")
}
실전 심화 보강
실전 예제: Decoder 기반 제네릭 로더
네트워크·파일에서 한 번만 디코딩 로직을 제네릭으로 묶으면 테스트와 재사용이 쉬워집니다.
import Foundation
enum LoadError: Error {
case badURL
case emptyData
}
struct Loader {
static func load<T: Decodable>(_ type: T.Type, from url: URL, decoder: JSONDecoder = JSONDecoder()) async throws -> T {
let (data, _) = try await URLSession.shared.data(from: url)
guard !data.isEmpty else { throw LoadError.emptyData }
return try decoder.decode(T.self, from: data)
}
}
struct Article: Codable {
let id: Int
let title: String
}
func example() async throws {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
throw LoadError.badURL
}
let post: Article = try await Loader.load(Article.self, from: url)
print(post.title)
}
자주 하는 실수
- 프로토콜에 연관 타입이 있는데
Array<some Container>처럼 잘못 쓰는 경우. Equatable제약 없이==를 쓰려다 제네릭 함수에서 컴파일 에러가 나는 경우.- existential
any Protocol과 제네릭을 혼동해 성능·표현력을 잃는 경우.
주의사항
- 이진 크기: 제네릭 특수화는 대체로 효율적이지만, 타입 파라미터가 많아지면 컴파일 시간이 늘 수 있습니다.
실무에서는 이렇게
- 공통 제약은 **
extension+where**로 묶어 가독성을 높입니다. - SwiftUI에서는 **
@ViewBuilder**와 제네릭 뷰를 함께 쓸 때 타입 추론 한계를Group/AnyView로 풀되, 남발은 피합니다.
비교 및 대안
| 패턴 | 설명 |
|---|---|
| 제네릭 | 컴파일 타임 특수화, 타입 안전 |
| 프로토콜 존재형 | 런타임 다형성, 타입 이레이저 비용 |
typealias | 복잡한 제약의 이름 부여 |
추가 리소스
정리
핵심 요약
- 제네릭 함수:
<T>타입 매개변수 - 제네릭 타입: Stack, Pair 등
- 제약: Equatable, Comparable 등
- where 절: 복잡한 제약 조건
- 연관 타입: 프로토콜의 제네릭
다음 단계
- Swift 에러 처리
- Swift 비동기
- Swift SwiftUI
관련 글
- C++ Generic Lambda |
- C++ 템플릿 |
- C++ Template Lambda |
- Rust 트레이트 | Trait, 제네릭, 트레이트 바운드
- Swift 시작하기 | iOS 개발 공식 언어 완벽 입문