1. 제네릭이란?
제네릭은 타입에 의존하지 않는 범용 코드를 작성할 때 사용한다. 제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 있다.
예를 들어, Stack 자료구조를 직접 만든다고 생각해보자.
Character를 담을 수 있는 Stack은 이렇게 짤 수 있을 것이다.
class CharStack {
private var elements: [Character] = []
func push(_ value: Character) {
elements.append(value)
}
func pop() -> Character? {
return elements.popLast()
}
func peek() -> Character? {
return elements.last
}
var isEmpty: Bool {
return elements.isEmpty
}
var count: Int {
return elements.count
}
}
하지만, 우리가 Stack을 이곳저곳에 사용하려면 Character만 담아서는 안되고 Array처럼 Int, String, 임의의 구조체, 튜플 등을 담을 수 있어야한다. 이때 제네릭을 사용하면 여러 타입의 Stack을 만들지 않고도 타입에 의존하지 않는 Stack을 만들 수 있다.
class Stack<T> {
private var elements: [T] = []
func push(_ value: T) {
elements.append(value)
}
func pop() -> T? {
return elements.popLast()
}
func peek() -> T? {
return elements.last
}
var isEmpty: Bool {
return elements.isEmpty
}
var count: Int {
return elements.count
}
}
// 사용시
let stack = Stack<Int>()
T를 Type Parameter라고 하는데, 해당 타입을 대신해주는 placeholder이다.
위 코드에서는 Stack선언 시, <Int>를 명시했기때문에 T 자리에 Int가 들어간다고 생각하면된다.
Type Parameter는 두 개 이상의 타입도 받을 수 있게 <One, Two, ...> 지원한다.
2. 타입 제약
제네릭 함수와 타입을 사용할 때 특정 클래스의 하위 클래스나, 특정 프로토콜을 준수하는 타입만 받을 수 있게 제약을 둘 수 있다.
예를 들어, 내가 Hashable을 채택한 타입만 쓰고 싶다면 아래와 같이 작성해줄 수 있다.
func printHashValue<T: Hashable>(_ value: T) {
print("Hash value: \(value.hashValue)")
}
클래스 제약은 해당 클래스 및 서브 클래스만 타입으로 받을 수 있도록 제약하는 것이다.
class Animal {
func sound() {
print("동물 소리")
}
}
class Dog: Animal {
override func sound() {
print("멍멍")
}
}
class Car {
func sound() {
print("부릉부릉")
}
}
// Animal의 서브클래스만 받을 수 있음
func makeSound<T: Animal>(_ animal: T) {
animal.sound()
}
let dog = Dog()
makeSound(dog)
let animal = Animal()
makeSound(animal)
let car = Car()
makeSound(car) // 컴파일 에러
3. Assosiated Type
프로토콜에서도 제네릭을 사용할 수 있는데 그냥 냅다 사용하면 에러가 뜬다.

"프로토콜(또는 그 상속받은 프로토콜)에 ‘T’라는 associated type이 선언되어 있어야 한다”
즉, protocol에서 제네릭을 쓰려면 아래처럼 associatedtype T를 반드시 선언해야 한다는 뜻이다.
protocol StackProtocol {
associatedtype T
mutating func push(_ element: T)
mutating func pop() -> T?
func peek() -> T?
var isEmpty: Bool { get }
}
실제 이 프로토콜에서 채택하는 구현체를 작성해보면
class CharacterStack: StackProtocol {
// 나는 T를 Character로 사용할거야
typealias T = Character
private var elements: [Character] = []
func push(_ element: Character) {
elements.append(element)
}
func pop() -> Character? {
return elements.popLast()
}
func peek() -> Character? {
return elements.last
}
var isEmpty: Bool {
return elements.isEmpty
}
}
typealias를 이용해서 사용할 타입을 명시해줄 수 있다.(추론이 가능한 경우 생략 가능)
4. 주의할 점
제네릭을 사용하면 유연한 코드를 작성할 수 있고 코드 중복 또한 피할 수 있다.
하지만 너무 많은 제네릭을 사용하면 코드가 지나치게 복잡해질 수 있다. (가독성 ↓)
또한, 제네릭은 컴파일 타임에 타입 정보를 처리하고 특정 타입에 대한 코드를 생성하기 때문에 오버헤드가 발생할 수 있다.
따라서 꼭 필요한 경우가 아니라면 타입을 구체적으로 명시하여 쓰는 것이 좋다.
'iOS > Swift' 카테고리의 다른 글
| [iOS] 기존 프로젝트에 Tuist v4 적용하기(2/2) (4) | 2025.08.13 |
|---|---|
| [Swift] 접근 제어(Access Control) (2) | 2025.07.21 |
| [Swift] 클로저, 클로저 캡쳐 (1) | 2025.07.08 |
| [Swift] ARC, weak, unowned (0) | 2025.07.01 |
| [SwiftUI] 렌더링 실험 (3) | 2025.06.25 |
댓글