🔥 제네릭 Where절

549자
7분

Swift의 제네릭은 코드의 재사용성과 유연성을 크게 향상시켜 줍니다. 제네릭을 사용하면 타입에 구애받지 않고 다양한 타입에 대해 동작하는 함수나 타입을 정의할 수 있습니다. 이런 제네릭의 강력한 기능 중 하나가 바로 "제네릭 where절"이에요.

제네릭 where절을 사용하면 제네릭 타입 매개변수나 연관 타입에 대한 추가적인 제약 조건을 명시할 수 있어요. 이를 통해 제네릭 코드의 유연성을 유지하면서도 타입 안정성을 확보할 수 있죠.

제네릭 Where절의 문법

제네릭 where절은 where 키워드 뒤에 제약 조건을 명시하는 형태로 작성됩니다. 제약 조건에는 다음과 같은 내용들이 포함될 수 있어요:

  • 연관 타입이 특정 프로토콜을 준수해야 한다는 것
  • 타입 매개변수나 연관 타입 간의 동일성

제네릭 where절은 타입이나 함수 본문의 시작 중괄호({) 바로 앞에 위치하는데요, 아래는 제네릭 where절의 간단한 예시 코드예요:

func someFunction<T: SomeProtocol, U: SomeProtocol>(a: T, b: U) -> Bool
    where T.SomeType == U.SomeType, T.AnotherType: AnotherProtocol {
        // ...
}
swift

위 코드에서는 두 개의 타입 매개변수 TU가 모두 SomeProtocol을 준수해야 해요. 추가로 where 절을 통해 아래와 같은 제약 조건을 명시하고 있네요:

  • T의 연관 타입 SomeTypeU의 연관 타입 SomeType이 동일해야 함
  • T의 연관 타입 AnotherTypeAnotherProtocol을 준수해야 함

이처럼 제네릭 where절을 활용하면 타입 매개변수와 연관 타입에 대해 매우 세밀한 제어가 가능해집니다!

제네릭 Where절 활용 예제

제네릭 where절이 실제로 어떻게 활용될 수 있는지 좀 더 구체적인 예제 코드를 통해 알아볼까요?

아래 코드는 두 컨테이너가 동일한 아이템들을 동일한 순서로 담고 있는지 검사하는 제네릭 함수랍니다:

func allItemsMatch<C1: Container, C2: Container>
        (_ someContainer: C1, _ anotherContainer: C2) -> Bool
        where C1.Item == C2.Item, C1.Item: Equatable {
 
    // 두 컨테이너의 아이템 개수가 같은지 검사해요
    if someContainer.count != anotherContainer.count {
        return false
    }
 
    // 각 아이템 쌍이 동등한지 검사합니다
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
 
    // 모든 아이템이 일치하므로 true 반환하네요
    return true
}
swift

위 함수는 someContaineranotherContainer라는 두 개의 매개변수를 받아요. someContainer의 타입은 C1, anotherContainer의 타입은 C2인데, 이 C1C2는 함수가 호출될 때 결정될 컨테이너 타입들을 나타내는 타입 매개변수랍니다.

함수 선언부의 제네릭 where절을 통해 아래와 같은 제약 조건들을 설정하고 있어요:

  • C1Container 프로토콜을 준수해야 함
  • C2Container 프로토콜을 준수해야 함
  • C1의 연관 타입 ItemC2의 연관 타입 Item이 동일해야 함
  • C1의 연관 타입 ItemEquatable 프로토콜을 준수해야 함

이런 제약 조건 덕분에 비록 서로 다른 컨테이너 타입이더라도 allItemsMatch 함수 내에서 두 컨테이너를 비교하는 것이 가능해집니다!

allItemsMatch 함수는 먼저 두 컨테이너의 아이템 개수가 동일한지 검사해요. 개수가 다르다면 두 컨테이너가 일치할 가능성이 없으므로 바로 false를 반환하죠.

개수가 같다면 for-in 루프를 통해 someContainer의 모든 아이템들을 순회하면서, 각 아이템이 anotherContainer의 같은 인덱스에 있는 아이템과 일치하는지 검사합니다. 만약 일치하지 않는 아이템 쌍이 발견되면 역시 false를 반환하게 되요.

루프가 끝날 때까지 불일치하는 아이템 쌍이 발견되지 않았다면, 두 컨테이너는 완전히 일치하는 것이므로 true를 반환하네요.

실제 사용 예시를 보면 제네릭 where절 덕분에 allItemsMatch 함수가 매우 유연하게 동작하는 것을 확인할 수 있어요:

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
 
var arrayOfStrings = ["uno", "dos", "tres"]
 
if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// "All items match." 출력됩니다
swift

StackArray는 서로 다른 타입의 컨테이너지만, 둘 다 Container 프로토콜을 준수하고 동일한 타입의 값을 담고 있기 때문에 allItemsMatch 함수에 전달될 수 있어요. 그리고 실제로 두 컨테이너에 담긴 문자열 값들이 완전히 일치하기 때문에, allItemsMatchtrue를 반환하게 되는 거죠!

이처럼 제네릭 where절은 연관 타입과 타입 매개변수에 추가 제약을 부여함으로써 제네릭 코드의 표현력과 유연성을 크게 향상시켜 줍니다. 제네릭을 사용할 때 꼭 알아두면 좋을 기능이라 할 수 있겠네요.