[2026] Swift 제네릭 | Generic 함수, 타입, 제약
이 글의 핵심
func swap<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp }.
들어가며
제네릭은 Array<Element>처럼 틀은 하나로 두고, 구체 타입은 호출부에서 정하는 방식입니다. 연관 타입(associatedtype)으로 프로토콜과 조합하기도 합니다.
1. 제네릭 함수
기본 제네릭 함수
다음은 swift를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
제네릭 함수 예제
다음은 swift를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 구현
다음은 swift를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 구현
아래 코드는 swift를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
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. 제네릭 제약
프로토콜 제약
아래 코드는 swift를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 절
아래 코드는 swift를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
다음은 swift를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 실전 예제
예제: 제네릭 캐시
다음은 swift를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 기반 제네릭 로더
네트워크·파일에서 한 번만 디코딩 로직을 제네릭으로 묶으면 테스트와 재사용이 쉬워집니다. 다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 절: 복잡한 제약 조건
- 연관 타입: 프로토콜의 제네릭