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 | 복잡한 제약의 이름 부여 |
추가 리소스
내부 동작과 핵심 메커니즘
이 글의 주제는 「Swift 제네릭 | Generic 함수, 타입, 제약」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
핵심 요약
- 제네릭 함수:
<T>타입 매개변수 - 제네릭 타입: Stack, Pair 등
- 제약: Equatable, Comparable 등
- where 절: 복잡한 제약 조건
- 연관 타입: 프로토콜의 제네릭
다음 단계
관련 글
- C++ Generic Lambda |
- C++ 템플릿 |
- C++ Template Lambda |
- Rust 트레이트 | Trait, 제네릭, 트레이트 바운드
- Swift 시작하기 | iOS 개발 공식 언어 완벽 입문
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Swift 제네릭에 대해 정리한 개발 블로그 글입니다. func swap
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Swift 프로토콜과 확장 | Protocol, Extension
- Swift 에러 처리 | do-catch, throw, Result
- Swift Combine | 반응형 프로그래밍 완벽 가이드
이 글에서 다루는 키워드 (관련 검색어)
Swift, Generic, 제네릭 등으로 검색하시면 이 글이 도움이 됩니다.