🔥 선택적인 프로토콜 요구사항
프로토콜에는 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
값을 메서드의 단일 인자로 전달합니다.
여기서 두 가지 수준의 옵셔널 체이닝이 동작합니다:
dataSource
가nil
일 수 있으므로,dataSource
뒤에 물음표가 붙어dataSource
가nil
이 아닐 때만increment(forCount:)
를 호출하도록 합니다.- 설령
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와의 상호 운용성을 고려할 때 특히 유용합니다. 하지만 주의할 점은 옵셔널 요구사항을 사용할 때는 항상 해당 요구사항이 구현되지 않았을 가능성을 고려하고 적절히 처리해야 한다는 것입니다. 옵셔널 체이닝과 옵셔널 바인딩을 활용하여 안전하게 옵셔널 요구사항을 사용할 수 있습니다.