Swift 제네릭 | Generic 함수, 타입, 제약

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복잡한 제약의 이름 부여

추가 리소스


정리

핵심 요약

  1. 제네릭 함수: <T> 타입 매개변수
  2. 제네릭 타입: Stack, Pair 등
  3. 제약: Equatable, Comparable 등
  4. where 절: 복잡한 제약 조건
  5. 연관 타입: 프로토콜의 제네릭

다음 단계

  • Swift 에러 처리
  • Swift 비동기
  • Swift SwiftUI

관련 글

  • C++ Generic Lambda |
  • C++ 템플릿 |
  • C++ Template Lambda |
  • Rust 트레이트 | Trait, 제네릭, 트레이트 바운드
  • Swift 시작하기 | iOS 개발 공식 언어 완벽 입문