Skip to content →

Generic? 꺽쇄 있는 그거?

Last updated on 2019년 11월 12일

Generic이 뭔지는 대충 알고 있었지만, “꺽쇄 있는 그거”로 대충 뭉개고 이해하고 있었는데, 이번에 Swift 5.1의 여러 기능들이 Generic과 연관이 많다고 해서 제대로 짚고 넘어가보려 합니다. 그리고 Generic의 효용을 알기 위해서 먼저 Swift의 타입시스템을 잠시 들여다 보겠습니다.

Swift의 타입시스템

Swift는 아주 강력한 타입시스템을 가진 언어입니다. 즉, 아래와 같은 JavaScript코드는 Swift에서는 빌드되지 않습니다.

function sum(a, b) {
    return a + b
}
sum(1,2) // returns 3
sum("1", 2) // returns "12"
1 == "1" // returns true
1 === "1" // returns false
func sum(a: Int, b: Int) {
    return a + b
}
sum(1,2) // returns 3
sum("1", 2) // Build Error!!
1 == "1"  // Build Error!! (Not even False)

이런 강력한 타입시스템은, 훨씬 더 안전하고 버그가 없는 프로그램을 만드는데 크게 기여합니다. 예를 들어 “1” + 1 = “11” 일 수도 있지만, “1” + 1 = 2 일 수도 있죠. 프로그램을 돌려보기 전 까지는 이 언어가 어떤 식으로 동작 할 지 예측 할 수 없습니다.

즉, JavaScript가 “이런들 어떠하리 저런들 어떠하리” 하면서 태연하게 두 값을 더해 버린다면, Swift는 “이 몸이 죽고죽어 일백번을 고쳐죽어도 숫자랑 문자열은 못 더한다!!” 라고 주장하는 언어입니다.

타입시스템의 안 좋은 점

그런데 이런 “안전함”에 따라오는 TradeOff가 있습니다. 예컨대, 두 정수를 더하는 함수를 만들어 봅시다.

func sum(a: Int, b: Int) -> Int {
    return a + b 
}
sum(1,2) // 3

아, 잘 더해집니다. 하지만 위 함수에는 큰 단점이 있습니다. 바로 Float형태의 값들은 다루지 못한다는 점입니다.

sum(1.0, 2.0) // Couldn't convert Double 1.0 into Int 

sum 함수는 당연히 정수형 값이 들어올 거라고 기대하고 있었는데 Float값이 들어오니, 당연히 불만을 가질 수 밖에 없지요.

따라서 우리는 어쩔 수 없이 Float 버전의 sum 함수를 만들어야만 합니다.

func sum(a: Float, b: Float) -> Float {
    return a + b
}
sum(1.0, 2.0) // 3.0

그리고 예상 했겠지만, Double 버전도 만들어야 합니다.

func sum(a: Double, b: Double) -> Double {
    return a + b
}

에… 또.. CGFloat 버전도 만들까요?

func sum(a: CGFloat, b: CGFloat) -> CGFloat {
    return a + b
}

음… 뭔가 바보같은 짓을 하고 있다는 생각이 슬슬 듭니다. JavaScript 개발자분들이 “거 봐 타입시스템이 있으면 이런 뻘짓거리를 하게 된다니까~” 하는 소리가 벌써 여기까지 들리네요.

Protocol을 써 볼까요?

이 쯤 되니까 이런 생각이 듭니다. Summable이라는 Protocol을 만들고, 이 프로토콜을 파라미터로 받는 함수를 만들면 어떨까요?

protocol Summable {
    static func +(lhs:Self, rhs: Self) -> Self
    // lhs와 rhs는 Summable 해야 하고, 그 둘과 리턴값의 Concrete Type은 같아야 합니다.
}

extension Int: Summable {}
extension Double: Summable {}
// Int나 Double은 처음부터 static func +(lhs:Self, rhs: Self) -> Self 가 구현되어 있기 때문에
// Summable 프로토콜을 채택하기만 하면 됩니다.

func sum(a: Summable, b: Summable) -> Summable {
    a + b
}

위 코드를 실행 해보면 컴파일이 되지 않을 겁니다. 위에서 작성한 sum 함수는, “a와 b는 Summable 해야 한다”고만 말하고 있습니다. 하지만 정말로 Summable 프로토콜에 부합하려면

  1. a와 b는 Summable 할 뿐만 아니라
  2. 같은 Concrete Type을 가져야 한다! (a가 Int이면 b도 Int여야 한다!)

는 두 개의 조건을 모두 충족시켜야 합니다.

따라서 2번 조건을 만족시키기 위해, 우리는 Concrete 타입 에 대한 정보를 추가로 전달 해 줘야 합니다.

Generic Function

func sum<T: Summable>(a: T, b: T) -> T {
    return a + b
}

sum(1,2) // 3
sum(1.2,1.3) // 2.5

이렇게 완성된 함수 sum을 우리는 “Generic Function”이라고 말합니다. 그리고 “T”에 해당하는 부분을 “Type Parameter”라고 부릅니다. 즉 sum 함수는 a, b, 그리고 a와b의 타입 까지 총 3개의 parameter를 받는 것입니다.

정리하자면,

  • Swift에서 함수는 원래 파라미터 및 리턴값의 타입을 명시합니다. (ex : func sum(a: Int, b: Int) → Int )
  • 그런데 “타입”도 파리미터가 될 수 있습니다! (ex: sum(a: T, b: T) → T)
  • “타입” 을 파라미터로 함께 받아서 여러 타입에서 General 하게 쓰일 수 있는 함수를 Generic Function 이라고 부릅니다.

Generic Type

General하게 쓰이는 Generic Function이 있는 만큼, General하게 쓰이는 Generic Type도 생각 해 볼 수 있습니다. 사실 우리가 Foundation 등에서 자주 쓰는 많은 클래스들이 GenericType들입니다.

예를 들어, Stack 자료구조를 간단하게 구현 해 봅시다. 먼저 Generic 하지 않게 만들어봅시다.

struct IntStack {
    var list:[Int] = []

    func push(_ element: Int) {
        list.append(element)
    }

    func pop() -> Int {
        return list.popLast()
    }
}

struct FloatStack {
    var list:[FloatStack] = []

    func push(_ element: FloatStack) {
        list.append(element)
    }

    func pop() -> FloatStack {
        return list.popLast()
    }
}

...

문제점이 바로 눈에 보이지요? 이 방법대로라면, Swift에 존재하는 모든 자료형의 개수만큼 struct를 만들어야 합니다. 전혀 확장성이 없지요.

그럼 이렇게 만들면 어떨까요?

struct Stack {
    var list: [Any] = []

    func push(_ element: Any) {
        list.append(element)
    }

    func pop() -> Any {
        return list.popLast()
    }
}

이것도 방법이지만, 이런 형태라면

var aStack = Stack()
aStack.push(1)
aStack.push("2")

도 가능해집니다. 이걸 원한 거라면 상관 없지만, “같은 타입의 값들 로만 스택을 만들게 강제하고 싶다”면 어떨까요?

struct Stack<Element> {
    var list: [Element] = []
    func push(_ element: Element) {
        list.append(element)
    }

    func pop() -> Element {
        return list.popLast()
    } 
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push("2") // Error!!

var stringStack = Stack<String>()
stringStack.push("1")
stringStack.push(2) // Error!!

이렇게 “타입파라미터(Element)”를 초기화 할 때 함께 지정 해주는 타입을 Generic Type이라고 합니다. 우리가 아주 많이 쓰는 Array, Dictionary 등이 모두 이런 Generic Type이지요.

마치며

제가 Generic 개념을 배우면서 많이 헷갈렸던 이유는, Generic을 “타입의 일종” 이라고 생각했기 때문입니다. 하지만 Generic은 형용사입니다. Generic이라는 타입이 있는 게 아니라, Generic 한 타입 or Generic 한 함수 가 있는 것입니다. 그리고 “Generic 하다” 라는 것은 “타입도 파라미터로 받는다 → 여러 타입들과 General 하게 사용 가능하다” 는 의미입니다.

혹시 여태까지 저처럼 Generic을 “꺾쇄 있는 그 어떤 거”라고 대충 뭉개서 생각하고 계시지 않으셨나요? 그렇다면 이번 기회에 Generic에 대한 개념을 확실히 다지시기 바랍니다!

참고문헌

Published in Basics 프로그래밍

Comments

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 블로거가 이것을 좋아합니다: