🔥 선택적인 프로토콜 요구사항

757자
10분

프로토콜에는 optional 키워드를 사용하여 선택적 요구사항을 정의할 수 있습니다. 이러한 요구사항은 프로토콜을 채택한 타입에서 반드시 구현할 필요가 없습니다. 옵셔널 요구사항은 Objective-C와 상호 운용성을 고려하여 제공되는 기능입니다. 프로토콜과 옵셔널 요구사항 모두 @objc 속성으로 표시되어야 합니다. 참고로 @objc 프로토콜은 클래스에서만 채택할 수 있으며, 구조체나 열거형에서는 채택할 수 없습니다.

옵셔널 요구사항의 메서드나 속성을 사용할 때, 해당 타입은 자동으로 옵셔널로 변환됩니다. 예를 들어, (Int) -> String 타입의 메서드는 ((Int) -> String)?로 변환됩니다. 여기서 주목할 점은 메서드의 반환 값이 아닌 전체 함수 타입이 옵셔널로 래핑된다는 것입니다.

옵셔널 프로토콜 요구사항은 프로토콜을 채택한 타입에서 해당 요구사항을 구현하지 않았을 가능성을 고려하여 옵셔널 체이닝을 통해 호출할 수 있습니다. 옵셔널 메서드의 구현 여부를 확인하기 위해 메서드 이름 뒤에 물음표를 붙여 호출합니다. 예를 들면 someOptionalMethod?(someArgument)와 같은 형태로 호출할 수 있습니다. 옵셔널 체이닝에 대한 자세한 내용은 옵셔널 체이닝을 참고하시기 바랍니다.

다음 예제에서는 외부 데이터 소스를 사용하여 증가량을 제공하는 Counter라는 정수 카운팅 클래스를 정의합니다. 이 데이터 소스는 CounterDataSource 프로토콜로 정의되며, 두 개의 옵셔널 요구사항을 가지고 있습니다:

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}
swift

CounterDataSource 프로토콜은 increment(forCount:)라는 옵셔널 메서드 요구사항과 fixedIncrement라는 옵셔널 속성 요구사항을 정의합니다. 이 요구사항들은 데이터 소스가 Counter 인스턴스에 적절한 증가량을 제공하는 두 가지 방법을 정의합니다.

아래에 정의된 Counter 클래스는 CounterDataSource? 타입의 옵셔널 dataSource 속성을 가지고 있습니다:

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
 
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}
swift

Counter 클래스는 현재 값을 count라는 변수 속성에 저장합니다. 또한 increment 메서드를 정의하여 메서드가 호출될 때마다 count 속성을 증가시킵니다.

increment() 메서드는 먼저 데이터 소스에서 increment(forCount:) 메서드의 구현을 찾아 증가량을 가져오려고 시도합니다. increment() 메서드는 옵셔널 체이닝을 사용하여 increment(forCount:)를 호출하고, 현재 count 값을 메서드의 단일 인자로 전달합니다.

여기서 두 가지 수준의 옵셔널 체이닝이 동작합니다:

  1. dataSourcenil일 수 있으므로, dataSource 뒤에 물음표가 붙어 dataSourcenil이 아닐 때만 increment(forCount:)를 호출하도록 합니다.
  2. 설령 dataSource가 존재하더라도 increment(forCount:)를 구현했다는 보장이 없습니다. 왜냐하면 이는 옵셔널 요구사항이기 때문입니다. 여기서도 increment(forCount:)가 구현되지 않았을 가능성은 옵셔널 체이닝으로 처리됩니다. increment(forCount:)는 존재할 때만 호출됩니다. 즉, nil이 아닐 때만 호출됩니다. 이것이 increment(forCount:) 뒤에도 물음표가 붙는 이유입니다.

increment(forCount:)의 호출은 위 두 가지 이유로 인해 실패할 수 있으므로, 호출 결과는 옵셔널 Int 값을 반환합니다. 이는 CounterDataSource의 정의에서 increment(forCount:)non-optional Int 값을 반환하도록 정의되어 있더라도 마찬가지입니다. 두 번의 연속적인 옵셔널 체이닝 작업이 있음에도 불구하고, 결과는 여전히 단일 옵셔널로 래핑됩니다. 여러 수준의 옵셔널 체이닝 사용에 대한 자세한 내용은 여러 수준의 체이닝 연결을 참고하시기 바랍니다.

increment(forCount:)를 호출한 후, 반환된 옵셔널 Int는 옵셔널 바인딩을 사용하여 amount라는 상수로 언래핑됩니다. 만약 옵셔널 Int가 값을 포함하고 있다면(즉, 델리게이트와 메서드가 모두 존재하고 메서드가 값을 반환했다면), 언래핑된 amount는 저장된 count 속성에 추가되고 증가 작업이 완료됩니다.

만약 increment(forCount:) 메서드에서 값을 가져올 수 없다면(dataSource가 nil이거나 데이터 소스가 increment(forCount:)를 구현하지 않은 경우), increment() 메서드는 대신 데이터 소스의 fixedIncrement 속성에서 값을 가져오려고 시도합니다. fixedIncrement 속성 역시 옵셔널 요구사항이므로, 그 값은 CounterDataSource 프로토콜 정의에서 non-optional Int 속성으로 정의되어 있더라도 옵셔널 Int 값입니다.

다음은 쿼리할 때마다 상수 값 3을 반환하는 간단한 CounterDataSource 구현입니다. 이는 옵셔널 fixedIncrement 속성 요구사항을 구현함으로써 이루어집니다:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}
swift

ThreeSource의 인스턴스를 새로운 Counter 인스턴스의 데이터 소스로 사용할 수 있습니다:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12
swift

위의 코드는 새로운 Counter 인스턴스를 생성하고, 데이터 소스를 새로운 ThreeSource 인스턴스로 설정한 다음, 카운터의 increment() 메서드를 4번 호출합니다. 예상대로 increment()가 호출될 때마다 카운터의 count 속성은 3씩 증가합니다.

다음은 Counter 인스턴스가 현재 count 값에서 0으로 증가하거나 감소하도록 만드는 TowardsZeroSource라는 더 복잡한 데이터 소스입니다:

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}
swift

TowardsZeroSource 클래스는 CounterDataSource 프로토콜의 옵셔널 increment(forCount:) 메서드를 구현하고, count 인자 값을 사용하여 카운팅 방향을 결정합니다. 만약 count가 이미 0이라면, 더 이상 카운팅이 이루어지지 않도록 메서드는 0을 반환합니다.

TowardsZeroSource의 인스턴스를 기존 Counter 인스턴스와 함께 사용하여 -4에서 0까지 카운팅할 수 있습니다. 카운터가 0에 도달하면 더 이상 카운팅이 이루어지지 않습니다:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
swift

이렇게 옵셔널 프로토콜 요구사항을 사용하면 프로토콜을 채택한 타입에서 특정 요구사항을 선택적으로 구현할 수 있는 유연성을 제공할 수 있습니다. 이는 Objective-C와의 상호 운용성을 고려할 때 특히 유용합니다. 하지만 주의할 점은 옵셔널 요구사항을 사용할 때는 항상 해당 요구사항이 구현되지 않았을 가능성을 고려하고 적절히 처리해야 한다는 것입니다. 옵셔널 체이닝과 옵셔널 바인딩을 활용하여 안전하게 옵셔널 요구사항을 사용할 수 있습니다.