Search

[Swift] Protocol(2)

The Swift Programming Language Documents를 참고해서 정리했습니다.
contents
[Swift] Protocol(1) 에 이어서 진행되는 포스팅입니다. 이전 포스팅에는 기본적인 Protocol 개념이 들어 있어 보는 걸 추천드립니다.

Use the protocol to save places

이번 포스팅에서는 프로토콜을 사용해서 장소를 저장하는 예시를 만들어 보려고 합니다.
일단, 이름 관련한 Named 라는 프로토콜을 만들어 보겠습니다. Named 프로토콜은 name 이라는 속성을 가지고 있습니다.
protocol Named { var name: String { get } }
Swift
복사
그리고, Location 클래스를 만들어 보겠습니다. Location 클래스는 이름 그대로 위도, 경도 속성을 가지고 있습니다.
class Location { var latitude: Double var longitude: Double init(latitude: Double, longitude: Double) { self.latitude = latitude self.longitude = longitude } }
Swift
복사
이제, 위치와 장소 이름을 가지는 Place 라는 클래스를 만들어 보겠습니다. Place는 Location 클래스를 상속받고 Named 프로토콜을 채택합니다.
class Place: Location, Named { var name: String init(name: String, latitude: Double, longitude: Double) { self.name = name super.init(latitude: latitude, longitude: longitude) } }
Swift
복사
이제 장소를 저장하는 save 메서드를 만들어 봅시다. 메서드에 매개변수로 place를 넣어줍니다.
place는 Location & Named 타입을 가지고 있습니다. 이는 한 타입이 여러 개를 동시에 준수하고 있다고 볼 수 있습니다. place가 가진 것처럼 클래스와 프로토콜 조합 말고도 프로토콜들로도 조합을 구성할 수 있습니다.
var places: [String: Location] = [:] func save(place: Location & Named) { let placeName = place.name let location = Location(latitude: place.latitude, longitude: place.longitude) places[placeName] = location }
Swift
복사
save에는 Location은 상속받지 않지만 Named를 채택하는 클래스는 매개 변수로 설정할 수 없습니다. 반대로 Location은 상속받지만 Named 프로토콜을 채택하지 않는 클래스도 매개 변수로 설정할 수 없습니다.
그런데, 장소 중에서도 위치는 있지만 장소에 마땅한 이름이 없는 곳이 존재할 수 있습니다. 이런 장소도 저장할 수 있도록 수정을 해보겠습니다. 하지만 해당 장소에 이름이 있다면 가지고 있는 이름을 장소의 이름으로 설정해주도록 하겠습니다.
먼저 Named 프로토콜을 가지지 않아도 save 메서드에서 저장할 수 있도록 수정해줍니다. 모든 장소가 Location은 가지기 때문에 Location 클래스인지만 확인하겠습니다.
func save(place: Location) { let location = Location(latitude: place.latitude, longitude: place.longitude) if let place = place as? Named { places[place.name] = location } else { places["unknown"] = location } }
Swift
복사
매개 변수로 들어오는 place는 이름을 가질 수도 있습니다. 이름을 가진다는건 Named 프로토콜을 준수하는 클래스라는 것이기에 Named로 다운 캐스팅 시켜줍니다. 해당 값이 nil이라는건 Named 프로토콜을 준수하지 않는 것이기에 장소 이름이 없습니다. 따라서 “unknown”의 value로 location 값을 넣어줍니다.
name를 가지는 cityHall과 name이 없는 unknownLocation를 각각 save 메서드에 넣으면,
let cityHall = Place(name: "City hall", latitude: 47.6, longitude: -122.3) let unknownLocation = Location(latitude: 100.0, longitude: 200.0) save(place: cityHall) save(place: unknownLocation)
Swift
복사
이름이 없는 value는 알아서 unknown로 처리됩니다.
근데 여기서 문제가 있습니다. “unknown”이 key value이기 때문에 이름이 없는 Location은 모두 “unknown” key를 가져서 이전에 있는 “unknown” value를 지우고 새로운 Location 값이 들어가게 됩니다.
즉, 이름이 없는 장소는 딱 하나만 저장될 수 있는 겁니다.
이런 문제를 해결하기 위해서 Location를 key value로 바꿉시다. Location은 하나이기 때문에 key value로 사용하기 적합합니다.
하지만, Location를 key value로 사용하려고 하면 에러가 발생합니다. Location이 Hashable 프로토콜을 준수하지 않는다고 하네요.
우리는 Hashable이라는 프로토콜을 만든 적이 없는데 뭘까요?
Swift에는 Equatable, Hashable, Comparable 프로토콜이 제공됩니다. 해당 프로토콜들은 프로토콜 요구 사항을 구현하기 위해서 반복적인 boiler plate code를 작성하지 않게 해줍니다. 해당 프로토콜에서 제공하는 메서드는 직접 구현하지 않아도 됩니다.
대신, 프로토콜을 준수하는 타입이 갖춰야 하는 조건이 있습니다.
Hashable 프로토콜은 다음과 같은 커스텀 타입에 대해서만 Hashable를 사용할 수 있도록 했습니다.
Hashable Protocol를 준수하는 Stored Property만 있는 Structure
Hashable Protocol를 준수하는 associated type만 있는 Enumeration
associated type이 없는 Enumeration
*Equatable, Comparable 프로토콜도 조건이 거의 동일
우리가 만든 커스텀 타입 Location은 클래스 타입이기 때문에 Hashable를 준수할 수 없습니다. 우리가 Location이 Hashable를 준수할 수 있도록 하려면 Location를 구조체로 만들어 주어야 합니다.
그럼 Hashable를 준수할 수 있도록 수정해줍시다.
struct Location: Hashable { var latitude: Double var longitude: Double init(latitude: Double, longitude: Double) { self.latitude = latitude self.longitude = longitude } }
Swift
복사
이렇게 수정하면 이제 Location를 key value로 사용할 순 있는데, Location를 상속받고 있던 Place 클래스에 문제가 생깁니다. 그리고 Location은 Struct이기 때문에 결론적으로 Location 구조체만 save 메소드에 들어갈 수 있게 됩니다. Location 값을 가지고 있는 타입이 save 메소드 안으로 들어올 수 있었으면 좋겠습니다.
따라서, Locationable 이라는 프로토콜을 만들어서 Location과 Place가 채택할 수 있도록 해줍니다.
protocol Locationable { var latitude: Double { get } var longitude: Double { get } }
Swift
복사
굳이 클래스로 만들 필요가 없어서 두 타입 모두 구조체로 변경했습니다.
struct Location: Locationable, Hashable { var latitude: Double var longitude: Double init(latitude: Double, longitude: Double) { self.latitude = latitude self.longitude = longitude } } struct Place: Locationable & Named { var name: String var latitude: Double var longitude: Double init(name: String, latitude: Double, longitude: Double) { self.name = name self.latitude = latitude self.longitude = longitude } }
Swift
복사
그리고 Location 타입을 받던 매개 변수는 Locationable를 준수하는 타입이 모두 들어올 수 있도록 수정했습니다.
func savePlace(in location: Locationable) { if let place = location as? Place { let location = Location(latitude: place.latitude, longitude: place.longitude) places[location] = place.name } else if let location = location as? Location { places[location] = "unknown" } }
Swift
복사
만약, location 이 Place 구조체로 다운 캐스팅이 된다면 이름을 넣고 Location 구조체로 다운 캐스팅이 된다면 “unknown”를 넣어줍니다. 아까 있던 문제가 해결됐는지 볼까요?
저장이 잘 되었군요. ٩( ˙0˙

Use the protocol to save places(2)

만약, Named에 해당 장소가 속한 도시를 저장하는 프로퍼티가 있다면 어떨까요?
하지만, 장소 관련한 타입이 아니라면 city 프로퍼티가 필요없을 수도 있겠네요.
이런 경우에는 optional 키워드를 사용하면 됩니다.
@objc protocol Named { var name: String { get } @objc optional var city: String { get } }
Swift
복사
optional 키워드를 사용한 프로토콜은 Class Protocol이기 때문에 클래스 타입만 사용할 수 있습니다. 또한, optional 키워드를 사용하려면 프로토콜과 해당 요구 사항에 @objc 키워드를 표시해줘야 합니다.
이제 city 프로퍼티는 optional 이기 때문에 필수적으로 구현하지 않아도 됩니다.
하지만, optional 키워드를 사용한 프로퍼티, 메소드를 구현하지 않아도 되기 때문에 문제가 생길 수도 있습니다. 모든 요구 사항이 optional 이라면 프로토콜을 준수하지만 어떠한 요구 사항도 구현하지 않는 타입이 생길 수 있습니다. 이러한 문제 때문에 기술적으로 optional 키워드의 사용이 허용되지만 사용이 권장되진 않습니다.
이번엔 Named 프로토콜에 name과 city 프로퍼티를 사용한 문장을 가지는 description 프로퍼티를 만들려고 합니다. 어떤 (city)에 있는 (name) 이라는 문장을 만들겁니다. 만약, city 프로퍼티가 없다면 그냥 name만 가질겁니다.
그리고 해당 프로퍼티는 해당 프로토콜을 준수하는 모든 타입이 가질 겁니다. 추가 수정없이 모든 타입이 자동으로 해당 프로퍼티를 얻을 순 없을까요?
이럴 때는 Protocol Extension를 사용해서 프로퍼티를 작성해주는겁니다. 그러면 자동으로 해당 프로퍼티를 얻을 수 있습니다. 물론 프로토콜을 준수하는 타입이 원한다면 추가적인 구현이 가능합니다.
extension Named { var description: String { if let city = city { return "\(city)에 있는 \(name)" } return name } }
Swift
복사
이번에는 모든 장소가 같은 위도(latitude)를 가지는지 확인하려고 합니다.
물론 위도를 일일이 확인하는 메서드를 만들 수도 있겠지만, 저는 Collection 프로토콜 안에 allEqual 이라는 메서드를 만들려고 합니다.
Collection에 들어가는 Element에 조건을 걸어서 Locationable과 Equatable를 준수하는 Element라면 allEqual 메서드로 Element를 비교할 수 있도록 해줬습니다.
extension Collection where Element: Equatable & Locationable { func allEqual() -> Bool { if self is Dictionary<Location, String>.Keys { for element in self { if element.latitude != self.first?.latitude { return false } } return true } return false } }
Swift
복사
먼저, 한 장소의 위도를 다르게 설정하고 테스트를 해봤습니다.
let cityHall = Place(name: "City hall", latitude: 47.6, longitude: -122.3) let store = Place(name: "Store1", latitude: 47.6, longitude: -122.5) let store2 = Place(name: "Store2", latitude: 47.6, longitude: -120.2) let operaHouse = Place(name: "Opera House", latitude: 47.7, longitude: -120.5) let museum = Place(name: "Museum", latitude: 47.6, longitude: -120.3) savePlace(in: cityHall) savePlace(in: store) savePlace(in: store2) savePlace(in: operaHouse) savePlace(in: museum)
Swift
복사
성공적으로 false를 출력했습니다! (۶•̀ᴗ•́)۶
이번엔 모두 같은 위도를 가지도록 수정했습니다.
let cityHall = Place(name: "City hall", latitude: 47.6, longitude: -122.3) let store = Place(name: "Store1", latitude: 47.6, longitude: -122.5) let store2 = Place(name: "Store2", latitude: 47.6, longitude: -120.2) let operaHouse = Place(name: "Opera House", latitude: 47.6, longitude: -120.5) let museum = Place(name: "Museum", latitude: 47.6, longitude: -120.3)
Swift
복사
이번에는 true를 출력했습니다. (۶•̀ᴗ•́)۶

️ 마치며

추가적으로 Dynamic Dispatch에 대한 내용도 넣어보고 싶었는데, 나중에 하나의 포스트로 깊게 다루는게 좋을 것 같아서 해당 포스팅에는 추가하지 못했습니다.
Swift Programming Language Document를 보면서 제가 Protocol를 깊게 알지 못했다는 생각이 들더라구요. 기본적으로 제공하는 Comparable, Hashable, Equatable도 알고 준수한 것이 아니고 경고창이 떠서 준수했던..
얼른 모든 내용들을 정리해야겠습니다. ٩(๑❛ᴗ❛๑)۶

참고 자료