🔥 제네릭 타입

567자
8분

Swift에서는 함수뿐만 아니라 사용자 정의 타입(custom class, structure, enumeration)에도 제네릭을 적용할 수 있습니다. 제네릭 타입(Generic Types)을 정의하면 ArrayDictionary처럼 어떤 타입과도 함께 동작하는 유연한 코드를 작성할 수 있게 됩니다.

이번 절에서는 Stack이라는 제네릭 컬렉션 타입을 작성해 보겠습니다. 스택(stack)은 배열과 유사하게 값을 순서대로 저장하는 자료구조입니다. 하지만 Swift의 Array 타입보다는 제한된 연산만 제공합니다. 배열에서는 어떤 위치에서든 새로운 요소를 삽입하거나 제거할 수 있지만, 스택에서는 컬렉션의 끝에만 새로운 요소를 추가(push)하거나 제거(pop)할 수 있습니다.

아래 그림은 스택의 push와 pop 동작을 보여줍니다:

lecture image

  1. 현재 스택에는 3개의 값이 있습니다.
  2. 스택의 맨 위에 네 번째 값을 push합니다.
  3. 이제 스택에는 4개의 값이 있고, 가장 최근에 추가된 값이 맨 위에 있습니다.
  4. 스택의 맨 위 값을 pop합니다.
  5. 값을 pop한 후에는 스택에 다시 3개의 값만 남아 있습니다.

먼저 제네릭이 아닌 버전의 Int 값만 저장하는 스택을 작성해 보겠습니다:

struct IntStack {
    var items: [Int] = []
 
    mutating func push(_ item: Int) {
        items.append(item)
    }
 
    mutating func pop() -> Int {
        return items.removeLast()
    }
}
swift

이 구조체는 items라는 Array 속성을 사용해 스택의 값들을 저장합니다. 그리고 pushpop 메서드를 통해 스택에 값을 push하거나 pop할 수 있습니다. 이 메서드들은 items 배열을 수정(mutate)해야 하므로 mutating 키워드로 표시되어 있습니다.

하지만 위의 IntStack 타입은 오직 Int 값만 사용할 수 있습니다. 훨씬 더 유용하게 하려면 어떤 타입의 값이라도 관리할 수 있는 제네릭 Stack 구조체를 정의해야 합니다.

제네릭 버전의 코드는 다음과 같습니다:

struct Stack<Element> {
    var items: [Element] = []
 
    mutating func push(_ item: Element) {
        items.append(item)
    }
 
    mutating func pop() -> Element {
        return items.removeLast()
    }
}
swift

제네릭 버전의 Stack이 제네릭이 아닌 버전과 본질적으로 동일하다는 점에 주목하세요. 다만 실제 Int 타입 대신 Element라는 타입 매개변수를 사용했을 뿐입니다. 이 타입 매개변수는 구조체 이름 바로 뒤의 꺾쇠 괄호(<Element>) 안에 작성됩니다.

Element는 나중에 제공될 실제 타입의 플레이스홀더 이름을 정의합니다. 이 미래의 타입은 구조체 정의 내의 어디에서든 Element로 참조될 수 있습니다. 여기서는 Element가 세 군데에서 플레이스홀더로 사용되었습니다:

  • Element 타입의 빈 배열로 초기화되는 items라는 속성을 생성할 때
  • push(_:) 메서드가 Element 타입의 item이라는 단일 매개변수를 받도록 지정할 때
  • pop() 메서드가 Element 타입의 값을 반환하도록 지정할 때

제네릭 타입이므로 StackArrayDictionary와 유사한 방식으로 Swift의 모든 유효한 타입의 스택을 생성하는 데 사용될 수 있습니다.

새로운 Stack 인스턴스를 생성하려면 스택에 저장할 타입을 꺾쇠 괄호 안에 작성하면 됩니다. 예를 들어, 새로운 문자열 스택을 생성하려면 Stack<String>()이라고 작성합니다:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 스택에는 이제 4개의 문자열이 포함됨
swift

이 네 값을 스택에 push한 후의 stackOfStrings는 다음과 같습니다:

lecture image

스택에서 값을 pop하면 맨 위의 값인 "cuatro"가 제거되고 반환됩니다:

let fromTheTop = stackOfStrings.pop()
// fromTheTop은 "cuatro"와 같고, 스택에는 이제 3개의 문자열만 포함됨
swift

맨 위의 값을 pop한 후의 스택은 다음과 같습니다:

lecture image

제네릭 타입을 사용하면 코드의 재사용성이 크게 향상됩니다. 동일한 코드를 여러 타입에 걸쳐 사용할 수 있기 때문입니다. Stack 구조체의 제네릭 구현은 Int, String, Double 등 어떤 타입의 값이라도 저장할 수 있습니다. 이는 마치 범용 부품을 조립해서 다양한 제품을 만드는 것과 같습니다.

타입 안정성도 보장됩니다. 제네릭을 사용하면 특정 타입에 종속되지 않고 추상화된 방식으로 코드를 작성할 수 있습니다. 컴파일러는 제네릭 코드가 실제 사용되는 시점에 타입 검사를 수행하므로, 런타임에 예기치 않은 타입 관련 오류가 발생할 가능성이 줄어듭니다.

성능 측면에서도 이점이 있습니다. Swift 컴파일러는 제네릭 코드를 실제 사용되는 구체적인 타입에 맞춰 특수화(specialization)합니다. 이를 통해 간접 비용(overhead) 없이 정적 디스패치(static dispatch)를 활용할 수 있어 런타임 성능이 향상됩니다.

요컨대 제네릭은 Swift에서 추상화와 코드 재사용을 가능케 하는 강력한 도구입니다. 제네릭을 활용하면 중복 코드를 제거하고, 타입 안정성을 보장하며, 성능을 향상시킬 수 있습니다. 제네릭 코드 작성에 익숙해지는 것이 좋은 Swift 개발자가 되는 데 중요한 역할을 할 것입니다.