View
이번에 야곰 리펙토링 강의를 들으면서 Swift의 “프로토콜” 이 다른 언어에 비해 굉장히 특이한 컨셉을 가지고 있다는 걸 알았다. 어떻게 보면 abstract class나 interface를 통해 구현하는 OOP의 개념들이 프로토콜에 담겨있는데, 이게 구조체나 enum 타입에 모두 적용되게 한다는게 꽤나 놀라운 일이라고 생각된다. 구조체는 값 타입이라며!!! enum도 값 타입이라며!!!
내용이 나왔던 김에 강의에서 소개해준 관련된 WWDC 영상들을 보면서 어떤 컨셉으로 설계되었고, 어떻게 동작하는 건지 공부하면서 한 번 정리해보려고 한다.
What I Learned
이번에 본 [ WWDC 15 의 Protocol-Oriented Programming in Swift ] 는 이미 많은 사람들이 블로그에 정리해둔 스위프트 WWDC의 기본같은 영상인 것 같다.
프로토콜에 대한 내용을 쭉 들어봤을 때, class의 상속관계를 사용할 때 보다 기존 코드의 확장이 필요한 시점에 프로토콜을 활용하면 원본 코드를 건드리지 않으면서 훨씬 더 자유롭게 확장을 할 수 있고, 다른 인스턴스와 상호작용을 하는 경우 해당 인스턴스의 타입이나 채택한 프로토콜에 따라 동작을 분기할 수도 있도록 설계되어있다. 요것 뿐만 아니라 클래스의 첫 설계 시점에 상속할 부모 클래스를 선택하는 것과 다르게 extension과 프로토콜 채택을 통해 “기능 확장이 필요한 시점에 채택이 이루어질 수 있다”는 것도 확장성에서 큰 장점으로 작용되는 특징이라고 느꼈다.
내부적인 동작 방식에 대해서는 또 다른 WWDC 영상에서 소개가 되고있는데, [ Understanding Swift Performance ] 라는 영상도 곧 들어볼 예정이다! 기대기대
우리가 클래스를 사용하는 이유
Encapsulation, Access Control, Abstraction, Namespace 분리, Expressive Syntax, Extensibility 등의 목적으로 클래스를 사용하도록 배웠다.
그러나 여기에 있는 목적들은 사실 Struct를 통해서도 만들어낼 수 있다.
클래스의 단점?
- Implicit Sharing
- reference를 통한 값 접근 방식에 의해 race condition, lock 등의 문제가 발생할 수 있음
- 그치만, 사실 swift의 collection 들은 모두 값 타입으로 구성되어 있기 때문에 요런게 문제가 되지는 않음 ㅎㅎ
- 상속의 불편한 점
- 단일 상속으로 인해 하나의 부모 클래스만 가질 수 있기 때문에, 클래스의 기능 확장 시점이 아닌 처음 만들 때 부모 클래스를 잘 선택해야함
- 자식 클래스 init 시점에 부모 클래스의 init이 모두 호출되어야함
- 요것도 사실은 swift에서는 delegate pattern으로 컨트롤 해주고 있는 문제
- 타입 관계
- 자식 클래스에서 부모 클래스의 함수를 끌어다 사용하더라도, 자식의 프로퍼티를 활용하기 어려움 (냄새나는 코드를 작성하기 쉬움)
class Ordered {
func precedes (other: Ordered) -> Bool { fatalError("implement me!") }
}
class Label: Ordered { var text: String = "" ⋯ }
class Number : Ordered {
var value: Double = 0
override func precedes (other: Ordered) -> Bool {
// other가 Number인지 확인해야함
// -> type safety hole
return value ‹ (other as! Number).value
}
}
더 나은 추상화 방법이 필요하다
- 값 타입을 지원해야함
- 정적 타입 관계 (static type relationship)과 동적 디스패치(dynamic dispatch)를 지원해야함
- 요건 바로 위의 타입 관계에서의 문제 해결을 말함
- 단일 상속이 아닌 복수개의 기능을 가져올 수 있어야 함
- retroactive modeling이 가능해야함
- 원본을 수정하지 않은 채로 확장판을 만들어낼 수 있어야 함
- 모델에게 데이터 초기화와 인스턴스 데이터에 대한 책임을 넘기지 않아야 함
- 무엇을 구현해야 하는지 정확하게 명시해야함
⇒ 이를 위해 Swift 에서는 Protocol 을 사용한다.
프로토콜 활용해서 개선하기
위에서 봤던 코드를 프로토콜을 사용하는 방식으로 개선하자면 아래처럼 된다.
protocol Ordered {
// 프로토콜은 함수를 구현하지 않음
// Self를 통해 프로토콜이 아닌 프로토콜을 채택한 구현체의 타입을 반영할 수 있음
func precedes (other: Self) -> Bool
}
class Label: Ordered { var text: String = "" ⋯ }
// protocol은 구조체에서도 채택할 수 있음
// -> ref로 인해 발생하는 문제 제거 가능
struct Number : Ordered {
var value: Double = 0
override func precedes (other: Number) -> Bool {
return value ‹ other.value
}
}
Self 처럼 타입을 고정하거나 Generic을 통해 하나의 타입을 가진 Array로 강제할 수 있다. 이를 Homogeneous Collection 이라고 부름!
// 기존 방식에서는 sortedKeys에 Ordered를 상속받는 여러 타입이
// 하나의 배열에 섞여서 한 번에 들어오는 문제가 발생할 수 있음
func binarySearch(sortedKeys: [Ordered], forKey k: T) -> Int { ... }
// swift에서 generic을 사용해주면 protocol의 함수이더라도
// 프로토콜의 구현체 하나의 타입만 들어오도록 만들 수 있음
func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int { ... }
프로토콜을 사용하면 장점
- 기존 코드를 건드리지 않고 확장 수정이 가능하다 (retroactive modeling)
- 기존 코드에 “새로운 기능을 추가한다”는 코드를 넣지 않더라도 쉽게 확장이 가능함
- 기존 코드에 “새로운 기능을 추가한다”는 코드를 넣지 않더라도 쉽게 확장이 가능함
protocol newFeature {
func 새로운기능() -> 반환값
}
extension originFeature: newFeature {
func 새로운기능() -> 반환값 { 구현 }
}
- 확장에 대한 컨트롤이 쉽다
protocol MyProtocol {
func 새로운기능() -> 반환값
}
extension MyProtocol {
func 새로운기능() -> 반환값 { 구현1 }
func 또다른기능() -> 반환값 { 구현2 }
}
extension OriginFeature: myProtocol {
func 새로운기능() -> 반환값 { 구현3 }
func 또다른기능() -> 반환값 { 구현4 }
}
// 함수 실행 시 OriginFeature의 Extension에서 함수를 찾아 실행한다.
let origin = OriginFeature()
origin.새로운기능() => 구현3 실행
origin.또다른기능() => 구현4 실행
// MyProtocol 타입으로 정한다면, MyProtocol에서 함수 명세를 찾게됨
// 이때 또다른 기능은 MyProtocol에 정의되어있지 않으므로,
// MyProtocol의 Extension에서 함수를 찾아 실행하게 된다.
let new: MyProtocol = OriginFeature()
new.새로운기능() => 구현3 실행
new.또다른기능() => 구현2 실행
-
- 사실 요건 이번에 WWDC를 보면서 처음 알게된 방식임… 오 신기하다 ㅋㅋㅋㅋ
-
extension MyType where T : BType { func indexOf(element: T) -> Index? { ... } } -> T가 BType을 따를 때에만 MyType에서 indexOf 함수를 사용할 수 있음
- retroactive와 조건부 확장을 함께 쓰면 이런 것도 가능함
protocol Drawable { func isEqualTo(other: Drawable) -> Bool func draw() } extension Drawable where Self : Equatable { // Equatable인 타입만 받아왔기 때문에, == 연산자는 사용이 가능한 상황 // 이때, Drawable 끼리의 비교가 필요한 상황! // 파라미터를 Self 타입으로 받아버리면 같은 타입끼리만 비교 가능 ... X func isEqualTo(other: Drawable) -> Bool { // 만약 현재 인스턴스와 other 인스턴스가 같은 타입이라면 // == 을 통해 값을 비교하기 if let o = other as? Self { return self == o } // 만약 다른 경우에는 비교 의미가 없으므로 false 반환 return false } }
- 프로토콜을 채택한 타입에 대한 정보를 가져와 함수 실행에 영향을 줄 수도 있음
protocol Drawable {
func isEqualTo(other: Drawable) -> Bool
func draw()
}
extension Drawable where Self : Equatable {
// Equatable인 타입만 받아왔기 때문에, == 연산자는 사용이 가능한 상황
// 이때, Drawable 끼리의 비교가 필요한 상황!
// 파라미터를 Self 타입으로 받아버리면 같은 타입끼리만 비교 가능 ... X
func isEqualTo(other: Drawable) -> Bool {
// 만약 현재 인스턴스와 other 인스턴스가 같은 타입이라면
// == 을 통해 값을 비교하기
if let o = other as? Self { return self == o }
// 만약 다른 경우에는 비교 의미가 없으므로 false 반환
return false
}
}
클래스는 언제 써야하냐
- Implicit Sharing이 필요할 때 (copy가 아닌 reference로 접근이 필요할 때)
- 외부 환경에 인스턴스의 lifetime이 엮여있는 경우 (외부에서 수정이 가능 … reference)
- 인스턴스가 단순히 write-only conduit 목적(데이터 전달용)으로 사용되는 경우
'Develop > iOS 개발' 카테고리의 다른 글
[iOS] SwiftUI에 MVVM이 적합하지 않다고? (0) | 2024.05.05 |
---|---|
iOS 취업공고 16개 자격요건, 우대사항 간추리기 (0) | 2024.04.25 |
[Swift] DateFormatter는 매번 생성하지말고 재활용하자 (0) | 2024.03.17 |
[Swift] Array를 ContiguousArray로 바꿔서 최적화 (0) | 2024.03.16 |
ValueObject로 코드를 깔끔하게 (0) | 2024.03.11 |