├── Asynchronous_error_handling_with_Result.md ├── Delivering_quality_Swift_code.md ├── Demystifying-initializers.md ├── Effortless-error-handling.md ├── Generics.md ├── Iterators-sequences-and-collections.md ├── Making-optionals-second-nature.md ├── Modeling-data-with-enums.md ├── Protocol_extensions.md ├── Putting-the-pro-in-protocol-oriented-programming.md ├── README.md ├── Swift_patterns.md ├── Understanding_map,flatMap,and_compactMap.md └── Writing_cleaner_properties.md /Asynchronous_error_handling_with_Result.md: -------------------------------------------------------------------------------- 1 | # Asynchronous error handling with Result 2 | 3 | ## This chapter covers 4 | - Learning about the problems with Cocoa style error handling 5 | - Getting an introduction to Apple's Result type 6 | - Seeing how Result provides compite-time safety 7 | - Preventing bugs involving forgotten callbacks 8 | - Transforming data robustly with map, mapError, and flatMap 9 | - Focusing on the happy path when handling errors 10 | - Mixing throwing functions with Result 11 | - Learning how AnyError makes Result less restrictive 12 | - How to show intent with the Never type 13 | 14 | ## Why use the Result type? 15 | chapter 6에서 동기적 상황의 에러를 핸들링하는 방법을 살펴봤다면 앞으로는 비동기 상황에서의 에러 핸들링을 살펴볼 것입니다. 16 | 17 | 스위프트에서는 공식적으로 비동기 상황에서의 에러 핸들링 기법을 제공하지 않습니다. 18 | 19 | 대신 Swift Package Manager에서 제공하는 Result 타입을 사용하게 됩니다. 20 | 21 | **Result is like Optional, with a twist** 22 | 23 | Result 타입은 옵셔널과 같은 열거형으로 구현부는 아래 코드와 같습니다. 24 | 25 | ```swift 26 | public enum Result { 27 | /// Indicates success with value in the associated object. 28 | case success(Value) 29 | 30 | /// Indicates failure with error inside the associated object. 31 | case failure(ErrorType) 32 | 33 | // ... The rest if left out for later 34 | } 35 | ``` 36 | 37 | Result는 success(Value)와 failure(ErrorType) 두 개의 케이스를 가진 열거형입니다. 38 | 39 | 열거형이기 때문에 success **또는** failure 중 하나의 상태만 가지게 됩니다. 40 | 다시 말해 success와 failure가 동시에 참이거나 거짓일 가능성을 차단합니다. 41 | 42 | 또한 Result의 ErrorType은 Swift.Error 타입으로 제약되어 있습니다. 43 | 따라서 ErrorType에 들어오는 타입은 반드시 Error 타입을 채택해야 합니다. 44 | 45 | 그에 반해 success의 Value 타입에 경우 타입 제약이 없기 때문에 모든 타입이 Value로 들어올 수 있습니다. 46 | 47 | 옵셔널은 Value 또는 nil을 가지지만, Result는 Value 또는 Error를 가지게 됩니다. 48 | failure의 경우 단순히 빈 값(nil)을 가지는 옵셔널과 달리 Error를 가지기 때문에, 실패에 대한 맥락을 제공할 수 있습니다. 49 | 50 | 결과적으로 Result 타입은 이후 패턴 매칭을 통해 success와 failure에 대한 대응을 모두 구현해야 합니다. 51 | 52 | 에러가 발생할 때 Result 타입에 ErrorType을 넘겨 대응하고 정상적으로 동작한다면 Value를 넘겨 에러를 핸들링할 수 있습니다. 53 | 54 | **Understanding the benefits of Result** 55 | 56 | Result 타입의 이점을 느끼기 위해 먼저 Cocoa Touch-style의 에러 핸들링이 비동기 상황에서 보이는 문제점을 살펴봅시다. 57 | Cocoa Toach-style 에러 핸들링의 문제점을 Result 타입으로 해결해 봅시다. 58 | 59 | 지금부터 구현할 API는 iTunes Store에서 특정 URL로 검색하는 기능을 하는 API입니다. 60 | 61 | 먼저 Cocoa Touch-style의 코드를 살펴봅시다. 62 | 63 | ```swift 64 | func callURL(with url: URL, completionHandler: @escaping (Data?, Error?) -> Void) { 65 | let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in 66 | completionHandler(data, error) 67 | }) 68 | task.resume() 69 | } 70 | 71 | let url = URL(string: "https://itunes.apple.com/search?term=iron%20man") 72 | 73 | callURL(with: url) { (data, error) in 74 | if let error = error { 75 | print(error) 76 | } else if let data = data { 77 | let value = String(data: data, encoding: .utf8) 78 | print(value) 79 | } else { 80 | // error도 없고 data도 없는 상황 - 말도 안되는 상황에 대한 처리를 해야합니다. 81 | // What goes here? 82 | } 83 | } 84 | ``` 85 | 86 | 위의 callURL 함수의 URLSession.dataTask의 작업이 끝났을 때 completionHandler가 호출됩니다. 87 | URLSession.dataTask 작업에 시간이 걸리기 때문에 @escaping 클로저로 completionHandler를 선언했습니다. 88 | 89 | callURL 함수의 호출부를 보면 error와 data를 모두 체크해야 합니다. 90 | 91 | Cocoa Touch-style 방식은 callURL 함수에서 이론적으로 error와 data를 둘 다 받을 수 있고 못받을 수도 있습니다. error와 data 모두 없는 말도 안 되는 상황까지 대응해야 합니다. 92 | 93 | 또한 Cocoa Touch-style 방식은 에러 핸들링에 있어 컴파일 타임 이점을 얻지 못합니다. 94 | 95 | 하지만 Result 타입은 열거형으로 error **또는** data를 가집니다. 96 | 97 | 열거형의 성격으로 success와 failure가 동시에 참이거나 거짓인 상황을 success와 failure 중 한 가지로 줄일 수 있습니다. 98 | 99 | Result 타입을 사용해 컴파일 타임에 response를 success(with a value) 또는 failure(with an error)로 강제할 수 있습니다. 100 | 101 | 아래 코드는 위 Cocoa Touch-style API 호출을 Result 타입을 사용한 방식으로 고친 코드입니다. 102 | 103 | ```swift 104 | enum NetworkError: Error { 105 | case fetchFailed(Error) 106 | } 107 | 108 | func callURL(with url: URL, completionHandler: @escaping (Result) -> Void) { 109 | let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in 110 | // ... details will be filled in shortly. 아래에서 살펴볼 예정입니다. 111 | }) 112 | task.resume() 113 | } 114 | 115 | let url = URL(string: "https://itunes.apple.com/search?term=iron%20man") 116 | 117 | callURL(with: url) { (result: Result = .success(5) 150 | do { 151 | let value = try integerResult.get() 152 | print("The value is \(value).") 153 | } catch { 154 | print("Error retrieving the value: \(error)") 155 | } 156 | // Prints "The value is 5." 157 | ``` 158 | 159 | **Bridging from Cocoa Touch to Result** 160 | 161 | 이제는 callURL 함수 안의 URLSession API 호출 코드에 Result 타입을 사용해 코드를 개선해 봅시다. 162 | 163 | ```swift 164 | URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in ... } 165 | ``` 166 | 167 | URLSession.shared.dataTask가 리턴하는 data, response, error 세 가지 데이터를 Result 타입으로 변환해야 합니다. 168 | 169 | 세 가지 데이터를 Result 타입으로 변환하기 위해 Result 타입에 custom init을 추가해야 합니다. 170 | URLSession.shared.dataTask가 리턴하는 세 가지 데이터를 Result 타입으로 변환해야 아래와 같은 callURL 함수의 completionHandler를 만족할 수 있습니다. 171 | 172 | ```swift 173 | func callURL(with url: URL, completionHandler: @escaping (Result) -> Void) 174 | ``` 175 | 176 | 아래 코드는 Result 타입에 custom init을 추가하고 callURL 함수의 URLSession.shared.dataTask에서 리턴되는 세 가지 데이터를 Result 타입으로 변환하는 코드입니다. 177 | 178 | ```swift 179 | publice enum Result { 180 | // ... 생략 181 | 182 | // custom init 183 | init(value: Value?, error: ErrorType?) { 184 | if let error = error { 185 | self = .failure(error) 186 | } else if let value = value { 187 | self = .success(value) 188 | } else { 189 | fatalError("Could not create Result") 190 | } 191 | } 192 | } 193 | 194 | func callURL(with url: URL, completionHandler: @escaping (Result) -> Void) { 195 | let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) -> Void in 196 | let dataTaskError = error.map { NetworkError.fetchFailed($0) } 197 | let result = Result(value: data, error: dataTaskError) // Result enum의 custom init으로 Result 타입 생성 198 | completionHandler(result) 199 | }) 200 | task.resume() 201 | } 202 | ``` 203 | 204 | 위의 URLSession.shared.dataTask에서는 data를 리턴하고 있지만, 모든 API가 항상 value를 리턴하는것은 아닙니다. 205 | 206 | 이때 Result<(), MyError> 또는 Result와 같이 () 또는 Void를 사용해 value 값을 가지지 않는 Result 타입을 만들 수 있습니다. 207 | 만약 URLSession.share.dataTask가 data를 리턴하지 않는 API라면 callURL 함수의 completionHandler에서 @escaping(Result) -> Void가 아닌 @escaping(Result<(), NetworkError>) -> Void로 선언할 수 있습니다. 208 | 209 | ## Propagating Result 210 | 211 | URL을 전달하는 callURL 함수 대신 문자열을 전달하여 iTunes Store에서 항목을 검색할 수 있도록 API를 좀 더 높은 수준으로 만들어 보겠습니다. 212 | 213 | 지금까지 다룬 NetworkError와 같이 저차원 에러가 아닌 고차원 에러(SearchResultError)를 다룰 것입니다. 214 | NetworkError와 같은 저차원 에러보다 SearchResultError가 검색 기능의 추상적인 개념과 더 적합하기 때문입니다. 215 | 216 | 고차원 에러인 SearchResultError 코드를 살펴봅시다. 217 | 218 | ```swift 219 | enum SearchResultError: Error { 220 | case invalidTerm(String) // when an URL can't be created 221 | case underlyingError(NetworkError) // underlyingError can carries the lower-level NetworkError for troubleshooting 222 | case invalidData // when the raw data could not be parsed to JSON 223 | } 224 | 225 | search(term: "Iron man") { result: Result<[String: Any]>, SearchResultError> in 226 | print(result) 227 | } 228 | ``` 229 | 230 | **Typealiasing for convenience** 231 | 232 | search() 함수를 구현하기 전에 **typealias** 키워드로 Result 타입을 축약해 편리하게 사용합시다. 233 | 234 | typealias 키워드를 통해 Result 타입의 data나 error의 타입을 고정할 수 있습니다. 235 | 236 | 예를 들어 Result 타입을 SearchResult로 typealias 한다면 에러 타입을 SearchResultError 타입으로 고정하게 됩니다. 237 | SearchResult 타입은 Value와 Error 타입 모두 제네릭 타입이었지만 이제는 Value만 제네릭 타입이 됩니다. 238 | 239 | 아래 코드로 살펴봅시다. 240 | 241 | ```swift 242 | // 에러 타입이 SearchResultError로 고정됩니다. 243 | typealias SearchResult = Result 244 | 245 | let searchResult = SearchResult("Tony Stark") 246 | print(searchResult) // success("Tony Stark") 247 | ``` 248 | 249 | 위 코드에서 SearchResult 타입의 에러 타입은 SearchResultError로 고정되었고 Value 타입은 고정되지 않았습니다. 250 | 251 | JSON 타입도 typealias 키워드를 통해 만들어 봅시다. 252 | [String: Any] 타입보다 JSON 타입이 읽기 쉬운 코드를 만듭니다. 253 | 254 | ```swift 255 | typealias JSON = [String: Any] 256 | ``` 257 | 258 | 결과적으로 typealias 키워드를 통해 SearchResult 타입을 만들었습니다. 259 | 260 | SearchResult 타입의 실제 타입은 Result<[String: Any], SearchResultError> 타입인 사실을 기억해야 합니다. 261 | 262 | **The search function** 263 | 264 | 이제 본격적으로 문자열을 전달하여 iTunes Store에서 항목을 검색하는 search 함수를 구현해 봅시다. 265 | 266 | search() 함수에서는 completionHandler로 SearchResult 타입을 리턴하기 위해, data를 JSON으로 파싱하고 저차원 에러 NetworkError를 고차원 에러 SearchResultError로 변환하는 과정을 포함하고 있습니다. 267 | 268 | 아래 코드로 살펴봅시다. 269 | 270 | ```swift 271 | func search(term: String, completionHandler: @escaping (SearchResult) -> Void) { 272 | // encodedString 변수는 옵셔널 타입입니다. 273 | let encodedString = term.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 274 | // map을 사용해 옵셔널 언래핑을 딜레이합니다. 275 | let path = encodedString.map { "https://itunes.apple.com/search?term=" + $0 } 276 | 277 | guard let url = path.flatMap(URL.init) else { 278 | completionHandler(SearchResult(.invalidTerm(term))) // 1. CompletionHandler 호출 279 | return 280 | } 281 | 282 | callURL(with: url) { result in 283 | switch result { 284 | case .success(let data): 285 | if let json = try? JSONSerialization.jsonObject(with: data, options: []), 286 | let jsonDictionary = json as? JSON { 287 | let result = SearchResult(jsonDictionary) 288 | completionHandler(result) // 2. CompletionHandler 호출 289 | } else { 290 | let result = SearchResult(.invalidData) 291 | completionHandler(result) // 3. CompletionHandler 호출 292 | } 293 | case .failure(let error): 294 | // lower-level error를 higher-level error로 변환합니다. 295 | let result = SearchResult(.underlyingError(error)) 296 | completionHandler(result) // 4. CompletionHandler 호출 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | 위의 코드에서는 네 번의 CompletionHandler를 호출합니다. 303 | 네 번의 CompletionHandler 호출에 필요한 Result 타입도 네 개가 필요합니다. 304 | 305 | 이런 점이 위의 코드를 boilerplate code라고 할 수 있습니다. 306 | 307 | 이때 **map, flatMap, flatMap**을 사용해 단일 Result 타입의 조작으로(Value와 Error를 변환) 한 번의 CompletionHandler 호출을 가진 함수로 구현할 수 있습니다. 308 | 309 | ## Transforming values inside Result 310 | 311 | 옵셔널에 map을 사용해 옵셔널 언래핑을 미뤘던 것처럼 Result 타입과 map도 함께 사용할 수 있습니다. 312 | 옵셔널을 매핑하듯이 Result를 매핑하여 변형할 수 있습니다. 313 | 314 | 매핑 없이 Result를 사용할 때, Result 타입을 전달하며 변형한 이후 switch(패턴 매칭)를 통해 Result 내부의 값을 추출할 수 있습니다. 315 | 316 | 하지만 map을 사용하여 옵셔널을 매핑할 때 옵셔널 언래핑 없이 내부 값을 다루고 다시 옵셔널로 감쌌듯이, Result를 매핑하면 success 케이스의 경우 내부 value가 클로저로 들어가고 failure 케이스 경우 map 연산이 무시됩니다. 317 | 이후 내부 value는 다시 Result로 감싸서 리턴됩니다. 318 | 319 | 위의 search 함수에서 Result 타입의 data를 JSON으로 변환할 때 map을 사용해 여러 번의 CompletionHandler 호출을 단일 호출 방식으로 고쳐봅시다. 320 | 321 | Result 타입에 map이 동작하는 방법을 살펴봅시다. 322 | 323 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/f6acd685-7b52-4394-8484-947a78d969c3) 324 | 325 | map은 failure 케이스의 Result 타입에는 동작하지 않고 success 케이스의 Result 타입에만 동작합니다. 326 | 327 | 하지만 **mapError**을 사용하면 map과 반대로 Result 타입이 failure 케이스일 때 에러를 매핑하고 success 케이스의 경우 mapError가 동작하지 않습니다. 328 | 329 | Result 타입에 mapError가 동작하는 과정을 살펴봅시다. 330 | 331 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/f9decc92-6cfa-4b7d-a235-fa3042eaeab2) 332 | 333 | **다시 말해 map을 통해 Result 타입의 Value를 매핑하여 값을 변환하고, mapError로 Result 타입의 Error를 매핑하여 값을 변환할 수 있습니다.** 334 | 335 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/01a3f81c-4caf-4c40-bb4e-63e391c5e6b3) 336 | 337 | map과 mapError의 기능을 결합하면 Result를 SearchResult이라고도 불리는 Result로 바꿀 수 있습니다. 338 | 339 | 지금부터 map과 mapError의 기능을 결합해 Result 타입의 데이터 변환하여 위의 보일러 플레이트 코드를 개선합시다. 340 | 341 | 아래 코드로 살펴봅시다. 342 | 343 | ```swift 344 | func search(term: String, completionHandler: @escaping (SearchResult) -> Void) { 345 | // ... 생략 346 | 347 | callURL(with: url) { result in 348 | let convertedResult: SearchResult = 349 | result 350 | .map { (data: Data) -> JSON in 351 | guard let json = try? JSONSerialization.jsonObject(with: data, options: []), 352 | let jsonDictionary = json as? JSON else { 353 | // data를 json으로 변환 시 실패하면 빈 딕셔너리를 리턴합니다. 354 | return [:] 355 | } 356 | return jsonDictionary 357 | } 358 | .mapError { (networkError: NetworkError) -> SearchResultError in 359 | return SearchResultError.underlying(networkError) 360 | } 361 | // 단일 Result를 조작하여 completionHandler를 한 번만 호출하도록 개선합니다. 362 | completionHandler(convertedResult) 363 | } 364 | } 365 | ``` 366 | 367 | 위의 코드와 같이 map과 mapError를 결합해 Result 타입 내의 value와 error를 변환하여, 단일 Result를 completionHandler로 리턴하여 한 번의 completionHandler만 호출하도록 함수를 개선했습니다. 368 | 369 | **매핑을 통해 옵셔널의 내부값을 얻기 전까지 언래핑을 연기했듯이, Result의 내부 값을 얻고 싶을 때까지 오류 처리를 연기합니다.** 370 | 371 | 하지만 위의 코드에서 data를 JSON으로 변환에 실패하면 빈 딕셔너리를 리턴하고 있습니다. 372 | 373 | 빈 딕셔너리를 리턴하는 대신 Error를 표현하는 방식으로 개선하려 합니다. 374 | 이때 failure 케이스인 Result를 통해 Error를 표현할 수 있습니다. 375 | 376 | 이때 우리는 **faltMap**을 사용하게 됩니다. 377 | 378 | **flatMapping over Result** 379 | 380 | Result 타입 없이, 빈 딕셔너리 대신 Error를 던지는 방식으로 개선할 수 있지만 Result 타입을 사용하는 것과 error throwing이 섞이면 혼란스러울 수 있습니다. (뒤에서 Result 타입과 error throwing을 섞는 방식도 살펴볼 예정입니다) 381 | 382 | 따라서 빈 딕셔너리를 리턴하는 대신 failure 케이스를 가진 Result를 리턴하는 방식으로 코드를 개선할 것입니다. 383 | 384 | 하지만 Result를 매핑한 map 클로저 내에서 빈 딕셔너리 대신 Result(failure)를 리턴하면, 결과적으로 중첩 Result 타입인 SearchResult>가 리턴됩니다. 385 | 386 | 이때 중첩 Result를 flatMap을 통해 단일 Result로 평탄화할 수 있습니다. 387 | 물론 failure 케이스인 Result는 map에서와 동일하게 flatMap에서도 무시됩니다. 388 | 389 | 결과적으로 flatMap을 사용해 flatMap 클로저 내부에서 진행되는 data -> JSON 변환 과정에서 발생되는 에러를 failure의 Result 타입으로 리턴하여 중첩 Result 타입을 단일 Result 타입으로 리턴합니다. 390 | 391 | flatMap이 Result와 동작하는 과정을 살펴봅시다. 392 | flatMap의 클로저에서 데이터를 변환하고 변환 실패 시 failure 케이스의 Result를 생성하고 성공 시 success 케이스의 Result를 생성하게 됩니다. 393 | 394 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/2004f1fb-8d88-4fb4-b324-068012c2ca0f) 395 | 396 | 위에서 데이터 변환 실패 시 빈 딕셔너리를 리턴하는 코드를 failure 케이스의 Result를 리턴하도록 고쳐봅시다. 397 | 398 | ```swift 399 | func search(term: String, completionHandler: @escaping (SearchResult) -> Void) { 400 | // ... 생략 401 | 402 | callURL(with: url) { result in 403 | let convertedResult: SearchResult = 404 | result 405 | .mapError { (networkError: NetworkError) -> SearchResultError in 406 | return SearchResultError.underlying(networkError) 407 | } 408 | .flatMap { (data: Data) -> SearchResult in // 리턴되는 타입이 JSON에서 SearchResult으로 수정됐습니다. 409 | guard let json = try? JSONSerialization.jsonObject(with: data, options: []), 410 | let jsonDictionary = json as? JSON else { 411 | // data를 json으로 변환 시 실패하면 빈 딕셔너리를 리턴하지 않고 failure 케이스를 가진 Result 타입을 리턴합니다. 412 | return SearchResult(.invalidData) 413 | } 414 | return SearchResult(jsonDictionary) 415 | } 416 | completionHandler(convertedResult) 417 | } 418 | } 419 | ``` 420 | 421 | 위에서 이야기 했듯이 Result 타입의 Error는 flatMap으로 변환할 수 없습니다. 422 | Result 타입의 Error는 mapError로 변환 가능합니다. 423 | 424 | ## Mixing Result with throwing functions 425 | 426 | 지금까지는 Result 타입의 mapping, flatmapping 연산 안에서(클로저 안에서) 에러를 던지는 함수를 호출하는 방식을 혼란스럽다는 이유로 피했습니다. 427 | 428 | 지금부터 Result 타입의 mapping, flatmapping 연산 안에서 에러를 던지는 함수를 추가해 보고, 마지막에는 에러를 던지지 않고 파이프라인 방식으로 Result 타입의 전달만으로 에러를 핸들링하는 방법을 살펴봅시다. 429 | 430 | search 함수의 flatMap 클로저 안에서 data를 JSON으로 변환할 때 실패 시 에러를 던지는 parseData 함수를 추가해 봅시다. 431 | parseData 함수는 JSON으로 데이터 변환에 실패했을 때 ParsingError 타입 에러를 던집니다. 432 | 433 | ParsingError 열거형과 parseData 함수를 코드로 살펴봅시다. 434 | 435 | ```swift 436 | enum ParsingError: Error { 437 | case couldNotParseJSON 438 | } 439 | 440 | // data를 JSON으로 변환하며 실패 시 에러를 던집니다. 441 | func parseData(_ data: Data) throws -> JSON { 442 | guard let json = try? JSONSerialization.jsonObject(with: data, options: []), 443 | let jsonDictionary = json as? JSON else { 444 | throw ParasingError.couldNotParseJSON 445 | } 446 | return jsonDictionay 447 | } 448 | ``` 449 | 450 | 에러를 던지는 함수가 에러를 던졌을 때 이를 failure 케이스의 Result 타입으로 변환하여 대응할 수 있습니다. 451 | 452 | Result의 생성자(init)에 에러를 던지는 함수를 try 키워드와 함께 넣습니다. 453 | 454 | 이때 함수가 에러를 던지는 여부에 따라 Result가 success 케이스로 생성될지 failure 케이스로 생성될지 결정됩니다. 455 | 함수가 에러를 던졌을 때 failure 케이스의 Result 타입을 생성하고 에러를 던지지 않을 때 success 케이스의 Result 타입이 생성됩니다. 456 | 457 | 아래 코드로 살펴봅시다. 458 | 459 | ```swift 460 | // parseData 함수가 에러를 던지면 Result failure, 던지지 않으면 Result success로 Result 타입이 생성됩니다. 461 | let searchResult: Result = Result(try parseData(data)) 462 | ``` 463 | 464 | 하지만 위의 코드는 한 가지 문제가 있습니다. 465 | 466 | parseData 함수가 던질 에러 타입을 런타임에서야 알 수 있고, 만약 parseData 함수가 SearchResultError 타입의 에러를 던지지 않는다면 다른 타입의 에러에는 추가로 대응해야 합니다. 467 | 468 | 이를 해결하기 위해서 아래 코드와 같이 **do-catch** 구문이 필요합니다. 469 | 470 | ```swift 471 | do { 472 | let searchResult: Result = Result(try parseData(data)) 473 | } catch { 474 | print(error) // ParsingError.couldNotParseData 475 | // 결국 SearchResultError와 다른 에러 타입이 리턴되면 catch에서 Result(.invalidData(data)를 리턴해야 합니다. 476 | let searchResult: Result = Result(.invalidData(data)) 477 | } 478 | ``` 479 | 480 | throwing function을 Result 타입으로 변환했다면, 위의 do-catch 구문으로 search 함수를 완성해 봅시다. 481 | 482 | 아래 코드로 살펴봅시다. 483 | 484 | ```swift 485 | func search(term: String, completionHandler: @escaping (SearchResult) -> Void) { 486 | // ... 생략 487 | 488 | callURL(with: url) { result in 489 | let convertedResult: SearchResult = 490 | result 491 | .mapError { SearchResultError.underlyingError($0) } 492 | .flatMap { (data: Data) -> SearchResult in 493 | do { 494 | let searchResult: SearchResultError = Result(try parseData(data)) 495 | return searchResult 496 | } catch { 497 | // parseData 함수에서 던지는 에러를 SearchResultError로 변환합니다. 498 | return SearchResult(.invalidData(data)) 499 | } 500 | } 501 | completionHandler(convertedResult) 502 | } 503 | } 504 | ``` 505 | 506 | **Weaving errors through a pipeline** 507 | 508 | 위에서 살펴본 map, flatMap, mapError를 파이프라인 형식으로 구성하여 에러를 던지는 함수 없이도 에러를 핸들링할 수 있습니다. 509 | Result 타입이 파이프라인을 통과하고 나서 최종적으로 패턴 매칭을 통해 에러를 핸들링합니다. 510 | 511 | flatMap은 에러가 발생한 상황에 프로그램의 흐름을 Error path로 바꾸지만, map은 항상 Happy path에 프로그램의 흐름을 유지하게 합니다. 512 | 513 | flatMap이 사용하게 된 이유가 에러를 던질 상황에 Result 타입을 리턴하기 위함으로 에러가 발생한 상황에 프로그램의 흐름을 Error path로 바꾸는 것입니다. 514 | 515 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/e4e0e055-c024-4cb2-9633-bf8bca4fcfc3) 516 | 517 | 에러를 던지는 함수 없이 파이프라인 방식으로 에러를 핸들링한 아래 코드를 살펴봅시다. 518 | 519 | ```swift 520 | func search(term: String, completionHandler: @escaping (SearchResult) -> Void) { 521 | // ... 생략 522 | 523 | callURL(with: url) { result in 524 | let convertedResult: SearchResult = 525 | result 526 | // Transform error type to SearchResultError 527 | .mapError { (networkError: NetworkError) -> SearchResultError in 528 | return SearchResultError.underlying(networkError) 529 | } 530 | // Parse Data to JSON, or return SearchResultError 531 | .flatMap { (data: Data) -> SearchResult in 532 | // 생략 533 | } 534 | // validate Data 535 | .flatMap { (json: JSON) -> SearchResult in 536 | // 생략 537 | } 538 | // filter value 539 | .map { (json: JSON) -> [JSON] in 540 | // 생략 541 | } 542 | // Save to database 543 | .flatMap { (mediaItems: [JSON]) -> SearchResult in 544 | // 생략 545 | database.store(mediaItems) 546 | } 547 | completionHandler(convertedResult) 548 | } 549 | } 550 | ``` 551 | 552 | map과 flatMap은 Result 타입이 failure 케이스일 경우 무시됩니다. 553 | flatMap에서 특정 에러로 인해 failure 케이스인 Result가 리턴된다면 이후의 mapping이나 flatmapping은 무시됩니다. 554 | 555 | ## Multiple errors inside of Result 556 | 557 | 지금까지는 에러가 발생했을 때 Result 타입의 failure 케이스가 단일 에러 타입을 가지도록 했습니다. 558 | 위에서 다뤘던 Result 타입의 단일 에러 타입은 SearchResultError입니다. 559 | 560 | 에러 타입을 하나로 특정하는 방법은 좋은 방법입니다. 561 | 562 | 하지만 다뤄야 할 에러 타입이 너무 다양하다면, 초기 프로젝트 시기에는 모든 에러 타입을 정확한 단일 에러 타입으로 확정하기에 부담스러울 수 있습니다. 563 | 564 | 이때 우리는 SPM(Swift Package Manager)에서 제공하는 제네릭 타입인 **AnyError**를 사용하여 에러의 타입을 런타임에 알 수 있도록 하여 정확한 에러 타입을 선언할 부담을 덜어줍니다. 565 | 566 | **AnyError는 Result 내부의 Error에 들어가는 모든 에러 타입에 대응할 수 있습니다.** 567 | Result 내부의 에러는 Error 타입을 따르기 때문에 AnyError 또한 Error 타입을 따르는 모든 에러에 대응할 수 있습니다. 568 | 569 | 예를 들어 Result와 같이 사용될 수 있습니다. 570 | 571 | AnyError를 가진 Result 타입을 생성하는 방법은 두 가지가 있습니다. 572 | 573 | 첫 번째로 Error 타입을 AnyError로 변환 후 Result 타입에 변환된 AnyError를 넣는 방식입니다. 574 | 이때 AnyError(Error)와 같은 형식으로 AnyError의 생성자에 Error 타입을 넣어 AnyError를 생성할 수 있습니다. 575 | 576 | 아래 코드와 같습니다. 577 | 578 | ```swift 579 | enum PaymentError: Error { 580 | case amountTooLow 581 | case insufficientFunds 582 | } 583 | 584 | let error: AnyError = AnyError(PaymentError.amountTooLow) 585 | let result: Result = Result(error) 586 | 587 | let directResult: Result = Result(PaymentError.amountTooLow) 588 | ``` 589 | 590 | result와 같이 AnyError를 Result 생성자에 넣을 수 있고 directResult와 같이 이미 자료형을 Result로 선언했다면 일반적인 Error를 Result 생성자에 바로 넣을 수 있습니다. 591 | 자료형이 Result이기 때문에 Result의 생성자로 들어온 Error 타입도 자동으로 AnyError 타입으로 형변환됩니다. 592 | 593 | 두 번째 방법은 에러를 던지는 함수를 Result의 생성자에 넣는 방법입니다. 594 | 595 | 아래 코드로 살펴봅시다. 596 | 597 | ```swift 598 | let otherResult: Result = Result(anyError: { () throws -> String in 599 | throw PaymentError.insufficientFunds 600 | } 601 | ``` 602 | 603 | AnyError는 에러 타입을 런타임에 알게 됩니다. 따라서 정확한 에러 타입을 알 필요 없을 때 AnyError를 Result 타입과 함께 사용할 수 있습니다. 604 | 파이프라인에서 단계별로 리턴되는 에러 타입이 다양할 경우, AnyError를 사용해 리턴되는 에러들을 특정 에러 타입으로 변환할 부담을 줄여줍니다. 605 | 606 | AnyError를 활용하여 processPayment라는 돈을 이체하는 함수를 만들어봅시다. 607 | 각 단계에서 다양한 유형의 오류를 반환할 수 있으므로 AnyError를 통해 다양한 오류를 하나의 특정 유형으로 변환해야 하는 부담을 줄일 수 있습니다. 608 | 609 | 아래 코드로 살펴봅시다. 610 | 611 | ```swift 612 | func processPayment(fromAccount: Account, toAccount: Account, amoutInCents: Int, completion: @escaping (Result) -> Void) { 613 | guard amountInCents > 0 else { 614 | completion(Result(PaymentError.amountTooLow)) 615 | return 616 | } 617 | 618 | guard isValid(toAccount) && isValid(fromAccount) else { 619 | completion(Result(AccountError.invalidAccount)) 620 | return 621 | } 622 | 623 | // Process payment 624 | 625 | moneyAPI.transfer(amountInCents, from: fromAccount, to: toAccount) { (result: Result) in 626 | let response = result.mapAny(parseResponse) // mapAny!! 627 | completion(response) 628 | } 629 | } 630 | ``` 631 | 632 | AnyError를 가진 Result 타입을 사용하면 Result 타입에 mapAny를 사용할 수 있습니다. 633 | 634 | mapAny 메소드는 에러를 던지는 함수를 허용한다는 점을 제외하면 map과 유사하게 작동합니다. 635 | 636 | mapAny 안의 함수에서 error를 던지면 해당 에러를 AnyError로 감싸서 리턴하게 됩니다. 637 | 즉 AnyError를 가진 Result failure 케이스를 리턴합니다. 638 | 639 | mapAny를 통해 에러 핸들링(catch) 없이 map으로 에러를 던지는 함수를 넘길 수 있습니다. 640 | 641 | mapAny는 flatMap 처럼 클로저 안에서 새로운 Result 타입을 리턴하여 Result의 에러 타입을 변환할 수는 없습니다. 642 | 그저 mapAny에 속하는 함수가 에러를 리턴하면 해당 에러를 AnyError로 감싸서 리턴 시켜줍니다. 643 | 644 | 따라서 map은 연산에 속하는 함수가 에러를 던지지 못하는 함수임을 나타내고, mapAny는 에러를 던질 수 있는 함수임을 나타냅니다! 645 | 646 | map과 mapAny의 차이점은 map이 모든 Result 유형에서 작동하지만, 함수 발생 시 오류를 포착하지 못한다는 것입니다. 647 | 반대로, mapAny는 에러를 던지는 함수와 던지지 않는 함수 모두에서 작동하지만, AnyError를 포함하는 Result 유형에서만 사용할 수 있습니다. 648 | 649 | **결과적으로 mapAny를 통해 Result의 value를 매핑하고 에러가 발생할 경우 AnyError로 감싼 에러를 가진 Result 타입을 얻을 수 있습니다.** 650 | 651 | **Matching with AnyError** 652 | 653 | AnyError 속 실제 에러 타입을 꺼내기 위해서는 **underlyingError** 프로퍼티를 사용해야 합니다. 654 | 655 | AnyError 속 에러에 따라 failure 케이스를 매칭하는 아래 코드를 살펴봅시다. 656 | 657 | ```swift 658 | processPayment(fromAccount: from, toAccount: to, amountInCents: 100) { (result: Result) in 659 | switch result { 660 | case .success(let value): print(value) 661 | case .failure(let error) where error.underlyingError is AccountError: 662 | // Result 타입이 가진 AnyError의 타입이 AccountError 타입일 경우를 나타냅니다. 663 | print("Account error") 664 | case .failure(let error): 665 | print(error) 666 | } 667 | } 668 | ``` 669 | 670 | AnyError를 사용하면 더욱 유연성있는 코드를 만듭니다. 671 | 672 | 하지만 AnyError는 Result의 error 타입을 컴파일 타임에 알 수 있도록 했던 장점을 잃게 됩니다. 673 | 따라서 프로젝트 초기가 아니고 시간적 여유가 있다면 AnyError가 아니라 일반적 error를 사용해 컴파일 타임에 이점을 얻을 수 있도록 합시다. 674 | 675 | ## Impossible failure and Result 676 | 677 | Result 타입을 가지는 프로토콜을 따를 때, 프로토콜을 따르는 타입에서 Result의 failure가 발생하는 상황이 절대 일어나지 않는 경우가 있습니다. (never fail) 678 | 679 | Result 타입은 success와 failure 케이스를 가지지만, failure 케이스가 발생할 가능성이 없을 때 빈 Error 타입을 만들어서 대응하거나 Never 타입으로 대응할 수 있습니다. 680 | 681 | 먼저 Result 타입을 가지는 프로토콜을 코드로 구현해 봅시다. 682 | 683 | ```swift 684 | protocol Service { 685 | associatedtype Value 686 | associatedtype Err: Error 687 | func load(complete: @escaping (Result) -> Void) 688 | } 689 | ``` 690 | 691 | 위의 Service 프로토콜을 따르는 SubscriptionsLoader 타입에서 load 함수를 항상 성공하는 함수라고 가정하겠습니다. 692 | 항상 성공하는 함수의 리턴이 Result 타입이기 때문에 Error 타입을 따르는 빈 열거형을 만들어 Result 속 Error로 넣어야 합니다. 693 | 694 | 아래 코드는 Service 프로토콜을 따르는 SubscriptionsLoader 클래스를 구현한 코드입니다. 695 | SubscriptionsLoader 클래스의 load 함수는 항상 성공하는 함수이기 때문에 Result 타입으로 리턴되는 Error에는 빈 열거형을 넣게 됩니다. 696 | 697 | ```swift 698 | struct Subscription { 699 | // ...details omitted 700 | } 701 | 702 | // 빈 열거형 703 | enum BogusError: Error {} 704 | 705 | final class SubscriptionsLoader: Service { 706 | func load(complete: @escaping (Result<[Subscription], BogusError>) -> Void) { 707 | // ...load data. Always succeeds 708 | let subscriptions = [Subscription(), Subscription()] 709 | complete(Result(subscriptions)) 710 | } 711 | } 712 | ``` 713 | 714 | BogusError는 빈 열거형이기 때문에 인스턴스화 할 필요도 할 수 없습니다. 715 | 또한 Error 케이스에 빈 열거형(BogusError)을 넣으면, Result 타입의 failure 케이스를 switch case 매칭할 수 없습니다. 716 | 717 | 물론 아래 코드와 같이 failure 케이스 매칭 없이 success 케이스만 case 매칭할 수 있습니다. 718 | 719 | ```swift 720 | let subscriptionsLoader = SubscriptionsLoader() 721 | subscriptionsLoader.load { (result: Result<[Subscription], BogusError>) in 722 | switch result { 723 | case .success(let subscriptions): print(subscriptions) 724 | // You don't need .failure 725 | } 726 | } 727 | ``` 728 | 729 | 이처럼 에러가 발생하지 않는 상황에서 빈 열거형을 Error 타입으로 넣는 방식의 장점은 열거형의 케이스를 줄여 패턴 매칭 코드를 줄이고 코드를 깔끔하게 만듭니다. 730 | 731 | 하지만 빈 열거형을 Error 타입으로 넣는 방식은 공식적인 방식은 아닙니다. 732 | 733 | **Never** 타입이 빈 열거형을 대신하는 공식적인 방식입니다. 734 | 735 | Never 타입은 컴파일러에 특정 경로(케이스)로 프로그램의 흐름이 가지 않는다는 사실을 알립니다. 736 | 다시 말해, 불가능한 경로를 나타냅니다. 737 | 738 | 아래 코드로 Never 타입을 살펴봅시다. 739 | 740 | ```swift 741 | func crashAndBurn() -> Never { 742 | fatalError("Something very, very bad happened") 743 | } 744 | ``` 745 | 746 | 위의 crashAndBurn 함수는 Never 타입을 리턴하기 때문에 절대 값을 리턴하지 않는 함수임을 알 수 있습니다. 747 | 748 | Never 타입의 구현부는 아래 코드와 같습니다. Never 타입도 빈 열거형입니다. 749 | 이미 스위프트에서 지원하는 빈 열거형인 Never 타입이 있는데 굳이 추가적인 빈 열거형을 사용할 필요는 없습니다. 750 | 751 | ```swift 752 | public enum Never {} 753 | ``` 754 | 755 | 앞에서 봤던 빈 열거형 BogusError를 Never 타입으로 대체 할 수 있습니다. 756 | 757 | 물론 Result 타입의 에러로 Never 타입을 사용하려면 Never 타입을 Error 타입을 따르도록 해야 합니다. 758 | Result 타입의 에러는 Error 타입을 따르도록 타입 제약이 있기 때문입니다. 759 | 760 | 아래 코드는 BogusError를 Never 타입으로 고친 코드입니다. 761 | 762 | ```swift 763 | // Never 타입이 Error 타입을 따르도록 합니다. 764 | extension Never: Error {} 765 | 766 | final class SubscriptionsLoader: Service { 767 | func load(complete: @escaping (Result<[Subscription], Never>) -> Void) { 768 | // ...load data. Always succeeds 769 | let subscriptions = [Subscription(), Subscription()] 770 | complete(Result(subscriptions)) 771 | } 772 | } 773 | ``` 774 | 775 | Result 타입의 Error에 Never를 넣을 수 있듯이 Success 케이스의 Value에도 Never를 넣을 수 있습니다. 776 | Success 케이스의 Value에 Never를 넣게 되면 절대 Success 되지 않는 동작을 의미합니다. 777 | 778 | ## Summary 779 | - Using the default way of URLSession's data tasks is an error-prone way of error handling. 780 | - Result is offered by the Swift Package Manager and is a good way to handle asynchronous error handling. 781 | - Result has two generics and is a lot like Optional, but has a context of why something failed. 782 | - Result is a compile-time safe way of error handling, and you can see which error to expect before running a program. 783 | - By using map and flatMap and mapError, you can cleanly chain transformations of your data while carrying an error context. 784 | - Throwing functions can be converted to a Result via a special throwing initializer. This initializer allows you to mix and match two error throwing idioms. 785 | - You can postpone strict error handling with the use of AnyError. 786 | - With AnyError, multiple errors can live inside Result. 787 | - If you're working with many types of errors, working with AnyError can be faster, at the expense of not knowing which errors to expect at compile time. 788 | - AnyError can be a good alternative to NSError so that you reap the benefits of Swift error types. 789 | - You can use the Never type to indicate that a Result can't have a failure case, or a success case. 790 | -------------------------------------------------------------------------------- /Delivering_quality_Swift_code.md: -------------------------------------------------------------------------------- 1 | # Delivering quality Swift code 2 | 3 | ## This chapter covers 4 | - Documenting code via Quick Help 5 | - Writing good comments that don't distract 6 | - How style isn't too important 7 | - Getting consistency and fewer bugs with SwiftLint 8 | - Splitting up large classes in a Swifty way 9 | - Reasoning about making types generic 10 | 11 | ## API documentation 12 | 드디어 마지막 장입니다. ^~^ 13 | 14 | 마지막 챕터(Delivering quality Swift code)에서는 코드적인 부분보다 프로젝트에 유용하게 쓸 수 있는 도구들을 살펴볼 예정입니다. 15 | 16 | 프로젝트는 주로 여러 동료와 함께합니다. 17 | 따라서 자연스럽게 동료들이 짠 코드를 읽어야 하는 일이 빈번합니다. 18 | 19 | 이때 API documentation을 만들어 프로젝트의 코드를 문서화할 수 있습니다. 20 | 21 | API documentation은 외부에서 접근할 수 있는 public 요소를 설명하는 중요한 역할을 합니다. 22 | 23 | 프로젝트의 코드를 문서화하는 방법은 Quick Help를 사용하거나 Jazzy 패키지를 사용해 문서화된 웹 페이지를 만드는 방법이 있습니다. 24 | 25 | 먼저 Quick Help를 사용하는 방법을 살펴봅시다. 26 | 27 | Quick Help는 짧은 마크다운으로 코드상에서 아래와 같이 표시됩니다. 28 | 29 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/0004055c-4ed8-467d-b25d-48d0a9d9fffa) 30 | 31 | 위와 같이 코드상에서 마우스를 통해 표시되며 아래와 같이 sidebar에도 표시됩니다. 32 | 33 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/92721cc4-2f3e-48b8-b493-5f6855be4e81) 34 | 35 | 위와 같은 Quick Help는 '///'을 통해 작성할 수 있습니다. 36 | 37 | 아래 코드로 살펴봅시다. 38 | 39 | ```swift 40 | /// A player's turn in a turn-based online game. 41 | enum Turn { 42 | /// Player skips turn, will receive gold. 43 | case skip 44 | /// Player uses turn to attack location. 45 | /// - x: Coordinate of x location in 2D space. 46 | /// - y: Coordinate of y location in 2D space. 47 | case attack(x: Int, y: Int) 48 | /// Player uses round to heal, will not receive gold. 49 | case heal 50 | } 51 | ``` 52 | 53 | **Adding callouts to Quick Help** 54 | 55 | Quick Help에 함수가 에러를 던지는 여부나 예시 코드를 추가할 수 있습니다. 56 | 57 | 이때 'tab text'를 통해 Quick Help에 설명을 추가합니다. 58 | 59 | 아래 코드로 살펴봅시다. 60 | 61 | ```swift 62 | /// Takes an array of turns and plays them in a row. 63 | /// 64 | /// - Parameter turns: One or multiple turns to play in a round. 65 | /// - Returns: A description of what happened in the turn. 66 | /// - Throws: TurnError 67 | /// - Example: Passing turns to `playTurn`. 68 | /// 69 | /// let turns = [Turn.heal, Turn.heal] 70 | /// try print(playTurns(turns)) "Player healed twice." 71 | func playTurns(_ turns: [Turn]) throws -> String { 72 | ``` 73 | 74 | 위와 같이 tab text가 추가되어 아래와 같은 Quick Help가 작성됩니다. 75 | 76 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/64052fdd-cd2f-4650-b173-9303bc8170d8) 77 | 78 | **Documentation as HTML with Jazzy** 79 | 80 | Quick Help를 작성하는 방법 이외에도 프로젝트를 문서화하는 방법이 있습니다. 81 | 82 | realm에서 제공하는 Jazzy 패키지를 사용하여 프로젝트를 문서화할 수 있습니다. 83 | 84 | You can apply Jazzy to any project where you'd like to generate a website with documentation. 85 | 86 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/e23d3992-9c35-43e3-9321-1f13186e72a9) 87 | 88 | Jazzy는 command-line에서 적용할 수 있고 아래 코드와 같이 실행할 수 있습니다. 89 | 90 | ```swift 91 | // Jazzy 패키지 설치합니다. 92 | gem install jazzy 93 | jazzy 94 | ``` 95 | 96 | ```swift 97 | // Jazzy output 98 | Running xcodebuild 99 | building site 100 | building search index 101 | downloading coverage badge 102 | jam out to your fresh new docs in `docs` 103 | ``` 104 | 105 | ## Comments 106 | 107 | 개발하다 보면 주석을 자주 보게 됩니다. 108 | 어렴풋이 주석은 코드의 가독성을 떨어뜨린다고 들었을 것입니다. 109 | 110 | 하지만 개발에 주석은 어쩔 수 없이 쓰이는 존재입니다. 111 | 112 | 주석이 코드 가독성을 떨어뜨릴 수 있지만, 반대로 가독성을 높일 수도 있습니다. 113 | 코드 가독성을 떨어뜨리는 주석과 가독성을 높이는 주석의 차이를 살펴봅시다. 114 | 115 | **Explain the "why"** 116 | 117 | "무엇"을 하는 변수인지 설명하는 주석은 코드의 가독성을 떨어뜨리고, 변수가 "왜" 여기 있는지를 설명하는 주석은 코드의 가독성을 높입니다. 118 | 119 | 변수가 "무엇"을 하는지 설명하는 가독성을 떨어뜨리는 주석을 먼저 살펴봅시다. 120 | 121 | ```swift 122 | struct Message { 123 | // The id of the message 124 | let id: String 125 | 126 | // The date of the message 127 | let date: Date 128 | 129 | // The contents of the message 130 | let contents: String 131 | } 132 | ``` 133 | 134 | 변수가 무엇을 하는지 설명하는 주석은 필요하지 않습니다. 135 | 이미 변수명으로 해당 변수가 어떤 역할을 하는지 충분히 설명하고 있기 때문입니다. 136 | 137 | 아래 코드와 같이 변수가 "왜" 여기 위치하는지 설명하는 주석으로 코드의 가독성을 높입시다. 138 | 139 | ```swift 140 | struct Message { 141 | let id: String 142 | let date: Date 143 | let contents: String 144 | 145 | // Messages can get silently cut off by the server at 280 characters. 146 | let maxLength = 280 147 | } 148 | ``` 149 | 150 | Message 구조체가 maxLength 변수를 가지는 이유를 주석으로 설명하고 있습니다. 151 | 152 | 이와 같은 왜 변수가 해당 위치에 있는지에 대한 설명은 변수명으로 부족할 수 있기 때문에 주석이 부족한 설명을 보충하게 됩니다. 153 | 154 | Not all "Why" need to be explained. 155 | 156 | 아무리 유용한 주석이더라도 시간이 지나면 쓸모없어지거나 오히려 코드에 피해만 끼칩니다. 157 | 따라서 무분별한 주석 작성은 피해야 합니다. 158 | 159 | 특히 적절하지 않은 변수명이나 함수명을 보충 설명하기 위한 주석은 최악입니다. 160 | 161 | **The code has the truth.** 162 | 163 | ## Settling on a style 164 | 165 | 팀원들과 개발하다 보면 서로 다른 코딩 스타일로 일관되지 못한 결과물이 만들어집니다. 166 | 어떤 개발자는 for loop을 선호하고 또 다른 개발자는 forEach를 선호할 수 있습니다. 167 | 168 | 이때 SwiftLint를 사용하면 일관되는 코딩 스타일을 가진 결과물을 만들 수 있습니다. 169 | 170 | SwiftLint는 아래와 같은 .yml 파일로 관리됩니다. 171 | 172 | ```swift 173 | disabled_rules: # rule identifiers to exclude from running 174 | - variable_name 175 | - nesting 176 | - function_parameter_count 177 | opt_in_rules: # some rules are only opt-in 178 | - control_statement 179 | - empty_count 180 | - trailing_newline 181 | - colon 182 | - comma 183 | included: # paths to include during linting. `--path` is ignored if present. 184 | - Project 185 | - ProjectTests 186 | - ProjectUITests 187 | excluded: # paths to ignore during linting. Takes precedence over `included`. 188 | - Pods 189 | - Project/R.generated.swift 190 | 191 | # configurable rules can be customized from this configuration file 192 | # binary rules can set their severity level 193 | force_cast: warning # implicitly. Give warning only for force casting 194 | 195 | force_try: 196 | severity: warning # explicitly. Give warning only for force try 197 | 198 | type_body_length: 199 | - 300 # warning 200 | - 400 # error 201 | 202 | # or they can set both explicitly 203 | file_length: 204 | warning: 500 205 | error: 800 206 | 207 | large_tuple: # warn user when using 3 values in tuple, give error if there are 4 208 | - 3 209 | - 4 210 | 211 | # naming rules can set warnings/errors for min_length and max_length 212 | # additionally they can set excluded names 213 | type_name: 214 | min_length: 4 # only warning 215 | 216 | error: 35 217 | excluded: iPhone # excluded via string 218 | reporter: "xcode" 219 | ``` 220 | 221 | .swiftlint.yml 파일을 수정하여 SwiftLint의 규칙을 수정할 수 있습니다. 222 | 223 | SwiftLint를 적용한 후 규칙을 따르지 않을 경우 아래와 같이 경고를 띄우거나 아예 에러를 띄울 수도 있습니다. 224 | 225 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/47699c60-61e6-444a-8920-33b8b84d13d4) 226 | 227 | 물론 :previous, :this 또는 :next 키워드로 특정 코드에서 SwiftLint의 규칙을 비활성화할 수 있습니다. 228 | 아래 코드로 살펴봅시다. 229 | 230 | ```swift 231 | // Turning off violating rules 232 | if list.count == 0 { // swiftlint:disable:this empty_count 233 | // swiftlint:disable:next force_unwrapping 234 | print(lastLogin!) 235 | } 236 | ``` 237 | 238 | ## Kill the managers 239 | 240 | 개발하다 보면 Manager 클래스를 빈번히 볼 수 있습니다. 241 | BluetoothManger나 ApiRequestManger와 같이 Manager가 접미사로 붙는 객체는 너무 많을 책임을 지게 됩니다. 242 | 하나의 클래스가 여러 책임을 질 경우 코드 수정이 어려워지고 가독성도 떨어집니다. 243 | 244 | 여러 책임을 지는 클래스를 작은 타입으로 나누고 앱 다른 부분에서도 쓰일 만한 타입은 제네릭 타입으로 만들어 봅시다. 245 | 246 | 여러 책임을 진 ApiRequestManager 클래스를 예시로 살펴봅시다. 247 | 248 | ApiRequestManager 클래스는 네트워크 호출, 데이터 캐싱, 큐에 저장, 웹 소켓과 관련된 모든 책임을 갖는 Manager 클래스입니다. ApiRequestManager 클래스가 가진 책임을 아래와 같이 명시하여 얼마나 많은 책임을 갖는지 살펴봅시다. 249 | 250 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/81346efe-dad3-45f1-bc02-6e0967e4c92c) 251 | 252 | 여러 책임을 나누기 위해서 먼저 여러 책임을 각각 독립된 타입으로 만들어야 합니다. 253 | 각 역할을 하는 타입들을 독립시켜, 특정 부분에 문제가 생겼을 때 특정 독립된 타입만 수정할 수 있습니다. 254 | 255 | 예를 들어 Cache에 문제가 발생했을 때 ApiRequestManager 클래스 전체가 아닌 ResponseCache 속 코드만 살펴보면 됩니다. 256 | 257 | 또한 ApiRequestManager 클래스를 Network 클래스로 더 정밀한 이름을 사용할 수 있습니다. 258 | 259 | **Paving the road for generics** 260 | 261 | 만약 queuing과 caching 동작이 Network 클래스 이외에 앱 다른 부분에도 사용된다면, ResponseQueue와 ResponseCache를 제네릭 타입으로 만들어 사용할 수 있습니다. 262 | 263 | 아래와 같이 ResponseQueue를 Queue로 수정하고 제네릭 T 타입에 Response 타입을 넣는 방식으로 제네릭 Queue를 만들게 됩니다. 264 | 265 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/98a8a3bb-a82d-44a8-9423-afeaf1a724b7) 266 | 267 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/40266d8f-644b-40c1-88db-9868cf7f0a88) 268 | 269 | ## Naming abstractions 270 | 271 | **Good names don't change** 272 | 273 | 변수명을 짓는 일이 중요하다는 것은 모두 알고 있습니다. 274 | 275 | 하지만 보통 별 고민 없이 변수명을 짓는 경우 아래와 같이 너무 특수화된(overspecified) 상황만 어울리는 변수명을 짓게 됩니다. 276 | 277 | 너무 특수화된 변수명은 추가적인 기능을 구현할 때 해당 기능과 어울리지 않을 수 있습니다. 278 | 279 | 아래 코드는 방문했던 커피 가게 중 가장 마음에 들었던 커피 가게 다섯 개를 뽑는 요구사항을 구현한 코드입니다. 280 | 281 | ```swift 282 | let locations = ... // extracted locations. 283 | let favoritePlaces = FavoritePlaces(locations: locations) 284 | let topFiveFavoritePlaces = favoritePlaces.calculateMostCommonPlaces() 285 | 286 | let coffeePlaces = topFiveFavoritePlaces.filter { place in place.type == "Coffee" }**** 287 | ``` 288 | 289 | 가장 마음에 들었던 커피 가게를 뽑는 요구사항에는 favoritePlaces 변수명이 적합할지 몰라도 favoritePlaces는 너무 특정 상황에 국한된 변수명입니다. 290 | 291 | 만약 사용자가 방문한 커피 가게 중 가장 적게 방문한 커피 가게를 뽑는 요구사항에는 favoritePlaces 변수명이 적합하지 않습니다. 292 | 293 | 이처럼 너무 특수화된 변수명은 새로운 요구사항에 어울리지 않을 수 있습니다. 294 | 295 | The type is named after how it is used, which is to find the favorite places. But the type's name would be better if you can name it after what it does, which is find and group occurrences of places. 296 | 297 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/379dd4d1-92d7-4ef7-8ccc-798703481a14) 298 | 299 | favoritePlaces와 달리 LocationGroper 변수명은 요구사항이 추가되어도 잘 어울려 변수명을 수정하지 않아도 됩니다. 300 | 301 | **결과적으로 좋은 변수명은 수정할 필요 없는 변수명입니다.** 302 | 303 | 몇 가지 예를 더 살펴봅시다. 304 | 305 | 1. Don't use something like 'redColor' as a button's property for a warning state; a 'warning' property might be better because the warning's design might change, but a warning's purpose won't. 306 | 2. When creating a 'UserheaderView' - which is nothing more than an image and label you can reuse as something else - perhaps 'ImageDescriptionView' would be more fitting as well as reusable. 307 | 308 | ## Summary 309 | - Quick Help documentation is fruitful way to add small snippets of documentation to your codebase. 310 | - Quick Help documentation is especially valuable to public and internal elements offered inside a project and framework. 311 | - Quick Help supports many useful callouts that enrich documentation. 312 | - You can use Jazzy to generate Quick Help documentation. 313 | - Comments explain the "why", not the "what". 314 | - Be stingy with comments. 315 | - Comments are no bandage for bad naming. 316 | - There's no need to let commented-out code, aka Zombie Code, linger around. 317 | - Code consistency is more important than code style. 318 | - Consistency can be enforced by installing SwiftLint. 319 | - SwiftLint supports configurations that you can let your team decide, which helps eliminate style discussions and disagreements. 320 | - Manager classes can drop the -Manager suffix and still convey the same meaning. 321 | - A large type can be composed of smaller types with focused responsibilities. 322 | - Smaller components are good candidates to make generic. 323 | - Name your types after what they do, not how they are used. 324 | - The more abstract a type is, the more easily you can make it generic and reusable. 325 | -------------------------------------------------------------------------------- /Demystifying-initializers.md: -------------------------------------------------------------------------------- 1 | # Demystifying initializers 2 | 3 | ## This chpater overs 4 | - Demystifying Swift's initializer rules 5 | - Understanding quirks of struct initializers 6 | - Understanding complex initializer rules when subclassing 7 | - How to keep the number of initializers low when subclassing 8 | - When and how to work with required initializers 9 | 10 | ## Struct initializer rules 11 | 구조체는 subclassing을 지원하지 않기 때문에 상대적으로 정직한 초기화 규칙을 가집니다. 12 | 그럼에도 특별한 규칙이 몇 가지 있습니다. 13 | 14 | 기본적으로 스위프트에서는 구조체와 클래스의 모든 프로퍼티가 초기화 시키는 것을 원칙으로 여깁니다. 15 | 따라서 구조체와 클래스의 프로퍼티가 초기화되지 않았다면 컴파일 단계에서 에러가 발생합니다. 16 | 17 | 구조체는 아래 코드와 같이 생성자를 따로 구현하지 않았다면 모든 프로퍼티를 초기화하는 memberwise init(default init)을 제공합니다. 18 | default init은 코드에는 보이진 않지만 객체 생성 시 사용할 수 있습니다. 19 | 20 | ```swift 21 | enum Pawn { 22 | case dog, cat, ketchupBottle, iron, shoe, hat 23 | } 24 | 25 | struct Player { 26 | let name: String 27 | let pawn: Pawn 28 | } 29 | 30 | let player = Player(name: "June", pawn = .shoe) 31 | ``` 32 | 33 | 만약 구조체에 custom init을 만들었다면 memberwise init(모든 프로퍼티를 초기화하는 default init)은 호출되지 않습니다. 34 | custom init을 만들었다고 memeberwise init을 왜 굳이 지원하지 않을까? 라는 생각을 할 수 있습니다. 35 | 하지만 이는 memeberwise init으로 custom init의 초기화 동작을 우회하는 행위를 방지하기 위함입니다. 이는 안정성 측면에서 훌륭한 방식입니다. 36 | 37 | 물론 memeberwise init을 제공 받는 동시에 custom init을 구현하는 방법도 존재합니다. 38 | extension을 사용해 custom init을 extension에 구현한다면 custom init과 함께 memeberwise init을 제공 받을 수 있습니다. 39 | 본인이 놓인 상황에 어울리는 방법을 선택해 사용합시다. 40 | 41 | 아래 코드는 extension을 통해 memeberwise init을 제공 받으며 custom init을 구현한 코드입니다. 42 | 43 | ```swift 44 | struct Player { 45 | let name: String 46 | let pawn: Pawn 47 | } 48 | 49 | extension Player { 50 | init(name: String) { 51 | self.name = name 52 | self.pawn = Pawn.allCases.randomElement()! 53 | } 54 | } 55 | 56 | let player = Player(name: "June", pawn = .shoe) // memeberwise init 57 | let anotherPlayer = Player(name: "Hong") // custom init 58 | ``` 59 | 60 | 위 코드를 읽으면 한 가지 궁금증이 생깁니다. 61 | 62 | 분명 위에서 Pawn은 열거형인데 어떻게 열거형에 allCases.randomElement()가 가능할까요! 63 | enum Pawn이 CaseIterable protocol을 채택하여 가능한 일입니다. 64 | 65 | CaseIterable protocol은 associated values가 없는 enum에서 사용 가능합니다. 66 | CaseIterable protocol은 열거형의 case들을 배열로 사용하도록 만들어 줍니다. 67 | 열거형에 포함된 모든 case를 allCases 타입 프로퍼티를 통해 컬렉션을 생성합니다. 68 | enum case의 컬렉션으로 만들어진 allCases 프로퍼티는 컬렉션이기 때문에 .randomElement() 함수도 사용 가능하게 됩니다. 69 | 70 | 단, associated values(연관 값)를 가진 enum의 경우 CaseIterable protocol을 채택하여 allCases를 생성할 수 없습니다. 71 | 연관 값을 가진 enum은 무한한 변화가 가능하기 때문입니다. 72 | 73 | 아래 링크으로 더 자세히 확인합시다. 74 | 75 | [Apple Developer Documentation](https://developer.apple.com/documentation/swift/caseiterable) 76 | 77 | 아래 코드로 열거형에 적용된 CaseIterable 프로토콜을 확인해 봅시다. 78 | 79 | ```swift 80 | enum Pawn: CaseIterable { 81 | case dog, cat, ketchupBottle, iron, shoe, hat 82 | } 83 | 84 | let allCases: [Pawn] = Pawn.allCases 85 | print(allCases) // [Pawn.dog, Pawn.cat, Pawn.ketchupBottle, Pawn.iron, Pawn.shoe, Pawn.hat] 86 | ``` 87 | 88 | ## Initializers and subclassing 89 | Subclassing isn't too popular in the Swift community, especially because Swift often is marketed as a protocol-oriented language, which is one subclassing alternative. 90 | Nevertheless, subclassing is still a valid tool that Swift offers. 91 | 92 | 지금부터 init이 class와 subclassing에 어떻게 동작하는지 살펴봅시다. 93 | 94 | 먼저 초기자에는 Designated init과 convenience init 두 가지가 있습니다. 95 | 그리고 초기화는 본인의 모든 프로퍼티를 초기화함과 상속받은 부모 클래스의 프로퍼티들을 커스터마이징 할 목적을 가집니다. 96 | 97 | Designated init(지정 초기자)은 모든 프로퍼티를 초기화해야 하며 클래스 타입에는 반드시 한 개 이상의 Designated init이 필요합니다. 98 | 부모 클래스가 있을 경우 자식 클래스는 부모 클래스의 Designated init을 상속받습니다. 99 | 100 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/3d2b333a-f23e-4d94-b264-e71e9674adb1) 101 | 102 | convenience init(편의 초기자)은 초기자 내부에서 마지막에는 반드시 Designated init를 호출해야 합니다. (self.init()) 103 | convenience init를 통해 default value를 제공하거나 단순화한 init을 제공할 수 있고 다른 convenience init를 호출할 수 있습니다. 104 | 105 | 아래 코드는 Designated init과 convenience init을 사용한 예시 코드입니다. 106 | 107 | ```swift 108 | class BoardGame { 109 | let players: [Player] 110 | let numberOfTiles: Int 111 | 112 | // Designated init 113 | init(players: [Player], numberOfTiles: Int) { 114 | self.players = players 115 | self.numberOfTiles = numberOfTiles 116 | } 117 | 118 | covenience init(players: [Player]) { 119 | self.init(players: players, numberOfTiles: 32) // 여기서 default value를 제공합니다. 120 | } 121 | 122 | convenience init(names: [String]) { 123 | var players = [Player]() 124 | for name in names { 125 | players.append(Player(name: name)) 126 | } 127 | self.init(players: players, numberOfTiles: 32) // 마지막에는 반드시 Designated init을 호출하고 있습니다. 128 | } 129 | } 130 | 131 | let boardGame = BoardGame(names: ["hong", "kim", "kang"]) 132 | 133 | let players = [ 134 | Player(name: "hong"), 135 | Player(name: "kim"), 136 | Player(name: "kang") 137 | ] 138 | let boardGame = BoardGame(players: players) 139 | 140 | let boardGame = BoardGame(players: players, numberOfTiles: 32) 141 | ``` 142 | 143 | 클래스는 구조체와 달리 memberwise init(default init)을 제공하지 않습니다. 144 | 145 | 지금부터 클래스를 subclassing 할 때를 살펴봅시다. 146 | 147 | 기본적으로 자식 클래스는 부모 클래스의 모든 init(Designated init & covenience init)을 상속받습니다. 148 | 따라서 자식 클래스도 부모 클래스와 동일한 방식으로 초기화 가능합니다. 149 | 150 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/a951fed1-8a32-4b25-a941-d5106ea30779) 151 | 152 | 하지만 자식 클래스에서 부모 클래스에 없는 새로운 프로퍼티를 추가했을 때는 다릅니다. 153 | 154 | 자식 클래스에 새로운 프로퍼티가 추가되면 자식 클래스에 있던 부모 클래스의 모든 init은 사라집니다. 155 | 위에서 말했듯이 클래스의 모든 프로퍼티는 초기화되어야 합니다. 하지만 부모 클래스에 없는 프로퍼티가 자식 클래스에 추가되며 더 이상 부모 클래스 init으로 자식 클래스의 프로퍼티를 초기화할 수 없어지게 됩니다. 156 | 157 | 이때 우린 자식 클래스의 모든 프로퍼티를 초기화하는 새로운 designated init을 구현해야 합니다. 158 | 159 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/cfa77de4-1a32-46b9-ba0e-26a801ba32c3) 160 | 161 | 자식 클래스에 초기화 되어야 하는 새로운 프로퍼티가 추가되면 자식 클래스에 새로운 designated init을 구현하고 자식 클래스의 designated init에서 부모 클래스의 designated init을 호출하는 방식으로 162 | 프로퍼티를 초기화해야 합니다. 163 | 164 | 아래 코드로 확인해 봅시다. 165 | 166 | ```swift 167 | class MutabilityLand: BoardGame { 168 | var scoreBoard = [String: Int]() 169 | var winner: Player? 170 | 171 | // 자식 클래스에 추가된 초기화 되어야 하는 새로운 프로퍼티 172 | let instructions: String 173 | 174 | // 자식 클래스의 designated init 175 | init(players: [Player], instructions: String, numberOfTiles: Int) { 176 | self.instructions = instructions 177 | super.init(players: players, numberOfTiles: numberOfTiles) 178 | } 179 | } 180 | ``` 181 | 182 | 위 코드에서 scoreBoard와 winner는 초기화될 필요 없는 프로퍼티입니다. (winner는 옵셔널 프로퍼티로 초기화 의무가 없습니다.) 183 | 따라서 자식 클래스에 scoreBoard와 winner 프로퍼티만 추가 되었다면 부모 클래스의 모든 init을 상속받을 수 있습니다. 184 | 185 | 하지만 MutabilityLand 클래스의 instructions 프로퍼티는 초기화 되어야 하는 새로운 프로퍼티입니다. 186 | instructions가 추가되며 MutabilityLane 클래스는 부모 클래스인 BoardGame 클래스의 모든 init을 상속받지 못합니다. 187 | 188 | 따라서 위와 같이 자식 클래스에 새로운 designated init을 만들어 초기화 되어야 하는 새로운 프로퍼티를 초기화 이후 super.init을 통해 부모 클래스로 부터 상속받은 프로퍼티를 초기화해야 합니다. 189 | 여기서 주의해야할 점은 super.init은 부모 클래스의 designated init을 자식 클래스에서 호출할 뿐이지 부모 클래스의 init을 상속받은건 아닙니다. 190 | 191 | 하지만 자식 클래스에 초기화될 필요가 있는 새로운 프로퍼티가 생겨도 부모 클래스의 init을 상속받는 방법이 있습니다! 192 | 193 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/191ffa6d-109d-44a6-a47c-f3e58bcb763b) 194 | 195 | 자식 클래스에서 새로운 designated init을 만들지 않고, 부모 클래스의 designated init을 override하면 자식 클래스는 부모 클래스의 모든 init을 상속받을 수 있습니다. 196 | 물론 override한 부모 클래스의 designated init에서는 자식 클래스의 새로운 프로퍼티를 반드시 초기화해야 합니다. 197 | 198 | 아래 코드를 살펴봅시다. 199 | 200 | ```swift 201 | class Mutability: BoardGame { 202 | // 생략 203 | // 부모 클래스의 designated init을 자식 클래스에서 override 204 | override init(players: [Player], numberOfTiles: Int) { 205 | self.instructions = "Read the manual" // super.init 호출 전 새로운 프로퍼티 초기화합니다. 206 | super.init(players, numberOfTiles: numberOfTiles) // 필수입니다. 207 | } 208 | } 209 | ``` 210 | 211 | 부모 클래스의 designated init을 자식 클래스에서 override 할 때 주의할 점은 super.init으로 부모 클래스로부터 상속받는 프로퍼티를 초기화하기 전에 자식 클래스에 새로 추가된 프로퍼티를 먼저 초기화해야 합니다. 212 | 213 | 하지만 designated init을 override을 하면 클래스 계층이 깊어질 수 있습니다. 214 | 215 | In a class hierarchy, convenience init go horizontal, and designated init go vertical! 216 | 217 | ## Minimizing class initializers 218 | 219 | 위에서 보았듯이 subclassing과 함께 자식 클래스에 초기화가 필요한 프로퍼티가 추가되었을 때 부모 클래스의 init을 상속받기 위해 designated init을 상속했습니다. 220 | 하지만 이 동작이 반복될수록 자식 클래스의 designated init이 한 개씩 늘어나게 됩니다. 221 | 222 | 만약 부모 클래스의 designated init 한 개와 convenience init 두 개를 가질 때 자식 클래스에서 부모 클래스의 designated init을 override한다고 가정해봅시다. 223 | 결과적으로 자식 클래스가 갖게 되는 init은 부모 클래스에서 상속받는 convenience init 두 개와 override designated init 그리고 자식 클래스 본인만의 (반드시 가져야하는) designated init이 있습니다. 224 | 225 | 만약 subclassing 될 때마다 위의 동작이 반복되면 자식 클래스는 여러 designated init을 가지게 될것입니다. 226 | (override designated init도 designated init입니다.) 이는 계층을 복잡하게 만들 수 있습니다. 227 | 228 | 자식 클래스에 초기화가 필요한 프로퍼티가 추가되어도 부모 클래스의 init을 모두 상속받으며 designated init을 한 개로 유지하는 방법을 살펴봅시다. 229 | 230 | designated init을 override 할 때 convenience init으로 부모 클래스의 designated init을 override한다면 자식 클래스의 designated init을 한 개로 유지할 수 있습니다. 231 | 232 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/faef01e3-ddcc-4c7f-a69c-01b7f1ffcd60) 233 | 234 | 여기서 부모 클래스의 designated init을 자식 클래스의 convenience init으로 override 할 때 자식 클래스의 convenience override init에서 자식 클래스의 designated init을 호출하고 designated init에서 부모 클래스의 designated init을 235 | 호출하도록 구현하면 부모 클래스의 모든 init을 상속 받으며 하위 자식 클래스의 designated init을 한 개로 유지할 수 있습니다. 236 | 237 | 아래 코드로 확인해봅시다. 238 | 239 | ```swift 240 | class MutabilityLand: BoardGame { 241 | var scoreBoard = [String: Int]() 242 | var winner: Player? 243 | 244 | let instructions: String 245 | 246 | // 부모 designated init을 override하는 convenience override init 247 | convenience override init(players: [Player], numberOfTiles: Int) { 248 | // The initializer now points sideways (self.init) versus upwards (super.init) 249 | self.init(player: players, instructions: "Read the manual", numberOfTiles: numberOfTiles) 250 | } 251 | 252 | // 자식 클래스의 designated int 253 | init(players: [Player], instructions: String, numberOfTiles: Int) { 254 | self.instructions = instructions 255 | super.self(players: players, numberOfTiles: numberOfTiles) 256 | } 257 | } 258 | ``` 259 | 260 | 위의 코드처럼 override init(designated init)을 convenience override init으로 바꾸면 자식 클래스의 designated init을 두 개에서 한 개로 줄입니다. 261 | 262 | 이제는 MutabilityLand의 자식 클래스가 생기면 자식 클래스에서는 하나의 designated init만 override하면 MutabilityLand의 모든 init을 상속 받을 수 있습니다. 263 | 264 | 물론 designated init의 특성상 designated init에서 모든 프로퍼티가 초기화되어야 합니다. 265 | 따라서 자식 클래스에 추가된 초기화가 필요한 프로퍼티는 designated init에서 초기화하고 부모 클래스의 designated init을 호출합시다. 266 | 267 | 다시 말하지만 designated init은 모든 클래스에 한 개 이상 존재해야 하고 모든 프로퍼티를 초기화할 수 있어야 합니다. 268 | 269 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/8b31c897-857c-4e05-ab5d-bfc380e5c4d5) 270 | 271 | ```swift 272 | class MutabilityLandJunior: MutabilityLand { 273 | let soundsEnabled: Bool 274 | 275 | init(soundsEnabled: Bool, players: [Player], instructions: String, numberOfTiles: Int) { 276 | self.soundsEnabled = soundsEnabled 277 | super.init(players: players, instructions: instructions, numberOfTiles: numberOfTiles) 278 | } 279 | 280 | // 부모 클래스의 designated init을 convenience init으로 override 했습니다. 281 | convenience override init(players: [Player], instructions: String, numberOfTiles: Int) { 282 | self.init(soundsEnabled: false, player: players, instructions: "Read the manual", numberOfTiles: numberOfTiles) 283 | } 284 | } 285 | ``` 286 | 287 | Thanks to convenience override, this subclass gets many initializers for free. 288 | 289 | ## Required initializers 290 | 291 | Required initializers play a crucial role when subclassing classes. 292 | 293 | init 앞에 required를 붙여 자식 클래스가 해당 init을 필수로 구현하도록 만들 수 있습니다. 294 | required init을 사용하는 경우는 크게 두 가지입니다. 295 | 296 | 1. factory methods 297 | 2. protocol with have init 298 | 299 | 먼저 factory methods는 인스턴스를 사전에 구성하도록 하는 전략입니다. 300 | 따라서 아래 makeGame과 같이 Self를 리턴하는 함수를 구현하게 될 것입니다. 301 | 302 | ```swift 303 | class BoardGame { 304 | // ...생략 305 | 306 | class func makeGame(players: [Player]) -> Self { 307 | let boardGame = self.init(players: players, numberOfTiles: 32) 308 | return boardGame 309 | } 310 | } 311 | ``` 312 | 313 | Self를 리턴하는 함수는 말 그대로 본인의 타입을 리턴하는 함수입니다. 314 | 위의 makeGame 함수는 BoardGame 타입을 리턴하게 됩니다. 315 | BoardGame 인스턴스를 self.init을 통해 만들어 집니다. 316 | 317 | 하지만 위 코드에는 required error가 발생합니다. 318 | makeGame 함수가 self.init을 사용하기 때문에 BoardGame 클래스의 자식 클래스가 생겼을 경우 리턴 타입인 Self와 makeGame 함수에서 319 | 만드는 self.init의 타입이 일치하지 않을 수 있습니다. 320 | 321 | 따라서 이 경우 자식 클래스에서 init을 구현하도록 강제해야합니다. 322 | 자식 클래스에서 required init을 재정의할 때는 override 키워드 대신 required 키워드를 사용합니다. 323 | 324 | 아래 코드를 확인해 봅시다. 325 | 326 | ```swift 327 | class BoardGame { 328 | // ...생략 329 | 330 | class func makeGame(players: [Player]) -> Self { 331 | let boardGame = self.init(players: players, numberOfTiles: 32) 332 | return boardGame 333 | } 334 | 335 | required init(players: [Player], numberOfTiles: Int) { 336 | self.players = players 337 | self.numberOfTiles = numberOfTiles 338 | } 339 | } 340 | 341 | class Mutability: BoardGame { 342 | //... 생략 343 | 344 | // convenience init으로 부모 클래스의 required init을 재정의했습니다. 345 | covenience required init(players: [Player], numberOfTiles: Int) { 346 | // self.init에서 초기화 이후, 부모 클래스의 designated init 호출합니다. 347 | self.init(players: players, instructions: "Read the manual", numberOfTiles: numberOfTiles) 348 | } 349 | } 350 | ``` 351 | 352 | 두 번째로 required init이 필요한 경우는 프로토콜이 init을 가졌을 때입니다. 353 | 클래스가 해당 프로토콜을 채택하면 반드시 required init을 구현해야 합니다. 354 | 355 | 아래와 같이 프로토콜이 init을 가질 수 있습니다. 356 | 357 | ```swift 358 | protocol BoardGameType { 359 | init(players: [Player], numberOfTiles: Int) 360 | } 361 | 362 | class BoardGame: BoardGameType { 363 | // ...생략 364 | required init(players: [Player], numberOfTiles: Int) { 365 | self.players = players 366 | self.numberOfTiles = numberOfTiles 367 | } 368 | } 369 | ``` 370 | 371 | BoardGame이 BoardGameType 프로토콜을 따르기 때문에 BoardGame의 자식 클래스들도 BoardGameType을 따라야 합니다. 372 | 따라서 BoardGame의 init에 required 키워드를 붙여 자식 클래스에서 init의 구현을 강제합니다. 373 | 374 | 만약, 클래스에 final 키워드를 붙였다면 어떨까요? 375 | 376 | init을 가진 프로토콜을 채택한 클래스를 final로 만든다면, 자식 클래스로 subclassing 될 가능성이 없어졌기 때문에 init을 required 할 필요가 없어집니다. 377 | 378 | required 자체가 subclassing의 상황에 대응하기 위해 사용하는데 final로 subclassing 기능을 막는다면 init을 가진 프로토콜, factory method 경우 모두 init에 required 키워드를 붙일 필요가 없습니다. 379 | 380 | ## Summary 381 | - Structs and classes want all their non-optional properties initialized 382 | - Structs generate "free" memberwise initializers 383 | - Structs lose memberwise initializers if you add a custom initializer 384 | - If you extend structs with your custom initializers, you can have both memberwise and custom initializers 385 | - Classes must have one or more designated initializers 386 | - Convenience initializers point to designated initializers 387 | - If a subclass has its own stored prperties, it won't directly inherit its superclass initializers 388 | - If a subclass overrides designated initializers, it gets the convenience initializers from the superclass 389 | - When overriding a superclass initializer with a convenience initializer, a subclass keeps the number of designated initializers down 390 | - The required keyword makes sure that subclasses implement an initializer and that factory methods work on subclasses 391 | - Once a protocol has an initializer, the required keyword makes sure that subclasses conform to the protocol 392 | - By making a class final, initializers can drop the required keyword 393 | -------------------------------------------------------------------------------- /Effortless-error-handling.md: -------------------------------------------------------------------------------- 1 | # Effortless error handling 2 | 3 | ## This chapter covers 4 | - Error-handling best practices (and downsides) 5 | - Keeping your application in a proper state when throwing 6 | - How errors are propagated 7 | - Adding information for customer-facing applications (and for troubleshooting) 8 | - Bridging to NSError 9 | - Making APIs easier to use without harming the integrity of an application 10 | 11 | ## Errors in Swift 12 | 13 | 에러는 여러 종류로 나뉩니다. 14 | 크게 세 종류로 나눠보면 Programming errors, User errors, Errors revealed at runtime으로 나눌 수 있습니다. 15 | 16 | Programming errors는 배열의 잘못된 인덱스 접근, 오버플로, 0으로 나누었을 때 발생하는 에러로 코드 레벨에서 충분히 고칠 수 있는 에러입니다. 17 | User errors는 쉽게 말해 사용자가 서비스를 사용할 때 발생되는 에러입니다. 18 | Errors revealed at runtime은 네트워크 상태가 불안정하거나 만료된 인증서를 사용하는 등 런타임에서 발생되는 에러입니다. 19 | 20 | 에러를 던지고 다루는 부분도 중요하지만 에러를 던질 때 애플리케이션을 예측 가능한 상태로 유지하는 것도 굉장히 중요합니다. 21 | 지금부터 스위프트에서 에러를 처리하는 과정과 에러를 던질 때 애플리케이션을 예측 가능한 상태로 유지하는 방법을 살펴봅시다. 22 | 23 | 스위프트는 에러를 처리할 때 Error 프로토콜을 제안합니다. Error 프로토콜은 필수로 구현해야 할 요구사항이 없습니다. 24 | 25 | 열거형은 각 case 별로 독립적이기 때문에 열거형은 에러 타입을 만들기에 적합합니다. 26 | 하지만 모든 에러를 열거형으로 만들 필요는 없습니다. 27 | 자주는 아니지만, 구조체로도 에러를 만들 수 있습니다. 구조체는 error에 더 많은 정보를 추가할 때 어울립니다. 28 | 29 | 아래 코드는 열거형으로 에러를 만든 예입니다. 30 | 31 | ```swift 32 | enum ParesLocationError: Error { 33 | case invalidData 34 | case locationDoesNotExist 35 | case middleOfTheOcean 36 | } 37 | ``` 38 | 39 | 에러에 더 많은 정보를 추가해야 할 때 구조체를 사용합니다. 40 | 41 | ```swift 42 | struct MultipleParseLocationErrors: Error { 43 | let parsingErrors: [ParseLocationError] 44 | let isShownToUser: Bool 45 | } 46 | ``` 47 | 48 | 에러는 던져지고 처리되기 위해 존재합니다. 49 | throws 키워드를 함수에 붙여 해당 함수가 에러를 던질 수 있다는 사실을 표현합니다. 50 | 51 | 아래 코드는 함수에 throws 키워드를 붙인 예입니다. 52 | 53 | ```swift 54 | struct Location { 55 | let latitude: Double 56 | let longitue: Double 57 | } 58 | 59 | func parseLocation(_ latitude: String, _ longitude: String) throws -> Location { 60 | guard let latitude = Double(latitude), let longitude = Double(longitude) else { 61 | throw ParseLocationError.invalidData // 에러를 던지고 62 | } 63 | return Location(latitude: latitude, longitude: longitude) 64 | } 65 | 66 | do { 67 | try parseLocation("I am not a double", "4.899431") 68 | } catch { // 에러를 받아 처리합니다. 69 | print(error) // invalidData 70 | } 71 | ``` 72 | 73 | 하지만 스위프트에서는 함수가 던질 에러의 정보를 드러내지 않습니다. 74 | 위 코드에서 parseLocation의 함수 정의문을 봤을 때 throws 키워드로 함수가 에러를 던질 수 있다는 사실은 알지만 어떤 종류의 에러를 던질 수 있을지 알 수 없습니다. 75 | 함수 내부를 보아야 ParseLocationError.invalidData 에러를 던진다는걸 알 수 있습니다. 76 | 77 | 따라서 가능하다면 던질 에러에 대한 정보를 제공하는걸 추천합니다. 78 | 79 | "Quick Help"를 사용해 함수가 어떤 에러를 던질지 정보를 제공할 수 있습니다. 80 | 에러를 던지는 함수에 커서를 올리고 cmd-Alt-/를 누르면 Quick Help templete을 만들 수 있습니다. 81 | 아래 코드처럼 Quick Help로 함수가 던지는 에러의 정보를 함수 구현부를 보지 않고도 알 수 있습니다. 82 | 83 | ```swift 84 | /// Turns two strings with a latitude and longitude value into a Location type 85 | /// 86 | /// - Parameters: 87 | /// - latitude: A string containing a latitude value 88 | /// - longitude: A string containing a longitude value 89 | /// - Returns: A Location struct 90 | /// - Throws: Will throw a ParseLocationError.invalidData if lat and long can't be converted to Double 91 | func parseLocation(_ latitude: String, _ longitude: String) throws -> Location { 92 | guard let latitude = Double(latitude), let longitude = Double(longitude) else { 93 | throw ParseLocationError.invalidData // 에러를 던지고 94 | } 95 | return Location(latitude: latitude, longitude: longitude) 96 | } 97 | ``` 98 | 99 | 위에서 이야기 했듯이 에러를 처리하는것 만큼 에러가 발생한 상황에서 애플리케이션 상태를 예측 가능한 상태로 유지하는것도 중요합니다. 100 | 101 | 에러가 발생해도 애플리케이션 상태는 기존과 동일하게 유지되어야 합니다. 변경되어서는 안됩니다. 102 | 다시 강조하지만 함수에서 에러를 던진다면 해당 애플리케이션의 상태(환경 & 인스턴스 상태)는 유지되어야 합니다. 103 | 104 | 에러가 발생한 상황에서 애플리케이션 상태를 유지하는 세 가지 방법을 살펴보겠습니다. 105 | 106 | "Make func immutable". 첫번째 방법은 함수가 외부의 상태를 조작하지 못하도록 만드는 것입니다. 107 | 108 | 함수의 인자로 들어온 값만 함수가 조작해 리턴한다면 외부의 상태를 조작하지 않는 함수입니다. 109 | 외부의 값을 변경하지 않으면 에러를 던지더라도 애플리케이션 상태을 유지할 수 있습니다. 110 | 111 | "use temporary value". 두번째 방법은 작업이 에러 없이 끝났다면! 작업의 결과(새로운 상태)를 저장하는 것입니다. 112 | 113 | 작업이 에러 없이 끝나기 전까지 작업의 결과는 temporary value(임시 변수)에 저장하고 작업이 에러 없이 끝난 이후 임시 변수의 결과를 실제 변수에 저장하는 방법입니다. 114 | 아래 코드는 temporary value를 사용하지 않은 코드와 사용한 코드를 모두 살펴 봅시다. 115 | 116 | ```swift 117 | // 임시 변수를 사용하지 않은 코드 - 에러가 발생됐을 때 애플리케이션 상태를 예측 가능한 상태로 유지하지 못합니다. 118 | enum ListError: Error { 119 | case invalidValue 120 | } 121 | 122 | struct TodoList { 123 | private var values = [String]() 124 | 125 | mutating func append(strings: [String]) throws { 126 | for string in strings { 127 | let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines) 128 | 129 | if trimmedString.isEmpty { 130 | throw ListError.invalidValue 131 | } else { 132 | values.append(trimmedString) 133 | } 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | 위 코드는 for 루프 중에 에러가 발생하지 않는다면 문제가 없지만, for 루프 중 에러가 발생해 함수가 종료될 경우 애플리케이션 상태는 에러 발생 이전의 상태와 달라집니다. 140 | 만약 세 번째 for 루프에서 에러가 발생하면 첫 번째와 두 번째의 trimmedString이 TodoList의 values에 추가되며 애플리케이션 상태를 유지하지 못합니다. 141 | 이는 에러가 발생한 상황에서 애플리케이션을 예측 불가능한 상태로 만든것 입니다. 142 | 143 | 아래 코드처럼 임시 변수를 만들어 에러 상황에서 애플리케이션 상태를 예측 가능한 상태로 유지합시다. 144 | 145 | ```swift 146 | // 임시 변수를 사용한 코드 - 에러가 발생됐을 때 애플리케이션 상태를 예측 가능한 상태로 유지합니다. 147 | enum ListError: Error { 148 | case invalidValue 149 | } 150 | 151 | struct TodoList { 152 | private var values = [String]() 153 | 154 | mutating func append(strings: [String]) throws { 155 | var tempValues = [String]() // 임시 변수 156 | for string in strings { 157 | let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines) 158 | 159 | if trimmedString.isEmpty { 160 | throw ListError.invalidValue 161 | } else { 162 | tempValues.append(trimmedString) 163 | } 164 | } 165 | values.append(tempValues) // 동작이 모두 에러 없이 끝난 경우, 임시 변수에 저장된 결과를 실제 변수에 저장합니다. 166 | } 167 | } 168 | ``` 169 | 170 | 임시 변수인 tempValues를 선언해 모든 for 루프에서 에러를 던지지 않을 때 작업의 결과를 실제 변수인 values에 저장하여 에러가 발생했을 때 애플리케이션 상태를 유지하게 됩니다. 171 | for 루프 중간에 에러가 발생하면 임시 변수를 실제 값에 대입하지 않으며 애플리케이션 상태를 예측 가능한 상태로 유지합니다. 172 | 173 | "Recovery code with defer". 마지막 방법은 defer 클로저를 사용하는 방법입니다. 174 | 175 | 에러가 발생했을 때 에러 발생 이전의 변경사항을 되돌려 애플리케이션 상태를 예측 가능한 상태로 유지하는 방식입니다. defer 클로저는 함수가 끝나면 실행됩니다. 176 | 함수에서 에러가 발생되었는지 유무와 관계없이 defer 클로저는 함수 끝에 항상 실행됩니다. 177 | defer 클로저에서 에러 발생 여부를 판단하고 에러가 발생했다면 에러 발생 이전의 상태로 되돌려 애플리케이션 상태를 유지합니다. 178 | 179 | 아래 코드에서는 storedUrls 배열 요소의 개수를 함수 입력으로 들어온 data 개수와 비교하여 에러 발생 여부를 판단하고 개수가 일치하지 않다면 함수 동작 중 에러가 발생한 상황으로 인식해 저장한 모든 url을 다시 삭제합니다. 180 | 이로써 에러가 발생하더라도 애플리케이션 상태를 예측 가능하도록 유지합니다. 181 | 182 | ```swift 183 | import Foundation 184 | 185 | func writeToFiles(data: [URL: String]) throws { 186 | var storedUrls = [URL]() 187 | defer { 188 | if storedUrls.count != data.count { 189 | for url in storedUrls { 190 | try! FileManager.default.removeItem(at: url) 191 | } 192 | } 193 | } 194 | 195 | for (url, contents) in data { 196 | try contents.write(to: url, atomically: true, encoding: String.Encoding.utf8) 197 | storedUrls.append(url) 198 | } 199 | } 200 | ``` 201 | 202 | defer 클로저를 사용하면 에러가 발생하기 이전의 상태를 정확하게 유지하기 유리합니다. 203 | 하지만 여러 상황이 복잡히 섞여있다면 defer 클로저가 대응해야 할 상황이 많아져 오히려 복잡성을 높일 수 있습니다. 204 | 205 | ## Error propagation and catching 206 | 207 | "My favorite way of dealing with problems is to give them to somebody else." 208 | 209 | 에러는 전달됩니다. 보통은 저차원 함수에서 고차원 함수로 에러를 전달합니다. 210 | 함수 호출은 고차원 함수에서 저차원 함수로 내려가고 저차원 함수에서 발생한 에러는 함수 호출을 거슬러 고차원 함수로 전달됩니다. 211 | 저차원 함수들은 에러를 처리할 방법을 모르고 발생하는 에러를 고차원 함수로 던질 뿐입니다. 고차원 함수에서 에러를 처리합니다. 212 | 213 | 아래 코드로 확인해봅시다. 214 | 215 | ```swift 216 | struct Recipe { 217 | let ingredients: [String] 218 | let steps: [String] 219 | } 220 | 221 | enum ParseRecipeError: Error { 222 | case parseError 223 | case noRecipeDetected 224 | case noIngredientsDetected 225 | } 226 | 227 | struct RecipeExtractor { 228 | let html: String 229 | 230 | // 고차원 함수인 extractRecipe에서 저차원 함수들이 던진 에러를 처리합니다. 231 | func extractRecipe() -> Recipe? { 232 | do { 233 | return try parseWebpage(html) 234 | } catch { 235 | print("Could not parse recipe") 236 | return nil 237 | } 238 | } 239 | 240 | private func parseWebpage(_ html: String) throws -> Recipe { 241 | let ingredients = try parseIngredients(html) 242 | let steps = try parseSteps(html) 243 | return Recipe(ingredients: ingredients, steps: steps) 244 | } 245 | 246 | private func parseIngrediants(_ html: String) throws -> [String] { 247 | // ... Parsing happens here 248 | 249 | // .. Unless an error is thrown 250 | throw ParseRecipeError.noIngredientsDetected 251 | } 252 | 253 | prviate func parseSteps(_ html: String) throws -> [String] { 254 | // ... Parsing happens here 255 | 256 | // .. Unless an error is thrown 257 | throw ParseRecipeError.noRecipeDetected 258 | } 259 | } 260 | ``` 261 | 262 | 위 코드로 함수 호출의 흐름과 에러 전달 흐름을 확인할 수 있습니다. 263 | 264 | 하지만 위와 같이 에러를 고차원 계층으로 전달하면 발생하는 단점이있습니다. 265 | 에러에 대한 정보는 에러가 실제로 발생한 저차원 계층에서 더 자세히 알 수 있습니다. 266 | 하지만 에러를 고차원 계층으로 전달하면 에러에 대한 자세한 정보 없이 에러가 발생했다는 사실만 전달됩니다. 에러에 대한 유용한 정보를 잃는 문제가 있습니다. 267 | 268 | 따라서 에러를 고차원 계층으로 전달할 때 에러와 함께 유용한 정보를 함께 전달해야 합니다. 269 | 에러에 대한 유용한 정보는 에러를 핸들링하는 고차원 계층에 유용합니다. 270 | 271 | 아래 ParseRecipeError의 parseError 케이스처럼 열거형의 케이스에 튜플을 추가하는 방식으로 에러에 대한 정보를 전달할 수 있습니다. 272 | 273 | ```swift 274 | enum ParseRecipeError: Error { 275 | case parseError(line: Int, symbol: String) 276 | case noRecipeDetected 277 | case noIngredientsDetected 278 | } 279 | 280 | struct RecipeExtractor { 281 | let html: String 282 | 283 | func extractRecipe() -> Recipe? { 284 | do { 285 | return try parseWebpage(html) 286 | } catch let ParseRecipeError.parseError(line, symbol) { 287 | print("Parsing failed at line: \(line) and symbol: \(symbol)") 288 | return nil 289 | } catch { 290 | print("Could not parse recipe") 291 | return nil 292 | } 293 | } 294 | 295 | // ...snip 296 | } 297 | ``` 298 | 299 | 위와 같이 에러에 정보를 추가할 때 열거형에 튜플을 추가하여 구현할 수 있지만, LocalizedError 프로토콜을 사용하여 더 명확한 에러에 대한 정보를 전달할 수 있습니다. 300 | LocalizedError 프로토콜은 에러의 정보를 보충하는 역할을 합니다. 301 | 302 | LocalizedError 프로토콜은 네 가지 프로퍼티를 지원하며 에러에 대한 정보를 보충하도록 도와줍니다. 303 | 네 가지 프로퍼티는 아래와 같습니다. 네 가지 프로퍼티는 항상 구현할 필요는 없습니다. 사용할 프로퍼티만 선택하여 구현하면 됩니다. 304 | 305 | - var errorDescription: String?, 에러 정보를 추가합니다. 306 | - var failureReason: String?, 에러 발생 이유를 설명합니다. 307 | - var helpAnchor: String?, apple's help viewer 링크로 연결합니다. 308 | - var recoverySuggestion: String?, 에러에 대처하는 방법을 설명합니다. 309 | 310 | 보통은 LocalizedError 프로토콜의 errorDescription, recoverySuggestion 프로퍼티 정도로 충분히 에러에 대한 정보를 전달할 수 있습니다. 311 | 아래 코드는 LocalizedError 프로토콜을 채택하여 에러에 정보를 추가한 코드입니다. 312 | 313 | ```swift 314 | extension ParseRecipeError: LocalizedError { 315 | var errorDescription: String? { 316 | switch self { 317 | case .parseError: 318 | return NSLocalizedString("The HTML file had unexpected symbols.", comment: "Parsing error reason unexpected symbols") 319 | case .noIngredientsDetected: 320 | return NSLocalizedString("No ingredients were detected.", comment: "Parsing error no ingredients") 321 | case .noRecipeDetected: 322 | return NSLocalizedString("No recipe was detected.", comment: "Parsing error no recipe") 323 | } 324 | } 325 | 326 | var failureReason: String? { 327 | switch self { 328 | case let .parseError(line: line, symbol: symbol): 329 | return String(format: NSLocalizedString("Parsing data failed at line: %i and symbol: %@, comment: "Parsing error line symbol"), line, symbol) 330 | case .noIngredientsDetected: 331 | // ...snip 332 | case .noRecipeDetected: 333 | // ...snip 334 | } 335 | } 336 | 337 | var recoverySuggestion: String? { 338 | return "Please try a different type" 339 | } 340 | } 341 | ``` 342 | 343 | 에러에 human-readable(by LocalizedError)을 추가하여 안정적으로 에러를 전달할 수 있습니다. 344 | 345 | objective-c의 전통적인 에러 처리인 'NSError'를 사용하기 위해서는 CustomNSError 프로토콜을 채택해야 합니다. 346 | 347 | Swift.Error를 NSError로 변환할 때 우리는 CustomNSError 프로토콜을 채택하여 에러의 타입을 NSError로 변환합니다. 348 | Swift.Error를 NSError로 변환할 때 CustomNSError 프로토콜을 사용하지 않으면 NSError에 적합한 code와 domain 정보가 없을 수 있습니다. 349 | 350 | 아래 코드와 같이 CustomNSError 프로토콜을 채택하여 NSError가 필요한 경우 대응합시다. 351 | 352 | ```swift 353 | extension ParseRecipeError: CustomNSError { 354 | static var errorDomain: String { return "com.recipeextractor" } 355 | 356 | var errorCode: Int { return 300 } 357 | 358 | var errorUserInfo: [String: Any] { 359 | return [ 360 | NSLocalizedDescriptionKey: errorDescription ?? "", 361 | NSLocalizedFailureReasonErrorKey: failureReason ?? "", 362 | NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion ?? "" 363 | ] 364 | } 365 | } 366 | 367 | let nsError: NSError = ParseRecipeError.parseError(line: 3, symbol: "#") as NSError 368 | ``` 369 | 370 | 지금부터는 에러가 발생했을 때 이를 처리할 위치에 대해 살펴봅시다. 371 | 에러를 처리하는 위치는 어디가 바람직할까요? 372 | 373 | 에러 핸들링을 중앙 집중화하는 것이 중요합니다. 374 | 저차원 함수에서 에러를 핸들링하기보다 고차원 함수로 에러를 전달하여 에러를 핸들링하는 방식이 바람직합니다. 375 | 저차원 함수 여기저기에 에러 핸들링이 나뉘어 있는 방식보다 고차원 함수에서 중앙 집중화된 에러 핸들링을 사용하는 방식입니다. 376 | 377 | 그렇다면 중앙 집중화된 에러 핸들링은 어떤 형태일까요? 378 | 아래 코드를 살펴봅시다. 379 | 380 | ```swift 381 | struct ErrorHandler { 382 | static let default = ErrorHandler() 383 | 384 | let genericMessage = "Sorry! Something went wrong" 385 | 386 | func handleError(_ error: Error) { 387 | presentToUser(massage: genericMessgae) 388 | } 389 | 390 | // func override 391 | func handleError(_ error: LocalizedError) { 392 | if let errorDescription = error.errorDescription { 393 | presentToUser(message: errorDescription) 394 | } else { 395 | presentToUser(message: genericMessage) 396 | } 397 | } 398 | 399 | func presentToUser(message: String) { 400 | print(message) 401 | } 402 | } 403 | ``` 404 | 405 | 위 코드에서 ErrorHandler 구조체가 에러 핸들링의 모든 책임을 집니다. 406 | 중앙 집중화된 에러 핸들링이라 볼 수 있습니다. 407 | 408 | 에러 핸들링 코드가 여러 곳에 흩어져 있다면 변경 사항에 대응하기 어려워집니다. 409 | ErrorHandler 구조체에서는 함수 오버라이드를 통해 여러 유형의 에러를 핸들링하고 있습니다. 410 | ErrorHandler 구조체에서는 static 변수로 싱글턴 패턴을 구현하여 에러 핸들링이 필요한 상황에 어디서든 접근할 수 있도록 만들었습니다. 411 | 412 | 위에서 살펴봤던 extractRecipe 구조체의 extractRecipe 함수는 Recipe?를 리턴하고 있었습니다. 413 | 하지만 extractRecipe 함수가 nil을 만났을 때 에러를 던지도록 구현한다면 Recipe을 옵셔널로 감싸지 않고 리턴할 수 있습니다. 414 | nil의 경우 함수에서 에러를 리턴하기 때문입니다. 415 | 416 | 아래 코드로 살펴봅시다. 417 | 418 | ```swift 419 | struct RecipeExtractor { 420 | let html: String 421 | 422 | func extractRecipe() throws -> Recipe { 423 | return try parseHTML(html) 424 | } 425 | 426 | private func parseHTML(_ html: String) throws -> Recipe { 427 | let ingredients = try extractIngredients(html) 428 | let steps = try extractSteps(html) 429 | return Recipe(ingredients: ingredients, steps: steps) 430 | } 431 | 432 | // ...snip 433 | } 434 | 435 | let html = ... 436 | let recipeExtractor = RecipeExtractor(html: html) 437 | 438 | do { 439 | let recipe = try recipeExtractor.extractRecipe() 440 | } catch { 441 | ErrorHandler.default.handleError(error) 442 | } 443 | ``` 444 | 445 | 위 코드의 do catch 구문을 살펴보면 함수 호출부에서 에러를 catch하고 해당 에러를 ErrorHandler로 넘기고 있습니다. 446 | 함수 호출부에서 에러 대응 방식들이 중앙 집중화 되어있는 에러 핸들러(ErrorHandler)로 에러를 넘겼습니다. 447 | 이는 에러 대응에 변경 사항이 생길 경우 대응하기 쉽게 에러 핸들링을 중앙 집중화할 수 있습니다. 448 | 449 | 물론 에러 핸들링을 한 곳으로 모으면 에러 핸들링 객체가 너무 커질 수 있습니다. 450 | 이때는 더 작은 단위로 핸들러를 나누도록 합시다. 451 | 452 | ## Delivering pleasant APIs 453 | 454 | 에러를 전달하고 핸들링하는 행위는 바람직합니다. 455 | 하지만 에러는 필연적으로 개발자에게 핸들링 책임을 지게 합니다. 이는 부담으로 여겨질 수 있습니다. 456 | 또한 에러 전달을 최소화한 APIs는 더욱 빠르고 쉽습니다. 457 | 458 | 에러 전달을 최소화하는 네 가지 방법을 살펴봅시다. 459 | 460 | 첫 번째 방법은 객체 생성 시점에 객체의 유효성을 평가하는 방법입니다. 461 | 462 | 객체 생성 시점에 객체의 유효성을 평가하여 유효하지 않은 객체가 코드상에 돌아다니지 않도록 만듭니다. 463 | 유효하지 않은 객체가 코드상에 없기 때문에 반복되는 유효성 평가를 피할 수 있습니다. 464 | 465 | 아래 코드는 객체 생성 시점에 유효성 평가를 하지 않은 코드입니다. 466 | 467 | ```swift 468 | enum ValidationError: Error { 469 | case noEmptyValueAllowed 470 | case invalidPhoneNumber 471 | } 472 | 473 | func validatePhoneNumber(_ text: String) throws { 474 | guard !text.isEmpty else { 475 | throw ValidationError.noEmptyValueAllowed 476 | } 477 | 478 | let pattern = "..." 479 | if text.range(of: pattern, optionbs: .regularExpression, range: nil, locale: nil) == nil { 480 | thorw ValidationError.invalidPhoneNumber 481 | } 482 | } 483 | 484 | do { 485 | try validatePhoneNumber("(123) 123-1234") 486 | print("PhoneNumber is valid") 487 | } catch { 488 | print(error) 489 | } 490 | ``` 491 | 492 | 위 코드는 반복되는 에러 핸들링을 하고 있습니다. 심지어 같은 번호라도 계속해서 do catch 구문에서 유효성 검사를 할 것입니다. 493 | 만약 핸드폰 번호 객체가 생성될 때, 번호의 유효성을 검사하여 유효하지 않은 번호는 에러를 발생하고 유효할 경우 객체를 생성한다면 애플리케이션 내에서 동일한 객체의 유효성을 반복적으로 검사할 필요가 없어집니다. 494 | 495 | 아래 코드는 객체 생성 시점에 객체의 유효성을 평가하도록 고친 코드입니다. 496 | 497 | ```swift 498 | struct PhoneNumber { 499 | let contents: String 500 | 501 | // 객체 생성과 동시에 객체 유효성 평가를 진행합니다. 502 | init(_ text: String) throws { 503 | guard !text.isEmpty else { 504 | throw ValidationError.noEmptyValueAllowed 505 | } 506 | 507 | let pattern = "..." 508 | if text.range(of: pattern, optionbs: .regularExpression, range: nil, locale: nil) == nil { 509 | thorw ValidationError.invalidPhoneNumber 510 | } 511 | self.contents = text 512 | } 513 | } 514 | 515 | do { 516 | let phoneNumber = try PhoneNumber("(123) 123-1234") 517 | print(phoneNumber.contents) 518 | } catch { 519 | print(error) 520 | } 521 | ``` 522 | 523 | PhoneNumber 객체 생성과 동시에 에러를 던지거나 올바른 객체를 생성하여 이후 반복적인 유효성 평가를 할 필요가 없어집니다. 524 | 이제는 전화번호가 유효한 객체만 코드상에 존재합니다. 이는 에러 전달을 최소화합니다. 525 | 526 | 에러 전달을 최소화하는 두 번째 방법은 try?를 사용하는 방법입니다. 527 | 528 | try?의 특징은 에러의 발생 이유에는 관심이 없다는 것입니다. 값이 생성되었느냐 아니냐(nil)에만 관심이 있습니다. 529 | try?를 사용한다면 에러가 발생할 경우 nil을 리턴하고 발생하지 않을 경우 옵셔널로 감싼 결과를 리턴합니다. 530 | 531 | 함수에서 에러를 던지지만, 호출부에서는 에러의 발생 이유에는 관심이 없을 때 try?를 사용해 함수의 리턴을 옵셔널 또는 nil로 받을 수 있습니다. 532 | 어떤 종류의 에러가 발생하더라도 try?는 nil을 리턴합니다. 533 | 에러의 이유가 중요하지 않은 상황에서는 try?를 사용해 모든 에러를 nil로 리턴 받아 에러 전달을 최소화할 수 있습니다. 534 | 535 | 아래 코드로 확인해 봅시다. 536 | 537 | ```swift 538 | let phoneNumber = try? PhoneNumber("(123) 123-1234") 539 | print(phoneNumber) // Optional(PhoneNumber(contents: "(123) 123-1234")) 540 | ``` 541 | 542 | 세 번째 방법은 try!를 사용하는 방법입니다. 543 | try?와 비슷한 성격이지만 에러가 발생하면 크래쉬를 발생시키기 때문에 사용하지 맙시다! 544 | 545 | 에러 전달을 최소화하는 네 번째 방법은 옵셔널을 리턴하는것 입니다. 546 | 547 | 옵셔널은 에러 핸들링 방법 중 하나입니다. 에러를 던지는 방법보다 더 좋은 대안이 될 수 있습니다. 548 | 에러가 아닌 옵셔널을 리턴하면 호출부에서는 에러를 핸들링할 부담이 줄어듭니다. 549 | 550 | 아래 코드로 확인합시다. 551 | 552 | ```swift 553 | func loadFile(name: String) -> Data? { 554 | let url = playgroundSharedDatadirectory.appendingPathComponent(name) 555 | return try? Data(contentsOf: url) // Data에서 발생하는 에러는 try?를 통해 옵셔널로 감싸집니다. (에러의 경우 nil이 됩니다.) 556 | } 557 | ``` 558 | 559 | 호출부에서는 항상 에러를 옵셔널로 변환할 수 있습니다. try?를 사용하면 옵셔널로 에러를 catch 할 수 있습니다. 560 | 하지만 에러의 발생 이유가 중요하다면 옵셔널이 아닌 에러를 던져야 합니다. 옵셔널의 경우 에러의 발생 이유에는 집중하지 않기 때문입니다. 561 | 562 | ## Summary 563 | 564 | - Even though errors are usually enums, any type can implement the Error protocol. 565 | - Inferring from a function which errors it throws isn't possible, but you can use Quick Help to soften the pain. 566 | - Keep throwing code in a predictable state for when an error occurs. You can achieve a predictable state via imuutable fuctions, working with copies or temporary values, and using defer to undo any mutations that may occur before an error is thrown. 567 | - You can handle errors four ways: do catch, try? and try! and propagation higher in the stack. 568 | - An error can contain technical information to help to troubleshoot. User-facing messages can be deduced from the technical information, by implementing the LocalizedError protocol. 569 | - By implementing the CustomNSError you can bridge an error to NSError. 570 | - A good practice for handling errors is via centralized error handling. With centralized error handling, you can easily change how to handle errors. 571 | - You can prevent throwing errors by turning them into optionals via the try? keyword. 572 | - If you're certain that an error won't occur, you can turn to retrieve a value from a throwing function with the try! keyword, with the risk of a crashing application. 573 | - If there is a single reason for failure, consider returning an optional instead of creating a throwing function. 574 | - A good practice is to capture validity in a type. Instead of having a throwing function you repeatedly use, create a type with a throwing initializer and pass this type around with the confidence of knowing that the type is validated. 575 | -------------------------------------------------------------------------------- /Generics.md: -------------------------------------------------------------------------------- 1 | # Generics 2 | 3 | ## This chapter covers 4 | - How and when to write generic code 5 | - Understanding how to reason about generics 6 | - Constraining generics with one or more protocols 7 | - Making use of the Equatable, Comparable and Hashable protocols 8 | - Creating highly reusable types 9 | - Understanding how subclasses work with generics 10 | 11 | ## The benefits of generics 12 | 13 | 제네릭은 스위프트에서 자주 등장합니다. 14 | 다형성을 위해서는 제네릭과 프로토콜은 필수적입니다. 15 | 16 | 제네릭 없이 여러 타입을 대응하는 함수를 만들기 위해서는 중복되는 코드가 씁니다. 17 | 제네릭을 사용한다면 여러 타입을 대응하는 하나의 함수를 만들 수 있습니다. 18 | 19 | 물론 Any 타입을 사용해 여러 타입에 대응할 수 있지만, Any를 사용하면 런타임에 Any를 특정 타입(String, Int 등)으로 다운 캐스팅해야 합니다. 20 | Any 타입과 함께 AnyObject 타입도 존재합니다. Any 타입은 값 타입, 참조 타입 모두 저장 가능하지만 AnyObject 타입은 클래스 타입만 저장할 수 있습니다. 21 | 하지만 Any 타입과 AnyObject 타입 모두 런타임에 타입이 결정됩니다. 22 | 또한 다운 캐스팅도 필수적입니다. 이는 보일러플레이트 코드로 이어집니다. 23 | 24 | Any 타입이나 AnyObject 타입보다는 제네릭을 사용하도록 합시다. 25 | 26 | 제네릭을 사용해 컴파일 타입에 다형성을 이룰 수 있습니다. 27 | 아래 코드는 제네릭 없이 구현하던 함수를 제네릭 함수로 고친 코드입니다. 28 | 29 | ```swift 30 | // 제네릭 X 31 | func firstLast(array: [Int]) -> (Int, Int) { 32 | return (array[0], array[array.count - 1]) 33 | } 34 | 35 | // 제네릭 O 36 | func firstLast(array:[T]) -> (T, T) { 37 | return (array[0], array[array.count -1]) 38 | } 39 | ``` 40 | 41 | 제네릭 함수를 만들 때는 함수명 뒤에 를 붙이고 함수 내부에서 사용되는 타입도 T로 바꾸면 됩니다. 42 | 물론 T 말고도 Wrapped, U, V 등 자유롭게 사용할 수 있습니다. 43 | 44 | 하지만 T로 제네릭을 구현했다면 함수 내부에서 T 타입을 가진 변수는 모두 동일한 데이터 타입을 따르게 됩니다. 45 | 제네릭에 Int, String 등과 함께 커스텀 데이터 타입도 넘길 수 있습니다. 46 | 47 | 제네릭으로 타입에 제약받지 않는 범용 코드를 만들고 코드 중복을 피할 수 있습니다. 48 | 49 | 결과적으로 제네릭 함수는 하나의 함수로 여러 타입에 대응할 수 있습니다. 50 | Any를 사용했다면 추가적인 다운 캐스팅을 해야 하지만, 제네릭으로 컴파일 타임에 모든 타입을 선언할 수 있습니다. 51 | 52 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/e1ac17ec-7377-4599-a458-8efa25af6219) 53 | 54 | 처음부터 제네릭 함수를 구현하는 건 어려울 수 있습니다. 55 | 먼저 제네릭 없이 함수를 구현해보고 이후 제네릭 함수로 고치는 방식이 더 쉽습니다. 56 | 57 | 유용한 제네릭이지만 주의해야 할 부분도 있습니다. 58 | 아래 코드로 확인해 봅시다. 59 | 60 | ```swift 61 | func illegalWrap(value: T) -> [Int] { 62 | return [value] 63 | } 64 | ``` 65 | 66 | illegalWrap 함수는 제네릭 타입의 value를 입력 받고 [Int]를 리턴하고 있습니다. 67 | illegalWrap 함수의 제네릭을 Int로 특정해 리턴하고 있기 때문에 컴파일 에러가 발생합니다. 68 | 69 | 제네릭은 타입을 특정할 수 없습니다! 70 | 당연하게 생각되지만 주의해야 합니다. 71 | 72 | 아래 코드로 더 확인해 봅시다. 73 | 74 | ```swift 75 | // 컴파일 에러 76 | func wrap (value: Int, secondValue: T) -> ([Int], U) { 77 | return ([value], secondValue) 78 | } 79 | 80 | // 컴파일 가능 81 | func wrap(value: Int, secondValue: T) -> ([Int], T) { 82 | return ([value], secondValue) 83 | } 84 | 85 | // 컴파일 에러 86 | func wrap(value: Int, secondValue: T) -> ([Int], T) { 87 | return ([value], secondValue) 88 | } 89 | 90 | // 컴파일 에러 91 | func wrap(value: Int, secondValue: T) -> ([Int], Int) { 92 | return ([value], secondValue) 93 | } 94 | 95 | // 컴파일 가능 96 | func wrap(value: Int, secondValue: T) -> ([Int], Int)? { 97 | if let secondValue = secondValue as? Int { 98 | return ([value], secondValue) 99 | } else { 100 | return nil 101 | } 102 | } 103 | ``` 104 | 105 | 제네릭을 사용하면 컴파일러가 Value Witness Tables이라 불리는 메타 데이터를 활용하여 106 | 제네릭 함수를 구체화하는 과정에서 구체적인 타입(Int 등)으로 치환된 코드를 컴파일러가 반복적으로 생성합니다. 107 | 108 | 결과적으로 제네릭을 사용하면 어떤 값을 다룰지 컴파일 타임에 알 수 있다는 장점이 있습니다. 109 | 110 | ## Constraining generics 111 | 112 | 지금까지 소개한 제네릭은 별다른 제약 조건이 없는 제네릭이었기 때문에 모든 타입에 대응할 수 있었습니다. 113 | 114 | 하지만 별다른 제약 조건이 없는 제네릭으로는 많은 일을 할 수 없습니다. 115 | 오히려 프로토콜을 통해 제네릭에 제약을 걸면 더 유용히 제네릭을 쓸 수 있습니다. 116 | 117 | 아래 예시는 제네릭에 제약을 걸지 않았을 때 문제가 발생하는 상황을 살펴봅시다. 118 | 아래 lowest 함수는 입력 데이터 중 가장 작은 값을 리턴하는 제네릭 함수입니다. 119 | 120 | ```swift 121 | // 아직 미완성된 lowest 함수입니다. 122 | func lowest(_ array: [T]) -> T? { 123 | let sortedArray = array.sorted { (lhs, rhs) -> Bool in 124 | return lhs < rhs // 에러의 원인입니다. 125 | } 126 | return sortedArray.first 127 | } 128 | 129 | lowest([3, 1, 2]) // Optional(1) 130 | lowest([40.2, 12.3, 99.9]) // Optional(12.3) 131 | lowest(["a", "b", "c"]) // Optional("a") 132 | ``` 133 | 134 | 위의 lowest 함수는 에러를 발생시킵니다. 135 | 136 | lowest 함수에서 제네릭에 제약을 걸지 않았기 때문에 입력 array에 모든 타입이 들어올 수 있습니다. 137 | 하지만 lowest 함수 안에서 비교 연산(<)을 수행하기 때문에 비교 연산이 가능하지 않은 타입이 입력으로 들어올 가능성은 곧 에러의 원인으로 이어집니다. 138 | 139 | 비교 연산이 가능한 타입만 함수 입력으로 들어오도록 제네릭 제약을 걸어 에러를 해결할 수 있습니다! 140 | 우린 프로토콜을 통해 제네릭에 제약을 걸 수 있습니다. 141 | 142 | 그렇다면 비교 연산과 관련 있는 프로토콜은 어떤 게 있을까요? 143 | Equatable 프로토콜과 Comparable 프로토콜이 대표적으로 비교 연산과 관련된 프로토콜입니다. 144 | 145 | 먼저 Equatable 프로토콜은 두 값이 같은지 확인하는 데 쓰입니다. 146 | 동일한 타입 간의 == 비교연산자를 제공합니다. 147 | 따라서 Equatable 프로토콜을 채택했을 때 static == 함수를 필수로 구현해야 합니다. 148 | Equatable 프로토콜의 static == 함수를 통해 동일한 타입 간의 '같다'는 기준을 만들 수 있습니다. 149 | 150 | 아래 코드는 Equatable 프로토콜 코드입니다. 151 | 152 | ```swift 153 | public protocol Equatable { 154 | static func == (lhs: Self, rhs: Self) -> Bool 155 | } 156 | ``` 157 | 158 | 그렇다면 Comparable 프로토콜은 어떨까요? 159 | 160 | Comparable 프로토콜은 Equatabel을 채택하고 있습니다. 161 | 클래스가 Comparable 프로토콜을 채택할 경우 Equatable 프로토콜의 static == 함수의 구현 의무도 지게 됩니다. 162 | Comparable 프로토콜은 Equatable 프로토콜과 마찬가지로 static 함수가 있지만 모든 static 함수 구현을 필수로 요구하지 않습니다. 163 | 164 | 하지만 적어도 하나의 static 함수는 구현해야 합니다. 165 | 166 | ```swift 167 | public protocol Comparable: Equatable { 168 | static func < (lhs: Self, rhs: Self) -> Bool 169 | static func <= (lhs: Self, rhs: Self) -> Bool 170 | static func >= (lhs: Self, rhs: Self) -> Bool 171 | static func > (lhs: Self, rhs: Self) -> Bool 172 | } 173 | ``` 174 | 175 | Comparable 프로토콜을 채택할 경우 static 함수 중 하나를 구현하면 나머지 static 함수들은 스위프트가 유추하기 때문에 추가적으로 구현할 필요가 없습니다. 176 | Int, float, string은 기본적으로 Comparable을 따르는 타입이기 때문에 우리가 자연스럽게 값을 비교할 수 있는것 입니다. 177 | 178 | 위에서 lowest 함수의 제네릭을 Comparable 프로토콜로 제약을 거는 방식으로 함수 안의 비교 연산에서 발생하는 에러를 고쳐봅시다. 179 | 180 | 아래 코드는 lowest 함수의 제네릭을 Comparable 프로토콜로 제약한 코드입니다. 181 | 이제 lowest 함수의 입력은 Comparable 프로토콜을 따르는 타입이어야 합니다. 182 | 183 | ```swift 184 | func lowest(_ array: [T]) -> T? { 185 | let sortedArray = array.sorted { (lhs, rhs) -> Bool in 186 | return lhs < rhs // array 입력으로 Comparable을 따르는 타입만 입력으로 들어오기 때문에 비교 연산이 가능합니다. 187 | } 188 | return sortedArray.first 189 | } 190 | 191 | // lowest 간략한 버전 192 | func lowest(_ array: [T]) -> T? { 193 | return array.sorted().first // array 입력이 항상 Comparable 프로토콜을 따르기 때문에 내장 함수인 sorted()도 사용 가능합니다. 194 | } 195 | ``` 196 | 197 | 그렇다면 lowest 함수에 입력으로 들어올 커스텀 타입은 어떤게 있을까요? 198 | 물론 Comparable 프로토콜을 따르도록 구현된 Int, float, string도 가능하겠지만 Comparable 프로토콜을 따르는 커스텀 타입도 입력으로 들어올 수 있습니다. 199 | 200 | 아래 코드는 Comparable 프로토콜을 따르는 커스텀 타입인 RoyalRank 타입의 코드입니다. 201 | 위에서 말했듯이 Comparable 프로토콜이 지원하는 static 함수 중 하나를 구현하여 나머지 static 함수를 컴파일러가 유추 가능하기 때문에 직접 구현하지 않은 static 함수도 사용할 수 있습니다. 202 | 203 | ```swift 204 | enum RoyalRank: Comparable { 205 | case emperor 206 | case king 207 | case duke 208 | 209 | static func <(lhs: RoyalRank, rhs: RoyalRank) -> Bool { 210 | switch (lhs, rhs) { 211 | case (king, emperor): return true 212 | case (duke, emperor): return true 213 | case (duke, king): return true 214 | default: return false 215 | } 216 | } 217 | } 218 | 219 | let king = RoyalRank.king 220 | let duke = RoyalRank.duke 221 | 222 | duke < king // true 223 | duke > king // false 224 | duke == king // false 225 | 226 | let ranks: [RoyalRank] = [.emperor, .king, .duke] 227 | lowest(ranks) // .duke 228 | ``` 229 | 230 | 제네릭의 장점 중 하나는 아직 존재하지 않은 타입에도 대응할 수 있다는 점입니다. 231 | 232 | 모든 타입이 Comparable 프로토콜을 따르진 않습니다. 233 | 예를 들어 Bool 타입은 Comparable 프로토콜을 따르지 않습니다. 따라서 lowest 함수에 Bool 타입은 입력으로 들어갈 수 없습니다. 234 | 235 | Comstraining a generic means trading flexibility for functionality. A constrained generic becomes more specialized but is less flexible. 236 | 237 | ## Multipule constraints 238 | 239 | 제네릭을 제약해 사용할 때 하나의 프로토콜 만으로는 부족한 경우가 있습니다. 240 | 241 | 예를 들어 값을 비교하고 해당 값을 딕셔너리에 저장해야 한다면, 우리는 비교 기능(Comparable)과 딕셔너리에 저장하는 기능(Hashable) 모두 필요합니다. 242 | 이를 위해 함수의 입력은 Comparable 프로토콜과 Hashable 프로토콜을 모두 따르는 타입이어야 합니다. 243 | 244 | 먼저 Hashable 프로토콜을 살펴 봅시다. 아래 코드는 Hashable 프로토콜 코드입니다. 245 | 246 | ```swift 247 | public protocol Hashable: Equatable { 248 | func hash(into hasher: inout Hasher) { 249 | // ...생략 250 | } 251 | } 252 | ``` 253 | 254 | Hashable 프로토콜도 Equatable 프로토콜을 채택하고 있습니다. 255 | 따라서 Hashable 프로토콜을 채택한다면 Equatable 프로토콜의 static == 함수와 Hashable 프로토콜의 hash 함수를 모두 필수로 구현해야 합니다. 256 | 257 | 구조체와 열거형에서는 Equatable과 Hashable 프로토콜의 필수 구현 함수 중 하나를 구현하면 나머지 함수들을 컴파일러가 유추하지만 클래스에서는 258 | 유추하지 못하기 때문에 클래스의 경우 모든 함수를 구현해야 합니다. 259 | 260 | Hashable 프로토콜의 hash 함수를 통해 정수 타입인 hash value로 불리는 값을 제공합니다. 261 | Hashable 프로토콜은 어떤 경우 채택해야 할까요? 262 | 263 | Set 또는 Dictionary의 Key로 Hashable을 준수하는 모든 타입을 사용할 수 있습니다.  264 | 스위프트 Dictionary 선언부를 보면 Dictionary 형태입니다. 265 | 여기서 KeyType은 반드시 Hashable 프로토콜을 따르는 Hashable 타입이어야 합니다. 266 | Hashable 프로토콜이 제공하는 hash value는 그 자체로 유일하게 표현이 가능한 방법을 제공합니다. 267 | 268 | 물론 스위프트의 기본 타입(Int, Double, String, Bool, enum, sets 등)들은 Hashable 프로토콜을 채택하기 때문에 Dictionary의 KeyType으로 사용할 수 있습니다. 269 | 하지만 커스텀 타입의 경우 Hashable 프로토콜을 채택해야 Dictionary의 KeyType으로 사용할 수 있습니다. 270 | 271 | 심지어 Set은 Hashable 프로토콜을 채택한 타입만 들어갈 수 있습니다. 272 | 273 | 그렇다면 Hashable 프로토콜이 제공하는 hash value는 어떤 값일까요? 274 | 275 | 예를 들어 "Hedgehog"을 hash 함수에 입력하면 43092483과 같은 Int 타입의 hash value가 리턴됩니다. 276 | 같은 타입의 인스턴스 a와 b가 있으면 a == b이면 a.hashValue == b.hashValue 입니다. 277 | Hash 함수는 동일한 입력에 동일한 출력을 보장합니다. 278 | 하지만 hash value가 같다고 해서 동일한 인스턴스는 아닐 수 있습니다. 279 | 280 | 또한 hash value는 프로그램의 실행에 따라 달라질 수 있습니다. 281 | 따라서 이후 실행에 사용할 hash value 값을 저장하지 않는 게 좋습니다. 282 | 과거 실행에 저장했던 hash value와 방금 실행한 코드의 hash value는 달라질 수 있기 때문입니다. 283 | 284 | Array와 달리 Set과 Dictionary는 "순서"가 없습니다. 285 | 따라서 특정 원소를 찾으려할 때 Hashable 프로토콜이 제공하는 정수 Hash(=hashValue)가 있기 때문에 우리가 찾으려는 원소를 빠르게 찾을 수 있습니다. 286 | 여기서 우리는 Set과 Dictionary가 중복을 허용하지 않는 이유도 추측해볼 수 있습니다. 287 | Hash 함수를 통해 원소에 접근하기 때문에 중복을 허용하지 않는것 입니다. 288 | 289 | 아래 코드와 같이 제네릭 타입을 하나 이상의 프로토콜로 제약할 수 있습니다. 290 | 291 | ```swift 292 | func lowestOccurrences(values: [T]) -> [T: Int] { 293 | //...생략 294 | } 295 | ``` 296 | 297 | 제네릭 제약을 여러 프로토콜로 할 때 where 구문을 사용하면 더 간략히 표현 가능합니다. 298 | 아래 코드로 확인해 봅시다. 299 | 300 | ```swift 301 | func lowestOccurrences(values: [T]) -> [T: Int] { 302 | where T: Comparable & Hashable { 303 | // ...생략 304 | } 305 | ``` 306 | 307 | ## Creating a generic type 308 | 309 | 지금까지는 제네릭 함수만 살펴보았지만, 제네릭 타입도 만들 수 있습니다. 310 | 옵셔널 또한 제네릭 타입으로 구현되어 있습니다. 311 | 아래 코드를 살펴봅시다. 312 | 313 | ```swift 314 | public enum Optional { 315 | case none 316 | case some(Wrapped) 317 | } 318 | ``` 319 | 320 | 옵셔널과 배열 등은 모두 제네릭 타입으로 구현되어 있습니다. 321 | 커스텀 타입 또한 제네릭 타입으로 만들 수 있습니다. 322 | 323 | 딕셔너리의 KeyType은 Hashable 타입을 요구합니다. 324 | 하지만 딕셔너리의 KeyType으로 두 개의 Hashable 타입을 함께 사용하는 건 불가능합니다. 325 | 튜플로 두 개의 Hashable 타입인 String 타입을 묶고 딕셔너리의 키로 사용하는 방법 또한 스위프트에서 허용하지 않습니다. 326 | 327 | 아래 코드로 살펴봅시다. 328 | 329 | ```swift 330 | let stringsTuple = ("I want to be part of a key", "Me too!") 331 | let anotherDictionary = [stringsTuple: "I am a value"] // error: type of expression is ambiguous without more context 332 | ``` 333 | 334 | 두 개의 Hashable 타입을 튜플에 넣으면 더 이상 해당 튜플은 Hashable 타입이 아닙니다. 335 | 이런 경우 딕셔너리의 키로 두 개의 Hashable 타입을 사용하려면 어떻게 할까요? 336 | 337 | Pair 커스텀 타입을 만들어 해결할 수 있습니다. 338 | Pair 타입에 Hashable 타입 두 개를 묶어 Hashable 타입으로 딕셔너리의 키에 사용될 수 있습니다. 339 | 아래 코드로 Pair 타입 구현을 살펴봅시다. 340 | 341 | ```swift 342 | struct Pair { 343 | let left: T 344 | let right: T 345 | 346 | init(_ left: T, _ right: T) { 347 | self.left = left 348 | self.right = right 349 | } 350 | } 351 | ``` 352 | 353 | 위와 같이 Pair 타입을 구현한다면 Pair의 left와 right는 항상 같은 타입이어야 합니다. 354 | left와 right가 다른 타입이어도 되도록 코드를 개선해 봅시다. 355 | 356 | ```swift 357 | struct Pair { 358 | let left: T 359 | let right: U 360 | 361 | init(_ left: T, _ right: U) { 362 | self.left = left 363 | self.right = right 364 | } 365 | } 366 | 367 | let pair = Pair("Tom", 20) 368 | let pair2 = Pair("Tom", "Jerry") 369 | ``` 370 | 371 | 이제는 Pair 타입의 left와 right가 Hashable을 따르는 서로 다른 타입이어도 좋습니다. 372 | 373 | 하지만 아직 Pair 타입은 Hashable 타입이 아닙니다. 374 | Pair 타입이 hash value를 가지는 Hashable 타입이 되기 위해서는 추가적인 조치가 필요합니다. 375 | 376 | 첫 번째 방법은 Swift에 맡기는 것입니다. 377 | Swift 버전 4.1부터 Pair와 같은 객체의 두 프로퍼티(left, right)가 모두 Hashable일 경우 Pair 타입을 Hashable로 자동으로 만듭니다. 378 | 하지만 해당 방법은 열거형과 구조체에만 적용됩니다. 379 | 380 | 클래스의 경우 두 번째 방법을 사용하여 Hashable 타입을 명시해야 합니다. 381 | 또한 클래스가 Hashable 프로토콜을 채택하면 hash 함수와 static == 함수를 모두 구현해야 합니다. 382 | 383 | 두 번째 방법은 직접 Pair 타입을 Hashable 프로토콜을 채택하여 Hashable 타입으로 만드는 것입니다. 384 | 아래 코드로 직접 Hashable 타입으로 만드는 방법을 살펴봅시다. 385 | 386 | ```swift 387 | struct Pair: Hashable { 388 | let left: T 389 | let right: U 390 | 391 | init(_ left: T, _ right: U) { 392 | self.left = left 393 | self.right = right 394 | } 395 | 396 | // Hashable 프로토콜을 직접 따르도록 구현했기 때문에 Hashable 프로토콜의 hash 함수를 구현해야 합니다. 397 | func hash(into hasher: inout Hasher) { 398 | hasher.combine(left) 399 | hasher.combine(right) 400 | } 401 | 402 | // Hashable 프로토콜을 직접 따르도록 구현했기 때문에 Hasahable 프로토콜이 따르는 Equatable 프로토콜의 static == 함수를 구현해야 합니다. 403 | static func ==(lhs: Pair, rhs: Pair) -> Bool { 404 | return lhs.left == rhs.left && lhs.right == rhs.right 405 | } 406 | } 407 | 408 | let pair = Pair(10, 20) 409 | print(pair.hashValue) // 52893198438921 410 | 411 | let set: Set = [ 412 | Pair("Laurel", "Hardy"), 413 | Pair("Harry", "Llody") 414 | ] 415 | ``` 416 | 417 | Pair 구조체에 Hashable 프로토콜을 명시적으로 채택하여 hash value를 가진 Hashable 타입으로 Pair 타입을 만들 수 있습니다. 418 | 이제 Pair 타입은 Hashable 타입이기 때문에 hasher를 넘길 수 있습니다. 419 | 420 | 아래 코드로 확인해 봅시다. 421 | 422 | ```swift 423 | let pair = Pair("Madonna", "Cher") 424 | 425 | var hasher = Hasher() 426 | hasher.combine(pair) // pair.hash(into: &hasher)와 동일한 의미입니다. 427 | let hash = hasher.finalize() 428 | print(hash) // 491240970192719 429 | ``` 430 | 431 | 그렇다면 제네릭을 활용해 Hashable 키를 통해 값을 저장하는 캐시를 만들어 봅시다. 432 | 아래 코드로 확인해 봅시다. 433 | 434 | ```swift 435 | class MiniCache { 436 | var cache = [T: U]() 437 | 438 | init() {} 439 | 440 | func insert(key: T, value: U) { 441 | cache[key] = value 442 | } 443 | 444 | func read(key: T) -> U> { 445 | return cache[key] 446 | } 447 | } 448 | 449 | let cache = MiniCache() 450 | cache.insert(key: 100, value: "Jeff") 451 | cache.insert(key: 200, value: "Miriam") 452 | cache.read(key: 200) // Optional("Miriam") 453 | cache.read(key:99) // Optional("Jeff") 454 | ``` 455 | 456 | ## Generics and subtypes 457 | 458 | 서브 클래싱과 제네릭을 같이 쓸 때 다소 복잡한 상황이 발생할 수 있습니다. 459 | 서브 클래싱 상황에서 제네릭이 어떻게 동작하는지 알아 봅시다. 460 | 461 | 온라인 교육 서비스의 데이터를 모델링을 예로 살펴봅시다. 462 | 아래는 간단한 온라인 교육 모델입니다. 463 | 464 | ```swift 465 | class OnlineCourse { 466 | func start() { 467 | print("Starting online course") 468 | } 469 | } 470 | 471 | class SwiftOnTheServer: OnlineCourse { 472 | override func start() { 473 | print("Starting Swift course.") 474 | } 475 | } 476 | 477 | var swiftCourse: SwiftOnTheServer = SwiftOnTheServer() 478 | var course: OnlineCourse = swiftCourse // 가능합니다. 479 | course.start // "Starting Swift course." 480 | ``` 481 | 482 | 위의 경우 제네릭을 사용하지 않은 상태에서 평범한 서브 클래싱입니다. 483 | 484 | 하지만 만약 Container와 Container 같이 상속 관계의 클래스를 제네릭 타입 안으로 넣을 경우 485 | Container와 Container는 더 이상 상속 관계를 유지하지 않습니다. 486 | 487 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/4951f891-846a-44fe-8037-81830d02cc0f) 488 | 489 | 다시 말해, 제네릭 안에서는 클래스끼리의 상속 관계를 잃습니다. 490 | 따라서 아래 코드를 보면 부모 클래스로 자식 클래스를 참조하려 할 때 제네릭으로 인해 상속 관계가 깨지며 부모 타입으로 자식 타입을 참조할 수 없습니다. 491 | 492 | ```swift 493 | struct Container {} 494 | 495 | var containerSwiftCourse: Container = Container() 496 | // error: cannot convert value of type 'Container' to specified type 'Container' 497 | var containerOnlineCourse: Container = containerSwiftCourse 498 | ``` 499 | 500 | 만약 데이터를 저장하는 제네릭 타입 Cache가 있을 경우를 생각해 봅시다. 501 | 아래 코드는 Cache를 간략히 구현한 코드입니다. 502 | 503 | ```swift 504 | struct Cache { 505 | // ...생략 506 | } 507 | 508 | func refreshCache(_ cache: Cache) { 509 | // ...생략 510 | } 511 | 512 | refreshCache(Cache) // 가능합니다. 513 | refreshCache(Cache) // 에러가 발생합니다. 514 | ``` 515 | 516 | 위에서 refreshCache 함수는 입력으로 Cache 타입을 받고 있습니다. 517 | 518 | 물론 제네릭 타입이 아니고 일반적인 상속관계를 가진 OnlineCourse라면 refreshCache 함수에 SwiftOnTheServer 타입을 전달할 수 있습니다. 519 | 하지만 제네릭 타입인 Cache이기 때문에 Cache는 Cache와 상속 관계를 가지지 않기 때문에 전달될 수 없습니다. 520 | 521 | 제네릭 타입으로 클래스를 받을 경우 해당 클래스의 상속 관계를 깨트립니다. 522 | 하지만 상속 관계가 깨지는 제한 사항은 커스텀 타입에만 해당합니다. 523 | Swift Standard Library의 Array와 Optional의 경우에는 상속 관계가 깨지지 않습니다. 524 | Array와 Optional 모두 제네릭 타입이지만 제네릭에 들어가는 클래스의 상속 관계는 유지됩니다. 525 | 526 | 아래 코드로 확인해 봅시다. 527 | 528 | ```swift 529 | func readOptionalCourse(_ value: Optional) { 530 | //... 생략 531 | } 532 | 533 | readOptionalCourse(OnlineCourse()) 534 | readOptionalCourse(SwiftOnTheServer()) 535 | ``` 536 | 537 | 위의 readOptionalCourse 함수의 매개변수 Optional는 Optional이기 때문에 제네릭의 상속 관계가 유지됩니다. 538 | 따라서 OnlineCourse()를 비롯해 OnlineCourse의 자식 클래스인 SwiftOnTheServer 타입도 전달할 수 있습니다. 539 | 540 | 다른 예로 Int?를 매개변수 타입으로 받는 함수에서 Int 값도 전달할 수 있는 이유도 옵셔널 타입이기 때문입니다. 541 | (Int 타입은 옵셔널 Int의 자식 클래스입니다.) 542 | 543 | 옵셔널과 제네릭은 함께 주로 등장합니다. 544 | 545 | 제네릭을 통한 추상화는 더 복잡해지고 해석하기 어렵다는 비용이 들지만, 유연성을 얻을 수 있습니다. 546 | 어디든 trade-off는 존재합니다. 547 | 548 | ## Summary 549 | - Adding an unconstrained generic to a function allows a function to work with all types. 550 | - Generics can't be specialized from inside the scope of a function or type. 551 | - Generic code is converted to specialized code that works on multipule types. 552 | - Generics can be constrained for more specialized behavior, which may exclude some types. 553 | - A type can be constrained to multiple generics to unlock more functionality on a generic type. 554 | - Swift can synthesize implementations for the Equatable and Hashable protocols on structs and enums. 555 | - Synthesizing that you write are invariant, and therefore you cannot use them as subtypes. 556 | - Generic types in the standard library are covariant, and you can use them as subtypes. 557 | -------------------------------------------------------------------------------- /Making-optionals-second-nature.md: -------------------------------------------------------------------------------- 1 | # Making optionals second nature 2 | 3 | ## This chapter covers 4 | - Best practice related to optionals 5 | - Handling multiple optionals with guards 6 | - Properly dealing with optional strings versus empty strings 7 | - Jugglings various optionals at once 8 | - Falling back to default values using the nil-coalescing operator 9 | - Simplifying optional enums 10 | - Dealing with optional Booleans in multiple ways 11 | - Digging deep into values with optional chaining 12 | - Force unwrapping guidelines 13 | - Taming implicity unwrapped optionals 14 | 15 | ## The purpose of optionals 16 | 옵셔널은 값을 가졌거나 가지지 않았을 가능성이 있는 열지 않은 상자와 비슷합니다. 17 | 18 | 옵셔널을 통해 값이 없을 때 발생하는 크래시를 막을 수 있고 옵셔널 안에 값이 없다면 이를 컴파일 타임에 알 수 있습니다. 19 | 옵셔널은 내부 값을 감싸고 있어 언래핑을 통해야만 옵셔널을 벗겨야 내부 값에 접근할 수 있습니다. 20 | 21 | ## Clean optional unwrapping 22 | 아래 코드를 보면 옵셔널 프로퍼티를 ?로 선언하고 있습니다. 23 | 다른 프로퍼티와 달리 옵셔널 프로퍼티는 초기화 의무를 지지 않습니다. 24 | 25 | ```swift 26 | struct Customer { 27 | let id: String 28 | let email: String 29 | let balance: Int 30 | let firstName: String? 31 | let lastName: String? 32 | } 33 | ``` 34 | 35 | 옵셔널은 실제로 어떻게 구현되어 있을까요? 36 | 37 | enum으로 구현되어 있습니다. 38 | 39 | 옵셔널의 성격이 "값이 있거나 없거나"이기에 "or"에 어울려 enum으로 구현되었다고 이해하면 쉽습니다. 40 | 옵셔널에는 내장 데이터 타입은 물론 커스텀 타임도 들어갈 수 있습니다. 따라서 옵셔널이 generic type이라는 사실도 알 수 있습니다. 41 | 42 | ```swift 43 | public enum Optional { 44 | case none 45 | case some(Wrapped) 46 | } 47 | ``` 48 | 49 | 스위프트에서 문법적 편리함을 위해 제공하는 기능들을 syntactic sugar라고 부릅니다. 50 | 변수의 옵셔널 선언을 ?로 할 수 있는 이유도 사실은 syntactic sugar의 도움에 있습니다. 51 | 원래라면 위와 같은 enum은 Optional 형태의 자료형으로 선언되어야 합니다. 52 | syntactic sugar 덕분에 ?선언만으로 옵셔널을 선언할 수 있습니다. 53 | 54 | 관용적으로 Optional보다 ?를 사용한 방식이 주로 쓰입니다. 55 | 아래 코드는 Optional와 ?를 사용해 옵셔널 변수를 선언한 코드입니다. 56 | 57 | ```swift 58 | // with syntatic sugar : ? 59 | struct Customer { 60 | let id: String 61 | let email: String 62 | let balance: Int 63 | let firstName: String? 64 | let lastName: String? 65 | } 66 | 67 | // without syntatic sugar : Optional 68 | struct Customer { 69 | let id: String 70 | let email: String 71 | let balance: Int 72 | let firstName: Optional 73 | let lastName: Optional 74 | } 75 | ``` 76 | 77 | 옵셔널의 내부 값에 접근하려면 옵셔널을 언래핑해야 합니다. 78 | 79 | 옵셔널 언래핑하면 가장 먼저 떠오르는 건 if let입니다. 물론 guard let을 사용해 옵셔널을 언래핑할 수 있습니다. 80 | 아래 코드는 if let을 사용한 옵셔널 언래핑 예시입니다. 81 | 82 | ```swift 83 | let customer = Customer(id: "30", email: "mayeloe@naver.com", firstName: "Jake", lastName: "Freemason", balance: 300) 84 | 85 | print(customer.firstName) // "Optional("Jake") 86 | if let firstName = customer.firstName { 87 | print(firstName) // "Jake" 88 | } 89 | ``` 90 | 91 | 위에서 보았듯이 if let을 사용해 언래핑이 가능합니다. 92 | 심지어 언래핑을 중복해 사용하기도 합니다. 93 | 언래핑을 중복해 사용해서 들여쓰기 단계를 줄일 수 있습니다. 들여쓰기 단계가 줄어들면 가독성이 높아집니다. 94 | 95 | 아래 코드를 확인해 봅시다. 96 | 97 | ```swift 98 | if let firstName = customer.firstName, let lastName = customer.lastName { 99 | print("Customer's full name is \(firstName) \(lastName)") 100 | } 101 | ``` 102 | 103 | 언래핑 if let과 함께 Bool 조건도 사용할 수 있습니다. 104 | 105 | ```swift 106 | if let firstName = customer.firstName, customer.balance > 0 { 107 | let welcomeMessage = "Dear \(firstName), you have money on your account, want to spend it on mayonnaise?" 108 | } 109 | ``` 110 | 111 | 언래핑 if let과 함께 패턴 매칭도 가능합니다. 112 | 113 | ```swift 114 | if let firstName = customer.firstName, 4500..<5000 ~= customer.balance { 115 | let notification = "Dear \(firstName), you are getting close to afford out $50 tub!") 116 | } 117 | ``` 118 | 119 | 위와 같이 multipule unwrapped, unwrapped with Bool, unwrapped with pattern matching을 통한 코드 정리는 바람직합니다. 120 | 121 | if let을 통한 언래핑은 언래핑 내부 값을 원할 때 적절합니다. 만약 내부 값이 필요 없다면 와일드카드(_)를 사용하여 값을 무시할 수 있습니다. 122 | 123 | ```swift 124 | if 125 | let _ = customer.firstName, 126 | let _ = customer.lastName { 127 | print("The customer entered his full name") 128 | } 129 | ``` 130 | 131 | 열거형은 case에 따른 패턴 매칭이 가능합니다. 132 | Optional도 열거형으로 구현되었기 때문에 패턴 매칭이 가능합니다. 133 | 아래 코드와 같이 Optional을 언래핑할 때 패턴 매칭이 유용히 사용됩니다. 134 | 135 | Optional의 패턴 매칭에도 syntactic sugar가 도움을 줍니다. 136 | 아래 코드를 확인해 봅시다. syntactic sugar의 유무와 관계없이 동일한 기능을 하는 코드입니다. 137 | 138 | ```swift 139 | // Optional pattern match without syntactic sugar 140 | switch customer.firstName { 141 | case .some(let name): print("First name is \(name)") 142 | case .none: print("Customer didn't enter a first name") 143 | } 144 | 145 | // Optional pattern match with syntactic sugar 146 | switch customer.firstName { 147 | case let name?: print("First name is \(name)") 148 | case nil: print("Customer didn't enter a first name") 149 | } 150 | ``` 151 | 152 | ## Variable shadowing 153 | "같은 이름으로 언래핑합시다!" 154 | 155 | 언래핑 이후 결과를 저장하는 변수명을 옵셔널 변수명과 같게 만듭시다. 156 | 새로운 변수명에 언래핑의 결과를 저장하는 방식은 권장하지 않습니다. 157 | 오히려 새로운 변수명은 혼란을 일으킵니다. 158 | 159 | ```swift 160 | extension Customer: Customer { 161 | var description: String { 162 | var customDescription: String = "\(id), \(email)" 163 | 164 | // 같은 이름으로 언래핑합시다! 165 | if let firstName = firstName { 166 | customDescription += ", \(firstName)" 167 | } 168 | if let lastName = lastName { 169 | customDescription += "\(lastName)" 170 | } 171 | 172 | return customDescription 173 | } 174 | } 175 | ``` 176 | 177 | ## When optionals are prohibited 178 | 지금까지는 if let을 사용해 옵셔널을 언래핑했습니다. 179 | if let은 옵셔널이 nil일 경우 함수를 종료시키지 않고 계속해서 함수를 진행합니다. 180 | 따라서 if let은 옵셔널이 nil이 필요한 경우 사용합니다. 181 | 182 | 만약 옵셔널 언래핑에 nil이 필요 없다면 guard let이 더 적합합니다. 183 | guard let에서 옵셔널이 nil이라면 해당 함수를 종료시킵니다. 184 | 185 | ```swift 186 | struct Customer { 187 | let id: String 188 | let email: String 189 | let firstName: String? 190 | let lastName: String? 191 | 192 | var displayName: String { 193 | guard let firstName = firstName, let lastName = lastName else { 194 | return "" 195 | } 196 | return "\(firstName) \(lastName)" 197 | } 198 | } 199 | ``` 200 | 201 | guard let을 통해 옵셔널을 언래핑하면 guard let 구문 아래로는 옵셔널 타입이 내려갈 일이 없어집니다. 202 | 203 | ## Returing optional strings 204 | optional string의 경우 언래핑되지 않았을 때 빈 문자열("")을 리턴하는건 흔하게 볼 수 있습니다. 205 | 빈 문자열을 리턴하는게 틀렸다는 건 아닙니다. 206 | 207 | 하지만 일부 경우에는 빈 문자열을 리턴하는것 보다 optional string을 리턴하는게 유용할 때가 있습니다. 208 | 209 | 예를 들어 위의 Customer 구조체가 가진 displayName에서 언래핑 실패 이후 빈 문자열을 던질 경우, displayName을 호출하는 부분에서 코드 작성자가 직접 displayName.isEmpty를 체크해야 합니다. 210 | 만약 displayName.isEmpty를 호출부 중 하나라도 빼먹는다면 버그로 이어지고 빼먹었다는 사실을 컴파일러가 알려주지 못합니다. 211 | 또한 displayName의 호출 부에서는 특정 이름을 기대하지 빈 문자열을 기대하고 있지 않습니다. 212 | 213 | 이럴 때 빈 문자열이 아닌 optional string을 리턴하면 displayName 호출부에서는 옵셔널을 리턴 받아 처리하기 때문에 옵셔널 언래핑 과정을 거치며 컴파일 타임 안전성을 제공받습니다. 214 | 만약 nil을 리턴한다면 언래핑 과정에서 컴파일러가 displayName이 값을 가지지 않음을 알 수 있습니다. 215 | 216 | 아래 코드는 옵셔널 언래핑 실패 시 빈 문자열을 던지던 displayName을 optional string을 던지도록 수정한 코드입니다. 217 | 옵셔널을 리턴하며 호출 부에서 옵셔널을 언래핑할 책임을 갖게 됩니다. 218 | 이는 컴파일러의 도움을 받기 때문에 더 안전한 코드가 됩니다. 219 | 220 | ```swift 221 | struct Customer { 222 | // 생략 223 | 224 | var displayName: String? { 225 | guard let firstName = firstName, let lastName = lastName else { 226 | return nil // nil을 리턴하며 optional string을 던지게 됩니다. 227 | } 228 | return "\(firstName) \(lastName)" 229 | } 230 | } 231 | 232 | if let displayName = customer.displayName { 233 | createConfirmationMessage(name: displayName, product: "Economy size party tub") 234 | } else { 235 | createConfirmationMessage(name: "customer", product: "Economy size party tub") 236 | } 237 | ``` 238 | 239 | ## Granular control over optionals 240 | 위의 예에서 displayName은 firstName과 lastName을 모두 요구하고 있습니다. 241 | 만약 firstName과 lastName 중 하나라도 있다면 displayName을 형성하도록 만들려면 어떻게 해야 할까요? 242 | 이런 경우 옵셔널을 세분화해서 다뤄야 합니다. 243 | 옵셔널을 세분화한다는게 무엇일까요? 결국 옵셔널 열거형을 패턴 매칭하겠다는 의미입니다. 244 | 245 | ?와 switch 그리고 튜플을 사용하여 옵셔널을 세분화하여 언래핑할 수 있습니다. (패턴 매칭과 비슷한 느낌입니다.) 246 | 247 | 아래 코드로 확인해 봅시다. 248 | 249 | ```swift 250 | struct Customer { 251 | // 생략 252 | let firstName: String? 253 | let lastName: String? 254 | 255 | var displayName: String? { 256 | // switch + optional in tuple + ? 257 | switch (firstName, lastName) { 258 | case let (first?, last?): return first + " " + last // return에 쓰이는 first와 last는 옵셔널 언래핑된 상태입니다. 259 | case let (first?, nil): return first // nil 값을 노골적으로 매칭할 수 있다. 260 | case let (nil,last?): return last 261 | default: return nil 262 | } 263 | } 264 | ``` 265 | 266 | 위와 같이 손님이 풀네임을 입력하지 않을 경우, 일부 이름으로 풀네임을 대신할 수 있습니다. 267 | 268 | 위 코드에서는 튜플이 옵셔널 두 개를 가지고 있습니다. 269 | 하지만 튜플에 너무 많은 옵셔널을 넣는 행위는 경계해야 합니다. 270 | 3개 미만의 옵셔널을 갖는 튜플이 적합합니다. 271 | 272 | ## Falling back when an optional is nil 273 | fallback value(??)는 옵셔널 언래핑 종료 중 하나입니다. 274 | 옵셔널이 nil일 경우 default 값으로 대신하며 언래핑합니다. 275 | 276 | 아래 코드로 확인해 봅시다. 277 | 278 | ```swift 279 | let title: String = customer.displayer ?? "customer" 280 | createConfirmationMessage(name: title, product: "Economy size party tub") 281 | ``` 282 | 283 | ## Simplifying optional enums 284 | 앞서 말했듯이 옵셔널은 열거형으로 구현되어 있습니다. 285 | 그렇다면 optional enum은 열거형 내부에 열거형이 구현된 것입니다. 286 | 287 | ?를 사용하면 옵셔널 언래핑과 옵셔널 내부 값 접근을 동시에 할 수 있습니다. 288 | optional enum을 언래핑할 때 if let을 대신하여 ?을 사용해 코드를 개선할 수 있습니다. 289 | 아래 코드를 보고 이해해 봅시다. 290 | 291 | ```swift 292 | enum Membership { 293 | case gold 294 | case silver 295 | } 296 | 297 | struct Customer { 298 | // 생략 299 | let merbership: Membership? 300 | } 301 | ``` 302 | 303 | 위와 같이 Memebership enum을 Customer 구조체에서 옵셔널로 선언했습니다. 304 | 이때 if let을 사용하여 언래핑한다면 아래와 같습니다. 305 | 306 | ```swift 307 | if let membership = customer.membership { 308 | switch membership { 309 | case .gold: print("its gold") 310 | case .silver: print("its silver") 311 | } 312 | } else { 313 | print("none") 314 | } 315 | ``` 316 | 317 | if let을 대신해서 ?를 사용한다면 옵셔널 언래핑과 옵셔널 내부 값 접근을 동시에 할 수 있습니다. 318 | 이는 코드를 더 짧게 만듭니다. 319 | 320 | 아래 방법으로 추가적인 if let 없이 enum의 패턴 매칭과 동시에 옵셔널 언래핑을 할 수 있습니다. 321 | 322 | ```swift 323 | switch customer.membership { 324 | case .gold?: print("its gold") 325 | case .silver?: print("its silver") 326 | case nil: print("none") 327 | } 328 | ``` 329 | 330 | 여기서 문제입니다. 331 | 332 | 만약 아래 코드와 같이 PasteBoardContents enum과 PasteBoardEvent enum이 있을 때 describeAction 함수 구현부를 완성해 봅시다. 333 | 334 | ```swift 335 | enum PasteBoardContents { 336 | case url(url:String) 337 | case emailAddress(emailAddress: String) 338 | case other(content: String) 339 | } 340 | 341 | enum PasteBoardEvent { 342 | case added 343 | case erased 344 | case pasted 345 | } 346 | 347 | func describeAction(event: PasteBoardEvent?, contents: PastBoardEvent?) -> String {} 348 | ``` 349 | 350 | describeAction 함수로 옵셔널이 2개 들어오기 때문에 두 옵셔널을 튜플에 넣고 ?와 함께 패턴 매칭하여 옵셔널을 언래핑하는 방식이 적절합니다. 351 | 352 | 아래 코드를 통해 확인해 봅시다. 353 | 354 | ```swift 355 | func describeAction(event: PasteBoardEvent?, contents: PastBoardEvent?) -> String { 356 | switch (event, contents) { 357 | case let (.added?, url(url)?): return "User added a url to pasteboard: \(url)" // url 값이 return에 쓰이기 때문에 let이 붙었습니다. 358 | case (.added?, _): return "User added something to pasteboard" 359 | case (.erased?, .emailAddress?): return "User erased an email address from the pasteboard" 360 | default: return "The pasteboard is updated" 361 | } 362 | ``` 363 | 364 | ## Chaining optionals 365 | 옵셔널 체이닝은 옵셔널이 본인의 프로퍼티로 또 다른 옵셔널을 가질 때, 옵셔널의 옵셔널 프로퍼티에 접근하기 위해 사용됩니다. 366 | 367 | ?를 사용해 옵셔널 체이닝을 표현합니다. 368 | 옵셔널 체이닝으로 옵셔널의 옵셔널 프로퍼티에 접근할 수 있지만 옵셔널 체이닝을 통해 내부 옵셔널에 접근만 한 것입니다. 369 | 해당한 프로퍼티 또한 옵셔널이기 때문에 추가적인 옵셔널 언래핑이 있어야 순수 값을 추출 가능합니다. 370 | 371 | 아래 코드를 통해 살펴봅시다. 372 | 373 | ```swift 374 | struct Product { 375 | let id: String 376 | let name: String 377 | let image: UIImage? 378 | } 379 | 380 | struct Customer { 381 | // 생략 382 | let favoriteProduct: Product? 383 | } 384 | 385 | let imageView = UIImageView() 386 | // optional value입니다. 옵셔널 체이닝을 통해 favoriteProduct의 image 옵셔널 값에 접근하고 있습니다. 387 | imageView.image = customer.favoriteProduct?.image 388 | 389 | if let image = customer.favoriteProduct?.image { 390 | imageView.image = image // if let으로 더 이상 optional value가 아닙니다. 391 | } else { 392 | imageView.image = UIImage(named: "missing_image") 393 | } 394 | 395 | imageView.image = customer.favoriteProduct?.image ?? UIImage(named: "missing_image") 396 | ``` 397 | 398 | 위 코드에서 Customer의 favoriteProduct는 옵셔널 프로퍼티 속 옵셔널 프로퍼티입니다. 옵셔널 체이닝으로 접근한 옵셔널 값을 if let 또는 ??로 언래핑할 수 있습니다. 399 | 옵셔널 체이닝으로 옵셔널 값이 갖는 옵셔널에 접근할 때 더욱 간결히 접근할 수 있습니다. 400 | 401 | ## Constraining optional Booleans 402 | Bool은 true / false 두 가지 상태를 가집니다. 403 | 그렇다면 optional Bool은 몇 가지 상태를 가질까요? 404 | 405 | 스위프트에서 optional Bool의 상태는 true / false / nil이 될 수 있습니다. 406 | 상황의 맥락에 따라 optional Bool 타입 값이 nil일 때 nil을 true / false / nil로 취급할 수 있습니다. 407 | 408 | 가장 먼저 optional Bool 타입이 두 가지 상태(true/false)를 가지는 경우를 살펴봅시다. 409 | optional Bool 타입이 nil일 때 해당 값을 true 또는 false로 취급할 경우 Bool 타입은 두 가지 상태를 갖게 됩니다. 410 | 이때는 ??을 통해 default 값을 설정하는 방식으로 구현할 수 있습니다. 411 | 위에서 보았듯이 ??는 언래핑과 동시에 해당 값이 nil일 경우 default 값을 따라가도록 합니다. 412 | 413 | 아래 코드를 살펴봅시다. 414 | 415 | ```swift 416 | let preferences = ["autoLogin": true, "faceIdEnabled": true] 417 | 418 | let isFaceIdEnabled = preferences["faceIdEnabled"] 419 | print(isFaceIdEnabled) // Optional(true) 420 | 421 | let isFaceIdEnabled = preferences["faceIdEnabled"] ?? false 422 | print(isFaceIdEnabled) // true 423 | ``` 424 | 425 | 물론 optional Bool 타입이 두 가지 상태(true/false)를 가질 경우 무조건 default를 false로 설정하는 건 아닙니다. 426 | 상황에 따라 default를 true 또는 false로 설정해야 합니다. 427 | 428 | 지금부터 optional Bool 타입이 세 가지 상태(true/false/nil)를 가지는 경우를 살펴봅시다. 429 | 430 | optional Bool 타입을 세 가지 상태로 사용할 때 더 여러 상황에 어울립니다. (더 유용합니다.) 431 | optional Bool 타입을 세 가지 상태로 사용할 때 열거형의 성격을 활용하게 됩니다. 432 | optional Bool 내부 값에 따라 열거형의 case에 패턴 매칭하는 방법으로 세 가지 상태를 나타낼 수 있습니다. 433 | 434 | 다시 말해, optional Bool의 내부 값을 custom enum으로 변환해 사용합시다. 이때 RawRepresentable Protocol까지 사용하면 더욱 swift 다워집니다. 435 | 436 | [Apple Developer Documentation](https://developer.apple.com/documentation/swift/rawrepresentable) 437 | 438 | Swift에서 열거형이 하나 이상의 연관 값을 가질 때 이 값의 rawValue를 정의하도록 만들어주는 프로토콜이 RawRepresentable Protocol입니다. (rawValue를 지원합니다.) 439 | RawRepresentable Protocol을 채택하여 타입을 rawValue로 변환하고 그 반대의 변환도 지원합니다. 440 | 특정 rawValue를 enum의 case로 매칭시켜 서로에게 대응할 수 있습니다. (Bool(rawValue) <-> Enum) 441 | 442 | 아래 코드를 보고 이해해 봅시다. 443 | 444 | ```swift 445 | enum UserPreference: RawRepresentable { 446 | case enabled // true가 매칭되는 case 447 | case disabled // false가 매칭되는 case 448 | case notSet // nil이 매칭되는 case 449 | 450 | init(rawValue: Bool?) { 451 | // rawValue를 switch문과 함께 쓸 수 있는 이유는 rawValue 타임을 옵셔널로 받기 때문입니다. 452 | // 옵셔널은 enum이기 때문에 switch문과 함께 쓸 수 있습니다. 453 | switch rawValue { 454 | case true?: self = .enabled 455 | case false?: self = .disabled 456 | default: self = .notSet 457 | } 458 | } 459 | 460 | // RawRepresentable protocol을 따르면 rawValue를 구현해야 합니다. 461 | // rawValue 프로퍼티로 enum case에 대응하는 rawValue를 리턴해야 합니다. 462 | var rawValue: Bool? { 463 | switch self { 464 | case .enabled: return true 465 | case .disabled: return false 466 | case .notSet: return nil 467 | } 468 | } 469 | } 470 | ``` 471 | 472 | 위와 같이 optional Bool을 enum과 함께 사용해 세 가지 상태를 표현할 때 컴파일 타임 안전성도 보장 받고 더 여러 상황에 유용합니다. 473 | 하지만 새로운 타입(enum)을 만드는 것이기 때문에 코드를 더럽힐 가능성이 있으니 주의해야 합니다. 474 | 475 | ## Force unwrapping guidelines 476 | 옵셔널을 강제 언래핑 했을 때 옵셔널이 nil이라면 충돌이 발생합니다. 477 | 아래 코드와 같이 !를 사용해 옵셔널을 강제 언래핑합니다. 478 | 479 | ```swift 480 | let forceUnwrappedUrl = URL(string: "https://www.the.com")! 481 | ``` 482 | 483 | 강제 언래핑은 실패 시 앱이 충돌하기 때문에 사용을 최대한 피합시다. (그냥 쓰지 맙시다.) 484 | 485 | 강제 언래핑으로 충돌을 발생시키기보다 충돌이 발생할 수 있는 경우라면, 충돌 메뉴얼을 만들어 더 디버깅에 필요한 정보를 제공하는 방식이 바람직합니다. 486 | fatalerror 함수를 사용해서 충돌을 메뉴얼화해 봅시다. 487 | 488 | ```swift 489 | guard let url = URL(string: path) else { 490 | fatalerror("Could not create url on \(#line) in \(#function)") 491 | } 492 | ``` 493 | 494 | ## Taming implicity unwrapped optionals 495 | Implicity unwrapped optionals(IUO)은 옵셔널이지만 언래핑하지 않고도 내부 값에 접근할 수 있는 옵셔널입니다. 496 | 옵셔널 내부 값에 접근할 때마다 옵셔널 언래핑을 하면 코드가 길어지고 가독성이 떨어지기 때문에 프로그램 특정 시점 이후로 옵셔널에 값이 항상 있다면 IUO를 사용할 수 있습니다. 497 | 498 | 하지만... 프로그램 특정 시점 이후로 옵셔널에 값이 항상 있다고 어떻게 장담할지 모르겠습니다. 499 | 500 | 일단 사용 방법은 아래와 같습니다. 501 | 502 | ```swift 503 | let lastName: String! = "Smith" 504 | ``` 505 | 506 | ## Summary 507 | - Optionals are enums with syntactic sugar sprinkled over them 508 | - You can pattern match on optionals 509 | - Patter match on multiple optionals at once by putting them inside a tuple 510 | - You can use nil-coalescing(??) to fall back to default values 511 | - Use optional chaining to dig deep into optional values 512 | - You can use nil-coalescing(??) to transform an optional Boolean into a regular Boolean 513 | - You can transform an optional Boolean into an enum for three explicit states 514 | - Return optional strings instead of empty strings when a value is expected 515 | - Use force unwrapping only if your program can't recover from a nil value 516 | - Use force unwrapping when you want to delay error handling, such as when prototyping 517 | - It's safer to force unwrap optionals if you know better than the compiler 518 | - Use implicity unwrapped optionals for properties that are instantiated right after initialization 519 | -------------------------------------------------------------------------------- /Modeling-data-with-enums.md: -------------------------------------------------------------------------------- 1 | # Modeling data with enums 2 | 3 | ## This chapter covers 4 | - How enums are an alternative to subclassing 5 | - Using enums for polymorhism 6 | - Learning how enums are "or" types 7 | - Modeling data with enums instead of structs 8 | - How enums and structs are algebraic types 9 | - Converting structs to enums 10 | - Safely handling enums with raw values 11 | - Converting strings to enums to create robust code 12 | 13 | ## Or vs. and 14 | 15 | 열거형은 "or" 타입으로 여겨집니다. 16 | 데이터를 모델링 할 때 "or" 성격이 보인다면 열거형을 고려합시다. 17 | 18 | 아래 코드는 채팅 서비스의 메세지 데이터를 모델링한 코드입니다. 19 | 메세지 데이터는 보내는 메세지, 채팅방 참여 메세지, 채팅방 퇴장 메세지, 풍선 이모티콘 중 하나의 상태를 가집니다. 20 | 21 | 먼저 구조체를 통해 메세지 데이터를 만든 코드를 살펴봅시다. 22 | 23 | ```swift 24 | import Foundation 25 | 26 | struct Message { 27 | let userId: String 28 | let contents: String? 29 | let date: Date 30 | 31 | let hasJoined: Bool 32 | let hasLeft: Bool 33 | let isBeingDrafted: Bool 34 | let isSendingBalloons: Bool 35 | } 36 | 37 | let joinMessage = Message( 38 | userId: "1", 39 | contents: nil, 40 | date: Date(), 41 | hasJoined: true, 42 | hasLeft: false, 43 | isBeingDrafted: false, 44 | isSendingBalloons: false 45 | ) 46 | 47 | let brokenMessage = Message( 48 | userId: "1", 49 | contents: "Hi there", 50 | date: Date(), 51 | hasJoined: true, 52 | hasLeft: true, 53 | isBeingDrafted: false, 54 | isSendingBalloons: false 55 | ) 56 | ``` 57 | 58 | 위와 같이 구조체로 모델을 만드는 방식이 일반적으로 떠올리는 메세지 데이터 모델입니다. 59 | 열거형이 "or" 개념이라면 구조체는 "And" 개념입니다. 60 | 61 | 우리가 만드는 메세지 데이터는 본인이 보낸 텍스트, 채팅방 참여 텍스트, 채팅방 퇴장 텍스트, 풍선 이모티콘이어야 합니다. 62 | 하지만 구조체는 여러 형태의 값을 만들 가능성이 있습니다. 63 | 예를 들어 hasJoined와 hasLeft가 동시에 true인 요상한 객체를 만들 수 있습니다. 64 | 만약 메세지 데이터의 유효성을 검사하려 하더라도, 구조체의 내부값이 잘못됐다는 사실을 컴파일 타임이 아닌 런타임에 알 수 밖에 없습니다. 65 | 66 | 이는 결국 버그로 이어집니다. 67 | 68 | 우린 열거형을 사용해 객체의 상태를 한정시키고 유효하지 않은 데이터를 컴파일 타임에 체크할 수 있습니다. 69 | 데이터 모델링에서 상호 배타적인(or) 성격을 가졌다면 구조체보다는 열거형을 고려합시다. 70 | 열거형과 함께 튜플을 사용하면 더 복잡한 데이터를 표현하기 쉽습니다. 71 | 72 | 아래 코드로 확인해 봅시다. 73 | 74 | ```swift 75 | import Foundation 76 | 77 | enum Message { 78 | case text 79 | case draft 80 | case join 81 | case leave 82 | case ballon 83 | } 84 | ``` 85 | 86 | ```swift 87 | import Foundation 88 | 89 | // 2. with value (enum + tuple) 90 | enum Message { 91 | case text(userId: String, contents: String, date: Date) 92 | case draft(userId: String, date: Date) 93 | case join(userId: String, date: Date) 94 | case leave(userId: String, date: Date) 95 | case ballon(userId: String, date: Date) 96 | } 97 | 98 | let textMessage = Message.text(userId: "2", contents: "Bonjour", date: Date()) 99 | let joinMessage = Message.join(userId: "2", date: Date()) 100 | ``` 101 | 102 | 이전 구조체와 달리 열거형은 다섯가지 케이스 중 하나를 선택했다면 해당 케이스에서 지원하는 튜플 속 프로퍼티만 요구합니다. 103 | 해당 요구사항을 따르지 않으면 당연히 컴파일 타임에 오류가 발생합니다. 104 | 열거형으로 메세지 데이터를 만들며 어떤 상황(case)에 어떤 데이터가 필요한지 명시하고 컴파일 타임에 검사할 수 있게 됩니다. 105 | 106 | 여기서 "or" 성격의 열거형에 강점이 드러납니다. 107 | 108 | 물론 열거형은 필연적으로 switch 구문을 요구합니다. 반복적인 switch 구문은 중복되는 코드를 발생시키기도 합니다. 109 | 우린 if case let 구문으로 반복적인 switch 구문을 줄일 수 있습니다. 110 | 111 | if case let 구문을 사용해 반복적인 switch 문을 피할 수 있습니다. 112 | 아래 코드를 확인해 봅시다. 113 | 114 | ```swift 115 | import Foundation 116 | 117 | if case let Message.text(userId: id, contents: contents, date: date) = { 118 | print("Received: \(contents)") 119 | } 120 | ``` 121 | 122 | 위 코드에서 userId와 date 변수는 사용하지 않기 때문에 와일드카드(_)를 사용해 무시할 수 있습니다 123 | 124 | ```swift 125 | import Foundation 126 | 127 | if case let Message.text(_, contents: contents, _) = { 128 | print("Received: \(contents)") 129 | } 130 | ``` 131 | 132 | 살펴봤듯이 열거형은 컴파일 타임에 이점이 있습니다. 하지만 열거형의 case가 하나뿐이라면 구조체가 더 좋은 선택일 수 있습니다. 133 | 134 | 본인이 작성한 코드에 구조체가 보인다면 구조체 속 프로퍼티들을 사용되는 케이스 별로 그룹화해 봅시다. 135 | 아래 메세지 구조체의 프로퍼티를 text, draft, join, leave, ballon 케이스 중 사용되는 부분에 적절히 그룹화한걸 볼 수 있습니다. 136 | 137 | ```swift 138 | struct Message { 139 | let userId: String 140 | let contents: String? 141 | let date: Date 142 | 143 | let hasJoined: Bool 144 | let hasLeft: Bool 145 | let isBeingDrafted: Bool 146 | let isSendingBalloons: Bool 147 | } 148 | 149 | enum Message { 150 | case text(userId: String, contents: String, date: Date) 151 | case draft(userId: String, date: Date) 152 | case join(userId: String, date: Date) 153 | case leave(userId: String, date: Date) 154 | case ballon(userId: String, date: Date) 155 | } 156 | ``` 157 | 158 | 구조체의 프로퍼티가 그룹화 된다면 열거형으로 구조체를 수정해 봅시다. 열거형이 더 좋은 선택지가 됩니다. 159 | 160 | ## Enums for polymorphism 161 | 162 | Polymorphism(다형성) means that a single function, method, array, dictionary - you name it - can work with different types. 163 | 164 | ```swift 165 | let arr: [Any] = [Date(), "aa", 789] 166 | ``` 167 | 168 | 위 코드와 같이 Any 타입 배열로 다형성을 만들 수도 있습니다. 하지만 Any 타입의 무분별한 사용은 피하는게 좋겠습니다. 169 | Any가 어떤 타입으로 표현될지 컴파일 타임에 알 수 없고 런타임에서야 알게 됩니다. 170 | 171 | Any 타입이 쓰이는 경우는 서버로부터 unknown data를 받을 때 정도입니다. 172 | 173 | 열거형을 사용하면 다형성을 구현함과 동시에 컴파일 타임 안전성을 보장받을 수 있습니다. 174 | 열거형이 제공한는 컴파일 타임 안전성은 열거형에 있는 케이스의 대응 여부를 컴파일러가 체크하는 것입니다. 175 | 열거형의 케이스에 대응하지 않아도 컴파일 타임 에러가 발생하고 열거형 케이스가 요구하는 데이터를 전달하지 않아도 컴파일 에러가 발생합니다. 176 | 177 | 열거형이 컴파일 타임 안전성을 보장한다는건 알겠는데 열거형이 다형성을 이룬다는 건 무슨 이야기일까요? 178 | Date 타입과 Range 타입을 같은 배열에 저장해야 하는 경우를 살펴봅시다. 179 | 180 | ```swift 181 | import Foundation 182 | 183 | // enum과 연관값 184 | enum DateTpye { 185 | case singleDate(Date) 186 | case dateRange(Range) 187 | } 188 | 189 | let now = Date() 190 | let hourFromNow = Date(timeIntervalSinceNow: 3600) 191 | 192 | // enum을 통한 다형성 193 | let dates: [DateType] = [ 194 | DateType.singleDate(now), 195 | DateType.dateRange(now..을 각 케이스 별 튜플에 넣어 다형성을 이루게 됩니다. 211 | 212 | ## Enums instead of subclassing 213 | 214 | 개발을 하다보면 클래스를 사용 서브클래싱으로 데이터 계층을 만드는 경우가 많습니다. 215 | 216 | 하지만 이때 자식 클래스에 어울리지 않는 코드를 부모 클래스가 가졌다면 해당 코드를 어쩔 수 없이 자식 클래스에 강제해야 하는 경우가 생깁니다. 217 | 기능이 변경되거나 추가되었을 때 자식 클래스를 만들며 상속 받는 부모 클래스와 어울리지 않을 수 있습니다. 218 | 219 | 열거형을 사용하면 새로운 요구사항에 대응하기 더 쉽습니다. 220 | 221 | 여러 운동 객체(Run, Cylcle)를 생성한다고 가정해 봅시다. 222 | 서브 클래싱 방식으로 구현한다면 Workout 부모 클래스를 만들고 여러 운동 객체를 Workout 클래스의 자식 클래스로 만들것 입니다. 223 | Workout 부모 클래스의 프로퍼티로 id, startTime, endTime, distance가 있습니다. 그리고 Run, Cycle 자식 클래스는 Workout 클래스의 프로퍼티가 모두 필요합니다. 224 | Workout 클래스는 부모 클래스로서 부족함이 없습니다. 225 | 226 | 하지만 id, repetitions, date 프로퍼티만 요구하는 Pushups 객체가 추가된다면 문제가 생깁니다. 227 | Workout 부모 클래스에서 id를 제외한 모든 프로퍼티는 새로 추가된 Pushups 객체에 어울리지 않게 됩니다. 228 | 229 | 만약 여기서 Workout 부모 클래스를 수정한다면 id 프로퍼티를 제외한 나머지 프로퍼티는 삭제됩니다. 230 | 너무 많은 수정이 따르게 됩니다. 231 | 232 | 심지어 id를 요구하지 않는 abs 객체가 추가된다면 더 이상 Workout 부모 클래스는 부모 클래스로서 역할하지 못합니다. 233 | 서브클래싱 방식은 자식 클래스의 중복을 줄일 수 있지만 변경에 용이하지 못합니다. 234 | 235 | 열거형을 사용하면 서브 클래싱과 달리 변경에 용이합니다. 236 | 아래 코드는 열거형을 사용해 운동 객체를 구현한 코드입니다. 237 | 238 | ```swift 239 | enum Workout { 240 | case run(Run) 241 | case cycle(Cycle) 242 | case pushups(Pushups) 243 | } 244 | ``` 245 | 246 | 여기서 abs가 추가된다면 아래 코드와 같이 간단히 추가할 수 있습니다. 247 | 248 | ```swift 249 | enum Workout { 250 | case run(Run) 251 | case cycle(Cycle) 252 | case pushups(Pushups) 253 | case abs(Abs) 254 | } 255 | ``` 256 | 257 | 물론 서브 클래싱을 사용한다면 공통 프로퍼티나 함수를 부모 클래스에 넣어 자식 클래스들에서의 중복을 줄일 수 있습니다. 258 | 하지만 기능이 추가되거나 변경되면 부모 클래스를 리팩터링해야 합니다. 259 | 열거형을 사용한다면 기능 확장에 추가적인 리팩터링이 필요 없습니다. 260 | 261 | These are trade-offs you'll have to make. If you can lock down your data model to a fixed, manageable number of cases, enums can be a good choice. 262 | 263 | ## Algebraic data types 264 | 265 | Algebraic data에는 sum types와 product types이 있습니다. 266 | sum types은 "or"로 설명되고 열거형이 해당합니다. product types은 "and"로 설명되고 구조체와 튜플이 해당합니다. 267 | sum types의 열거형은 고정된 개수의 데이터를 표현합니다. product types은 여러 개수의 데이터를 표현합니다. 268 | 269 | 데이터 모델링할 때 데이터가 표현할 수 있는 상태의 수를 체크하는 게 중요합니다. 270 | 데이터가 표현할 수 있는 상태가 많을수록 표현할 수 있는 타입을 추론하기 어렵습니다. 271 | 272 | ```swift 273 | enum PaymentType { 274 | case invoice 275 | case creditcard 276 | case cash 277 | } 278 | 279 | struct PaymentStatus { 280 | let paymentDate: Date? 281 | let isRecurring: Bool 282 | let paymentType: PaymentType 283 | } 284 | ``` 285 | 286 | 위와 같이 PaymentStatus를 만든다면 2가지(Bool)와 3가지(PaymentType)를 곱하여 6가지 변화를 가지는 데이터로 볼 수 있습니다. 287 | 288 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/1ab4c111-7b7b-4bab-8261-054d0cea4b15) 289 | 290 | 이를 sum types인 열거형을 통해 리팩토링한다면 3가지 변화를 가지는 데이터로 바꿀 수 있습니다. 291 | 292 | ```swift 293 | enum PaymentStatus { 294 | case invoice(paymentDate: Date?, isRecurring: Bool) 295 | case creditcard(paymentDate: Date?, isRecurring: Bool) 296 | case cash(paymentDate: Date?, isRecurring: Bool) 297 | } 298 | ``` 299 | 300 | 위와 같이 수정한다면 PaymentStatus 타입 하나만 다룰 수 있다는 장점이 있습니다. 301 | 302 | ## A safer use of strings 303 | 304 | string과 열거형을 함께 사용하는 경우는 흔합니다. 305 | 열거형에서 raw value를 추가할 수 있습니다. 306 | 이때 모든 케이스스는 raw value를 가져야 합니다. 307 | raw value로는 String, Char, Int, float만 가능합니다. 308 | 309 | ```swift 310 | enum Currency: String { 311 | case euro = "euro" 312 | case usd = "usd" 313 | case gbp = "gbp" 314 | } 315 | ``` 316 | 317 | 열거형을 raw value로 사용할 때는 아래와 같이 간략히 사용할 수도 있습니다. 318 | 319 | ```swift 320 | enum Currency: String { 321 | case euro 322 | case usd 323 | case gbp 324 | } 325 | ``` 326 | 327 | 지금까지 보던 열거형의 연관 값(튜플)들은 런타임에 정의됩니다. 하지만 열거형을 raw values로 사용하면 해당 값은 컴파일 타임에 정의됩니다. 328 | 329 | 열거형을 raw values와 사용할 때 코드 작성자에 의한 버그에 주의해야 합니다. 330 | 열거형을 raw values로 사용하면 올바르지 않은 값을 넣어도 컴파일러가 알아차리지 못합니다. 331 | 332 | 아래 코드와 같이 euro를 eur로 잘못 적었다면 컴파일러는 euro를 eur로 인식해서 버그가 숨어들게 됩니다. 333 | 334 | ```swift 335 | enum Currency: String { 336 | case euro = "eur" 337 | case usd 338 | case gbp 339 | } 340 | 341 | let parameters = ["filter": currency.rawValue] 342 | ``` 343 | 344 | 다음과 같은 버그를 방지하기 위해 currency 열거형형의 raw values를 무시하고 raw values가 필요할 때 데이터를 다시 생성하도록 아래와 같이 구현합니다. 345 | 346 | ```swift 347 | enum Currency: String { 348 | case euro = "eur" 349 | case usd 350 | case gbp 351 | } 352 | 353 | let parameters = [String: String] 354 | switch currency { 355 | case .euro: parameters = ["filter": "euro"] 356 | case .usd: parameters = ["filter": "usd"] 357 | case .gbp: parameters = ["filter": "gbp"] 358 | } 359 | ``` 360 | 361 | 위 코드와 같이 열거형의 raw value를 무시하고 해당 값이 필요할 때마다 switch로 얻는다면 버그를 방지할 수 있습니다. 362 | 다시 말해 열거형의 raw value를 사용하면 결과적으로 컴파일러 타임 안전성을 잃게 됩니다. 363 | 364 | 열거형을 사용할 때 string과 패턴 매칭하는 경우도 빈번합니다. 365 | 366 | ```swift 367 | func iconName(for fileExtension: String) -> String { 368 | switch fileExtension { 369 | case "jpg": return "assetIconJpeg" 370 | case "bmp": return "assetIconBitmap" 371 | case "gif": return "assetIconGif" 372 | default: return "assertIconUnknown" 373 | } 374 | } 375 | 376 | iconName(for: "jpg") 377 | iconName(for: "JPG") 378 | ``` 379 | 380 | 위에서 jpg를 iconName 함수에 넣었을 때는 성공적으로 패턴 매칭이 됩니다. 381 | 하지만 JPG에 매칭되는 결과는 없기에 버그가 발생하게 됩니다. 코드 작성자가 실수할 가능성까지 버그로 봅니다. 382 | 이를 enum을 통해 개선할 수 있습니다. 383 | 384 | ```swift 385 | enum ImageType: String { 386 | case jpg 387 | case bmp 388 | case gif 389 | 390 | init?(rawValue: String) { 391 | switch rawValue.lowercasted() { 392 | case "jpg", "jpeg": self = .jpg 393 | case "bmp", "bitmap": self = .bmp 394 | case "gif", "gifv": self = .gif 395 | default: return nil 396 | } 397 | } 398 | } 399 | 400 | func iconName(for fileExtension: String) -> String { 401 | guard let imageType = ImageType(rawValue: fileExtension) else { 402 | return "assertIconUnknown" 403 | } 404 | 405 | switch fileExtension { 406 | case "jpg": return "assetIconJpeg" 407 | case "bmp": return "assetIconBitmap" 408 | case "gif": return "assetIconGif" 409 | default: return "assertIconUnknown" 410 | } 411 | } 412 | 413 | iconName(for: "jpg") 414 | iconName(for: "JPG") 415 | iconName(for: "JPEG") 416 | ``` 417 | 418 | 이제는 ImageType에 case가 추가되면 컴파일러가 알려줍니다. 위의 코드는 대문자에 대응하고 다른 jpg, jpeg와 같이 다른 명칭에도 대응하도록 개선한 코드입니다. 419 | 420 | ## Summary 421 | 422 | 1. Enums are sometimes an alternative to subclassing, allowing for a flexible architecture. 423 | 2. Enums give you the ability to catch problem at compile time instead of runtime. 424 | 3. You can use enums to group properties together. 425 | 4. Enums are sometimes called sum types, based on algebratic data types. 426 | 5. Structs can be distributed over enums. 427 | 6. When working with enum's raw values, you forego catching problems at compile time. 428 | 7. Handling strings can be made safer by converting them to enums. 429 | 8. When converting a string to an enum, grouping cases and using a lowercased strings makes conversion easier. 430 | -------------------------------------------------------------------------------- /Protocol_extensions.md: -------------------------------------------------------------------------------- 1 | # Protocol extensions 2 | 3 | ## This chapter covers 4 | - Flexibly modeling data with protocols instead of subclasses 5 | - Adding default behavior with protocol extensions 6 | - Extending regular types with protocols 7 | - Working with protocol inheritance and default implementations 8 | - Applying protocol composition for highly flexible code 9 | - Showing how Swift prioritizes method calls 10 | - Extending types containing associated types 11 | - Extending vital protocols, such as Sequence and Collection 12 | 13 | ## Class inheritance vs. protocol inheritance 14 | 15 | 객체지향 프로그래밍은 서브 클래싱을 사용해 전형적으로 데이터를 모델링하는 방법입니다. 16 | 17 | 하지만 서브 클래싱이 형성하는 수직적 데이터 구조는 다소 엄격한 성격을 지닙니다. 18 | 클래스 기반의 상속을 대신해서 스위프트는 프로토콜 기반의 상속을 제공합니다. 19 | 20 | 클래스 기반의 상속은 수직적 데이터 구조를 만들고 프로토콜 기반의 상속은 수평적 데이터 구조를 이룹니다. 21 | 22 | 클래스 기반의 상속에서 부모 클래스를 상속한 자식 클래스에 새로운 함수를 추가하거나 부모 클래스의 함수를 오버라이드 할 수 있습니다. 23 | 24 | 네트워크 호출을 위한 URLRequest 타입을 생성하는 RequestBuilder 타입을 서브 클래싱 방식으로 만들고 프로토콜을 사용한 방식으로도 만들어봅시다. 25 | 26 | 먼저 서브 클래싱 방식으로 RequestBuilder 타입을 구현해 봅시다. 27 | 28 | RequestBuilder 클래스를 최상위 부모 클래스로 두고, RequestBuiler 클래스를 상속하는 RequestHeaderBuilder 자식 클래스를 만들어 RequestHeaderBuilder 클래스에서 header를 붙이는 작업을 추가합니다. 29 | 30 | 또한 RequestHeaderBuilder 클래스를 상속한 EncryptedRequestHeaderBuilder 자식 클래스를 만들고 네트워크 요청의 데이터를 암호화하는 기능까지 추가합니다. 31 | 32 | 이처럼 서브 클래싱을 하며 자식 클래스에 필요한 기능을 추가하는 방식으로 데이터 구조를 형성합니다. 33 | 34 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/5346bc05-2af9-49be-9166-d9a16b0f775a) 35 | 36 | 이처럼 클래스 기반 상속은 수직적(vertical) 데이터 구조를 형성합니다. 37 | 38 | 하지만 클래스 기반 상속과 달리 프로토콜 기반의 상속을 통해 RequestBuilder 타입을 만든다면 수평적(horizontal) 데이터 구조를 형성할 수 있습니다. 39 | 40 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/809509e1-51a7-4ad6-817a-f82d9ad1dd78) 41 | 42 | RequestBuilder를 비롯해 나머지 프로토콜을 따르면 해당 프로토콜에서 제공하는 함수를 사용할 수 있습니다. 43 | enum, struct, class, subclass 상관없이 모두 프로토콜을 따를 수 있습니다. 44 | 45 | 프로토콜을 통해 기능을 분리하여 특정 기능이 필요할 때 해당 기능을 지원하는 프로토콜을 채택하는 방식으로 새로운 기능(함수)을 추가할 수 있습니다. 46 | 또한 프로토콜 상속을 통해서 새로운 함수를 추가하고 부모 프로토콜의 함수를 오버라이드 할 수 있습니다. 47 | 48 | 프로토콜로 기능을 분리하는 방식은 코드 유연성과 재사용성을 높입니다. 49 | 50 | 서브 클래싱 방법은 하나의 부모 클래스에 제한된 자식 클래스를 가지지만, 프로토콜을 활용한다면 하나의 부모 클래스에 제한되지 않을 수 있습니다. 51 | 52 | **Creating a protocol extension** 53 | 54 | 프로토콜에서는 함수를 정의할 수만 있고, 프로토콜을 채택한 타입에서 프로토콜에 정의된 함수를 구현해야 합니다. 55 | 56 | 하지만 프로토콜 확장을 통해 프로토콜에서 정의한 함수의 구현부(default implementation)를 제공할 수 있습니다. 57 | 58 | 아래 코드로 살펴봅시다. 59 | 60 | ```swift 61 | protocol RequestBuilder { 62 | var baseURL: URL { get } 63 | // makeRequest 함수 정의 64 | func makeRequest(path: String) -> URLRequest 65 | } 66 | 67 | extension RequestBuilder { 68 | // makeRequest 함수 구현 69 | func makeRequest(path: String) -> URLRequest { 70 | let url = baseURL.appendingPathComponent(path) 71 | var request = URLRequest(url: url) 72 | request.httpShouldHandleCookies = false 73 | request.timeoutInterval = 30 74 | } 75 | } 76 | ``` 77 | 78 | 위 코드와 같이 프로토콜 함수에서 정의한 함수의 구현부(default implementation)를 제공하기 위해서는 프로토콜을 확장하여 함수 구현부를 제공해야 합니다. 79 | 80 | 또한 확장에서 구현하는 함수 구현부에서는 프로토콜의 프로퍼티에 접근할 수 있습니다. 81 | 82 | 이제 RequestBuilder 프로토콜을 채택한 타입에서는 makeRequest 함수 구현 없이 RequestBuiler 프로토콜 확장에서 제공하는 makeRequest 함수 구현부를 사용할 수 있습니다. 83 | 84 | 물론 RequestBuiler 프로토콜을 채택한 타입에서 makeRequest 함수를 직접 구현하여 사용할 수 있습니다. 85 | 이때는 RequestBuiler 프로토콜의 확장에서 구현된 makeRequest 함수가 아닌 해당 타입에서 직접 구현한 makeRequest 함수를 호출합니다. 86 | 87 | 프로토콜 확장에서 제공하는 함수 구현부를 오버라이드하는 내용은 이후에 더 살펴볼 예정입니다. 88 | 89 | 아래 코드는 프로토콜 확장에서 제공하는 함수의 구현부를 해당 프로토콜을 따르는 타입에서 사용하는 코드입니다. 90 | 91 | ```swift 92 | struct BikeRequestBuilder: RequestBuilder { 93 | let baseURL: URL = URL(string: "https://www.biketriptracker.com")! 94 | } 95 | 96 | let bikeRequestBuilder = BikeRequestBuilder() 97 | let request = bikeRequestBuilder.makeRequest(path: "/trips/all") 98 | print(request) // https://www.biketriptracker.com/trips/all 99 | ``` 100 | 101 | BikeRequestBuilder 타입은 RequestBilder 프로토콜을 채택하여 makeRequest 함수의 구현부(default implementation)를 사용할 수 있습니다. 102 | 103 | **Multiple extension** 104 | 105 | 클래스 상속의 경우 자식 클래스가 하나의 부모 클래스만 상속받을 수 있습니다. 106 | 107 | 하지만 클래스 상속과 달리, 프로토콜의 경우에는 하나의 타입이 여러 프로토콜을 채택할 수 있습니다. 108 | 다시 말해 다중 프로토콜 채택이 가능합니다. 109 | 110 | 아래 코드로 살펴봅시다. 111 | 112 | ```swift 113 | enum ResponseError: Error { 114 | case invalidResponse 115 | } 116 | 117 | protocol ResponseHandler { 118 | func validate(response: URLResponse) throws 119 | } 120 | 121 | extension ResponseHandler { 122 | func validate(response: URLResponse) throws { 123 | guard let httpresponse = response as? HTTPURLResponse else { 124 | throw ResponseError.invalidResponse 125 | } 126 | } 127 | } 128 | 129 | // BikeAPI 타입이 여러 프로토콜을 동시에 채택할 수 있습니다. 130 | class BikeAPI: RequestBuilder, ResponseHandler { 131 | let baseURL: URL = URL(string: "https://www.biketriptracker.com")! 132 | } 133 | ``` 134 | 135 | ## Protocol inheritance vs. Protocol composition 136 | 137 | 지금까지 프로토콜 확장을 통해 프로토콜 함수의 구현부(default implementation)를 제공하는 방법을 살펴봤습니다. 138 | 139 | 지금부터는 데이터 모델링에 유용하게 쓰이는 프로토콜 상속과 프로토콜 컴포지션에 대해 살펴보겠습니다. 140 | 141 | 이메일을 보내는 SMTP 프레임워크를 구현하며 프로토콜 상속과 프로토콜 컴포지션을 살펴보고 차이점을 확인해 보겠습니다. 142 | 143 | 먼저 프로토콜 상속으로 SMTP를 구현하고 이후 프로토콜 컴포지션 방식으로 구현하여 trade-offs를 확인하겠습니다. 144 | 145 | SMTP의 기본이 되는 Email 구조체와 Mailer 프로토콜을 아래 코드와 같이 구현했습니다. 146 | 147 | ```swift 148 | // MailAddress가 단순 String 보다 더욱 의도를 드러냅니다. 149 | struct MailAddress { 150 | let value: String 151 | } 152 | 153 | struct Email { 154 | let subject: String 155 | let body: String 156 | let to: [MailAddress] 157 | let from: MailAddress 158 | } 159 | 160 | protocol Mailer { 161 | func send(email: Email) 162 | } 163 | 164 | // Mailer 프로토콜을 확장하여 send 함수의 구현부를 제공합니다. 165 | extension Mailer { 166 | func send(email: Email) { 167 | // Omitted: Connect to server 168 | // Omitted: Submit email 169 | print("Email is sent!") 170 | } 171 | } 172 | ``` 173 | 174 | 이제 Mailer 프로토콜의 확장에서 구현한 함수의 구현부(default implementation)에 메일을 보내기 전에 메일의 유효성 검사를 추가하려 합니다. 175 | 176 | 그러나 Mailer 프로토콜을 따르는 모든 타입에서 메일의 유효성 검사를 요구하진 않습니다. 177 | 특정 타입에서만 Mailer 프로토콜을 따르며 추가로 메일의 유효성 검사를 요구하는 상황입니다. 178 | 179 | 먼저 프로토콜 상속으로 위와 같은 조건을 구현해 봅시다. 180 | 181 | Mailer 프로토콜을 상속받은 ValidatingMailer 자식 프로토콜을 생성하여 ValidatingMailer 프로토콜에 유효성 검사 기능을 추가합니다. 182 | 183 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/50192d93-3627-425d-999a-83c05000926e) 184 | 185 | 아래 코드로 살펴봅시다. 186 | 187 | ```swift 188 | protocol Mailer { 189 | func send(email: Email) 190 | } 191 | 192 | // Mailer 프로토콜을 확장하여 send 함수의 구현부를 제공합니다. 193 | extension Mailer { 194 | func send(email: Email) { 195 | // Omitted: Connect to server 196 | // Omitted: Submit email 197 | print("Email is sent!") 198 | } 199 | } 200 | 201 | protocol ValidatingMailer: Mailer { 202 | func send(email: Email) throws // 부모 프로토콜의 send 함수를 오버라이드 합니다. 203 | func validate(email: Email) throws 204 | } 205 | 206 | extension ValidatingMailer { 207 | func send(email: Email) throws { 208 | try validate(email: Email) 209 | // Connect to Server 210 | // Submit email 211 | print("Email validated and sent.") 212 | } 213 | 214 | func validate(email: Email) throws { 215 | // Check email address, and whether subject is missing. 216 | } 217 | } 218 | 219 | struct SMTPClient: ValidatingMailer { 220 | // Implementation omitted. 221 | } 222 | 223 | let client = SMTPClient() 224 | try? client.send( 225 | email: Email( 226 | subject: "Learn Swift", 227 | body: "Lorem ipsum", 228 | to: [MailAddress(value: "john@naver.com")], 229 | from: MailAddress(value: "Stranger@naver.com") 230 | ) 231 | ) 232 | ``` 233 | 234 | Mailer 프로토콜을 상속하여 ValidationMailer 자식 프로토콜에 validate 함수를 추가하고 Mailer 프로토콜의 send 함수를 오버라이드하고 있습니다. 235 | 236 | Mailer 프로토콜의 자식 프로토콜인 ValidatingMailer에서 send 함수를 오버라이드 했기 때문에 ValidationMailer 프로토콜을 확장하여 send 함수의 구현부를 제공해야 합니다. 237 | 238 | 프로토콜 상속에서 자식 프로토콜인 ValidatingMailer 프로토콜이 메일의 유효성 검사와 메일을 보내는 두 가지 성격의 일을 모두 처리해야 합니다. 239 | 이처럼 프로토콜 상속을 통해 데이터를 모델링할 때 프로토콜이 의미 단위로 분리되지 않는다는 단점이 존재합니다. 240 | 241 | 만약 메일 유효성 검사를 필요로 하지 않은 타입에서는 ValidatingMailer가 아닌 Mailer 프로토콜을 채택해야만 합니다. 242 | 243 | 그렇다면 프로토콜 상속이 아닌 프로토콜 컴포지션을 사용하면 어떨까요? 244 | 245 | 프로토콜 컴포지션 방식은 Mailer 프로토콜을 상속하는 ValidatingMailer 프로토콜을 생성하지 않고, 메일 유효성을 검사하는 독립된 MailValidator 프로토콜을 생성합니다. 246 | 247 | 이후 메일 유효성 검사 기능이 있어야 하는 타입에 Mailer 프로토콜과 함께 MailValidator 프로토콜을 다중 채택하여 메일 유효성 검사 기능을 제공합니다. 248 | 249 | 아래 코드는 독립된 MailValidator 프로토콜을 구현한 코드입니다. 250 | 251 | ```swift 252 | protocol MailValidator { 253 | func validate(email: Email) throws 254 | } 255 | 256 | extension MailValidator { 257 | func validate(email: Email) throws { 258 | // Omitted: Check email address, and whether subject is missing. 259 | } 260 | } 261 | 262 | struct SMTPClient: Mailer, MailValidator { 263 | // Implementation omitted. 264 | } 265 | ``` 266 | 267 | 이제는 Mailer 프로토콜에서 MailValidator 프로토콜의 존재를 모르고, MailValidator 프로토콜도 Mailer 프로토콜의 존재를 모릅니다. 268 | 269 | **Protocol intersection(교차점)** 270 | 271 | 두 프로토콜을 모두 채택한 타입은 두 프로토콜의 교차점에 위치하는 타입이라고 볼 수 있습니다. 272 | 273 | 두 프로토콜의 교차점에 위치하는 타입에 특정 기능을 제공하도록 교차점을 확장할 수 있습니다. 274 | 275 | 예를 들어 Mailer 프로토콜과 MailValidator 프로토콜을 모두 따르는 SMTPClient 타입이 두 프로토콜의 교차점에 있는 타입이라고 볼 수 있습니다. 276 | Mailer 프로토콜과 MailValidator 프로토콜의 교차점을 확장했다면 확장된 기능을 교차점에 있는 SMTPClient 타입이 사용할 수 있습니다. 277 | 278 | 두 프로토콜의 교차점을 확장하기 위해서는 **Self 키워드**를 통해 둘 중 하나의 프로토콜을 확장하여 나머지 한 프로토콜을 따르도록 해야 합니다. 279 | 280 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/6ef6ed4a-464f-4770-9111-da3d55f19aa6) 281 | 282 | 아래 코드는 Mailer 프로토콜과 MailValidator 프로토콜의 교차점을 확장한 코드입니다. 283 | 284 | ```swift 285 | extension MailValidator where Self: Mailer { 286 | func send(email: Email) throws { 287 | try validate(email: email) 288 | // Connect to server 289 | // Submit email 290 | print("Email Validated and sent.") 291 | } 292 | } 293 | ``` 294 | 295 | 두 프로토콜의 교차점을 확장하여 send 함수의 구현부(default implementation)를 제공합니다. 296 | 교차점의 확장에서 제공하는 send 함수는 Mailer 프로토콜에서 제공하는 send 함수를 오버라이드한 함수입니다. 297 | 298 | 위 코드에서 MailValidator 프로토콜을 확장하고 Mailer 프로토콜을 채택했지만, 반대로 Mailer 프로토콜을 확장하고 MailValidator 프로토콜을 채택해도 상관없습니다. 299 | 300 | 이제 Mailer 프로토콜과 MailValidator 프로토콜을 모두 채택하는 SMTPClient 타입에서 send 함수의 구현부는 두 프로토콜의 교차점에서 제공하게 됩니다. 301 | 302 | 프로토콜 교차점에서 기존 함수를 오버라이드하는 경우외에도 아래 코드처럼 새로운 함수를 추가할 수 있습니다. 303 | 304 | ```swift 305 | extension MailValidator where Self: Mailer { 306 | func send(email: Email) throws { 307 | try validate(email: email) 308 | // Connect to server 309 | // Submit email 310 | print("Email Validated and sent.") 311 | } 312 | 313 | // 두 프로토콜의 교차점을 확장하여 추가한 새로운 send(email:, at:) 함수 314 | func send(email: Email, at: Date) throws { 315 | try validate(email: email) 316 | // Connect to server 317 | // Submit email 318 | print("Email Validated and sent.") 319 | } 320 | } 321 | ``` 322 | 323 | 실제로 Mailer 프로토콜과 MailValidator 프로토콜을 모두 채택하여 두 프로토콜의 교차점에 있는 SMTPClient 타입에서 두 프로토콜의 교차점을 확장해 제공하는 함수들을 사용하는 모습을 살펴봅시다. 324 | 325 | ```swift 326 | struct SMTPClient: Mailer, MailValidator {} 327 | 328 | let client = SMTPClient() 329 | let email = Email( 330 | subject: "Learn Swift", 331 | body: "Lorem ipsum", 332 | to: [MailAddress(value: "john@naver.com")], 333 | from: MailAddress(value: "Stranger@naver.com") 334 | ) 335 | 336 | try? client.send(email: email) 337 | try? client.send(email: email, at: Date(timeIntervalSinceNow: 3600)) 338 | ``` 339 | 340 | 두 프로토콜을 채택한 타입도 두 프로토콜의 교차점에 있지만, 제네릭 타입에서 한 타입이 두 프로토콜로 타입 제약된 경우에도 해당 타입이 두 프로토콜의 교차점에 있다고 볼 수 있습니다. 341 | 342 | 따라서 두 프로토콜로 타입이 제약된 제네릭 타입에서도 교차점 확장의 함수들을 사용할 수 있습니다. 343 | 344 | 아래 코드로 살펴봅시다. 345 | 346 | ```swift 347 | // Mailer, MailValidator 프로토콜로 타입 제약된 제니릭 타입도 두 프로토콜 교차점을 확장한 기능을 사용 가능합니다. 348 | func submitEmail(sender: T, email: Email) where T: Mailer, T: MailValidator { 349 | try? sender.send(email: email, at: Date(timeIntervalSinceNow: 3600)) 350 | } 351 | ``` 352 | 353 | 프로토콜 상속과 달리 프로토콜 컴포지션을 사용하면 의미 단위로 코드를 분리할 수 있습니다. 354 | 355 | 하지만 너무 잘게 분리될 경우 오히려 단점으로 적용됩니다. 356 | 또한 프로토콜 상속과 달리 컴포지션 방식의 경우 여러 프로토콜을 다중 채택해야 합니다. 357 | 358 | 물론 프로토콜 상속은 단일 프로토콜을 채택하도록 하여 경직된 데이터 구조를 형성합니다. 359 | 360 | 프로토콜 상속과 프로토콜 컴포지션 방식의 균형을 맞춰 추상화를 이루도록 노력해야 합니다. 361 | 362 | ## Overriding priorities 363 | 364 | **Overriding a default implementation** 365 | 366 | 앞에서 프로토콜 확장으로 프로토콜에 선언된 함수의 구현부를 제공하지만, 해당 프로토콜을 채택한 타입에서 구현부를 제공하는 함수를 오버라이드 할 경우를 잠시 살펴봤습니다. 367 | 368 | 지금부터 더 자세히 알아봅시다. 369 | 370 | grow 함수를 가진 Tree 프로토콜을 구현하여 프로토콜 상속 과정을 살펴봅시다. 371 | 372 | Tree 프로토콜을 확장하여 grow 함수를 구현하고 Oak 구조체에서 Tree 프로토콜을 채택하려 합니다. 373 | 이때 Oak 구조체에서 grow 함수를 오버라이드하는 상황입니다. 374 | 375 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/16695ca4-c9ed-4c03-aa7e-75293a827af4) 376 | 377 | 만약 Oak 구조체에서 grow 함수를 오버라이드 하지 않는다면 Oak 구조체가 채택한 Tree 프로토콜의 grow 함수를 호출합니다. 378 | 379 | 하지만 Oak 구조체에서 Tree 프로토콜이 제공하는 grow 함수를 오버라이드 한다면 Oak 구조체에서 호출되는 grow 함수는 Oak 구조체에서 오버라이드한 grow 함수가 호출됩니다. 380 | (If a type implements the same method as the one on a protocol extension, Swift ignores the protocol extension's method) 381 | 382 | 프로토콜의 확장에서 구현한 함수를 프로토콜을 채택하는 타입에서 오버라이드 할 수 있지만, 반대로 프로토콜 확장을 통해 실제 타입의 함수를 오버라이드 할 수 없습니다. 383 | 384 | **Overriding with protocol inheritance** 385 | 386 | 지금부터는 프로토콜이 상속될 때 확장에 추가한 함수 구현부(default implementation)가 상속되는 규칙을 알아봅시다. 387 | 388 | 위에서 봤던 Tree 프로토콜을 Plant 프로토콜의 상속을 받는 자식 프로토콜로 만들고, Oak 구조체가 Tree 프로토콜을 채택하도록 구현하면 Oak 구조체가 호출하는 grow 함수가 무엇인지 살펴봅시다. 389 | 390 | **스위프트는 가장 특수화된 구현을 고르는 특성이 있습니다.** 391 | 392 | 여기서 특수화된 구현이란 자신의 타입과 가장 가까운 구현을 뜻합니다. 393 | 394 | 부모 프로토콜인 Plant 프로토콜과 Plant 프로토콜을 상속받는 Tree 자식 프로토콜이 있고, Oak 구조체가 Tree 프로토콜을 채택하는 상황에서 Plant 프로토콜, Tree 프로토콜, 그리고 Oak 구조체 모두 grow 함수를 오버라이드하고 있습니다. 395 | 396 | 이때 Oak 구조체에서 가장 가까운 grow 함수 구현부는 본인이 가진 grow 함수라고 볼 수 있습니다. 397 | 따라서 Oak 구조체에서 호출하는 grow 함수는 본인이 오버라이드한 함수입니다. 398 | 399 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/847f79b2-cf72-4f14-b17a-4959b7f37417) 400 | 401 | 만약 Plant 프로토콜과 Tree 프로토콜에만 grow 함수를 오버라이드 했다면, Tree 프로토콜을 채택하는 Oak 구조체에서 호출되는 grow 함수는 Tree 프로토콜의 grow 함수가 될 것입니다. 402 | 403 | Plant 프로토콜보다 Tree 프로토콜이 Oak 구조체 기준으로 가까운 구현(특수화된 구현)이기 때문입니다. 404 | 405 | 마지막으로 Plant 프로토콜에만 grow 함수를 구현했다면 Oak 구조체에서 호출되는 grow 함수는 Plant 프로토콜의 grow 함수가 됩니다. 406 | 407 | 프로토콜 확장이 함수 구현부(default implementation)를 제공할 때, 해당 프로토콜을 상속하고 오버라이드하는 과정을 아래 코드로 살펴봅시다. 408 | 409 | ```swift 410 | func growPlant(_ plant: P) { 411 | plant.grow() 412 | } 413 | 414 | protocol Plant { 415 | func grow() 416 | } 417 | 418 | extension Plant { 419 | func grow() { 420 | print("Growing a plant") 421 | } 422 | } 423 | 424 | protocol Tree: Plant {} 425 | 426 | extension Tree { 427 | func grow() { 428 | print("Growing a tree") 429 | } 430 | } 431 | 432 | struct Oak: Tree { 433 | func grow() { 434 | print("The mighty oak is growing") 435 | } 436 | } 437 | 438 | struct CherryTree: Tree {} 439 | 440 | struct KiwiPlant: Plant {} 441 | 442 | growPlant(Oak()) // The mighty oak is growing 443 | growPlant(CherryTree()) // Growing a tree 444 | growPant(KiwiPlant()) // Growing a plant 445 | ``` 446 | 447 | 클래스 상속에서의 오버라이드 규칙과 비슷하게 프로토콜 상속에서의 오버라이드도 유사하게 동작한다는 사실을 알 수 있습니다. 448 | 이와 같은 프로토콜의 동작은 클래스, 구조체, 그리고 열거형과 관계없이 동일하게 적용됩니다. 449 | 450 | ## Extending in two directions 451 | 452 | **Opting in to extensions** 453 | 454 | 특정 프로토콜의 요구사항을 모든 타입에서 원하지 않고 일부 타입에서만 원하는 경우가 많습니다. 455 | 저차원의 프로토콜을 확장할 경우 확장한 기능이 필요하지 않은 타입까지 제공될 수 있습니다. 456 | 457 | 따라서 프레임워크에 확장을 추가할 때는 항상 주의해야 합니다. 458 | 459 | 예를 들어 사용자의 동작을 분석하는 AnalyticsProtocol을 UIViewController에 채택하는 상황을 살펴봅시다. 460 | 또한 AnalyticsProtocol에서는 프로토콜 확장을 통해 함수 구현부(default implementation)까지 제공하고 있습니다. 461 | 462 | 이때 UIViewController 타입이 AnalyticsProtocol을 채택하면 모든 UIViewController에서 AnalyticsProtocol이 제공하는 기능을 사용하게 됩니다. 463 | 464 | 심지어 UIViewController 타입의 자식 클래스까지 AnalyticsProtocol의 기능을 제공하게 됩니다. 465 | 466 | AnalyticsProtocol의 기능은 모든 UIViewController에서 필요로 하지 않고, 일부 ViewController에 필요한 기능입니다. 467 | 468 | 개발자는 UIViewController 타입이 AnalyticsProtocol의 기능을 기본으로 가진다고 생각하지 못합니다. 469 | 만약 개발자가 AnalyticsProtocl의 기능을 인지하지 못하고 UIViewController 타입에 AnalyticsProtocol과 동일한 기능을 추가하는 충돌 상황까지 발생할 수 있습니다. 470 | 471 | 이와 같은 이슈를 해결하기 위해 확장을 접어야 합니다. (flip the extension) 472 | 473 | 확장을 접는다는 말은 프로토콜 교차점을 확장하는 것과 같은 맥락입니다. 474 | 475 | 다시 말해, AnalyticsProtocol과 UIViewController의 교차점을 확장하여 기존에 AnalyticsProtocol에서 제공하려는 기능을 추가하는 방법입니다. 476 | 477 | 아래 코드로 살펴봅시다. 478 | 479 | ```swift 480 | protocol AnalyticsProtocol { 481 | func track(event: String, parameters: [String: Any]) 482 | } 483 | 484 | // Not like this: 485 | extension UIViewController: AnalyticsProtocol { 486 | func track(event: String, parameters: [String: Any]) { // ...snip } 487 | } 488 | 489 | // But as follows: 490 | extension AnalyticsProtocol where Self: UIViewController { 491 | func track(event: String, parameters: [String: Any]) { // ...snip} 492 | } 493 | ``` 494 | 495 | 위 코드와 같이 UIViewController가 AnalyticsProtocol을 채택하지 않고 두 타입의 교차점을 확장하여 AnalyticsProtocol에서 제공하는 track 함수를 추가합니다. 496 | 497 | 이제 뷰 컨트롤러 중 UIViewController 타입과 AnalyticsProtocol을 모두 따르는 뷰 컨트롤러에서만 AnalyticsProtocol이 제공하는 track 함수와 함수 구현부를 사용할 수 있습니다. 498 | 499 | 모든 뷰 컨트롤러가 AnalyticsProtocol을 따르는 상황을 막을 수 있습니다. 500 | 501 | ```swift 502 | extension NewsViewController: UIViewController, AnalyticsProtocol { 503 | // ...snip 504 | 505 | override func viewDidAppear(_ animated: Bool) { 506 | super.viewDidAppear(animated) 507 | // UIViewController와 AnalyticsProtocl을 모두 채택해 두 타입의 교차점에 있기 때문에 교차점을 확장한 함수(track)를 사용할 수 있습니다. 508 | track("News.appear", params: [:]) 509 | } 510 | } 511 | ``` 512 | 513 | Extensions are not namespaced, so be careful with adding public extensions inside a framework, because implementers may not want their classes to adhere to a protocol by default. 514 | 515 | ## Extending with associated types 516 | 517 | 연관 값을 가진 프로토콜을 확장했을 때를 살펴봅시다. 518 | 519 | 배열(Array)에 중복 값을 제거하는 unique 함수를 적용하고 싶을 때를 예시로 살펴봅시다. 520 | 521 | 아래 코드와 같이 앞으로 구현할 unique 함수는 배열 속의 중복된 값을 제거하고 유일한 값만 가진 배열로 만듭니다. 522 | 523 | ```swift 524 | [3, 2, 1, 1, 2, 3].unique() // [3, 2, 1] 525 | ``` 526 | 527 | 배열은 Element라는 연관 값을 가진 구조체입니다. 528 | 529 | ```swift 530 | @frozen 531 | struct Array 532 | ``` 533 | 534 | 따라서 unique 함수도 Element 연관 값을 다뤄야 합니다. 535 | 536 | 먼저 배열을 확장하여 unique 함수를 추가해 보겠습니다. 537 | 538 | 이때 unique 함수에서 각 element들을 비교하기 위해서 Element 연관 값은 Equatable 프로토콜로 타입 제약되어야 합니다. 539 | 540 | 아래 코드로 살펴봅시다. 541 | 542 | ```swift 543 | extension Array where Element: Equatable { 544 | func unique() -> [Element] { 545 | var uniqueValues = [Element]() 546 | for element in self { 547 | if !uniqueValues.contains(element) { 548 | uniqueValues.append(element) 549 | } 550 | } 551 | return uniqueValues 552 | } 553 | } 554 | ``` 555 | 556 | Equatable로 Element 연관 값을 타입 제약했기 때문에 Element 연관 값의 타입은 Equatable 프로토콜을 따라야만 unique 함수를 사용할 수 있습니다. 557 | 558 | Element 연관 값이 Equatable 프로토콜을 따라야만 unique 함수를 사용할 수 있습니다. 559 | 만약 Equatable 프로토콜을 따르지 않는 요소가 배열로 들어갈 경우 unique 함수를 사용할 수 없습니다. 560 | 561 | 위와 같이 배열을 확장해 unique 함수를 추가하는 방법은 좋은 시작입니다. 562 | 563 | 그렇다면 저차원으로 내려가 Collection 프로토콜을 확장해 unique 함수를 추가해 봅시다. 564 | Collection 프로토콜을 따르는 더 많은 타입에 unique 함수를 제공할 수 있습니다. 565 | 566 | ```swift 567 | protocol Collection : Sequence 568 | ``` 569 | 570 | 물론 Collection 프로토콜도 Element라는 연관 값을 가지기 때문에 Element 연관 값을 Equatable 타입으로 제약해야 합니다. 571 | 572 | 아래 코드를 살펴봅시다. 573 | 574 | ```swift 575 | extension Collection where Element: Equatable { 576 | func unique() -> [Element] { 577 | var uniqueValues = [Element]() 578 | for element in self { 579 | if !uniqueValues.contains(element) { 580 | uniqueValues.append(element) 581 | } 582 | } 583 | return uniqueValues 584 | } 585 | } 586 | ``` 587 | 588 | 이제 Array보다 저차원인 Collection 프로토콜을 확장하여 unique 함수를 추가했기 때문에 더 많은 타입에서 unique 함수를 사용할 수 있습니다. 589 | 590 | 여기서 Array가 Collection보다 저차원인 이유는 Collection 프로토콜의 자식 프로토콜들을 Array가 따르기 때문입니다. 591 | 592 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/f87cec24-1de8-47f8-83fb-8a1713ff1872) 593 | 594 | Collection 프로토콜을 확장해 추가한 unique 함수를 아래 코드와 같이 사용할 수 있습니다. 595 | 596 | ```swift 597 | // Array still has unique() 598 | [3, 2, 1, 1, 2, 3].unique() // [3, 2, 1] 599 | 600 | // Strings can be unique() now, too 601 | "aaaaaaabcdef".unique() // ["a", "b", "c", "d", "e", "f"] 602 | 603 | // Or a Dictionary's values 604 | let uniqueValues = [ 605 | 1: "Waffle", 606 | 2: "Banana", 607 | 3: "Pancake", 608 | 4: "Pancake", 609 | 5: "Pancake" 610 | ].values.unique() 611 | print(uniqueValues) // ["Banana", "Pancake", "Waffle"] 612 | ``` 613 | 614 | 이렇게 여러 타입에 unique 함수를 적용할 수 있습니다. 615 | 특정 타입(concrete type)을 확장하지 않고 프로토콜을 확장했기 때문에 얻을 수 있는 이점입니다. 616 | 617 | 물론 Collection보다 더 저차원인 Sequence 프로토콜도 존재합니다. 618 | 이후에 Sequence 프로토콜을 확장하는 경우도 살펴봅시다. 619 | 620 | **A specialized extension** 621 | 622 | 위에서 구현한 unique 함수는 성능 측면에서 더 개선할 부분이 있습니다. 623 | 624 | 위의 unique 함수에서는 입력으로 들어오는 배열의 요소마다 uniqueValues 배열에 이미 있는 요소인지 확인해야 합니다. 625 | 다시 말해 배열 요소 하나마다 uniqueValuew 배열을 모두 순회해야 합니다. 626 | 입력으로 들어오는 배열의 크기를 N으로 가정하면, unique 함수는 O(N**2)의 시간 복잡도를 가집니다. 627 | 628 | 만약 배열로 유일한 요소를 저장하지 않고 Set을 사용해 hash value를 통해 값을 비교하고 유일한 요소들을 저장한다면 unique 함수의 성능을 개선할 수 있습니다. 629 | 630 | Set을 사용하려면 Element 연관 값의 타입 제약을 Equatable 프로토콜이 아닌 Hashable 프로토콜로 제약해야 합니다. 631 | 632 | Element 연관 값을 Hashable 프로토콜로 타입 제약한 상태에서 Collection 프로토콜 확장에 unique 함수를 추가 구현하게 됩니다. 633 | Element 연관 값을 Equatable 프로토콜로 타입 제약한 unique 함수와 Hashable 프로토콜로 타입 제약한 unique 함수 모두 Collection 확장에 구현합니다. 634 | 635 | **연관 값의 상속 관계에서도 스위프트는 가장 특수화된 구현을 고릅니다.** 636 | 637 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/51fd9ba0-ee80-4269-8ee4-dc193260a5e7) 638 | 639 | Equatable 프로토콜을 상속받은 Hashable 프로토콜을 경우, Equatable 타입을 따르는 요소를 가진 배열은 성능적으로 개선되지 못한 배열을 사용한 unique 함수를 호출하게 됩니다. 640 | 641 | 반면에 Hashable 타입을 따르는 요소를 가진 배열은 Hashable 프로토콜 제약을 가한 새로운 unique 함수를 호출합니다. 642 | 물론 Hashable 프로토콜로 Element의 타입 제약을 가한 unique 함수가 없다면 Equatable 프로토콜로 타입 제약을 가한 unique 함수를 호출하게 됩니다. 643 | 644 | 이를 통해 프로토콜 연관 값의 상속 관계에서도 스위프트는 가장 특수화된 구현, 즉 본인과 가장 구현을 고른다는 특징을 알 수 있습니다. 645 | 646 | ```swift 647 | extension Collection where Element: Hashable { 648 | func unique() -> [Element] { 649 | var set = Set() 650 | var uniqueValues = [Element]() 651 | for element in self { 652 | if !set.contains(element) { 653 | uniqueValues.append(element) 654 | set.insert(element) 655 | } 656 | } 657 | return uniqueValues 658 | } 659 | } 660 | ``` 661 | 662 | 이제 Collection 프로토콜은 두 번의 확장을 통해 연관 값의 타입 제약이 다른 두 unique 함수를 구현했습니다. 663 | 664 | Collection에서 Element의 타입 제약이 다를 때, Collection에 들어온 요소의 타입 입장에서 가장 특수화된 unique 구현을 고릅니다. 665 | 666 | Element 연관 값을 Hashable 프로토콜로 타입 제약한 unique 함수를 살펴보면 set 자체로도 유일한 값이 저장되고 있지만 추가로 [Element] 배열의 uniqueValues 변수를 만들어 사용하고 있습니다. 667 | 668 | 물론 unique 함수의 리턴 타입이 [Element]이기 때문에 배열인 uniqueValues 변수가 필요했지만, 아래와 같이 Set을 확장하여 Set을 배열로 변환하도록 만들면 불필요한 uniqueValues 변수를 사용하지 않게 됩니다. 669 | 670 | ```swift 671 | extension Set { 672 | func unique() -> [Element] { 673 | return Array(self) 674 | } 675 | } 676 | ``` 677 | 678 | Set을 배열로 변환하는 unique 함수 덕분에 아래와 같이 uniqueValues 배열 없이 Collection 확장의 unique 함수를 구현할 수 있습니다. 679 | 680 | ```swift 681 | extension Collection where Element: Hashable { 682 | func unique() -> [Element] { 683 | var set = Set() 684 | for element in self { 685 | if !set.contains(element) { 686 | set.insert(element) 687 | } 688 | } 689 | return set.unique() 690 | } 691 | } 692 | ``` 693 | 694 | The point is, finding the balance between extending the lowest common denominator without weakening the API of concrete type is a bit of an art. 695 | 696 | 스위프트는 결국 가장 구체적인 구현을 선택합니다. 가장 특수화된(가까운) 구현을 선택한다는 것과 같은 의미입니다. 697 | 698 | ## Extending with concrete constraints 699 | 700 | 프로토콜의 연관 값(associated types)에 타입 제약을 가할 때 프로토콜이 아닌 특정 타입(concrete type)으로 연관 값의 타입을 제약할 수 있습니다. 701 | 702 | 아래 코드로 살펴봅시다. 703 | 704 | ```swift 705 | // Article 구조체가 특정 타입(concrete type)입니다. 706 | struct Article: Hashable { 707 | let viewCount: Int 708 | } 709 | 710 | // Not like this: 711 | extension Collection where Element: Article { ... } 712 | 713 | // But like this: 714 | extensioin Collection where Element == Article { 715 | var totalViewCount: Int { 716 | var count = 0 717 | for article in self { 718 | count += article.viewCount 719 | } 720 | return count 721 | } 722 | } 723 | ``` 724 | 725 | 특정 타입으로 프로토콜의 연관 값에 타입 제약을 할 때 : 가 아닌 == 연산자를 사용해 연관 값에 제약을 줄 수 있습니다. 726 | 727 | 위의 Collection 확장을 아래 코드와 같이 사용할 수 있습니다. 728 | 729 | ```swift 730 | let articleOne = Article(viewCount: 30) 731 | let articleTwo = Article(viewCount: 200) 732 | 733 | // Getting the total count on an Array. 734 | let articlesArray = [articleOne, articleTwo] 735 | articlesArray.totalViewCount // 230 736 | 737 | // Getting the total count on a Set 738 | let articlesSet: Set
= [articleOne, articleTwo] 739 | articlesSet.totalViewCount // 230 740 | ``` 741 | 742 | 기능 추가를 위해 확장을 사용할 때 얼마나 저차원 타입을 확장할지 결정하기는 어렵습니다. 743 | 744 | 어떤 경우에는 저차원 Collection 프로토콜까지 확장할 필요 없을 수 있습니다. 745 | 746 | 물론 저차원인 Collection 프로토콜을 확장할 경우 더 많은 타입에 확장한 기능을 사용할 수 있게 됩니다. 747 | Collection보다 더 저차원인 Sequence를 확장하면 더 많은 타입에 확장한 기능을 사용할 수 있습니다. 748 | 749 | ## Extending Sequence 750 | 751 | Sequence 프로토콜을 확장하면 Sequence 프로토콜을 따르는 Set, Array, Dictionary 등의 타입에 확장된 기능을 제공할 수 있습니다. 752 | 753 | 실제로 Sequence 프로토콜을 확장하는 기술은 프로젝트에 큰 도움이 됩니다. 754 | 755 | **Looking under the hood of filter** 756 | 757 | Sequence 프로토콜을 확장하기 전에 filter 함수를 먼저 살펴봅시다. 758 | 759 | ```swift 760 | // filter 함수의 선언부입니다. 761 | func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> Set 762 | ``` 763 | 764 | filter 함수는 클로저를 입력으로 받습니다. 765 | 766 | 아래 코드와 같이 filter 함수가 사용됩니다. 767 | 768 | ```swift 769 | let moreThanOne = [1, 2, 3].filter { (int: Int) in 770 | int > 1 771 | } 772 | print(moreThanOne) // [2, 3] 773 | ``` 774 | 775 | 이제 filter 함수의 구현부를 살펴봅시다. 776 | 777 | ```swift 778 | // filter 함수의 구현부입니다. 779 | public func filter( 780 | _ isIncluded: (Element) throws -> Bool 781 | ) rethrows -> [Element] { 782 | var result = ContiguousArray() 783 | 784 | var iterator = self.makeIterator() 785 | 786 | while let element = iterator.next() { 787 | if try isIncluded(element) { 788 | result.append(element) 789 | } 790 | } 791 | // ContiguousArray 타입을 다시 Array 타입으로 변환해 리턴합니다. 792 | return Array(result) 793 | } 794 | ``` 795 | 796 | filter 구현부를 보면 rethrows 키워드가 붙습니다. 797 | 798 | rethrows 키워드를 사용해 매개변수로 전달받은 클로저가 오류를 던질 수 있음을 나타냅니다. 799 | 또한 입력받은 클로저가 오류를 던진다면 해당 오류를 filter 함수의 호출부로 전달한다는 의미이기도 합니다. 800 | 801 | 만약 rethrows를 사용하지 않은 경우, filter 함수를 호출하는 부분에서 오류를 처리하는 게 아닌 filter 함수 내부에서 처리해야 합니다. 802 | 803 | filter 함수에 rethrows가 붙어서 filter 함수의 구현부에서 try 키워드와 함께 catch를 사용하지 않아도 됩니다. 804 | rethrow 키워드에 의해 try 키워드에서 오류가 발생할 때 filter 함수의 호출부로 에러가 던지기 때문에 catch 구문이 필요하지 않습니다. 805 | 806 | filter 구현부에 등장하는 **ContiguousArray** 타입은 배열 요소로 클래스나 Objectivew-C 프로토콜을 가졌을 때 성능적 이점이 있습니다. 하지만 그외의 요소를 가진다면 일반 Array 타입과 동일한 성능을 보입니다. 807 | 808 | filter 함수와 같은 저차원 함수는 성능이 중요시해집니다. 성능을 향상하기 위해 ContiguousArray를 사용합니다. 809 | 810 | 아래 링크에서 ContiguousArray와 관련된 자세한 내용을 살펴봅시다. 811 | ContiguousArray가 Array보다 성능이 좋다면 왜 우린 Array보다 ContiguousArray를 자주 사용하지 않는지 궁금증이 있었습니다. 812 | 813 | ContiguousArray는 연속적으로 저장되어야 한다는 특징과 Array 자체로도 성능적으로 충분하다는 점이 결론입니다. 814 | 815 | https://zeddios.tistory.com/599 816 | 817 | https://jeong9216.tistory.com/555 818 | 819 | **Creating the take(while:) method** 820 | 821 | 지금부터 Sequence 프로토콜을 확장해 봅시다. 822 | 823 | Sequence 프로토콜이 제공하는 drop(while:) 함수와 정반대의 기능을 하는 take(while:) 함수를 Sequence 프로토콜 확장에 추가하려 합니다. 824 | 825 | drop(while:) 함수는 while로 입력받는 클로저 속 조건을 만족할 때 요소들을 순회하며 리턴하지 않다가(drop) 조건에 만족하지 않는 요소를 만나면 해당 요소를 포함하여 이후의 요소들을 리턴합니다. 826 | 827 | 아래 공식 문서를 통해 더 자세히 알아봅시다. 828 | 829 | https://developer.apple.com/documentation/swift/sequence/drop(while:) 830 | 831 | 아래 코드와 같이 drop(while:) 함수를 사용할 수 있습니다. 832 | 833 | ```swift 834 | let numbers = [3, 7, 4, -2, 9, -6, 10, 1] 835 | let startingWithNegative = numbers.drop(while: { $0 > 0 }) 836 | // startingWithNegative == [-2, 9, -6, 10, 1] 837 | ``` 838 | 839 | 그렇다면 drop(while) 함수와 반대의 기능을 하는 take(while:) 함수는 while로 입력받은 클로저의 조건에 만족하는 요소들을 리턴하다가 조건에 만족하지 않을 때 순회를 종료합니다. 840 | 841 | 아래와 같이 take(while:) 함수가 동작합니다. 842 | 843 | ```swift 844 | let lines = 845 | """ 846 | We start with text. 847 | OKOK let's start. 848 | 849 | This is ignored because it came after empty space 850 | and more text 851 | """.components(seperatedBy: "\n") 852 | 853 | let firstParts = lines.take(while: { (line) -> Bool in 854 | !line.isEmpty 855 | }) 856 | 857 | print(firstParts) //["We start with text.", "OKOK let's start."] 858 | ``` 859 | 860 | take(while:) 함수를 구현할 때 앞에서 살펴봤던 filter 함수의 구현부의 rethrow와 ContiguousArray 사용을 모방해 봅시다. 861 | 862 | 아래 코드는 take(while:) 함수를 Sequence 프로토콜 확장에 추가한 코드입니다. 863 | 864 | ```swift 865 | extension Seequence { 866 | public func take( 867 | while predicate: (Element) throws -> Bool 868 | ) rethrows -> [Element] { 869 | 870 | var iterator = makeIterator() 871 | 872 | var result = ContiguousArray() 873 | 874 | while let element = iterator.next() { 875 | if try predicate(element) { 876 | result.append(element) 877 | } else { 878 | break 879 | } 880 | } 881 | return Array(result) 882 | } 883 | ``` 884 | 885 | 위와 같이 take(while:) 함수를 Sequence 프로토콜 확장에 추가해서 프로젝트에 유용하게 사용할 수 있습니다. 886 | 887 | **Creating the Inspect method** 888 | 889 | 위에서는 Sequence 프로토콜을 확장하여 take(while:) 함수를 추가했다면 이번에는 inspect 함수를 추가해 봅시다. 890 | 891 | inspect 함수는 파이프라인 구조에서 데이터를 다룰 때 디버깅 용도로 사용되는 함수입니다. 892 | 893 | filter나 forEach는 데이터를 조작하여 변형된 데이터를 하위 파이프라인으로 전달하지만, inspect 함수에서는 데이터를 조작하지만, 하위 파이프라인으로는 조작된 데이터가 아닌 조작되기 이전의 데이터를 전달합니다. 894 | 895 | 따라서 inspect 함수는 파이프라인 중간에 데이터를 디버깅하는 용도로 주로 사용할 수 있습니다. 896 | 897 | ```swift 898 | extension Sequence { 899 | pubilc func inspect( 900 | _ body: (Element) throws -> Void 901 | ) rethrows -> Self { 902 | for element in self { 903 | try body(element) 904 | } 905 | // inspect 함수의 입력으로 들어오는 클로저로 데이터가 변형되지만 실제로 리턴하는 데이터는 변형되지 않은 상태로 리턴합니다. 906 | return self 907 | } 908 | } 909 | 910 | ["C", "B", "A", "D"] 911 | .sorted() 912 | .inspect { (string) in 913 | print("Inspecting: \(string)") 914 | }.filter { (string) -> Bool in 915 | string < "C" 916 | }.forEach { 917 | print("Result: \($0)") 918 | } 919 | 920 | // Output: 921 | // Inspecting: A 922 | // Inspecting: B 923 | // Inspecting: C 924 | // Inspecting: D 925 | // Result: A 926 | // Result: B 927 | ``` 928 | 929 | 저차원의 Sequence 프로토콜을 확장하여 배열을 비롯해 Sequence 프로토콜을 따르는 여러 타입에 확장된 기능을 추가할 수 있습니다. 930 | 931 | 프로토콜과 프로토콜 확장은 재사용성이 높은 코드를 만들고 의미 단위로 코드를 분리합니다. 932 | 933 | 하지만 어떤 경우에서는 특정 타입(concrete type)이 추상화를 구현할 때 더 어울릴 경우도 있습니다. 934 | 프로토콜 확장과 특정 타입을 균형 있게 사용해야 합니다. 935 | 936 | ## Summary 937 | - Protocols can deliver a default implementation via protocol extensions. 938 | - With extensions, you can think of modeling data horizontally, whereas with subclassing, you're modeling data in a more rigid vertical way. 939 | - You can override a default implementation by delivering an implementation on a concrete type. 940 | - Protocol extensions cannot override a concrete type. 941 | - Via protocol inheritance, you can override a protocol's default implementation. 942 | - Swift always picks the most concrete implementation. 943 | - You can create a protocol extension that only unlocks when a type implements two protocols, called a protocol intersection. 944 | - A protocol intersection is more flexible than protocol inheritance, but it's also more abstract to understand. 945 | - When mixing subclasses with protocol extensions, extending a protocol and constraining it to a class is a good heuristic (as opposed to extending a class to adhere to a protocol). This way, an implementer can pick and choose a protocol implementation. 946 | - For associated type, such as Element on the Collection protocol, Swift picks the most specialized abstraction, such as Hashable over Equatable elements. 947 | - Extending a low-level protocol - such as Sequence - means you offer new methods to many types at once. 948 | - Swift uses a special ConiguousArray when extending Sequence for extra performance. 949 | -------------------------------------------------------------------------------- /Putting-the-pro-in-protocol-oriented-programming.md: -------------------------------------------------------------------------------- 1 | # Putting the pro in protocol-oriented programming 2 | 3 | ## This chapter covers 4 | - The relationship and trade-offs between generics and using protocols as types 5 | - Understanding associated types 6 | - Passing around protocols with associated types 7 | - Storing and constraining protocols with associated types 8 | - Simplifying your API with protocol inheritance 9 | 10 | ## Runtime versus compile time 11 | 프로토콜은 크게 런타임과 컴파일 타임에 동작하는 것으로 나뉩니다. 12 | '제네릭을 제약할 때의 프로토콜'과 '단일 타입으로 쓰이는 프로토콜'로 나뉩니다. 13 | '제네릭을 제약할 때의 프로토콜'은 컴파일 타임에 타입을 결정하고 '단일 타입으로 쓰이는 프로토콜'의 경우 런타임에 타입을 결정합니다. 14 | 15 | 지금부터 코인 포토폴리오를 구현하며 프로토콜에 대해 살펴봅시다. 16 | 코인 포토폴리오는 다양한 코인에 대응할 수 있어야 합니다. 17 | 18 | 다양한 코인에 대응하는 코인 포토폴리오를 구현하기 위해서는 열거형을 떠올릴 수 있습니다. 19 | 하지만 모든 코인에 대응해야하기 때문에 열거형에 너무 많은 case들이 나오기 때문에 열거형은 좋은 선택지는 아닙니다. 20 | 21 | 이때 우리는 프로토콜을 통해 코인 포토폴리오를 구현하여 여러 종류의 코인에 대응하도록 추상화 할 수 있습니다. 22 | 아래 코드로 살펴봅시다. 23 | 24 | ```swift 25 | import Foundation 26 | 27 | protocol CryptoCurrency { 28 | var name: String { get } 29 | var symbol: String { get } 30 | var holdings: Double { get set } 31 | var price: NSDecimalNumber? { get set } 32 | } 33 | 34 | struct Bitcoin: CryptoCurrency { 35 | let name = "Bitcoin" 36 | let symbol = "BTC" 37 | var holdings: Double 38 | var price: NSDecimalNumber? 39 | } 40 | 41 | struct Ethereum: CryptoCurrency { 42 | let name = "Ethereum" 43 | let symbol = "ETH" 44 | var holdings: Double 45 | var price: NSDecimalNumber? 46 | } 47 | ``` 48 | 49 | CryptoCurrency 프로토콜을 통해 코인들이 공통적으로 가지는 프로퍼티를 선언합니다. 50 | 51 | 프로토콜의 프로퍼티는 항상 var로 선언되어야 합니다. 52 | 프로토콜을 따르는 구현부에서 프로퍼티를 var 또는 let으로 구현이 가능합니다. 53 | 만약 프로토콜의 프로퍼티가 { get set }이라면 조회/수정이 가능한 변수이기 때문에 구현부에서 해당 프로퍼티를 var로 구현해야 합니다. 54 | 55 | 코인 포토폴리오는 코인 프로퍼티를 가지고 있습니다. 56 | 코인 프로퍼티를 가지는 Portfolio 클래스를 만들어 봅시다. 57 | 코인 프로퍼티는 CrypoCurrency를 채택한 상태이고 코인 포토폴리오는 여러 종류의 코인에 대응해야 합니다. 58 | 59 | 먼저 '프로토콜을 제네릭 제약'으로 사용할 때의 코드를 살펴봅시다. 60 | 아래 코드를 확인해 봅시다. 61 | 62 | ```swift 63 | final class Portfolio { 64 | var coins: [Coin] 65 | 66 | init(coins: [Coin]) { 67 | self.coins = coins 68 | } 69 | 70 | func addCoin(_ newCoin: Coin) { 71 | coins.append(newCoin) 72 | } 73 | } 74 | ``` 75 | 76 | 위 코드에 문제가 보이십니까? 77 | Chapter 6. Generics에서 다루었던 내용입니다. 78 | 프로토콜을 통한 제네릭 제약과 함께 서브 클래싱이 발생하는 상황에서는 제네릭 타입으로 쓰이는 타입이 커스텀 타입일 경우 상속 관계를 잃게 됩니다. 79 | 80 | 따라서 Portfolio 클래스의 제네릭 타입으로 CrytoCurrency 프로토콜을 따르는 타입 중 하나를 넣게 되면, 처음 넣은 데이터 타입으로 제네릭 타입이 컴파일 타임에 고정됩니다. 81 | 제네릭의 데이터 타입이 고정된 이후에는 CrytoCurrency 프로토콜을 따르더라도 고정된 타입과 같은 타입이 아닌 이상 컴파일 에러가 발생합니다. 82 | 83 | ```swift 84 | let coins = [ 85 | Ethereum(holdings: 4, price: NSDecimalNumber(value: 500)), 86 | // if we mix coins, we can't pass them to Portfolio 87 | // Bitcoin(holdings: 4, price: NSDecimalNumber(value: 6000)) 88 | ] 89 | let portfolio = Portfolio(coins: coins) 90 | 91 | let btc = Bitcoin(holdings: 3, price: nil) 92 | portfolio.addCoin(btc) // error: cannot convert value of type 'Bitcoin' to expected argument type 'Ethereum' 93 | 94 | // 타입을 확인해 봅시다. 95 | print(type(of: portfolio)) // Portfolio, 프로토콜을 제네릭 제약에 사용했을 때 컴파일 타임에 제네릭의 타입이 Ethereum로 고정됩니다. 96 | print(type(of: portfolio.coins)) // Array, 프로토콜을 제네릭 제약에 사용했을 때 컴파일 타임에 제네릭의 타입이 Ethereum로 고정됩니다. 97 | ``` 98 | 99 | 제네릭과 프로토콜을 함께 사용했을 때 제네릭이 어떤 타입을 다루는지 컴파일 타임에 알 수 있다는 장점이 있습니다. 100 | 컴파일러의 성능도 최적화할 수 있습니다. 101 | 102 | 하지만 우린 여러 코인 타입을 함께 담고 싶습니다. 제네릭과 프로토콜을 함께 사용하면 컴파일 타임에 타입이 고정되고 상속 관계도 잃기 때문에 요구사항을 충족할 수 없습니다. 103 | 그렇다면 어떻게 프로토콜을 통해 여러 코인 타입을 함께 담을 수 있을까요? 104 | 105 | 제네릭 없이 프로토콜을 하나의 타입으로 사용하면 여러 코인 타입에 대응하도록 Portfolio 클래스를 만들 수 있습니다. 106 | 제네릭 없이 쓰이는 프로토콜은 런타임에 타입이 결정되기 때문입니다. 또한 상속 관계도 잃지 않습니다. 107 | 108 | 아래 코드는 제네릭의 제약으로 쓰이던 프로토콜을 하나의 타입으로 쓰이는 프로토콜로 변경한 코드입니다. 109 | 110 | ```swift 111 | // Before : 제네릭 제약으로 쓰인 프로토콜 112 | final class Portfolio { 113 | var coins: [Coin] 114 | // ...생략 115 | } 116 | 117 | // After : 타입으로 쓰인 프로토콜 118 | final class Portfolio { 119 | var coins: [CryptoCurrency] 120 | // ...생략 121 | } 122 | 123 | let portfolio = Portfolio(coins: []) 124 | let coins: [CryptoCurrency] = [ 125 | Ethereum(holdings: 4, price: NSDecimalNumber(value: 500)), 126 | Bitcoin(holdings: 4, price: NSDecimalNumber(value: 6000)) 127 | ] 128 | portfolio.coins = coins // '타입으로 쓰이는 프로토콜'로 서브 타입까지 수용 가능합니다. 129 | 130 | // 타입을 확인해 봅시다. 131 | print(type(of: portfolio)) // Portfolio 132 | let retrievedCoins = porfolio.coins 133 | print(type(of: rerievedCoins)) // Array 134 | ``` 135 | 136 | 제네릭 없이 타입으로 쓰이는 프로토콜은 런타임에 동작하며 서브 타입까지 모두 대응할 수 있습니다. 137 | 보다 유연한 코드로 생각됩니다. 138 | 139 | 앞서 '제네릭을 제약하는 프로토콜'은 coins의 타입이 고정되었지만 '타입으로 쓰이는 프로토콜'의 경우 Array로 추상화 되어있습니다. 140 | 만약 런타임에 동작하는 프로토콜을 원하다면 프로토콜을 타입이나 인터페이스로 사용하면 됩니다. 141 | 142 | 하지만 '타입으로 쓰이는 프로토콜'로 추상화 할 경우에도 단점이 있습니다. 143 | 144 | 만약 portfolio.coin에 추가 되는 Bitcoin 타입 객체가 CryptoCurrency protocol에서 제공하는 함수 외에 추가적인 함수가 있을 때, 145 | Array로 추상화 된 상태에서는 Bitcoin 타입의 추가적인 함수에 접근할 수 없습니다. 146 | 해당 함수에 접근하기 위해서는 CryptoCurrency 타입을 Bitcoin 타입으로 형 변환해야 합니다. 147 | 하지만 이는 안티패턴으로 이어질 수 있고 CryptoCurrency 타입을 따르는 코인의 종류가 많아지면 대응하기 어려워질 수 있으니 주의해야 합니다. 148 | 149 | 앞에서 보았듯이 '제네릭 타입 제약에 쓰이는 프로토콜'은 컴파일 타임에 타입이 결정되며 결정된 타입이 외에 어떤 타입도 허용하지 않습니다. 150 | 그렇다면 '제네릭 타입 제약에 쓰이는 프로토콜'은 어떤 경우에 더 좋은 선택지가 될까요? 151 | 152 | 비트 코인을 함수에 넘겨 가장 최신 가격 동일한 타입의 코인을 리턴 받는 상황에 '제네릭 타입 제약에 쓰이는 프로토콜'의 사용해 유리한 점을 살펴봅시다. 153 | 154 | ```swift 155 | // 타입으로 쓰이는 프로토콜 156 | func retrievePriceRunTime(coin: CryptoCurrency, completion: ((CryptoCurrency) -> Void)) { 157 | // ...생략 158 | var copy = coin 159 | copy.price = 6000 160 | completion(copy) 161 | } 162 | 163 | // 제네릭 타입 제약으로 쓰이는 프로토콜 164 | func retrievePriceCompileTime(coin: Coin, completion:((Coin) -> Void) { 165 | // ...생략 166 | var copy = coin 167 | copy.price = 6000 168 | completion(copy) 169 | } 170 | 171 | let btc = Bitcoin(holdings: 3, price: nil) 172 | retrievePriceRunTime(coin: btc) { (updatedCoin: CryptoCurrency) in // 런타임 전까지 updatedCoin의 정확한 타입을 모릅니다. 173 | print("Updated value runtime is \(updatedCoin.price?.doubleValue ?? 0)") 174 | } 175 | 176 | retrievePriceCompileTime(coin: btc) { (updatedCoin: Bitcoin) in // 컴파일 타임에 updatedCoin의 타입이 Bitcoin으로 결정되었다. 177 | print("Updated value runtime is \(updatedCoin.price?.doubleValue ?? 0)") 178 | } 179 | ``` 180 | 181 | '제네릭 제약으로 사용된 프로토콜'은 컴파일 타임에 타입이 결정되어 클로저로 들어올 타입을 이미 알고 있습니다. 182 | '타입으로 사용된 프로토콜'은 런타임에서야 타입이 결정되기 때문에 Bitcoin 타입에 새로 추가된 프로퍼티나 함수에 접근하기 위해서는 CryptoCurrency 타입에서 Bitcoin 타입으로 형 변환을 해야 합니다. 183 | 하지만 '제네릭 제약으로 사용된 프로토콜'의 경우 이미 컴파일 타임에 Bitcoin 타입으로 확정되기 때문에 Bitcoin 타입의 새로운 프로퍼티나 함수에 추가적인 형 변환 없이 접근 가능합니다. 184 | 185 | 결과적으로 '제네릭 제약으로 사용된 프로토콜'과 '타입으로 사용된 프로토콜' 모두 trade-off가 존재합니다. 186 | '타입으로 사용된 프로토콜'은 여러 타입에 대응할 수 있지만 런타임에 타입이 결정되어 특정 자식 타입에 추가된 함수나 프로퍼티에 접근하기 위해서는 형 변환을 필요로 합니다. 187 | 그에 반해 '제네릭 제약으로 사용된 프로토콜'은 컴파일 타임에 타입이 결정되어 상속 관계를 잃으며 여러 타입에 대응하기는 어렵지만 컴파일 타임에 타입이 결정되어 성능적 이점과 추가적인 형 변환을 필요로 하지 않습니다. 188 | 189 | '제네릭 제약으로 사용된 프로토콜'이 생각하기 어렵지만, 더 좋은 선택입니다. 190 | 191 | ## The why of associated types 192 | 193 | 프로토콜은 추상화에 자주 쓰입니다. 194 | 하지만 프로토콜과 함께 연관 값(associated types)을 사용하면 더 추상적인 코드를 구현할 수 있습니다. 195 | 우린 연관 값(associated types)을 가진 프로토콜을 PATs로 줄여 부릅니다. 196 | 197 | 연관 값 없이 프로토콜 만으로 데이터 모델링을 할 수 있습니다. 198 | 하지만 한계가 있습니다. 199 | 프로토콜 만으로 데이터 모델링을 했을 때 발생하는 문제를 먼저 살펴봅시다. (해당 문제를 프로토콜의 연관 값을 통해 해결할 것입니다.) 200 | 201 | 아래 예시는 이메일을 손님에게 보내거나 데이터 베이스를 다루거나 이미지 사이즈를 조절하는 등 성격들이 다른 '일'을 추상화하는 프로토콜로 만들었습니다. 202 | 203 | ```swift 204 | protocol Worker { 205 | @discardableResult 206 | func start(input: String) -> Bool 207 | } 208 | 209 | class MailJob: Worker { 210 | func start(input: String) -> Bool { 211 | // send mail to email address 212 | } 213 | } 214 | ``` 215 | 216 | 위 코드에서 @discardableResult 키워드는 함수의 리턴 값을 무시할 수 있도록 만듭니다. 217 | @discardableResult 키워드가 붙은 함수의 리턴은 사용하지 않아도 경고를 띄우지 않습니다. 218 | 219 | Worker 프로토콜의 start 함수는 입력과 출력 데이터 타입이 String과 Bool로 고정되어 있습니다. 220 | 하지만 '일'들은 start 함수를 공통으로 가질 수 있지만 start 함수의 입력과 출력 데이터 타입이 다양할 것입니다. 221 | 위의 Worker 프로토콜의 start 함수로는 다양한 타입에 대응하지 못합니다. 222 | 만약 파일을 삭제하는 FileRemover 객체가 URL을 입력 받아 [String]을 리턴하는 start 함수가 필요하다면 위와 같은 Worker 프로토콜의 start 함수로는 대응할 수 없습니다. 223 | 224 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/7df87670-5c98-44f3-8cbc-29262fa56863) 225 | 226 | 단순한 프로토콜(연관 값이 없는 프로토콜)은 적용 가능한 범위가 넓지 못합니다. 227 | 228 | 위와 같이 함수(start) 동작의 과정은 동일하지만, 동작에 사용되는 타입이 다를 경우 프로토콜에 연관 값을 추가해서 구현할 수 있습니다. 229 | 프로토콜과 연관 값을 함께 사용하는 방법을 살펴보기 전에 불완전한 두 가지 방법을 먼저 살펴봅시다. 230 | 불완전한 방법이지만 생각해볼만한 접근 방법입니다. 231 | 232 | 첫 번째 방법은 Worker 프로토콜 start 함수의 입력과 출력 데이터 타입을 모두 프로토콜로 만드는 방법입니다. 233 | Input, Output 프로토콜을 만들고 start 함수의 모든 입력과 출력 데이터 타입이 Input 또는 Output 프로토콜을 따르도록 합니다. 234 | 그리고 Worker 프로토콜의 start 함수 입력에 쓰이는 데이터 타입은 Input 프로토콜을 따르고 출력에 쓰이는 데이터 타입은 Output 프로토콜을 따르도록 만듭니다. 235 | 236 | 물론 동작은 할 것입니다. 아래 코드를 확인해봅시다. 237 | 238 | ```swift 239 | protocol Input {} 240 | protocol Output {} 241 | 242 | protocol Worker { 243 | @discardableResult 244 | func start(input: Input) -> Output 245 | } 246 | ``` 247 | 248 | 하지만 start 함수에 쓰이는 모든 데이터 타입이 Input 또는 Output 프로토콜을 따르도록 하는 방법은 boilerplate 코드를 유발합니다. 249 | 심지어 Input, Output 프로토콜에 새로운 프로퍼티나 함수가 추가된다면 이를 따르는 모든 타입에서 추가적인 구현이 필요합니다. 250 | 251 | 두 번째 방법은 프로토콜에 제네릭을 추가하는 방법입니다. 252 | 아래 코드로 살펴봅시다. 253 | 254 | ```swift 255 | protocol Worker { 256 | @discardableResult 257 | func start(input: Input) -> Output 258 | } 259 | ``` 260 | 261 | 프로토콜에 제네릭을 추가하여 Worker 프로토콜을 채택한 구현부에서 Input과 Output이 어떤 타입이 될지 결정하도록 구현했습니다. 262 | 굉장히 좋은 접근이지만 아쉽게도 스위프트에서 프로토콜과 제네릭의 사용을 허락하지 않습니다. 263 | 위 코드는 다음과 같은 컴파일 에러를 발생시킵니다. 264 | 265 | error: protocols do not allow generic parameters; use assciated types instead 266 | 267 | 스위프트에서는 하나의 타입에 특정 프로토콜을 채택하고 요구사항을 구현하는 행위의 중복을 허용하지 않습니다. 268 | 다시 말해 MailJob 클래스가 Worker 프로토콜을 채택할 때 한 가지의 구현만 허용합니다. 269 | 270 | 스위프트에서 프로토콜과 제네릭의 사용을 허락하지 않는 이유도 하나의 타입에 한 번의 프로토콜 채택과 구현만을 허용하기 때문입니다. 271 | 만약 프로토콜과 제네릭이 함께 사용된다면 아래와 같은 코드가 작성될 것입니다. 272 | 273 | ```swift 274 | // Not supported: Implementing a generic Worker. 275 | class MailJob: Worker { 276 | // ...생략 277 | } 278 | 279 | class MailJob: Worker { 280 | // ...생략 281 | } 282 | ``` 283 | 284 | 스위프트에서 같은 타입(MailJob)이 Worker 프로토콜을 단일 채택하고 구현해야 합니다. 285 | 위와 같이 같은 타입이 Worker 프로토콜에 대해 여러 구현부를 가진다면 컴파일 에러가 발생합니다. 286 | 287 | 컴파일 되지는 않는 코드지만 프로토콜과 제네릭을 함께 사용하는 방법은 좋은 접근입니다. 288 | 결과적으로 프로토콜과 제네릭스러운 요소(연관 값)가 함께 사용되어야 Worker 프로토콜이 여러 타입의 '일'에 대응할 수 있습니다. 289 | 여기서 프로토콜과 함께 쓰일 제네릭스러운 요소가 바로 연관 값(associatedtype)입니다. 290 | 291 | 그렇다면 우리의 결론이었던 프로토콜과 연관 값을 사용해 Worker 프로토콜을 만들어 봅시다. 292 | start 함수의 입력과 출력을 연관 값으로 만들어 Worker 프로토콜이 여러 '일'에 대응하도록 만듭니다. 293 | 프로토콜에서 associatedtype 키워드로 연관 값을 선언하고 구현부에서는 typealias 키워드를 사용해 연관 값의 타입을 지정합니다. 294 | 295 | ```swift 296 | // 연관 값을 사용한 프로토콜 297 | protocol Worker { 298 | associatedtype Input // just like generics 299 | associatedtype Output // just like generics 300 | 301 | @discardableResult 302 | func start(input: Input) -> Output 303 | } 304 | 305 | class MailJob: Worker { 306 | typealias Input = String // the Input associated type is defined as String 307 | typealias Output = Bool // the Output associated type is defined as Bool 308 | 309 | func start(input: String) -> Bool { 310 | // send mail to email address 311 | } 312 | } 313 | ``` 314 | 315 | associatedtype은 제네릭과 비슷한 성격을 가지지만 제네릭과 달리 프로토콜 내부에 정의됩니다. 316 | Worker 프로토콜의 associatedtype을 통해 MailJob은 Input을 string 타입, Output을 Bool 타입으로 구현할 수 있고 FileRemover는 Input을 URL 타입, Output을 [string] 타입으로 구현할 수 있습니다. 317 | 318 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/dedf272d-6a39-4852-904d-a4561958a14b) 319 | 320 | 프로토콜의 연관 값을 통해 프로토콜을 따르는 타입이 특정 프로토콜에 대한 유일한 구현을 가지는 동시에 여러 타입에 대응하도록 만들 수 있습니다. 321 | 322 | 위 코드의 MailJob 클래스는 typealias 키워드로 프로토콜 연관 값의 타입을 확정하고 있지만, 컴파일러가 MailJob 클래스의 구현부를 통해 타입이 추론 가능하다면 typealias 구문을 생략할 수 있습니다. 323 | 아래 코드는 typealies 키워드룰 생략한 코드입니다. 324 | FileRemover 클래스의 start 함수에서 입력과 출력 데이터 타입을 모두 명시하고 있기 때문에 typealias 키워드 없이도 컴파일러가 Worker 프로토콜의 연관 값을 추론할 수 있습니다. 325 | 326 | ```swift 327 | class FileRemover: Worker { 328 | // typealias Input = String 329 | // typealias Output = Bool 330 | 331 | func start(input: URL) -> [String] { 332 | do { 333 | var results = [String]() 334 | let fileManager = FileManager.default 335 | let fileURLs = try fileManager.contentsOfDirectory(a: input, includingPropertiesForKeys: nil) 336 | 337 | for fileURL in fileURLs { 338 | tryfileManager.removeItem(at: fileURL) 339 | results.append(filedURL.absoluteString) 340 | } 341 | return results 342 | } catch { 343 | print("Clearing direcory failed.") 344 | return [] 345 | } 346 | } 347 | } 348 | ``` 349 | 350 | 스위프트에서는 프로토콜과 연관 값은 함께 자주 사용됩니다. 351 | IteratorProtocol, Sequence, Collection 프로토콜들이 대표적으로 Element 연관 값을 가지고 있습니다. 352 | 353 | 또한 여러 프로토콜에서 Self를 볼 수 있는데 Self도 마찬가지로 연관 값입니다. 354 | 아래 코드는 Self 연관 값을 가진 Equatable 프로토콜 코드입니다. 355 | 356 | ```swift 357 | public protocol Equatable { 358 | static func == (lhs: Self, rhs: Self) -> Bool 359 | } 360 | ``` 361 | 362 | 스위프트 내부적으로 프로토콜과 연관 값을 함께 사용한 경우 외에도 다양한 용도에 사용됩니다. 363 | 프로토콜을 따르는 타입에서 여러 타입에 대응해야 할 때 프로토콜과 연관 값이 함께 사용됩니다. 364 | 프로토콜과 연관 값이 함께 사용되는 상황들을 살펴봅시다. 365 | 366 | - A Recording protocol - Each recording has a duration, and it could also suport scrubbing through time via a seek() method, but the actual data could be different for each implementation, such as an audio file, video file, or YouTube stream. 367 | - A Service protocol - It loads data; one type could return JSON data from an API, and another could locally search and return raw string data. 368 | - A Message protocol - It's on a social media tool that tracks posts. In one implementation, a message represents a Tweet; in another, a message represents a Facebook direct message; and in another, it could be a message on WhatsApp. 369 | - A SearchQuery protocol - It resembles database queries, where the result is different for each implementation. 370 | - A Paginator protocol - It can be given a page and offset to browse through a database. Each page could represent some data. Perhaps it has some users in a user table in a database, or perhaps a list of files, or a list of products inside a view. 371 | 372 | 서브 클래싱 방식의 코드를 프로토콜 방식으로 코드로 고쳐 봅시다. 373 | 아래 코드로 확인해 봅시다. 374 | 375 | ```swift 376 | class AbstractDamage {} 377 | 378 | class AbstractEnemy { 379 | func attack() -> AbstractDamage { 380 | fatalError("This method must be implemented by subclass.") 381 | } 382 | } 383 | 384 | class Fire: AbstractDamage {} 385 | class Imp: AbstractEnemy { 386 | override func attack() -> Fire { 387 | return Fire() 388 | } 389 | } 390 | 391 | class BluntDamage: AbstractDamage {} 392 | class Centaur: AbstractEnemy { 393 | override func attack() -> BluntDamge { 394 | return BluntDamage() 395 | } 396 | } 397 | ``` 398 | 399 | 위의 서브 클래싱 기반 코드를 아래 프로토콜 기반 코드로 수정했습니다. 400 | 401 | ```swift 402 | protocol AbstractEnemy { 403 | associatedtype damage 404 | 405 | func attack() -> damage 406 | } 407 | 408 | struct Fire {} 409 | class Imp: AbstractEnemy { 410 | func attack() -> Fire { 411 | return Fire() 412 | } 413 | } 414 | 415 | struct BluntDamage {} 416 | class Centaur: AbstractEnemy { 417 | func attack() -> BluntDamage { 418 | return BluntDamage() 419 | } 420 | } 421 | ``` 422 | 423 | ## Passing protocols with associated types 424 | 425 | 위에서는 프로토콜의 연관 값을 살펴보았다면, 지금부터는 연관 값을 가진 프로토콜을 함수의 인자로 넘기는 방법을 살펴봅시다. 426 | 427 | 연관 값을 가진 프로토콜을 함수에 넘길 때 프로토콜의 연관 값에 직접 접근할 수 있습니다. 428 | 또한 함수에서 제네릭 제약처럼 프로토콜 연관 값의 타입에 제약을 걸 수도 있습니다. 429 | 430 | 먼저 함수로 연관 값을 가진 프로토콜을 넘기는 코드를 살펴봅시다. 431 | 432 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/1819622d-b699-48b4-ad7b-8c62996e13f3) 433 | 434 | ```swift 435 | let mailJob = MailJob() 436 | runWorker(worker: mailJob, input: ["grover@sesamestreetcom", "bigbird@sesamestreet.com"]) 437 | 438 | let fileRemover = FileRemover() 439 | runWorker(worker: fileRemover, input: [ 440 | URL(fileURLWithPath: "./cache", isDirectory: true), 441 | URL(fileURLWithPath: "./tmp", isDirectory: true) 442 | ]) 443 | ``` 444 | 445 | 위 코드에서 input이 W.Input 타입으로 지정하듯이 프로토콜의 연관 값에 직접 접근할 수 있습니다. 446 | input이 W.Input 타입으로 선언했기 때문에 worker.start(input: value)로 값을 전달할 수 있습니다. 447 | 프로토콜의 연관 값은 컴파일 타임에 타입이 결정됩니다. 448 | 449 | 제네릭 함수에서 where 절을 사용했듯이 프로토콜의 연관 값을 입력 받는 함수도 where 절로 입력을 받을 수 있습니다. 450 | 또한 where 절에서 연관 값의 타입에 제약을 걸 수 있습니다. 이는 제네릭 제약과 비슷한 느낌입니다. 451 | 하지만 문법적으로 차이가 있으니 아래 코드를 살펴봅시다. 452 | 453 | ```swift 454 | final class User { 455 | let firstName: String 456 | let secondName: String 457 | init(firstName: String, lastName: String) { 458 | self.firstName = firstName 459 | self.secondName = secondName 460 | } 461 | } 462 | 463 | func runWorker(worker: W, input: [W.Input]) 464 | where W: Worker, W.Input == User { 465 | // associatedtype에 제약을 걸어 User Input에 특수화된 함수를 구현할 수 있었습니다. 466 | input.forEach { (user: W.Input) in 467 | worker.start(input: user) 468 | print("Finished processing user \(user.firstName) \(user.lastName)") 469 | } 470 | } 471 | ``` 472 | 473 | By constraining an associated type, the function is specialized to work only with users as input!! 474 | 475 | 지금까지는 연관 값을 가진 프로토콜을 함수에 넘기는 방법을 살펴봤고 이제 구조체, 클래스, 열거형과 같은 타입들과 프로토콜의 연관 값을 함께 사용하는 방법을 살펴봅시다. 476 | 477 | 이미지를 다루는 ImageProcesser 클래스를 만들며 다양한 타입들과 프로토콜 연관 값이 함께 사용되는 상황을 살펴 봅시다. 478 | 아래의 예시에서는 Worker 프로토콜을 따르는 ImageCropper 클래스를 프로퍼티로 가진 ImageProcessor 클래스를 만들고 있습니다. 479 | ImageProcessor 클래스가 하는 일은 Worker 프로토콜에 의해 결정됩니다. 480 | 481 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/95b35a7f-8afb-4dda-9280-7506708860c5) 482 | 483 | 아래 코드로 확인해 봅시다. 484 | 485 | ```swift 486 | protocol Worker { 487 | associatedtype Input // just like generics 488 | associatedtype Output // just like generics 489 | 490 | @discardableResult 491 | func start(input: Input) -> Output 492 | } 493 | 494 | final class ImageCropper: Worker { 495 | let size: CGSize 496 | 497 | init(size: CGSize) { 498 | self.size = size 499 | } 500 | 501 | func start(input: UIImage) -> Bool { 502 | // 이미지 조작 이후 성공 시 true 리턴 503 | return true 504 | } 505 | } 506 | 507 | final class ImageProcessor 508 | where W.Input == UIImage, W.Output == Bool { 509 | // where 절을 사용해 프로토콜의 연관 값(Input, Output)에 제약을 걸어 ImageProcessor에 필요한 타입을 확정했습니다. 510 | let worker: W 511 | 512 | init(worker: W) { 513 | self.worker = worker 514 | } 515 | 516 | private func process() { 517 | // start batches 518 | var results = [Bool]() 519 | 520 | let amount = 50 521 | var offset = 0 522 | var images = fetchImages(amount: amount, offset: offset) 523 | var failedCount = 0 524 | while !images.isEmpty { 525 | for image in images { 526 | if !worker.start(input: image) { 527 | failedCount += 1 528 | } 529 | } 530 | offset += amount 531 | images = fetchImages(amount: amount, offset: offset) 532 | } 533 | print("\(failedCount) images failed.") 534 | } 535 | 536 | private func fetchImages(amount: Int, offset: Int) -> [UIImages] { 537 | // return images from database 538 | return [UIImage(), UIImage()] 539 | } 540 | } 541 | 542 | let cropper = ImageCropper(size: CGSize(width: 200, height: 200)) 543 | let imageProcessor = ImageProcessor(worker: cropper) 544 | ``` 545 | 546 | ImageProcessor 클래스에 제네릭을 Worker 프로토콜로 제약하여 Worker 프로토콜을 따르는 image croppers, resizers 등에도 대응하는 클래스를 만들었습니다. 547 | 물론 W.Input과 W.Output의 타입은 연관 값 제약에 맞춰 UIImage 타입과 Bool 타입으로 들어와야 합니다. 548 | 549 | 만약 이미지를 다루는 '일'이 아니라 URL을 다루는 '일'이라면 Worker 프로토콜의 연관 값을 수정해 UrlCropper 클래스를 만들고, 550 | UrlProcessor 클래스의 연관 값 타입 제약을 URL에 어울리도록 구현해주면 됩니다. 551 | 552 | 이미지와 관련된 작업일 경우 Worker 프로토콜의 연관 값인 Input, Output의 타입은 UIImage와 Bool이어야 합니다. 553 | 따라서 이미지와 관련된 작업을 하는 클래스마다 where 절로 연관 값 제약 구문을 반복해서 작성해야 합니다. 554 | 다시 말해 프로토콜의 연관 값을 제약하는 코드가 각 클래스마다 중복됩니다. 555 | 556 | 우리는 프로토콜을 채택한 구현부가 아니라 프로토콜 수준에서 연관 값 제약을 걸어 해당 프로토콜을 따르는 타입의 연관 값에 제약을 줄 수 있습니다. 557 | 이 방법은 프로토콜 연관 값 중복을 줄여줍니다. 558 | 아래 코드로 확인해 봅시다. 559 | 560 | ```swift 561 | protocol Worker { 562 | associatedtype Input // just like generics 563 | associatedtype Output // just like generics 564 | 565 | @discardableResult 566 | func start(input: Input) -> Output 567 | } 568 | 569 | protocol ImageWorker: Worker where Input == UIImage, Output == Bool { 570 | // extra methods can go here if you want 571 | } 572 | ``` 573 | 574 | 위와 같이 Worker 프로토콜을 따르는 타입 중 Worker 프로토콜의 연관 값을 UIImage, Bool로 제약해야 하는, 575 | 즉 이미지를 다루는 객체가 여러 개라면 Worker 프로토콜을 채택한 ImageWorker 프로토콜을 만들고 연관 값 제약 구문을 추가해 중복되는 코드를 줄일 수 있습니다. 576 | 577 | ImageWorker 프로토콜에 연관 값 제약 코드를 추가한 덕분에 아래와 같이 깨끗한 코드가 나옵니다. 578 | 이미지를 다루는 클래스는 ImageWorker 프로토콜을 채택하여 반복적인 프로토콜 연관 값 제약 코드를 방지할 수 있습니다. 579 | 580 | ```swift 581 | // Before : 582 | final class ImageProcessor 583 | where W.Input == UIImage, W.Output == Bool { ... } 584 | 585 | // After 586 | final class ImageProcessor { ... } 587 | ``` 588 | 589 | 연관 값을 가진 프로토콜과 제네릭은 추상적인 코드를 만들어 줍니다. 590 | 하지만 연관 값을 가진 프로토콜은 훌륭한 방법이지만 코드를 다소 어렵게 만듭니다. 591 | 항상 trade-off는 존재합니다. 592 | 593 | You don't always have to make things difficult, however. Sometimes a single generic or concrete code is enough to give you what you want. 594 | 595 | ## Summary 596 | - You can use protocols as generic constraints. But protocols can also be used as a type at runtime (dynamic dispatch) when you step away from generics. 597 | - Using protocols as a generic constraint is usually the way to go, until you need dynamic dispatch. 598 | - Associated types are generics that are tied to a protocol. 599 | - Protocols with associated types allow a concrete type to define the associated type. Each concrete type can specialize an associated type to a different type. 600 | - Protocols with Self requirements are a unique flavor of associated types referencing the current type. 601 | - Protocols with associate types or Self requirements force you to reason about types at compile time. 602 | - You can make a protocol inherit another protocol to further constrain its associated types. 603 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftInDepth 2 | 3 | ![image](https://github.com/hongjunehuke/Swift-in-depth/assets/83629193/5505a939-8807-49e6-b62e-2b2c8c5f9f4c) 4 | 5 | Swift In Depth를 읽고 정리한 리포지토리입니다. 6 | 7 | 좀 더 스위프트 언어에 어울리는 코드를 짜고 싶어 읽기 시작했습니다. 8 | 9 | ## Tools 10 | 11 | - :books: [Swift In Depth](https://www.manning.com/books/swift-in-depth) ([Ebook link](https://livebook.manning.com/book/swift-in-depth/about-this-book/14)) 12 | - [Swift In Depth example codes](https://github.com/tjeerdintveen/manning-swift-in-depth) 13 | - Xcode 10 and Swift 4.2. 14 | 15 | ### brief contents 16 | 17 | | | Chapter Name | Date | 18 | | ---- | ------------------------------------------------------------ | ------------ | 19 | | 1 | [Modeling data with enum](https://github.com/hongjunehuke/swift-in-depth/blob/master/Modeling-data-with-enums.md) | 2023.11.16 | 20 | | 2 | [Writing cleaner properties](https://github.com/hongjunehuke/swift-in-depth/blob/master/Writing_cleaner_properties.md) | 2023.11.18 | 21 | | 3 | [Making optionals second nature](Making-optionals-second-nature.md) | 2023.11.19 | 22 | | 4 | [Demystifying initializers](https://github.com/hongjunehuke/swift-in-depth/blob/master/Demystifying-initializers.md) | 2023.11.28 | 23 | | 5 | [Effortless error handling](Effortless-error-handling.md) | 2023.12.10 | 24 | | 6 | [Generics](Generics.md) | 2023.12.17 | 25 | | 7 | [Putting the pro in protocol-oriented programming](Putting-the-pro-in-protocol-oriented-programming.md) | 2023.12.23 | 26 | | 8 | [Iterators, sequences, and collections](Iterators-sequences-and-collections.md) | 2023.12.31 | 27 | | 9 | [Understanding map, flatMap, and compactMap](Understanding_map,flatMap,and_compactMap.md) | 2024.01.11 | 28 | | 10 | [Asynchronous error handling with Result](Asynchronous_error_handling_with_Result.md) | 2024.01.24 | 29 | | 11 | [Protocol extensions](Protocol_extensions.md) | 2024.01.31 | 30 | | 12 | [Swift patterns](Swift_patterns.md) | 2024.02.07 | 31 | | 13 | [Delivering quality Swift code](Delivering_quality_Swift_code.md) | 2024.02.11 | 32 | -------------------------------------------------------------------------------- /Swift_patterns.md: -------------------------------------------------------------------------------- 1 | # Swift patterns 2 | 3 | ## This chapter covers 4 | - Mocking types with protocols and associated types 5 | - Understanding conditional conformance and its benefits 6 | - Applying conditional conformance with custom protocols and types 7 | - Discovering shortcomings that come with protocols 8 | - Understanding type erasure 9 | - Using alternatives to protocols 10 | 11 | ## Dependency injection 12 | 13 | 앞으로 스위프트 개발에 유용한 패턴들을 살펴보겠습니다. 14 | 15 | 먼저 의존성 주입(Dependency injection)을 살펴봅시다. 16 | 17 | 의존성 주입에서 말하는 의존성이란 서로 다른 객체 사이에 의존 관계를 말합니다. 18 | A 클래스에 B 클래스 객체를 인스턴스로 사용한다면 A 클래스와 B 클래스 사이에 의존 관계가 있는 것입니다. 19 | 20 | 다시 말해, 의존하는 객체가 수정되면 다른 객체도 영향을 받게 됩니다. 21 | 22 | 또한 의존성 주입에서 주입이란 생성자를 활용한 방법 등으로 외부에서 생성한 객체를 넣는 것을 의미합니다. 23 | 24 | 의존성을 가진 코드가 많을수록 코드의 재사용성이 떨어집니다. 25 | 재사용을 위해 코드를 수정하면 매번 의존성을 가진 객체들을 함께 수정해야 하기 때문입니다. 26 | 27 | 의존성을 강하게 가지는 코드를 의존성 주입으로 의존 관계를 끊을 수 있습니다. 28 | 29 | 또한 의존성 주입을 통해 아래와 같은 이점을 얻을 수 있습니다. 30 | 31 | 1. Unit Test가 용이해집니다. 32 | 2. 코드의 재활용성을 높여줍니다. 33 | 3. 객체 간의 의존성(종속성)을 줄이거나 없앨 수 있습니다. 34 | 4. 객체 간의 결합을 낮추면서 유연한 코드를 작성할 수 있습니다. 35 | 36 | 의존성 주입은 DIP(의존 관계 역전 법칙)와도 관련이 깊습니다. 37 | 38 | 의존 관계 역전 법칙은 객체 지향 프로그래밍 설계의 다섯 가지 기본 원칙(SOLID) 중 하나입니다. 39 | 40 | 추상화된 것은 구체적인 것에 의존하면 안 되고, 구체적인 것이 추상화된 것에 의존해야 합니다. 41 | 즉, 구체적인 객체는 추상화된 객체에 의존해야 한다는 것이 핵심입니다. 42 | 43 | 스위프트에서 말하는 추상화는 프로토콜로 구현할 수 있습니다. 44 | 45 | **Swapping an implementation** 46 | 47 | 구현부를 교환할 수 있는 특징은 의존성 주입에 의해 만들어집니다. 48 | 지금부터 의존성 주입을 활용하여 구현부를 교환할 수 있는 WeatherAPI를 구현할 것입니다. 49 | 50 | 구체적 객체에 의존하지 않고 추상화된 객체에 의존하기 때문에 추상화된 객체를 따르는 여러 구현부를 교환할 수 있게 됩니다. 51 | 52 | WeatherAPI는 실제 네트워크 세션(URLSession), 오프라인 네트워크 세션(OfflineURLSession) 그리고 테스트 세션(MockSession) 구현부에 모두 대응하도록 구현합니다. 53 | 세 가지 세션은 WeaterAPI의 생성자로 주입 받는 객체(구현부)가 됩니다. 54 | 55 | WeatherAPI가 URLSession과 같은 구체적인 타입에 의존하면 강한 의존 관계가 형성되며 OfflineURLSession이나 MockSession이 주입되었을 때 대응하지 못합니다. 56 | 따라서 세 가지 세션을 추상화한 Session 프로토콜을 WeatherAPI에서 의존해야 합니다. 57 | 58 | 추상화된 Session 프로토콜에 의존하기 때문에 주입 받는 세션의 타입과 상관없이 Session 프로토콜의 함수를 사용할 수 있습니다. 59 | 60 | 의존성 주입을 통해 WeatherAPI는 URLSession, OfflineURLSession 그리고 MockSession 타입과 강한 의존 관계를 만들지 않게 됩니다. 61 | 62 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/c0e48dae-9db6-48a0-a787-0575b6ed8b96) 63 | 64 | WeatherAPI가 가진 Session 프로토콜에 의해 Session 프로토콜을 따르는 URLSession, OfflineSession, MockSession 타입을 주입 받을 수 있습니다. 65 | 의존성 주입으로 구현부를 교환할 수 있습니다. 66 | 67 | Session 프로토콜을 코드로 구현해 봅시다. 68 | 69 | ```swift 70 | protocol Session { 71 | associatedType Task 72 | 73 | func dataTask( 74 | with url: URL, 75 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 76 | ) -> Task 77 | } 78 | ``` 79 | 80 | dataTask 함수에서 URLSessionDataTask나 다른 특정 타입을 리턴하지 않고 연관 값 Task를 리턴하도록 구현했습니다. 81 | 프로토콜의 연관 값은 컴파일 타임에 타입이 결정되기 때문에 여러 타입에 대응할 수 있습니다. 82 | 83 | WeatherAPI가 Session 프로토콜에 의존하기 때문에 WeatherAPI에 주입할 URLSession 타입은 Session 프로토콜을 따라야 합니다. 84 | 85 | ```swift 86 | extension URLSession: Session {} 87 | ``` 88 | 89 | 본격적으로 WeatherAPI의 구현부를 살펴봅시다. 90 | 91 | ```swift 92 | final class WeatherAPI { 93 | let session: S 94 | 95 | init(session: S) { 96 | self.session = session 97 | } 98 | 99 | func run() { 100 | guard let url = URL(string: "https://www.someweatherstartup.com") else { 101 | fatalError("Could not create url") 102 | } 103 | let task = session.dataTask(with: url) { (data, response, error) in 104 | // Work with retrieved data. 105 | } 106 | task.resume() 107 | } 108 | } 109 | 110 | let weatherAPI = WeatherAPI(session: URLSession.shared) 111 | weatherAPI.run() 112 | ``` 113 | 114 | WeatherAPI는 제네릭 타입을 활용하여 Session 프로토콜을 따르는 구현부를 교환하여 입력받도록 만들었습니다. 115 | 116 | 위에서 task.resume() 함수를 호출하고 있지만, 아직 Session 프로토콜의 연관 값인 Task에 resume 함수를 구현하지 않았습니다. 117 | resume 함수를 가진 DataTask 프로토콜을 만들고 Session 프로토콜의 연관 값인 Task를 DataTask로 타입 제약한다면, task의 resume 함수를 호출할 수 있습니다. 118 | 119 | 아래 코드로 살펴봅시다. 120 | 121 | ```swift 122 | protocol DataTask { 123 | func resume() 124 | } 125 | 126 | protocol Session { 127 | // 연관 값 Task를 DataTask 타입으로 제약합니다. 128 | associatedtype Task: DataTask 129 | 130 | func dataTask( 131 | with url: URL, 132 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 133 | ) -> Task 134 | } 135 | ``` 136 | 137 | 먼저 DataTask 프로토콜을 만들고 resume 함수를 선언했습니다. 138 | 139 | 이후 Session 프로토콜의 연관 값 Task에 DataTask 프로토콜로 타입 제약을 걸어 dataTask 함수가 리턴하는 Task 타입의 객체에 resume 함수를 호출할 수 있게 되었습니다. 140 | 141 | 지금까지의 추상화 과정을 그림으로 살펴봅시다. 142 | 143 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/ab8ce8de-e1c1-4388-b25b-a3ec806ec9ee) 144 | 145 | 구체적인 객체인 URLSession과 URLSessionDataTask 모두 추상화(protocol)된 Session 프로토콜과 DataTask 프로토콜에 의존하고 있습니다. 146 | 147 | URLSession에서 쓰일 URLSessionDataTask도 DataTask 프로토콜을 따르도록 만들어주면 URLSession 코드가 완성됩니다. 148 | 149 | ```swift 150 | extension URLSessionDataTask: DataTask {} 151 | ``` 152 | 153 | 이제 URLSession을 생성하여 WeatherAPI에 주입할 수 있게 되었습니다. 154 | 물론 URLSession 이외에도 Session 프로토콜을 따르는 타입이라면 WeatherAPI의 생성자로 주입할 수 있습니다. 155 | 156 | 지금부터 URLSession 이외에 OfflineURLSession과 MockSession을 생성하여 WeatherAPI가 구현부를 교체(Swapping an implementation)해 보겠습니다. 157 | 158 | 의존성 주입으로 WeatherAPI가 구체적 타입이 아닌 추상화에 의존하기 때문에 구현부를 URLSession, OfflineURLSession, MockSession 등으로 교체해도 WeatherAPI에 수정할 부분이 없습니다. 159 | 160 | 먼저 OfflineURLSession과 OfflineTask를 구현하겠습니다. 161 | 162 | ```swift 163 | final class OfflineURLSession: Session { 164 | var sessions = [URL: OfflineTask]() 165 | 166 | func dataTask( 167 | with url: URL, 168 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 169 | ) -> OfflineTask { 170 | let task = OfflineTask(completionHandler: completionHandler) 171 | sessions[url] = task 172 | return task 173 | } 174 | } 175 | 176 | enum ApiError: Error { 177 | case couldNotLoasData 178 | } 179 | 180 | struct OfflineTask: DataTask { 181 | typealias Completion = (Data?, URLResponse?, Error?) -> Void 182 | let completionHandler: Completion 183 | 184 | init(completionHandler: @escaping Completion) { 185 | self.completionHandler = completionHandler 186 | } 187 | 188 | func resume() { 189 | let url = URL(fileURLWithPath: "prepared_response.json") 190 | let data = try! Data(contentsOf: url) 191 | completionHandler(data, nil, nil) 192 | } 193 | } 194 | ``` 195 | 196 | 위의 OfflineURLSession은 서버 연결 없이 로컬 데이터가 로드되도록 하여 서버 환경에 구애받지 않고 WeatherAPI 클래스를 테스트할 수 있도록 합니다. 197 | 198 | 우리는 의존성 주입으로 객체 간의 의존 관계를 서로가 아니라 추상적 대상에 두어 구현부를 교체할 때 추가적인 코드 수정이 발생하지 않도록 만듭니다. 199 | 아래 코드와 같이 단지 주입하는 객체만 달라질 뿐, 구현부가 교체되었다고 해서 WeatherAPI의 코드를 수정하지 않습니다. 200 | 201 | ```swift 202 | let productionAPI = WeatherAPI(session: URLSession.shared) 203 | let offlineAPI = WeatherAPI(session: OfflineURLSession()) 204 | ``` 205 | 206 | Session 프로토콜이 Task 연관 값을 활용하는 것처럼 URLSession, OfflineURLSession, MockSession 등 하나의 타입으로 인스턴스화 하기 어려울 때 프로토콜의 연관 값은 유용하게 사용됩니다. 207 | 208 | 이번에는 MockSession과 MockTask를 구현해 WeatherAPI 클래스를 테스트하는 코드를 작성해 봅시다. 209 | 210 | ```swift 211 | class MockSession: Session { 212 | let expectedURLs: [URL] 213 | let expectation: XCTestExpectation 214 | 215 | init(expectation: XCTestExpectation, expectedURLs: [URL]) { 216 | self.expectation = expectation 217 | self.expectedURLs = expectedURLs 218 | } 219 | 220 | func dataTask( 221 | with url: URL, 222 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 223 | ) -> MockTask { 224 | return MockTask(expectedURLs: espectedURLs, url: url, expectation: expectation) 225 | } 226 | } 227 | 228 | struct MockTask: DataTask { 229 | let expectedURLs: [URL] 230 | let url: URL 231 | let expectation: XCTestExpectation 232 | 233 | func resume() { 234 | guard expectedURLs.contains(url) else { 235 | return 236 | } 237 | self.expectation.fulfill() 238 | } 239 | } 240 | ``` 241 | 242 | 이번에는 API를 테스트하는 APITestCase 클래스를 살펴봅시다. 243 | 244 | ```swift 245 | class APITestCase: XCTestCase { 246 | var api: API! 247 | 248 | func testAPI() { 249 | let expectation = XCTestExpectation(description: "Expected someweatherstartup.com") 250 | let session = MockSession(expectation: expectation, expectedURLs: [URL(string: "www.someweatherstartup.com")!]) 251 | api = API(session: session) 252 | api.run() 253 | wait(for: [expectation], timeout: 1) 254 | } 255 | } 256 | 257 | let testcase = APITestCase() 258 | testcase.testAPI() 259 | ``` 260 | 261 | Session 프로토콜을 확장하여 dataTask 함수의 구현부를 제공할 수도 있습니다. 262 | 기존의 dataTask 함수가 아닌, Result 타입 클로저를 가진 dataTask 함수를 새로 만들고 해당 함수의 구현부를 프로토콜 확장에서 제공해 봅시다. 263 | 264 | 챕터11에서 살펴본 Result 타입을 활용해 (Data?, URLResponse?, Error?)를 Result로 변형시켜 다루면 에러 핸들링에 유리합니다. 265 | 266 | 아래 코드로 살펴봅시다. 267 | 268 | ```swift 269 | protocol Session { 270 | associatedtype Task: DataTask 271 | 272 | func dataTask( 273 | with url: URL, 274 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void 275 | ) -> Task 276 | 277 | func dataTask( 278 | with url: URL, 279 | completionHandler: @escaping (Result) -> Void 280 | ) -> Task 281 | } 282 | 283 | extension Session { 284 | func dataTask( 285 | with url: URL, 286 | completionHandler: @escaping (Result) -> Void 287 | ) -> Task { 288 | return dataTask(with: url, completionHandler: { data, response, error in 289 | if let error = error { 290 | let anyError = AnyError(error) 291 | completionHandler(Result.failure(anyError)) 292 | } else if let data = data { 293 | completionHandler(Result.success(data)) 294 | } else { 295 | fatalError() 296 | } 297 | }) 298 | } 299 | } 300 | 301 | // dataTask 함수 호출부 302 | URLSession.shared.dataTask(with: url) { (result: Result in 303 | // ... 304 | } 305 | 306 | OfflineURLSession().dataTask(with: url) { (result: Result in 307 | // ... 308 | } 309 | ``` 310 | 311 | 의존성 주입을 활용해 여러 구현(production, debugging, testing)에 대응하는 WeatherAPI 타입을 만들 수 있습니다. 312 | 또한 추상화(프로토콜)에 구체적 타입을 의존하게 만들어 구체적 타입끼리의 의존관계도 피할 수 있었습니다. 313 | 314 | ## Conditional conformance 315 | 316 | Conditional conformance를 직역하면 **조건부 적합성**입니다. 317 | 318 | 조건부 적합성은 Swift 4.1부터 도입되었고 특정 조건에서만 해당 타입이 프로토콜을 따르도록 만들 수 있습니다. 319 | 320 | ```swift 321 | protocol Purchaseable { 322 | func buy() 323 | } 324 | 325 | struct Book: Purchaseable { 326 | func buy() { 327 | print("Buy book") 328 | } 329 | } 330 | 331 | // 조건부 적합성 예시입니다. (Array의 Element 연관 값이 Purchaseable 프로토콜을 따를 때 Array도 Purchaseable 프로토콜을 따른다) 332 | extension Array: Purchaseable where Element: Purchaseable { 333 | func buy() { 334 | for element in self { 335 | element.buy() 336 | } 337 | } 338 | } 339 | ``` 340 | 341 | extension Array: Purchaseable where Element: Purchaseable과 같은 코드가 생소할 수 있습니다. 342 | 이는 Array 타입의 연관 값인 Element가 Purchaseable 프로토콜을 따를 때 Array 타입이 Purchaseable 프로토콜을 따른다는 조건부 적합성 코드입니다. 343 | 344 | With conditional conformance, you can make a type adhere to a protocol but only under certain conditions. 345 | 346 | **Free functionality** 347 | 348 | Equatable 프로토콜이나 Hashable 프로토콜을 채택하여 두 프로토콜에서 제공하는 함수를 별도의 구현 없이 사용하는 것이 조건부 적합성의 예입니다. 349 | 350 | 아래 코드와 같이 Equatable 프로토콜을 채택한 타입의 경우 == 함수 구현 없이 == 함수를 사용할 수 있습니다. 351 | 물론 동일하다는 조건을 수정하기 위해서는 == 함수를 오버라이드하여 재구현해야 합니다. 352 | 353 | ```swift 354 | struct Movie: Equatable { 355 | let title: String 356 | let rating: Float 357 | } 358 | 359 | let movie = Movie(title: "The princess bride", rate: 9.7) 360 | movie == movie // true. You can already compare without implementing Equatable. 361 | ``` 362 | 363 | Swift synthesizes this for free on some protocols, such as Equatable and Hashable, but not every protocol. 364 | For instance, you don't get Comparable for free, or the ones that you introduce yourself. 365 | 366 | Unfortunately, Swift doesn't synthesize methods on classes. 367 | 368 | **Conditional conformance on associated types** 369 | 370 | 그렇다면 조건부 적합성을 언제 사용하면 좋을까요? 371 | 372 | 결론부터 이야기하면 조건부 적합성은 제네릭을 인스턴스로 가지는 객체에 자주 쓰입니다. 373 | 374 | 내부 인스턴스가 특정 프로토콜을 따를 때 내부 인스턴스를 감싸는 객체 또한 해당 프로토콜을 따르도록 만들어 외부 객체에서도 해당 프로토콜의 함수를 사용하도록 만듭니다. 375 | 376 | ```swift 377 | protocol Sequence 378 | 379 | @frozen 380 | struct Array 381 | ``` 382 | 383 | Array는 Element 연관 값을 가졌기 때문에 조건부 적합성을 설명하기에 적합합니다. 384 | 385 | Array가 구조체인데 어떻게 연관 값을 가졌는지 의문일 수 있지만, Array의 연관 값 Element는 Sequence 프로토콜로부터 온 연관 값입니다. 386 | 387 | 조건부 적합성을 사용하지 않고 코드를 구현한다면 어떤 문제가 발생하는지 살펴봅시다. 388 | 아래 코드는 일반적인 프로토콜과 해당 프로토콜을 따르는 구조체입니다. 389 | 390 | ```swift 391 | protocol Track { 392 | func play() 393 | } 394 | 395 | struct AudioTrack: Track { 396 | let file: URL 397 | 398 | func play() { 399 | print("playing audio at \(file)") 400 | } 401 | } 402 | ``` 403 | 404 | Track 프로토콜을 따르는 객체를 배열에 저장할 때, 배열 자체에 play 함수를 만들어 배열의 play 함수가 호출될 때 배열 요소들의 play 함수가 호출되도록 할 생각입니다. 405 | 406 | 다시 말해, Array의 연관 값 Element가 Track 프로토콜을 따를 때, Array 타입에 play 함수를 제공합니다. 407 | 408 | 아래 코드로 살펴봅시다. 409 | 410 | ```swift 411 | extension Array where Element: Track { 412 | // Track 프로토콜의 play 함수와 별개로 Array 타입에 추가한 함수입니다. 413 | func play() { 414 | for element in self { 415 | // Element가 Track 프로토콜을 따르기 때문에 Track 프로토콜에서 제공하는 play 함수를 호출 가능합니다. 416 | self.play() 417 | } 418 | } 419 | } 420 | ``` 421 | 422 | 위 코드로 Array의 Element가 Track 프로토콜을 따를 때 Array 객체에 play 함수를 제공하게 됩니다. 423 | 424 | 하지만 문제는 Array 자체로 Track 프로토콜을 따르지 않는다는 점입니다. 425 | 단지 Element가 Track 프로토콜을 따를 때 Array를 확장해 추가한 play 함수를 사용할 뿐입니다. 426 | 427 | 따라서 Track 타입을 입력으로 받는 함수에 연관 값 Element가 Track 프로토콜을 따르는 Array를 넘길 수 없습니다. 428 | 429 | 또한 중첩 배열에서는 내부 값이 Track 프로토콜을 따르더라도 play 함수를 호출할 수 없습니다. 430 | Array 자체로 Track 프로토콜을 따르지 않기 때문입니다. 431 | 432 | 아래 코드로 위 문제들을 살펴봅시다. 433 | 434 | ```swift 435 | let tracks = [ 436 | AudioTrack(file: URL(fileURLWithPath: "1.mps")) 437 | AudioTrack(file: URL(fileURLWithPath: "2.mps")) 438 | ] 439 | 440 | // If An Array is nested, you can't call play() any more. 441 | [tracks, tracks].play() // error: type of expression is ambigous without more context 442 | 443 | // Or you can't pass an array if anything expects the Track protocol. 444 | func playDelayed(_ track: T, delay: Double) { 445 | // ...snip 446 | } 447 | 448 | playDelayed(tracks, delay: 2.0) // argument type '[AudioTrack]' does not conform to expected type 'Track' 449 | ``` 450 | 451 | **Making Array conditionally conform to a custom protocol** 452 | 453 | 위와 같은 문제는 조건부 적합성으로 해결할 수 있습니다. 454 | 455 | 지금까지는 Array의 Element가 특정 프로토콜을 따를 때 Array의 확장에서 함수를 제공하는 조건부 적합성을 만들었습니다. 따라서 Array가 특정 프로토콜을 따를 수 없었습니다. 456 | 457 | 이제는 Array의 Element가 특정 프로토콜을 따를 때 Array의 확장에서 함수를 제공하는 동시에 Array 또한 해당 프로토콜을 따르는 타입이 되도록 구현할 예정입니다. 458 | 459 | 아래 코드로 살펴봅시다. 460 | 461 | ```swift 462 | // Before. Not conditionally conforming!! 463 | extension Array where Element: Track { 464 | // ... snip 465 | } 466 | 467 | // After. You have conditional conformance!!! 468 | extension Array: Track where Element: Track { 469 | func play() { 470 | for element in self { 471 | element.play() 472 | } 473 | } 474 | } 475 | ``` 476 | 477 | extension Array: Track where Element: Track을 통해 Element 연관 값이 Track 프로토콜을 따를 때, Array 또한 Track 프로토콜을 따르는 타입이 됩니다. 478 | 479 | Array에 조건부 적합성을 적용해 Track 타입을 원하는 함수로 Array를 넘길 수 있고 중첩 배열에서 play 함수도 호출할 수 있습니다. 480 | 481 | ```swift 482 | let nestedTracks = [ 483 | [ 484 | AudioTrack(file: URL(fileURLWithPath: "1.mps")), 485 | AudioTrack(file: URL(fileURLWithPath: "2.mps")) 486 | ], 487 | [ 488 | AudioTrack(file: URL(fileURLWithPath: "3.mps")), 489 | AudioTrack(file: URL(fileURLWithPath: "4.mps")) 490 | ] 491 | ] 492 | 493 | // Nesting works. 494 | nestedTracks.play() 495 | 496 | // And, you can pass this array to a function expecting a Track! 497 | playDelayed(tracks, delay: 2.0) 498 | ``` 499 | 500 | **Conditional conformance and generics** 501 | 502 | 지금까지 연관 값을 가진 프로토콜에 조건부 적합성을 적용했다면, 이제는 조건부 적합성을 제네릭 타입에 사용해 봅시다. 503 | 조건부 적합성은 연관 값을 가진 프로토콜에도 사용할 수 있지만 제네릭 타입에도 사용할 수 있습니다. 504 | 505 | Optional은 대표적인 제네릭 타입을 가진 열거형입니다. 506 | 열거형 Optional은 Wrapped 제네릭 타입을 가지고 있습니다. 507 | 508 | 아래 코드로 Optional에 조건부 적합성을 적용한 코드를 살펴봅시다. 509 | 510 | ```swift 511 | // Optional의 연관 값 Wrapped가 Track 프로토콜을 따르면 Optional도 Track 프로토콜을 따릅니다. 512 | extension Optional: Track where Wrapped: Track { 513 | func play() { 514 | switch self { 515 | case .some(let track): 516 | track.play() 517 | case nil: 518 | break // do nothing 519 | } 520 | } 521 | } 522 | ``` 523 | 524 | 위 코드는 Optional의 제네릭 타입인 Wrapped가 Track 프로토콜을 따를 때, Optional 또한 Track 프로토콜을 따르고 play 함수를 제공합니다. 525 | 526 | 물론 extension Optional: Track where Wrapped: Track 방식이 아닌 extension Optional where Wrapped: Track 코드를 사용해도 play 함수를 제공받지만, Optional 자체가 Track 타입이 되진 못합니다. 527 | 528 | ```swift 529 | let audio: AudioTrack? = AudioTrack(file: URL(fileURLWithPath: "1.mps")) 530 | audio?.play() 531 | ``` 532 | 533 | audio가 Optional(AudioTrack) 타입이기 때문에 audio?는 Optional(AudioTrack)의 Value 값이므로 Track 타입입니다. 따라서 Track 타입을 입력 받는 함수에 audio?를 넣을 수 있습니다. 534 | 535 | ```swift 536 | let audio: AudioTrack? = AudioTrack(file: URL(fileURLWithPath: "1.mps")) 537 | playDelayed(audio, delay: 2.0) 538 | ``` 539 | 540 | **Conditional conformance on your types** 541 | 542 | 위에서 봤듯이 조건부 적합성은 제네릭 인스턴스를 가진 타입이나 연관 값을 가진 프로토콜에 사용하기 좋습니다. 543 | 544 | Conditional conformace becomes powerful when you hace a generic type storing an inner type, and you want the generic type to mimic the behavior of the inner type inside. 545 | 546 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/c2e1874a-494e-4cf7-93b6-fb3f45281ba8) 547 | 548 | 조건부 적합성을 통해서 inner 값의 타입이 Track 타입일 때 Array가 Track 타입이 됩니다. 549 | 550 | Optional의 제네릭 타입 외에도 커스텀 제네릭 타입에도 조건부 적합성을 적용해 봅시다. 551 | 552 | CachedValue 타입을 만들어 커스텀 제네릭 타입에 조건부 적합성을 적용해 봅시다. 553 | CachedValue는 시간이 오래 소요되는 계산의 결과를 캐싱하고 리프레쉬 전까지 캐싱한 결과를 유지합니다. 554 | 555 | 먼저 CachedValue가 동작하는 방식을 살펴봅시다. 556 | 557 | ```swift 558 | // CachedValue를 생성하고 커스텀 클로저를 전달합니다. 559 | let simplecache = CachedValue(timeToLive: 2, load: { () -> String in 560 | print("I am being refreshed!") 561 | return "I am the value inside CachedValue" 562 | } 563 | 564 | // Prints: "I am being refreshed!" 565 | simplecache.value // "I am the value inside CachedValue" 566 | simplecache.value // "I am the value inside CachedValue" 567 | 568 | sleep(3) // wait 3 seconds 569 | 570 | // Prints: "I am being refreshed!" 571 | simplecache.value // "I am the value inside CachedValue" 572 | ``` 573 | 574 | 아래 코드로 CachedValue의 구현부를 살펴봅시다. 575 | 576 | ```swift 577 | final class CachedValue { 578 | private let load: () -> T 579 | private var lastLoaded: Date 580 | 581 | private var timeToLive: Double 582 | private var currentValue: T 583 | 584 | public var value: T { 585 | let needsRefresh = abs(lastLoaded.timeIntervalSinceNow) > timeToLive 586 | if needsRefresh { 587 | currentValue = load() 588 | lastLoaded = Date() 589 | } 590 | return currentValue 591 | } 592 | 593 | init(timeToLive: Double, load: @escaping (() -> T)) { 594 | self.timeToLive = timeToLive 595 | self.load = load 596 | self.currentValue = load() 597 | self.lastLoaded = Date() 598 | } 599 | } 600 | ``` 601 | 602 | 아직 CachedValue 타입은 조건부 적합성이 적용되지 않았습니다. 603 | 604 | CachedValue의 제네릭 타입이 Equatable을 따를 때, Comparable을 따를 때 그리고 Hashable을 따를 때, 제네릭 타입을 가진 CachedValue 또한 해당 프로토콜을 따르도록 조건부 적합성을 구현해 봅시다. 605 | 606 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/33637c6f-3da8-49a2-84a1-3c4c76772527) 607 | 608 | 코드로 살펴봅시다. 609 | 610 | ```swift 611 | // Conforming to Equatable 612 | extension CachedValue: Equatable where T: Equatable { 613 | static func == (lhs: CachedValue, rhs: CachedValue) -> Bool { 614 | return lhs.value == rhs.value 615 | } 616 | } 617 | 618 | // Conforming to Hashable 619 | extension CachedValue: Hashable where T: Hashable { 620 | func hash(into hasher: inout Hasher) { 621 | hasher.combine(value) 622 | } 623 | } 624 | 625 | // Conforming to Comparable 626 | extension CachedValue: Comparable where T: Comparable { 627 | static func <(lhs: CachedValue, rhs: CachedValue) -> Bool { 628 | return lhs.value < rhs.value 629 | } 630 | 631 | static func == (lhs: CachedValue, rhs: CachedValue) -> Bool { 632 | return lhs.value == rhs.value 633 | } 634 | } 635 | ``` 636 | 637 | 위와 같이 CachedValue에 조건부 적합성을 적용하면 아래 코드와 같이 사용할 수 있습니다. 638 | 639 | ```swift 640 | let cachedValueOne = CacheValue(timeToLive: 60) { 641 | // Perform expensive operation 642 | // E.g. Calculate the purpose of life 643 | return 42 644 | } 645 | 646 | let cachedValueTwo = CacheValue(timeToLive: 120) { 647 | // Perform another expensive operation 648 | return 100 649 | } 650 | 651 | cachedValueOne == cachedValueTwo // Equatable: You can check for equality. 652 | cachedValueOne > cachedValueTwo // Equatable: You can compare two cached values. 653 | 654 | let set = Set(arrayLiteral: cachedValueOne, cachedValueTwo) // Hashable: You can store CachedValue in a set 655 | ``` 656 | 657 | Conditional conformance works best when storing the lowest common denominator inside the generic, meaning that you should aim to not add too many constraints on T in the case. 658 | 659 | 조건부 적합성을 사용할 때 제네릭에 너무 많은 타입 제약을 걸지 않아야 합니다. 660 | 너무 많은 타입을 제약하지 않아야 제네릭 타입에 조건부 적합성을 적용할 경우 타입을 확장하기 쉽습니다. 661 | 662 | 과장해서 CacheValue의 제네릭 T가 10개의 프로토콜의 타입 제약을 받는다면 매우 적은 타입이 제네릭 T를 만족합니다. 663 | 따라서 조건부 적합성의 이점을 크게 얻을 수 없습니다. 664 | 665 | ## Dealing with protocol shortcomings 666 | 667 | 프로토콜은 좋은 방법이지만, 단점도 있습니다. 668 | 669 | 지금부터 프로토콜의 단점을 살펴보고 두 가지 방법(enum, type erasure)으로 해결해 봅시다. 670 | 671 | Hashable 타입을 Dictionary Key에 저장하거나 Array, Set에 저장할 때 프로토콜의 단점을 찾을 수 있습니다. 672 | 673 | Hashable 프로토콜을 따르는 PokerGame 프로토콜과 PokerGame 프로토콜을 따르는 StudPoker 구조체와 TexasHoldem 구조체를 만들었습니다. 674 | 675 | ```swift 676 | protocol PokerGame: Hashable { 677 | func start() 678 | } 679 | 680 | struct StudPoker: PokerGame { 681 | func start() { 682 | print("Starting StudPoker") 683 | } 684 | } 685 | 686 | struct TexasHolem: PokerGame { 687 | func start() { 688 | print("Starting Texas Hodlem") 689 | } 690 | } 691 | ``` 692 | 693 | 위의 PokerGame 프로토콜을 따르는 타입은 동시에 Hashable을 따르기 때문에 Set에 저장되거나 Dictionary의 Key에 저장될 수 있습니다. 694 | 695 | 하지만 아래와 같이 StudPoker 구조체나 TexasHolem 구조체 같은 특정 타입이 아니라 PokerGame 프로토콜로 Dictionary Key의 타입을 선언할 수 없습니다. 696 | 697 | If you want to mix and match different types of PokerGame as keys in a dictionary or inside an array, you stumble upon a shortcoming. 698 | 699 | 코드로 살펴봅시다. 700 | 701 | ```swift 702 | // This won't work 703 | var numberOfPlayers = [PlayerGame: Int]() 704 | 705 | // The error that the Swift compiler throws is: 706 | // error: using 'PokerGame' as a concrete type conforming to protocol 'Hashable' is not supported 707 | var numberOfPlayers = [PokerGame: Int]() 708 | ``` 709 | 710 | 프로토콜이 특정 타입(concrete type)으로 사용될 수 없기 때문에 프로토콜로 타입을 지정하고 해당 프로토콜을 따르는 타입들을 섞을 수 없습니다. 711 | 712 | PlayerGame 프로토콜을 따르는 StudPoker 구조체와 같이 특정 타입(concrete type)을 사용할 경우 [StudPoker: Int]와 같이 Dictionary의 Key나 Array에 저장할 수 있습니다. 713 | 714 | 하지만 StudPoker 타입을 사용할 경우 PlayerGame 프로토콜을 따르는 타입들을 섞어서 저장할 수 없습니다. 715 | 716 | 그렇다면 제네릭을 사용하면 추상화된 타입을 사용해 하위 타입들을 섞고 컴파일 에러도 피할 수 있을까요? 717 | 718 | 아래 코드와 같이 제네릭을 사용해 해결하려...해봅시다. (제네릭으로 문제를 해결할 수 없습니다.) 719 | 720 | ```swift 721 | func storeGame(games: [T]) -> [T: Int] { 722 | /// ... snip 723 | } 724 | ``` 725 | 726 | 하지만 위 코드와 같은 제네릭 함수는 PokerGame 프로토콜을 따르는 단일 타입으로만 결정됩니다. 727 | 결국 제네릭 타입을 사용해도 특정 프로토콜을 따르는 여러 타입을 섞을 수 없습니다. 728 | 729 | 아래와 코드와 같이 결국 단일 특정 타입으로 storeGame 함수의 입력 타입이 결정됩니다. 730 | 731 | ```swift 732 | func storeGame(games: [TexasHoldem]) -> [TexasHoldem: Int] { 733 | /// ... snip 734 | } 735 | 736 | func storeGame(games: [StudPoker]) -> [StudPoker: Int] { 737 | /// ... snip 738 | } 739 | ``` 740 | 741 | Again, you can't easily mix and match PokerGame types into a single container, such as a dictionary. 742 | 743 | 제네릭으로 해결할 수 없었지만, 열거형을 사용하거나 type erasure를 사용해 지금까지의 문제를 해결할 수 있습니다. 744 | 745 | **Avoiding a protocol using an enum** 746 | 747 | 열거형은 프로토콜과 달리 특정 타입(concrete type)입니다. 748 | 749 | 먼저 열거형의 케이스에 사용할 타입들을 넣습니다. 750 | 예를 들어 위에서 프로토콜 PokerGame을 따르는 StudPoker와 TexasHoldem 타입을 열거형의 케이스에 넣습니다. 751 | 752 | 아래 코드로 살펴봅시다. 753 | 754 | ```swift 755 | enum PokerGame: Hashable { 756 | case studPoker(StudPoker) 757 | case texasHoldem(TexasHoldem) 758 | } 759 | 760 | struct StudPoker: Hashable { 761 | // ... 구현부 생략 762 | } 763 | 764 | struct TexasHoldem: Hashable { 765 | // ... 구현부 생략 766 | } 767 | ``` 768 | 769 | 프로토콜과 달리 StudPoker와 TexasHoldem 구조체 모두 Hashable 프로토콜을 채택해야 합니다. 770 | 771 | 위와 같이 섞어서 사용할 타입들을 열거형의 케이스에 넣어 Set, Array 그리고 Dictionary의 Key 등에 사용할 수 있습니다. 772 | 773 | 이때 프로토콜과 달리 열거형은 특정 타입(concrete type)이기 때문에 컴파일 에러를 피할 수 있습니다. 774 | 775 | **Type erasing a protocol** 776 | 777 | 프로토콜을 Dictionary Key, Array, Set 등의 타입으로 사용할 수 없을 때 위와 같이 열거형을 사용하는 방식은 유용합니다. 778 | 779 | 하지만 많은 타입을 섞으려 할 경우 너무 많은 케이스를 가진 열거형을 만들어 혼란스럽게 됩니다. 780 | 781 | 또한 프로토콜은 테스트를 위한 의존성 주입을 위해 꼭 필요합니다. (열거형으로 대체되기 어려울 수 있습니다.) 782 | 783 | 이때 우리는 Type erasing을 사용해 프로토콜을 사용하면서 프로토콜을 따르는 타입들을 섞어서 사용할 수 있습니다. 784 | Type erasing으로 컨테이너에 타입을 감싸서 기존 컴파일 타임 프로토콜을 런타임으로 옮길 수 있습니다. 785 | 786 | 그렇다면 PockerGame 예시를 Type erasing 한 구조를 그림으로 살펴봅시다. 787 | 788 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/7cc1901f-2ec6-4fe6-8a7b-b773d0508b77) 789 | 790 | Type erasing으로 프로토콜을 따르는 변수를 감싸는 컨테이너는 Any를 붙인 변수명으로 대게 선언합니다. 791 | 792 | 위 그림과 같이 Type erasing은 프로토콜을 따르는 인스턴스를 만들어 구조체로 감싸는 개념입니다. 793 | PokerGame 예시에서도 PokerGame 프로토콜을 따르는 변수(타입)를 만들어 AnyPokerGame 구조체로 감싸고 있습니다. 794 | 795 | 이때 AnyPokerGame 구조체도 PokerGame 프로토콜을 따르도록 하여 AnyPokerGame 객체에 PokerGame 프로토콜이 제공하는 함수를 호출했을 때 함수 호출을 내부 인스턴스로 전달해야 합니다. 796 | 797 | Type erasing을 통해 외부 컨테이너는 구조체로 특정 타입(concrete type)이기 때문에 Array, Set, 그리고 Dictionary의 Key로 넣을 수 있습니다. 798 | 799 | 또한 외부 컨테이너의 내부 값이 PokerGame 프로토콜을 따르는 변수로 PokerGame 프로토콜의 자식 프로토콜을 섞어서 저장할 수 있게 됩니다. 800 | 801 | Each AnyPokerGame can store a different PokerGame type. 802 | 803 | 이제 AnyPokerGame은 구조체로 특정 타입(concrete type)이 되었기 때문에 AnyPokerGame을 가진 Array, Set, Dictionary 등에 넣어 AnyPokerGame의 내부 값을 PokerGame 프로토콜을 따르는 다양한 타입을 저장할 수 있습니다. 804 | 805 | ![image](https://github.com/hongjunehuke/swift-in-depth/assets/83629193/def7b28a-0489-4650-8934-7a75705287d5) 806 | 807 | 이번에는 코드로 AnyPokerGame이 동작하는 방식을 살펴봅시다. 808 | 이제 AnyPokerGame 타입을 Array, Set, Dictionary key로 사용할 수 있습니다. 809 | 810 | ```swift 811 | let studPoker = StudPoker() 812 | let holdEm = TexasHoldem() 813 | 814 | // You can mix multiple poker game inside an array. 815 | // Wrapper type으로 추상화합니다. 816 | let games: [AnyPokerGame] = [ 817 | AnyPokerGame(studPoker), 818 | AnyPokerGame(holdEm) 819 | ] 820 | 821 | games.forEach { (pokerGame: AnyPokerGame) in 822 | // Wrapper type의 start 함수 호출 시 inner value의 start 함수가 호출되도록 Wrapper type에 구현해야 합니다. 823 | pokerGame.start() 824 | } 825 | 826 | // You can store them inside a Set, too 827 | let setOfGames: Set = [ 828 | AnyPokerGame(studPoker), 829 | AnyPokerGame(holdEm) 830 | ] 831 | 832 | // You can even use poker games as keys! 833 | var numberOfPlayers = [ 834 | AnyPokerGame(studPoker): 300, 835 | AnyPokerGame(holdEm): 400 836 | ] 837 | ``` 838 | 839 | 이전 장에서 살펴본 AnyError(Result 속 error에 들어가는 모든 타입에 대응, 모든 error를 대표 가능)와 AnyIterator 모두 type erased가 사용된 것들입니다. 840 | 841 | 다시 말해, PokerGame을 감싸는 AnyPokerGame 구조체는 내부 값으로 PokerGame 프로토콜을 따르는 변수를 가지고 AnyPokerGame 또한 PokerGame을 채택합니다. 842 | 843 | AnyPokerGame이 PokerGame을 채택하여 PokerGame이 제공하는 함수가 호출되었을 때 AnyPokerGame에서 내부 값으로 함수 호출을 전달할 수 있습니다. 844 | 845 | 이제 AnyPokerGame 구조체의 구현부를 살펴봅시다. 846 | 847 | ```swift 848 | struct AnyPokerGame: PokerGame { 849 | 850 | private let _start: () -> Void 851 | private let _hashable: AnyHashable 852 | 853 | init(_ pokerGame: Game) { 854 | _start = pokerGame.start 855 | _hashable: AnyHashable(pokerGame) 856 | } 857 | 858 | func start() { 859 | // Wrapper 타입에서 type erasing 하려는 프로토콜 타입 인스턴스로 함수 호출을 전달합니다. 860 | _start() 861 | } 862 | } 863 | 864 | extension AnyPokerGame: Hashable { 865 | func hash(into hasher: inout Hasher) { 866 | _hashable.hash(into: &hasher) 867 | } 868 | 869 | static func ==(lhs: AnyPokerGame, rhs: AnyPokerGame) -> Bool { 870 | return lhs._hashable == rhs._hashable 871 | } 872 | } 873 | ``` 874 | 875 | PokerGame 프로토콜이 Hashable 프로토콜을 따르기 때문에 AnyPokerGame 구조체도 Hashable 프로토콜을 따르도록 해야 합니다. 876 | 877 | 이때 type erased가 사용된 AnyHashable을 사용해 Hashable 프로토콜이 제공하는 함수를 AnyPokerGame에서도 제공할 수 있습니다. 878 | 879 | AnyPokerGame 구현부처럼 함수의 호출을(start 함수 호출) Wrapped type의 내부 인스턴스로 전달해야 합니다. 880 | 881 | PokerGame 프로토콜이 start 함수만 가졌기 때문에 AnyPokerGame이 start 함수만 전달하도록 구현했지만, 더 많은 함수를 PokerGame 프로토콜이 가졌다면 모두 함수 호출을 내부 인스턴스로 전달해야 합니다. 882 | 883 | AnyPokerGame wraps any PokerGame type, and now you're free to use AnyPokerGame inside Collections. 884 | 885 | ## An alternative to protocols 886 | 887 | 프로토콜은 지금까지 살펴봤던 것처럼 유용합니다. 888 | 특히 복잡한 API를 구현할 때 API 사용하는 곳에서 알지 못하도록 복잡성을 숨길 수 있습니다. 889 | 890 | 하지만 프로토콜의 오용은 프로젝트에 불필요한 복잡성을 증가시킵니다. 891 | 892 | 따라서 프로토콜의 대안으로 **제네릭 구조체**를 살펴볼 예정입니다. 893 | 894 | 먼저 프로토콜을 사용하여 데이터의 유효성 여부를 검사하는 Validator 구조체를 예시로 살펴봅시다. 895 | 구현부는 아래 코드와 같습니다. 896 | 897 | ```swift 898 | protocol Validator { 899 | associatedtype Value 900 | func validate(_ value: Value) -> Bool 901 | } 902 | 903 | struct MinimalCountValidator: Validator { 904 | let minimalChars: Int 905 | 906 | func validate(_ value: String) -> Bool { 907 | guard minimalChars > 0 else { return true } 908 | // isEmpty is faster than count check 909 | guard !value.isEmpty else { return false } 910 | return value.count >= minimalChars 911 | } 912 | } 913 | 914 | let validator = MinimalCountValidator(minimalChars: 5) 915 | validator.validate("1234567890") // true 916 | ``` 917 | 918 | 프로토콜을 사용한 방식은 프로토콜이 가진 연관 값의 타입에 따라 각각의 프로토콜 구현부를 구현해야 합니다. 919 | 920 | **Creating a generic struct** 921 | 922 | 이때 Validator를 프로토콜이 아닌 제네릭 구조체로 만들면, 각 타입에 대응하는 구현부를 만들 필요 없이 여러 타입에 대응하는 하나의 구현부를 만들 수 있습니다. 923 | 924 | 아래 코드와 같이 Validator 제네릭 구조체를 만들었습니다. 925 | 926 | ```swift 927 | struct Validator { 928 | let validate: (T) -> Bool 929 | 930 | init(validate: @escaping (T) -> Bool) { 931 | self.validate = validate 932 | } 933 | } 934 | ``` 935 | 936 | 제네릭 구조체에서는 유효성의 조건을 클로저로 입력받아 여러 타입을 하나의 구현부로 대응할 수 있게 됩니다. 937 | 938 | 또한 Validator 구조체를 확장하여 함수도 추가할 수 있습니다. 939 | 940 | ```swift 941 | extension Validator { 942 | func combine(_ other: Validator) -> Validator { 943 | let combinedValidator = Validator(validate: { (value: T) -> Bool in 944 | let ownResult = self.validate(value) 945 | let otherResult = other.validate(value) 946 | return ownResult && otherResult 947 | }) 948 | return combinedValidator 949 | } 950 | } 951 | 952 | let notEmpty = Validator(validate: { string -> Bool in 953 | return !string.isEmpty 954 | }) 955 | 956 | let maxTenChars = Validator(validate: { string -> Bool in 957 | return string.count <= 10 958 | }) 959 | 960 | let combinedValidator: Validator = notEmpty.combine(maxTenChars) 961 | combinedValidator.validate("") // false 962 | combinedValidator.validate("Hi") // true 963 | combinedValidator.validate("This one is way too long") // false 964 | ``` 965 | 966 | 이처럼 제네릭 구조체는 프로토콜의 대안이 될 수 있습니다. 967 | 968 | **Rules of thumb for polymorphism** 969 | 970 | Here are some heuristics to keep in mind when reasoning about polymorphism in Swift. 971 | 972 | - Light-weight polymorphism -> Use enums. 973 | - A type that needs to work with multiple types -> Make a generic type. 974 | - A type that needs a single configurable implementation -> Store a closure. 975 | - A type that works on multiple types and has a single configurable implementation -> Use a generic struct or class that stores a closure. 976 | - When you need advanced polymorphism, default extensions, and other advanced use cases -> Use protocols. 977 | 978 | ## Summary 979 | - You can use protocols as an interface to swap out implementations, for testing, or for other use cases. 980 | - An associated type can resolve to a type that you don't own. 981 | - With conditional conformance, a type can adhere to a protocol, as long as its generic type or associated type adheres to this protocol. 982 | - Conditional conformance works well when you have a generic type with very few constraints. 983 | - A protocol with associated types or Self requirements can't be used as a concrete type. 984 | - Sometimes, you can replace a protocol with an enum, and use that as a concrete type. 985 | - You can use a protocol with associated types or Self requirements at runtime via type erasure. 986 | - Often a generic struct is an excellent alternative to a protocol. 987 | - Combining a higher-order function with a generic struct enables you to create hightly flexible types. 988 | -------------------------------------------------------------------------------- /Writing_cleaner_properties.md: -------------------------------------------------------------------------------- 1 | # Writing cleaner properties 2 | 3 | ## This chapter cover 4 | - How to create getter and setter computed properties 5 | - When(not) to use computed properties 6 | - Improving performance with lazy properties 7 | - How lazy properties behave with structs and mutability 8 | - Handling stored properties with behavior 9 | 10 | ## Computed properties 11 | 연산 프로퍼티는 프로퍼티로 가장한 함수로 여겨집니다. 어떤 값을 저장하지 않지만, 저장 프로퍼티와 형태는 유사합니다. 12 | 연산 프로퍼티에 접근할 때마다 동작합니다. "always up to date" 성질을 가집니다. 13 | 14 | 함수보다 연산 프로퍼티가 가독성이 높습니다. 15 | 함수 중에서도 "들어오는 인자 없이 리턴 값만 있는 함수"가 연산 프로퍼티로 바꿀 후보가 되는 함수입니다. 16 | 함수를 연산 프로퍼티로 고치는 코드를 살펴봅시다. 17 | 18 | ```swift 19 | import Foundation 20 | 21 | struct Run { 22 | let id: String 23 | let startTime: Date 24 | var endTime: Date? 25 | 26 | func elapsedTime() -> TimeInterval { 27 | return Date().timeIntervalSince(startTime) 28 | } 29 | 30 | func isFinished() -> Bool { 31 | return endTime != nil 32 | } 33 | 34 | mutating func setFinished() { 35 | self.endTime = Date() 36 | } 37 | 38 | init(id: String, startTime: Date) { 39 | self.id = id 40 | self.startTime = startTime 41 | self.endTime = nil 42 | } 43 | } 44 | 45 | // use 46 | run.elapsedTime() 47 | run.isFinished() 48 | ``` 49 | 50 | 위 코드에서 elapsedTimed와 isFinished 함수는 인자 없이 리턴 값만 존재하는 함수입니다. 51 | 두 함수를 아래와 같은 연산 프로퍼티로 수정하며 가독성을 높일 수 있습니다. 52 | 연산 프로퍼티의 자료형은 해당 프로퍼티가 리턴턴 하는 값의 자료형을 명시하면 됩니다. 53 | 54 | ```swift 55 | import Foundation 56 | 57 | struct Run { 58 | let id: String 59 | let startTime: Date 60 | var endTime: Date? 61 | 62 | var elapsedTime: TimeInterval { 63 | return Date().timeIntervalSince(startTime) 64 | } 65 | 66 | var isFinished: Bool { 67 | return endTime != nil 68 | } 69 | 70 | mutating func setFinished() { 71 | self.endTime = Date() 72 | } 73 | 74 | init(id: String, startTime: Date) { 75 | self.id = id 76 | self.startTime = startTime 77 | self.endTime = nil 78 | } 79 | } 80 | 81 | // use by computed properties 82 | run.elapsedTime 83 | run.isFinished 84 | ``` 85 | 86 | 연산 프로퍼티를 통해 훨씬 가독성이 높아졌습니다. 앞으로 함수 중 연산프로퍼티로 바꿀 것들은 바꿔서 사용합시다. 87 | 더 깔끔한 코드가 될 것입니다. 88 | 89 | ## Lazy properties 90 | 앞에서 보았듯이 연산 프로퍼티는 타입에 동적 성질을 부여합니다. 하지만 연산 프로퍼티가 항상 최선은 아닙니다. 91 | 연산 프로퍼티에서 발생하는 계산이 복잡해질 경우(시간이 오래 소요될 경우) 부적합합니다. 이때는 lazy 프로퍼티를 고려해야 합니다. 92 | 연산 프로퍼티는 접근할 때마다 연산을 진행하게 됩니다. 따라서 복잡한 계산을 포함할 경우 성능이 굉장히 떨어집니다. 93 | 94 | 하지만 lazy 프로퍼티는 다릅니다. lazy 프로퍼티는 연산 프로퍼티와 다르게 처음 호출되었을 때 한 번만 실행됩니다. 95 | 96 | lazy properties make sure properties are calcuted at a later time(if at all) and only once!! 97 | 98 | 아래 코드는 복잡한 계산을 가진 연산 프로퍼티가 발생시키는 문제점을 보여줍니다. 99 | 100 | ```swift 101 | struct LearningPlan { 102 | let level: Int 103 | var description: String 104 | 105 | // contents is a computed property - 2초 딜레이 발생생 106 | var contents: String { 107 | print("I'm taking my sweet time to calculate.") 108 | sleep(2) 109 | 110 | switch level { 111 | case ..<25: return "Watch an English documentary." 112 | case ..<50: return "Translate a newspaper article." 113 | case ..<25: return "Read two academic papers." 114 | case default: return "Try to read English for 30 minutes." 115 | } 116 | } 117 | } 118 | 119 | var plan = LearningPlan(level: 18, description: "A special plan for today!") 120 | 121 | print(Date()) // 2023-11-18 18:04:43 +0000 122 | print(plan.contents) // "Watch an English documentary." 123 | print(Date()) // 2023-11-18 18:04:45 +0000 124 | ``` 125 | 126 | 위 contents는 동작에 2초가 소요되는 연산 프로퍼티입니다. 만약 contents에 5번 연속으로 접근하면 몇초가 소요될까요? 127 | 연산 프로퍼티는 접근할 때마다 동작하기 때문에 2초씩 5번 총 10초가 소요됩니다. 굉장한 딜레이가 발생합니다. 128 | 129 | 물론 2초가 소요되는 동작의 속도를 줄이면 좋겠지만, 동작 자체의 속도와 별개로 lazy 프로퍼티를 사용하면 딜레이를 최소화할 수 있습니다. 130 | lazy 프로퍼티는 본인이 호출되었을 때 단 한 번만 동작합니다. 반복되어 호출되어도 가장 첫 호출에만 동작하고 이후 호출에는 첫 동작을 결과를 리턴합니다. 131 | lazy 프로퍼티는 연산 프로퍼티와 달리 첫 호출로 계산된 값을 저장하여 이후 호출에 리턴합니다. 132 | 133 | 아래 코드처럼 연산 프로퍼티를 lazy 프로퍼티로 고칠 수 있습니다. 하지만 아직 완벽한 코드는 아닙니다. 134 | 135 | ```swift 136 | struct LearningPlan { 137 | let level: Int 138 | var description: String 139 | 140 | // contents is a computed property 141 | lazy var contents: String = { 142 | print("I'm taking my sweet time to calculate.") 143 | sleep(2) 144 | 145 | switch level { 146 | case ..<25: return "Watch an English documentary." 147 | case ..<50: return "Translate a newspaper article." 148 | case ..<25: return "Read two academic papers." 149 | case default: return "Try to read English for 30 minutes." 150 | } 151 | }() 152 | } 153 | ``` 154 | 155 | 스위프트는 기본적으로 구조체의 모든 프로퍼티가 초기화되기를 바랍니다. 156 | 따라서 구조체의 custom init을 만들지 않는다면, 컴파일러가 모든 프로퍼티를 초기화하는 init을 제공합니다. 157 | 158 | 하지만 LearningPlan 구조체가 생성과 동시에 lazy 프로퍼티가 초기화된다면 프로퍼티가 더 이상 수정되지 못하여 프로퍼티로서 역할을 못합니다. 159 | 이를 custom init을 통해 해결할 수 있습니다. 160 | 161 | ```swift 162 | struct LearningPlan { 163 | // ... 생략 164 | init(level: Int, description: String) { 165 | self.level = level 166 | self.description = description 167 | } 168 | } 169 | ``` 170 | 171 | 위의 custom init처럼 lazy 프로퍼티를 제외하고 나머지 프로퍼티를 초기화해야 합니다. 172 | custom init으로 객체 생성과 동시에 lazy 프로퍼티가 초기화되지 않기 때문에 lazy 프로퍼티의 연산이 필요할 때 접근해 사용할 수 있습니다. 173 | 174 | 앞에서 말했듯이 lazy 프로퍼티는 중복해서 해출되어도 한 번만 초기화되고 처음 초기화된 값을 저장합니다. 175 | 하지만 누군가 lazy 프로퍼티에 접근해 임의로 값을 변경한다면 어떻게 될까요? 176 | 177 | 내부 로직을 무시하고 lazy 프로퍼티를 임의로 변경하는 행동은 예상과 다른 결과를 초래하기며 혼란을 가져옵니다. 178 | 179 | 아래 코드에서 plan 객체를 level 18로 초기화하여 plan.contents의 값을 "Watch an English documentary"로 기대합니다. 180 | 하지만 plan.contents = "Let's eat pizza and watch Netflix all day"에서 lazy 프로퍼티 값을 바꾸면서 예상과 다른 결과가 보입니다. 181 | 182 | ```swift 183 | var plan = LearningPlan(level: 18, description: "A special plan for today!") 184 | plan.contents = "Let's eat pizza and watch Netflix all day" 185 | print(plan.contents) // "Let's eat pizza and watch Netflix all day" 186 | ``` 187 | 188 | lazy 프로퍼티 값을 임의로 변경할 가능성을 막는 방법은 접근 제어자에 있습니다. 189 | lazy 프로퍼티의 접근 제어자를 private(set)으로 설정한다면 변경을 막을 수 있습니다. 190 | private(set)으로 읽기 전용 프로퍼티가 되어 lazy 프로퍼티의 구조체 외부에서의 변경 가능성을 막습니다. 191 | 192 | ```swift 193 | struct LearningPlan { 194 | lazy private(set) var contents: String = { 195 | // 생략 196 | }() 197 | } 198 | ``` 199 | 200 | "Once you initialized a lazy property, you cannot change it!!" 201 | 202 | 앞에서 이야기했듯이 lazy 프로퍼티는 한 번 초기화되면 이후로 변경되지 않습니다. 203 | 하지만 lazy 프로퍼티 안에서 쓰이는(참조하는) 변수가 변경 가능성이 있다면 어떻게 될까요? 204 | 아래 코드를 확인해 봅시다. 205 | 206 | ```swift 207 | var intensePlan = LearningPlan(level: 138, description: "A special plan for today!") 208 | intensePlan.contents 209 | var easyPlan = intensePlan 210 | easyPlan.level = 0 211 | print(easyPlan.contents) // "Read two academic papers." 212 | ``` 213 | 214 | 코드에서는 easyPlan의 level을 0으로 만들어 0에 해당하는 contents를 기대하지만 실제로는 기존 intensePlan의 138에 해당하는 contents가 출력됩니다. 215 | 216 | lazy 프로퍼티가 초기화된 이후 lazy 프로퍼티 안에서 쓰이는(참조하는) 변수가 변경되어도, lazy 프로퍼티에 처음 저장된 값은 변경되지지 않습니다. 217 | 오히려 코드를 읽는 사람에게 혼란을 주게 됩니다. 218 | 219 | 아래 코드처럼 객체 복사 이후 lazy 프로퍼티를 호출한다면 intensePlan, easyPlan의 level에 어울리는 contents가 출력됩니다. 220 | 221 | ```swift 222 | var intensePlan = LearningPlan(level: 138, description: "A special plan for today!") 223 | var easyPlan = intensePlan 224 | easyPlan.level = 0 225 | print(intensePlan.contents) // "Read two academic papers." 226 | print(easyPlan.contents) // "Watch an English documentary." 227 | ``` 228 | 229 | lazy 프로퍼티 안에서 쓰이는(참조하는) 변수가 변경 가능성이 있다면 굉장히 복잡해집니다. 230 | 따라서 lazy 프로퍼티 안에서 쓰이는(참조하는) 변수는 let으로 선언하도록 합시다. 231 | 232 | lazy 프로퍼티 안에서 쓰이는 변수를 let으로 선언하여 lazy 프로퍼티를 immutable하게 만들어 복잡성을 줄입시다. 233 | lazy 프로퍼티를 가진 객체가 복사될 때 더 주의해야 합니다. 234 | 235 | 다시 말해, lazy 프로퍼티가 초기화된 이후 lazy 프로퍼티 내부에서 참조하는 변수가 변경되더라도 236 | 해당 변경 사항이 lazy 프로퍼티에 영향을 미칠 수 없기 때문에 lazy 프로퍼티 내부에서 참조하는 변수를 불변하게 만듭시다. 237 | 238 | lazy var 프로퍼티를 사용하다보면 retain cycle을 주의해야 합니다. 239 | 240 | 아래와 같이 블록{} 마지막에 () 가 붙은 형태는 nonescaping closure라고 해서 실행 즉시 결과를 반환함을 가정하기 때문에 클로저 내부에서 self를 명시적으로 참조할 필요가 없고, self instance의 retain count를 올리지도 않기 때문에 retain cycle을 걱정할 필요가 없습니다. 241 | 242 | 마지막에 () 을 통해서 즉시 실행하고 결과를 돌려주기에 메모리 누수는 없습니다. 243 | 244 | ```swift 245 | lazy var contents: String = { 246 | print("I'm taking my sweet time to calculate.") 247 | sleep(2) 248 | 249 | switch level { 250 | case ..<25: return "Watch an English documentary." 251 | case ..<50: return "Translate a newspaper article." 252 | case ..<25: return "Read two academic papers." 253 | case default: return "Try to read English for 30 minutes." 254 | } 255 | }() 256 | ``` 257 | 258 | 반면 아래와 같은 형태로 closure를 타입으로 선언 후 사용하면 self를 명시적으로 참조해야 하고, closure가 실행되고 나서도 self의 retain count가 증가한 상태로 유지되기 때문에 weak capture없이 self를 사용하면 retain cycle이 발생할 수 있습니다. 259 | 260 | ```swift 261 | lazy var contents: () -> String = { 262 | print("I'm taking my sweet time to calculate.") 263 | sleep(2) 264 | 265 | switch self.level { 266 | case ..<25: return "Watch an English documentary." 267 | case ..<50: return "Translate a newspaper article." 268 | case ..<25: return "Read two academic papers." 269 | case default: return "Try to read English for 30 minutes." 270 | } 271 | }() 272 | ``` 273 | 274 | contents 프로퍼티는 () -> String 클로저를 리턴하고 있습니다. 따라서 [weak self]를 통해서 메모리 누수를 방지해주어야 합니다. 275 | contents 프로퍼티에 의해 메모리에는 값이 아닌 클로저가 올라가 있고 해당 클로저는 self의 참조를 유지하고 있습니다. 276 | 277 | ```swift 278 | lazy var contents: () -> String = { [weak self] in 279 | print("I'm taking my sweet time to calculate.") 280 | sleep(2) 281 | 282 | switch self?.level { 283 | case ..<25: return "Watch an English documentary." 284 | case ..<50: return "Translate a newspaper article." 285 | case ..<25: return "Read two academic papers." 286 | case default: return "Try to read English for 30 minutes." 287 | } 288 | }() 289 | ``` 290 | 291 | 따라서 앞에서 lazy var 프로퍼티가 값을 리턴한다면 프로퍼티 초기화 이후 계속해서 동일한 값을 리턴하지만 클로저를 리턴하는 경우 클로저 안에서 참조하는 변수의 292 | 변경에 따라 리턴 값이 달라질 수 있습니다. 293 | 294 | 그렇다면 lazy 프로퍼티는 항상 옳을까요? 295 | 296 | 아쉽게도 아닙니다. 297 | 298 | lazy 프로퍼티는 비동기 작업, 멀티 쓰레드 환경에서 안전하지 못합니다. 299 | lazy 프로퍼티가 단일 쓰레드 환경에서는 초기 접근에만 동작하고 이후에는 첫 동작의 결과를 리턴하지만 멀티 쓰레드 환경에서는 이 특징이 보장되지 않습니다. 300 | 301 | 아래 링크는 멀티 쓰레드 환경에서 lazy 프로퍼티가 가지는 취약점을 잘 설명한 글입니다. 302 | 책을 읽으며 lazy 프로퍼티를 초기화에 높은 비용이 드는 경우 사용하면 유용하겠다고 생각했지만 thread-safe하지 않다는 글을 보니 자주 사용할지 의구심이 들긴합니다. 303 | 304 | [Swift-Lazy-진짜-필요할-때만-씁시다](https://velog.io/@niro/Swift-Lazy-%EC%A7%84%EC%A7%9C-%ED%95%84%EC%9A%94%ED%95%A0-%EB%95%8C%EB%A7%8C-%EC%94%81%EC%8B%9C%EB%8B%A4) 305 | 306 | [Swift의 lazy var](https://brunch.co.kr/@tilltue/71) 307 | 308 | ## Property observers 309 | Property observers are actions triggered when a stored property changes value. 310 | 311 | 저장 역할과 함께 연산 기능까지 프로퍼티에서 구현하고 싶을 때 Property observers를 사용합니다. 312 | Property observers로는 didSet과 willSet이 있습니다. 313 | 314 | didSet은 변수 변경 이후 trigger되고 315 | willSet은 변수 변경 이전에 trigger됩니다. 316 | 317 | 스위프트에서는 init으로 초기화되는 경우 didSet이 따로 호출되지 않습니다. 318 | 따라서 아래와 같은 문제가 생깁니다. 319 | 아래 코드는 사용자가 이름을 입력했을 때 공백이 포함될 경우 공백을 지워주는 코드입니다. 320 | 321 | ```swift 322 | import Foundation 323 | 324 | class Player { 325 | let id: String 326 | 327 | var name: String { 328 | didSet { 329 | print("My previous name was \(oldValue)") 330 | name = name.trimmingCharacters(in: .whitespaces) 331 | } 332 | } 333 | 334 | init(id: String, name: String){ 335 | self.id = id 336 | self.name = name 337 | } 338 | } 339 | 340 | let jeff = Player(id: "1", name: "SuperJeff ") 341 | print(jeff.name) // "SuperJeff " 342 | print(jeff.name.count) // 13 343 | 344 | jeff.name = "SuperJeff " 345 | print(jeff.name) // "SuperJeff" 346 | print(jeff.name.count) // 9 347 | ``` 348 | 349 | 위 코드에서 name 프로퍼티의 didSet에는 공백 문자를 지워주는 기능을 제공하고 있습니다. 350 | 하지만 init을 통해 name 프로퍼티를 초기화했을 때 didSet에 도달하지 못해 공백 문자를 지우지 못했습니다. 351 | 위에서 말했듯이 스위프트에서는 init으로 프로퍼티를 초기화할 때는 Property observers가 호출되지 않습니다. 352 | 353 | 만약 init으로 프로퍼티를 초기화할 때부터 Property observers를 호출하고 싶다면, defer 클로저를 사용하면 가능합니다. 354 | defer 클로저는 함수 안에 위치하고 해당 함수가 종료되었을 때 호출되는 클로저입니다. 355 | 아래 코드와 같이 init 안에 defer 클로저를 추가하여, defer 클로저에서 변수를 초기화하여 didSet을 호출할 수 있습니다. 356 | 357 | ```swift 358 | class Player { 359 | // 생략 360 | 361 | init(id: String, name: String) { 362 | defer { self.name = name } 363 | self.id = id 364 | self.name = name // 생략 가능하다. 365 | } 366 | } 367 | ``` 368 | 369 | defer 클로저는 init과 별개이기 때문에 defer에서 name 초기화하면 didSet이 호출됩니다. 370 | 물론 defer 클로저가 생겨난 이유가 init에서 Property observers를 호출하기 위함은 아닙니다. 371 | 그렇치만 근사한 방법입니다. 372 | 373 | ## Summary 374 | - You can use computed properties for properties with sepcific behavior but with-out storage. 375 | - Computed properties are functions masquerading as properties. 376 | - You can use computed properties when a value can be different each time you call it. 377 | - Only lightweight functions should be made into computed properties. 378 | - Lazy properties are excellent for expensive or time-consuming computations. 379 | - Use lazy properties to delay a computation or if it may not even run at all. 380 | - Lazy properties allow you to refer to other properties inside classes and structs. 381 | - You can use the private(set) annotation to make properties read-only to outsiders of a class or struct. 382 | - When a lazy property refers to another preperty, make sure to keep this other property immutable to keep complexity low. 383 | - You can use property observers such as willSet and didSet to add behavior on stored properties. 384 | - You can use defer to trigger property observers from an initializer. 385 | --------------------------------------------------------------------------------