-
Swift - Protocol as TypeProgramming/Swift 2021. 12. 29. 23:02
안녕하세요 BeePeach입니다 :0
이번 포스팅에서 공부해볼 주제는 Protocol Type입니다.
Protocol에서 기능들을 직접 구현을 할 수는 없지만 프로토콜을 Type으로 사용할 수 있습니다.
이를 다른 말로 existential type이라고 합니다.
이 말은 '프로토콜을 (conform)따르는Type이 존재한다.' 라는 말에서 유래했습니다.
Protocol도 Type이기 때문에 protocol의 이름을 지어줄 때 UpperCamelCase 컨벤션을 따른 것입니다.
Protocol Type
위에서 설명했듯이 protocol도 First Class Citizen입니다.
(First Class Citizen에 대해서 모르신다면 여기를 참고해주세요.)
First Class Citizen이기 때문에
- function, method, init에서 파라미터, 리턴 타입으로 사용될 수 있고
- 변수 상수 프로퍼티의 타입으로 사용될 수 있으며
- array dictionnary 등의 element로도 사용될 수 있습니다.
쉽게 말해서 Int, String처럼 protocol도 Type입니다.
Apple document에서 나와있는 예제를 한 번 보도록 하겠습니다.
이 예제를 통해서 protocol이 Type으로 어떻게 사용되는지 이해할 수 있을 거예요.
CustomRandomNumberGenerator라는 프로토콜을 정의했습니다.
이제 이 protocol을 채용하는 Type들은 요구 메서드로 random()을 구현해야 합니다.
그리고 이 프로토콜을 채용한 LinearCongruentialGenerator class를 만들었습니다.
필수 요구사항인 random() 메서드도 구현했습니다.
안에 코드가 어려워 보일 수 있지만 이 class는 0.0 ..< 1.0 사이의 random한 Double 값을 생성하는 역할을 합니다.
그리고 직접 실행해보면 random한 수가 나오지만 나오는 수의 순서가 정해져 있습니다.
첫 번째는 0.3746499199817101
두 번째는 0.729023776863283
완벽한 random수가 나오는 게 아니라 정해진 random수가 나옵니다.
(물론 Doulbe에서 제공하는 타입 메서드인 random(in:) 메서드를 이용하면 더 쉽고 진짜 random number를 생성할 수 있습니다. 하지만 apple document에서 예시를 이렇게 들어서 한 번 해봤습니다...)
이제 Dice라는 class를 구현했습니다.
sides 프로퍼티는 주사위의 면을 의미하는 프로퍼티입니다.
generator 프로퍼티는 randomNumber를 생성하기 위한 프로퍼티입니다.
여기서 타입을 보면 우리가 만든 CustomRandomNumberGenerator입니다.
이 의미는 generator 프로퍼티에 올 수 있는 값의 타입은 CustomRandomNumberGenerator 프로토콜을 채용한 타입이면 어떠한 타입도 가능하다는 의미입니다.
init(sides:generator:) 생성자로 초기화를 담당하고 있습니다.
파라미터를 잘 보면 generator의 타입이 또 CustomRandomNumberGenerator인 것을 확인할 수 있죠??
CustomRandomNumberGenerator를 채용하는 타입은 어떠한 타입이라도 이 파라미터로 전달될 수 있다는 의미입니다.
그 아래에 주사위를 굴리는 roll() 메서드를 선언했습니다.
이 코드를 보면 protocol에서 선언했던 random() 메서드를 호출하는 것을 볼 수 있습니다.
그럼 Dice 인스턴스를 생성하고 roll 메서드를 호출해보도록 하겠습니다.
인스턴스를 생성할 때 파라미터로 LinearCongruentialGenerator()를 전달했습니다.
LinearCongruentialGenerator는 CustomRandomNumberGenerator 프로토콜을 채용하고 있기 때문에 파라미터로 전달할 수 있습니다.
dice6는 결과적으로 6개의 면을 가진 주사위를 생성한 것입니다.
roll() 메서드를 이용해서 주사위를 굴려보면 랜덤한 수가 나오는 것을 확인할 수 있습니다.
이 예제를 통해서 프로퍼티의 Type으로 protocol Type을 사용할 것을 확인할 수 있고
생성자의 파라미터 Type에도 protocol Type을 사용한 것을 확인할 수 있었습니다.
Delegation
Delegation이란 디자인 패턴 중에 하나로써 class나 struct가 다른 Type의 인스턴스에게 책임의 일부를 위임해주는 것을 말합니다.
이때 책임을 캡슐화하는 프로토콜을 정의함으로써 위임을 받는 객체가 넘겨받은 책임의 기능을 구현하도록 보장해줍니다.
우리가 iOS 앱 개발을 할 때 TableViewDelegate, CollectionViewDelegate와 같은 패턴을 자주 사용을 하죠??
그리고 이것들은 protocol로 선언되어있는 것을 확인할 수 있습니다.
필수 메서드를 꼭 구현해야 하는 것처럼 책임의 기능을 꼭 구현하도록 보장해줍니다.
위에서 구현한 Dice를 이용한 예제를 한 번 보도록 하겠습니다.
예제가 여러 울 수 있는데 이 예제를 통해서 delegate를 어떻게 구현하는지 알 수 있습니다.
DiceGame 프로토콜은 주사위를 이용한 게임들이 구현해야 할 프로퍼티와 메서드를 정의하고 있습니다.
그리고 DiceGameDelegate는 DiceGame이 해야 하는 작업(책임)을 위임받아서 처리하는 객체가 구현해야 하는 메서드가 정의되어 있습니다.
게임이 시작되었을 때 호출될 gameDidStart(_:),
새로운 턴이 시작될 때 호출될 game(_:didStartNewTrunWithDiceRoll:),
턴이 끝날 때 호출되는 game(_:didEndTurnWithDiceRoll:),
게임이 끝나면 호출될 gameDidEnd(_:) 메서드가 있습니다.
이제 주사위를 이용한 게임 중 위에 그림과 같은 Snake And Ladders 게임을 구현해보도록 하겠습니다.
사다리를 만나면 위로 이동하는 것이고 뱀을 만나면 아래로 이동하는 규칙입니다.
SnakeAndLadders class는 총 25칸으로 이루어져 있고 주사위는 6면 주사위를 사용합니다.
init()을 통해 게임보드를 생성합니다.
여기서 주목해야 하는 부분은 delegate를 설정하는 부분입니다.
class에서 delegate를 설정하려면 reference cycle방지는 위해서 weak로 선언하는 것이 좋습니다.
(이에 대해서는 ARC에서 자세히 다루도록 하겠습니다.)
그리고 delegate로 작업을 위임할 수도 안 할 수도 있으니 Optional로 선언해줍니다.
play() 메서드를 보면 원하는 시점에 delegate에서 선언한 메서드를 호출해서 적절한 시점에 delegate에서 선언한 메서드가 호출되도록 구현합니다. 이러한 방식으로 책임을 위임받은 객체가 적절한 시점에 메서드를 호출할 수 있도록 하는 것입니다.
만약 delegate를 설정하지 않는다면 nil이 할당되고 해당 메서드들은 실행되지 않고 넘어가게 됩니다.
그럼 이제 DiceGameDelegate를 채용한 class를 구현하고 해당 메서드를 구현하면 됩니다.
DiceGameTracker는 DiceGameDelegate를 채용하고 있으므로 필수 요구 메서드 4개를 구현해야 합니다.
gameDidStart(_:)에서는 게임이 시작할 때 시작 메시지를 표시하도록 구현했습니다.
game(_:didStartNewTrunWithDiceRoll:)에서는 턴을 하나씩 증가시키면서 주사위가 몇이 나왔는지 표시해줍니다.
game(_:didEndTurnWithDiceRoll:)에서는 snake 게임이라면 턴이 종료되었다는 메시지와 현재 위치를 나타내 주도록 구현했습니다.
마지막으로 gameDidEnd(_:)에서는 몇 번째 턴에서 게임이 종료되었는지 표시하도록 구현했습니다.
이렇게 구현을 하면 DiceGameTracker는 DiceGame으로부터 기능 구현의 일부를 위임받아서 우리가 원하는 대로 해당 시점에 무엇을 할지 구현할 수 있게 됩니다.
각각의 시점마다 실행할 코드를 구현한 것은 DiceGame이 아니라 DiceGameTracker입니다.
DiceGame이 구현(책임)의 일부를 DiceGameTracker에게 넘겨준 것을 알 수 있습니다.
그럼 직접 사용을 해봐야겠죠??
traker와 game 인스턴스를 생성한 뒤에 game의 delegate프로퍼티에 traker 객체를 할당시켜줍니다.
그렇게 되면 traker가 game의 책임의 일부를 위임받게 되고 우리가 구현한 대로 로그가 출력되는 것을 확인할 수 있습니다.
만약 여기서 delegate에 traker를 할당하지 않는다면 로그는 출력되지 않습니다.
이 예제로 delegate 디자인 패턴을 어떻게 구현하는지 알 수 있었습니다.
Collections of Protocol Types
앞에서 Protocol은 First Class Citizen이기 때문에 Colltecion의 element로도 사용이 가능하다고 했습니다.
Protocol Type으로 된 Array, Dictionary 등 Collection을 생성할 수 있습니다.
바로 예시를 보도록 하겠습니다.
Readable, Wrtiable, Vedio 프로토콜을 선언하고 VedioFile, TextFile 클래스를 구현했습니다.
두 클래스는 공통적으로 Readable 프로토콜을 채용하고 각각 Vedio, Writabe 프로토콜을 채용 중입니다.
그럼 프로토콜인 Readable로 만들어진 array를 생성해 보도록 하겠습니다.
VedioFile, TextFile 인스턴스를 두 개씩 만들고 둘을 Readble 배열에 집어넣었습니다.
둘은 다른 class Type이지만 Readable이라는 공통적인 프로토콜을 채용하고 있기 때문에 [Readable] 배열에 같이 들어갈 수 있습니다.
Collection이기 때문에 for-in문에서 반복이 가능하겠죠???
반복 상수 readable을 보면 접근 가능한 멤버는 Readable에서 정의해놓은 read() 메서드뿐입니다.
나머지 멤버들은 안정성을 보장할 수 없기 때문에 실제로 초기화되어있지만 접근이 불가능합니다.
play(), write() 메서드를 호출하면 에러가 발생하는 것을 확인할 수 있습니다.
그럼 어떻게 사용할 수 있을까요??
이렇게 type casting과 optional binding을 이용하면 바뀐 type의 멤버에 접근할 수 있게 됩니다.
위 코드를 보면 반복 상수인 readable이 만약 Vedio 프로토콜을 채용한다면 play() 메서드를 호출하도록 구현을 했습니다.
하지만 생각해볼 것은 여기서 Vedio 프로토콜로 type casting 하기 때문에 다시 read() 메서드에는 접근할 수가 없습니다.
만약 둘 다 접근하고 싶다면 Vedio 프로토콜이 아닌 VedioFile class Type으로 캐스팅해주면 되겠죠??
이번에는 play(), read() 모두 호출 가능한 것을 확인할 수 있습니다.
이렇게 프로토콜을 collection의 element로 사용한다면 프로토콜에 정의한 멤버에만 접근이 가능한 것을 꼭 기억해주세요.
참고자료
https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
Protocols — The Swift Programming Language (Swift 5.6)
Protocols A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of tho
docs.swift.org
728x90'Programming > Swift' 카테고리의 다른 글
Swift - Protocol Composition (0) 2022.01.03 Swift - Protocol Inheritance (0) 2022.01.02 Swift - Protocol Initializer , Subscript (0) 2021.12.28 Swift - Protocol (0) 2021.12.22 Swift - Extension (0) 2021.12.20