[WWDC23] 매개변수 팩으로 API 범용화하기

kimseawater
daily-monster
Published in
13 min readMay 14, 2024

안녕하세요 화요일에 돌아오는 kimseawater입니다!

저희의 앞날에 대한 내부적인 회의 (?)를 거쳐서 약간의 규칙 조정이 있었는데요 .. 그래서 앞으로 매주 오진 않을 것 같고여… 대신 좀 더 깊이 공부하고 가져와보겠습니다…

이번 주에는 Swift 5.9에서 나온 매개변수 팩에 대한 WWDC를 시청해봤습니다!

Parameter Pack이 왜 필요한가요?

let x: Int = 10

이런 코드가 있다고 해봅시다. 이 코드는 값, 타입으로 구성되고 값을 추상화하려면, 서로 다른 값을 매개변수로 받는 함수를 작성하면 됩니다.

func radians(from degrees: Double) -> Double

radians(from: 100)
struct Array<Element>

Array<Int>

타입을 추상화하려면 제네릭을 사용하면 됩니다. 위에서 Element는 플레이스 홀더에요. 제네릭 코드 다수는 타입과 값을 모두 추상화합니다.

서버에 요청하고, 응답을 받는 func query()라는 함수가 있다고 해봅시다. 그리고 이는 응답으로 Payload라는 하나의 객체를 리턴합니다. 만약에 하나의 함수에 여러 요청을 담아서 전달하고 싶으면 어떻게 해야 할까요?

func query(_ item: Requst...) -> ???

이런식으로 가변매개변수를 써주면 됩니다. 하지만, 가변 매개변수를 쓰면 매개변수는 여러개 보낼 수 있지만.. 응답을 가변으로 선언해줄 수가 없다는 문제가 있어요..

또 만약 여러 개를 받고는 싶은데 이때 타입을 다르게 받고 싶다고 해봅시다. 근데 가변 매개변수를 쓰면 이 타입이 type eraser되어있지 않는 이상 쓸 수가 없고, 각 변수의 구체적인 타입 정보를 가지고 있을 수가 없습니다.

만약 리턴값으로 파라미터 개수만큼 튜플을 받고 싶으면, 결국 하나씩 다 함수를 선언해줘야만 합니다.

func query<Payload>(
_ item: Request<Payload>
) -> Payload

func query<Payload>(
_ item1: Request<Payload>,
_ item2: Request<Payload>
) -> (Payload1, Payload2)

func query<Payload>(
_ item1: Request<Payload>,
_ item2: Request<Payload>,
_ item3: Request<Payload>
) -> (Payload1, Payload2, Payload3)

… 언제까지 써야하죠..?

이런 문제를 parameter pack이 해결해줍니다!!!!

위처럼 뭔가 계속 오버로딩이 필요한 상황이라면, 이때가 바로 매개변수 팩을 사용해볼 때 입니다~~

Parameter Pack 어떻게 읽죠

일단 어떤 상황에서 파라미터 팩이 필요한지는 봤고, 이제 이게 어떻게 생긴것인지를 봅시다.

보통 코드는 이런식으로 하나의 타입, 값으로 작업을 하죠

type pack

매개변수 팩은 여러 타입이나 값을 하나로 뭉쳐서 파라미터로 만들고, 이를 함수에 전달할 수 있게 합니다.

여기서 타입을 각각 담은 팩을 type pack이라고 부릅니다.

value pack

그리고 값을 담은 팩은 value pack이라고 합니다.

그래서 이런식으로 type pack과 value pack은 같이 쓰이게 되고, 각각에 타입과 값을 하나씩 대응시킵니다. 대응하는 타입과 값은 각자의 팩 안에서 동일한 위치에 존재하게 됩니다.

for request in requests {
evaluate(request)
}

컬렉션은 이런식으로 작동하는데요,,

Parameter pack을 적용하면 하나의 제네릭 코드만 작성해도, 코드가 pack에 든 모든 요소에 대해 각각 작동하게 됩니다. 위의 컬렉션과 비슷하게요 ㅎ

보통 제네릭을 쓸 때 타입 매개변수는 이런식으로 작성하는데요

타입 매개변수 팩의 경우는 이런식으로 선언합니다. 이렇게 만들면, 함수가 타입 매개변수를 하나만 갖는 대신에 각각 구체적인 페이로드 타입을 받아들일 수 있게 됩니다.

함수에 적용하면 이런식으로 되는 것이죠 ㅎㅎ

여기서 또 주의할점은 잘 읽히려면 Payloads같은 복수형 아니고, 단수형으로 선언해주는게 좋습니다.

repeat이라는 키워드를 이용하면 주어진 parameter pack의 각 요소에, 저런 패턴이 반복될 것임을 나타냅니다. each는 플레이스 홀더로 반복이 될때마다 pack에 든 각 요소로 대체됩니다.

이런 상황이라면

이런식으로 바뀔 수 있죠~

repeat같은 경우에는 각각의 타입을 쉼표로 구분하기 때문에, 쉼표로 구분이 가능한 곳에서만 사용할 수 있습니다. 쉼표로 구분된 목록은 함수 파라미터나 제네릭 인수 등이 있습니다.

이렇게 만들면, 호출하는 쪽에서 요청 인스턴스 개수를 임의로 보낼 수 있게 되고, parameter들은 parameter팩에 묶여서 함수에 전달됩니다.

func query<Payload>(
_ item: Request<Payload>
) -> Payload

func query<Payload>(
_ item1: Request<Payload>,
_ item2: Request<Payload>
) -> (Payload1, Payload2)

func query<Payload>(
_ item1: Request<Payload>,
_ item2: Request<Payload>,
_ item3: Request<Payload>
) -> (Payload1, Payload2, Payload3)

아까 이렇게 썼던거 기억나시죠..? 이제 이걸 파라미터 팩을 이용하면

func query<each Payload>(_ item: repeat Request<each Payloads>) -> (repeat each Payload)

이렇게 아주 간단하게 인수에 상관없이, 파라미터와 리턴의 길이가 똑같은 함수를 작성할 수 있게 됩니다.

let results = query(Request<Int>(), Request<String>(), Request<Bool>())

이런식으로 사용할 수 있어요.

이 코드에서 보면 결국 구체적인 파라미터 팩은 호출부분의 파라미터로부터 추론된 것이고, each Payload의 플레이스 홀더에 해당하는 구체적인 타입은 결국 호출할때 파라미터 목록에서 수집되고, 이것들이 팩으로 묶이고, 반환타입으로 생성되는 것이죠,,

(Request<Int>, Request<String>, Request<Bool>) -> (Int, String, Bool)

이런식으로 됩니다~

parameter pack에 제약걸기

그럼 특정 제약을 만족하는 파라미터 타입만 들어올 수 있게 하려면 어떻게 해야할까요? 기존에 타입 제약 걸어주던 거랑 비슷하게 걸어주면 됩니다.

// 방법1
func query<each Payload: Equatable>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
// 방법2
func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload)
where repeat each Payload: Equatable

하나 이상이 들어오도록 제약 걸기

만약 매개변수 팩에 든 인수가 꼭 하나 이상이도록 하고 싶으면 어떻게 하면 될까요? 위의 방법으로는 사실 0개도 들어올 수 있으니까..! 최소는 1개부터 하고 싶다면 아래와 같이 해주면 됩니다.

func query<FirstPayload, each Payload>(
_ first: Request<FirstPayload>, _ item: repeat Request<each Payload>
) -> (FirstPayload, repeat each Payload)
where FirstPayload: Equatable, repeat each Payload: Equatable

이렇게 하면 호출할 때 꼭 하나 이상의 파라미터를 적어줘야 합니다.

Parameter Pack 사용해보기

struct Request<Payload> {
func evaluate() -> Payload
}

func query<each Payload>(_ item: repeat Request<each Payload>) -> (repeat each Payload) { }

이런 함수가 있다고 해봅시다. 그리고 각 item에 대한 evalueate() 메서드를 호출하고 싶다면 어떻게 해야할까요?

repeat (each item).evaluate()

이렇게 써주면 됩니다. 이렇게 하면 팩 내에 든 모든 값을 통해서 반복됩니다.

(repeat (each item).evaluate())

이렇게 감싸주면, 투플안에 들어가게 됩니다.

struct Request<Payload> {
func evaluate() -> Payload
}

func query<each Payload>(
_ item: repeat Request<each Payload>
) -> (repeat each Payload) {
return (repeat (each item).evaluate())
}

이렇게 해주면 팩에든 여러 매개변수를 evaluate해서 각각의 결과를 투플로 반환해주게 됩니다.

위의 예제를 더 리팩토링 해봅시다.

struct Request<Payload> {
func evaluate() -> Payload
}

struct Evaluator<each Payload> {

var item: (repeat Request<each Payload>)

func query(
_ item: repeat Request<each Payload>
) -> (repeat each Payload) {
return (repeat (each item).evaluate())
}
}

이렇게 함수에서 Evaluator타입으로 타입 매개변수 팩을 옮겨 봅시다. 그리고 이를 프로퍼티 안에 저장할 수 있게 해봅시다.

protocol Request {
associatedType Input
associatedType Output

func evaluate(_ input: Input) -> Output
}

struct Evaluator<each Payload> {

var item: (repeat Request<each Payload>)

func query() -> (repeat each Payload) {
return (repeat (each item).evaluate())
}
}

Request를 프로토콜로 바꾸고, Input과 Output이라는 associatedType을 지정해줍니다. 이렇게 하면, 메서드 반환타입이 파라미터 매개변수 타입과 달라질 수 있게 됩니다.

protocol RequestProtocol {
associatedType Input
associatedType Output

func evaluate(_ input: Input) -> Output
}

struct Evaluator<each Request: RequestProtocol> {

var item: (repeat Request)

func query() -> (repeat each Request) {
return (repeat (each item).evaluate())
}
}

그리고 이름을 바꿔줍니다.

protocol RequestProtocol {
associatedType Input
associatedType Output

func evaluate(_ input: Input) -> Output
}

struct Evaluator<each Request: RequestProtocol> {

var item: (repeat Request)

func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output) {
return (repeat (each item).evaluate(each input))
}
}

이렇게 해주면 query 메서드는 Input타입을 받아서 Output타입으로 반환하게 됩니다.

만약 반복 흐름을 일찍 종료해야 한다면 어떻게 해야 할까요? 이때는 throw를 해줄 수 있고, do-catch를 넣어서 반복을 종료할 수 있습니다.

protocol RequestProtocol {
associatedType Input
associatedType Output

func evaluate(_ input: Input) -> Output
}

struct Evaluator<each Request: RequestProtocol> {

var item: (repeat Request)

func query(
_ input: repeat (each Request).Input
) -> (repeat (each Request).Output)? {
do {
return (repeat try (each item).evaluate(each input))
} catch {
return nil
}
}
}

아주 신기한 기능이네요.. 함 적용해봐야겠어요 ㅎ

--

--