Swift 제네릭(Generic)에 대해 설명해주세요.

소혜 (Sohye)
xohxe (김소혜)· April 10, 2024

제네릭은 Swift의 가장 강력한 기능 중 하나로 Swift 표준 라이브러리의 대부분은 제너릭 코드로 구축되어있다. 바로 Array, Dictionary 이 대표적인 제네릭 타입이다. Swift를 사용하며 자각하지 못했을지 모르지만, Swift 전반에 걸쳐 사용한 제네릭에 대해 파헤쳐 보도록 하자.

1. 제네릭 타입 파라미터

먼저 제네릭 함수를 정의할 때의 예시 코드를 살펴보자.

COPY
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
	let temporaryA = a
	a = b
	b = temporaryA
}

여기서 T타입 파라미터(Type Parameter) 이다. T라는 새로운 형식이 생성되는 것이 아니라, 실제 함수가 호출될 때마다 해당 매개변수의 타입으로 대체되는 Placeholder이다.

실제 swapTwoValues함수가 호출될 때, 타입 파라미터인 T의 타입이 결정되는 것이다.

COPY
	var someInt = 1
	var aotherInt = 2
	swapTwoValues(&someInt,  &aotherInt)        
	// 함수 호출 시 T는 Int 타입으로 결정됨
 
 
	var someString = "Hi"
	var aotherString = "Bye"
	swapTwoValues(&someString, &aotherString)    
	// 함수 호출 시 T는 String 타입으로 결정됨

만약 일반함수로 작성한다면, 다른 타입들을 각각 다 작성해줘야한다. (Swift는 타입에 민감한 언어니까😭)

COPY
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

제네릭은 타입에 대해 제한을 두지 않는 코드를 사용하고 싶을 때 정말 유용하게 쓸 수 있다. 제네릭은 앞서 설명한 함수 func 말고도 구조체, 클래스, 열거형에도 선언할 수 있다.

대표적인 예가 Stack인데, 제네릭을 사용하면 모든 타입을 대상으로 동작할 수 있기때문에 타입별 Stack을 일일이 구현할 필요가 없다.

COPY
struct Stack<T> {
	let items: [T] = []
	
    mutating func push(_ item: T) { ... }
    mutating func pop() -> T { ... }
}

2. 제네릭 타입 제약(Generic Type Constraints)

앞서 제네릭으로 구현된 코드를 보면, 모든 타입에서 잘 동작할 수 있게할 수 있다.

하지만 특정한 타입만 받게 하면 좋을 때가 있지 않을까?

아래 코드를 먼저 살펴보자.

COPY
func findIndex<T>(of valueToFind: T, in array:[T]) -> 
	Int? {
	
	for (index, value) in array.enumerated() {        
		if value == valueToFind {            
			return index        
		} 
	}    
	return nil
} 

이 코드에서는 Binary operator '==' cannot be applied to two 'T' operands 오류가 발생한다. 이는 모든 타입이 ==라는 연산자로 비교될 수 없기 때문이다.

이럴 때는 T라는 타입 파라미터에 Equatable을 붙여 특정 프로토콜을 준수하는 타입만 받을 수 있게 제약을 두면, 보다 안전하게 제네릭을 사용할 수 있다.

COPY
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

3. 제네릭 사용 시 주의할 점

  1. Complexity 복잡성
    제네릭은 코드를 더 유연하고 재사용 가능하게 만들지만, 앞서 예시처럼 일반적으로 여러 타입에서 동작하기 때문에 모든 타입에서 효율적이지 않을 수 있다. 코드를 더 복잡하게 만들 수도 있으니 명확한 이점을 제공하고, 코드를 읽기 쉽게 유지하는 경우에 제네릭을 사용해야한다.
  2. Type Constraint 타입제약
    제네릭 타입에 적용하는 제약조건에 유의해야한다. 지나치게 제한하면 일반 코드의 재사용이 제한될 수 있고, 제한이 부족하면 런타임 오류나 예상치 못한 동작이 발생할 수 있다.
  3. Type Inference 타입 추론
    Swift의 타입 추론은 일반적으로 제네릭과 함께 사용 중인 타입을 파악할 수 있지만, 때로는 모호성을 피하거나 컴파일러를 안내하기 위해 명시적인 타입을 제공해야 할 수도 있다.

4. 제네릭은 왜 사용하는가?

제네릭을 사용하는 이유는 다음과 같이 정리할 수 있다.

  1. 안전성있는 코드를 사용할 수 있다.
    제네릭을 사용하면 모든 유형에서 작동하지만, 타입 제약을 통해 특정 프로토콜을 준수하는 타입만 받을 수 있다. 이를 통해 컴파일러에서 확인되는 방식으로 코드를 작성할 수 있는데, 이는 Swift의 강력한 유형 안전 기능을 유지할 수 있음을 의미한다.

  2. 코드 재사용을 줄일 수 있다.
    다른 유형에 대해 함수를 여러 번 작성할 필요 없이 함수를 한 번 작성하면 여러 유형에 사용할 수 있어, 똑같은 내용의 함수를 오버로딩 할 필요없다. 즉, 타입에 대해 제한을 두지 않는 코드를 작성하고 싶다면 제네릭을 사용을 추천한다.

다음 게시물
델리게이션 패턴(Delegation Pattern)과 클로저의 차이점은 무엇인가요?
이전 게시물
Swift에서 프로토콜(Protocol)이란 무엇이며, 어떻게 사용하나요?

© 2023 - 2024 xohxe. All Rights Reserved.