Generics

이원희·2021년 3월 7일
0

 🐧 Swift

목록 보기
26/32
post-thumbnail

제네릭에 대해서 오늘 포스팅할 생각은 없었는데...ㅋㅋ
나는 제네릭에 대해서 그래도 알고 있다고 생각했는데 누가 물어볼때 간결하게 대답을 못하더라....ㅋㅋㅋㅋㅋㅋㅋ
그래서 쓰는 제네릭 포스팅...!
물론 이번 포스팅도 공식 문서를 기준으로 작성했다.

Generics

제네릭을 사용하면 모든 타입에 작동할 수 있는 코드를 작성할 수 있다.
유연하고 재사용 가능한 함수 및 타입을 작성할 수 있음에 따라 중복을 피하고 명확하고 추상적인 의도를 표현할 수 있다.

제네릭은 Swift의 강력한 기능 중 하나이다.
많은 Swift의 라이브러리에서 제네릭을 사용한다.
ArrayDictionary도 제네릭을 사용하고 있다.

그럼 아래의 과정을 따라서 제네릭을 알아가보자.


The Problem That Generics Solve

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

이 함수는 inout 파라미터를 사용해 a와 b를 바꿔주고 있다.
함수에서 a와 b의 타입을 Int로 선언했으므로 해당 함수는 두개의 Int 변수만 바꿔줄 수 있다.

그렇다면 Int가 아닌 String 변수를 바꿔주고 싶다면?

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

위와 같은 함수를 정의해야 한다.

흠... 근데 사실상 위의 두 함수를 잘 보면 함수명과 파라미터의 타입만 다르고 구현부는 똑같은걸 확인할 수 있다.

이러한 중복을 줄이고 더 유연하게 대응하기 위해서 제네릭을 사용할 수 있다.


Generic Functions

그렇다면 위의 함수를 제네릭을 통해 개선해보자.

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

제네릭으로 구현한 함수에서는 IntString같은 실제 타입 이름이 아닌 T를 타입 이름 placeholder로 사용하고 있다.
이 placeholder 타입 이름은 어떤 구체적인 타입을 의미하지는 않지만 a와 b가 같은 타입여야만 한다.
(a와 b의 타입이 T로 정해져 있으므로)

제네릭을 사용한 함수는 정의부를 살펴보면 어떤 구체 타입을 넣어야하는지 알 수 없다.
언제 알 수 있을까?
해당 함수가 호출되는 부분에서 구체 타입은 정해진다.

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

위의 코드를 보면 a와 b에 동일한 타입의 변수를 넣어주는 것을 볼 수 있다.
swapTwoValues()는 호출될때 구체 타입이 정해진다고 했다.
그럼 첫번째 swapTwoValues() 호출에서 TInt로 정해지고, 두번째 swapTwoValues() 호출에서 TString으로 정해진다.

T? U? V?

위의 예시에서는 T로 제네릭 타입을 지정했지만 다른 타입을 사용할 수도 있다.

T, U, V 등이 있다.
그리고 이들은 동시에 입력 받을 수 있다.

func testMultiType<T, U>(a: T, b:U) -> U {}

이런 식으로!

Dictionary<Key, Value>,Array<Element>의 Key, Value, Element 같이 설명을 포함하는 이름을 갖는 경우도 있다.
이는 특별한 의미를 갖는거는 아니고 읽는 사람이 타입 파라미터와 제네릭 혹은 함수와의 관계를 알 수 있는 역할일 뿐이다.


타입 제한

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

위의 코드는 어떤 타입이든 T의 구체 타입으로 사용할 수 있다.
그렇다면 이 T에 어떤 class를 상속 받아야 한다던지 어떤 protocol을 준수해야 한다던지 타입을 제한할 수 있을까?


사용

흠... 그렇다면 다음과 같은 상황을 가정해보자.
서버에서 받은 JSON data를 내가 원하는 data로 디코딩하고 싶다.
디코딩할 data 타입이 하나라면 제네릭을 사용할 필요 없지만 여러개라면 코드 중복을 피하기 위해서라도 제네릭이 필요하다.
(일단 그러라고 있는 제네릭이니까)

struct Person: Decodable {
    let id: String
}

func decode<T>(dataType: T.Type) -> T? {
    guard let decodedData = try? JSONDecoder().decode(dataType, from: Data()) else {
        return nil
    }
    return decodedData
}

decode(dataType: Person.self)

그래서 JSON data를 내가 정의한 타입으로 디코딩하는 함수를 제네릭을 통해 선언했다.

  • Person: 해당 타입으로 Decoding 하기 위해서는 Decodable 프로토콜을 준수해야 한다.
  • decode(): <T>를 사용해 제네릭을 구현했다.

흠... 과연 이렇게하면 잘 돌아갈까?

T가 Decodable을 채택해야 한다고 한다!

struct Person: Decodable {
    let id: String
}

func decode<T: Decodable>(dataType: T.Type) -> T? {
    guard let decodedData = try? JSONDecoder().decode(dataType, from: Data()) else {
        return nil
    }
    return decodedData
}

decode(dataType: Person.self)

이렇게 변경해주면 아주 잘된다!

위의 경우는 디코딩할때 무조건 디코딩하려는 타입이 Decodable을 채택해야만 하기 때문에 T가 프로토콜을 준수해야 하는 경우이다.

만약, 그런 조건이 없었다면?

decode()Decodable을 준수하지 않는 타입을 구체 타입으로 사용한다면 컴파일 에러가 날 것이다.

아무튼... 이런 식으로 제네릭 타입에 제한을 줄 수 있다.


Generics vs Any

그렇다면 Generics과 Any는 어떤 차이점이 있을까?
우선 공통점을 확인해보자.

Any는 Swift의 모든 타입을 사용할 수 있다.

var anyArray: [Any] = []
anyArray.append(1)
anyArray.append("hi")
anyArray.append(2.0)

즉, 위의 코드가 가능하다는 말이다.

우리가 위에서 살펴본 Generics도 구체 타입으로 모든 타입을 사용할 수 있다.

그렇다면 어떤 차이점이 있을까?

Any는 어떤 값이든 지정할 수 있는 대신에 값을 사용할때 캐스팅 과정을 거쳐야 한다.
(캐스팅에 대한 문서는 여기에 있다.)

Generics은 위에서 살펴본 바와 같이 호출할때 구체 타입이 정해진다.
따라서 Any처럼 캐스팅 작업이 필요 없다.

또한, Any는 런타임에 해당 타입을 알 수 있고, Generics는 컴파일 타임에 타입을 알 수 있다.


마무리

제네릭에 대해서 알아봤다.
이제는 누가 물어보면 대답할 수 있겠지...ㅋㅋㅋ
그럼 이만👋

0개의 댓글