본문으로 건너뛰기 Swift 제네릭 | Generic 함수, 타입, 제약

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

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

이 글의 핵심

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. 연관 타입: 프로토콜의 제네릭

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Swift 제네릭에 대해 정리한 개발 블로그 글입니다. func swap(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp }. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Swift, Generic, 제네릭 등으로 검색하시면 이 글이 도움이 됩니다.