본문 바로가기
개린이 이야기

Protocol과 associatedtype에 관하여

by iOS 개린이 2023. 6. 6.

프로토콜과 제네릭

-제네릭은 유연하고 재사용 가능한 타입을 작성하는데 도움이 되는 범용 타입이다. 

즉, 제네릭은 범용성을 위한 기능이다.

 

그럼 Protocol에서 범용성을 가지기 위해 제네릭을 사용하는건 어떨까요?

protocol MyProtocol<T> { //An associated type named 'T' must be declared in the protocol 'MyProtocol' or a protocol it inherits
    func performAction(with item: T)
}

 

위 코드는 컴파일 오류를 발생시킨다. 이렇게 프로토콜에서 제네릭을 적용하는 것은 허용되지 않는다.

대신 프로토콜에서는 'associatedtype' 을 사용하여 제네릭과 같은 기능을 구현할 수 있다.

 

associatedtype이란?

-associatedtype은 프로토콜에서 제네릭 타입의 역할을 하는 키워드이다.

프로토콜이 특정 작업을 수행하는데 필요한 타입을 정의하지 않고, 프로토콜을 채택하는 구체적인 타입에 의해 결정되도록 할 수 있다.

 

associatedtype의 사용

protocol Container {
    associatedtype Item
    
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

 

 

'associatedtype Item'

컨테이너 프로토콜이 다루는 항목의 타입을 나타낸다.

이 타입은 프로토콜을 준수하는 곳에서 결정되고, 이를 통해 어떤 타입의 항목을 저장할 지 범용성 있게 다룰 수 있다.

 

'mutating func append(_ item: Item)'

컨테이너에 새로운 항목을 추가하는 기능이다. 'item' 매개변수의 타입은 위에서 설명했듯이 프로토콜을 준수하는 곳에서 결정된다.

 

'var count: Int {get}'

컨테이너 내부 요소의 수를 나타낸다.

 

'subscript(i: Int) -> Item {get}'

컨테이너의 특정 위치에 있는 항목을 가져오는 서브스크립트.

 

 

프로토콜 채택하기

struct IntContainer: Container {
  
    typealias Item = Int
    
    private var items = [Int]()
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

 

구조체 'IntContainer' 는 'Container' 프로토콜을 준수한다.

 

'typealias Item = Int'

'associatedtype' 으로 지정했던 'Item' 을 'Int' 타입으로 지정한다.

 

'private var items = [Int]()'

내부적으로 Int 타입의 값들을 저장하는 배열.

 

'mutating func append { }'

Int 타입의 요소를 배열에 추가.

 

'var count: Int { }'

배열의 요소 개수를 반환.

 

'subscript(i: Int) -> Int { }'

주여진 인덱스에 맞는 요소를 반환.

 

이렇게 해당 프로토콜을 채택하는 곳에서 associatedtype 으로 지정했던 'Item' 에 대한 구체적인 타입을 지정하여,

한 타입에 한정되지 않는 유연한 프로토콜을 만들 수 있다.

 

Swift의 타입추론 기능

struct IntContainer: Container {
    private var items = [Int]()

    mutating func append(_ item: Int) {
        items.append(item)
    }
 
  ...
}

 

Swift의 타입 추론 기능을 통해 이용하여

굳이 'typealias Item = Int' 코드를 작성하지 않고 사용하는 것도 가능하다.

 

 

associatedtype과 타입제약

-제네릭에 타입 제약을 걸었듯이, 

associatedtype에도 'where' 절을 이용하여 타입에 제약을 걸 수 있다.

 

1. 'Stack' 프로토콜 정의.

protocol Stack {
    associatedtype Element
    
    mutating func push(_ item: Element)
    mutating func pop() -> Element?
}

 

'push()' 메서드와 'pop()' 메서드를 통해 각각 스택에 항목을 추가하고, 항목을 제거하는 동작을 정의한다.

'Element' 라는 associatedtype 을 통해

메소드에 전달되고, 반환될 항목의 타입을 범용성 있게 만든다.

 

 

2. 'Stackable' 프로토콜 정의.

protocol Stackable {
    associatedtype StackType: Stack
    associatedtype ItemType where ItemType == StackType.Element
    
    var stack: StackType { get set }
    
    mutating func addToStack(item: ItemType)
    mutating func removeFromStack() -> ItemType?
}

 

 

'associatedtype StackType: Stack'

연관 타입 'StackType' 은 'Stack' 프로토콜을 준수하는 타입이어야 한다.

 

'associatedtype ItemType where ItemType == StackType.Element' 

연관 타입 'ItemType' 은 'StackType' 의 'Element' 타입과 동일해야 한다.

즉, Stack에 추가되거나 제거될 수 있는 아이템의 타입을 말한다.

 

'var stack: StackType {get set}'

'StackType' 의 스택을 나타낸다. 

 

'mutating func addToStack(item: ItemType)'

'ItemType' 의 아이템을 스택에 추가한다.

 

'mutating func removeFromStack() -> ItmeType'

스택에서 'ItemType' 의 아이템을 제거하고, 반환한다. 

스택이 비어있을 경우 'nil' 을 반환한다.

 

 

3. 'Stack' 및 'Stackable' 프로토콜 적용.

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

struct StackUser: Stackable {
    typealias StackType = IntStack
    typealias ItemType = Int
    
    var stack: IntStack
    
    mutating func addToStack(item: Int) {
        stack.push(item)
    }
    
    mutating func removeFromStack() -> Int? {
        return stack.pop()
    }
}

 

'IntStack' 은 'Stack' 프로토콜을 채택하며, 연관 타입 'Element' 를 타입 추론을 통하여 'Int' 타입으로 지정했다.

정수 배열 'Items' 를 내부적으로 선언하고, 'push()' 와 'pop()' 을 통해 배열 요소를 관리한다.

 

'StackUser' 는 'Stackable' 프로토콜을 채택하며, 

연관타입 'StackType' 을 'Stack' 프로토콜을 채택한 'IntStack' 구조체로 선언하고,

연관타입 'ItemType' 을 'Stack' 프로토콜의 'Element' 타입과 동일한 'Int' 타입으로 선언했다.

 

정리하면..

'Stack' 과 'Stackable' 프로토콜을 사용하여 스택이라는 자료구조가 작동하는 구조를 보여준다.

 

1. 'IntStack' 구조체는 'Stack' 프로토콜을 준수하며, 정수 타입의 스택을 구현하고 있다.

'push()' 와 'pop()' 메서드를 통해 각각 스택에 항목을 추가하고 제거하는 기능을 수행한다.

 

2. 'Stackable' 프로토콜은 어떤 'Stack' 타입이든 다룰 수 있지만, 스택의 요소 타입과 'ItemType' 은 일치해야 한다.

'StackUser' 구조체는 'Stackable' 프로토콜을 준수하며, 'IntStack' 구조체를 사용하여 작동한다.

 

3. 'addToStack()' 과 'removeFromStack()' 메서드는 각각 'IntStack'에 항목을 추가하고 제거한다. 

 

따라서 예제를 통해 associatedtype 과 타입제약을 사용하여 프로토콜의 유연성을 유지하면서도, 

필요한 타입의 안전성을 보장하는 방법에 대해서 배울 수 있다.

 

 

Swift 표준 라이브러리에서 사용되는 associatedtype

-Swift에서 associatedtype과 타입 제약의 사용은 많이 있지만, 그 중에서 'Sequence' 프로토콜의 사례를 알아보자.

 

'Sequence' 프로토콜이란?

-순차적으로 접근할 수 있는 값들의 집합을 정의하며, for-in 구문을 통해 요소들을 순환하기 위해서는 'Sequence' 프로토콜을 준수하고 있어야 한다.

예를 들어 Array, Set, Dictionary 등 다양한 타입이 'Sequence' 프로토콜을 준수하고 있다.

 

public protocol Sequence {
    associatedtype Element
}

 

Swift의 표준 라이브러리를 보면 위 코드와 같이 'Sequence' 프로토콜이 정의되어 있다.

associatedtype인 'Element' 는 시퀸스의 개별 요소의 타입을 나타낸다.

만약, [Int] 타입의 배열이면 'Element' 는 Int 타입이 되는 것이다.

 

associatedtype Iterator: IteratorProtocol where Iterator.Element == Element

 

'Sequence' 프로토콜 정의에서는 이렇게 타입 제약을 사용한 코드도 볼 수 있다.

associatedtype인 'Iterator' 는 'IteratorProtocol' 을 준수한 타입이어야 하고,

'Iterator' 가 가지는 Element 타입은 'Sequence' 프로토콜의 'Element' 타입과 동일해야 한다는 뜻이다.

 

'Sequence' 프로토콜을 준수하기 위해서는 'func makeIterator()' 메서드를 구현해주어야 하는데,

이 메서드의 반환타입이 'Iterator' 타입이다.

우리가 for-in 구문을 통해 배열을 순회하려고 할 때, 내부적으로는 'makeIterator()' 가 실행되는 것이다.

 

따라서 타입제약을 통해 시퀸스의 요소타입과 반복을 도와주는 Iterator 타입의 일관성을 가져오고, 이것은 코드의 안전성을 향상시켜줄 수 있다.

 

 

associatedtype의 이점

 

1. 유연성

프로토콜의 제네릭과 같은 기능이기 때문에 특정 타입에 한정되지 않는 유연성을 가질 수 있다.

 

2. 재사용성

하나의 프로토콜을 다양한 타입과 함께 사용할 수 있도록 도와준다.

Swift의 'Sequence' 프로토콜에서 보았듯이 'Int' 배열, 'String' 배열 등 다양한 타입에서 사용하는

재사용성이 향상된다.

 

3. 타입 안전성

associatedtype 에 타입 제약을 추가하여, 해당 타입이 특정 프로토콜을 준수하도록 제약을 걸 수 있다.

이 기능을 통해 잘못된 타입의 사용을 방지할 수 있고, 안전성과 정확성을 향상시켜준다.

 

 

 

 

 

Reference:

https://github.com/apple/swift/blob/main/stdlib/public/core/Sequence.swift

https://hyunsikwon.github.io/swift/Swift-AssociatedType/ 

https://babbab2.tistory.com/180

'개린이 이야기' 카테고리의 다른 글

Git에 관하여  (0) 2023.06.25
Swift의 API Design Guidelines  (0) 2023.06.12
Protocol Composition에 관하여  (0) 2023.06.06
Protocol-Extension에 관하여  (0) 2023.06.05
타입으로서 프로토콜에 관하여  (0) 2023.06.05