├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── 10장_예외 ├── item69.md ├── item70.md ├── item71.md ├── item73.md ├── item75.md ├── item76.md └── item77.md ├── 11장_동시성 └── item82.md ├── 12장_직렬화 └── item0.md ├── 2장_객체_생성과_파괴 ├── item1.md ├── item2.md ├── item3.md ├── item4.md ├── item5.md ├── item6.md ├── item7.md ├── item8.md ├── item9.md └── resources │ └── item3-singleton.png ├── 3장_모든_객체의_공통_메서드 ├── item10.md ├── item11.md ├── item12.md ├── item13.md └── item14.md ├── 4장_클래스와_인터페이스 ├── item15.md ├── item16.md ├── item17.md ├── item18.md ├── item19.md ├── item20.md ├── item21.md ├── item22.md ├── item23.md ├── item24.md ├── item25.md └── resources │ └── buttons.png ├── 5장_제네릭 ├── item27.md ├── item28.md ├── item29.md ├── item30.md └── item32.md ├── 6장_열거_타입과_애너테이션 ├── item34.md ├── item35.md ├── item36.md ├── item37.md ├── item39.md ├── item40.md ├── item41.md └── resources │ └── item36-optionsSetMethod.png ├── 7장_람다와_스트림 ├── item43.md ├── item44.md ├── item45.md └── item46.md ├── 8장_메서드 ├── item49.md ├── item51.md ├── item53.md ├── item54.md ├── item55.md └── item56.md ├── 9장_일반적인_프로그래밍_원칙 ├── item57.md ├── item58.md ├── item59.md ├── item60.md ├── item62.md ├── item63.md ├── item64.md ├── item65.md ├── item67.md └── item68.md ├── LICENSE └── README.md /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### PR 체크리스트(완료 후 지우고 원하는 내용을 써주세요) 2 | 3 | 1. Assignees 본인 지정하기 4 | 2. 분류용 레이블 지정하기 5 | * `contents` — 컨텐츠 추가 6 | * `documentation` — 기타 Repo 문서 작업 7 | 3. 리뷰용 레이블 지정하기 8 | * 나 외의 스터디원들을 레이블로 지정합니다. ex) `delma`, `Lena`, `Lin` 9 | * 리뷰어는 리뷰 후 자신의 이름이 적힌 레이블을 삭제합니다. 10 | * 이름이 적힌 레이블들이 모두 사라진 것은 `main` 브랜치에 머지가 가능함을 나타내며, 이제 PR 작성자가 머지할 수 있습니다. 11 | * PR 목록에서 리뷰 진행 상황을 한 번에 볼 수 있도록 하는 것이 목적입니다. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /10장_예외/item69.md: -------------------------------------------------------------------------------- 1 | # Item 69. 예외는 진짜 예외 상황에만 사용하라 2 | 3 | 이번 장에서는 말 그대로 예외처리는 진짜 예외 처리가 필요한 부분에서만 예외처리를 하라는 것입니다. 4 | 5 |
6 | 7 | ### **잘못된 예외처리** 8 | 9 | **Java** 10 | 11 | ```java 12 | try { 13 | int i = 0; 14 | while(true) { 15 | range[i++].climb(); 16 | } 17 | } catch (ArrayIndexOutOfBoundsException e) { 18 | } 19 | 20 | ``` 21 | 22 | **Swift** 23 | 24 | ```swift 25 | let array: [String] = ["one","two","three","four","five"] 26 | 27 | do { 28 | var index = 0 29 | while(true) { 30 | let value = try array[index] 31 | // 배열에 Index를 통해 직접 접근하는 경우 'OutOfIndexError'가 예상되기 때문에 try 를 시도함 32 | // yellow error: No calls to throwing functions occur within 'try' expression 33 | print("\\(value)") 34 | index += 1 35 | } 36 | } catch (let error) { 37 | print("fatalError: \\(error.localizedDescription)") 38 | } 39 | 40 | // output 41 | // Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444 42 | // 2021-03-20 19:43:37.193369+0900 Test_Iterator[18969:1365911] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444 43 | 44 | ``` 45 | 46 | - Swift에서 `Index out of range` Error 타입으로 제공하는 것이 없어서 `let error` 대신 catch하였습니다. 47 | - Swift에서는 배열에 접근할 때 try 문을 통해 throw할 방법이 없습니다. 48 | - 직접 catch 한 것 외에도 시스템에서 FatalError를 내보냅니다. 49 | 50 |
51 | 52 | **배열에 직접 접근할 때 try문을 사용할 수 없는 이유** 53 | 54 | Array를 inex를 통해 직접 접근하는 경우 55 | 56 | ``` 57 | let value = array[3] 58 | 59 | ``` 60 | 61 | NSArray의 object 메소드를 사용하게 됩니다. 62 | 63 | ``` 64 | func object(at index: Int) -> Any 65 | 66 | ``` 67 | 68 | - 애초에 `object(at:)` 라는 메소드에서 throw 처리가 안되어 있기 때문에 `try` 를 사용할 수가 없습니다. 69 | 70 | 책에서는 위와 같은 예시를 통해 잘못된 예외처리 상황을 설명하고 있습니다. 71 | 72 |
73 | 74 | ### **잘못된 이유** 75 | 76 | 1. 예외는 예외 상황에 쓸 용도로 설계되었기 때문에 **최적화가 되어있지 않습니다**. 그렇기 때문에 위와 같은 try-catch 블럭에 반복문을 설계한 것은 전체적인 성능을 떨어뜨립니다. 77 | 2. 기본적으로 배열을 순회하는 표준 관용구에서는 JVM에서 최적화해 처리해줍니다. 78 | 3. 위의 코드의 경우 배열에 문제가 있는 경우에 `ArrayIndexOutOfBoundsException` 예외로 처리되기 때문에 디버깅하기가 더 어려워집니다. 79 | 80 | **Java** 81 | 82 | ```java 83 | for (Iterator i = collection.iterator(); i.hasNext();) { 84 | Foo foo = i.next() 85 | ... 86 | } 87 | 88 | ``` 89 | 90 | 일반적으로 java에서는 Iterator 타입에서 제공하는 hasNext()를 통해 쉽게 반복문을 사용할 수 있습니다. 91 | 92 | **Swift** 93 | 94 | ```swift 95 | // IteratorProtocol.swift 96 | 97 | public protocol IteratorProtocol { 98 | mutating func next() -> Self.Element? 99 | } 100 | 101 | // Example 102 | 103 | struct Countdown: Sequence { 104 | let start: Int 105 | 106 | func makeIterator() -> CountdownIterator { 107 | return CountdownIterator(self) 108 | } 109 | } 110 | 111 | struct CountdownIterator: IteratorProtocol { 112 | let countdown: Countdown 113 | var times = 0 114 | 115 | init(_ countdown: Countdown) { 116 | self.countdown = countdown 117 | } 118 | 119 | mutating func next() -> Int? { 120 | let nextNumber = countdown.start - times 121 | guard nextNumber > 0 122 | else { return nil } 123 | 124 | times += 1 125 | return nextNumber 126 | } 127 | } 128 | 129 | let countdown = Countdown(start: 3) 130 | for count in countdown { 131 | print("\(count)...") 132 | } 133 | // Prints "3..." 134 | // Prints "2..." 135 | // Prints "1..." 136 | 137 | ``` 138 | 139 | Swift에서는 IteratorProtocol을 통해 직접 구현체를 만들 수 있습니다. 140 | 141 | - Swift에서는 hasNext() 메소드는 제공하고 있지 않습니다. 142 | - Swift에서는 nil을 이용해 java의 hasNext() 메소드를 대신합니다. 143 | 144 |
145 | 146 | ### 상태 검사 메소드, 옵셔널, 특정 값 147 | 148 | 책에서는 상태 검사 메소드 이외에 옵셔널을 사용하거나 특정 값을 사용하는 선택지도 있다고 합니다. 149 | 150 | 1. 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 **옵셔널**이나 **특정 값**을 사용합니다.( 상태 검사 메소드를 호출하는 사이에 상태가 변할 수도 있기 때문입니다. ) 151 | 2. 성능이 중요한 상황에서 상태 검사 메소드가 상태 의존적 메소드 작업 일부를 중복 수행한다면 **옵셔널**이나 **특정 값**을 사용합니다. 152 | 3. 다른 모든 경우엔 상태 검사 메소드 방식이 조금 더 낫습니다. 153 | 154 | 예외는 예외 상황에서 쓸 의도로 설계되었습니다. 정상적인 제어 흐름에서 사용해서는 안되며, 이를 강요하는 API를 만들어서는 안됩니다. 155 | -------------------------------------------------------------------------------- /10장_예외/item71.md: -------------------------------------------------------------------------------- 1 | # Item71. 필요없는 검사 예외 사용은 피하라 2 | 3 | # Java 4 | 5 | ## 예외처리를 해야하는 검사 예외, 잘 쓰면 약 못 쓰면 독. 6 | 7 | 검사 예외를 제대로 활용하면 API와 프로그램 질을 높일 수 있습니다. 결과를 코드로 반환하거나 비검사 예외를 던지는 것과 달리, **검사 예외는 발생한 문제를 프로그래머가 처리**하여 안정성을 높이게끔 해줍니다. 8 | 9 | 하지만 검사 예외를 과도하게 사용하면 오히려 불편한 API가 됩니다. 메서드가 검사 예외를 던질 수 있다고 선언됐다면, 이를 호출하는 코드에서는 catch 블록을 두어 **그 예외를 붙잡아 처리하거나 더 바깥으로 던져 문제를 전파**해야만 합니다. 10 | 11 | 더구나 검사 예외를 던지는 메서드는 **스트림 안에서 직접 사용할 수 없기 때문에** 자바 8부터는 부담이 더욱 커집니다. 따라서 API를 제대로 사용해도 발생할 수 있는 예외이거나, 프로그래머가 의미있는 조치를 취할 수 있는 경우라면 이 정도 부담쯤은 받아들일 수 있을 것입니다. 12 | 13 | * **그러나 둘 중 어디에도 해당하지 않는다면 비검사 예외를 사용하는게 좋습니다.** 14 | 15 | * 또 하나의 선택지로는 **적절한 결과 타입을 담은 옵셔널을 반환하는 것입니다.** 검사 예외를 던지는 대신 단순히 빈 옵셔널을 반환하면 됩니다. 하지만 이 방식의 단점이라면 **예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없는 것**입니다. 16 | 17 | 참조한 곳: https://pridiot.tistory.com/54, https://reference-m1.tistory.com/246 18 | 19 | # Swift 20 | 21 | 스위프트의 경우에 자바의 검사 예외에 해당하는 것은 Error(enum type)입니다. Error도 자바의 검사 예외와 마찬가지로 **발생한 문제를 프로그래머가 처리하여 안정성을 높이게끔 해줍니다.** 22 | 23 | 따라서 사용자가 처리할 수 있는 상황이면 Error를 사용하면 되는데, 자바와 마찬가지로 Error를 과도하게 사용하면 오히려 불편한 API를 될 수 있겠습니다. 24 | 25 | * 따라서 검사 예외를 던지는 게 오히려 성가시거나 적절치 않는 경우라면 스위프트도 단순히 옵셔널을 반환하는 선택지가 있습니다. 단, 스위프트의 옵셔널도 왜 옵셔널이 반환됐는지에 대한 정보를 담을 수 없습니다. -------------------------------------------------------------------------------- /10장_예외/item73.md: -------------------------------------------------------------------------------- 1 | # Item73. 추상화 수준에 맞는 예외를 던지라 2 | 3 | ## Java 4 | 5 | 수행하려는 일과 관련 없어 보이는 예외가 튀어나오면 당황스러울 것입니다. 6 | 메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버릴때 종종 일어나는 일입니다. 7 | 당황시키는 것 뿐만 아니라 내부 구현방식을 상위에 드러내어 **윗 레벨 API를 오염 시킬 수 있고**, 다음 릴리스에서 구현방식을 바꾸면 **다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있습니다.** 해결책으로 예외 번역, 예외 연쇄가 있습니다. 8 | 9 | ### 예외 번역 10 | 11 | 상위 메서드에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 합니다. 12 | 이를 예외 번역(Exception Translation)이라 합니다. 13 | 14 | ```java 15 | try { 16 | ... // 저수준 추상화를 이용한다. 17 | } catch (LowerLevelException e) { 18 | // 추상화 수준에 맞게 번역한다. 19 | throw new HigherLevelException(...); 20 | } 21 | ``` 22 | 23 | ```java 24 | /** 25 | * Returns the element at the specified position in this list. 26 | * 27 | *

This implementation first gets a list iterator pointing to the 28 | * indexed element (with {@code listIterator(index)}). Then, it gets 29 | * the element using {@code ListIterator.next} and returns it. 30 | * 31 | * @throws IndexOutOfBoundsException {@inheritDoc} 32 | */ 33 | public E get(int index) { 34 | try { 35 | return listIterator(index).next(); 36 | } catch (NoSuchElementException exc) { 37 | throw new IndexOutOfBoundsException("Index: "+index); 38 | } 39 | } 40 | ``` 41 | 42 | ### 예외 연쇄 43 | 44 | 예외 연쇄(Exception chaining)이란 문제의 근본 원인(cause)인 저수준 예외를 고수준 예외에 실어 보내는 방식입니다. 45 | 별도의 접근자 메서드(Throwable의 getCause메서드)를 통해 필요하면 언제든 저수준 예외를 꺼내 볼 수 있습니다. 46 | 47 | ```java 48 | try { 49 | ... // 저수준 추상화를 이용한다. 50 | } catch (LowerLevelException e) { 51 | // 저수준 예외를 고수준 예외에 실어 보낸다. 52 | throw new HigherLevelException(cause); 53 | } 54 | ``` 55 | 56 | 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있습니다. 57 | 그렇지 않은 예외라도 Throwable의 initCause 메서드를 이용해 원인 을 직접 못박을 수 있습니다 58 | 예외 연쇄는 문제의 원인을 프로그램에서 접근할 수 있게 해주며 원인과 고수준 예외의 Stack trace를 잘 통합해줍니다. 59 | 60 | ```java 61 | public class Exception extends Throwable { 62 | public Exception() { 63 | super(); 64 | } 65 | 66 | /// 생략 67 | 68 | // 예외 연쇄용 생성자 69 | public Exception(Throwable cause) { 70 | super(cause); 71 | } 72 | } 73 | ``` 74 | 75 | **가능하다면 저수준 메서드가 반드시 성공하도록 하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선입니다.때로는 상위 계층 메서드의 매개변수 값을 아래 계층 메서드로 건네기 전에 미리 검사하는 방법으로 달성할 수 있습니다.** 76 | 차선책도 있습니다. 아래 계층에서의 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에 전파하지 않는 방법이 있습니다. 77 | 이 경우 발생한 예외는 로깅을 활용하여 개발자가 분석할 수 있도록 조치를 취하게 해줍니다. 78 | 79 | ## Swift 80 | 81 | * Alamofire의 `AFError` 를 참고하였습니다. 82 | 83 | ### Enum의 associated value 사용하여 에러 연쇄 나타내기 84 | 85 | * 상위 에러는 AFError.parameterEncodingFailed 이고 하위 에러는 ParameterEncodingFailureReason 의 연관값인 `error` 입니다. 86 | 87 | ```swift 88 | public enum AFError: Error { 89 | /// The underlying reason the `.parameterEncodingFailed` error occurred. 90 | public enum ParameterEncodingFailureReason { 91 | /// The `URLRequest` did not have a `URL` to encode. 92 | case missingURL 93 | /// JSON serialization failed with an underlying system error during the encoding process. 94 | case jsonEncodingFailed(error: Error) 95 | /// Custom parameter encoding failed due to the associated `Error`. 96 | case customEncodingFailed(error: Error) 97 | } 98 | 99 | case parameterEncodingFailed(reason: ParameterEncodingFailureReason) 100 | // 나머지 생략 101 | } 102 | ``` 103 | 104 | ```swift 105 | open class JSONParameterEncoder: ParameterEncoder { 106 | // 생략 107 | 108 | /// `JSONEncoder` used to encode parameters. 109 | public let encoder: JSONEncoder 110 | 111 | /// Creates an instance with the provided `JSONEncoder`. 112 | /// 113 | /// - Parameter encoder: The `JSONEncoder`. `JSONEncoder()` by default. 114 | public init(encoder: JSONEncoder = JSONEncoder()) { 115 | self.encoder = encoder 116 | } 117 | 118 | open func encode(_ parameters: Parameters?, 119 | into request: URLRequest) throws -> URLRequest { 120 | guard let parameters = parameters else { return request } 121 | 122 | var request = request 123 | 124 | do { 125 | let data = try encoder.encode(parameters) 126 | request.httpBody = data 127 | if request.headers["Content-Type"] == nil { 128 | request.headers.update(.contentType("application/json")) 129 | } 130 | } catch { 131 | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) // 이 에러가 하위 에러인 EncodingError.invalidValue 이다. 132 | } 133 | 134 | return request 135 | } 136 | 137 | // 생략 138 | } 139 | ``` 140 | 141 | ### Enum의 연산 프로퍼티 사용하여 LowerLevel 에러 원인 나타내기 142 | 143 | * 연산 프로퍼티를 이용해 AFError을 사용하는 외부에서 쉽게 하위 에러가 무엇인지 파악하고 에러 처리할 수 있습니다. 144 | 145 | ```swift 146 | extension AFError.ParameterEncodingFailureReason { 147 | var underlyingError: Error? { 148 | switch self { 149 | case let .jsonEncodingFailed(error), 150 | let .customEncodingFailed(error): 151 | return error 152 | case .missingURL: 153 | return nil 154 | } 155 | } 156 | } 157 | 158 | extension AFError { 159 | public var underlyingError: Error? { 160 | switch self { 161 | case let .parameterEncodingFailed(reason): 162 | return reason.underlyingError 163 | // 이하 생략 164 | } 165 | } 166 | } 167 | ``` 168 | -------------------------------------------------------------------------------- /10장_예외/item75.md: -------------------------------------------------------------------------------- 1 | # Item 75. 예외의 상세 메시지에 실패 관련 정보를 담으라 2 | 3 | 4 | 5 | 개발자가 실패를 분석하기 위해서 예외 메시지는 필수적으로 사용됩니다. 더군다나 그 실패가 재현하기 어렵다면 더더욱 그렇습니다. 6 | 7 | 따라서 예외의 `toString` 메소드에 실패에 대한 원인을 최대한 많이 담아야 합니다. 8 | 9 |
10 | 11 | **실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 합니다.** 12 | 13 | ```swift 14 | class IndexOutOfRangeError: LocalizedError { 15 | 16 | struct IndexOutOfRangeErrorContents { 17 | private let lowerBound: Int 18 | private let upperBound: Int 19 | private let index: Int 20 | 21 | init(lowerBound: Int, upperBound: Int, index: Int) { 22 | self.lowerBound = lowerBound 23 | self.upperBound = upperBound 24 | self.index = index 25 | } 26 | } 27 | 28 | private let contents: IndexOutOfRangeErrorContents 29 | 30 | // 해당 error에 대한 설명 31 | var errorDescription: String? { 32 | return "Index out of range" 33 | } 34 | 35 | // 해당 error에 대한 사용자를 위한 설명 36 | var errorDescriptionToUser: String? { 37 | return "잘못된 접근입니다." 38 | } 39 | 40 | // 해당 error의 원인 41 | var failureReason: String? { 42 | return "\\(dump(contents))" 43 | } 44 | 45 | init(contents: IndexOutOfRangeErrorContents) { 46 | self.contents = contents 47 | } 48 | } 49 | 50 | ``` 51 | 52 | 책의 `IndexOutOfBoundsException`에 대한 코드를 Swift로 변환하였습니다. 53 | 54 | - `LocalizedError` 는 error를 채택하고 있는 프로토콜입니다. 55 | - 에러 메시지에서 사용될 내용들은 내부 `struct`를 이용하여 표현합니다. 56 | - 통상적으로 필요할 수 있는 description, descritionToUser, failureReson을 구현하였습니다. 57 | 58 |
59 | 60 | **LocalizedError** 61 | 62 | ```swift 63 | public protocol LocalizedError : Error { 64 | 65 | /// A localized message describing what error occurred. 66 | var errorDescription: String? { get } 67 | 68 | /// A localized message describing the reason for the failure. 69 | var failureReason: String? { get } 70 | 71 | /// A localized message describing how one might recover from the failure. 72 | var recoverySuggestion: String? { get } 73 | 74 | /// A localized message providing "help" text if the user requests help. 75 | var helpAnchor: String? { get } 76 | } 77 | 78 | ``` -------------------------------------------------------------------------------- /10장_예외/item76.md: -------------------------------------------------------------------------------- 1 | # item76. 가능한 한 실패 원자적으로 만들라 2 | 3 | ### 실패 원자적 4 | 5 | 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지하는 특성을 실패 원자적(failure-atomic)이라고 합니다. 6 | 7 | 8 | 9 | ## 메서드를 실패 원자적으로 만드는 방법 10 | 11 | ### 불변 객체로 설계한다 12 | 13 | 불변 객체는 태생적으로 실패 원자적이므로 메서드가 실패하면 새로운 객체가 만들어지지 않을 수는 있으나 기존 객체가 불안정한 상태에 빠지는 일은 결코 없습니다. 14 | 15 | ### 작업 수행에 앞서 매개변수 유효성을 검사한다 16 | 17 | 객체의 내부 상태를 변경하기 전에 잠재적 예외의 가능성을 대부분 걸러낼 수 있는 방법입니다. 또한 추상화 수준에 어울리는 에러를 던지도록 할 수 있습니다. 혹은 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법도 있습니다. 18 | 19 | ```java 20 | public Object pop() { 21 | if (size == 0) 22 | throw new EmptyStackException(); 23 | Object result = elements[--size]; 24 | elements[size] = null; // 다 쓴 참조 해제 25 | return result; 26 | } 27 | ``` 28 | ### 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치한다 29 | 30 | * 작업 수행에 앞서 매개변수 유효성을 검사하는 것과 비슷한 취지의 방법입니다. 계산을 수행해보기 전에 인수의 유효성을 검사해볼 수 없을 때 앞서의 방식에 덧붙여 쓸 수 있는 기법입니다. 31 | * 예를 들어 TreeMap을 생각해봅시다. TreeMap은 원소들을 어떤 기준으로 정렬하고, 따라서 TreeMap에 원소를 추가하려면 원소가 TreeMap에 기준에 따라 비교할 수 있는 타입이어야 합니다. 그래서 해당 TreeMap의 원소타입에 맞지 않은 엉뚱한 타입의 원소를 추가하면 **해당 원소가 들어갈 위치를 찾는 과정에서** `ClassCastException`을 던질 것이고 **TreeMap의 상태는 변하지 않을 것**입니다. 32 | 33 | ### 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체한다 34 | 35 | 데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방법입니다. 예를 들어 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 훨씬 빠르게 접근할 수 있기 때문에 어떤 정렬 메서드에서 정렬을 수행하기 전에 입력 리스트의 원소를 배열로 옮겨 담습니다. 물론 이는 성능을 높이고자한 결정이지만, 혹여 정렬에 실패해도 입력 리스트는 변하지 않는 효과를 얻을 수 있게 됩니다. 36 | 37 | 38 | ### 작업 도중 발생하는 실패를 가로채는 복구 코드로 실패시 작업 전 상태로 되돌린다 39 | 40 | 주로 (디스크 기반의) 내구성(durability)를 보장해야 하는 자료구조에 쓰이는데, 자주 사용되는 방법은 아닙니다. 41 | 42 | 43 | 44 | ## 실패 원자성을 꼭 지켜야 할까? 45 | 46 | 권장되는 덕목이지만 항상 달성 가능하지는 않습니다. 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있습니다. 따라서 예외를 던졌다고 해서 그 객체가 여전히 사용할 수 있는 상태라고 가정해서는 안됩니다. 47 | 48 | 실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산도 있기 때문에 실패 원자적으로 만들 수 있더라도 항상 그렇게 해야하는 건 아닙니다. 그래도 문제가 무엇인지 알고 나면 실패 원자성을 꽁짜로 얻을 수 있는 경우가 더 많습니다. 49 | -------------------------------------------------------------------------------- /10장_예외/item77.md: -------------------------------------------------------------------------------- 1 | # Item 77. 예외를 무시하지 말라 2 | 3 | 4 | 5 | ### 책 요약 6 | 7 | > API 설계자가 메서드 선언에 예외를 명시하는 이유는 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것 입니다. 8 | > 9 | > 메서드 호출을 try 문으로 감싼 후 catch 블록에서 아무일도 하지 않으면 예외를 무시하는 것 입니다. 10 | > 11 | > 예외는 문제 상황에 잘 대처하기 위해 존재하는데 catch 블록을 비워두면 예외가 존재할 이유가 없어집니다. 12 | > 13 | > 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꿔놓도록 합시다. 14 | > 15 | > 예측할 수 있는 예외 상황이든 프로그래밍 오류든, 빈 catch 블록으로 못 본척 지나치면 그 프로그램은 오류를 내재한 채 동작하게 됩니다. 그러다가 어느 순간 문제의 원인과 아무 상관없는 곳에서 갑자기 죽어버릴 수도 있습니다. 16 | > 17 | > 예외를 적절히 처리하면 오류를 완전히 피할 수도 있습니다. 무시하지 않고 바깥으로 전파되게만 놔둬도 최소한 디버깅 정보를 남긴 채 프로그램이 신속히 중단되게는 할 수 있습니다. 18 | 19 | 20 | 21 | ### Swift에서의 Error Handling 22 | 23 | swift programming language guide에서는 오류가 발생할 때, 일부 주변 코드는 오류의 처리를 책임져야 한다고 언급하고 있습니다. (예를 들어, 문제를 수정하거나 대안적 접근법을 시도하거나 사용자에게 고장을 알리는 것) 24 | 25 | 여기서 제시하는 스위프트에서 오류를 처리하는 방법은 네 가지가 입니다. 26 | 27 | 1. 함수에서 함수를 호출하는 코드로 오류를 전파 (Propagating Errors Using Throwing Functions) 28 | 2. do-catch 문을 사용하여 오류를 처리 (Handling Errors Using Do-Catch) 29 | 3. 오류를 Optional 값으로 처리 (Converting Errors to Optional Values) 30 | 4. 오류가 발생하지 않는다고 주장 (Disabling Error Propagation) 31 | 32 | 함수가 오류를 범하면 프로그램의 흐름이 변경되므로 코드에서 오류를 범할 수 있는 위치를 신속하게 식별할 수 있어야 합니다. 코드에서 이러한 위치를 식별하려면 오류를 던질 수 있는 함수, 메서드 또는 이니셜라이저를 호출하는 코드 조각 앞에 try 키워드(또는 try?` or `try! 변형)를 사용하라고 명시하고 있습니다. 33 | 34 | > Note (참고용) 35 | > 36 | > NOTE 37 | > 38 | > Error handling in Swift resembles exception handling in other languages, with the use of the `try`, `catch` and `throw` keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift doesn’t involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a `throw` statement are comparable to those of a `return` statement. 39 | 40 | 41 | 42 | #### 1. Propagating Errors Using Throwing Functions 43 | 44 | ```swift 45 | enum VendingMachineError: Error { 46 | case invalidSelection 47 | case insufficientFunds(coinsNeeded: Int) 48 | case outOfStock 49 | } 50 | 51 | struct Item { 52 | var price: Int 53 | var count: Int 54 | } 55 | 56 | class VendingMachine { 57 | var inventory = [ 58 | "Candy Bar": Item(price: 12, count: 7), 59 | "Chips": Item(price: 10, count: 4), 60 | "Pretzels": Item(price: 7, count: 11) 61 | ] 62 | var coinsDeposited = 0 63 | 64 | func vend(itemNamed name: String) throws { 65 | guard let item = inventory[name] else { 66 | throw VendingMachineError.invalidSelection // throw 예시 ⭐️ 67 | } 68 | 69 | guard item.count > 0 else { 70 | throw VendingMachineError.outOfStock // throw 예시 ⭐️ 71 | } 72 | 73 | guard item.price <= coinsDeposited else { 74 | throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited) // throw 예시 ⭐️ 75 | } 76 | 77 | coinsDeposited -= item.price 78 | 79 | var newItem = item 80 | newItem.count -= 1 81 | inventory[name] = newItem 82 | 83 | print("Dispensing \(name)") 84 | } 85 | } 86 | 87 | // 사용 88 | let favoriteSnacks = [ 89 | "Alice": "Chips", 90 | "Bob": "Licorice", 91 | "Eve": "Pretzels", 92 | ] 93 | func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws { 94 | let snackName = favoriteSnacks[person] ?? "Candy Bar" 95 | try vendingMachine.vend(itemNamed: snackName) // try 사용 ⭐️ 96 | } 97 | 98 | ``` 99 | 100 | `vend(itemNamed:)` 메서드는 모든 오류를 전파하므로 이 메서드를 호출하는 모든 코드는 `do-catch` 문 또는 `try?` 또는 `try!` 을 사용하여 오류를 처리하거나 오류를 계속 전파합니다. 101 | 102 | #### 2. Handling Errors Using Do-Catch 103 | 104 | Do-catch 문을 사용하여 코드 블록을 실행하여 오류를 처리할 수 있습니다. 만약 'do' 절의 코드에 의해 에러가 발생한다면, 그것은 'catch' 절과 일치하여 그 중 어느 하나가 에러를 처리할 수 있는지를 결정한다. 105 | 106 | 오류가 발생하면 즉시 실행은 캐치 절로 전송되며, 캐치 절은 전파를 계속할 것인지 여부를 결정합니다. 일치하는 패턴이 없으면 최종 캐치 절에 의해 오류가 발생하고 로컬 오류 상수에 바인딩됩니다. 오류가 발생하지 않으면 'do' 문의 나머지 문이 실행됩니다. 107 | 108 | 109 | 110 | ```swift 111 | var vendingMachine = VendingMachine() 112 | vendingMachine.coinsDeposited = 8 113 | func nourish(with item: String) throws { 114 | do { // do - catch 예시 ⭐️ 115 | try vendingMachine.vend(itemNamed: item) 116 | } catch is VendingMachineError { // do - catch 예시 ⭐️ 117 | print("Couldn't buy that from the vending machine.") 118 | } 119 | } 120 | 121 | do { // do - catch 예시 ⭐️ 122 | try nourish(with: "Beet-Flavored Chips") 123 | } catch { // do - catch 예시 ⭐️ 124 | print("Unexpected non-vending-machine-related error: \(error)") 125 | } 126 | // Prints "Couldn't buy that from the vending machine." 127 | 128 | 129 | func eat(item: String) throws { 130 | do { // do - catch 예시 ⭐️ 131 | try vendingMachine.vend(itemNamed: item) 132 | } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock { // do - catch 예시 ⭐️ 133 | print("Invalid selection, out of stock, or not enough money.") 134 | } 135 | } 136 | ``` 137 | 138 | #### 3. Converting Errors to Optional Values 139 | 140 | try?를 사용하여 오류를 Optional 값으로 변환합니다. try? 식을 평가하는 동안 오류가 발생하는 경우 식 값은 'nil'입니다. try?'를 사용하면 모든 오류를 동일한 방식으로 처리하고자 할 때 간결한 오류 처리 코드를 작성할 수 있습니다. 141 | 142 | ```swift 143 | func fetchData() -> Data? { 144 | if let data = try? fetchDataFromDisk() { return data } 145 | if let data = try? fetchDataFromServer() { return data } 146 | return nil 147 | } 148 | ``` 149 | 150 | #### 4. Disabling Error Propagation 151 | 152 | 때때로 던지기(throwing) 기능이나 방법은 실제로 런타임에 오류를 던지지 않는다는 것을 알 수 있습니다. 이러한 경우 표현식 앞에 `try!`를 작성하여 오류 전파를 비활성화하고 오류가 발생하지 않는다는 런타임 어설션(assertion, 주장)으로 호출을 마무리할 수 있습니다. (하지만 실제로 오류가 발생하면 런타임 오류가 발생합니다.) 153 | 154 | 예를 들어 다음 코드는 이미지 로드(경로:) 함수를 사용하여 지정된 경로에 이미지 리소스를 로드하거나 이미지를 로드할 수 없는 경우 오류를 발생시킵니다. 이 경우 이미지가 애플리케이션과 함께 제공되므로 런타임에 오류가 발생하지 않으므로 오류 전파를 사용하지 않도록 설정하는 것이 좋습니다. 155 | 156 | ```swift 157 | let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg") 158 | ``` 159 | 160 | 161 | 162 | ### 참고 163 | 164 | [Error Handling - the swift programming language swift 5.4](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) 165 | 166 | -------------------------------------------------------------------------------- /12장_직렬화/item0.md: -------------------------------------------------------------------------------- 1 | # Item 0. 제목은 H1로 써주세요 2 | 3 | 템플릿 페이지 입니다. 4 | 5 | ### 소제목 부터는 H3로 써주세요 6 | 7 | 소제목 내용 입니다. 8 | 9 | - 이 페이지를 복사해서 파일명을 담당한 아이템 번호에 맞게 변경 후 작업하면 편해요 10 | - 브랜치 명은 `item*`으로 지으세요 11 | - 내용 작성 완료 후 `main` 브랜치를 향해 PR을 열어 주세요 12 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item1.md: -------------------------------------------------------------------------------- 1 | # Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라 2 | 3 |
4 | 5 | ### 목차 6 | 7 | - 정적 팩터리 메서드의 장점 8 | - 호출될 때마다 인스턴스를 새로 생성하지 않아도 됩니다. 9 | - 반환 타입의 하위 타입 객체를 반환할 수 있습니다. 10 | - 입력 매개변수에 따라 매번 다른 클래스 객체를 반환할 수 있습니다. 11 | - 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됩니다. 12 | - 정적 팩터리 메서드의 단점 13 | - 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없습니다. 14 | - 정적 팩터리 메서드의 사용 예제 15 | - 핵심 정리 16 | 17 | 18 | 19 | ----- 20 | 21 | 22 | 23 | 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자입니다. 하지만 모든 프로그래머가 꼭 알아둬야 할 기법이 하나 더 있습니다. 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있습니다. 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드입니다. 24 | 25 | ```swift 26 | public static func valueOf(b: Bool) -> Bool { 27 | return b ? true : false 28 | } 29 | ``` 30 | 31 | > 지금 얘기하는 정적 팩터리 메서드는 디자인 패턴의 팩터리 메서드(Factory Method)와 다릅니다. 디자인 패턴 중에는 이와 일치하는 패턴은 없습니다. 32 | 33 | 클래스는 클라이언트에 public 생성자 대신 (혹은 생성자와 함께) 정적 팩터리 메서드를 제공할 수 있습니다. 이 방식에는 장점과 단점이 모두 존재합니다. 먼저 정적 팩터리 메서드가 생성자보다 좋은 장점을 알아봅시다. 34 | 35 | 36 | 37 | ## 장점 38 | 39 | ### 첫 번째, 호출될 때마다 인스턴스를 새로 생성하지는 않아도 됩니다. 40 | 41 | 이 덕분에 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다. 따라서 (특히 생성비용이 큰) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 줍니다. 플라이웨이트 패턴([Flyweight pattern](https://ko.wikipedia.org/wiki/플라이웨이트_패턴))도 이와 비슷한 기법이라 할 수 있습니다. 42 | 43 | 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있습니다. 이런 클래스를 인스턴스 통제(instance-controlled) 클래스라 합니다. 그렇다면 인스턴스를 통제하는 이유는 무엇일까요? 44 | 45 | 46 | 인스턴스를 통제하면 클래스를 싱글턴(singleton)으로 만들 수도, 인스턴스화 불가(noninstantiable)로 만들 수도 있습니다. 47 | 48 | ```swift 49 | // 인스턴스화 불가 50 | class NonInstanceClass { 51 | private init() { } 52 | 53 | public static func initMethod() -> NonInstanceClass { 54 | return NonInstanceClass() 55 | } 56 | } 57 | let n = NonInstanceClass.initMethod() 58 | ``` 59 | 60 | 61 | 62 |
63 | 64 | ### 두 번째, 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있습니다. 65 | 66 | 이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물합니다. 이러한 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있습니다. 67 | 68 | 69 | ```swift 70 | class Animal { 71 | private var hasWings: Bool? 72 | private var hasLegs: Bool? 73 | private var legCount: Int? 74 | 75 | init(hasWings: Bool? = false, hasLegs: Bool? = false, legCount: Int? = nil) { 76 | self.hasWings = hasWings 77 | self.hasLegs = hasLegs 78 | self.legCount = legCount 79 | } 80 | 81 | static func owl() -> Owl { 82 | return Owl(hasWings: true, hasLegs: true, legCount: 2) 83 | } 84 | 85 | static func snake() -> Snake { 86 | return Snake() 87 | } 88 | } 89 | 90 | final class Owl: Animal { 91 | func fly() { 92 | print("fly") 93 | } 94 | } 95 | 96 | final class Snake: Animal { 97 | func crawl() { 98 | print("crawl") 99 | } 100 | } 101 | 102 | let owl = Animal.owl() 103 | let snake = Animal.snake() 104 | ``` 105 | 106 | 위와 같이 작성하게 되면 Owl이나 Snake의 실제 구현 클래스를 알지 않아도 Animal을 통해 생성할 수 있게 됩니다. 107 | 108 |
109 | 110 | ### 세 번째, 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있습니다. 111 | 112 | 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없습니다. 113 | 114 | 115 | ```swift 116 | class Animal { 117 | private var hasWings: Bool? 118 | private var hasLegs: Bool? 119 | private var legCount: Int? 120 | 121 | init(hasWings: Bool? = false, hasLegs: Bool? = false, legCount: Int? = nil) { 122 | self.hasWings = hasWings 123 | self.hasLegs = hasLegs 124 | self.legCount = legCount 125 | } 126 | 127 | static func canFly(hasWings: Bool) -> Animal { 128 | return hasWings ? Owl(hasWings: true, hasLegs: true, legCount: 2) : Snake() 129 | } 130 | } 131 | ``` 132 | 133 |
134 | 135 | ### 네 번째, 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됩니다. 136 | 137 | 서비스 제공자 프레임워크에 대해 설명해 놓은 글 - [이펙티브 자바 01. 정적 팩토리 메소드와 서비스 제공자 인터페이스 (JDBC 예제)](https://plposer.tistory.com/61) 138 | 139 | - 자바 클래스를 컴파일하면 .class 파일로 변환됩니다. 여기에 해당 클래스의 정보가 들어있습니다. 140 | - 실제 해당 라인이 (처음?)실행될 때 해당 클래스 정보가 메모리의 클래스 영역(메서드 영역)에 로드됩니다. (마치 인터프리터 언어처럼) 141 | - 또한 Class.forName("com.mysql.jdbc.Driver");를 호출하면 런타임에 클래스 로더에 의해 해당 클래스 정보가 메모리에 로드됩니다. 142 | - 어떤 클래스 정보가 메모리에 로드될 때는 그 클래스 안의 static 멤버들(static 필드, static block 등..)이 실행(분석?)되며, 그로 인해 예를 들어 static 메서드 안에 명시된 클래스의 정보 또한 클래스 영역에 로드됩니다. 이 때문에 구현체 클래스를 코드에 명시할 경우 이 클래스의 정보가 클래스 영역에 로드됩니다. 143 | 144 | ```java 145 | class A { 146 | static AInterface of() { 147 | // 명시했으므로 클래스 A의 정보가 메모리에 로드될 때 148 | // Aimpl의 클래스 정보 또한 클래스 영역에 로드된다. 149 | // 그래야 of 메서드가 실행될 때 Aimpl을 인스턴스화해서 돌려줄 수 있음 150 | return Aimpl() 151 | } 152 | } 153 | ``` 154 | 155 | 156 | (내가 생각하기에) 이 방법의 장점은 DB 드라이버의 구현체를 아무데서도 명시하지 않아서, 모든 드라이버들이 컴파일은 되지만, Class.forName으로 명시한 클래스만 메모리에 로드되는 듯합니다. 157 | - 또한, 여기서 정적 팩터리 메서드를 이용하면 반환할 객체의 클래스를 명시하지 않고 인터페이스만으로 다룰 수 있어서 해당 클래스 정보가 메모리에 로드되지 않습니다. 158 | 159 | 160 |
161 | 162 | ## 단점 163 | 164 | ### 상속을 하려면 public이나 internal 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없습니다. 165 | 166 | 167 | ```swift 168 | // 인스턴스화 불가 169 | class NonInstanceClass { 170 | private init() { } 171 | 172 | public static func initMethod() -> NonInstanceClass { 173 | return NonInstanceClass() 174 | } 175 | } 176 | 177 | class InheritedClass: NonInstanceClass { 178 | 179 | } 180 | 181 | let inheritedClass = InheritedClass() // 에러 발생 'InheritedClass' cannot be constructed because it has no accessible initializers 182 | ``` 183 | 184 | 이런식으로 인스턴스화가 불가하도록 만들어진 클래스는 상속이 불가합니다. 불변타입으로 만들기 위해서는 이러한 제약이 장점으로 받아들여질 수도 있습니다. 185 | 186 |
187 | 188 | ## 사용 예제 189 | 190 | 정적 팩터리 메서드가 유용하게 사용될 수 있는 두가지 방법에 대해 소개하고자 합니다. 191 | 192 | - UI 요소 생성시 193 | - 테스트 stub 객체 생성시 194 | 195 | UIViewController의 하위 요소들을 configure할 때 보통은 아래와 같은 방법을 사용합니다. 196 | 197 | ```swift 198 | class TitleLabel: UILabel { 199 | override init(frame: CGRect) { 200 | super.init(frame: frame) 201 | 202 | font = .boldSystemFont(ofSize: 24) 203 | textColor = .darkGray 204 | adjustsFontSizeToFitWidth = true 205 | minimumScaleFactor = 0.75 206 | } 207 | } 208 | ``` 209 | 210 | 위와 같은 접근방식에 문제가 있는 것은 아니지만, 우리는 종종 같은 종류의 UI에 대해 세부 요소만 다른 여러 개의 하위 클래스를 갖게 됩니다.(`TitleLabel`, `SubtitleLabel`, `FeaturedTitleLabel` 등) 211 | 212 | 서브클래싱은 중요한 작업이지만, 현재 작업은 새로운 동작을 추가하는 것이 아닌 인스턴스화 하는 과정이어서 서브클래싱 하는 것이 이 목적에 부합한가 하는 의문이 듭니다. 그래서 어떤 동작을 가지고 있는 것이 아니라면, 정적 팩터리 메서드를 통해 새로운 인스턴스를 만들 수 있습니다. 213 | 214 | ```swift 215 | private extension UILabel { 216 | static func makeForTitle() -> UILabel { 217 | let label = UILabel() 218 | label.font = .boldSystemFont(ofSize: 24) 219 | label.textColor = .darkGray 220 | label.adjustsFontSizeToFitWidth = true 221 | label.minimumScaleFactor = 0.75 222 | return label 223 | } 224 | } 225 | ``` 226 | 227 | 이런 접근의 장점은, 설정 부분을 실질적인 동작 부분과 분리할 수 있다는 것입니다. 또한 private 접근 제한자를 추가해 단일 파일로 범위를 지정할 수 있어서 앱의 일부에만 단일 기능을 가지도록 확장할 수 있습니다. 228 | 229 | ```swift 230 | class ProductViewController { 231 | private lazy var titleLabel = UILabel.makeForTitle() 232 | } 233 | ``` 234 | 235 | 그럼 위와 같이 간단하게 UI 요소를 만들 수 있습니다. 236 | 237 | 238 | 239 | 그리고 테스트 코드를 작성하는 경우가 있습니다. 특히 특정 모델에 의존하여 테스트 코드를 작성할 때, 보일러플레이트가 많이 발생하는 코드를 작성하게 되는 경우가 많아 읽기 어렵고 디버깅이 어려워질 수 있습니다. 240 | 241 | 이럴 때 정적 팩터리 메서드로 stub 데이터를 가진 모델객체를 생성하게끔 만들어 놓으면, 테스트 시 해당 메소드만을 호출하여 간단하게 stub을 가져다 쓸 수 있습니다. 242 | 243 | ```swift 244 | extension User { 245 | static func makeStub(permissions: Set) -> User { 246 | return User( 247 | name: "TestUser", 248 | age: 30, 249 | signUpDate: Date(), 250 | permissions: permissions 251 | ) 252 | } 253 | } 254 | ``` 255 | 256 | 정적 팩토리 메서드의 이름을 지정하여 메인 앱에 추가하지 않고 테스트용으로만 사용할 수 있습니다. 이렇게 하게되면 코드를 실제 로직과 명확하게 구분할 수 있고, 깨끗한 테스트 코드를 쉽게 작성하는데에 도움이 됩니다. 257 | 258 |
259 | 260 | ## 핵심 정리 261 | 262 | 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋습니다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고쳐야 합니다. 263 | 264 | 265 | 266 |
267 | 268 | ### 참고 269 | https://www.swiftbysundell.com/articles/static-factory-methods-in-swift/ 270 | 271 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item3.md: -------------------------------------------------------------------------------- 1 | # Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 2 | 3 | ### 싱글턴(Singleton) 4 | 5 | 싱글턴이란 앱이 요청하는 횟수에 관계없이 동일한 인스턴스를 반환하는 클래스를 의미합니다. 일반적인 클래스는 호출하는 만큼 클래스의 인스턴스를 만들 수 있도록 허용하는 반면, 싱글턴 클래스의 경우 프로세스 당 인스턴스가 하나만 존재할 수 있습니다. 따라서 환경설정, 네트워크 관리와 같이 앱 전체에서 공유되는 리소스 또는 서비스에 주로 사용합니다. `FileManager.default`, `URLSession.shared`와 같이 실제 Cocoa 프레임워크 계층의 여러 클래스에 싱글턴 디자인 패턴이 적용되어 있습니다. 6 | 7 |
8 | 9 | 10 | 11 | > 출처: [Cocoa Core Competencies - Singleton](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Singleton.html) 12 | 13 |
14 | 15 | ### 싱글턴 생성 16 | 17 | 책에서 자바를 사용한 싱글턴 생성 방식으로 `private` 생성자와 열거 타입을 소개하는데, 스위프트는 `private` 생성자를 사용하는 방식을 취합니다. 스위프트의 열거 타입은 값 타입(value type)으로, 앱 전체에서 공유하여 사용하는 싱글턴의 용도에 부합하지 않고, 열거 타입 내부에 원시값으로 싱글턴 인스턴스를 갖는다면 외부에서 생성이 가능한 것이기 때문에 그 자체로 의미가 없습니다. 18 | 19 | 스위프트의 싱글턴은 `static` 타입 프로퍼티와 `private` 접근 수준의 생성자를 사용하여 생성할 수 있습니다. `static` 타입 프로퍼티를 통한 지연 연산으로 처음 요청될 때 자신의 유일한 인스턴스를 생성하고, 생성자가 `private`으로 설정되어 외부에서 호출할 수 없기 때문에 다른 인스턴스를 생성할 수 없도록 합니다. 20 | 21 | ```swift 22 | class Singleton { 23 | static let shared = Singleton() 24 | 25 | private init() { } 26 | } 27 | ``` 28 | 29 |
30 | 31 | ### 주의할 점 32 | 33 | 싱글턴이 일반적으로 야기하는 문제는 다음과 같습니다. 34 | 35 | - 싱글턴 생성 시 사용하는 `static` 타입 프로퍼티는 여러 스레드에서 동시에 접근하는 경우 한 번만 생성되는 것을 보장하지만, 동시에 참조할 경우 원치 않은 결과를 가져올 수 있으므로 코드가 스레드로부터 안전한지 고려해야 합니다. 36 | - 앱 전체에서 공유되기 때문에 싱글턴 인스턴스의 상태가 예기치 못하게 변경되면 버그가 발생할 수 있습니다. 37 | - 테스트하기가 어려워질 수 있습니다. 각 테스트 케이스에서 깨끗한 초기 상태로 시작할 수 없기 때문입니다. 또한 프로토콜을 채택한 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 인스턴스로 대체하기 어렵습니다. 38 | 39 |
40 | 41 | ### 의존성 주입 42 | 43 | 위에서 설명한 싱글턴의 문제점을 보완하기 위해 의존성 주입을 활용할 수 있습니다. 여기서 의존성이란 서비스로 사용할 수 있는 객체이고, 주입은 의존성(서비스)을 사용하려는 객체로 전달하는 것을 의미합니다. 즉, 객체가 어떤 서비스를 사용할 것인지 지정하는 대신, 객체에게 어떤 서비스를 사용할 것인지를 말해주는 것입니다. 44 | 45 | 현재 로그인한 사용자의 이름을 표시하고 버튼을 탭하면 로그아웃하는 상황을 가정해보겠습니다. 아래의 싱글턴 예시에서는 사용자 모델과 계정 처리 기능을 싱글턴 클래스인 `UserManager`가 포함하고, 화면이 표시될 때 사용자의 이름에 `UserManager.shared.user?.name`과 같이 옵셔널로 접근하는 것을 볼 수 있습니다. `UserManager` 클래스에 `User` 타입을 옵셔널로 정의한 이유는 사용자의 정보가 존재하지 않을 수 있기 때문입니다. 예를 들어, 사용자가 로그인을 하기 전까지는 사용자 정보가 존재하지 않습니다. 46 | 47 | 의존성 주입 예시에서는 옵셔널이 아닌 `User`와 `SignOutService`를 주입합니다. 결과적으로 훨씬 더 명확하고 관리하기 쉬워집니다. 이 방식을 택할 경우 모델에 안전하게 의존할 수 있으며, 명확한 API를 갖습니다. 48 | 49 | - 싱글턴 50 | 51 | ```swift 52 | class UserManager { 53 | static let shared = UserManager() 54 | var user: User? 55 | 56 | private init() { } 57 | 58 | func signUp() { 59 | // sign up code 60 | } 61 | 62 | func signIn() { 63 | // sign in code 64 | } 65 | 66 | func signOut() { 67 | // sign out code 68 | } 69 | } 70 | 71 | class User { 72 | var name: String 73 | 74 | init(name: String) { 75 | self.name = name 76 | } 77 | } 78 | 79 | class ProfileViewController: UIViewController { 80 | private lazy var nameLabel = UILabel() 81 | 82 | override func viewDidLoad() { 83 | super.viewDidLoad() 84 | nameLabel.text = UserManager.shared.user?.name 85 | } 86 | 87 | private func signOutButtonTapped() { 88 | UserManager.shared.signOut() 89 | } 90 | } 91 | ``` 92 | 93 | - 의존성 주입 94 | 95 | ```swift 96 | class User { 97 | var name: String 98 | 99 | init(name: String) { 100 | self.name = name 101 | } 102 | } 103 | 104 | class SignOutService { 105 | func signOut() { 106 | // sign out code 107 | } 108 | } 109 | 110 | class ProfileViewController: UIViewController { 111 | private let user: User 112 | private let signOutService: SignOutService 113 | private lazy var nameLabel = UILabel() 114 | 115 | init(user: User, signOutService: SignOutService) { 116 | self.user = user 117 | self.signOutService = signOutService 118 | super.init(nibName: nil, bundle: nil) 119 | } 120 | 121 | override func viewDidLoad() { 122 | super.viewDidLoad() 123 | nameLabel.text = user.name 124 | } 125 | 126 | private func signOutButtonTapped() { 127 | signOutService.signOut() 128 | } 129 | } 130 | ``` 131 | 132 |
133 | 134 | ### 결론 135 | 136 | 싱글턴은 Apple 자체에서도 많이 사용하는 만큼 편리하다는 장점이 있습니다. 어디에서나 접근이 가능하고 앱 전체에서 공유될 수 있습니다. 그러나 멀티 스레드 환경에서 동시에 참조한다거나, 객체 간의 명확한 분리 없이 광범위하게 사용할 경우 원치 않은 결과와 버그가 발생할 수 있습니다. 따라서 객체 간에 보다 잘 정의된 관계를 만들고, 의존성 주입을 사용하는 등 주의가 필요합니다. 137 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item4.md: -------------------------------------------------------------------------------- 1 | # 인스턴스화를 막으려거든 private 생성자를 사용하라 2 | 3 | ### 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때. 4 | > 이따금 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있을 것이다. 5 | 전역적으로 사용하기 위해서 사용합니다. 클래스를 인스턴스화 하지 않고도 직접 호출할 수 있습니다. 6 | 보통 안티패턴이라고 이야기하는데, 그럼 언제 사용해야 할까요? 7 | 8 | 여기서도 예제를 들어주고 있는데 9 | 1. **관련된 메서드들을 한데 모아놓고 사용할때** 10 | ex) java.lang.Math, java.util.Arrays 11 | (실제로 정적프로퍼티와 정적 메소드로 이루어져 있습니다.) 12 | Swift에서 기본 제공하는 Class에서는 찾을 수가 없었고 예시를 가져와봤습니다. 13 | ```swift 14 | enum AppStyles { 15 | enum Colors { 16 | static let mainColor = UIColor(red: 1, green: 0.2, blue: 0.2, alpha: 1) 17 | static let darkAccent = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) 18 | } 19 | 20 | enum FontSizes { 21 | static let small: CGFloat = 12 22 | static let medium: CGFloat = 14 23 | static let large: CGFloat = 18 24 | static let xlarge: CGFloat = 21 25 | } 26 | } 27 | ``` 28 | 색상이나 글꼴 크기를 인스턴스화 시켜 앱 곳곳에 흩어져있게 하는것 보다 위와같이 한곳에 정의하는 것이 훨씬 관리하기 편합니다.
29 | enum을 사용한 이유는 이니셜라이저가 없어 인스턴스화가 불가능하고, static property를 사용해 인스턴스화 하지 않고 바로 접근할 수 있기 때문입니다.
30 | (전역적으로 설정 + 애초에 enum은 stored property를 가지고 있지 못합니다.) 31 | 32 | 2. **특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드나 팩토리를 구현할때** 33 | ex) java.util.Collections 34 | 사용자 대신해서 객체의 인스턴스를 만들어 낸다던가, 간단하게 사용해서 복잡한 객체를 만들어 낼때 사용합니다. 35 | ```swift 36 | enum BlogpostFactory { 37 | static func create(withTitle title: String, body: String) -> Blogpost { 38 | let metadata = Metadata(/* metadata properties */) 39 | return Blogpost(title: title, body: body, createdAt: Date(), metadata: metadata) 40 | } 41 | } 42 | ``` 43 | 3. **final 클래스와 관련한 메서드들을 모아놓을때** 44 | 45 | ### 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다. 46 | 매개변수를 받지 않는 public 생성자가 만들어지고, 사용자는 구분할 수 없습니다.
47 | 직접 선언한 생성자가 없는 경우에 한해서 컴파일러가 자동으로 기본 생성자를 만들어준다고 합니다.
48 | -> 해당부분은 Swift와 동일합니다. 49 | 50 | ### 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 51 | Swift는 추상 클래스라는 것이 존재하지 않습니다.
52 | 비슷한 개념이 있다면 Protocol을 들 수 있겠습니다. 53 | 54 | #### 그럼 Java에서 추상 클래스란? 55 | Protocol과 비슷합니다.
56 | (재)사용할 프로퍼티와 메소드 이름을 통일하여 객체의 유지보수성을 높이고 통일성을 유지할 수 있는 장점을 얻기 위해 사용합니다. 57 | 58 | 다른 클래스가 추상 클래스를 '상속'받아 실제 내용(프로퍼티, 메소드)를 구현합니다.
59 | Protocol은 클래스가 '채택'하여 사용이 가능합니다. 60 | 61 | ### 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 62 | 추상 클래스 관련이야기라 Swift와 관계 없다고 판단하였습니다.
63 | (Protocol로는 인스턴스화를 막을 수 없다..와 같은 맥락은 아닌것 같다고 생각합니다.) 64 | 65 | ### private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다. 66 | java에서 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을때 뿐이라고 합니다.
67 | -> Swift또한 private한 생성자를 만들 수 있습니다.
68 | -> 여기서도 언급을 하지만, 생성자가 존재하는데 호출할 수 없는 코드는 직관적이지 않아 주석을 달아주는 것을 권장하고 있습니다. 69 | 70 | 해당 방식은 상속을 불가능하게 하는 효과가 있습니다.
71 | : 하위 클래스를 만들더라도, 상위 클래스의 생성자에 접근하지 못하므로 상속이 이루어지지 않는 효과도 동시에 누립니다.
72 | -> 인스턴스화 + 상속의 문제를 동시에 해결하려면 Enum을 써야하는게 맞는것 같다고 생각합니다.
73 | -> 상속의 문제는 final class를 만들어 해결하고, 파편화된 인스턴스화가 신경이 쓰인다면 공유인스턴스 혹은 싱글톤을 만들어 사용하는게 좋은 방법이라 생각됩니다. 74 | 75 | ```swift 76 | // 코드 4-1 인스턴스를 만들 수 없는 유틸리티 클래스 (26~27쪽) 77 | class UtilityClass { 78 | // 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용). 79 | 80 | private init() { 81 | print("private init!") 82 | } 83 | 84 | // 나머지 코드는 생략 85 | } 86 | ``` 87 | 88 | - 인스턴스화 시도한 결과 89 | ![](https://i.imgur.com/EIt5PgR.png) 90 | 91 |
92 | 93 | 참고한 페이지 94 | 1. https://www.donnywals.com/effectively-using-static-and-class-methods-and-properties/ 95 | 2. https://limkydev.tistory.com/188 96 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item5.md: -------------------------------------------------------------------------------- 1 | # Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 2 | 3 | 클래스가 내부적으로 하나 이상의 자원에 의존하고 그 자원이 클래스 동작에 영향을 줄 때의 경우 자원을 직접 명시하지 말고 의존 객체 주입을 사용하는 것이 좋습니다. 4 | 5 | ### 정적 유틸리티와 싱글턴을 잘못 사용한 예 6 | 7 | 사용하는 자원에 따라 동작이 달라지는 클래스에서 정적 유틸리티 클래스나 싱글턴 방식은 적합하지 않습니다. 8 | 9 | 1. 정적 유틸리티를 잘못 사용한 예 10 | ```swift 11 | class SpellChecker { 12 | private static var dictionary = Lexicon() 13 | private init() { ... } 14 | 15 | static func isValid(word: String) -> Bool { ... } 16 | static func suggestion(typo: String) -> [String] { ... } 17 | } 18 | ``` 19 | 2. 싱글턴을 잘못 사용한 예 20 | ```swift 21 | class SpellChecker { 22 | private var dictionary = Lexicon() 23 | private init( ... ) {} 24 | 25 | static let sharedInstance = SpellChecker() 26 | func isValid(word: String) -> Bool { ... } 27 | func suggestion(typo: String) -> [String] { ... } 28 | } 29 | ``` 30 | 3. 상수 dictionary을 변수로 변경하고 다른 사전으로 교체하는 메서드를 추가한 예 31 | ```swift 32 | class SpellChecker { 33 | private var dictionary = Lexicon() 34 | private init( ... ) {} 35 | 36 | static let sharedInstance = SpellChecker() 37 | func isValid(word: String) -> Bool { ... } 38 | func suggestion(typo: String) -> [String] { ... } 39 | func changeDictionary(to newDictionary: Lexicon) { ... } 40 | } 41 | ``` 42 | 43 | * 코드 1과 코드 2는 사전을 단 하나만 사용한다고 가정하고 구현하였습니다. 유연하지 않고 테스트하기 어렵습니다. 44 | * 코드 3은 멀티스레드 환경에서 사용할 수 없습니다. 45 | 46 | **따라서 클래스가 여러 인스턴스를 지원해야하며 클라이언트가 원하는 자원을 사용해야하는 경우에는 정적 유틸리티 클래스나 싱글턴 방식을 사용하기 보다 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 것이 좋습니다.(의존 객체 주입의 한 형태로 생성자 주입에 해당)** 47 | 48 | ### 생성자 주입을 사용한 예 49 | 50 | 1. 생성자 주입을 사용한 예 51 | ```swift 52 | class SpellChecker { 53 | private var dictionary = Lexicon() 54 | private init(newDictionary: Lexicon) { 55 | self.dictionary = newDictionary 56 | } 57 | 58 | func isValid(word: String) -> Bool { ... } 59 | func suggestion(typo: String) -> [String] { ... } 60 | } 61 | ``` 62 | * 코드 4는 테스트 용이성을 높여줍니다. 63 | * 생성자에 팩터리 메서드 패턴(Factory Method pattern)을 이용하여 자원 팩터리(호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체)를 넘겨주는 방식으로 변형해 활용할 수 있습니다. 64 | 65 | ### 의존성 주입(Dependency Injection) 66 | 67 | * **의존성 주입(Dependency Injection)이란?** 68 | - 의존성: 함수에 필요한 클래스 또는 참조 변수나 객체에 의존하는 것. 69 | - 주입: 내부에서 필요한 객체를 생성하여 참조/사용하지 않고 외부에서 객체를 생성해 넣어주는 것. 70 | - 의존성 주입: 코드에서 두 모듈 간의 연결. 객체지향 언어에서는 두 클래스 간의 관계라고도 한다. 71 | 72 | * **의존성 주입의 장점** 73 | 1. 객체 간의 결합도(Coupling)을 낮춰 의존성을 줄여 유지보수가 용이해집니다. 74 | - 객체 간 의존성(종속성)이 감소해 변경에 민감하지 않습니다. 75 | 2. 재사용성이 증가합니다. 76 | 3. 리팩토링이 수월합니다. 77 | 4. Protocol을 사용하는 경우, Protocol에 구현체를 쉽게 교체하면서 상황에 따라 적절한 행동을 정의할 수 있습니다. 78 | 5. 테스트가 용이합니다. 79 | - 주입할 의존 객체를 Mock 객체로 구현한 후 주입할 수 있습니다. 80 | 6. 보일러 플레이트 코드(Boilerplate code , 꼭 필요하면서 간단한 기능에 비해 많은 코드를 필요로 하는 코드를 의미 한다. 예를 들면 setter, getter을 의미한다.) 감소시킬 수 있습니다. 81 | 7. 같은 자원을 사용하려는 여러 클라이언트가 의존 객체들을 안전하게 공유할 수 있습니다. 82 | 8. 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있습니다. 83 | 84 | * **DI의 단점은?** 85 | 1. 의존성 주입을 위한 선행 작업 필요합니다. 86 | 2. 코드를 추척하고 읽기 어려울 수 있습니다. 87 | 88 | ### 의존성 주입 방법 89 | 90 | 1. Initializer injection(Constructor Injection) 91 | 92 | ```swift 93 | protocol Drinkable { 94 | var volume: Int { get } 95 | } 96 | 97 | struct Pepsi: Drinkable { 98 | var volume: Int { 99 | return 500 100 | } 101 | } 102 | 103 | class VendingMachine { 104 | let beverage: Drinkable 105 | 106 | init(_ beverage: Drinkable) { 107 | self.beverage = beverage 108 | } 109 | } 110 | 111 | let vendingMachine = VendingMachine(Pepsi()) 112 | ``` 113 | 114 | 2. Property Injection 115 | 116 | ```swift 117 | protocol Drinkable { 118 | var volume: Int { get } 119 | } 120 | 121 | struct Pepsi: Drinkable { 122 | var volume: Int { 123 | return 500 124 | } 125 | } 126 | 127 | class VendingMachine { 128 | var beverage: Drinkable? 129 | } 130 | 131 | let vendingMachine = VendingMachine() 132 | vendingMachine.beverage = Pepsi() 133 | ``` 134 | 135 | 3. Method Injection 136 | 137 | ```swift 138 | protocol Drinkable { 139 | var volume: Int { get } 140 | } 141 | 142 | struct Pepsi: Drinkable { 143 | var volume: Int { 144 | return 500 145 | } 146 | } 147 | 148 | class VendingMachine { 149 | var beverage: Drinkable? 150 | 151 | func setupBeverage(_ beverage: Drinkable) { 152 | self.beverage = beverage 153 | } 154 | } 155 | 156 | let vendingMachine = VendingMachine() 157 | vendingMachine.setupBeverage(Pepsi()) 158 | ``` 159 | 160 | ### iOS 활용 예시 161 | 162 | 1. Property Injection 163 | ```swift 164 | // Property Injection 165 | import UIKit 166 | 167 | class ViewController: UIViewController { 168 | var requestManager: RequestManager? 169 | } 170 | 171 | class newViewController { 172 | let viewController = ViewController() 173 | viewController.requestManager = RequestManager() 174 | } 175 | 176 | // Using Protocol 177 | protocol Serializer { 178 | func serialize(data: AnyObject) -> NSData? 179 | } 180 | 181 | class RequestSerializer: Serializer { 182 | func serialize(data: AnyObject) -> NSData? { 183 | ... 184 | } 185 | } 186 | 187 | class DataManager { 188 | var serializer: Serializer? = RequestSerializer() 189 | } 190 | ``` 191 | 2. Initializer injection 192 | * 네트워크에 이미지 요청 193 | 194 | ```swift 195 | import UIKit 196 | 197 | protocol UserManagable { 198 | func getUser(with userID: Int) 199 | } 200 | 201 | protocol ImageManagable { 202 | func getImages(of userID: Int) -> [UIImage] 203 | } 204 | 205 | class ImageNetworkManager: ImageManagable { 206 | private let userManager: UserManagable 207 | 208 | // 의존성 주입 209 | init(userManager: UserManagable) { 210 | self.userManager = userManager 211 | } 212 | 213 | func getImages(of userID: Int) -> [UIImage] { 214 | let user = userManager.getUser(with: userID) 215 | let images = { ... } 216 | return images 217 | } 218 | } 219 | 220 | class PhotoGalleryController { 221 | private let imageManager: ImageManagable 222 | 223 | // 의존성 주입 224 | init(imageManager: ImageManagable) { 225 | self.imageManager = imageManager 226 | } 227 | 228 | func getImagesSortedInRecentOrder(of userID: Int) -> [UIImage] { 229 | let images = imageManager.getImages(of: userID) 230 | return images.sorted { $0.timestamp > $1.timestamp } 231 | } 232 | } 233 | ``` 234 | 235 | ### 핵심 정리 236 | 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋습니다. 이 자원들을 클래스가 직접 만들게 해서도 안됩니다. 대신 필요한 자원을(혹은 그 자원을 만들어주는 팩터리를) 생성자에 (혹은 정적 팩터리나 빌더에) 넘겨주는 것이 좋습니다. 의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해줍니다. 237 | 238 | ### 참고 239 | 240 | 1. [[DI\] 의존성 주입(Dependency Injection) 을 해주는 세가지 방법](https://eunjin3786.tistory.com/115) 241 | 2. [[DI] Dependency Injection 이란?](https://medium.com/@jang.wangsu/di-dependency-injection-%EC%9D%B4%EB%9E%80-1b12fdefec4f) 242 | 3. [DI(Dependency Injection)에 대해 알아보자 ](https://velog.io/@jojo_devstory/DIDependency-Injection%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90) 243 | 4. [Dependency Injection in Swift](https://cocoacasts.com/dependency-injection-in-swift) 244 | 245 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item6.md: -------------------------------------------------------------------------------- 1 | # Item 6. 불필요한 객체 생성을 피하라 2 | 3 | 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많습니다. 재사용은 빠르고 세련됩니다. 특히 불변 객체(아이템 17)은 언제든 재사용할 수 있습니다. 4 | 5 | 불필요한 객체를 생성하는 경우, 즉 객체를 재사용하는게 좋은 경우에 대해 알아봅시다. 6 | 7 |
8 | 9 | ### 자주 사용되는 객체의 재사용 10 | 11 | 같은 기능을 가진 객체를 새로 생성하는 것보다는 재사용하는 것이 나을 때가 있습니다. 이처럼 싱글톤 패턴으로 인스턴스를 만들고 재사용할 수 있습니다. 12 | 13 | ```swift 14 | class Device { 15 | private static let sharedInstance: Device = { 16 | let instance = Device() 17 | // 디바이스 설정 18 | return instance 19 | }() 20 | 21 | private init() { } 22 | 23 | public static func initMethod() -> Device { 24 | return instance 25 | } 26 | } 27 | ``` 28 | 29 |
30 | 31 | ### 인스턴스 생성 비용이 높은 객체의 재사용 32 | 33 | 서버에서 받아온 데이터나 디스크에서 읽어온 데이터처럼 큰 용량의 객체나 인스턴스를 생성할 때 비용의 높은 객체의 경우에도 객체를 캐싱하는 등 재사용하는 것이 좋습니다. 34 | 35 | ```swift 36 | class ArticleLoader { 37 | typealias Handler = (Result) -> Void 38 | 39 | private let cache = Cache() 40 | 41 | func loadArticle(withID id: Article.ID, 42 | then handler: @escaping Handler) { 43 | if let cached = cache[id] { 44 | return handler(.success(cached)) 45 | } 46 | 47 | performLoading { [weak self] result in 48 | let article = try? result.get() 49 | article.map { self?.cache[id] = $0 } 50 | handler(result) 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | 57 | 58 |
59 | 60 | 이번 아이템을 "객체 생성은 비싸니 피해야 한다"로 오해하면 안 됩니다. ARC같은 자동 참조 해제 기능이 있으므로, 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않습니다. 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일입니다. 61 | 62 | 거꾸로, 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 *객체 풀(pool)을 만들지는 마세요. 물론 객체 풀을 만드는 게 나은 예가 있긴 합니다. 네트워크 연결처럼 접근 비용이 비싸지는 경우 재사용하는 편이 낫습니다. 하지만 일반적으로는 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨립니다. 63 | 64 | > 객체 풀(Object pool)? 65 | > 66 | > 객체를 매번 할당, 해제하지 않고 고정 크기 풀에 들어있는 객체를 재사용함으로써 메모리 사용 성능을 개선함. 67 | > 68 | > 객체들의 크기가 비슷하거나 객체를 빈번하게 생성/삭제하는 경우, 객체를 힙에 생성하기가 느리거나 생성 비용이 비싼 객체를 캡슐화하는 경우 사용. 객체를 위한 메모리 크기가 고정되어 가장 큰 자료형에 맞춰야하고, 메모리 낭비 가능성이 있어 사용에 주의해야 함. 69 | 70 | 71 | 72 | ### 결론 73 | 74 | 이번 아이템은 방어적 복사(defensive copy)를 다루는 아이템 50과 대조적입니다. 이번 아이템이 **"기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라"**라면, 아이템 50은 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라"입니다. **방어적 복사가필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하세요. **방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 줍니다. 75 | 76 | 77 | 78 | ##### References 79 | 80 | - [디자인 패턴 - 객체 풀(Object pool)](http://hajeonghyeon.blogspot.com/2017/06/object-pool.html) 81 | 82 | - [cashing in swift - swiftbysundell](https://www.swiftbysundell.com/articles/caching-in-swift/) 83 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item8.md: -------------------------------------------------------------------------------- 1 | # Item 8. finalizer와 cleaner 사용을 피하라 2 | 3 | 4 | 5 | Java의 객체 소멸자에 대해서 간단하게 소개한 후 Swift의 디이니셜라이저를 활용한 예제를 소개합니다. 6 | 7 | ### Java의 객체 소멸자 8 | 9 | [finalize()](https://docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--) 메서드는 클래스의 인스턴스가 더이상 참조되지 않을 때 가비지 컬렉터(Garbage Collector)가 힙에서 객체를 제거하기 전에 자동으로 호출 됩니다. 10 | 11 | ```java 12 | @Override 13 | public void finalize() { 14 | try { 15 | reader.close(); 16 | System.out.println("Closed BufferedReader in the finalizer"); 17 | } catch (IOException e) { 18 | // ... 19 | } 20 | } 21 | ``` 22 | 23 | [Cleaner](https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Cleaner.html) 는 해당 객체가 [phantom reachable](https://docs.oracle.com/javase/8/docs/api/java/lang/ref/PhantomReference.html)이 되었을 때를 통지 받은 후 실행되도록 Cleaning actions을 등록합니다. GC에 의해 수거될 객체들은 `register()` 메서드를 사용하여서 cleaner 객체에 등록되어야 합니다. 24 | 25 | ```java 26 | public class CleaningExample implements AutoCloseable { 27 | // A cleaner, preferably one shared within a library 28 | private static final Cleaner cleaner = ; 29 | 30 | static class State implements Runnable { 31 | 32 | State(...) { 33 | // initialize State needed for cleaning action 34 | } 35 | 36 | public void run() { 37 | // cleanup action accessing State, executed at most once 38 | } 39 | } 40 | 41 | private final State; 42 | private final Cleaner.Cleanable cleanable 43 | 44 | public CleaningExample() { 45 | this.state = new State(...); 46 | this.cleanable = cleaner.register(this, state); // 등록 47 | } 48 | 49 | public void close() { 50 | cleanable.clean(); 51 | } 52 | } 53 | ``` 54 | 55 | 56 | 57 | ### Java의 finalizer와 cleaner 특징 58 | 59 | Java의 객체 소멸자인 finalizer와 cleaner는 다음과 같은 특징을 가지고 있습니다. 60 | 61 | * finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요해 기본적으로 사용하지 않는 것을 권합니다. 62 | * cleaner는 finalizer보다 덜 위험하지만 finalizer와 마찬가지로 예측할 수 없고 느리며 일반적으로 불필요합니다. 63 | * finalizer와 cleaner가 얼마나 신속히 수행할지는 [가비지 컬렉터(CG)](chapter2/item7.md) 알고리즘에 달려있으며 가비지 컬렉터 구현마다 상이합니다. 64 | * Java 언어 명세는 finalizer와 cleaner의 수행 시점 뿐만 아니라 수행 여부도 보장하지 않습니다. 65 | * Finalizer 스레드는 다른 애플리케이션 스레드보다 우선순위가 낮아 실행될 기회를 얻지 못할 수도 있습니다. 자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않습니다. 66 | 67 | 68 | 69 | ### Swift에서는 70 | 71 | Swift에서는 클래스의 인스턴스 레퍼런스 카운트가 0이 되면 메모리에서 할당 해제합니다. 그리고 클래스의 인스턴스가 소멸되기 직전에 [deinit](https://docs.swift.org/swift-book/LanguageGuide/Deinitialization.html)이 호출됩니다. 즉, Java에서와 달리 개발자가 인스턴스의 소멸 시점과 deinit이 불릴 시점을 예측할 수 있습니다. 72 | 73 | 그렇다면 deinit에서는 어떤 일을 할 수 있을까요? 74 | 75 | 책 본문에 나온 C++의 내용과 같이 Swift도 인스턴스가 소멸될 때 deinit을 통해 비메모리 자원 회수 용도(reclaim other [nonmemory resources](https://stackoverflow.com/a/7037712))로 사용할 수 있습니다. 76 | 77 | > * 비메모리 자원(nonmemory resources) 78 | > : 메모리의 일부를 차지하면서 다른 리소스 일부에도 접근할 수 있는 권한이 있는 데이터베이스, 네트워크, 파일 등 79 | 80 | `deinit` 때 처리해줄 일의 예시(NotificationCenter, FileHandle, DBConnection(SQLite))로는 [item9](item9.md)에서 설명하고 있습니다. 이번 아이템에서는 추가적인 예시로 RxSwift의 `Dispose()` 메서드와 `DisposeBag`에 대해서 설명하겠습니다. 아래에 예시는 다양한 구현방법 중 하나입니다. 81 | 82 | 83 | 84 | ### RxSwift 85 | 86 | `Dispose()` 메서드와 `DisposeBag`을 소개하기 앞서 두 가지 필요한 내용을 정리하겠습니다. 87 | 88 | > * Obsevable: 변화의 알림을 보냅니다. 89 | > 90 | > * Observer: Observable을 구독하고 Observable이 변화되었을 때 알림을 받습니다. 91 | 92 | 93 | 94 | ### RxSwift의 `Dispose()` 95 | 96 | RxSwift 에서 [Observable](https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Observable.swift) 을 subscribe(구독) 하면 항상 [Disposable](https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Disposable.swift) 을 반환합니다. 이 Disposable 들을 dispose 해주지 않으면 메모리에 계속 남아 메모리 누수가 발생합니다. 따라서 구독한 Disposable 들을 명시적으로 dispose 해줘야합니다. 97 | 98 | ```swift 99 | final class MyViewController: UIViewController { 100 | var subscription: Disposable? 101 | 102 | override func viewDidLoad() { 103 | super.viewDidLoad() 104 | subscription = theObservable().subscribe(onNext: { 105 | // handle your subscription 106 | }) 107 | } 108 | 109 | deinit { 110 | subscription?.dispose() 111 | } 112 | } 113 | ``` 114 | 115 | 116 | 117 | **위와 같은 방법을 사용하면 일일이 Dispoable 프로토콜에 구현되어있는 dispose() 를 호출하여 없애줘야 합니다.** 하지만 [RxSwift 가이드 문서의 Disposing](https://github.com/ReactiveX/RxSwift/blob/master/Documentation/GettingStarted.md#disposing) 에서 권장하는 방법 중 하나는 `dispose()` 를 직접 호출하지 않고 `DisposeBag`을 사용하는 것입니다. `DisposeBag` 이 할당 해제될 때, 각 dispoable에 `dispose` 메서드가 호출됩니다. 118 | 119 | 120 | 121 | ### RxSwift의 `DisposeBag` 122 | 123 | > * DisposeBag은 메모리 관리와 ARC 관리를 위해 RxSwift에서 제공하는 툴로, 부모 객체의 상위 객체를 할당 해제하면 DeleteBag에서 Observer 객체가 폐기됩니다. 124 | > * DisposeBag을 가지고 있는 객체의 deinit이 호출될 때, 각 disposable Observer는 관찰하고 있던 것에서 자동으로 구독해지됩니다. 125 | > * DisposeBag을 사용하지 않으면 Observer는 retain cycle을 만들 수 있습니다. (무기한으로 옵저빙에 매달리거나, 할당을 취소하여 크러쉬를 유발할 수 있습니다.) 126 | 127 | 128 | 129 | [DisposeBag](https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Disposables/DisposeBag.swift) 의 내부구현 중 일부를 살펴보면 130 | 131 | ```swift 132 | public final class DisposeBag: DisposeBase { 133 | 134 | private var disposables = [Disposable]() 135 | 136 | private func dispose() { 137 | let oldDisposables = self._dispose() 138 | 139 | for disposable in oldDisposables { 140 | disposable.dispose() 141 | } 142 | } 143 | 144 | private func _dispose() -> [Disposable] { 145 | self.lock.performLocked { 146 | let disposables = self.disposables 147 | 148 | self.disposables.removeAll(keepingCapacity: false) 149 | self.isDisposed = true 150 | 151 | return disposables 152 | } 153 | } 154 | 155 | deinit { 156 | self.dispose() 157 | } 158 | } 159 | ``` 160 | 161 | 이렇게 `DisposeBag`의 `Disposable` 타입의 배열에 담긴 `Disposable` 들을 `dispose` 하는 메서드가 구현되어 있습니다. 그리고 `DisposeBag`이 할당 해제될 때(`deinit` 에서) `dispose()` 메서드를 호출해 `DisposeBag`이 담고있는 `Disposable`들을 `dispose`해 각 disposable Observer는 관찰하고 있던 것에서 자동으로 구독해지시킵니다. 162 | 163 | 아래 예제 코드는 `dispose()` 메서드를 사용한 예제를 `DisposeBag`을 사용하는 방법으로 바꾼 것입니다. 164 | 165 | ```swift 166 | final class MyViewController: UIViewController { 167 | let disposeBag = DisposeBag() 168 | 169 | override func viewDidLoad() { 170 | super.viewDidLoad() 171 | 172 | let parsedObject = theObservable 173 | .map { [parser] json in 174 | return parser.parse(json) 175 | } 176 | parsedObject.subscribe(onNext: { 177 | // handle your subscription 178 | }) 179 | .disposed(by: disposeBag) 180 | } 181 | } 182 | ``` 183 | 184 | 185 | 186 | ### 참고 187 | 188 | ### Java 189 | 190 | 1. [finalize - docs.oracle](https://docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--) 191 | 192 | 2. [Class Cleaner - docs.oracle](https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Cleaner.html) 193 | 194 | 3. [Phantom Reachable - docs.oracle](https://docs.oracle.com/javase/8/docs/api/java/lang/ref/PhantomReference.html) 195 | 196 | ### Swift 197 | 198 | 1. [Deinitialization - Swift.org](https://docs.swift.org/swift-book/LanguageGuide/Deinitialization.html) 199 | 200 | 2. [Memory management in RxSwift – DisposeBag](http://adamborek.com/memory-managment-rxswift/) 201 | 202 | 3. [RxSwift](https://github.com/ReactiveX/RxSwift) 203 | 204 | 4. [RxSwift - DisposeBag](https://github.com/ReactiveX/RxSwift/blob/main/RxSwift/Disposables/DisposeBag.swift) 205 | 206 | 5. [[Question-Archive](https://github.com/TTOzzi/Question-Archive)](https://github.com/TTOzzi/Question-Archive/blob/master/contents/week-3.md#q) 207 | 208 | 6. [Getting Started With RxSwift and RxCocoa](https://www.raywenderlich.com/1228891-getting-started-with-rxswift-and-rxcocoa) 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/item9.md: -------------------------------------------------------------------------------- 1 | # try-finally보다는 try-with-resources를 사용해라 2 | 3 | ## 목차 4 | - try-finally 5 | - try-with-resources 6 | - 명시적으로 자원을 회수해야하는 상황 7 | - NotificationCenter 8 | - FileHandle 9 | - DBConnection (SQLite) 10 | - 마무리 11 | 12 |
13 | 14 | ### try-finally 15 | ```java 16 | // java의 try-finally 문 17 | try { 18 | //run code 19 | } catch () { 20 | // exception handling 21 | } finally { 22 | // After try or catch is terminated 23 | } 24 | ``` 25 | 26 | try-catch-finally 구문은 Swift의 ```do-catch``` 구문과 마찬가지로 try문에서 발생한 오류를 catch문에서 잡기 위해 사용합니다. 27 | 28 | ```swift 29 | // swift의 do-try-catch 문 30 | let fileManager = FileManager() 31 | 32 | do { 33 | let contentsDIR = try fileManager.contentsOfDirectory(atPath: "") 34 | } catch { 35 | print(error) 36 | // exception handling 37 | } 38 | ``` 39 | 40 | 책에도 나와 있듯이 finally 구문은 작업이 끝나거나 작업 도중 에러가 발생했을 때 필요한 동작이나 자원을 회수(닫는)하는 용도로 쓰입니다. 41 | 스위프트에서는 finally 같은 구문이 없지만 명시적으로 자원을 닫아주는 작업이 필요할 때가 있는데, 이는 하단에서 다루도록 하겠습니다. 42 | 43 |
44 | 45 | ### try-with-resources 46 | 47 | ```java 48 | try(run code) { 49 | 50 | } catch { 51 | 52 | } 53 | ``` 54 | 위의 try-finally와 비교해서 코드가 더 간결해지고, 오류 로그에서도 try 구문 내에서 발생된 예외가 기록되어 추적하기 쉬워진다는 장점들을 말하고 있습니다. 55 | 56 | try문에서 인스턴스를 생성할때, 해당 자원이 `AutoCloseable`을 구현하였으면 자동으로 자원을 회수해줍니다. Swift에서는 해당 인터페이스와 매핑되는것이 없어 넘어가도록 하겠습니다. 57 | 58 |
59 | 60 | ### 명시적으로 자원을 회수해야하는 상황 61 | Swift도 명시적으로 자원을 회수해야하는 상황이 종종 있습니다. NotificationCenter 제거, FileHandler의 fileDescriptor 할당해제, dbConnection close(Sqlite)를 예로 들 수 있습니다. 62 | 63 | 보통 `viewDidLoad()`에서 할당을 해주고, 클래스 인스턴스가 해제되기 직전에 불리는 `deinit()`에서 할당을 해제해주는 코드를 작성합니다. 64 | 65 | #### NotificationCenter 66 | 공식문서에서는 iOS 9.0 이나 macOS 10.11 이후 버전에서 앱을 제공한다면 자동으로 제거해주므로 따로 제거해주지 않아도 된다고 나와있지만, 명시적으로도 제거 할 수 있습니다. 67 | 68 | ```Swift 69 | class ViewController: UIViewController { 70 | // 할당 71 | viewDidLoad() { 72 | NotificationCenter.default.addObserver(self, 73 | selector: #selector(testFunc), 74 | name: NSNotification.Name(rawValue: "testButton"), 75 | object: nil) 76 | } 77 | 78 | // 해제 79 | deinit() { 80 | NotificationCenter.default.removeObserver(self, 81 | name: NSNotification.Name(rawValue: "testButton"), 82 | object: nil) 83 | } 84 | } 85 | 86 | ``` 87 |
88 | 89 | #### FileHandler 90 | File handle 객체를 사용해서 파일, 소켓, 파이프 및 디바이스와 관련된 데이터에 접근 할 수 있습니다.
91 | 파일의 경우에는 읽기, 쓰기, 검색이 가능합니다. 92 |
93 | 94 | 이니셜라이저를 사용해 소켓에 쓸수있거나 읽을 수 있는 file handle을 반환하게되는데 해당 객체가 file descriptor를 소유하게 되어 닫아줘야한다고 쓰여져 있습니다.
95 | 96 | 하지만 FileHandle 객체를 사용하나 명시적으로 닫아주지 않아도 되는 경우도 있습니다.
97 | 밑의 예시 코드에서 2번째 케이스로 사용된 `init(fileDescriptor fd: Int32, 98 | closeOnDealloc closeopt: Bool)` 이니셜라이저에서 두번째 인자를 true로 전달해주게되면 자동으로 file descriptor를 닫아준다고 합니다. 99 | 100 | 101 | ```Swift 102 | // case 1 103 | let file = FileHandle(forReadingAtPath: filepath) { 104 | 105 | if file == nil { 106 | print("File open failed") 107 | } else { 108 | file?.closeFile() 109 | } 110 | 111 | // case 2 112 | let file = FileHandle(fileDescriptor: 'fileDescriptor', closeOnDealloc: true) 113 | ``` 114 | 115 |
116 | 117 | #### DBConnection 118 | SQLite를 예로 들어보겠습니다. SQLite를 사용하기위해서 connection을 생성하고 이후 사용하지 않을때는 connection을 닫아줘야합니다.
119 | sqlite3_open()를 사용하게 되면 SQLite db handle은 두번째 인자에 sqlite3 객체의 인스턴스에 대한 포인터를 반환합니다.
120 | 문서에서는 sqlite3_close()를 사용해 더 이상 연결이 필요하지 않을때 해제하라고 작성되어있습니다. 121 | 122 | ```Swift 123 | class ViewController: UIViewController { 124 | let dbConnection: OpaquePointer? 125 | 126 | viewDidLoad() { 127 | self.dbConnection = openDatabase() 128 | } 129 | 130 | // connection 해제 131 | deinit() { 132 | sqlite3_close(dbPointer) 133 | } 134 | 135 | // SQLite 연결 136 | func openDatabase() -> OpaquePointer? { 137 | var db: OpaquePointer? 138 | guard let part1DbPath = part1DbPath else { 139 | print("part1DbPath is nil.") 140 | return nil 141 | } 142 | if sqlite3_open(part1DbPath, &db) == SQLITE_OK { 143 | print("Successfully opened connection to database at \(part1DbPath)") 144 | return db 145 | } else { 146 | print("Unable to open database.") 147 | return nil 148 | } 149 | } 150 | } 151 | 152 | ``` 153 | 154 |
155 | 156 | ### 마무리 157 | 158 | 이번 아이템은 Swift와 매핑되는 부분이 많이 없었지만 Swift에서 명시적으로 자원을 회수해 줘야 하는 상황이 있을 때, 자원관리에 대해서 한 번 더 생각해 볼 수 있는 아이템이었습니다. 159 | 160 |
161 | 162 | ### 참고 문서 163 | - [FileHandler 애플공식문서](https://developer.apple.com/documentation/foundation/filehandle) 164 | - [SQLite 공식문서](https://sqlite.org/c3ref/open.html) 165 | - [FileHandle 예제](https://www.techotopia.com/index.php/Working_with_Files_in_Swift_on_iOS_8) 166 | -------------------------------------------------------------------------------- /2장_객체_생성과_파괴/resources/item3-singleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSwiftists/effective-swift/03f4cd8721979a28c7f221663fa33511bd73a902/2장_객체_생성과_파괴/resources/item3-singleton.png -------------------------------------------------------------------------------- /3장_모든_객체의_공통_메서드/item10.md: -------------------------------------------------------------------------------- 1 | # Item 10. equals는 일반 규약을 지켜 재정의하라 2 | 3 | Item 10에서는 equals를 재정의하기에 적합한 상황을 설명하고, 재정의할 때 지켜야 할 규약들을 설명합니다. 4 | 5 | ### equals를 재정의하기 적합한 경우 6 | 7 | 자바의 Object는 equals의 기본 구현을 제공하는데, 이 메서드에서는 두 레퍼런스 변수가 같은 인스턴스를 가리키고 있는지를 비교합니다. 8 | 9 | ```java 10 | public boolean equals(Object obj) { 11 | return (this == obj); 12 | } 13 | ``` 14 | 15 | 만약 두 객체가 물리적으로 같은지가 아니라 논리적으로 같은지를 비교하려 한다면, 위 메서드를 재정의하여 사용할 수 있습니다. 16 | 17 | ### 스위프트에서는… 18 | 19 | 스위프트에서는 커스텀 타입에 Equatable 프로토콜을 채택하는 방식으로 객체 간의 논리적 동치 확인을 구현할 수 있습니다. 또한 Equatable을 채택하여 구현한 객체들은 `==` 연산자를 이용해 같은지를 비교할 수 있으며, Equatable을 채택한 객체들로 이루어진 컬렉션에서는 `firstIndex(of:)`, `contains()` 등의 메서드들을 사용할 수 있습니다. 20 | 21 | ### equals를 재정의할 때 지켜야 할 규약들 22 | 23 | 컬렉션 클래스들을 포함한 수많은 클래스들에서는 equals 메서드를 사용하고 있으며, equals가 특정 규약들을 지킨다고 가정하고 구현되어 있으므로 equals를 재정의할 때는 이 규약들을 반드시 따라야 합니다. 24 | 25 | - Reflexivity(반사성) 26 | - Symmetry(대칭성) 27 | - Transitivity(추이성) 28 | - Consistency(일관성) 29 | - Non-nullity 30 | 31 | ### 스위프트에서는… 32 | 33 | 스위프트 또한 Equatable에 의존하는 여러 타입들과 메서드들이 있어서, Equatable을 따르는 커스텀 타입들 또한 몇몇 규약을 만족해야 합니다. — 참조: [Equatable](https://developer.apple.com/documentation/swift/equatable/1539854) 34 | 35 | 다만, 자바에서는 Object의 equals 메서드를 재정의하는 방식으로 논리적 동치 확인을 구현하지만, 스위프트에서는 프로토콜을 채택하는 방식으로 구현하도록 만들어 위에서 언급한 규약들을 지키는 데에 도움을 주는 것 같습니다. 36 | 37 | 예를 들어 책에 쓰여 있는 올바른 equals 메서드 구현 단계 중, `instanceof` 연산자를 이용해 입력되는 인스턴스의 타입이 올바른지 확인하고 해당 타입으로 형변환하는 절차가 있습니다. 자바에서는 equals 메서드의 파라미터 타입이 항상 Object이기 때문에 거쳐야 하는 절차들입니다. 38 | 39 | ```swift 40 | public protocol Equatable { 41 | static func == (lhs: Self, rhs: Self) -> Bool 42 | } 43 | ``` 44 | 45 | 반면 Equatable 프로토콜에서는 left hand side와 right hand side 파라미터들의 타입이 둘 다 Self 키워드로 명시되어 있어서, 구현체에서 `==` 연산자를 구현할 때 타입이 다른 경우를 고려하지 않아도 되며, 번거로운 형변환 작업도 필요없습니다. 46 | 47 | 또한, (의도된 것인지는 모르겠지만) class가 아닌 static으로 선언되어 있어 하위 클래스에서 오버라이드할 수 없도록 강제해 놓았습니다. 책에서 “구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다”라고 쓰여 있는데, 어차피 규약을 만족시킬 방법이 없으므로 오버라이딩을 막아둔 것이 아닐까 생각됩니다. 48 | 49 | 자바보다 자율성은 낮지만, 프로그램이 의도치 않게 런타임에 이상 동작할 가능성을 줄이기 위해 최대한 컴파일 시점에 문제를 발견하도록 설계한 스위프트의 언어적 특성이 보입니다. 50 | 51 | ### 상속 관계에서의 Equivalence 확인 방법 제안 52 | 53 | [코드스쿼드 자판기 앱 프로젝트](https://github.com/seizze/swift-vendingmachineapp)에서 음료수 객체들 간의 상속 관계를 구현하였는데, 이때 인스턴스들 간의 동치를 확인할 때 hashValue를 이용하였습니다. 54 | 55 | 먼저, 가장 상위 클래스에서는 Hashable을 채택하여 `==` 연산자와 `hash(into:)` 메서드를 구현합니다. 56 | 57 | ```swift 58 | class Beverage: Hashable { 59 | 60 | let price: Int 61 | let name: String 62 | 63 | init(price: Int, name: String) { 64 | self.price = price 65 | self.name = name 66 | } 67 | 68 | func hash(into hasher: inout Hasher) { 69 | hasher.combine(price) 70 | hasher.combine(name) 71 | } 72 | 73 | static func == (lhs: Beverage, rhs: Beverage) -> Bool { 74 | return lhs.hashValue == rhs.hashValue 75 | } 76 | } 77 | ``` 78 | 79 | 이 때 `==` 연산자에서는 hashValue가 같은지를 비교합니다. `hash(into:)` 메서드에서는 동치를 확인하기 위한 클래스 안의 핵심 프로퍼티들을 사용합니다. 80 | 81 | ```swift 82 | class Coffee: Beverage { 83 | 84 | private let caffeineContent: Int 85 | private let temperature: Int 86 | 87 | init( 88 | price: Int, 89 | name: String, 90 | caffeineContent: Int, 91 | temperature: Int 92 | ) { 93 | self.caffeineContent = caffeineContent 94 | self.temperature = temperature 95 | super.init(price: price, name: name) 96 | } 97 | 98 | override func hash(into hasher: inout Hasher) { 99 | super.hash(into: &hasher) 100 | hasher.combine(caffeineContent) 101 | hasher.combine(temperature) 102 | } 103 | } 104 | ``` 105 | 106 | 하위 클래스에서는 `==` 연산자가 아닌 `hash(into:)` 메서드를 오버라이드하며, 상위 `hash(into:)` 메서드를 호출 후 하위 클래스의 핵심 필드를 hash 계산에 포함시킵니다. 107 | 108 | Beverage와 Coffee의 인스턴스들을 비교한 결과는 다음과 같습니다. 109 | 110 | ```swift 111 | let beverage = Beverage(price: 1000, name: "A") 112 | let coffee = Coffee(price: 1000, name: "A", caffeineContent: 32, temperature: 99) 113 | 114 | print(beverage == coffee) // false 115 | ``` 116 | 117 | Coffee 클래스에서 `hash(into:)`를 오버라이드하지 않았을 경우, `print(beverage == coffee)`는 true를 출력합니다. 118 | 119 | ### 주의할 점 120 | 121 | 위 방법은 인스턴스의 개수가 아주 많아졌을 경우, 해시 충돌 가능성이 있으므로 주의해야 합니다. 122 | 123 | ### References 124 | 125 | - [Equatable](https://developer.apple.com/documentation/swift/equatable/1539854) 126 | - [코드스쿼드 자판기 앱 프로젝트](https://github.com/seizze/swift-vendingmachineapp) 127 | - [자판기 앱 PR #204](https://github.com/code-squad/swift-vendingmachineapp/pull/204) 128 | -------------------------------------------------------------------------------- /3장_모든_객체의_공통_메서드/item12.md: -------------------------------------------------------------------------------- 1 | # toString을 항상 재정의하라 2 | 3 | ## 목차 4 | - toString 5 | - 왜 재정의 해야하나? 6 | - Swift에서의 toString, CustomStringConvertible 7 | - CustomStringConvertible 8 | - CustomDebugStringConvertible 9 | - String(describing:), String(reflecting:) 10 | - 디버깅을 위한 로그 남기기 11 | 12 |
13 | 14 | ## 내용 15 | ### toString 16 | Java에서의 toString은 Java의 Object 클래스에 정의되어 있는 메소드를 오버라이드해서 사용할수 있도록 되어있습니다.
17 | 18 | System.out.print문 내에서 숫자 타입의 변수나 값은 자동으로 String으로 바뀌는데, 이때 컴파일러는 해당 클래스의 toString() 메소드를 이용합니다.
19 | 20 | toString을 오버라이딩하지 않으면 Object 클래스에 정의되어 있는 toString을 사용하는데, 기본적으로는 **클래스_이름@16진수로_표시한_해시코드**로 표현되어 나타내게 됩니다.
21 | 22 | ```java 23 | getClass().getName() + '@' + Integer.toHexString(hashCode()) 24 | ``` 25 | 26 |
27 | 28 | ### 왜 재정의 해야하나? 29 | >toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다. 30 | 31 |
32 | 33 | 디버깅시 로그 추적을 용이하기 위해 재정의하여 객체가 가진 주요 정보를 모두 반환함에 있습니다. 위의 목차에서 봤듯이 클래스 이름, 해시코드만으로는 디버깅에 필요한 정보를 얻기 힘들기 때문입니다. 34 | 35 |
36 | 37 | ### Swift에서의 toString, CustomStringConvertible 38 | 39 | #### CustomStringConvertible 40 | Java에서는 toString이 존재하듯이 Swift에는 비슷한 개념으로 CustomStringConvertible이 존재합니다.
41 | 42 | CustomStringconvertible은 프로토콜입니다. 해당 프로토콜을 채택하여 description을 재정의 할 경우 print문을 사용해 출력할때 재정의한 description의 형식이 반환됩니다.
43 | 44 | UIViewController에서는 CustomStringConvertible을 채택하지 못하는데 이미 UIViewcontroller가 NSObjectProtocol을 채택하고 있어 따로 채택할 필요없이 바로 description을 재정의하여 사용하면 됩니다.
45 | 46 | ```swift 47 | class ViewController: UIViewController { 48 | 49 | override func viewDidLoad() { 50 | // code 51 | } 52 | 53 | override var description: String { 54 | // code 55 | } 56 | 57 | } 58 | 59 | ``` 60 |
61 | 62 | - descrption을 재정의 하지 않은 상태로 print문을 실행했을때 63 | `````` 64 | 65 |
66 | 67 | - description을 재정의 한 후 print문을 실행했을때 68 | ```swift 69 | class ViewController: UIVewController { 70 | override func viewDidLoad() { 71 | super.viewDidLoad() 72 | print(self) 73 | } 74 | 75 | override var description: String { 76 | return "여기는 ViewController 입니다." 77 | } 78 | } 79 | 80 | // 여기는 ViewController 입니다. 출력 81 | ``` 82 | 83 | #### CustomDebugStringConvertible 84 | 85 | 위에서 보았던 ```CustomStringConvertible```이외에 ```CustomDebugStringConvertible```이란 프로토콜도 존재합니다.
86 | 87 | ```CustomStringConvertible```과 차이점은 재정의 해야하는 프로퍼티가```debugDescription``` 로 바뀐다는 것입니다.
88 | 89 | 조금 더 깊숙히 들어가보면, 구현부에서 차이가 나는 것을 알 수 있습니다.
90 | 91 | #### String(describing:), String(reflecting:) 92 | describing, reflecting 모두 어떤 타입이든 인자로 받아 String으로 변환해주는 String 이니셜라이저입니다.
93 | 94 | ```CustomStringConvertible```, ```CustomDebugStringConvertible``` 프로토콜을 95 | 1. **모두 채택하지 않고** describing, reflecting을 부를 경우 96 | : Swift stand libaray가 자동으로 지원해줍니다. 97 | 2. **둘 중 하나만 채택하고** describing, reflecting을 부를 경우 98 | : 재정의 된 프로퍼티(description 혹은 debugDescription)로 반환 됩니다. 99 | 3. **모두 채택하고** describing, reflecting을 부를 경우 100 | : 각각 재정의된 프로퍼티로 반환됩니다. 101 | 102 | ```swift 103 | // Point 104 | struct Point: CustomStringConvertible, CustomDebugStringConvertible { 105 | let x: Int, y: Int 106 | 107 | var debugDescription: String { 108 | return "(debug : \(x), \(y))" 109 | } 110 | 111 | var description: String { 112 | return "(description \(x), \(y))" 113 | } 114 | } 115 | 116 | // Print 117 | let p = Point(x: 1, y: 2) 118 | 119 | let describe = String(describing: p) 120 | let reflect = String(reflecting: p) 121 | 122 | print(describe) // (description 1, 2) 123 | print(reflect) // (debug : 1, 2) 124 | 125 | ``` 126 |
127 | 128 | ### 디버깅을 위한 로그 남기기 129 | 130 | #### print, debugPrint 131 | 132 | 하지만 저렇게 일일이 String 객체를 만들기에는 번거로워, 보통의 경우 debugPrint 사용을 권장하고 있습니다.
133 | 134 | > debugPrint(_:separator:terminator:) 135 | Writes the textual representations of the given items most suitable for debugging into the standard output. 136 | 137 | ```swift 138 | debugPrint(1...5) 139 | // Prints "ClosedRange(1...5)" 140 | ``` 141 | 142 | print와 debugPrint또한 description, debugDescription을 기반으로 출력하는 것 같다고 보입니다.
레퍼런스 체크를 진행하지 못했지만 위의 코드에 이어서 print, debugPrint를 사용해보면 동일한 결과가 나오는 것을 확인할 수 있었습니다. 143 | 144 | ```swift 145 | print(p) // (description 1, 2) 146 | debugPrint(p) // (debug : 1, 2) 147 | ``` 148 | 149 | 150 | 151 | #### file, function, line 152 | Swift에서는 단순히 객체정보나 값 뿐만 아니라 현재 파일명, 함수명, 라인번호까지 출력할 수 있습니다.
153 | 154 | 155 | ```swift 156 | // Example 157 | struct Logger { 158 | public static func debug(_ msg: Any, file: String = #file, function: String = #function, line: Int = #line) { 159 | print("[\(dateFormatter.string(from: Date()))] [\(fileName)] [\(funcNmae)(\(line))] : \(msg)") 160 | } 161 | } 162 | ``` 163 | 164 | 이렇게 로그를 남기고 싶은곳에 msg만 인자로 주게되면 콘솔에 ```[시간] [파일명] [함수명(라인)] : msg```로 남게 되어 필요한 정보를 보기 편하게 사용할 수도 있습니다. 165 | 166 |
167 | 168 | ### 마무리 169 | 170 | 현재 제가 있는 곳에서는 다른 방식으로 로그를 남기지만 같이 일하는 동료와 협의한 포맷을 만들어 descrption을 재정의하고, 해당 프로토콜을 객체마다 전부 채택해줘야 하는 다소 높은 초기비용을 넘어서기만 한다면 좀 더 다양한 정보를 디버깅할때 볼 수 있을것 같습니다 :) 171 | 172 | 173 | ### 참고한 레퍼런스 174 | - [공식문서: Expression](https://docs.swift.org/swift-book/ReferenceManual/Expressions.html) 175 | - [공식문서: debugPrint](https://developer.apple.com/documentation/swift/1539920-debugprint) 176 | - [공식문서: CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible) 177 | - customStringConvertible : https://hiddenviewer.tistory.com/249 178 | -------------------------------------------------------------------------------- /3장_모든_객체의_공통_메서드/item13.md: -------------------------------------------------------------------------------- 1 | # Item 13. clone 재정의는 주의해서 진행하라 2 | 3 | Item 13에서는 clone 메서드를 재정의할 때 주의할 점들을 설명합니다. 4 | 5 | ### Object.clone() 6 | 7 | 자바 Object 클래스에서 기본으로 구현되어 있는 clone() 메소드는 Cloneable 인터페이스를 구현했는지 확인하고, 구현하지 않았다면 예외를 던집니다. 구현되어 있으면, 원본과 같은 객체를 새로 생성 후 모든 필드들에 원본 필드를 각각 대입시켜 복사하는 방식으로 구현되어 있습니다. 8 | 9 | ### clone 메서드의 일반 규약 10 | 11 | clone 메서드의 일반 규약을 요약하면, clone 메서드는 원본 객체와 같은 타입의 복사본을 생성해 반환하며, 관례상 반환된 객체와 원본 객체는 독립적이라는 것입니다. 하지만 이 규약들이 필수로 지켜져야 하는 것은 아닙니다. 12 | 13 | ### 가변 상태를 참조하지 않는 클래스의 clone 14 | 15 | 모든 필드가 primitive 타입이거나, immutable한 객체를 참조하는 변수들이라면 상위 클래스의 clone()을 호출하기만 하면 됩니다. 16 | 17 | ### 가변 상태를 참조하는 클래스의 clone 18 | 19 | 가변 객체를 참조하는 클래스의 경우, 복사본에서의 변경으로 인해 원본 객체까지 변경되면 안되므로 해당 객체가 참조하고 있는 (가변)객체들까지 복사본을 생성하여야 합니다. 20 | 21 | ### 결론 22 | 23 | Cloneable을 구현해야 하는 경우가 아니라면 복사 생성자와 복사 팩터리를 제공하는 편이 더 낫습니다. 24 | 25 | ```java 26 | public Yum(Yum yum) { ... }; // 복사 생성자 27 | public static Yum newInstance(Yum yum) { ... }; // 복사 팩터리 28 | ``` 29 | 30 | ### 스위프트에서는... 31 | 32 | struct의 경우 value type 이므로 객체를 다른 변수에 대입하면 복사본이 만들어집니다. class의 경우, 객체를 복사하는 기능을 지원하려면 생성자 혹은 정적 팩터리 메서드를 구현하는 편이 좋습니다. 33 | -------------------------------------------------------------------------------- /3장_모든_객체의_공통_메서드/item14.md: -------------------------------------------------------------------------------- 1 | # Item 14. Comparable을 구현할지 고려하라 2 | 3 | 이번에는 Swift에서 Java의 `Comparable` 인터페이스의 유일무이한 메서드인 `compareTo`의 역할에 대응하는 프로토콜에 대해 알아봅니다. 4 | 5 | `compareTo`는 단순 동치성 비교와 순서 비교가 가능합니다. Java에서 `Comparable`을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻합니다. 그래서 `Comparable`을 구현한 객체는 `Arrays.sort(a);` 처럼 손쉽게 정렬할 수 있습니다. 6 | 7 | 이에 대응하는 Swift의 프로토콜로는 `Equatable`, `Comparable`, `Sequence` , `IteratorProtocol` 등이 있습니다. 각각의 용도와 쓰임새에 대해 알아봅니다. 8 | 9 |
10 | 11 | ### Equatable 12 | 13 | 값이 동일한 지를 비교할 수 있는 타입으로 Equatable 프로토콜을 준수하는 타입은 등호 연산자(==) 또는 같지 않음 연산자(!=)를 사용해 동등성을 비교할 수 있습니다. Swift 표준 라이브러리 대부분의 기본 데이터타입은 Equatable을 따릅니다. 14 | 15 | ``` swift 16 | let one = 1 17 | let two = 2 18 | 19 | if one == two { 20 | } else { } 21 | ``` 22 | 23 | 그래서 위와같이 기본타입인 Int의 경우 등호연산자(==)로 비교가 가능하고 Float, Double, String, Bool과 같은 타입도 Equatable을 채택하고 있기 때문에 비교가 가능합니다. 24 | 25 | 그럼 Equatable 프로토콜을 언제 사용해야 할까요? 26 | 27 | Equatable 프로토콜은 Hashable, Comparable 프로토콜의 기반이 되므로 해당 프로토콜을 구현하기 위해서는 Equatable 프로토콜을 구현해야 합니다. 그리고 커스텀 타입을 만든 경우 비교를 원한다면 Equatable 프로토콜을 채택하고 구현해주어야 합니다. 28 | 29 | - 구조체 커스텀 타입이 Equatable을 준수하게 하려면 내부 프로퍼티도 Equatable 준수해야 합니다. 30 | - 열거형 커스텀 타입이 Eqautable을 준수하게 하려면 그 안의 연관 값도 Equatable 준수해야 합니다. 31 | 32 | ```swift 33 | class StreetAddress { 34 | let number: String 35 | let street: String 36 | let unit: String? 37 | 38 | init(_ number: String, _ street: String, unit: String? = nil) { 39 | self.number = number 40 | self.street = street 41 | self.unit = unit 42 | } 43 | } 44 | 45 | extension StreetAddress: Equatable { 46 | static func == (lhs: StreetAddress, rhs: StreetAddress) -> Bool { 47 | return 48 | lhs.number == rhs.number && 49 | lhs.street == rhs.street && 50 | lhs.unit == rhs.unit 51 | } 52 | } 53 | ``` 54 | 55 | `public static func ==(lhs: Self, rhs: Self) -> Bool` 는 필수로 구현되어야 하는 함수입니다. 위의 예제처럼 해당 메서드 내에서 개별 요소에 관한 항목을 비교하도록 구현해주면 됩니다. 56 | 57 |
58 | 59 | ### Comparable 60 | 61 | 연산자 <, <=, >=, > 와 연관된 비교를 가능하게 하는 타입으로 String이나 숫자처럼 고유한 순서를 가진 타입에 주로 사용됩니다. 연산자나 표준 라이브러리 메서드를 사용하여 인스턴스를 비교하는 경우 Comparable 메소드를 채택하면 됩니다. 62 | 63 | ```swift 64 | struct MyData: Comparable { 65 | var value: Int = 0 66 | 67 | static func < (lhs: MyData, rhs: MyData) -> Bool { 68 | return lhs.value < rhs.value 69 | } 70 | } 71 | 72 | let v1 = MyData(value: 1) 73 | let v2 = MyData(value: 2) 74 | 75 | print(v1 > v2 ? "true" : "false") //false 76 | print(v1 >= v2 ? "true" : "false") //false 77 | print(v1 <= v2 ? "true" : "false") //true 78 | print(v1 == v2 ? "true" : "false") //false 79 | ``` 80 | 81 | 그리고 Comparable이 Equatable을 준수하므로 별도로 채택하여 구현 할 필요는 없습니다. 82 | 83 |
84 | 85 | ### Sequence 86 | 87 | 해당 요소에 순서와 반복적인 접근을 제공하는 타입으로 Sequence는 한 번에 하나씩 단계별로 실행할 수 있는 값의 목록입니다. 시퀀스의 요소들을 반복하는 일반적인 방법은 for-in 루프가 있습니다. 다시 말해, Sequence 프로토콜을 준수하는 타입은 for-in 루프로 순회할 수 있습니다. 88 | 89 | Swift의 기본 라이브러리이고, Array, Dictionary, Set과 같은 Collection 타입의 기반이 되는 프로토콜입니다. Sequence 프로토콜을 구현하면 `forEach`, `map`, `filter`, `flatMap`과 같은 다양한 함수를 사용할 수 있습니다. 90 | 91 | ```swift 92 | struct Countdown: Sequence, IteratorProtocol { 93 | var count: Int 94 | 95 | mutating func next() -> Int? { 96 | if count == 0 { 97 | return nil 98 | } else { 99 | defer { count -= 1 } 100 | return count 101 | } 102 | } 103 | } 104 | 105 | let threeToGo = Countdown(count: 3) 106 | for i in threeToGo { 107 | print(i) 108 | } 109 | // Prints "3" 110 | // Prints "2" 111 | // Prints "1" 112 | ``` 113 | 114 | 커스텀 타입을 생성하는 경우, Sequence 프로토콜을 채택하면 유용한 오퍼레이션들을 손쉽게 가져다 쓸 수 있습니다. Sequence 프로토콜을 채택하기 위해선 makeIterator() 메서드를 추가해야합니다. Sequence 내부에 associatedtype으로 IteratorProtocol타입이 있어 순회하려는 대상은 IteratorProtocol 타입이어야 합니다. 115 | 116 | Sequence 내에 특정 값이 포함되어 있는지 확인할 때와 Sequence의 끝에 도달하거나 특정값을 찾을 때까지 순차적으로 탐색할 수 있습니다. 이렇게 순회가 가능함은 어떤 시퀀스 상에서든 많은 양의 연산을 위해 접근이 가능하다는 것을 의미합니다. 117 | 118 | 또한 Sequence 프로토콜은 `contains(_:)` 메서드를 지원하는데, 이 메서드를 사용하면 수동으로 값을 순회 할 필요 없이 값의 포함 유무를 판단할 수 있습니다. 119 | 120 |
121 | 122 | ### IteratorProtocol 123 | 124 | 시퀀스 값을 한 번에 하나씩 제공하는 타입으로 Sequence 프로토콜과 함께 사용됩니다. 시퀀스는 반복 프로세스를 트래킹하고, 한번에 한 요소를 반환하는 Iterator를 생성해 개별 요소에 접근할 수 있게 합니다. IteratorProtocol의 목적은 컬렉션을 반복 순회하는 `next()` 메서드를 통해 컬렉션의 반복 상태를 캡슐화 하는 것입니다. 125 | 126 | ```swift 127 | protocol IteratorProtocol { 128 | associatedtype Element 129 | mutating func next() -> Element? 130 | } 131 | ``` 132 | 133 | 위 코드에서 associtatedtype으로 선언된 Element는 Iterator가 생성하는 값의 유형을 지정합니다. 그리고 `next()`는 해당 시퀀스에서 다음번 요소를 반환하거나, 다음번 요소가 없는 경우 nil을 반환합니다. 134 | 135 | 136 | 137 |
138 | 139 | ### 결론 140 | 141 | 값이 동일한지 비교하고싶다 -> Equatable 142 | 143 | 값의 크고 작음을 비교하고싶다 -> Comparable 144 | 145 | 순서를 가지게 하고싶다 -> Sequence 146 | 147 | 순서를 가진 타입을 순회하고싶다 -> IteratorProtocol 148 | 149 | 150 | 151 | #### References 152 | 153 | - [Equatable - Apple Developer Document](https://developer.apple.com/documentation/swift/equatable) 154 | - [Sequence - Apple Developer Document](https://developer.apple.com/documentation/swift/sequence) 155 | - [IteratorProtocol - Apple Developer Document](https://developer.apple.com/documentation/swift/iteratorprotocol) 156 | - [Comparable - Apple Developer Document](https://developer.apple.com/documentation/swift/comparable) 157 | - https://kor45cw.tistory.com/260 158 | 159 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item15.md: -------------------------------------------------------------------------------- 1 | # 클래스와 멤버의 접근 권한을 최소화하라. 2 | 3 | ## 목차 4 | - 읽기 전에! 5 | - 캡슐화(은닉화) 6 | - 장점 7 | - 모든 클래스와 멤버의 접근성을 가능한 좁혀라 8 | - public 클래스의 인스턴스 필드는 되도록 public이 아니여야 한다 9 | - public static final 10 | 11 |
12 | 13 | ### 읽기 전에! 14 | 이번 아이템의 내용 중 package-orivate에 대한 내용들이 있어 자바의 패키지와 대치되는 스위프트의 프레임워크와 open, public을 묶어 설명하려 하였습니다. 15 | 16 | 하지만 비슷하게 대치되는 개념이 아니라고 생각하고, 커스텀 프레임워크 또한 일반적으로 자주 쓰이지 않는다고 생각하여 패키지에 대한 내용을 클래스로 바꾸어 fileprivate과 연관되어 작성하도록 하겠습니다. 17 | 18 | 또한 protected는 대치되는 개념이 없다고 생각해 생략하도록 하겠습니다. 19 | 20 |
21 | 22 | ### 캡슐화 (은닉화) 23 | > 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리이다. 24 | 25 | 캡슐화는 객체의 속성(프로퍼티)과 행위(메서드)를 하나로 묶고 실제 구현 내용 일부를 외부에 감추어 은닉하는것이라고 정의되고 있습니다. 26 | 27 | 외부에 감추는 방법으로는 접근 제한자를 사용하여 프로퍼티나 메서드에 대한 접근을 제한하도록 설정합니다. 28 | 29 | #### 장점 30 | 책에서는 장점을 다섯가지로 서술하고 있습니다. 31 | 1. 시스템 개발 속도를 높인다. 32 | 2. 시스템 관리 비용을 낮춘다. 33 | 3. 캡슐화 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 34 | 4. 소프트웨어 재사용성을 높인다. 35 | 5. 큰 시스템을 제작하는 난이도를 낮춰준다. 36 | 37 | 공통적으로 시스템을 구성하는 컴포넌트를 독립시켜 서로에게 영향을 주지 않음으로써 위에 서술한 장점이 나타나게 된것으로 생각합니다. 38 | 39 |
40 | 41 | ### 모든 클래스와 멤버의 접근성을 가능한 좁혀라 42 | 자바에서는 정보 은닉을 위한 다양한 장치를 제공하는데 클래스, 인터페이스, 멤버의 접근성은 해당 요소가 선언된 위치와 접근 제한자로 정해집니다. 43 | 44 | 정보 은닉의 핵심은 이 **접근 제한자를 제대로 활용하는 것**이 핵심이라고 이야기하고 있습니다. 45 | 46 | 자바에서 제공하는 접근 제한자는 네 가지가 있습니다. 47 | 접근 범위가 좁은 것부터 순서대로 나열했습니다. 48 | - private 49 | - package-private 50 | : package는 폴더의 개념입니다. 같은 package 내부에서만 접근이 가능하게 됩니다. 51 | - protected : package-private의 접근 범위를 포함하고 추가로 하위 클래스에서도 접근이 가능하게 됩니다. 52 | - public 53 | 54 | 스위프트에서는 다섯가지 접근 제한자를 제공하고 있습니다. 55 | (자바와 마찬가지로 밑으로 명시된 접근 제한자일수록 접근 범위가 넓어집니다.) 56 | - private : 엔티티(entites)의 사용을 해당 엔티티를 둘러싼 선언과 동일 파일내의 extension으로만 제한합니다. 57 | - fileprivate : 엔티티의 사용을 자신이 정의된 소스 파일로만 제한합니다. 58 | - internal : 엔티티가 정의된 모듈의 어떤 소스 파일에서도 사용할 수 있지만, 외부 모듈에서는 사용하지 못하도록 합니다. 59 | - public : 엔티티가 정의된 모듈의 어떤 소스 파일에서도 사용가능하고 외부 모듈에서도 사용이 가능합니다. 60 | - open : `public`과 동일하지만 추가로 모듈 외부에서 하위 클래스를 만들고 '재정의'가 가능합니다.
61 | `open`을 명시했다는 의미는 클래스를 상위 클래스로 사용하는 다른 모듈의 코드에 대한 영향을 이미 고려했으며, 그에 따라 클래스 코드를 설계했음을 포함하게 됩니다. 62 | 63 | 64 | 공개 API 이외의 프로퍼티나 메소드들은 private으로 만드는 것을 권장하고 있습니다. 65 | 그런 다음 같은 소스파일내에 다른 클래스(혹은 구조체 등)가 접근해야 하는 멤버에 한해서 fileprivate으로 접근 범위를 확장시켜줍니다. 66 | 67 | ```swift 68 | // 같은 소스파일 내부 69 | final class ImageViewController: UIViewController { 70 | 71 | fileprivate var imageView: UIImageView! 72 | 73 | } 74 | 75 | struct ImageProvider { 76 | 77 | let newImage: UIImage 78 | 79 | func updateImage(in viewController: ImageViewController) { 80 | // 외부에서는 접근하지 못함. 81 | viewController.imageView.image = newImage 82 | } 83 | } 84 | 85 | ``` 86 | 87 |
88 | 89 | ### public 클래스의 인스턴스 필드는 되도록 public이 아니여야 한다 90 | > public 가변 필드를 갖는 클래스는 일반적으로 thread safety 하지 않다. 91 | 제 생각으로는 `public`이어서가 아니라 '가변' 필드기 때문에 thread unsafety한게 크다고 생각합니다. 92 | 93 | 물론 `private`으로 선언해주면 직접 접근이 불가능하여 접근에 있어서 까다로워 지는 효과는 있겠지만 완전하게 thread safety하게는 만들어주지 못합니다. 94 | Swift에서 GCD를 이용해 처리를 해주거나 데이터 사본을 사용해서 작업을 해주는게 thread 접근 측면에 있어서 좋다고 생각합니다. 95 | 96 | 현재 item에서는 접근제한자에 대한 주제를 다루고 있기 때문에 좀더 관심이 있으신 분은 [해당 글](https://uynguyen.github.io/2018/06/05/Working-In-Thread-Safe-on-iOS/)을 읽어보시는것도 좋겠네요. 97 | 98 | 여기에서는 가변 객체를 참조하는 필드, final이 아닌 필드를 public으로 선언하지 말라고 권장합니다. 99 | 100 | 다만 예외를 하나 두는데 해당 클래스에 꼭 필요한 구성요소로써의 상수라면 `public static final` 필드로 공개해도 좋다고 합니다. 101 | 또한, 기본 타입 값이나 불변 객체를 참조해야 합니다. 102 | 103 | 스위프트에서는 `static let`으로 상수를 선언해 주면 될 것 같습니다. 자바와 마찬가지로 가변 객체를 참조해서 불이익을 받지 않도록 해야겠습니다. 104 | 105 | 참조된 객체 자체는 수정이 가능하니까요. 106 | 107 |
108 | 109 | ### public static final 110 | public static final 배열 필드를 두거나 해당 필드에 접근할 수 있는 메소드를 둔다면 수정이 가능하기 때문에 두 가지 해결책을 제시하고 있습니다. 111 | 112 | 1. public을 private으로 선언하고 접근 가능한 필드에는 불변리스트를 추가하는 것 113 | 2. public을 private으로 선언하고 복사본을 반환하는 메서드를 만드는 것 114 | 115 | 스위프트에서는 굳이 1, 2번의 방법처럼 프로퍼티나 메서드를 추가하기 보다는 116 | ```swift 117 | public static let values = [ ... ] 118 | ``` 119 | 120 | 이렇게 접근이 가능하더라도 `let`으로 선언해버려 수정이 불가능 하도록 만들어주면 될 것 같습니다. 121 | 122 |
123 | 124 | ### 참고한 곳 125 | - https://ko.wikipedia.org/wiki/%EC%BA%A1%EC%8A%90%ED%99%94 126 | - https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html#//apple_ref/doc/uid/TP40014097-CH41-ID3 127 | https://zeddios.tistory.com/383 128 | - https://www.avanderlee.com/swift/fileprivate-private-differences-explained/ 129 | - http://xho95.github.io/swift/programming/language/grammar/2017/02/28/The-Swift-Programming-Language.html 130 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item16.md: -------------------------------------------------------------------------------- 1 | # item16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 2 | 3 | 이따금 인스턴스 필드들을 모아놓는 일 외에는 아무 목적도 없는 퇴보한 클래스를 작성하려 할 때가 있습니다. 4 | 5 | ```SWIFT 6 | class Point { 7 | var x: Int = 0 8 | var y: Int = 0 9 | } 10 | ``` 11 | 12 | 이런 클래스는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못합니다(아이템 15). 13 | 14 | `public` 클래스가 프로퍼티를 공개하면 이를 사용하는 클라이언트가 생겨나게 되므로 불변식을 보장할 수 없고, 외부에서 프로퍼티에 접근 할 때 사이드이펙트가 발생할 여지가 있습니다. 또한 객체간의 결합도가 높아지며, 객체의 오용을 야기할 수 있습니다. 15 | 16 | 아래는 흔하게 만들 수 있는 캡슐화된 클래스입니다. 프로퍼티들을 `private` 접근제어자로 감싸고, getter와 setter를 두어 프로퍼티를 읽고 쓰게 만들었습니다. 17 | 18 | ```swift 19 | class Point { 20 | private var x: Int 21 | private var y: Int 22 | 23 | init(x: Int, y: Int) { 24 | self.x = x 25 | self.y = y 26 | } 27 | 28 | public func x() { return x } 29 | public func y() { return y } 30 | 31 | public func setX(x: Int) { self.x = x } 32 | public func setY(y: Int) { self.y = y } 33 | } 34 | ``` 35 | 36 | `public`인 클래스라면 반드시 이정도의 캡슐화는 해야 합니다. 이렇게 하면 메소드를 두어 내부의 속성들을 언제든지 바꿀 수 있는 유연성을 제공할 수 있게 됩니다. 37 | 38 | 만약 `let` 으로 선언된 불변 프로퍼티라면 직접 노출할 때의 단점이 조금은 줄어 들지만, 여전히 결코 좋은 생각은 아닙니다. 프로퍼티를 읽을 때 변경하는 등의 부수 작업을 실행할 수 없고, API를 변경하지 않는 이상 저장된 값을 바꿀 수 없습니다. 39 | 40 | 코드의 변경에 유연하게 대응하기 위해서는 캡슐화 하는 것이 중요합니다. 새로운 기능이 추가되면 객체는 새로운 책임을 갖거나, 원래 설계되지 않은 방식으로의 변경이 일어날 수 있습니다. 이런식으로 새로운 기능을 추가하게되면 추후의 유지보수에 악영향을 미치게 됩니다. 41 | 42 | 여러 면에서 이것은 API 설계로 귀결되는데, API를 명확하게 정의하면 코드를 캡슐화하고 불필요한 세부 구현 사항을 다른 객체와 공유하는 것을 막을 수 있습니다. 43 | 44 |
45 | 46 | ### 캡슐화를 하지 않는다면 47 | 48 | 그럼 다른 객체의 세부 구현사항을 숨겨 캡슐화 하는 것이 왜 이리 중요할까요? 아래 예제를 통해 함께 알아봅시다. 49 | 50 | ```swift 51 | class ProfileViewController: UIViewController, ProfileHeaderViewDelegate { 52 | lazy var headerView = ProfileHeaderView() 53 | 54 | override func viewDidLoad() { 55 | super.viewDidLoad() 56 | headerView.delegate = self 57 | view.addSubview(headerView) 58 | } 59 | } 60 | ``` 61 | 62 | 위의 코드는 매우 간단해 보입니다만 세부 구현 사항을 노출하게 됩니다. `headerView`가 `private`이 아니기 때문에 `ProfileViewController` 외부에서 `headerView`를 조작 할 가능성이 얼마든지 있어 버그를 발생할 위험을 증가시키게 됩니다. 63 | 64 | 65 | 66 | 예를 들어, 사용자가 프리미엄 구독을 한다고 가정하고 외부에서 `headerView`를 `PremiumHeaderView`로 지정한다고 해봅시다. 67 | 68 | ```swift 69 | func userUnlockedPremiumSubsription() { 70 | profileViewController.headerView = PremiumHeaderView() 71 | } 72 | ``` 73 | 74 | 위의 함수를 실행하면 인스턴스를 교체하기 때문에 `profileViewController`와 `headerView` 간의 델리게이트 관계가 손실되는 문제가 발생합니다. 이는 멈춰버리는 등의 버그를 발생시킬 가능성이 높습니다. 75 | 76 |
77 | 78 | ### 속성을 감추자 79 | 80 | 이러한 버그가 발생하지 않게끔 하려면 `headerView` 속성을 접근제어자를 통해 감추면 됩니다. 81 | 82 | ```swift 83 | class ProfileViewController: UIViewController, ProfileHeaderViewDelegate { 84 | private lazy var headerView = ProfileHeaderView() 85 | } 86 | ``` 87 | 88 | 이제는 `profileViewController.headerView` 로 접근 할 수 없게 됩니다. 하지만 설계상 `headerView`를 `PremiumHeaderView`로 지정하는 함수가 반드시 필요한데, 이는 어떻게 할 수 있을까요? 89 | 90 | `headerView` 자체를 노출하는 대신 다음과 같이 `ProfileViewController`가 사용자 모드를 지정할 수 있는 API를 만드는 것입니다. 91 | 92 | ```swift 93 | extension ProfileViewController { 94 | enum Mode { 95 | case standard 96 | case premium 97 | } 98 | 99 | func enterMode(_ mode: Mode) { 100 | switch mode { 101 | case .standard: 102 | headerView.applyStandardAppearance() 103 | case .premium: 104 | headerView.applyPremiumAppearance() 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | > `headerView`에 `Mode`를 노출하지 않는다는 것에 주목할 만 합니다. 대신 headerView 내부에 별도의 모드를 적용하는 메서드를 구현합니다.(UI 수준의 조정) 이렇게 하면 ProfileViewController와 HeaderView 사이에 결합도를 낮출 수 있습니다. 111 | 112 | 113 | 114 | 이제 사용자가 프리미엄 구독을 할 때 실행하는 코드는 다음과 같습니다. 115 | 116 | ```swift 117 | func userDidUnlockPremiumSubscription() { 118 | profileViewController.enterMode(.premium) 119 | } 120 | ``` 121 | 122 | 123 | 124 | 이렇게 캡슐화 함으로써 `ProfileHeaderView`가 잘못된 방식으로 사용되는 위험을 줄일 수 있고, 명시적 API를 추가함으로 앱이 계속 발전함에 따라 수반되는 변경사항을 보다 쉽게 적용할 수 있게 되었습니다. 125 | 126 |
127 | 128 | ### 핵심정리 129 | 130 | public 클래스는 절대 가변 프로퍼티를 직접 노출해서는 안됩니다. 불변 프로퍼티라면 노출해도 덜 위험하지만 완전히 안심할 수는 없습니다. 131 | 132 | 133 | 134 | ### 참고 135 | 136 | https://www.swiftbysundell.com/articles/code-encapsulation-in-swift/ 137 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item17.md: -------------------------------------------------------------------------------- 1 | # Item 17. 변경 가능성을 최소화하라 2 | 3 | 인스턴스의 내부 값을 수정할 수 없는 불변 클래스는 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 안전합니다. 4 | 5 | 클래스를 불변으로 만들려면 다음 규칙들을 따르면 됩니다. 6 | 7 | - 객체의 상태를 변경하는 메서드를 제공하지 않는다. 8 | - 클래스의 서브클래싱을 막아서 하위 클래스에서 객체의 상태를 변경하지 못하도록 막는다. 9 | - 모든 필드를 final로 선언하여 변경되지 않도록 만든다. 10 | - 모든 필드를 private으로 선언하여 필드가 참조하는 가변 객체까지 변경하지 못하도록 한다. 11 | - 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 12 | 13 | ### 불변 객체의 장점 14 | 15 | 불변 객체의 장점은 다음과 같습니다. 16 | 17 | - 생성된 시점의 상태를 파괴될 때까지 그대로 간직하고 있어서 단순하다. 가변 객체는 임의의 복잡한 상태에 놓일 수 있다. 18 | - Thread-Safe하여 안심하고 공유할 수 있다. 이를 활용하여 자주 쓰이는 값들을 여러 클라이언트가 재활용할 수 있어 메모리 사용량과 GC 비용이 줄어든다. 19 | - 불변 객체들끼리 내부 데이터를 공유할 수 있다. 내부 데이터가 가변이라 하더라도, 외부에서 그 값을 변경할 수 없어서 내부 데이터를 불변 객체들끼리 안전하게 공유할 수 있다. 20 | - 객체를 만들 때 다른 불변 객체들을 컴포넌트로 사용하면, 이 컴포넌트 객체들이 바뀌지 않을 것을 알기 때문에 이점이 많다. 예를 들어, 불변 객체는 맵의 Key과 집합의 원소로 쓰이기 좋다. 21 | - 불변 객체는 그 자체로 실패 원자성을 제공한다. 일시적으로 inconsistency한 상태에 빠질 가능성이 없다. 22 | 23 | ### 불변 객체의 단점 24 | 25 | 반면, 불변 객체는 값이 다르면 반드시 독립된 객체로 만들어야 해서 값의 가짓수가 많다면 객체 생성 비용이 많이 들 수 있다는 단점도 있습니다. 26 | 27 | ### Immutable 클래스 예시 28 | 29 | ```java 30 | public final class Complex { 31 | private final double re; 32 | private final double im; 33 | 34 | public Complex(double re, double im) { 35 | this.re = re; 36 | this.im = im; 37 | } 38 | 39 | public Complex plus(Complex c) { 40 | return new Complex(re + c.re, im + c.im); 41 | } 42 | } 43 | ``` 44 | 45 | 연산 메서드에서 자신을 수정하지 않고 새로운 인스턴스를 만들어 반환합니다. 이런 방식은 코드에서 불변이 되는 영역의 비율이 높아지도록 해 줍니다. 이 때 메서드의 이름으로 객체의 값을 변경하지 않는다는 점을 강조해 전치사 등을 사용했습니다. 46 | 47 | ```java 48 | // 생성자를 접근하지 못하게 하고 정적 팩터리(아이템 1)를 제공하여 상속을 제한할 수도 있습니다. 49 | public class Complex { 50 | private final double re; 51 | private final double im; 52 | 53 | private Complex(double re, double im) { 54 | this.re = re; 55 | this.im = im; 56 | } 57 | 58 | public static Complex valueOf(double re, double im) { 59 | return new Complex(re, im); 60 | } 61 | } 62 | ``` 63 | 64 | 이 방법을 사용할 경우, 클라이언트 측에서의 상속이 제한되어 있기 때문에, 클라이언트에서 바라본 이 불변 객체는 사실상 final입니다. 이 방법은 바깥에서 볼 수 없는 여러 구현 클래스들을 원하는 만큼 만들어 활용할 수 있고, 다음 릴리즈에서 API 변경 없이 기능 추가도 가능하여 유연합니다. 65 | 66 | 이런 식으로 단순한 값 객체는 반드시 불변으로 만드는 것이 좋고, 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여서 예측하기 쉽게 만들고 오류 가능성을 낮추는 편이 좋습니다. 67 | 68 | ### Swift에서의 Immutable 69 | 70 | 불변 객체의 예시를 구조체를 사용하여 작성해 보겠습니다. 구조체를 사용한 이유는 클래스보다 객체 생성 비용이 훨씬 적기 때문입니다. 앞서 언급한 불변 객체의 단점인 객체 생성 비용이 많이 들 수 있다는 점을 어느 정도 완화해 줍니다. 71 | 72 | 또한, 스위프트에서는 구조체 안에서 프로퍼티의 값을 변경하려 할 때 해당 프로퍼티가 `var`로 선언되어 있더라도 디폴트로 자기 자신을 변경하지 못하게 하며, 변경하려 할 경우 컴파일 에러를 발생시킵니다. 컴파일 에러를 해결하기 위해서는 mutating 키워드를 명시해야 합니다. 이런 구조체의 특성이 내부 프로퍼티의 변경을 지양하려는 저자의 구현 방식에 부합한다고 생각했습니다. 73 | 74 | 또한 구조체는 상속이 제한되어 있어 이전에 언급한 규칙들 중 서브클래싱을 막아야 한다는 규칙은 자동으로 만족합니다. 75 | 76 | ```swift 77 | // 권장되지 않는 방식 78 | struct Complex { 79 | private var real: Double 80 | private var imaginary: Double 81 | 82 | init(real: Double, imaginary: Double) { 83 | self.real = real 84 | self.imaginary = imaginary 85 | } 86 | 87 | // 자기 자신의 프로퍼티를 변경하려 할 경우 mutating 키워드를 추가해야 합니다. 88 | mutating func add(_ complex: Complex) { 89 | real += complex.real 90 | imaginary += complex.imaginary 91 | } 92 | } 93 | ``` 94 | 95 | 위 예시처럼 내부 프로퍼티를 변경하는 방식보다 아래 방식이 더 좋습니다. 96 | 97 | ```swift 98 | struct Complex { 99 | private let real: Double 100 | private let imaginary: Double 101 | 102 | init(real: Double, imaginary: Double) { 103 | self.real = real 104 | self.imaginary = imaginary 105 | } 106 | 107 | func plus(_ complex: Complex) -> Complex { 108 | return Complex(real: real + complex.real, imaginary: imaginary + complex.imaginary) 109 | } 110 | } 111 | ``` 112 | 113 | 자기 자신을 수정하지 않고 새로운 인스턴스를 만들어 반환하도록 구현하였습니다. 114 | 115 | ### 스위프트에서의 메서드 네이밍 규칙 116 | 117 | 앞서 메서드의 이름으로 객체의 값을 변경하지 않는다는 점을 강조한다는 설명을 했었는데, 스위프트에서도 메서드의 동작에 따른 네이밍 규칙이 있습니다. [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/#strive-for-fluent-usage)를 보면, side-effect를 고려하여 메서드의 이름을 지어야 한다는 내용이 있습니다. 118 | 119 | > side-effect가 없으면 `x.distance(to: y)`, `i.successor()`와 같이 명사형으로 짓습니다. 120 | 121 | 예를 들어 함수를 호출 했을 때 기존 인스턴스의 값을 변경하지 않고 새로운 객체를 생성하여 리턴하는 경우, side-effect가 없다고 할 수 있습니다. 이 경우 함수의 이름을 명사형으로 짓습니다. 122 | 123 | > side-effect가 있으면 동사형으로 읽혀야 합니다. 예를 들어, `x.sort()`, `x.append(y)`입니다. 124 | 125 | 예를 들어 호출로 인해 인스턴스의 값이 변경되는 함수의 경우, side-effect가 있습니다. 이 경우 함수의 이름이 동사형으로 읽히도록 짓습니다. 126 | 127 | > Mutating/nonmutating 메서드 pair들의 이름을 일관적으로 짓습니다. 어떤 mutating 메서드는, 비슷한 동작을 하지만 인스턴스의 값을 업데이트하는 대신 새로운 값을 반환하는 nonmutating variant를 가질 수 있습니다. 128 | 129 | 함수의 동작이 동사로 설명될 경우, mutating 메서드는 명령형으로 짓고, 대응되는 nonmutating 메서드에는 "ed", 또는 "ing" 접미사를 붙입니다. 130 | 131 | - `x.sort()` ↔︎ `z = x.sorted()` 132 | - `x.append(y)` ↔︎ `z = x.appending(y)` 133 | 134 | 함수의 동작이 명사로 설명될 경우, nonmutating 메서드에 명사를 사용하고, 대응되는 mutating 메서드에는 "form" 접두사를 적용합니다. 135 | 136 | - `x = y.union(z)` ↔︎ `y.formUnion(z)` 137 | - `j = c.successor(i)` ↔︎ `c.formSuccessor(&i)` 138 | 139 | ### 마무리 140 | 141 | 인스턴스 내부 값을 변경할 수 없도록 제한하여 불변 객체로 만들면, 객체가 단순해지며 안전해지는 등 여러 장점이 있습니다. 객체를 불변으로 만들기 위한 규칙과 그 예시에 대해 설명하였습니다. 142 | 143 | 하지만 객체를 불변으로 만들다 보면 객체 생성 비용이 많이 들 수 있다는 단점도 있는데, 이런 단점을 완화하였으며 불변 객체로 만들기에도 적합한 특성을 갖고 있는 스위프트의 구조체에 대해서도 정리하였습니다. 144 | 145 | 마지막으로, 불변 객체에 관련한 스위프트 네이밍 규칙에 대해서도 추가하였습니다. 146 | 147 | ### References 148 | 149 | - [Swift API Design Guidelines — Strive for Fluent Usage](https://swift.org/documentation/api-design-guidelines/#strive-for-fluent-usage) 150 | 151 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item18.md: -------------------------------------------------------------------------------- 1 | # Item 18. 상속보다는 컴포지션을 사용하라 2 | 3 | 상속은 잘못 사용하면 오류를 내기 쉽습니다. 상속을 이용하게 되면 하위 클래스는 상위 클래스의 내부 구현에 의존하게 되기 때문입니다. 상위 클래스의 내부 구현에 의존할 경우, 자신의 다른 부분을 사용하는 self-use 여부에 의해 잘못 동작할 수 있습니다. 4 | 5 | ```java 6 | public class InstrumentedHashSet extends HashSet { 7 | private int addCount = 0; // 추가된 원소의 수 8 | 9 | @Override public boolean add(E e) { 10 | addCount++; 11 | return super.add(e); 12 | } 13 | 14 | @Override public boolean addAll(Collection c) { 15 | addCount += c.size(); 16 | return super.addAll(c); 17 | } 18 | 19 | // ... 20 | } 21 | ``` 22 | 23 | 만약 HashSet에 추가되는 원소의 개수를 세고 싶어서 위와 같이 구현한다면, 제대로 작동하지 않습니다. HashSet의 `addAll` 메서드가 내부적으로 `add` 메서드를 사용해 구현되어 있기 때문에, InstrumentedHashSet을 사용할 때 `addAll`을 호출한다면 HashSet 안에서 재정의된 `add`를 호출하게 되어 `addCount`는 중복되어 증가합니다. 24 | 25 | 또한 상위 클래스의 내부 구현은 릴리즈 시마다 얼마든지 달라질 수 있기 때문에, 하위 클래스의 코드를 변경하지 않아도 상위 클래스의 내부 구현이 바뀌면 하위 클래스가 오동작할 수 있습니다. 26 | 27 | 만약 하위 클래스의 데이터가 특정 조건을 만족해야 정상 동작하는 경우, 상위 클래스에 이 데이터를 변경 혹은 추가할 수 있는 새로운 메서드가 추가된다면 하위 클래스의 정상 동작을 보장할 수 없게 됩니다. 28 | 29 | 이런 오동작을 막기 위해 메서드 재정의를 피하고 새로운 메서드만 추가하는 방식으로 상속하더라도, 운 나쁘게 다음 릴리즈에서 상위 클래스에 같은 시그니처의 메서드가 생긴다면 재정의한 꼴이 되며 리턴 타입만 다르다면 컴파일 에러가 납니다. 30 | 31 | ### 컴포지션과 포워딩 32 | 33 | 컴포지션을 이용하면 앞서 언급한 문제들을 모두 피할 수 있습니다. 컴포지션은 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하도록 만드는 설계 방식입니다. 34 | 35 | 그리고 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환하게 합니다. 이 방식을 포워딩(forwarding)이라 합니다. 36 | 37 | 다음은 컴포지션과 포워딩 방식으로 구현한 코드입니다. 38 | 39 | ```java 40 | // 상속 대신 컴포지션을 사용하는 Wrapper 클래스 41 | public class InstrumentedSet extends ForwardingSet { 42 | private int addCount = 0; 43 | 44 | @Override public boolean add(E e) { 45 | addCount++; 46 | return super.add(e); 47 | } 48 | 49 | @Override public boolean addAll(Collection c) { 50 | addCount += c.size(); 51 | return super.addAll(c); 52 | } 53 | 54 | // ... 55 | } 56 | 57 | // 재사용이 가능한 포워딩 클래스 58 | public class ForwardingSet implements Set { 59 | private final Set s; 60 | public ForwardingSet(Set s) { this.s = s; } 61 | 62 | public boolean add(E e) { return s.add(e); } 63 | public boolean addAll(Collection c) { return s.addAll(c); } 64 | 65 | // ... 66 | } 67 | ``` 68 | 69 | 이처럼 구현할 경우, `addAll()`메서드의 자기 사용 여부에 상관 없이 `addCount`가 정상 동작합니다. 그 이유는 전달 클래스 덕분에 `ForwardingSet`의 `addAll()`에서 `add()`를 호출하지 않고 `Set`의 오버라이드되지 않은 원본 `addAll()`을 호출하기 때문입니다. 70 | 71 | 또한 새로운 클래스는 기존 클래스의 내부 구현 방식에 의존하지 않게 되며, 기존 클래스에 새로운 메서드가 추가 되더라도 전혀 영향을 받지 않습니다. 그리고 인터페이스를 활용해 설계되었기 때문에 유연합니다. 72 | 73 | ### 스위프트에서의 컴포지션 예시 74 | 75 | 오버라이드로 인해 문제가 발생하는 예시는 아니지만, 스위프트에서 컴포지션을 사용한 예시를 설명하겠습니다. 76 | 77 | ```swift 78 | import RxSwift 79 | 80 | /// PublishRelay is a wrapper for `PublishSubject`. 81 | /// 82 | /// Unlike `PublishSubject` it can't terminate with error or completed. 83 | public final class PublishRelay: ObservableType { 84 | private let _subject: PublishSubject 85 | 86 | // Accepts `event` and emits it to subscribers 87 | public func accept(_ event: Element) { 88 | self._subject.onNext(event) 89 | } 90 | 91 | // ... 92 | } 93 | ``` 94 | 95 | UI 바인딩의 경우 Error나 Completed로 인해 스트림이 끊기지 않을 필요가 있습니다. Relay는 Subject의 인스턴스를 갖고 있으면서 onNext 이벤트만 전달해 스트림이 끊기지 않도록 하는 wrapper입니다. 96 | 97 | 만약 여기서 컴포지션이 아니라 상속과 오버라이드를 사용하였다면, 여러 문제가 발생합니다. 먼저 PublishSubject의 메서드들이 이미 public이기 때문에 클라이언트에서의 onError, onCompletion 호출을 완전히 막을 수는 없게 됩니다. 이 메서드들은 사용자를 혼란스럽게 할 수 있을 뿐더러, 상위 클래스의 메서드를 호출하여 하위 클래스인 PublishRelay의 정상 동작을 해칠 수도 있습니다. 98 | 99 | 또 다른 문제로는 만약 (그럴 리는 없겠지만) PublishSubject에 onBefore와 같은 메서드가 추가된다면, 클라이언트 측에서 해당 메서드를 사용해 의도되지 않은 이벤트를 전달할 수 있습니다. 100 | 101 | ### 스위프트에서의 기능 확장 102 | 103 | 추가적으로, 스위프트에서는 객체의 기능 확장을 목적으로 상속 뿐만 아니라 extension을 사용하기도 합니다. extension을 사용해서 기존 타입에 연산 프로퍼티, 메서드, nested 타입 등을 추가할 수 있습니다. 이번 아이템에서 소개한 문제점들을 바탕으로 기능 확장에 extension을 사용하는 경우엔 어떤 장단점이 있을지 확인해 보겠습니다. 104 | 105 | ```swift 106 | extension Array { 107 | func take(_ number: Int) -> ArraySlice { 108 | return self[0.. 15 | 16 | ### 상속을 위한 문서화 17 | 클래스를 안전하게 상속할 수 있도록 문서화를 해놓는 것을 권장하고 있습니다. 특히, 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야합니다. 18 | - 문서화를 할때 고려해야 할 부분들 입니다. 19 | 1. 상속용 클래스(상위 클래스)는 재정의 할 수 있게 만든 메서드들을 내부적으로 어떻게 사용하는지 문서로 남겨야 합니다. 20 | 2. 공개(public, open) 메서드에서 재정의 가능한 메서드를 호출 할 때, API에 명시하여야 합니다. 21 | 3. 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 합니다. 22 | 23 | #### 부작용 24 | 문서화할 때 '어떻게' 동작하는지도 설명해야 하는 것이 부작용입니다. 좋은 API 문서는 API가 '어떻게' 동작하는지 아닌 '무엇'을 하는지만을 설명해야 하지만, 상속으로 인해 캡슐화를 해치게 되어 내부 구현 방식을 설명해야하기 때문입니다. 25 | 26 |
27 | 28 | #### Java 29 | 자바의 경우 API 문서에서는 Implementation Requirements로 시작하는 절이 있는데, 해당 절은 @implSpec 태그를 붙여주면 자바독 도구가 생성해줍니다. 30 | > Javadoc : Java 소스를 문서화 하는 방법, html로 열 수 있습니다. 31 | 32 | #### Swift 33 | 스위프트의 경우 JavaDoc처럼 따로 문서를 만들 수는 없지만, Xcode에서 다양한 주석 및 마크다운을 이용해 메서드 상단에 명시함으로써 호출하는 곳에서 해당 메서드의 설명을 팝업으로 볼 수 있게 되어있습니다. 34 | 35 |
36 | 37 | ### 상속을 위한 설계 38 | #### 어떤 메서드를 재정의 할 수 있게 해야하나? 39 | 1. 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 재정의 가능한 메서드를 제공해야 할 수도 있습니다. 40 | > hook(hooking) : 함수 호출, 중간에서 가로챈다고 표현한다. 41 | 42 |
43 | 44 | 예제에서는 Java의 `removeRange` 메서드를 예로 들고있습니다. 45 | 해당 메서드를 protected로 제공한 이유는 `clear` 메서드가 아래에 나와있는 `removeRange` 메서드를 부르기 때문입니다. 46 | 47 | ```Java 48 | protected void removeRange(int fromIndex, int toIndex) { 49 | ListIterator it = listIterator(fromIndex); 50 | for (int i=0, n=toIndex-fromIndex; i Swift가 이러한 초기화 방식을 둔 이유 중 하나는 초기화되기 전에 속성 값에 접근하는 것을 막아 주고, 예상치 못하게 속성 값이 또 다른 생성자에 의해 엉뚱한 값으로 설정되어 버리는 것도 막아주기 위함입니다.
93 | 양이 방대하여 해당 문서에 따로 정리하지 않으나, Swift 공식문서 중 **Initialization**항목을 참고해보시면 도움이 될 것 같습니다. 94 | - 다만 개인적인 생각으로는 재정의 가능한 함수를 생성자에서 호출시킨다는 것은 예측가능하기 어려운 흐름이 발생할 수 도 있기 때문에 지양하는 방향이 올바르다고 생각합니다. 95 | 96 |
97 | 98 | ### 상속용 클래스가 아닌 일반적인 클래스에 대한 경우 99 | 클래스가 final로 선언되지도 않았고, 상속용으로 설계되거나 문서화도 해놓지 않았을 경우입니다. 100 | 이러한 클래스는 수정이 생길때마다 하위 클래스가 오동작 할 수 있는 가능성이 있기 때문에 해결할 수 있는 몇가지 방법들이 있습니다. 101 | 102 | 1. final 클래스로 만들어 상속을 금지 시킨다. 103 | 2. 모든 생성자를 private으로 선언하고 정적팩터리 메서드를 제공합니다. (아이템 1, 17 참조) 104 | 105 | #### 그래도 상속을 해야하는 경우 106 | 1. 내부에서 재정의 가능 메서드를 호출하지 않게 만들고 이를 문서화 합니다. 107 | 2. 재정의 가능 메서드의 내부 로직을 private한 '도우미 메서드'로 옮기고, 이 '도우미 메서드'를 호출하도록 수정합니다. 108 | 이렇게 하면 재정의 가능 메서드를 호출하는 다른 코드들은 '도우미 메서드'만을 호출하도록 합니다. 109 | 110 |
111 | 112 | ### 참고한 곳 113 | - https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID216 114 | - http://xho95.github.io/xcode/swift/grammar/initialization/2016/01/23/Initialization.html 115 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item20.md: -------------------------------------------------------------------------------- 1 | # item 20. 추상 클래스보다는 인터페이스를 우선하라 2 | 3 | `item 20`의 주제는 `추상 클래스보다는 인터페이스를 우선하라` 입니다. 이 아이템에서 Java의 추상 클래스와 인터페이스가 각각 무엇이고 어떤 목적을 위해 사용하는 지 알아볼 예정입니다. 그리고 스위프트에서 같은 목적을 이루기 위해 어떤 것들이 마련되어 있고, 어떻게 사용할 수 있는지를 함께 알아봅니다. 4 | 5 | 6 | 7 | ## Java 8 | 9 | ### Abstract class 10 | 11 | 추상 클래스란 구체적이지 않은 클래스를 의미합니다. 하나 이상의 추상 메소드(abstract method)를 포함합니다. **추상 메소드는 선언만 있고 본체는 없는 함수이며, 선언부에 `abstract` 라는 키워드를 붙입니다.** 추상 메소드가 포함되었다면 클래스도 추상 클래스이므로 클래스명 앞에도 `abstract` 키워드를 붙여야 합니다. 12 | 13 | 그리고 추상메소드의 접근 지정자로 `private`은 사용할 수 없습니다. 자식 클래스에서 해당 메소드를 구현해야만 하기 때문입니다. 14 | 15 | ```java 16 | public abstract class Animal { 17 | public String name; //일반 멤버 변수 18 | 19 | public abstract void sing(); //추상 메소드 20 | public void move() { //일반 메소드 21 | System.out.println("어슬렁 어슬렁"); 22 | } 23 | } 24 | ``` 25 | 26 | 추상 클래스는 추상 메서드를 포함하고 객체화 할 수 없다는 점을 제외하면 일반 클래스와 다르지 않습니다. 그리고 생성자, 멤버변수와 일반 메서드도 가질 수 있습니다. 인스턴스를 생성할 수 없으므로 추상 클래스 자체로는 객체를 생성할 수 없지만, 새로운 클래스를 작성하는 데 있어 부모 클래스로서의 중요한 역할을 가집니다. 27 | 28 | ```java 29 | public class Dog extends Animal { 30 | public void move() { 31 | System.out.println("타닥 타닥"); 32 | } 33 | 34 | public void sing() { 35 | System.out.println("멍멍"); 36 | } 37 | } 38 | 39 | public class Cat extends Animal { 40 | public void sing() { 41 | System.out.println("냥냥"); 42 | } 43 | } 44 | ``` 45 | 46 | **추상 클래스는 다른 클래스들이 공통으로 가져야하는 메소드의 원형을 정의하고, 그것을 상속받은 경우 반드시 구현하도록 강요합니다.** 메소드 오버라이드와 유사해서 혼동하기 쉬우나 오버라이드는 안해도 상관없습니다. 위의 예에서도 `Cat`은 `move()`를 오버라이드 하지 않았습니다. 그럼 `Cat`의 `move()`를 호출하게 되는 경우, `Animal`의 `move()`가 호출됩니다. 하지만 `sing()`은 `Animal`을 상속받은 `Dog`과 `Cat` 모두 구현해야만 합니다. 그리고 만약 어떤 추상클래스를 상속받은 자식 클래스에서 추상 메소드를 구현하지 않았다면 자식 클래스도 추상클래스가 되어야 합니다. 47 | 48 |
49 | 50 | ### Interface 51 | 52 | 인터페이스는 추상 클래스보다 한단계 더 추상화된 클래스라고 볼 수 있습니다. 추상 클래스보다 추상화 정도가 높아서 구현부를 지닌 일반 메소드나 멤버 변수를 가질 수 없고 오직 추상 메서드와 상수만을 가질 수 있습니다. 53 | 54 | ```java 55 | interface 인터페이스이름 { 56 | public static final 자료형 변수명 = 변수값; 57 | public abstract 반환자료형 함수명(입력인자); 58 | } 59 | 60 | //생략된 버전 61 | interface 인터페이스이름 { 62 | 자료형 변수명 = 변수값; 63 | 반환자료형 함수명(); 64 | } 65 | ``` 66 | 67 | 인터페이스의 멤버 변수는 `public static final` 로만 지정이 가능하고 생략할 수 있습니다. `final`이므로 선언시 변수 값을 반드시 지정해 주어야 한다. 멤버는 `public abstract`가 기본형이고 생략 가능합니다. 68 | 69 | 인터페이스도 직접 객체를 생성할 수 없습니다. 70 | 71 | ```java 72 | interface Talkable { 73 | int volume = 1; 74 | void talk(); 75 | } 76 | 77 | public class Robot implements Talkable { 78 | public void talk() { 79 | System.out.println("#$%^&*"); 80 | } 81 | } 82 | 83 | public class Parrot implements Talkable { 84 | public void talk() { 85 | System.out.println("안녕"); 86 | } 87 | } 88 | ``` 89 | 90 | 인터페이스를 구체화하기 위해선 `implements` 키워드를 사용합니다. 그리고 `talk()`의 접근지정자는 반드시 `public`이어야 하는데, 인터페이스에서 (생략되었다 할 지라도) `public abstract`로 정의되었기 때문입니다. 91 | 92 | 인터페이스는 구현하고자 하는 여러 클래스의 공통적인 부분(정적 변수와 public 함수)만 기술해놓은 기초 설계도와 같습니다. 인터페이스를 구현하는 클래스에서 추상 메소드의 몸체를 모두 정의하도록 강요합니다. 만약 인터페이스가 구현하는 어떤 클래스가 모든 추상 메소드를 구현하지 않는다면 그 클래스는 추상클래스가 되어야 합니다. 93 | 94 |
95 | 96 | 97 | 98 | ### 추상 클래스와 인터페이스의 공통점 99 | 100 | - 선언만 있고 구현 내용은 없다 101 | - 인스턴스화 할 수 없다 102 | - 추상 클래스를 상속받은 자식과 인터페이스를 구현한 객체만 인스턴스화 할 수 있다 103 | 104 | 105 | 106 | ### 추상 클래스와 인터페이스의 차이점 107 | 108 | - 추상클래스: 단일 상속 109 | - 추상 클래스의 목적: 상속을 받아 기능을 확장시키는 것(부모의 유전자를 물려받음) 110 | - 인터페이스: 다중 상속 111 | - 인터페이스의 목적: 구현하는 모든 클래스에 대해 반드시 특정한 메서드가 존재하도록 강제하는 역할. 구현 객체가 같은 동작을 한다는 걸 보장하기 위함 112 | 113 | 114 | 115 | ### 추상클래스와 인터페이스의 장점 116 | 117 | - 개발 시간을 단축할 수 있다 118 | - 개발자들이 각각의 부분을 완성할 때까지 기다리지 않고 정의된 규약을 따라 각각 작업할 수 있음 119 | 120 | - 표준화가 가능하다 121 | - 클래스의 기본 틀을 제공해 개발자들에게 정형화된 개발을 강요할 수 있음 122 | 123 | - 서로 관계 없는 클래스들에게 관계를 맺어줄 수 있음 124 | - 클래스들의 과도한 상속을 줄여 코드의 종속성을 줄이고 유지보수를 용이하게 함 125 | 126 | - 독립적인 프로그래밍이 가능함 127 | - 클래스간의 직접적인 관계가 아닌 인터페이스로 연결된 간접적인 관계를 맺어주면, 한 클래스의 변경이 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능해짐 128 | 129 |
130 | 131 | 추상 클래스와 인터페이스를 통해 얻을 수 있는 장점을 위해 Swift에서는 어떤 걸 이용할 수 있을지 알아보겠습니다. 132 | 133 | ## Swift 134 | 135 | ### Implementing abstract class 136 | 137 | 스위프트에서는 추상클래스를 지원하지 않습니다. 추상 클래스에 대한 대안을 찾아보기위해 먼저 추상 클래스 패턴을 모방해보겠습니다. 138 | 139 | ```swift 140 | class Animal { 141 | func sound() { 142 | print("새근새근") 143 | } 144 | } 145 | 146 | class Cat: Animal { 147 | override func sound() { 148 | print("냥") 149 | } 150 | } 151 | 152 | class Dog: Animal { 153 | override func sound() { 154 | print("멍") 155 | } 156 | } 157 | ``` 158 | 159 | 자바에서 추상클래스를 상속 받아 사용하는 것처럼 스위프트에서도 부모 클래스를 자식이 상속받아 사용할 수 있습니다. 하지만 여기의 `Animal` 은 추상적이지 않습니다. 160 | 161 | ```swift 162 | class Animal { 163 | func sound() { 164 | fatalError("Subclasses need to implement the `sound()` method.") 165 | } 166 | } 167 | class Cat: Animal { 168 | override func sound() { 169 | print("냥") 170 | } 171 | } 172 | 173 | class Dog: Animal { 174 | 175 | } 176 | 177 | let cat = Cat() 178 | cat.sound() 179 | 180 | let dog = Dog() 181 | dog.sound() // fatalError 182 | ``` 183 | 184 | 만약 `Animal`의 `sound()`를 호출하게 되는 경우 런타임에러가 발생하도록 구현해서, 상속받는 자식 객체들은 반드시 `sound`를 override 해야 한다고 가정해봅시다. 하지만 상속을 이용하면 자식 객체가 특정 메소드를 반드시 구현해야 하는 것을 강요할 수 없습니다. 그래서 `Dog`이 `sound()`를 override 하지 않는다면 `dog.sound()`는 부모의 `sound()`를 호출하게 되어 런타임시점에 에러를 발생하게 됩니다. 이러한 누락이 컴파일 타임에 감지되지 않으면 개발자에게 무척 피곤한 일이 됩니다. 185 | 186 | 앞서 작성한 이러한 패턴은 추상 클래스를 그저 모방하는 것입니다. 이러한 방식 대신 Protocol이 무엇이고 어떻게 사용하는 지에 대해 알아봅시다. 187 | 188 | 189 | 190 | ### Protocol 191 | 192 | 프로토콜은 특정 역할을 하기 위한 메서드, 프로퍼티 등의 청사진으로, 구조체, 클래스, 열거형은 프로토콜을 채택해 요구사항을 실제로 구현할 수 있습니다. 프로토콜은 정의를 하고 제시 할 뿐 스스로 기능을 구현하지는 않습니다. 하나의 타입으로 사용됩니다. 193 | 194 | ```swift 195 | protocol 프로토콜이름 { 196 | //프로토콜 정의 197 | } 198 | ``` 199 | 200 | 프로토콜에서는 프로퍼티가 저장 프로퍼티인지 연산 프로퍼티인지 명시하지 않고, 이름과 타입, gettable/settable 여부만 명시합니다. 또한 프로퍼티는 항상 var로 선언되어야 합니다. 201 | 202 | ```swift 203 | protocol Women { 204 | var height: Double { get set } 205 | var name: String { get } 206 | 207 | static func eat() 208 | func talk(to women: Women) 209 | mutating func run() 210 | } 211 | ``` 212 | 213 | 프로토콜에서는 인스턴스 메서드와 타입 메서드를 정의할 수 있습니다. 하지만 메서드 파라미터의 기본 값은 프로토콜 안에서 사용할 수 없습니다. 선언부만 작성하고 구현부는 작성하지 않기 때문입니다. `mutating` 키워드를 사용하면 인스턴스에서 변경 가능하다는 것을 나타냅니다. 214 | 215 | 프로토콜이 올바르게 구현되지 않았다면 컴파일러가 컴파일 시점에 알려주는 장점이 있습니다. 216 | 217 | ```swift 218 | struct Delma: Women { 219 | var heartRate = 100 220 | var roundingHeight: Double = 0.0 221 | var height: Double { 222 | get { 223 | return roundingHeight 224 | } 225 | set { 226 | roundingHeight = 160.0 227 | } 228 | } 229 | var name: String = "delma" 230 | 231 | static func eat() { 232 | print("냠냠") 233 | } 234 | 235 | func talk(to women: Women) { 236 | print("\(women) 안녕") 237 | } 238 | 239 | mutating func run() { 240 | heartRate += 10 241 | } 242 | } 243 | ``` 244 | 245 | 246 | 247 | 248 | 249 |
250 | 251 | ### 참고 252 | 253 | [자바의 인터페이스](https://studymake.tistory.com/426?category=646127) 254 | 255 | [자바의 추상클래스](https://studymake.tistory.com/423) 256 | 257 | [추상 클래스와 인터페이스](https://velog.io/@lshjh4848/추상클래스와-인터페이스-bok2k2qjrg) 258 | 259 | [추상 클래스와 인터페이스 차이](https://cbw1030.tistory.com/47) 260 | 261 | [프로토콜](https://medium.com/@jgj455/오늘의-swift-상식-protocol-f18c82571dad) 262 | 263 | [추상클래스 In Swift](https://cocoacasts.com/how-to-create-an-abstract-class-in-swift) 264 | 265 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item21.md: -------------------------------------------------------------------------------- 1 | # Item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 2 | 3 | ### Java의 인터페이스 디폴트 메서드(Interface Default Method) 4 | 5 | 본문에서는 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 인터페이스(Interface)의 디폴트 메서드(Default Method)를 작성하기 어렵고 때문에 디폴트 메서드로 인해 기존 클라이언트를 망가뜨릴 수 있기 때문에 꼭 필요한 경우가 아니면 인터페이스를 구현하는 쪽으로 생각해 설계하라고 권고하고 있습니다. 6 | 7 | 이렇듯 책에서는 인터페이스의 디폴트 메서드 구현을 지양하고 인터페이스를 구현하는 쪽으로 생각해 설계하라고 했는데, Swift에서는 어떨까요? 8 | 9 | 10 | 11 | ### Swift의 프로토콜 기본구현(Protocol Default Implementations) 12 | 13 | Java에서 인터페이스의 디폴트 메서드를 작성할 수 있듯이 Swift에서도 프로토콜(Protocol)의 요구사항을 익스텐션(Extension)을 통해 구현할 수 있는데, 이를 [**프로토콜 기본구현(Protocol Default Implementations)**](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID521) 이라고 합니다. 14 | 15 | Swift에서는 어떻게 바라봐야할지 생각하기에 앞서 기본 구현에 대한 강점과 프로토콜 기본 구현시 고려해야할 점들을 살펴봅시다. (이후에 내린 [결론](#결론)을 마지막에 작성해놓았습니다.) 16 | 17 | 제 생각에는 Swift에서(특히, 프로토콜 지향 프로그래밍(Protocol Oriented Programming)에서) 프로토콜을 익스텐션해서 프로토콜의 요구사항을 구현하는 것은 Swift가 가지고있는 강점 중 하나인 것 같습니다. 그 이유는 아래와 같습니다. 18 | 19 | - 기존 타입의 소스 코드에 대한 액세스 권한이 없더라도 익스텐션으로 기존 타입을 확장하여 새 프로토콜을 채택하고 준수할 수 있도록 만들어줄 수 있고 익스텐션으로 기존 타입에 새로운 프로퍼티(저장 프로퍼티 제외), 메서드 및 서브스크립트(subscript)를 추가할 수 있어 프로토콜이 요구할 수 있는 요구사항을 추가할 수도 있습니다. (다만 프로토콜에서 저장 프로퍼티(Stored Property)를 구현할 수 없으므로 저장 프로퍼티는 프로토콜을 채택한 타입에서 직접 구현해야합니다.) 이렇게 프로토콜과 익스텐션을 사용하여서 기본 구현을 해두면 중복된 코드를 줄일 수 있는 장점이 있습니다. 20 | - 프로토콜에서 정의한 요구사항의 경우 익스텐션에서 기본 구현해 둔 기능을 사용하지 않을 때에는 정의할 수 있습니다. 특정 프로토콜을 준수하는 타입에 프로토콜의 요구사항이 이미 구현되어 있다면 그 기능을, 그렇지 않다면 프로토콜 기본 구현 기능을 호출합니다. 이처럼 프로토콜 기본 구현을 통해 기능을 구현한다면 프로토콜 채택만으로 타입에 기능을 추가해 사용할 수 있습니다. 21 | 22 | <간단한 예시> 23 | 24 | ```swift 25 | protocol TextRepresentable { 26 | var describeSelf: String { get } 27 | } 28 | 29 | extension TextRepresentable { 30 | func describeSelf() { 31 | print(self) 32 | } 33 | } 34 | 35 | extension Int: TextRepresentable { } // 자세한 구현은 생략 36 | extension String: TextRepresentable { } // 자세한 구현은 생략 37 | extension Double: TextRepresentable { } // 자세한 구현은 생략 38 | 39 | 8291.describeSelf() // 8291 40 | 3.14.describeSelf() // 3.14 41 | "Effective Swift".describeSelf() // "Effective Swift" 42 | ``` 43 | 44 | 또한 기존에 제공되는 프로토콜들의 익스텐션을 통한 기본 구현 코드를 볼 수는 없지만, 공개되어있는 내부 구현을 살펴봤을 때 스위프트의 많은 기능들이 프로토콜, 익스텐션, 제네릭의 조합으로 구현되어 있는 것 같습니다. 그 중에서 한 가지 예를 들어 살펴보자면, `Array`, `Set`, `Dictionary` 에서 공통으로 채택하고 있는 프로토콜들은 `CustomDebugStringConvertible`, `CustomReflectable`, `CustomStringConvertible` 등이 있는데 타입들에 공통으로 들어가는 코드의 중복을 줄이기 위해 타입별로 공유하는 부분들을 각 프로토콜에서 익스텐션을 통해 기본 구현했을 것이라고 예상해볼 수 있을 것 같습니다. 45 | 46 | ### 프로토콜 기본 구현시 고려해야할 점 47 | 48 | 다만 이런 점들은 프로토콜 기본 구현시 고려해야합니다.
(예제 중 ⭐️한 곳을 유의해서 봐주세요.) 49 | 50 | 1. **Method Dispatch** 51 | 52 | ```swift 53 | protocol SampleProtocol { 54 | func foo() 55 | } 56 | extension SampleProtocol { 57 | func foo() { 58 | print("protocol foo") 59 | } 60 | func bar() { 61 | print("protocol bar") ⭐️ 62 | } 63 | } 64 | class SampleClass: SampleProtocol { 65 | func foo() { 66 | print("class foo") 67 | } 68 | func bar() { 69 | print("class bar") 70 | } 71 | } 72 | let sample: SampleProtocol = SampleClass() 73 | sample.foo() // "class foo" 74 | sample.bar() // "protocol bar" ⭐️ 75 | ``` 76 | 77 | * `foo()`와 같은 **Protocol-required method**는 런타임에 실행할 메서드를 선택하는 **dynamic dispatch**를 사용합니다. 78 | * `bar()`와 같은 **Extension-defined method**는 빌드 타임에 실행할 메서드를 선택하는 **static dispatch**를 사용합니다. 즉, 프로토콜의 요구사항으로 정의되어있지 않고 익스텐션에서만 구현되어있는 메서드(위 예제에서 `bar()`)는 프로토콜을 채택한 타입에서 같은 이름으로 메서드를 덮어쓰는 경우 적용되지 않고 extension 익스텐션에서 구현한 기본구현을 사용해야합니다. 79 | 80 | 2. **Dispatch Precedence and Constraints** 81 | 82 | ```swift 83 | protocol SampleProtocol { 84 | func foo() 85 | } 86 | extension SampleProtocol { 87 | func foo() { 88 | print("SampleProtocol") 89 | } 90 | } 91 | protocol BarProtocol {} 92 | extension SampleProtocol where Self: BarProtocol { 93 | func foo() { 94 | print("BarProtocol") ⭐️ 95 | } 96 | } 97 | class SampleClass: SampleProtocol, BarProtocol {} 98 | let sample: SampleProtocol = SampleClass() 99 | sample.foo() // "BarProtocol" ⭐️ 100 | ``` 101 | 102 | 프로토콜에서 요구하는 메서드(Protocol-required method)일 경우에는 제약을 사용하여 기본 구현을 정의할 수 있습니다. 그리고 이러한 경우 *제약이 있는(constrained) 기본 구현이 제약이 없는 기본 구현보다 더 우선합니다.* 103 | 104 | * 우선 순위: *프로토콜을 준수한 class/struct/enum > 제약이 있는 protocol extension > 단순 protocol extension*. 105 | 106 | 107 | 108 | ### 결론 109 | 110 | [이번 챕터의 내용](#Java의-인터페이스-디폴트-메서드(Interface-Default-Method))은 자바와 스위프트 두 언어 뿐만 아니라 프로그래밍에서 공통적으로 적용되는 내용이라고 생각합니다.
그렇기 때문에 자바에서와 같은 맥락으로 Swift에서 역시 생각할 수 있는 사항들(프로토콜 기본 구현시 고려해야할 점들 등)을 고려하여 프로그래밍을 하더라도 일어날 수 있는 모든 상황에서 불변식을 해치지 않는 프로토콜(Protocol)의 기본 구현(Default Implementations)를 작성하기는 어렵습니다. 그리고 기본 구현으로 인해 기존 클라이언트를 망가뜨릴 가능성도 있습니다. **그렇기 때문에 책의 내용에서처럼 꼭 필요한 경우가 아니면 프로토콜의 요구사항를 구현하는 쪽으로 생각해 설계해야한다는 결론을 내렸습니다.** **즉, 프로토콜 사용시 채택한 곳에서 프로토콜의 요구사항을 구현하도록해서 다형성으로 동작하도록 하는게 프로토콜의 장점을 더 살리는 방향이라고 결론을 내렸습니다.** (또한, 이 아이디어의 연장선이 iOS에서 많이 사용되고 있는 Delegation 패턴인 것 같습니다. Delegation 패턴에 대해서도 살펴보면 좋을 것 같습니다.) 111 | 112 | ### 참고 113 | 114 | 1. [Adding Protocol Conformance with an Extension - The Swift Programming Language](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID276) 115 | 2. [Protocol - The Swift Programming Language](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID276) 116 | 3. [Protocol Extensions - The Swift Programming Language](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID521) 117 | 4. [프로토콜 지향 프로그래밍 POP](https://yagom.net/courses/swift-basic/lessons/프로토콜-지향-프로그래밍-p-o-p/) 118 | 5. [Swift: Why You Should Avoid Using Default Implementations in Protocols](https://medium.com/better-programming/swift-why-you-should-avoid-using-default-implementations-in-protocols-eeffddbed46d) 119 | 120 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item23.md: -------------------------------------------------------------------------------- 1 | # Item 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 2 | 3 | 두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 표시하는 클래스는 불필요한 코드가 많으며 가독성 면에서도 좋지 않습니다. 4 | 5 | ### 태그 달린 클래스 예시 6 | 7 | 이번 아이템에서 말하는 "태그 달린 클래스"의 문제점은 자바와 스위프트 두 언어 모두에서 거의 비슷하기 때문에 바로 스위프트 코드로 설명하겠습니다. 8 | 9 | ```swift 10 | class Figure { 11 | enum Shape { 12 | case rectangle, circle 13 | } 14 | 15 | private let shape: Shape 16 | 17 | // 사각형일 때만 쓰이는 프로퍼티 18 | private let length: Double? 19 | private let width: Double? 20 | 21 | // 원일 때만 쓰이는 프로퍼티 22 | private let radius: Double? 23 | 24 | var area: Double? { 25 | switch shape { 26 | case .rectangle: 27 | guard let length = length, let width = width else { return nil } 28 | return length * width 29 | case .circle: 30 | guard let radius = radius else { return nil } 31 | return Double.pi * radius * radius 32 | } 33 | } 34 | 35 | init(length: Double, width: Double) { 36 | self.shape = .rectangle 37 | self.length = length 38 | self.width = width 39 | self.radius = nil 40 | } 41 | 42 | init(with radius: Double) { 43 | self.shape = .circle 44 | self.length = nil 45 | self.width = nil 46 | self.radius = radius 47 | } 48 | } 49 | ``` 50 | 51 | 위 태그 달린 클래스에는 문제점이 많습니다. 우선 여러 구현이 한 클래스에 혼합되어 있어서 가독성이 좋지 않습니다. 코드를 읽는 사람 입장에서, 모양이 어떤 경우인지 먼저 생각하고 읽어야 합니다. 52 | 53 | 또 다른 문제점은, 객체를 생성할 때 `shape`을 제외하고는 해당 모양에 쓰이지 않는 프로퍼티들에 0과 같은 의미 없는 값을 넣거나, `shape`을 제외한 모든 프로퍼티들을 옵셔널로 선언하여 `nil`을 입력해 두거나 해야 합니다. 의미 없는 값을 넣는 방식의 경우 프로그램이 런타임에 오동작할 위험이 높아지며, 옵셔널로 선언할 경우 옵셔널 언래핑 등의 추가 코드를 작성해야 합니다. 54 | 55 | 삼각형 등의 새 도형을 추가하려 한다면, `Figure` 클래스의 전반적인 수정이 필요합니다. 새 열거형 case와 저장 프로퍼티를 추가하고, 모든 switch 문을 찾아 새 case를 처리하는 코드를 추가해야 합니다. 스위프트에서는 자바와 달리 switch 문에서 모든 열거형 case가 처리되지 않으면 컴파일 에러를 발생시켜서 도와주지만, 그 부분들을 모두 수정해야 한다는 사실에는 변함이 없습니다. 56 | 57 | 마지막으로, 인스턴스의 타입만으로는 해당 인스턴스가 어떤 모양을 나타내고 있는지 알기 어렵습니다. 해당 인스턴스의 `shape` 프로퍼티를 보고 판단해야 하는데, 이는 런타임에 결정되므로 코드의 의미를 파악하기가 더 어려워집니다. 58 | 59 | 이렇게 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적입니다. 60 | 61 | ### 계층 구조로 개선하기 62 | 63 | 태그 달린 클래스는 계층 구조로 만들어 개선하는 것이 좋습니다. 태그 달린 클래스를 계층 구조로 바꾸려면, 계층 구조의 root가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언합니다. 태그 값에 상관없는 공통적인 메서드나 프로퍼티들은 루트 클래스에 추가합니다. 64 | 65 | 그 다음, 루트 클래스를 확장한 구체 클래스를 각각 정의합니다. 여기서는 `Circle`과 `Rectangle`입니다. 각 구체 타입들에 각자의 의미에 해당하는 프로퍼티들을 추가하고, 추상 클래스에 정의된 메서드를 각 의미에 맞게 구현합니다. 66 | 67 | 저는 스위프트의 프로토콜이 인스턴스를 생성할 수 없으며 필요에 따라 선택적으로 extension을 통해 기본 구현을 제공할 수 있다는 점에서 추상 클래스와 유사하면서, 이번 예제에도 적합하다는 생각이 들어 프로토콜로 추상화하는 방법을 선택했습니다. 68 | 69 | ```swift 70 | protocol Figure { 71 | var area: Double { get } 72 | } 73 | 74 | struct Circle: Figure { 75 | private let radius: Double 76 | 77 | var area: Double { return Double.pi * radius * radius } 78 | 79 | init(radius: Double) { 80 | self.radius = radius 81 | } 82 | } 83 | 84 | struct Rectangle: Figure { 85 | private let length: Double 86 | private let width: Double 87 | 88 | var area: Double { return length * width } 89 | 90 | init(length: Double, width: Double) { 91 | self.length = length 92 | self.width = width 93 | } 94 | } 95 | ``` 96 | 97 | 계층 구조로 구성된 클래스들은 태그 달린 클래스의 단점을 모두 해결해 줍니다. 각 타입이 간결하고 명확하며, 옵셔널 언래핑 코드, switch 문 등 불필요한 코드들도 모두 사라졌습니다. 클래스가 표현하지 않는 모양에 관련된 프로퍼티들도 없어졌기 때문에, 의미 없는 값을 넣거나 옵셔널로 선언하지 않아도 됩니다. 98 | 99 | 또 다른 장점은, 기존 코드의 변경 없이 계층구조를 확장하거나 새 도형을 추가할 수 있다는 점입니다. 예를 들어 삼각형을 추가하려 한다면, `Figure`의 변경 없이 `Triangle` 타입을 하나 더 구현하면 됩니다. 100 | 101 | ### 계층 구조 활용법 102 | 103 |

104 | 105 | 위 이미지처럼, 비슷하지만 약간씩 다른 여러 종류의 버튼을 만들어야 한다면, 열거형으로 date, people, price 등의 case로 나누어 태그 달린 클래스로 구현하는 것 보다, Rounded Button을 상속한 세 가지 버튼으로 만드는 편이 더 좋습니다. 106 | 107 | 만약 새로운 필터 버튼을 추가하려 할 때도, 계층 구조로 설계했다면 변경에 유연하여 기존 코드의 변경 없이 새 필터 버튼을 추가할 수 있습니다. 108 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item24.md: -------------------------------------------------------------------------------- 1 | # item 24. 멤버 클래스는 되도록 static으로 만들라 2 | 3 | ### 개요 4 | 이번 아이템에서는 Java에서 사용하는 4가지 중첩 클래스와 언제 그리고 왜 사용해야하는지 살펴봅니다. 5 | 6 |
7 | 8 | ### 중첩 클래스의 종류 9 | - 정적 멤버 클래스 10 | - (비정적) 멤버 클래스 11 | - 익명 클래스 12 | - 지역 클래스 13 | 14 |
15 | 16 | #### 정적 멤버 클래스 17 | 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 쓰입니다. 18 | 19 | 예제에서는 `enum`인 `Operation`(item 34)을 예로 들어주는데 Java의 경우 `enum`의 타입은 `class`이고 `enum` 상수 하나당 인스턴스가 만들어지며, 각각의 타입은 `public static final` 입니다. 20 | 그렇기 때문에 Java에서는 아래 예제 코드와 같이 상수 선언시 몸체를 만들 수 있습니다. 21 | 22 | ```java 23 | public class Calculator { 24 | public enum Operation { 25 | PLUS {public double apply(double x, double y){return x + y}}; 26 | } 27 | } 28 | 29 | // 호출시 30 | Calculator.Operation.PLUS 31 | ``` 32 | 이렇게 계산기(Calculator)가 지원하는 연산 종류를 정의해서 사용할 수 있겠네요. 33 |
34 | 35 | 하지만 Swift에서는 `static class`를 지원하지 않으며(정확히 말하자면 이미 static하게 되어있습니다.) enum 또한 저러한 형태로 사용되지 못합니다. 36 | 최대한 호출하는 구조를 비슷하게 만들어보면 이러한 형식이 될것 같습니다. 37 | 38 | ```swift 39 | class Calculator { 40 | enum Operation { 41 | case minus 42 | case plus 43 | case time 44 | case divide 45 | 46 | func calculate(x: Double, y: Double) -> Double { 47 | switch self { 48 | case .minus: return x - y 49 | case .plus: return x + y 50 | case .time: return x * y 51 | case .divide: return x / y 52 | } 53 | } 54 | } 55 | } 56 | 57 | Calculator.Operation.minus.calculate(x: 3, y: 4) 58 | ``` 59 | 60 |
61 | 62 | #### (비정적) 멤버 클래스 63 | 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 `어댑터`를 정의할때 자주 쓰입니다. 64 | 65 | 책에서는 Java코드로 자신의 반복자를 구현한다는 예제 코드가 올라와있는데, 해당 코드를 이해하는데 어려움이 있어서 중첩 클래스로 어댑터를 구현하는 대신에 `어댑터 패턴`을 예시로 들겠습니다. 66 | 67 | 추후 보충하도록 하겠습니다. 68 | ```swift 69 | // 기본적으로 사용되고 있는 Target Class 70 | class Target { 71 | func request() -> String { 72 | return "Target: The default target's behavior." 73 | } 74 | } 75 | 76 | // Target과 동작은 비슷하지만 같은 타입은 아님. 77 | class Adaptee { 78 | public func specificRequest() -> String { 79 | return ".eetpadA eht fo roivaheb laicepS" 80 | } 81 | } 82 | 83 | // 어댑터를 통해 타입을 맞춰준다.(클래스 래핑) Adaptee -> Traget 84 | class Adapter: Target { 85 | private var adaptee: Adaptee 86 | 87 | init(_ adaptee: Adaptee) { 88 | self.adaptee = adaptee 89 | } 90 | 91 | override func request() -> String { 92 | return "Adapter: (TRANSLATED) " + adaptee.specificRequest().reversed() 93 | } 94 | } 95 | 96 | // 사용자는 target만을 주입받음. 97 | class Client { 98 | // ... 99 | static func someClientCode(target: Target) { 100 | print(target.request()) 101 | } 102 | // ... 103 | } 104 | 105 | 106 | Client.someClientCode(target: Target()) 107 | let adaptee = Adaptee() 108 | Client.someClientCode(target: Adapter(adaptee)) 109 | ``` 110 | 111 | #### 익명 클래스 112 | Swift에서는 지원하지 않아 다루지 않겠습니다. 113 | 114 | #### 지역 클래스 115 | 116 | 지역클래스는 메소드 내부의 지역변수처럼 선언해서 쓸수 있으나 Swift와의 용법에서는 맞지 않는 것 같아 역시 익명 클래스와 마찬가지로 다루지 않겠습니다. 117 | 118 |
119 | 120 | ### Swift는 중첩클래스를 언제 흔하게 사용할까? 121 | Swift는 Java처럼 중첩클래스를 자세하게 나누어서 쓰는 용법은 흔하게 볼수 있지 않았습니다. 122 | 보통 namespace를 정의할때 많이 쓰이는것 같았습니다. 123 | 124 | Java의 경우 정적 멤버 클래스를 제외하고 자동으로 외부 클래스의 인스턴스에 대한 참조를 가지기 때문에 외부 클래스의 인스턴스가 있는 경우에만 내부 클래스의 인스턴스를 만들 수 있습니다. 125 | 126 | Swift에서는 내부 클래스의 인스턴스는 외부 클래스의 인스턴스와 독립적입니다. 127 | 따라서 인스턴스화 할때 차이가 있는 것을 보실 수가 있습니다. 128 | 129 | #### Swift 예시 코드 130 | 131 | ```Swift 132 | class Master { 133 | var testProperty = 2; 134 | 135 | class Nested{ 136 | init() { 137 | // ... 138 | } 139 | } 140 | 141 | func foo() { 142 | // ... 143 | } 144 | } 145 | 146 | var master = Master() 147 | // Java랑은 다르게 Master의 인스턴스가 필요없다. 148 | var nested = Master.Nested() 149 | nested.foo() 150 | 151 | // error : Static member 'Nested' cannot be used on instance of type 'Master' 152 | var nested2 = master.Nested() 153 | ``` 154 | 155 | #### Java 예시 코드 156 | ```java 157 | public class Master { 158 | private static int testProperty = 100; 159 | 160 | class Nested { 161 | private int testProperty = 200; 162 | 163 | public void display() { 164 | // ... 165 | } 166 | } 167 | } 168 | 169 | Master out = new Master(); 170 | // Master의 인스턴스가 존재해야지만 인스턴스화 가능 171 | Master.Nested in = out.new Nested(); 172 | 173 | in.display(); 174 | ``` 175 | 176 |
177 | 178 | ### 참고한 곳 179 | - https://johngrib.github.io/wiki/java-enum/ 180 | - https://refactoring.guru/design-patterns/adapter/swift/example 181 | - https://stackoverflow.com/questions/26806932/swift-nested-class-properties 182 | - https://academy.realm.io/kr/posts/swift-namespace-typealias/ 183 | - https://onelife2live.tistory.com/15 184 | - https://gyrfalcon.tistory.com/entry/JAVAJ-Nested-Class [Minsub's Blog] 185 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/item25.md: -------------------------------------------------------------------------------- 1 | # Item 25. 톱레벨 클래스는 한 파일에 하나만 담으라 2 | 3 | ### Java에서의 톱레벨 클래스 선언 4 | 5 | Java에서는 하나의 소스 파일에 톱레벨 클래스를 여러 개 선언할 수 있습니다. 다만, 이는 아무런 득이 없을 뿐더러 심각한 위험을 감수해야 하는 행위입니다. 한 클래스를 여러 가지로 정의할 수 있으며, 그 중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하느냐에 따라 달라지기 때문입니다. 컴파일러에 어떤 소스를 먼저 건네느냐에 따라 동작이 달라지는 문제가 생길 수 있습니다. 6 | 7 | ```swift 8 | public class Main { 9 | public static void main(String[] args) { 10 | System.out.println(Utensil.NAME + Dessert.NAME); 11 | } 12 | } 13 | 14 | // 코드 25-1 두 클래스가 한 파일(Utensil.java)에 정의되었다. - 따라 하지 말 것! 15 | class Utensil { 16 | static final String NAME = "pan"; 17 | } 18 | 19 | class Dessert { 20 | static final String NAME = "cake"; 21 | } 22 | 23 | // 코드 25-2 두 클래스가 한 파일(Dessert.java)에 정의되었다. - 따라 하지 말 것! 24 | class Utensil { 25 | static final String NAME = "pot"; 26 | } 27 | 28 | class Dessert { 29 | static final String NAME = "pie"; 30 | } 31 | 32 | // 코드 25-3 톱레벨 클래스들을 정적 멤버 클래스로 바꿔본 모습 33 | public class Test { 34 | public static void main(String[] args) { 35 | System.out.println(Utensil.NAME + Dessert.NAME); 36 | } 37 | 38 | private static class Utensil { 39 | static final String NAME = "pan"; 40 | } 41 | 42 | private static class Dessert { 43 | static final String NAME = "cake"; 44 | } 45 | } 46 | ``` 47 | 48 | 49 | 50 | ### Swift에서의 톱레벨 클래스 선언 51 | 52 | Xcode에서 하나의 소스 파일에 톱레벨 클래스를 여러 개 선언할 수 있습니다. 하지만 Java와 달리 다른 파일에 있다고 하더라도 같은 이름의 톱레벨 클래스를 선언할 수 없습니다. `Invalid redeclaration of 'className'` 라는 오류 메세지를 띄우며 클래스를 생성할 수 없도록 막아놓았기 때문에 같은 이름의 클래스를 생성할 수 없습니다. 53 | 54 |

55 | 56 | **추가적으로, item25와 직접적으로 관련된 내용은 아니지만 두 언어에서 클래스를 어떻게 식별하는지에 대해 조사하였습니다. 아래 내용은 _알아두면 좋은 내용_ 으로 봐주시면 좋을 것 같습니다.** 57 | 58 | ### 개체를 구분하는 방식, 네임스페이스 59 | 60 | * 네임스페이스(Namespace) 61 | 62 | >In [computing](https://en.wikipedia.org/wiki/Computing), a **namespace** is a set of signs (*names*) that are used to identify and refer to objects of various kinds. A namespace ensures that all of a given set of objects have unique names so that they can be easily [identified](https://en.wikipedia.org/wiki/Identifier). 63 | > 64 | >-Wikipedia 65 | 66 | > Namespace is a named region of program used to group variable, types and methods. 67 | > -iOS 9 Programming Fundamentals with Swift 68 | 69 | 네임스페이스는 다양한 종류의 개체를 식별하고 참조하기 위해 사용되는 기호(이름)의 집합으로, 지정된 모든 개체 집합의 고유한 이름을 보장하므로 쉽게 식별할 수 있습니다. 즉, 개체를 구분할 수 있는 범위를 나타내는 말로 일반적으로 하나의 이름 공간에서는 하나의 이름이 단 하나의 개체만을 가리킵니다. 70 | 71 | 네임 스페이스는 다음과 같은 이점이 있습니다. 72 | 73 | 1. 이름 충돌을 방지합니다. 74 | 2. 캡슐화를 제공합니다. 75 | 76 | < 예시 > 77 | 78 | ```swift 79 | class Manny() { 80 | class Klass { } 81 | } 82 | ``` 83 | 84 | - Manny 안에 Klass 를 효과적으로 숨길 수 있습니다. 85 | - Manny는 바로 네임스페이스입니다. 86 | - Manny 내부의 코드는 Klass 를 직접 볼 수 있지만, 외부에서는 직접 볼 수 없습니다. 이를 위해서 ‘dot notation’을 사용. e.g.) Manny.Klass 87 | - 네임스페이스는 이처럼 공간을 구획하기 위한 편의를 제공합니다. 88 | 89 | **Java**에서 패키지(package) 정의를 통해 네임스페이스를 관리하며 `import` 를 정의하여 패키지에 정의된 클래스를 불러옵니다. 그리고 컴파일시 클래스가 유일한 식별이 가능한지를 확인합니다. 90 | 91 | Swift에 대한 내용을 설명하기 전에, Swift 이전에 사용하였던 언어인 **Objective-C**에 대해 간단히 설명하고 넘어가겠습니다. 92 | 93 | **Objective-C**에서는 네임스페이스가 없어 다른 라이브러리, 프레임워크와 이름이 충돌되지 않기 위해 **UI**View, **CG**Rect, **CA**Layer 와 같이 Objective-C 클래스에 접두어를 붙여 고유한 이름을 사용하였습니다. 하지만 **Swift**는 모듈 단위의 네임스페이스를 사용하여 이름 충돌 문제를 해결하였고 때문에 대부분의 경우 모듈 접두사가 필요하지 않습니다. 94 | 95 | #### Swift에서의 모듈과 프레임워크 96 | 97 | - 최상위 레벨의 **네임스페이스**가 바로 **모듈**입니다. 만약 새로 앱을 만들 때 **MyApp** 이라는 이름을 붙이고 최상위 레벨에 `Manny` 클래스를 선언한 경우, 이 클래스의 정확한 이름은 `MyApp.Manny` 가 됩니다. 98 | - **프레임워크** 또한 모듈이며, 하나의 **네임스페이스**라고 할 수 있습니다. 99 | - 예를 들어 코코아의 `Foundation` 프레임워크 내의 `NSString` 클래스는 문자열 선언을 위한 최상위 레벨의 모듈입니다. 파운데이션 프레임워크를 `import`한 경우 `Foundation.NSString` 이 아닌 `NSString` 로 간단하게 클래스를 사용할 수 있습니다. 100 | 101 | 공식문서에 설명된 모듈(module)의 정의: 102 | 103 | > A *module* is a single unit of code distribution—a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s `import` keyword. 104 | > -the swift programming language swift 5.3 105 | 106 | ### 참고 107 | 108 | 1. [Access Control(Modules and Source Files) - the swift programming language swift 5.3](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) 109 | 2. [Package Manager - the swift programming language swift 5.3](https://swift.org/package-manager/#conceptual-overview) 110 | 3. [Namespace - Wikipedia](https://en.wikipedia.org/wiki/Namespace) 111 | 4. 『iOS 9 Programming Fundamentals with Swift(스위프트로 하는 iOS 9 프로그래밍)』, Neuburg&Matt, O'Reilly Media(2015) 112 | 5. [The Power of Namespacing in Swift](https://www.vadimbulavin.com/the-power-of-namespacing-in-swift/) 113 | 114 | ### 인용 115 | 116 | * 스위프트에는 네임스페이스가 암시되어있습니다.* 117 | 118 | > Namespacing is implicit in swift, all classes (etc) are implicitly scoped by the module (Xcode target) they are in. no class prefixes needed. 119 | > [*Chris Lattner*의 네임 스페이스에 대한 트윗](https://twitter.com/clattner_llvm/status/474730716941385729) 120 | > (Chris Lattner는 LLVM과 Clang 컴파일러, **Swift** 프로그래밍 언어의 주 작성자로 가장 잘 알려진 미국의 소프트웨어 엔지니어입니다.) 121 | 122 | -------------------------------------------------------------------------------- /4장_클래스와_인터페이스/resources/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSwiftists/effective-swift/03f4cd8721979a28c7f221663fa33511bd73a902/4장_클래스와_인터페이스/resources/buttons.png -------------------------------------------------------------------------------- /5장_제네릭/item27.md: -------------------------------------------------------------------------------- 1 | # item27. 비검사 경고를 제거하라 2 | 3 | 4 | 5 | ### Java에서는 6 | 7 | 모든 비검사 경고는 런타임에 *ClassCastException*을 일으킬 수 있는 잠재적 가능성을 뜻하기 때문에 최대한 제거하라고 권고합니다. 혹시 경고를 없앨 방법을 찾지 못했다면 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 *@SuppressWarnings("unchecked")* 애너테이션(항상 가능한 한 좁은 범위에 적용)으로 경고를 숨긴 다음 경고를 숨기기로 한 근거를 주석으로 남기는 것이 좋습니다. 8 | 9 | **Java에서 비검사 경고란** 10 | 11 | 여기서 _비검사 경고_는 프로그래머에게 캐스트(cast)가 다른 곳에서 예외(Exception)를 발생시킬 수 있음을 알려주는 컴파일러 경고이며, @SuppressWarnings("unchecked")로 경고를 숨기는 것은 프로그래머가 컴파일러에게 예기치 않은 예외를 발생시키지 않는다고 알리는 의미입니다. 이 경고를 무시하고 실행할 경우 런타임에서 ClassCastException이 발생할 수 있습니다. 12 | 13 | < 비검사 경고 예시 > 14 | 15 | - 예시1: `warning: [unchecked]` 16 | 17 | ```java 18 | Set exaltation = new HashSet(); 19 | 20 | Venery.java:9: warning: [unchecked] unchecked conversion 21 | Set exaltation = new HashSet(); 22 | ^ 23 | required: Set 24 | found: HashSet 25 | ``` 26 | 27 | * 예시2: 이미지 28 | 29 | ![Unchecked cast warning](https://i.stack.imgur.com/zNKeg.png) 30 | 31 | **Java에서 *ClassCastException*이란?** 32 | 33 | ClassCastException은 Java에서 발생하는 런타임 에러로, 클래스를 한 타입에서 다른 타입으로 부적절하게 캐스트하려고 할 때 발생합니다. 34 | 35 | 36 | 37 | 그렇다면 Swift에서 어떻게 대응하고 있을까요? 38 | 39 | ### Swift의 타입캐스팅 40 | 41 | Swift에는 타입캐스트 연산자(Type Cast Operator) `as?` 와 `as!`를 사용하여 부모 클래스를 자식클래스 타입으로 다운캐스팅할 수 있습니다. Swift에서 타입 캐스팅을 할 때 런타임 에러가 발생하는 대표적인 경우는 `as!` 연산자를 사용하여 다운캐스팅을 시도했을 때 입니다. `as!` 연산자의 경우 다운캐스팅이 무조건 성공할 것이라고 확신하는 경우에 사용하며 다운캐스팅이 성공할 경우 옵셔널이 아닌 인스턴스가 반환되고 **실패할 경우 런타임 오류가 발생**합니다. 42 | 43 | 예시와 함께 살펴보면 이렇습니다. 44 | 45 | ```swift 46 | class Coffee { 47 | let name: String 48 | let shot: Int 49 | 50 | var description: String { 51 | return "\(shot) shot(s) \(name)" 52 | } 53 | 54 | init(shot: Int) { 55 | self.shot = shot 56 | self.name = "coffee" 57 | } 58 | } 59 | 60 | class Latte: Coffee { 61 | var flavor: String 62 | 63 | override var description: String { 64 | return "\(shot) shot(s) \(flavor) latte" 65 | } 66 | 67 | func addMilk() { } 68 | 69 | init(flavor: String, shot: Int) { 70 | self.flavor = flavor 71 | super.init(shot: shot) 72 | } 73 | } 74 | 75 | let coffee: Coffee = Coffee(shot: 1) 76 | let myCoffee: Latte = Latte(flavor: "vanilla", shot: 3) 77 | 78 | // Success 79 | let newCoffee: Coffee = myCoffee as! Coffee 80 | //경고 메세지: Forced cast from 'Latte' to 'Coffee' always succeeds; did you mean to use 'as'? 81 | 82 | // 런타임 오류 발생. 강제 다운캐스팅 실패. 83 | let newLatte: Latte = coffee as! Latte 84 | ``` 85 | 86 | 이렇듯 다운캐스팅에 실패할 가능성이 있다면 조건부 연산자인 `as?`를 사용해야 합니다. 조건부 연산자 as?를 사용하면 다운캐스팅에 성공할 경우 옵셔널 타입으로 인스턴스를 반환하며, 실패할 경우 nil을 반환합니다. 87 | 88 | ```swift 89 | guard let newLatte: Latte = coffee as? Latte else { return } 90 | ``` 91 | 92 | 93 | 94 | ### Java vs Swift 95 | 96 | Java는 제네릭을 사용할 때 경고를 통해 타입 불안정성을 알려주지만, Swift는 경고가 아닌 컴파일 오류를 통해 타입 불안전성을 더 확실히 알려줍니다. 덧붙여 Java의 비검사 경고는 잠재적으로 런타임 에러 가능성이 있는 코드를 경고하지만, 이와 달리 Swift의 타입 캐스팅 관련 경고들은 직접적으로 런타임 오류 가능성이 나는 부분은 아니지만 교정해야 하는 부분(ex.`as를 써도 되는 부분에 as? 를 쓴 경우`, `타입이 서로 상관이 없어 타입 캐스팅이 항상 실패하는 경우`)을 알려주는 역할을 합니다. Swift의 타입 캐스팅 관련 경고(warning)들은 런타임 오류 가능성를 내포하지 않습니다. 97 | 98 | 99 | ### 참고 100 | 101 | 1. [Java ClassCastException - Oracle Docs](https://docs.oracle.com/javase/9/docs/api/java/lang/ClassCastException.html) 102 | 2. [Type Casting - The Swift Programming Language](https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html) 103 | 3. 야곰, 『스위프트 프로그래밍 3판』, 한빛미디어(2019) 104 | 4. [Effective Java Generics](https://www.informit.com/articles/article.aspx?p=2861454&seqNum=2) 105 | 5. [What is SuppressWarnings (“unchecked”) in Java?](https://stackoverflow.com/a/48366669) 106 | -------------------------------------------------------------------------------- /5장_제네릭/item28.md: -------------------------------------------------------------------------------- 1 | # item28. 배열보다는 리스트를 사용하라 2 | 3 | > Swift에는 Array만 있는데요? 4 | 5 | 6 | 7 | ## Java 8 | 9 | 배열과 제네릭 타입에는 중요한 차이가 두 가지 있습니다. 첫 번째, 배열은 공변(covariant)입니다. 어려워 보이는 단어지만 뜻은 간단한데요, Sub가 Super의 하위 타입이라면 배열 `Sub[]`는 배열 `Super[]`의 하위 타입이 됩니다.(공변, 즉 함께 변한다는 뜻) 반면, 제네릭은 불공변(invariant)입니다. 즉, 서로 다른 Type1과 Type2가 있을 때, List은 List의 하위 타입도 아니고 상위 타입도 아닙니다. 이것만 보면 제네릭에 문제가 있다고 생각할 수도 있지만, 사실 문제가 있는 건 배열 쪽입니다. 다음은 문법상 허용되는 코드입니다. 10 | 11 | ```java 12 | //런타임에 실패하는 코드 13 | Object[] objectArray = new Long[1]; 14 | objectArray[0] = "타입이 달라 넣을 수 없다"; //ArrayStoreExtension을 던짐 15 | ``` 16 | 17 | 하지만 다음 코드는 문법에 맞지 않습니다. 18 | 19 | ```java 20 | //컴파일이 되지 않는 코드 21 | List objectList = new ArrayList(); //호환되지 않는 타입 22 | objectList.add("타입이 달라 넣을 수 없다"); 23 | ``` 24 | 25 | 어느 쪽이든 Long용 저장소에 String을 넣을 수 없습니다. **다만 배열에서는 그 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있습니다.** 26 | 27 | 두 번째 주요 차이로는 배열은 실체화(reify)됩니다. **배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인합니다.** 그래서 위 코드에서 보듯 Long 배열에 String을 넣으려 하면 `ArrayStoreException`이 발생합니다. 반면, 앞서 이야기했듯 **제네릭은 타입 정보가 런타임에는 소거(erasure)됩니다.** 원소 타입을 컴파일타임에만 검사하며 런타임에는 알 수조차 없다는 뜻입니다. 소거는 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 메커니즘입니다. 28 | 29 | 위의 차이들로 배열과 제네릭은 잘 어우러지지 못합니다. 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없습니다. 즉 코드를 new List\[], new List\[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킵니다. 30 | 31 | 제네릭 배열을 만들지 못하게 막은 이유는 무엇일까요? **타입 안전하지 않기 때문**입니다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 `ClassCastException`이 발생할 수 있습니다. 런타임에 `ClassCastException`이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것입니다. 32 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | ## Swift 40 | 41 | Array와 ArrayList가 별개로 있는 Java에 반해, Swift는 Array 뿐입니다. Java의 Array와 ArrayList의 차이는 다음과 같습니다. 42 | 43 | ```Java 44 | //Array vs ArrayList - Java 45 | 46 | 1. Array는 크기가 고정되어 있지만, ArrayList는 사이즈가 동적인 배열입니다. 47 | 2. Array는 Primitive Type(int, byte, char 등)과 Object 모두를 담을 수 있지만, 48 | ArrayList는 Object Element만 담을 수 있습니다. 49 | 3. Array는 제네릭을 사용할 수 없지만, ArrayList는 타입 안정성을 보장해주는 제네릭을 사용할 수 있습니다. 50 | ``` 51 | 52 | 53 | 54 | Swift의 Array는 Java의 그것과는 다릅니다. 동적 할당 기능을 가지고 있고, 정수부터 문자열, 클래스에 이르기까지 모든 데이터 타입을 저장할 수 있습니다. 주된 차이점 중 하나인 동적 할당 기능에 대해 조금 더 자세히 알아봅시다. 55 | 56 | 57 | 58 | ### Array의 크기 늘리기 59 | 60 | Array가 내부적으로 작동하는 방식에 대해 알아보기 전에, Array의 속성인 `count`와 `capacity`에 대해 알아봅시다. `count`는 배열에 있는 요소의 수를 나타냅니다. `capacity`는 새 메모리를 초과하여 할당하기 전에, 배열에 포함될 수 있는 요소의 총량을 나타냅니다. 61 | 62 | 모든 Array는 내용을 보관하기 위해 특정 양의 메모리를 예약합니다. 배열에 요소를 추가하고, 해당 배열이 예약 된 `capacity`를 초과하기 시작하면 배열은 더 큰 메모리 영역을 할당하고 해당 요소를 새 스토리지에 복사합니다. 63 | 64 | **새 저장소는 이전 저장소 크기의 배수**입니다. 이 기하 급수적 성장 전략은 요소 추가가 일정한 시간에 발생하여 많은 추가 작업의 성능을 평균화한다는 것을 의미합니다. 재할당을 발생시키는 추가 작업에는 성능 비용이 발생하지만 Array가 커질수록 발생 빈도가 점점 줄어 듭니다. 65 | 66 | 67 | 68 | 위의 그림을 봅시다. 용량을 꽉 찬 배열에 "Peach" 를 추가하면 항목이 즉시 추가되지는 않습니다. 다른 곳에 새 메모리가 생성되고 모든 항목이 복사된 후 마지막으로 "Peach"가 배열에 추가됩니다. 메모리의 다른 영역에 새 스토리지를 할당하는 이 방법을 **재할당(Reallocation)** 이라고 합니다. 69 | 70 | 저장해야하는 요소 수를 대략적으로 알고있는 경우 `reserveCapacity(_:)` 메서드를 사용하면 중간에 일어나는 재할당을 방지할 수 있습니다. 하지만 예약된 용량을 초과하도록 요소가 추가된다면 다시 재할당이 일어나게 됩니다. 71 | 72 | 73 | 74 |
75 | 76 | 77 | 78 | ### 참고 79 | 80 | - [Array | Apple Developer Documentation](https://developer.apple.com/documentation/swift/array) 81 | 82 | 83 | - [Array vs ArrayList](https://zorba91.tistory.com/287) 84 | -------------------------------------------------------------------------------- /5장_제네릭/item29.md: -------------------------------------------------------------------------------- 1 | # item 29. 이왕이면 제네릭 타입으로 만들라 2 | 3 | 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편리합니다. 따라서 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하는 것이 좋고 그러기 위해 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하는 것을 권장하고 있습니다. 4 | 5 | ### 제네릭의 작동 방식 6 | 7 | ```swift 8 | func min(_ x: T, _ y: T) -> T { 9 | return y < x ? y : x 10 | } 11 | ``` 12 | 13 | 컴파일러에는 이 함수를 위해 코드를 보내는데 필요한 두 가지 필수 정보인 1. _T타입 변수의 사이즈_ 와 2. _런타임에 호출해야하는_ `<` 메서드의 특정 오버로드 주소가 없습니다. 14 | 15 | 컴파일러가 제네릭 타입을 가진 값(value)을 발견할 때마다 해당 값을 컨테이너에 보관합니다. 이 컨테이너는 값을 저장할 수 있는 고정 크기가 있습니다. 값이 너무 크면 Swift는 힙(heap)에 할당하고 해당 컨테이너에 대한 참조(주소값)를 저장합니다. 16 | 17 | 컴파일러는 또한 제네릭 타입 매개 변수당 하나 이상의 감시 테이블(witness table)의 리스트를 유지 관리합니다. 하나는 값 감시 테이블(value witness table)이고, 또 하나는 해당 타입의 각 프로토콜 제약 조건에 대한 프로토콜 감시 테이블(protocol witness table)입니다. 감시 테이블은 런타임에 올바른 구현에 대한 함수 호출을 동적으로 디스패치하는 데 사용됩니다.(The witness tables are used to dynamically dispatch function calls to the correct implementations at runtime.) 18 | 19 | 이 내용에 대해 더 자세한 내용은 '참고 - Witness Table에 대해 참고한 자료'에 있는 자료들을 참고해주세요. 20 | 21 | ### 제네릭 타입으로 해결 가능한 문제 22 | 23 | 제네릭 타입을 사용하는 대표적인 예는 Stack(스택)입니다. 일반적인 스택의 특징은 아래와 같습니다. 24 | 25 | * 스택의 특징 26 | 1. 스택의 요소로 한 타입을 지정해주면 그 타입으로 계속 스택이 동작 27 | 2. 처음 지정해주는 타입은 스택을 사용하고자 프로그래머가 지정할 수 있음. 28 | 스택의 인스턴스를 생성할 때 실제로 어떤 타입을 사용할지 명시. 29 | 30 | 제네릭 타입을 사용했을 때와 사용하지 않았을 때를 비교하여 살펴보겠습니다. 31 | 32 | < 제네릭 타입을 사용하지 않았을 경우 > 33 | 34 | 타입에 따른 Stack을 일일이 구현해줘야 합니다. 35 | 36 | ```swift 37 | struct IntStack { 38 | var items = [Int]() 39 | mutating func push(_ item: Int) { 40 | items.append(item) 41 | } 42 | mutating func pop() -> Int { 43 | return items.removeLast() 44 | } 45 | } 46 | 47 | // 사용 예 48 | var integerStack: IntStack = IntStack() 49 | integerStack.push(3) 50 | print(integerStack.items) // [3] 51 | integerStack.pop() 52 | print(integerStack.items) // [] 53 | ``` 54 | 55 | <. 제네릭 타입을 사용했을 경우 > 56 | 57 | 하지만 제네릭 타입을 사용하여 Stack을 구현했을 경우 모든 타입을 대상으로 동작할 수 있기 때문에 타입별 Stack을 일일이 구현할 필요가 없습니다. 58 | 59 | ```swift 60 | struct Stack { 61 | var items = [Element]() 62 | mutating func push(_ item: Element) { 63 | items.append(item) 64 | } 65 | mutating func pop() -> Element { 66 | return items.removeLast() 67 | } 68 | } 69 | 70 | // 사용 예 71 | var doubleStack: Stack = Stack() 72 | doubleStack.push(1.0) 73 | print(doubleStack.items) // [1.0] 74 | doubleStack.pop() 75 | print(doubleStack.items) // [] 76 | 77 | var stringStack: Stack = Stack() 78 | stringStack.push("Effective Swift") 79 | print(stringStack.items) // ["Effective Swift"] 80 | stringStack.pop() 81 | print(stringStack.items) // [] 82 | 83 | // Any 타입을 사용하면 요소로 모든 타입을 수용할 수 있습니다. 84 | var anyStack: Stack = Stack() 85 | anyStack.push("Effective Swift") 86 | print(anyStack.items) // ["Effective Swift"] 87 | anyStack.push(3.0) 88 | print(anyStack.items) // ["Effective Swift", 3.0] 89 | anyStack.pop() 90 | print(anyStack.items) // ["Effective Swift"] 91 | ``` 92 | 93 | 이렇게 제네릭 타입을 사용하면 **훨씬 유연하고 광범위**하게 사용할 수 있습니다. 또한 Element의 타입을 정해주면 그 타입에만 동작하도록 제한할 수도 있기 때문에 프로그래머가 의도한 대로 기능을 사용하도록 유도할 수 있습니다. 94 | 95 | ### 타입 제약 (Type Constraints) 96 | 97 | 제네릭 타입은 타입의 제약 없이 사용할 수 있지만, 때로는 아래와 같이 타입 제약이 필요한 상황이 있을 수 있습니다. 98 | 99 | - 제네릭 함수가 처리해야 할 기능이 특정 타입에 한정되어야만 처리할 수 있는 경우 100 | Ex) 아래 '타입 제약 예시'의 `substractTwoValue(_: _:)` 뺄셈 함수의 경우, 101 | _뺄셈 연산자를 사용할 수 있는 타입_ 이어야 하기 때문에 매개변수를 BinaryInteger 프로토콜을 준수하는 타입으로 한정. 102 | 103 | - 제네릭 타입을 특정 프로토콜을 따르는 타입만 사용할 수 있도록 제약을 두어야 하는 경우 104 | 105 | 등 106 | 107 | 이런 경우, 타입 제약을 사용하여 타입 매개변수가 가져야 할 제약사항을 지정할 수 있습니다. 이때, 타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다. 108 | 109 | <타입 제약 예시> 110 | 111 | ```swift 112 | // Dictionary의 키는 Hashable 프로토콜을 준수하는 타입으로만 사용 가능 113 | public struct Dictionary : Collection, ExpressibleByDictionaryLiteral { /* 상세 구현부 생략 */ }> 114 | 115 | // T를 뺼셈 연산자를 사용할 수 있는 BinaryInteger 타입으로 제한 116 | func substractTwoValue(_ a: T, _ b: T) -> T { 117 | return a - b 118 | } 119 | 120 | // 여러 제약을 추가하고 싶을 때 - where 절 사용 121 | // T를 BinaryInteger 프로토콜을 준수하고 FloatingPoint 프로토콜도 준수하는 타입으로 제약 122 | func swapTwoValues(_ a: inout T, _ b: inout T) where T: FloatingPoint { /* 상세 구현부 생략 */ } 123 | 124 | ``` 125 | 126 | 127 | 128 | ### Any vs Generic 129 | 130 | Swift에서는 불특정 타입(nonspecific types) 작업을 위해 제공하는 두 가지 특수 타입(Any, AnyObject) 중 하나로 `Any`는 함수 타입을 포함해 모든 타입의 인스턴스를 나타낼 수 있습니다. 위의 '제네릭 타입으로 해결 가능한 문제 - <제네릭 타입으로 해결 가능한 문제>의 `anyStack`'에서 알 수 있듯이 **Any**는 타입을 고정하지 않고 계속해서 변경할 수 있습니다. **Any**로 선언된 변수의 값을 가져다 쓰려면 매번 타입 확인 및 변환을 해줘야 하기 때문에 불편할 뿐더러 예기치 못한 오류의 위험을 증가시키기 때문에 사용을 지양해야 합니다. 때문에 타입 안전성을 중요시하는 프로그래밍 언어인 Swift에서는 **Any**는 될 수 있으면 사용하지 않는 것을 권장하고 있습니다. 131 | 132 | 반면 **Generic**은 제네릭을 사용하여 구현한 타입(struct, class 등)의 인스턴스를 생성할 때 실제로 어떤 타입을 사용할지 지정해준 이후로 Any와 같이 변경할 수 없습니다. 133 | 134 |
135 | 136 | 번외로 연관 타입에 대해서도 조사해봤습니다. 137 | 138 | ### 연관 타입 (Accociated Type) 139 | 140 | 위에서 알아본 제네릭을 정리하자면, 어떤 타입이 들어올지 모를때 타입 매개변수를 통해 종류는 알 수 없지만 어떤 타입이 여기에 쓰일 것이라는 걸 표시해주는 것을 의미합니다. 타입 매개변수의 이러한 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능이 바로 연관타입입니다. 141 | 142 | <연관 타입 예시> 143 | 144 | ``` swift 145 | protocol Container { 146 | associatedtype Item // 연관 타입 147 | mutating func append(_ item: Item) 148 | var count: Int { get } 149 | subscript(i: Int) -> Item { get } 150 | } 151 | 152 | struct IntStack: Container { 153 | typealias Item = Int // 🙌🏻 154 | // original IntStack implementation 155 | var items = [Int]() 156 | mutating func push(_ item: Int) { 157 | items.append(item) 158 | } 159 | mutating func pop() -> Int { 160 | return items.removeLast() 161 | } 162 | // conformance to the Container protocol 163 | mutating func append(_ item: Int) { 164 | self.push(item) 165 | } 166 | var count: Int { 167 | return items.count 168 | } 169 | subscript(i: Int) -> Int { 170 | return items[i] 171 | } 172 | } 173 | 174 | ``` 175 | 176 | `IntStack`에서 `typealias Item = Int` (🙌🏻 표시) 는 Container 프로토콜의 구현을 위해 추상 타입(abstract type) 이었던 items를 구체 타입(concrete type) Int로 바꿉니다. 177 | 178 | ### 참고 179 | 180 | * [Generic - The Swift Programming Language](https://docs.swift.org/swift-book/LanguageGuide/Generics.html#ID184) 181 | * [Power of Swift Generics — Part 1](https://medium.com/swift-india/power-of-swift-generics-part-1-ab722a030dc2) 182 | * [The Swift Programming Language Swift 5.3 - Type Casting for Any and AnyObject](https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html#ID342) 183 | * Witness Table에 대해 참고한 자료 184 | 1. [WWDC2015 Understanding Swift Performance - Protocol Witness Table](https://developer.apple.com/videos/play/wwdc2016-416/?time=1570) 185 | 2. [WWDC2015 Understanding Swift Performance - Value Witness Table](https://developer.apple.com/videos/play/wwdc2016/416/?time=1681) 186 | 3. [Understanding method dispatch in Swift](https://heartbeat.fritz.ai/understanding-method-dispatch-in-swift-684801e718bc) 187 | 4. [Apple Github - Type Layout](https://github.com/apple/swift/blob/main/docs/ABI/TypeLayout.rst) 188 | 5. [Why does Swift need witness tables?](https://softwareengineering.stackexchange.com/a/331993) 189 | 190 | -------------------------------------------------------------------------------- /5장_제네릭/item30.md: -------------------------------------------------------------------------------- 1 | # item30. 이왕이면 제네릭 메서드로 만들라 2 | 3 | ### 개요 4 | 제네릭 메서드에 대해서 알아봅니다. 5 | 6 |
7 | 8 | ### Swift에서 Generic 메서드 만들어보기 9 | Java와 선언하는 방식이나 개념이 크게 다르지 않습니다. 10 | Swift에서는 이렇게 정의하고 있습니다. 11 | > 어떠한 타입과도 같이 사용할 수 있는 유연하고, 재사용 가능한 함수와 타입을 작성할 수 있습니다. 12 | 13 | Java에서는 `Collections` 내부의 일부 메서드들(`sort`, `binarySearch`)등은 제네릭 메서드로 되어있고, Swift에서는 기본 제공 클래스 중`Array`, `Dictionary`가 제네릭 콜렉션입니다. 14 | 15 | 그 중 `Array`를 살펴보면 타입이 `Array`이고, `append`, `shuffle` 등과 같은 메서드가 제네릭 메소드로 선언되어 있습니다. 16 | 17 |
18 | 19 | #### 제네릭 메서드 예제 20 | 제네릭 메서드는 아래 예제 코드들을 통해 설명드리겠습니다. 21 | 22 | ```swift 23 | func swapTwoInts(_ a: inout Int, _ b: inout Int) { 24 | let temporaryA = a 25 | a = b 26 | b = temporaryA 27 | } 28 | 29 | var someInt = 3 30 | var anotherInt = 107 31 | swapTwoInts(&someInt, &anotherInt) 32 | ``` 33 | 이렇게 `Int`타입인 두 값(someInt, anotherInt)을 바꿔주는 메서드가 있습니다. 34 | 35 | 유용한 함수긴 하지만 매개변수로 `Int`타입인 값밖에 사용할 수가 없습니다. 혹여 다른 타입을 사용하려고 하면 똑같은 함수를 타입만 바꿔서 만들어야 할것입니다. 36 | 37 | ```swift 38 | func swapTwoDoubles(_ a: inout Double, _ b: inout Double) { 39 | let temporaryA = a 40 | a = b 41 | b = temporaryA 42 | } 43 | 44 | var someDouble = 6.0 45 | var anotherDouble = 99.0 46 | swapTwoDoubles(&someDouble, &anotherDouble) 47 | ``` 48 | 49 | 이렇게 Double타입인 두 값을 바꿔주는 메서드를 만들어 보았습니다. 50 | 그럼 String이나 다른 타입을 바꿔주고 싶으면 메서드를 또 만들어야 할까요?🧐 51 | 52 |
53 | 54 | 위와 아래의 메서드를 보시면 아시겠지만 똑같은 로직을 가지고 있고 다른점은 오직 받아들이는 값의 타입입니다. 55 | 56 | 그럼 이제는 제네릭 메서드를 만들어 어떤 타입의 두 값이라도 바꿔줄 수 있는 메서드를 작성해보겠습니다. 57 | 58 | ```swift 59 | func swapTwoValues(_ a: inout T, _ b: inout T) { 60 | let temporaryA = a 61 | a = b 62 | b = temporaryA 63 | } 64 | ``` 65 | 66 | 이렇게 메서드명 옆에 자리표시용 타입인 ``를 붙여 Swift에게 제네릭 메서드임을 알려주게 됩니다. 67 | `T`의 자리에 사용될 실제 타입은` swapTwoValues(_:_:)` 함수를 호출하는 매 순간 결정됩니다. 68 | 69 |
70 | 71 | #### 타입 매개 변수 72 | 위에서 보았던 `T`는 타입 매개 변수의 한 가지 예입니다. 73 | 타입 매개 변수는 자리 표시용 타입을 지정하고(함수이름 바로 뒤에`` 붙임) 이름을 정하는 것입니다. 74 | 75 | 타입 매개 변수를 지정하게 되면, 이를 사용해서 매개 변수의 타입을 정의하거나, 반환 타입을 지정할 수 있습니다. 76 | 77 | 하나 이상의 타입 매개 변수를 제공하려면 `<>`안에 쉼표로 구분하여 작성할 수 도 있습니다. 78 | 예를 들면 `Dictionary`가 되겠네요. 79 | 80 |
81 | 82 | #### 타입 매개 변수 이름짓기 83 | 대부분의 경우 `descriptive name`을 가지게 되는데 위에 언급되었던 `Dictionary`의 `Key`, `Value` 및 `Array`의 `Element`를 예를 들 수 있겠습니다. 84 | 85 | 이처럼 타입 매개 변수와 이것을 사용하는 제네릭 타입/메서드 사이의 관계에 대해서 말해주지만, `swapTwoValues(_:_:)`처럼 의미있는 관계가 아닐때는 `T`, `U`와 같은 단일 문자 + Upper camel case를 사용하여 이름을 짓는 것이 전통적이라고 설명하고 있습니다. 86 | 87 |
88 | 89 | #### 타입 제약(Type Constraints) 90 | Java의 경우 제네릭 타입에 대한 제약조건이 3가지 정도로 비교적 간단한 편입니다. 91 | ```Java 92 | // 1. 제약 없음 93 | public void someFunction() { 94 | // code 95 | } 96 | 97 | // 2. T는 SomeClass를 상속받는 클래스만 가능. 98 | public void someFunction() { 99 | // code 100 | } 101 | 102 | // 3. T는 SomeClass의 상위 클래스들만 가능. 103 | public void someFunction() { 104 | // code 105 | } 106 | ``` 107 | 108 | Swift 또한 제네릭 타입에 대해 제약을 둘 수 있습니다. Java 처럼 특정 클래스만을 제약할 수도 있고 프로토콜을 지정해서 제약을 둘 수 있습니다. 109 | ```Swift 110 | // T는 SomeClass의 하위 클래스만 가능, U는 SomeProtocol을 채택하는 타입만 가능 111 | func someFunction(someT: T, someU: U) { 112 | // code 113 | } 114 | ``` 115 | 116 |
117 | 118 | ### 참고한 곳 119 | - https://docs.swift.org/swift-book/LanguageGuide/Generics.html 120 | - http://xho95.github.io/swift/language/grammar/generic/2020/02/29/Generics.html 121 | - https://zeddios.tistory.com/226 122 | -------------------------------------------------------------------------------- /6장_열거_타입과_애너테이션/item35.md: -------------------------------------------------------------------------------- 1 | # Item35. ordinal 메서드 대신 인스턴스 필드를 사용하라 2 | 3 | 자바의 경우, 대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응됩니다. 그리고 모든 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal이라는 메서드를 제공합니다. 4 | 5 | ## Java 6 | 7 | ### ordinal를 잘못 사용한 경우 8 | 9 | 코드 35-1 ordinal을 잘못 사용한 예 - 따라하지 말것! 10 | ```java 11 | public enum Ensemble { 12 | SOLO, DUET, TRIO, QUARTET, QUINTET, 13 | SEXTET, SEPTET, OCTET, NONET, DECTET; 14 | 15 | public int numberOfMusicians() { return ordinal() + 1; } 16 | } 17 | ``` 18 | 19 | => 동작은 하지만 유지보수하기가 끔찍한 코드입니다. 상수 선언 순서를 바꾸는 순간 `numberOfMusicians`가 오동작하며 이미 사용중인 정수와 값이 같은 상수는 추가할 방법이 없습니다. 또한 값을 중간에 비워둘 수도 없습니다. 20 | 21 | ### ordinal를 대체하는 해결책 22 | 23 | 해결책은 간단합니다. **열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고, 인스턴스 필드에 저장하면 됩니다.** 24 | 25 | ```java 26 | public enum Ensemble { 27 | SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), 28 | SEXTET(6), SEPTET(7), OCTET(8), DOUBlE_QUARTET(8), 29 | NONET(9), DECTET(10), TRIPLE_QUARTET(12); 30 | 31 | private final int numberOfMusicians; 32 | Ensemble(int size) { this.numberOfMusicians = size; } 33 | public int numberOfMusicians() { return numberOfMusicians; } 34 | } 35 | ``` 36 | 37 | ### ordinal를 잘 사용한 예 38 | 39 | EnumSet, EnumMap 의 key로 사용하는 경우입니다. 이런 성격의 목적에만 ordinal()를 사용하도록 합니다. 40 | 41 | ```java 42 | import java.util.*; 43 | 44 | enum days { 45 | SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY 46 | } 47 | 48 | public class EnumSetExample { 49 | public static void main(String[] args) { 50 | Set set = EnumSet.of(days.TUESDAY, days.WEDNESDAY); 51 | // Traversing elements 52 | Iterator iter = set.iterator(); 53 | while (iter.hasNext()) 54 | System.out.println(iter.next()); 55 | } 56 | } 57 | ``` 58 | 59 | ## Swift 60 | 61 | * Swift의 rawValue는 값을 채택해야만 얻을 수 있습니다. (ex. Int, String) 62 | 63 | ### 잘못된 쓰임새: Int - rawValue 사용하기 64 | 65 | * 자바 Enum의 ordinal()과 마찬가지로 스위프트에서는 Enum의 rawValue가 있습니다. 66 | Int로 rawValue를 갖고 값을 명시하지 않으면 자바의 ordinal()과 같은 효과를 볼 수 있습니다. 67 | 68 | ```Swift 69 | enum Ensemble: Int { 70 | case solo 71 | case duet 72 | case trio 73 | case quartet 74 | case quintet 75 | case sextet 76 | case septet 77 | case octet 78 | case nonet 79 | case dectet 80 | 81 | var numberOfMuscians: Int { 82 | return rawValue + 1 83 | } 84 | } 85 | ``` 86 | => 자바의 ordinal()과 마찬가지로, 동작은 하지만 유지보수하기가 끔찍한 코드입니다. 상수 선언 순서를 바꾸는 순간 `numberOfMusicians`가 오동작하며 이미 사용중인 정수와 값이 같은 상수는 추가할 방법이 없습니다. 또한 값을 중간에 비워둘 수도 없습니다. 87 | 88 | > rawValue에 명시적으로 값 대입하여 사용하기 89 | 90 | * 명시적으로 rawValue에 값을 대입하면 위와 같은 상황이 해결될 것처럼 보입니다. 상수 선언 순서에도 의존되지 않고, 중간에 값을 비워도 되죠. 91 | 그러나 값이 중복이 될때는 rawValue의 명시적 사용도 해결책이 될 순 없습니다. 92 | 왜냐하면 rawValue는 unique 해야 되기 때문에 OCTET(=8), DOUBLE_QUARTET(=8) 처럼 값이 중복이 되는 경우에는 rawValue를 사용할 수 없기 때문입니다. 93 | 그리고 numberOfMusicians 값을 rawValue의 값으로 할당해도 되는 대표 값인지 근거가 없기 때문에 사용하는 것은 부적절해 보입니다. 94 | 95 | ```Swift 96 | enum Ensemble: Int, CaseIterable { 97 | case solo = 1 98 | case duet = 2 99 | case trio = 3 100 | case quartet = 4 101 | case quintet = 5 102 | case sextet = 6 103 | case septet = 7 104 | case octet = 8 105 | case doubleQuartet = 8 // 컴파일 에러! => Raw value for enum case is not unique 106 | case nonet = 9 107 | case dectet = 10 108 | case tripleQuartet = 12 109 | 110 | var numberOfMusicians: Int { 111 | return rawValue 112 | } 113 | } 114 | ``` 115 | 116 | ### rawValue를 대체하는 해결책: 연산프로퍼티(or 메서드) + default 구문 없는 switch 사용하기 117 | 118 | * Swift 에는 저장 프로퍼티를 둘 수 없고, 오직 연산 프로퍼티나 메서드만이 가능합니다. 따라서 rawValue의 대안책으로는 연산프로퍼티(or 메소드) + switch 방법입니다. 119 | * Swift의 switch는 **exhaustive** 해서 default를 쓰지 않는한 값을 추가할 때 switch 컴파일 오류를 통해 case를 추가해야 함을 알 수 있습니다. 120 | 121 | ```Swift 122 | enum Ensemble: Int { 123 | case solo, duet, trio, quartet, quintet, sextet, septet, octet, doubleQuartet, nonet, dectet, tripleQuartet 124 | 125 | var numberOfMuscians: Int { 126 | switch self { 127 | case .solo: 128 | return 1 129 | case .duet: 130 | return 2 131 | case .trio: 132 | return 3 133 | case .quartet: 134 | return 4 135 | case .quintet: 136 | return 5 137 | case .sextet: 138 | return 6 139 | case .septet: 140 | return 7 141 | case .octet: 142 | return 8 143 | case .doubleQuartet: 144 | return 8 145 | case .nonet: 146 | return 9 147 | case .dectet: 148 | return 10 149 | case .tripleQuartet: 150 | return 12 151 | } 152 | } 153 | } 154 | ``` 155 | => 하지만 이 방식은 연산 프로퍼티가 하나씩 추가될 때마다 위처럼 계속 나열해야하는 단점이 있습니다. 156 | 157 | ### rawValue를 대체하는 해결책2: 구조체 + 중첩 열거형 + private init 158 | 159 | 또 다른 방법으로는 구조체(struct)와 열거 타입(enum)을 혼합해서 사용하는 방법이 있습니다. 160 | * 먼저 구조체(`Ensemble`)로 선언합니다. 따라서 **저장 프로퍼티를 가질 수 있으므로, numberOfMusians를 프로퍼티로 갖게 할 수 있습니다.** 161 | * 그리고 중첩 열거 타입(`Kind`)을 갖게 합니다. 위의 예시와 똑같이 **Ensemble의 모든 case를 갖게 할 수 있습니다.** 162 | * 마지막으로 **init을 private**하게 두어, 외부에서는 생성할 수 없게 합니다. 그러면 **정적 상수(`static let`)로서** 선언할 수 밖에 없습니다. 163 | * 결과적으로, 자바 Enum의 인스턴스 필드의 경우처럼 **중간에 값을 비우거나 끼워 넣을 수 있고, numberOfMusicians의 중복이 가능**합니다. 164 | 165 | ```swift 166 | struct Ensemble { 167 | enum Kind { 168 | case solo, duet, trio, quartet, quintet, sextet, septet, octet, doubleQuartet, nonet, dectet, tripleQuartet 169 | } 170 | 171 | let kind: Kind 172 | let numberOfMusicians: Int 173 | 174 | private init(kind: Kind, numberOfMusicians: Int) { 175 | self.kind = kind 176 | self.numberOfMusicians = numberOfMusicians 177 | } 178 | 179 | static let solo = Ensemble(kind: .solo, numberOfMusicians: 1) 180 | static let duet = Ensemble(kind: .duet, numberOfMusicians: 2) 181 | static let trio = Ensemble(kind: .trio, numberOfMusicians: 3) 182 | static let quartet = Ensemble(kind: .quartet, numberOfMusicians: 4) 183 | //... 생략 184 | } 185 | 186 | var ensemble: Ensemble = .solo 187 | print(ensemble.numberOfMusicians) // 1 188 | 189 | ensemble = .octet 190 | print(ensemble.numberOfMusicians) // 8 191 | 192 | ensemble = .doubleQuartet 193 | print(ensemble.numberOfMusicians) // 8 194 | // numberOfMusicians의 값으로 중복이 가능합니다. 195 | ``` 196 | => `Kind`는 예시로 든 이름일 뿐이지, 꼭 따르지 않아도 됩니다. struct 이름으로 `Choir`, 중첩 enum 이름으로 `Ensemble`도 어울립니다. 197 | 198 | * 또 struct 임에도 불구하고, 외부에서 분기처리할 때 중첩 열거 타입(`Kind`)를 사용하면 일반 열거형처럼 분기처리할 수 있습니다. 199 | 200 | ```Swift 201 | // 외부에서 사용하는 경우 202 | func foo(ensemble: Ensemble) { 203 | switch ensemble.kind { 204 | case .solo: 205 | break 206 | case .duet: 207 | break 208 | case .trio: 209 | break 210 | //... 생략 211 | } 212 | ``` 213 | 214 | * private init 으로 생성을 제한하고 상수로만 선언했기 때문에, 아래 스크릿샷처럼 xcode에서 마치 enum인 것처럼 **타입을 제안**받을 수 있습니다. 215 | 216 | 스크린샷 2021-02-12 오후 10 09 10 217 | 218 | ## 번외: enum 상수들의 총 개수가 필요할때 CaseIterable을 채택해라 219 | 220 | * 과거 c언어에서는 enum의 모든 case의 개수를 구하기 위해 다음과 같이 코드를 작성하곤 했습니다. 221 | 222 | ```c 223 | enum MyType { 224 | Type1, // 0 225 | Type2, // 1 226 | Type3, // 2 227 | NumberOfTypes // 3 == all count 228 | } 229 | ``` 230 | => 마지막 `NumberOfTypes`가 3이 되어 저절로 모든 case의 총 개수로 이용될 수 있습니다. 하지만 이 방법은 `NumberOfTypes`가 무엇을 뜻하는지 팀원들끼리 공유해야 하고, 이후 코드를 잘못 건드릴 경우 총 개수로서 작동하지 않을 수 있는 위험이 있습니다. 231 | 232 | * 하지만 Swift 에는 CaseIterable을 채택만 하면 위와 같은 처리를 하지 않아도 안전하게 모든 case의 총 개수를 구할 수 있습니다. 233 | 234 | ```Swift 235 | enum MyType: CaseIterable { 236 | case type1 237 | case type2 238 | case type3 239 | 240 | static var count: Int { 241 | return allCases.count 242 | } 243 | } 244 | ``` 245 | 246 | ## References 247 | 248 | https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html 249 | -------------------------------------------------------------------------------- /6장_열거_타입과_애너테이션/item40.md: -------------------------------------------------------------------------------- 1 | # item40. @Override 애너테이션을 일관되게 사용하라 2 | 3 | > override 키워드를 붙여야 하는 이유 4 | 5 |
6 | 7 | ## Java 8 | 9 | `@Override` 애너테이션은 가장 자주 사용되는 애너테이션 중 하나일 것입니다. `@Override` 애너테이션은 메서드 선언에만 달 수 있으며, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했음을 뜻합니다. 이 애너테이션을 일관되게 사용하면 여러가지 버그들을 예방해줍니다. 다음의 Bigram 프로그램을 함께 살펴봅시다. 이 클래스는 바이그램, 즉 영어 알파벳 2개로 구성된 문자열을 표현합니다. 10 | 11 | ```java 12 | // 영어 알파벳 2개로 구성된 문자열을 표현하는 클래스 - 버그를 찾아봅시다 13 | public class Bigram { 14 | private final char first; 15 | private final char second; 16 | 17 | pulbic Bigram(char first, char second) { 18 | this.first = first; 19 | this.second = second; 20 | } 21 | public boolean equals(Bigram b) { 22 | return b.first == first && b.second == second; 23 | } 24 | public int hashCode() { 25 | return 31 * first + second; 26 | } 27 | 28 | public static void main(String[] args) { 29 | Set s = new HashSet<>(); 30 | for(int i = 0; i < 10; i++) { 31 | for(char ch = 'a'; ch <= 'z'; ch++) 32 | s. add(new Bigram(ch, ch)); 33 | System.out.println(n.size()); 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 40 | 41 | `main` 메서드를 보면 똑같은 소문자 2개로 구성된 바이그램 26개를 10번 반복해 집합에 추가한 다음, 그 집합의 크기를 출력합니다. Set은 중복을 허용하지 않으니 26이 출력될 것 같지만, 실제로는 260이 출력됩니다. 무엇인가 잘못되었군요. 42 | 43 | 확실히 Bigram 작성자는 `equals` 메서드를 재정의하려 한 것으로 보이고 `hashCode`를 재정의해야 한다는 사실도 잊지 않았습니다. 그러나 안타깝게도 `equals`를 재정의(overriding) 한 게 아니라 다중정의(overloading) 해버렸습니다. `Object`의 `equals`를 재정의하려면 매개변수 타입을 `Object`로 해야만 하는데, 그렇게 하지 않은 것입니다. 그래서 `Object`에서 상속한 `equals`와는 별개인 `equals`를 새로 정의한 꼴이 되었습니다. 44 | 45 | 이러한 오류를 컴파일러가 찾아주도록 하기 위해선 `@Override` 애너테이션을 붙여 재정의한다는 의도를 명시해야 합니다. 46 | 47 | ```java 48 | @Override public boolean equals(Bigram b) { 49 | return b.first == first && b.second == second; 50 | } 51 | ``` 52 | 53 | 그럼 컴파일 오류가 발생해 수정이 필요한 부분들을 지적해 개발자로 하여금 수정할 수 있게 합니다. 그러므로 상위 클래스의 메서드를 재정의하려는 모든 메서드에 `@Override` 애너테이션을 달아야 합니다. 54 | 55 | 56 | 57 |
58 | 59 | ## Swift 60 | 61 | Swift에서도 비슷한 상황이 발생할 수 있습니다. 62 | 63 | ```swift 64 | class SuperClass { 65 | func method(param: String) { 66 | print(param) 67 | } 68 | } 69 | 70 | class SubClass: SuperClass { 71 | func method(param: Int) { 72 | print(param) 73 | } 74 | } 75 | 76 | let sub = SubClass() 77 | sub.method(param: "str") //str 78 | ``` 79 | 80 | SubClass가 SuperClass를 상속받고 있고 타입만 다른 메서드를 재정의하려고 합니다. 하지만 다른 타입의 파라미터를 지정함으로 재정의(overriding)된 것이 아니라 다중정의(overloading)되어버렸습니다. 81 | 82 | 83 | 84 | 이를 방지하기 위해서는 재정의하려는 메소드 앞에 `override` 키워드를 추가합니다. 이렇게 하면 재정의하려는 의도를 명확히 할 수 있으며 예기치 않은 오동작을 방지할 수 있습니다. `override` 키워드가 없는 재정의는 코드가 컴파일 될 때 오류로 진단됩니다. 또한 `override` 키워드를 붙임으로 재정의하는 클래스의 수퍼클래스 중 재정의를 위한 선언과 일치하는 선언이 있는지 Swift 컴파일러가 확인하도록 합니다. 85 | 86 | 87 | 88 |
89 | 90 | 91 | 92 | ### 참고 93 | 94 | - [Swift Override](https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html#ID196) 95 | -------------------------------------------------------------------------------- /6장_열거_타입과_애너테이션/item41.md: -------------------------------------------------------------------------------- 1 | # item. 41 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 2 | 3 | 4 | 5 | ## 목차 6 | 7 | * 마커 인터페이스란? 8 | * 마커 어노테이션 VS 마커 인터페이스 9 | * 정리 10 | 11 | 12 | 13 | 14 | 15 | ## 마커 인터페이스란? 16 | 17 | 책에서 정의하기로는 18 | 19 | **아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스** 라고 정의합니다. 20 | 21 | 22 | 23 | ### 마커 인터페이스의 목적 24 | 25 | 여기서 말하는 **마커 인터페이스** 는 디자인 패턴의 한 종류인 **마커 인터페이스 패턴** 으로 여겨질 수 있습니다. 26 | 27 | 이런 **마커 패턴**을 사용하는 이유는 크게 두 가지로 볼 수 있습니다. 28 | 29 | 1. 객체에 대한 정보 구체화 30 | 2. 객체의 런타임에 대한 정보 제공 31 | 32 |
33 | 34 | 35 | 36 | #### 1. 객체에 대한 정보 구체화 37 | 38 | `Java` 에서는 39 | 40 | ```java 41 | public interface Describable { } 42 | 43 | public class Student implements Describable { 44 | private String name; 45 | private int age; 46 | 47 | public Student (String name, int age) { 48 | this.name = name; 49 | this.age = age; 50 | } 51 | 52 | public void printDescription() { 53 | System.out.println(this.name + "'s" + "age is " + this.age); 54 | } 55 | } 56 | ``` 57 | 58 | 위와 같이 빈 `interface`를 만들어서 객체의 특성을 구체화 할 수 있습니다. 59 | 60 |
61 | 62 | 개발 초기에는 **`Serializable`**, **`Cloneable`** 와 같은 표현을 하기 위해 사용되었다고 합니다. 63 | 64 | **Swfit** 에서는 **`Codable`**, **`Decodable`**, **`Encodable`** 등이 **마커 인터페이스 패턴** 형태로 사용되곤 합니다. 65 | 66 |
67 | 68 | #### 2. 객체의 런타임에 대한 정보 제공 69 | 70 | 위에 1번 **객체에 대한 정보 구체화** 가 코드의 의미적인 측면에서의 용도로 사용되었다면 71 | 72 | 2번은 런타임에 객체 타입을 구체화하기 위해 사용됩니다. 73 | 74 |
75 | 76 | ```swift 77 | protocol Encodable {} 78 | protocol Decodable {} 79 | typealias Codable = Encodable & Decodable 80 | 81 | struct Student: Codable { 82 | private name: String 83 | private age: Int 84 | } 85 | 86 | open class JSONEncoder { 87 | public struct OutputFormatting : OptionSet { ... } 88 | 89 | public init() {} 90 | open var outputFormatting: JSONEncoder.OutputFormatting 91 | open func encode(_ value: T) throws -> Data where T : Encodable 92 | } 93 | 94 | 95 | func getStudentData(_from student: Student) -> Data? { 96 | let encoder: JSONEncoder = JSONEncoder() 97 | return encoder.encode(student) 98 | } 99 | // 위와 같이 Student를 직접 매개변수의 타입으로 사용하여 메소드를 만들 수도 있지만, 100 | 101 | func getData(_from object: Codable) -> Data? { 102 | let encoder: JSONEncoder = JSONEncoder() 103 | return encoder.encode(object) 104 | } 105 | // 위 처럼 Codable 이라는 마커를 이용해서 확장성을 더 넓힐 수도 있습니다. 106 | 107 | let gwonii: Student = .init(name: "gwonii", age: 28) 108 | let jason: Adult = .init(name: "jason", age: 28, maritalStatus: .single) 109 | // getData 메소드를 이용한다면 두 객체의 JSON 데이터를 얻을 수 있습니다. 110 | ``` 111 | 112 | > `Swift` 에서는 `protocol `을 이용해서 **마커 인터페이스 패턴** 을 구현합니다. 113 | 114 | 115 | 116 | ## 마커 어노테이션 VS 마커 인터페이스 117 | 118 | * **마커 어노테이션** 의 어노테이션으로써 다양한 기능들을 사용할 수 있지만, **마커 인터페이스** 는 어노테이션이 제공해주는 기능들을 사용할 수 없습니다. 119 | * **마커 인터페이스** 는 인스턴스들을 구분하는 타입으로 사용될 수 있지만, **마커 어노테이션** 은 그렇지 않습니다. 120 | 121 |
122 | 123 | **마커 어노테이션** 과 **마커 인터페이스** 는 각자의 쓰임이 있습니다. 124 | 125 | 새로 추가하는 메소드가 없이 타입 정의가 목적이라면 **마커 인터페이스** 를 사용하는 것이 좋습니다. 126 | 127 | 반면 어노테이션을 적극 활용하여 프레임워크의 일부로 그 마커를 편입시키고자 한다면 **마커 어노테이션** 을 사용하는 것이 좋습니다. 128 | 129 | 130 | 131 | ## 정리 132 | 133 | `Java`의 경우 **마커 인터페이스 패턴** 을 위해서 **어노테이션**을 만들고 자주 사용한다. 하지만 `Swift` 에서는 따로 어노테이션이 134 | 135 | 존재 하지 않기에 `protocol` 을 이용해서 직접 구현해야 합니다. 136 | 137 | 이전에 **마커 인터페이스 패턴** 에 대해 잘 알지 못했지만 타입을 마킹하는 용도로 무의식적으로 자주 사용해왔었습니다. 138 | 139 | 이제부터는 목표를 분명히 하여 **마커 인터페이스 패턴** 을 사용하면 코드의 퀄리티가 좋아질 것 같은 기분이 듭니다. 140 | 141 | 142 | -------------------------------------------------------------------------------- /6장_열거_타입과_애너테이션/resources/item36-optionsSetMethod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSwiftists/effective-swift/03f4cd8721979a28c7f221663fa33511bd73a902/6장_열거_타입과_애너테이션/resources/item36-optionsSetMethod.png -------------------------------------------------------------------------------- /7장_람다와_스트림/item44.md: -------------------------------------------------------------------------------- 1 | # 표준 함수형 인터페이스를 사용하라 2 | 3 | 자바에는 함수형 인터페이스가 있고, 특히 매개변수로 함수 객체를 사용할 때 **타입**으로 사용됩니다. 4 | 5 | ```java 6 | private List filter(List list, Predicate predicate) { 7 | List filteredList = new ArrayList<>(); 8 | for(T element: list) { 9 | if (predicate.test(element)) { 10 | filteredList.add(element); 11 | } 12 | } 13 | return filteredList; 14 | } 15 | ``` 16 | 17 | * 해당 자바 코드를 스위프트로 바꾸면 아래와 같습니다. 18 | 19 | ```swift 20 | private func filter(list: [T], isIncluded: (T) -> Bool) -> [T] { 21 | var filteredList = [T]() 22 | for element in list { 23 | if isIncluded(element) { 24 | filteredList.append(element) 25 | } 26 | } 27 | return filteredList 28 | } 29 | ``` 30 | 31 | ## 표준 함수형 인터페이스란? 32 | 33 | 그 중에서도 java.util.function 패키지에서 제공하는 함수형 인터페이스를 **표준 함수형 인터페이스**라고 합니다. **책에서는 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라고 조언합니다.** 34 | 35 | > 표준 함수형 인터페이스를 왜 사용하는가? 36 | 37 | 자바도 스위프트처럼 함수를 일급 객체로 다룹니다. 즉 함수 자체를 함수 인수(매개변수)로 전달 할 수 있습니다. 38 | 그럼 함수를 파라미터 타입으로 표현해야 하는데 자바의 경우 함수형 인터페이스(ex. `Predicate`, `Supplier`)로 표현합니다. 근데 함수형 인터페이스 각각은 `(인수) -> 반환타입` 이 같아도 됩니다. 아래 예시처럼요. 39 | 40 | ```java 41 | interface Predicate { 42 | boolean test(T t); 43 | } 44 | 45 | interface PredicateA { 46 | boolean test(T t); 47 | } 48 | 49 | interface PredicateB { 50 | boolean test(T t); 51 | } 52 | 53 | interface PredicateC { 54 | boolean test(T t); 55 | } 56 | ``` 57 | 58 | 이처럼 같은 `(인수)-> 반환타입` 이어도 여러 개의 함수형 인터페이스로 나타낼 수 있고, 이는 이미 똑같은 `(인수) -> 반환타입`인 함수형 인터페이스가 존재하여도 그때그때 직접 구현하는 것은 매우 비효울적이라 할 수 있습니다. 59 | 따라서 자바에서는 표준 함수형 인터페이스라는 걸 만든 것이고 함수를 전달할 때 **직접 구현하지 말고 만들어둔 표준 함수형 인터페이스를 쓰라고 하는 것**입니다. 또 이렇게 표준 함수형 인터페이스를 사용하게 되면 직접 구현한 함수형 인터페이스와 달리 인터페이스 이름만 보고도 **어떤 `(인수) -> 반환타입` 인지 알 수 있는 장점**이 있습니다. 60 | 61 | ## 자바의 대표적인 표준 함수형 인터페이스 62 | 63 | | 인터페이스 | 함수 시그니처 | 예 | 64 | |---------|------------|---| 65 | |UnaryOperator\ | T apply(T t) | String::toLowerCase | 66 | |BinaryOperator\ | T apply(T t1, T t2) | BigInteger::add | 67 | |Prediacate\ | boolean test(T t) | Collection::isEmpty | 68 | |Supplier\ | T get() | Instant::now | 69 | |Consumer\ | void accept(T t) | System.out::println | 70 | 71 |
72 | 73 | * 해당 자바의 표준 함수형 인터페이스의 함수 시그니처를 스위프트 버전으로 바꾸면 아래와 같습니다. 74 | 75 | | 인터페이스 | Swift 버전 함수 시그니처 | 76 | |---------|-----------------| 77 | |UnaryOperator\ | (T) -> T | 78 | |BinaryOperator\ | (T, T) -> T | 79 | |Prediacate\ | (T) -> Bool | 80 | |Supplier\ | () -> T | 81 | |Consumer\ | (T) -> Void | 82 | 83 | 사실, 스위프트에는 인수로 함수 객체를 `(인수) -> (반환타입)` 형태로 나타내기 때문에 따로 표준 함수형 인터페이스가 필요없고, 따라서 Predicate니 Supplier니 용어를 알 필요가 자바보다는 적다고 할 수 있습니다. 84 |
하지만 그럼에도 Swift 코드에서도 인수 이름으로 `Predicate` 가 많이 보여 해당 부분은 알면 좋겠다고 판단하여 `Predicate` 에 대해 조사하였습니다. `Predicate`도 `Object` 나 `atomic` 처럼 프로그래밍 세계에서의 고유명사라고 생각하시면 될 것 같습니다. 85 | 86 | ## Predicate의 사전적 의미 87 | 88 | * 술어, 술부, 빈사, 단언하다, 서술하다 89 | * 단정하다 90 | 91 | 예시) 92 | 93 | predicate the eternity of human life 94 | => 인간의 생명은 영원하다고 단언하다 95 | 96 | Most religions predicate life after death. 97 | => 대개의 종교는 내세가 있다고 단언한다. 98 | 99 | 100 | : 프로그래밍적으로 해석하자면,,, 난 이 내용의 결과가 `true` 라고 확신해,, 이 결과는 `false`라고 단언해,, 101 | 102 | ## predicate 가 파라미터로 포함된 Swift 메소드들 103 | 104 | > 다음 메소드들은 Array의 메소드들로 predicate가 파라미터로 포함된 메소드들입니다. 105 | 106 | * `@inlinable public func first(where predicate: (Element) throws -> Bool) rethrows -> Element?` 107 | 108 | * `@inlinable public func drop(while predicate: (Element) throws -> Bool) rethrows -> ArraySlice` 109 | 110 | * `@inlinable public func prefix(while predicate: (Element) throws -> Bool) rethrows -> ArraySlice` 111 | 112 | * `@inlinable public func firstIndex(where predicate: (Element) throws -> Bool) rethrows -> Int?` 113 | 114 | * `@inlinable public func last(where predicate: (Element) throws -> Bool) rethrows -> Element?` 115 | 116 | * `@inlinable public func lastIndex(where predicate: (Element) throws -> Bool) rethrows -> Int?` 117 | 118 | 등등 predicate가 파라미터로 있는 메소드들이 있습니다. 119 | 120 | ## NSPredicate 121 | 122 | * `NSArray`, `NSMutableArray`, `NSMutableSet`, `NSOrderedSet` 등등에서 filter 혹은 filtered 메소드에서 파라미터로 사용됩니다. 123 | 124 | ```swift 125 | let array = NSArray(arrayLiteral: "A", "B", "", "C", "", "D") 126 | let predicate = NSPredicate(format: "SELF != ''") 127 | 128 | let filteredArray = NSArray(array: array.filtered(using: predicate)) 129 | print("Input array: \(array)") 130 | print("Filterd array: \(filteredArray)") 131 | 132 | // 실행 결과 133 | // Input array: ( 134 | // A, 135 | // B, 136 | // "", 137 | // C, 138 | // "", 139 | // D 140 | // ) 141 | // Filterd array: ( 142 | // A, 143 | // B, 144 | // C, 145 | // D 146 | // ) 147 | ``` 148 | 149 | * 정규표현식을 사용할 때에도 NSPredicate 가 사용될 수 있습니다. 150 | 151 | ```swift 152 | func validateEmail(withString userEmail: String) -> Bool { 153 | let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z]+\\.[A-Za-z]{2,}" 154 | 155 | let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) 156 | return emailTest.evaluate(with: userEmail) 157 | } 158 | 159 | print(validateEmail(withString: "kimdo2297@gmail.com")) 160 | print(validateEmail(withString: "kim!!2297@gmail.com")) 161 | print(validateEmail(withString: "kimdo2297@gmail.c")) 162 | print(validateEmail(withString: "kimdo2297@g1230.com")) 163 | 164 | // 실행 결과 165 | // true 166 | // false 167 | // false 168 | // false 169 | ``` 170 | 171 | ### 참고 172 | 173 | * [자바 표준 함수형 인터페이스](https://johngrib.github.io/wiki/java-functional-interface/) 174 | * [RegEx with NSPrediacate](https://stackoverflow.com/questions/16852875/filter-nsarray-using-nspredicate) 175 | * [NSArray using NSPredicate](https://stackoverflow.com/questions/16852875/filter-nsarray-using-nspredicate) 176 | * [Predicates in Swift](https://www.swiftbysundell.com/articles/predicates-in-swift/) -------------------------------------------------------------------------------- /7장_람다와_스트림/item45.md: -------------------------------------------------------------------------------- 1 | # Item 45. 스트림은 주의해서 사용해라 2 | 3 | 4 | 5 | 스트림 파이프라인은 6 | 7 | `소스 스트림` - `중간 연산` - `종단 연산` 으로 진행됩니다. 8 | 9 | ### 중간 연산 10 | 11 | - map 12 | - mapToInt → IntStream 13 | - mapToLong → LongStream 14 | - ... 15 | - flatMap 16 | - filter 17 | 18 | ... 19 | 20 | 중간의 연산은 스트림을 어떠한 방식으로 변환합니다. 21 | 22 | 23 | 24 | ### 스트림의 장점 25 | 26 | 적절하게 사용하면 27 | 1. 코드 길이가 짧습니다. 28 | 2. 코드가 선언적이고 깔끔해집니다. 29 | 30 | ### 스트림의 단점 31 | 32 | 과하게 사용하면 33 | 1. 잘못 사용하면 읽기 어렵습니다. 34 | 2. 유지보수가 어려워질 수 있습니다. 35 | 36 | 스트림을 사용하는 경우 37 | 38 | **Java** 39 | 40 | ```java 41 | public class Anagrams { 42 | public static void main(String[] args) throws IOException { 43 | Path dictionary = Paths.get(args[0]); 44 | int minGroupSize = Integer.parseInt(args[1]); 45 | 46 | try (Stream words = Files.lines(dictionary) { 47 | words.collect( 48 | groupingBy(word -> word.chars().sorted() 49 | .collect(StringBuilder::new, 50 | (sb, c) -> sb.append((char) c), 51 | StringBuilder::append).toString() 52 | ) 53 | ) 54 | .values().stream() 55 | .filter(group -> group.size() >= minGroupSize) 56 | .map(group -> group.size() + ": " + group) 57 | .forEach(System.out.println); 58 | } 59 | } 60 | } 61 | 62 | ``` 63 | 64 | **Swift** 65 | 66 | ```swift 67 | class Anagrams { 68 | typealias Group = [String: [String]] 69 | 70 | private let words: [String] 71 | private let minGroupSize: Int = 5 72 | 73 | init(from source: [String] = []) { 74 | self.words = source 75 | } 76 | 77 | func invoke() { 78 | Group.init(grouping: words, by: { $0.lowercased().sorted(by: { $0 > $1 }).description }) 79 | .map { $0.value } 80 | .filter { $0.count >= minGroupSize } 81 | .map { "\\($0.count): \\($0)" } 82 | .forEach { print($0) } 83 | } 84 | } 85 | 86 | let anagram: Anagrams = .init(from: ["abc", "bac", "ddd", "dde"]) 87 | anagram.invoke() 88 | 89 | // result 90 | // 1: ["ddd"] 91 | // 2: ["abc", "bac"] 92 | // 1: ["dde"] 93 | 94 | ``` 95 | 96 | 97 | 98 | ### 람다 사용시 주의점 99 | 100 | - **람다 매개변수의 이름은 주의해서 정해라** 101 | 102 | 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지됩니다. 103 | 104 | - **도우미 메소드를 잘 활용하라** 105 | 106 | 도우미 메소드를 잘 활용하면 반복적인 코드들을 확실하게 줄이면서 가독성도 높힐 수 있습니다. 107 | 108 | - **스트림과 반복문을 적절히 조합하라** 109 | 110 | 람다에서는 final이거나 final 객체만 읽을 수 있기 때문에, 지역변수를 수정할 수 없습니다. 111 | 112 | 람다에서는 `return`, `break`, `continue` 와 같은 키워드를 사용할 수 없습니다. 113 | 114 | **수정된 코드** 115 | 116 | ```swift 117 | class Anagrams { 118 | typealias Group = [String: [String]] 119 | 120 | private let words: [String] 121 | private let minGroupSize: Int = 5 122 | 123 | init(from source: [String] = []) { 124 | self.words = source 125 | } 126 | 127 | func invoke() { 128 | Groups.init(grouping: words, by: { (word) in 129 | word.lowercased().sorted(by: { $0 > $1 }).description 130 | }) 131 | .map { (groups) -> [String] in groups.value } 132 | .filter { (group) in group.count >= minGroupSize } 133 | .map { (group) in "\\(group.count): \\(group)" } 134 | .forEach { print($0) } 135 | } 136 | } 137 | 138 | ``` 139 | 140 | - 필요할 때는 인자에 이름을 붙이고 그렇지 않을때는 `$0` `$1` 등으로 간단하게 표기합니다. 141 | 142 | ### 스트림을 사용하기 좋은 환경 143 | 144 | 1. 원소들의 시퀸스를 일관되게 변환합니다. 145 | 2. 원소들의 시퀸스를 필터링합니다. 146 | 3. 원소들의 시퀸스를 하나의 연산을 사용해 결합합니다. 147 | 4. 원소들의 시퀸스를 컬렉션에 모읍니다. 148 | 5. 원소들의 시퀸스에서 특정 조건을 만족하는 원소를 찾습니다. -------------------------------------------------------------------------------- /7장_람다와_스트림/item46.md: -------------------------------------------------------------------------------- 1 | # Item46. 스트림에서는 부작용 없는 함수를 사용하라 2 | 3 | 스트림은 처음 봐서는 이해하기 어려울 수 있습니다. 4 | 원하는 작업을 스트림 파이프 라인으로 표현하는 것 조차 어려울지 모릅니다. 5 | 성공하여 프로그램이 동작하더라도 장점이 무엇인지 쉽게 와 닿지 않을 수도 있습니다. 6 | **스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임**이기 때문입니다. 7 | 스트림이 제공하는 **표현력, 속도, (상황에 따라서는)병렬성**을 얻으려면 API는 말할 것도 없고 이 패러다임까지 함께 받아들여야 합니다. 8 | 9 | 스트림 패러다임의 핵심은 계산을 **일련의 변환(transformation)** 으로 재구성하는 부분입니다. 10 | 이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 **순수 함수(pure function)** 여야 합니다. 11 | 순수 함수란 **오직 입력만이 결과에 영향을 주는 함수**를 말합니다. 12 | 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않습니다. 13 | 이렇게 하려면 스트림 연산에 건네는 함수 객체는 **모두 부작용(side effect)가 없어야 하므로 순수 함수**이어야 합니다. 14 | 15 | ## Side effect가 있는 스트림 코드 16 | 17 | ```swift 18 | func sideEffectStream(file: String) { 19 | var frequency = [String: Int]() 20 | let words = file.split(separator: " ").map { String($0) } 21 | words.forEach { word in 22 | frequency.merge(key: word.lowercased(), value: 1) { count, increment count + increment } 23 | } 24 | print(frequency) 25 | } 26 | 27 | extension Dictionary { 28 | mutating func merge(key: Key, value: Value, remappingHandler: (Value, Value) -> (Value)) { 29 | let oldValue = self[key] 30 | let newValue = (oldValue == nil) ? value : remappingHandler(oldValue!, value) 31 | 32 | self[key] = newValue 33 | } 34 | } 35 | 36 | sideEffectExample(file: "Hello I'm Jason. Why not? i'm jason.") // ["not?": 1, "hello": 1, "jason.": 2, "why": 1, "i\'m": 2] 37 | ``` 38 | 39 | 위 코드는 외부 상태(frequency)를 수정하며 side-effect를 발생시키는 스트림 코드입니다. forEach 문으로 반복적으로 frequency를 수정하는 것을 알 수 있습니다. 40 |
forEach가 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 하는 것을 보니 나쁜 코드일 것 같은 냄새가 납니다. **forEach가 계산을 하는 코드는 보통 외부 값을 수정하는 side effect가 일어나는 코드**이기 때문입니다. 41 | 42 | 다음은 올바르게 작성한 스트림 코드를 보겠습니다. 43 | 44 | ## Side effect가 없는 스트림 코드 45 | 46 | ```swift 47 | func nonSideEffectExample(file: String) { 48 | let words = file.split(separator: " ").map { String($0) } 49 | let frequency: [String: Int] = [String: [String]]( 50 | grouping: words, 51 | by:{ $0.lowercased() } 52 | ).mapValues { values -> Int in values.count } 53 | 54 | print(frequency) 55 | } 56 | ``` 57 | 58 | ```swift 59 | @inlinable public init(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence 60 | ``` 61 | 62 | => 앞서와 같은 일을 하지만, 이번엔 스트림 API를 제대로 사용했습니다. 그뿐만 아니라 짧고 명확합니다. 63 | 64 | ## forEach문은 보고할 때만 사용하고 계산할 때는 사용하지 마십시오 65 | 66 | 자바 프로그래머(스위프트도 마찬가지)라면 for-each 반복문을 사용할 줄 알텐데, for-each 반복문은 forEach 종단 연산과 비슷하게 생겼습니다. 67 | 하지만 forEach 종단 연산은 종단 연산 중 기능이 가장 적고 가장 **'덜' 스트림**답습니다. 68 | 대놓고 **반복적이라서 병렬화할 수도 없습니다.** 69 |
**forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 마십시오.** 70 | forEach로 계산한다는 것은 외부 상태를 수정한다는 뜻입니다. 71 | 반복문을 사용하세요. 72 | 물론 가끔은 forEach문이 스트림 계산 결과를 기존 컬렉션에 추가하는 등의 다른 용도로는 쓰일 수 있습니다. 73 | 74 | ## Collectors Method: toList, toMap, toSet 75 | 76 | * 자바에서는 스트림을 사용하는데 수집기(Collector)를 사용할 수 있습니다. `java.util.stream.Collectors` 클래스는 메서드를 무려 39개나 가지고 있고, 타입 매개변수가 5개나 되는 것도 있습니다. 77 |
수집기는 총 세가지로, toList(), toMap(), toSet() 가 주인공입니다. 이 중에서 toList()와 toMap()을 알아보았습니다. 78 | 79 | * 스위프트는 따로 Collectors Method를 제공하고 있지 않습니다. 따라서 자바 스트림 코드를 그대로 변환만 해보았습니다. 80 | 81 | ### toList 82 | 83 | * 스트림을 컬렉션 List로 변환해주는 메서드입니다. 84 | 85 | > Java 86 | ```java 87 | List topTen = frequency.keySet().stream() 88 | .sorted(comparing(frequency::get).reversed()) 89 | .limit(10) 90 | .collect(Collectors.toList()); 91 | ``` 92 | 93 | > Swift 94 | ```swift 95 | let topTen: [String] = frequency.keys 96 | .sorted { (lhs, rhs) -> Bool in frequency[lhs]! > frequency[rhs]! } 97 | .enumerated() 98 | .filter { (index, _ ) in return index >= 0 && index < 10 } 99 | .map { $0.element } 100 | ``` 101 | 102 | ### toMap 103 | 104 | * 스트림을 컬렉션 Map으로 변환해주는 메서드입니다. 105 | 106 | > Java 107 | ```java 108 | private static final Map stringToEnum 109 | = Stream.of(values()).collect(toMap(Object::toString, e -> e)); 110 | ``` 111 | 112 | > Swift 113 | ```Swift 114 | enum Operation: CaseIterable { 115 | case plus 116 | case minus 117 | case times 118 | case divide 119 | 120 | static func stringToEnum() -> [String: Operation] { 121 | return [String: [Operation]]( 122 | grouping: allCases, 123 | by: { operation in "\(operation)" } 124 | ).mapValues { value in value.first! } 125 | } // 일단 이렇게 변환하는게 저는 최선입니다. 126 | } 127 | ``` 128 | 129 | ## Collectors Method: groupingBy 130 | 131 | groupingBy 메서드는 Collectors의 또 다른 메서드로, 입력으로 분류함수(classifier)를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환합니다. 그리고 **이 카테고리가 해당 원소의 맵 키로 쓰입니다.** 132 | groupingBy는 다중정의된 메서드로 총 3가지 메서드가 있습니다. 133 | 134 | * classifier만 사용하는 메서드 135 | 136 | groupingBy 메서드는 가장 간단한 것으로서 분류함수 classfier만 인수로 받고 Map을 반환합니다. 반환된 맵에 담긴 각각의 값은 해당 카테고리에 속하는 원소들을 모두 담은 List 입니다. 137 | 138 | > Java 139 | 140 | ```java 141 | Map> map = words.collect(groupingBy(word -> alphabetize(word))) 142 | ``` 143 | 144 | * classifier와 downstream을 사용하는 메서드 145 | 146 | * groupingBy가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림(downstream) 수집기도 명시해야 합니다. 아래와 같이 다운스트림 수집기로 counting()을 건네는 방법도 있습니다. 이렇게 하면 각 카테고리(키)를 (원소를 담은 컬렉션이 아닌) 해당 카테고리에 속하는 원소의 개수(값)와 매핑한 맵을 얻는다. 147 | 148 | ```java 149 | Map frequency; 150 | try(Stream words = new Scanner(file).tokens()) { 151 | frequency = words.collect(groupingBy(String::toLowerCase, counting())); 152 | } 153 | ``` 154 | 155 | * classfier와 downstream, mapFactory를 모두 사용하는 메서드가 있습니다. 156 | 157 | > Swift 158 | 159 | 자바의 groupingBy에 대응되는 것은 딕셔너리의 생성자 `init(grouping:by:)` 라고 할 수 있겠습니다. 160 | 161 | * Declaration 162 | 163 | `init(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence` 164 | 165 | => 선언에서 알수 있듯이 해당 생성자를 사용하면 해당 딕셔너리의 value 타입은 타입 파라미터 S의 배열임을 알 수 있습니다. 즉 타입은 `[S: [S]]` 입니다. 166 | 167 | * 위 자바 코드에 대응되는 스위프트 코드입니다. 168 | 169 | ```swift 170 | let map: [String: [String]] = [String: [String]].init(grouping: words, by: { (word) in alphabetize(word) }) 171 | ``` 172 | 173 | ## Swift 의 enumerated, zip 174 | 175 | 또 스위프트의 스트림을 사용할 때 유용한 메소드로 `enumerated` 와 `zip` 이 있습니다. 176 | 177 | * enumerated 178 | 179 | 쌍의 시퀀스 (n, x)를 반환합니다. 여기서 n은 0에서 시작하는 연속 정수 즉 index를 나타내고, x는 시퀀스의 요소(value)를 나타냅니다. 180 | 181 | ```swift 182 | "Swift" 183 | .enumerated() 184 | .forEach { n, x in print(n, x) } 185 | // Prints "0: 'S'" 186 | // Prints "1: 'w'" 187 | // Prints "2: 'i'" 188 | // Prints "3: 'f'" 189 | // Prints "4: 't'" 190 | ``` 191 | 192 | * zip 193 | 194 | 두 타입을 합쳐 tuple로 만들어 주는 기능을 합니다. 195 | 196 | > 예시 코드 197 | ```swift 198 | let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"] 199 | zip(names.indices, names) 200 | .filter { (indice, name) -> Bool in return name.count <= 5} 201 | .forEach { (indice, name) in print(names[indice]) } 202 | // Prints "Sofia" 203 | // Prints "Mateo" 204 | ``` 205 | 206 | * [출처](https://developer.apple.com/documentation/swift/1541125-zip) 207 | 208 | > 또 다른 예시 코드 209 | ```swift 210 | let wizards2 = ["Harry", "Ron", "Hermione", "Draco"] 211 | let animals2 = ["Hedwig", "Scabbers", "Crookshanks"] 212 | 213 | for (wizard, animal) in zip(wizards2, animals2) { 214 | print("\(wizard) has \(animal)") 215 | } 216 | 217 | /* 출력 결과 218 | Harry has Hedwig 219 | Ron has Scabbers 220 | Hermione has Crookshanks 221 | */ 222 | ``` 223 | * [출처](https://www.hackingwithswift.com/example-code/language/how-to-use-the-zip-function-to-join-two-arrays) -------------------------------------------------------------------------------- /8장_메서드/item49.md: -------------------------------------------------------------------------------- 1 | # item49. 매개변수가 유효한지 검사하라 2 | 3 | 메서드와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기 바랍니다. 이런 제약들은 반드시 문서화해야 하며 메서드 몸체가 시작되기 전에 검사해야 합니다. `오류는 가능한 한 빨리(발생한 곳에서)잡아야 한다` 는 일반 원칙의 한 사례이기도 합니다. 오류를 발생 즉시 잡지 못하면 해당 오류를 감지하기 어려워지고, 감지하더라도 오류의 발생 지점을 찾기 어려워집니다. 4 | 5 | 메서드 몸체가 실행되기 전에 매개변수를 확인해 잘못된 값이 넘어왔다면 즉각적으로 예외를 던지는 방식을 택할 수 있습니다. 만약 매개변수 검사를 제대로 하지 못하면 몇 가지 문제가 생길 수 있습니다. 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있고 더 나쁜 상황은 메서드는 문제없이 수행됐지만, 어떤 객체를 의도하지 않은 방향으로 만들어놔서 미래의 알 수 없는 시점에 이 메서드와 관련없는 에러를 내뱉을 때입니다. 6 | 7 | 매개변수 제약을 문서화한다면 그 제약을 어겼을 때 발생하는 예외도 함께 기술되어야 합니다. 이런 간단한 방법으로도 사용자가 제약을 지키도록 유도할 수 있습니다. 8 | 9 | ```swift 10 | enum CaculateError: Error { 11 | case notDualNumber 12 | } 13 | 14 | enum ParameterError: Error { 15 | case invalidValue 16 | } 17 | /* 18 | - Parameters: 19 | - value: 계수(양수어야 함) 20 | - Throws: value가 0보다 작거나 같으면 에러 던짐 21 | - Returns: 현재 값 mod value 22 | */ 23 | func mod(value: Int) throws -> Int { 24 | guard value > 0 else { throw CaculateError.notDualNumber } 25 | 26 | // 계산 수행.. 27 | } 28 | 29 | // 아래와같이 유효하지 않은 매개변수가 입력된 경우 즉각적으로 예외를 던질 수 있습니다 30 | func mod(value: Int?) throws -> Int { 31 | guard let value = value else { throw ParameterError.invalidValue } 32 | 33 | // 계산 수행.. 34 | } 35 | ``` 36 | 37 | 38 | 39 | 그리고 매개변수가 논옵셔널인 메서드에 옵셔널 매개변수를 넣고자 할 때는 빌드 경고 메시지가 떠 개발자가 유효한 매개변수를 입력할 수 있습니다. 40 | 41 | 메서드가 직접 사용하지는 않지만 나중에 쓰기 위해 저장하는 매개변수는 특히 더 신경써서 검사해야 합니다. 생성자의 경우에도 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체가 만들어지지 않게 하는 데 꼭 필요합니다. 42 | 43 | 메서드가 호출되는 상황을 통제 할 수 있는 상황에서는 유효한 값만이 메서드에 인자로 넘겨지리라는 것을 보증할 수 있게 만들 수 있고 그렇게 해야만 합니다. 아래와같이 `assert(_:)` 를 사용해 매개변수 유효성을 검증할 수 있습니다. 44 | 45 | ```swift 46 | private func sort(_ array: [Int], offset: Int, count: Int) { 47 | assert(!array.isEmpty) 48 | assert(offset >= 0 && offset <= array.count) 49 | assert(count >= 0 && count <= array.count - offset) 50 | // 계산 수행.. 51 | } 52 | ``` 53 | 54 | 여기서 핵심은 이 단언문들은 자신이 단언한 조건이 무조건 참이라고 선언한다는 것입니다. 이 메서드가 선언된 객체를 사용하는 쪽에서 어떻게 사용하는가와는 상관이 없습니다. 이 `assert(_:)`는 디버깅모드에서만 작동하고 릴리즈모드에서는 제외됩니다. 그래서 이 메서드를 자주 사용해도 실제 프로덕션 앱의 성능에는 영향을 끼치지 않습니다. 그래서 프로그래머의 실수를 추적하는데 적합합니다. 주로 디버깅 중 조건의 검증을 위해 사용됩니다. 55 | 56 | 메서드 몸체 실행 전에 매개변수 유효성을 검사해야 한다는 규칙에도 예외는 있습니다. 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때입니다. 때로는 계산 과정에서 필요한 유효성 검사가 이뤄지지만 실패했을 때 잘못된 예외를 던지기도 합니다. 달리 말하면, 계산 중 잘못된 매개변수 값을 사용해 발생한 예외와 메서드 도움말에서 던지기로 한 예외가 다를 수 있다는 뜻입니다. 이런 경우에는 아이템 73에서 설명하는 예외 번역(exeption translate) 관용구를 사용해 메서드 도움말에 기재된 예외로 번역해주어야 합니다. 57 | 58 | 이번 아이템은 `매개변수에 제약을 두는 게 좋다`고 말하는 것이 아닙니다. 메서드는 최대한 범용적으로 설계해야 합니다. 메서드가 건네받은 값으로 제대로 된 일을 할 수 있다면 매개변수 제약은 적을수록 좋습니다. 하지만 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않습니다. 59 | 60 | 그러므로 메서드나 생성자를 작성할 때, 그 매개변수들에 어떤 제약이 있을지 생각해야 합니다. 그리고 그 제약들을 문서화하고 메서드 시작 부분에서 명시적으로 검사해야 합니다. -------------------------------------------------------------------------------- /8장_메서드/item53.md: -------------------------------------------------------------------------------- 1 | # Item 53. 가변인수는 신중히 사용하라 2 | 3 | 원서 제목: **Use varargs judiciously(분별력 있게, 현명하게)** 4 | 5 | 6 | ### 핵심 정리 7 | 8 | > 인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변인수가 반드시 필요하다. 메서드를 정의할 때 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자. 9 | > -출처: 이펙티브 자바 10 | 11 | Swift 문서에 programming language 문서(swift 5.3)에 소개된 Variadic Parameter에 대해 소개합니다. 12 | ### Variadic Parameters 13 | 14 | ``` swift 15 | func functionName(parameter1: Type, parameter2: Type...) -> returnType { 16 | // implement 17 | return 18 | } 19 | ``` 20 | 21 | **Variadic**: '임의의 갯수 인수를 받을 수 있는' 22 | 23 | 즉, **Variadic parameter** 는 0개 이상의 특정 타입 인수를 받을 수 있는 매개변수를 말하며 메서드가 호출될 때 전달되어 사용 가능합니다. **Variadic parameter** 를 사용하기 위해서는 매개변수 타입 이름 뒤에 `...` 를 붙여 사용합니다. **Variadic parameter** 에 전달된 값은 함수 본문 내에서 적절한 타입의 배열로 사용할 수 있습니다. 예를 들어 `doubleArray` 를 이름으로 갖고있는 `Double...` 타입의 **Variadic parameter** 는 `[Double]` 타입의 상수 배열로 함수 본문 내에서 사용할 수 있습니다. 24 | 25 | **Variadic parameter**는 인수 개수가 정해지지 않았을 때 유용합니다. 26 | 27 | 28 | ### 활용 예시 29 | 30 | * 인수가 1개 이상이어야 할 때 가변인수를 제대로 사용하는 방법 31 | ```swift 32 | static func min(firstArg: Int, remainArgs: Int...) -> Int { 33 | var min = firstArg 34 | for arg in remainArgs { 35 | if arg < min { 36 | min = arg 37 | } 38 | } 39 | return min 40 | } 41 | ``` 42 | 43 | * 산술 평균 구하기 (주어진 수의 합을 수의 개수로 나눈 값) 44 | 45 | ```swift 46 | // 산술 평균 구하기 (주어진 수의 합을 수의 개수로 나눈 값) 47 | func arithmeticMean(_ numbers: Double...) -> Double { 48 | var total: Double = 0 49 | for number in numbers { 50 | total += number 51 | } 52 | return total / Double(numbers.count) 53 | } 54 | arithmeticMean(1, 2, 3, 4, 5) // returns 3.0 55 | arithmeticMean(3, 8.25, 18.75) // returns 10.0 56 | ``` 57 | 58 | * 합 구하기 59 | ```swift 60 | func sum(integers: Int...) -> Int { 61 | return integers.reduce(0,combine: +) 62 | } 63 | 64 | sum(1,2,3,4,5,6,7,8,9) // returns 45 65 | 66 | sum(1,2,3) // returns 6 67 | ``` 68 | 69 | * subView 추가하기 70 | ```swift 71 | // 화면 바탕에 깔리는 백그라운드 뷰의 역할을 하는 Canvas 클래스. 72 | class Canvas { 73 | func add(_ views: UIView...) { 74 | for view in views { 75 | self.addSubView(view) 76 | } 77 | } 78 | } 79 | 80 | let circle = Circle(center: CGPoint(x: 5, y: 5), radius: 5) 81 | let lineA = Line(start: .zero, end: CGPoint(x: 10, y: 10)) 82 | let lineB = Line(start: CGPoint(x: 0, y: 10), end: CGPoint(x: 10, y: 0)) 83 | 84 | let canvas = Canvas() 85 | canvas.add(circle, lineA, lineB) 86 | ``` 87 | 88 | * API에 첨부파일을 보내기 89 | ```swift 90 | func send(_ message: Message, attaching attachments: Attachment...) { 91 | ... 92 | } 93 | 94 | // Passing no variadic arguments: 95 | send(message) 96 | 97 | // Passing either a single, or multiple variadic arguments: 98 | send(message, attaching: image) 99 | send(message, attaching: image, document) 100 | ``` 101 | 102 | ### Variadic Parameters vs Array Parameters 103 | 104 | 사실, 함수 안에서 Variadic Parameter도 Array로 사용되기 때문에 Variadic Parameter와 Array 둘 중에 어떤 것을 사용하더라도 문제가 없습니다. 그럼 어떤 상황에서 Variadic Parameters를 쓰면 좋은지, Array Parameters를 쓰면 좋은지 비교해봅시다. 105 | 106 | * Variadic Parameters의 특징 107 | 108 | > **NOTE** 109 | > A function may have at most one variadic parameter. 110 | > 111 | > -출처: the swift programming languageswift 5.3 112 | 113 | 1. 함수는 최대 하나의 Variadic Parameters를 가질 수 있습니다. 114 | 115 | 2. 함수 호출 시 여러 인자들과의 모호성을 피하기 위해 Variadic Parameters는 항상 메서드 인자 목록 마지막에 위치를 합니다. 116 | 117 | 3. 함수가 하나 이상의 기본 값을 가지는 인자와 Variadic Parameters를 가지고 있다면, 기본값을 가지는 인자 뒤에 Variadic Parameters 가 위치해야 합니다. 118 | 119 | 4. 매개변수로 넘길 값이 없을 경우 함수 호출시 아규먼트 레이블을 비워도 됩니다. 120 | 0개 이상의 매개변수에 대한 입력이 배열로 변환된다고 이해하면 쉽습니다. 121 | 122 | ```swift 123 | func printNums(items: Int...) { 124 | for item in items { 125 | print (item) 126 | } 127 | } 128 | printNums() // presents no output, as no items are specified 129 | printNums(items: 1, 2) // outputs 1 and 2 to the console. 130 | ``` 131 | 132 | 이 특징을 고려했을 때, 함수에서 Variadic Parameters를 두 개 이상 사용해야 하는 경우나 Variadic Parameters가 함수 인자 목록에서 마지막에 위치하는 것이 코드의 가독성을 해친다면 Array Parameters를 고려하는게 좋습니다. 133 | 134 | 135 | ```swift 136 | // 컴파일 오류가 발생하는 코드 137 | func shoppingList(items: String..., prices: Float...) { ... } 138 | ``` 139 | 140 | 141 | * Array Parameters의 특징 142 | 143 | 1. 매개변수로 넘길 값이 없을 경우에도 함수 호출시 빈 배열을 아규먼트 레이블에 명시해야합니다. 144 | 145 | ```swift 146 | func printNums(items: [Int]) { 147 | for item in items { 148 | print (item) 149 | } 150 | } 151 | printNums([]) // presents no output, as no items are specified 152 | printNums(items: 1, 2) // outputs 1 and 2 to the console. 153 | ``` 154 | 155 | 이 특징을 고려했을 때, 매개변수의 수가 0개인지 그 이상인지 확실하지 않을 경우에는 굳이 빈 배열을 만들지 않아도 되는 Variadic Parameters를 고려하는게 좋습니다. 156 | 157 | 158 | 159 | 160 | ### 참고 161 | 162 | 1. [Variadic Parameters - the swift programming language swift 5.3](https://docs.swift.org/swift-book/LanguageGuide/Functions.html#ID166) 163 | 2. [Variadic Parameters v Array Parameter [closed]](https://stackoverflow.com/questions/30572738/variadic-parameters-v-array-parameter) 164 | 3. [#15 Using variadic parameters](https://github.com/JohnSundell/SwiftTips#15-using-variadic-parameters) 165 | 4. [The power of variadic parameters](https://www.swiftbysundell.com/tips/the-power-of-variadic-parameters/) 166 | 167 | -------------------------------------------------------------------------------- /8장_메서드/item54.md: -------------------------------------------------------------------------------- 1 | # Item 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라 2 | 3 | 4 | 5 | ### 핵심 정리 6 | 7 | > null이 아닌, 빈 배열이나 컬렉션을 반환하라. null을 반환하는 API는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은 것도 아니다. 8 | > 9 | > -출처: 이펙티브 자바 10 | 11 | 12 | 13 | 이펙티브 자바에서는 반환할 값이 없을 경우 null을 반환하는 게 아니라 빈 배열이나 컬렉션을 반환하라고 권하고 있습니다. 14 | 15 | 그렇다면 Swift 에서는 어떻게 바라보는게 좋을까요? 16 | 17 | ### Empty Array vs Optional 18 | 19 | Swift에서는 특별히 Best Practice나 공공연하게 권장되는 바는 없는 것 같습니다. 제 생각에는 목적에 맞게 데이터를 더 잘 설명할 수 있는 걸 선택하는 것이 좋을 것 같습니다. 20 | 21 | * return Empty Array: 22 | Swift는 Type Safety (타입 안전성)을 중요시하는 언어입니다. 이런 면에서 생각해봤을 때 반환할 값이 없을 때라도 메서드의 리턴 타입을 nil이 아닌 빈 배열을 반환하는 것은 이미 타입의 래퍼(wrapper) 역할을 한다고 할 수 있습니다. 23 | 24 | EX) 쇼핑몰에서 모든 유저가 장바구니를 가지고 있지만 장바구니에 아무것도 담겨있지 않은 상태를 길이가 0인 빈 배열로 표현할 수 있습니다. 25 | 26 | * return Optional: 27 | nil을 사용했을 때 말이 되는 경우는 호출자에게 값이 존재하지 않는 경우(nil)나 존재하지 않을 수도 있음을 확실히 전달하려는 경우. 28 | 29 | 추가적으로 스터디원분들의 의견이 있으면 이야기 나눠보고 싶습니다 :) 30 | 31 | ### 참고 32 | 33 | 1. [Optional array vs. empty array in Swift](https://stackoverflow.com/questions/26811787/optional-array-vs-empty-array-in-swift) -------------------------------------------------------------------------------- /8장_메서드/item56.md: -------------------------------------------------------------------------------- 1 | # item56. 공개된 API 요소에는 항상 문서화 주석을 작성하라 2 | 3 | > Markup Formatting References in Xcode 4 | 5 | 로버트 C. 마틴은 그의 저서 *클린 코드* 에서 `주석은 나쁜 코드를 보완하지 못한다` 고 했습니다. 허나 어떤 주석들은 개발자의 이해도와 사용성을 높이기에 아주 유용합니다. 이번 아이템에서는 Xcode 내에서 Quick Help 기능을 제공할 수 있는 주석을 작성하는 방법에 대해 알아봅니다. 6 | 7 | 8 | 9 | ### Xcode Quick Help란? 10 | 11 | Markup을 이용해 Swift 코드의 모든 심볼(Class, Method, enum..)에 대해 정형화된 Quick Help 주석을 작성하면 Option키를 클릭했을 때 작성된 주석을 Pop-up 형식으로 확인할 수 있습니다. 또한 입력 포인터가 심볼에 위치해 있을 때 Xcode 오른쪽에 있는 인스펙터 탭에 Quick Help 부분에서 작성된 주석을 확인할 수 있습니다. 12 | 13 | MFR_code_quick_help_2x 14 | 15 |
16 | 17 | ### 기본 Markup 18 | 19 | 기본적인 Markup 문법을 이용해 작성할 수 있습니다. 각각에 대해 조금 더 자세히 살펴봅시다. 20 | 21 | - 문단은 빈 줄로 구분됩니다. 22 | - 순서가 없는 목록은 불렛 문자(`-`, `+`, `*`)로 작성합니다. 23 | - 순서가 있는 목록은 `1.`이나 `1)` 처럼 작성합니다. 24 | - `#` 로 헤더를 나타냅니다. 혹은 바로 다음 줄에 `=`나 `-`를 표시해 헤더를 나타낼 수 있습니다. 25 | - 링크와 이미지 또한 모두 작동하며, 웹 기반 이미지가 Xcode에 직접 표시됩니다. 26 | - `----`, `***` 를 이용해 긴 수평선을 표시할 수 있습니다. 27 | - 슬래시 3개를 사용하여 한줄짜리 Formatting Reference를 작성할 수 있습니다 28 | 29 | ```swift 30 | /** 31 | # 목록 32 | 33 | *이탤릭체*와 **볼드체** 혹은 `코드`를 한 줄에 적용할 수 있습니다. 34 | 35 | ## 순서가 없는 목록 36 | 37 | - 목록을 작성할 수 있지만 38 | - 각각의 문단은 연관되어있지 않습니다. 39 | - 그래서 서브목록을 포맷팅하기엔 40 | - 썩 좋은 방법은 아닙니다. 41 | 42 | ## 순서가 있는 목록 43 | 44 | 1. 순서가 있는 목록은 45 | 2. 자동으로 순서대로 정렬됩니다. 46 | 3. 일반적인 숫자로 표기 가능합니다. 47 | */ 48 | ``` 49 | 50 | 51 | 52 |
53 | 54 | ### Parameters & Return Values 55 | 56 | - Parameters: `Parameter :` 으로 시작하며 매개변수에 대한 설명을 작성합니다. 57 | - Return values: `Returns:` 로 시작하며 리턴 값에 대한 정보를 작성합니다. 58 | - Thrown errors: `Throws:` 로 시작하며 발생할 수 있는 에러에 대한 설명을 작성합니다. Swift는 에러 타입이 `Error`인지만을 확인하므로 오류를 적절하게 문서화하는 것이 중요합니다. 59 | 60 | ```swift 61 | /** 62 | 받는 사람을 위한 맞춤 인사말을 생성 63 | 64 | - Parameter recipient: 환영 받을 사람 65 | 66 | - Throws: 만약 `recipient`이 "Derek"이면 67 | `MyError.invalidRecipient` 발생 68 | 69 | - Returns: `recipient`를 향한 새로운 문자열 반환 70 | */ 71 | func greeting(to recipient: String) throws -> String { 72 | guard recipient != "Derek" else { 73 | throw MyError.invalidRecipient 74 | } 75 | 76 | return "Greetings, \(recipient)!" 77 | } 78 | ``` 79 | 80 | 81 | 82 | 매개변수가 많은 경우 아래처럼 줄바꿈해 작성할 수 있습니다. 83 | 84 | ```swift 85 | /// 주어진 요소로부터 벡터의 크기를 3차원으로 반환 86 | /// 87 | /// - Parameters: 88 | /// - x: vector의 *x*값 89 | /// - y: vector의 *y*값 90 | /// - z: vector의 *z*값 91 | func magnitude3D(x: Double, y: Double, z: Double) -> Double { 92 | return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)) 93 | } 94 | ``` 95 | 96 | 97 | 98 |
99 | 100 | ### Code blocks 101 | 102 | 코드 블럭을 임베드해 함수의 적절한 사용법이나 구현 세부사항을 보여줄 수 있습니다. 최소한 4개의 공백으로 코드 블럭을 삽입할 수 있습니다. 또는 세개의 백틱(` ``` `) 또는 틸드(`~~~`)로 코드 블럭 앞 뒤를 묶어 구분할 수 있습니다. 103 | 104 | ```swift 105 | /** 106 | `Shape`의 인스턴스 영역 107 | 108 | 생성된 인스턴스 모양에 따라 계산이 달라진다. 109 | 삼각형의 경우 `area`는 다음과 같다 110 | 111 | let height = triangle.calculateHeight() 112 | let area = triangle.base * height / 2 113 | */ 114 | var area: CGFloat { get } 115 | ``` 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 | 125 | ### 추가적인 필드들 126 | 127 | `Parameters`, `Throws`, `Returns`외에도 미리 정의된 키워드들이 있는데, 이를 이용하면 섹션이 생성돼 추가적인 정보를 작성할 수 있습니다. 128 | 129 | - 알고리즘/안정성 정보: `Precondition`, `Postcondition`, `Requires`, `Invariant`, `Complexity`, `Important`, `Warning` 130 | - 메타데이터: `Author`, `Authors`, `Copyright`, `Date`, `SeeAlso`, `Since`, `Version` 131 | - 일반적 주석 & 권고사항: `Attention`, `Bug`, `Experiment`, `Note`, `Remark`, `ToDo` 132 | 133 | 134 | 135 |
136 | 137 | ### 작성시 유의사항 138 | 139 | - 메서드의 주석은 `어떻게 동작하는지`가 아니라 `무엇을 하는지`에 초점을 맞춰서 작성합니다. 140 | - 상속 할 가능성이 있는 클래스에는 되도록 상세하게 작성해 상속 받은 쪽에서 해당 메서드를 올바르게 재정의(override)할 수 있도록 합니다. 141 | - 제네릭 타입이나 제네릭 메서드인 경우 모든 타입 매개변수에 주석을 작성해야 합니다. 142 | - 열거형을 문서화 할 때는 모든 상수들에도 주석을 달아야 합니다. 설명이 짧다면 주석 전체를 한 문장으로 써도 좋습니다. 143 | - 여러 클래스가 상호작용하는 복잡한 구조라면 전체 아키텍처를 설명하는 별도의 설명이 필요할 때가 있습니다. 관련 설명 문서가 있다면 관련 클래스의 주석에 `SeeAlso`등의 필드로 문서 링크를 달 수도 있습니다. 144 | 145 | 146 | 147 |
148 | 149 | ### 참고 150 | 151 | - https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/SymbolDocumentation.html 152 | - https://nshipster.com/swift-documentation/ 153 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item57.md: -------------------------------------------------------------------------------- 1 | # Item 57. 지역변수의 범위를 최소화하라 2 | 3 | 4 | 5 | 6 | ### 요점 7 | > 지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아집니다. 8 | > -출처: 이펙티브 자바 9 | 10 | 9장 일반적인 프로그래밍 원칙이므로 이 문서에서는 책에 소개된 Swift에서도 알아두면 좋은 원칙들을 정리하고 책에 소개된 Java와 Swift의 다른 점을 소개합니다. 11 | 12 | ### 지역변수의 범위를 줄이는 2가지 기법 13 | 14 | #### 1. 지역변수의 범위를 줄이는 가장 강력한 기법은 역시 '가장 처음 쓰일 때 선언하기'입니다. 15 | 16 | 다음과 같은 이유로 이유없이 미리 선언부터 하지 않기를 권장합니다. 17 | 18 | * 코드가 어수선해져 가독성이 떨어집니다 19 | * 변수를 실제로 사용하는 시점엔 타입과 초깃값이 기억나지 않을 수 있습니다. 20 | * 지역 변수를 생각 없이 선언하다보면 변수가 쓰이는 범위보다 너무 앞서 선언하거나 다 쓴 뒤에도 여전히 살아있게 되기 쉽습니다. 이런 경우 의도하지 않은 사이드 이펙트를 야기할 수 있습니다. 21 | 22 | **< 거의 모든 지역변수는 선언과 동시에 초기화해야 합니다. >** 23 | 24 | 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 합니다. 25 | 26 | 단, try- catch 문은 예외입니다. 27 | 28 | 변수 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면 try 블록 안에서 초기화해야 합니다. 29 | 30 | **< 반복문은 독특한 방식으로 변수 범위를 최소화해줍니다. >** 31 | 32 | > 반복 변수 (loop variable)의 범위가 반복문의 몸체, 그리고 for 키워드와 몸체 사이의 괄호 안으로 제한됩니다. 따라서 반복 변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니라면 while 문보다 for 문을 쓰는 편이 낫습니다. 33 | > -출처: 이펙티브 자바 34 | - 이펙티브 자바 35 | 36 | 반복문은 독특한 방식으로 변수 범위를 최소화해줍니다는 내용은 Swift와 Java 모두 공통적으로 해당하는 말이지만 '반복 변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니라면 while 문보다 for 문을 쓰는 편이 낫습니다.' 이 말이 Swift 에도 해당하는지 확인해봅시다. 37 | 38 | ```swift 39 | // For-In Loops 40 | // Array 41 | let names = ["Anna", "Alex", "Brian", "Jack"] 42 | for name in names { 43 | print("Hello, \(name)!") 44 | } 45 | // Hello, Anna! 46 | // Hello, Alex! 47 | // Hello, Brian! 48 | // Hello, Jack! 49 | 50 | // Dictionary 51 | let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] 52 | for (animalName, legCount) in numberOfLegs { 53 | print("\(animalName)s have \(legCount) legs") 54 | } 55 | // cats have 4 legs 56 | // ants have 6 legs 57 | // spiders have 8 legs 58 | 59 | // with Numeric ranges 60 | for index in 1...5 { 61 | print("\(index) times 5 is \(index * 5)") 62 | } 63 | // 1 times 5 is 5 64 | // 2 times 5 is 10 65 | // 3 times 5 is 15 66 | // 4 times 5 is 20 67 | // 5 times 5 is 25 68 | ``` 69 | (위의 예는 Swift Language Guide 5.3에 있는 예제입니다. ) 70 | 71 | 그렇다면 이제 While 문을 살펴봅시다. 72 | 73 | ```java 74 | // 책에 나온 예시 75 | Iterator i = c.iterator(); 76 | while (i.hasNext()) { 77 | doSomething(i.next()); 78 | } 79 | ... 80 | Iterator i2 = c2.iterator(); 81 | while (i.hasNext()) { 82 | doSomething(i2.next()); // runtime error 83 | } 84 | ``` 85 | 아래는 위 Java 코드를 Swift로 변환한 예제입니다. 86 | ```swift 87 | let numbers = [2, 3, 5, 7] 88 | var numbersIterator = numbers.makeIterator() 89 | 90 | while let num = numbersIterator.next() { 91 | print(num) 92 | } 93 | // Prints "2" 94 | // Prints "3" 95 | // Prints "5" 96 | // Prints "7" 97 | 98 | while let num2 = numbersIterator.next() { 99 | print(num) // compile error 100 | } 101 | ``` 102 | 103 | Swift에서는 while문에 필요한 변수의 범위를 지정할 수 있습니다. 자바와 달리 while 조건문에 필요한 변수를 while문 범위 밖에 선언하지 않아도 됩니다. 즉, Java와 달리 Swift에서는 while과 for 키워드 모두 몸체 사이의 괄호 안으로 제한할 수 있습니다. 따라서 책에 나온 'while 문보다 for 문을 쓰는 편이 낫습니다.'는 Swift에서는 해당하지 않습니다. 104 | 105 | ### 다른 의견 106 | 107 | 다른 의견을 피드백으로 주신 내용을 함께 문서로 남깁니다. 108 | > * 자바 코드처럼 numbersIterator2를 만들어 놓으면 잘못 참조할 수 있는 것 같습니다. 따라서 `while 문 보다 for문을 쓰는 편이 낫습니다`는 Swift 에서도 유효한 것 같습니다. 109 | > * 다만 swift 의 iterator는 hasNext라는 메서드 없이 next 메서드만 있으므로 잘못된 참조를 덜 할 것 같긴 합니다만, 그럼에도 for 문을 사용하면 while 문 보다 지역변수를 최소화 할 수 있겠네요. 110 | > * 반복변수 `num`은 자바 코드에서도 while 문 범위 안에 선언되어 있습니다. 따라서 numbersIterator 만 해당하는 사항이니 글 수정하면 좋을 것 같습니다. 111 | 특히 `'while 문보다 for 문을 쓰는 편이 낫습니다.'는 Swift에서는 해당하지 않습니다.` 라는 말이 고쳐지면 좋을 것 같네요! 😃 112 | 113 | ```swift 114 | // 다른 지역 변수 참조가 가능한 while 문 115 | var numbersIterator = numbers.makeIterator() 116 | var numbersIterator2 = numbers2.makeIterator() 117 | 118 | while let num = numbersIterator.next() { 119 | print(num) 120 | } 121 | 122 | while let num2 = numbersIterator.next() { // 잘못 참조된 numbersIterator (원래는 numbersIterator2를 받아야 한다) 123 | print(num2) 124 | } 125 | ``` 126 | ```swift 127 | // 다른 지역 변수 참조가 불가능한 for 문 128 | var numbersIterator = numbers.makeIterator() 129 | var numbersIterator2 = numbers2.makeIterator() 130 | 131 | while let num = numbersIterator.next() { 132 | print(num) 133 | } 134 | 135 | while let num2 = numbersIterator.next() { // 잘못 참조된 numbersIterator (원래는 numbersIterator2를 받아야 한다) 136 | print(num2) 137 | } 138 | ``` 139 | 140 | #### 2. 메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다. 141 | 142 | 한 메서드에서 여러 가지 기능을 처리한다면 그 중 한 기능과만 관련된 지역변수라는 다른 기능을 수행하는 코드에서 접근할 수 있을 것입니다. 해결책은 단순한데, 단순히 메서드를 기능별로 쪼개면 됩니다. 143 | 144 | ### 참고 145 | 1. 이펙티브 자바 (Effective Java), Effective Java 3/E ,조슈아 블로크 146 | 2. [Control Flow - the swift programming language swift 5.4](https://docs.swift.org/swift-book/LanguageGuide/ControlFlow.html) 147 | 3. [next() - apple developer documentation](https://developer.apple.com/documentation/swift/iteratorprotocol/1641682-next) 148 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item58.md: -------------------------------------------------------------------------------- 1 | # item58. 전통적인 for문보다는 for-each문을 사용하라 2 | 3 | `Array`나 `Set`과 같은 컬렉션을 순회 할 때, for문을 사용할지, forEach문을 사용할지 고민됩니다. 두 방법 모두 순차적으로 element에 접근이 가능합니다. 메커니즘이 매우 유사해 선호도나 스타일에 따라 달리 사용하기도 하지만 몇가지 뚜렷한 면에서 차이가 있습니다. 책에서는 전통적인 for문과 향상된 for문(swift의 for in문)을 비교하고있는데, swift 3버전 이후로는 전통적인 for문을 이용할 수 없게 되어 이번 아이템에선 대표적으로 사용되는 swift의 for문과 forEach문의 차이점에 대해 알아봅니다. 4 | 5 | 6 | 7 | ### for문 8 | 9 | for문의 반복은 코드 제어 흐름 내에서 직접 수행되어 이러한 반복을 훨씬 더 정확하게 제어 할 수 있다는 것입니다. 예를 들어 `continue` 키워드를 이용해서 다음 요소로 넘어갈지 혹은 `break` 키워드를 이용해서 반복을 멈출지 결정할 수 있습니다. 10 | 11 | ```swift 12 | func categorizeDogs(among dogs: [Dog]) -> [Dog] { 13 | var results = [Dog]() 14 | 15 | for dog in dogs { 16 | guard dog.kind == .jindo else { 17 | // 즉시 다음 요소로 스킵 18 | continue 19 | } 20 | 21 | results.append(dog) 22 | 23 | guard results.count < 5 else { break } 24 | } 25 | 26 | return results 27 | } 28 | ``` 29 | 30 | for문에서는 `where`절을 적용할 수 있어 위와 같은 코드는 아래처럼 간결하게 사용할 수도 있습니다. 31 | ```swift 32 | for dog in dogs where dog.kind == .jindo { 33 | results.append(dog) 34 | 35 | guard results.count < 5 else { break } 36 | } 37 | ``` 38 | 이러한 흐름 제어 관련 기능(`continue`, `break`, `return`, `where`)은 for문을 선택하는 데 좋은 이유가 될 수 있습니다. 그리고 배열을 순회하면서 그 요소의 값 일부 혹은 전체를 변형하거나 선택된 요소를 제거해야 할 때와 여러 배열을 병렬로 순회하는 경우에도 for문을 이용하는 것이 좋습니다. 하지만 이러한 수준의 제어가 필요하지 않은 경우라면 forEach를 통해 조금 더 간단한 코드를 만들 수 있습니다. 39 | 40 |
41 | 42 | ### forEach 43 | 44 | ```swift 45 | @inlinable 46 | public func forEach(_ body: (Element) throws -> Void) rethrows { 47 | for element in self { 48 | try body(element) 49 | } 50 | } 51 | ``` 52 | 위의 코드는 forEach의 구현부입니다. closure 방식으로 사용되고 내부에선 for문을 사용해 모든 요소를 반복하고 각 요소를 파라메터로 사용해 요소의 갯수만큼 클로저를 호출하는 메서드임을 알 수 있습니다. 53 | 54 | ```swift 55 | dogs.forEach { dog in 56 | results.append(dog) 57 | } 58 | ``` 59 | 이처럼 사용이 가능하지만 for문에서 사용이 가능했던 `continue`, `break`, `return`, `where`등의 키워드는 forEach 내부에서는 사용이 불가능합니다. 컬렉션 타입의 모든 요소를 예외없이 반복하는 경우에 forEach를 사용하면 안전하게 사용할 수 있습니다. 60 | 61 |
62 | 63 | ### References 64 | - [SwiftBySundell](https://www.swiftbysundell.com/tips/picking-between-for-and-for-each/) 65 | - [DonnyWals](https://www.donnywals.com/choosing-between-a-for-loop-and-a-foreach/) 66 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item59.md: -------------------------------------------------------------------------------- 1 | # Item 59. 라이브러리를 익히고 사용하라 2 | 3 | 4 | 5 | ### 표준 라이브러리 사용의 5가지 이점 6 | 7 | #### 1. 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여러분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있습니다. 8 | 9 | #### 2. 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 됩니다. 10 | 11 | #### 3. 따로 노력하지 않아도 성능이 지속해서 개선됩니다. 12 | 13 | #### 4. 기능이 점점 많아집니다. 라이브러리에 부족한 부분이 있다면 개발자 커뮤니티에서 이야기가 나오고 논의된 후 다음 릴리스에 해당 기능이 추가되곤 합니다. 14 | 15 | #### 5. 개발자가 작성한 코드가 많은 사람에게 낯익은 코드가 됩니다. 자연스럽게 다른 개발자들이 더 읽기 좋고, 유지보수하기 좋고, 재활용하기 쉬운 코드가 됩니다. 16 | 17 | Swift에서 제공하는 표준 라이브러리는 어떤게 있을까요? 18 | Swift Standard Library는 Swift 프로그램을 작성하기 위한 기본 기능 계층(base layer of functionality)을 정의합니다. 19 | 20 | > Fundamental data types such as Int, Double, and String 21 | > Common data structures such as Array, Dictionary, and Set 22 | > Global functions such as print(_:separator:terminator:) and abs(_:) 23 | > Protocols, such as Collection and Equatable, that describe common abstractions. 24 | > Protocols, such as CustomDebugStringConvertible and CustomReflectable, that you use to customize operations that are available to all types. 25 | > Protocols, such as OptionSet, that you use to provide implementations that would otherwise require boilerplate code. 26 | 27 | ### 이런 표준 라이브러리의 기능에도 직접 구현해서 쓰는 경우, 그 이유는? 28 | 29 | 가능성 있는 이유 중 하나는 아마도 라이브러리에 그런 기능이 있는지 모르기 때문일 것입니다. 메이저 릴리스마다 주목할 만한 수많은 기능이 라이브러리에 추가됩니다. 30 | 31 | Swift의 경우는 매년 발표되는 WWDC 영상과 release note를 확인하면 되겠죠? 🤓 32 | 33 | 아래는 WWDC What's New in Swift 영상 모음입니다. 34 | 1. [What's New in Swift 2016](https://developer.apple.com/videos/play/wwdc2016/402/) 35 | : the first major release built with the open source community. Gain insight into the latest changes in Xcode including enhanced migration support to help move your code to Swift 3 36 | 2. [What's New in Swift 2017](https://developer.apple.com/videos/play/wwdc2017/402/) 37 | : new String and improved generics, see how Swift 4 maintains support for your existing Swift 3 code 38 | 3. [What's New in Swift 2018](https://developer.apple.com/videos/play/wwdc2018/401/) 39 | : improvements to build times, code size, and runtime performance 40 | 4. [What's New in Swift 2019](https://developer.apple.com/videos/play/wwdc2019/402/) 41 | : Swift is now the language of choice for a number of major frameworks across all of Apple's platforms, including SwiftUI, RealityKit and Create ML. Join us for a review of Swift 5.0 and an exploration of Swift 5.1, new in Xcode 11. 42 | 5. [What's New in Swift 2020](https://developer.apple.com/videos/play/wwdc2020/10170/) 43 | : Discover the latest advancements in runtime performance, along with improvements to the developer experience that make your code faster to read, edit, and debug. Find out how to take advantage of new language features like multiple trailing closures. Learn about new libraries available in the SDK, and explore the growing number of APIs available as Swift Packages. 44 | 45 | 46 | ### 라이브러리가 필요한 기능을 충분히 제공하지 못할 경우 47 | 이런 경우에도 우선은 라이브러리를 사용하려고 시도해봅시다. 어떤 영역의 기능을 제공하는지 살펴보고 원하는 기능이 아니라고 판단되면 대안을 사용합시다. 이런 경우 대안은 고품질의 서드파티 라이브러리가 될 수 있습니다. 48 | 49 | EX) 대표적인 Third Party 라이브러리: 50 | 51 | 1. [Alamofire](https://github.com/Alamofire/Alamofire) 52 | : Alamofire is an elegant and composable way to interface to HTTP network requests. It builds on top of Apple’s [URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system/) provided by the Foundation framework. At the core of the system are *URLSession* and *URLSessionTask* subclasses. 53 | 54 | 2. [RxSwift](https://github.com/ReactiveX/RxSwift) 55 | : Rx's intention is to enable easy composition of asynchronous operations and event/data streams. KVO observing, async operations and streams are all unified under abstraction of sequence. This is the reason why Rx is so simple, elegant and powerful. 56 | 57 | 3. [Kingfisher](https://github.com/onevcat/Kingfisher) 58 | : powerful, pure-Swift library for downloading and caching images from the web. It will download the image from url, send it to both memory cache and disk cache, and display it in an UIImageView, NSImageView, NSButton or UIButton. When you try to retrieve an image with the same URL later, the image will be retrieved from cache and shown immediately. 59 | 60 | 4. [Lottie](https://github.com/airbnb/lottie-ios) 61 | : ince creating animations by hand using UIView or CoreGraphics animations can prove to be quite challenging and time-consuming, Lottie provides us with a perfect tool to incorporate desinger's creations into our applications. Lottie loads and renders animations and vectors exported in the bodymovin JSON format. 62 | 63 | 5. [SnapKit](https://github.com/SnapKit/SnapKit) 64 | : SnapKit is an Auto Layout library that simplifies writing auto layout in code with a minimal amount of code needed without losing readability. It is type safe by design to help you avoid programming errors while coding your user interface. 65 | 66 | 6. [Realm](https://realm.io/) 67 | : Apple’s Core Data can probably fulfill most of your persistence needs, but it was always likely that something would come and do it better. Realm is one such alternative, providing a framework that is faster and easier to work with. 68 | 69 | ### 핵심 정리 70 | 71 | > 바퀴를 다시 발명하지 맙시다. 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 큽니다. 그런 라이브러리가 있다면, 쓰면 됩니다. 있는지 잘 모르겠다면 찾아보는 걸 권장합니다. 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 큽니다. 여러분의 실력을 폄하하는 게 아닙니다. 코드 품질에도 규모의 경제가 적용됩니다. 즉, 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 훨씬 많이 받으므로 코드 품질도 그만큼 높아집니다. 72 | -출처: 이펙티브 자바 73 | 74 | ### 참고 75 | 1. 이펙티브 자바 (Effective Java), Effective Java 3/E ,조슈아 블로크 76 | 2. [Swift Standard Library](https://developer.apple.com/documentation/swift/swift_standard_library) 77 | 3. [Top 10 Most Useful iOS Libraries in 2020](https://infinum.com/the-capsized-eight/top-10-most-useful-iOS-libraries) -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item60.md: -------------------------------------------------------------------------------- 1 | # Item.60 정확한 답이 필요하다면 float과 double은 피하라 2 | 3 | ## 부동소수 타입 float, double 4 | 5 | float과 double 타입은 과학과 공학 계산용으로 설계되었습니다. 이진 부동소수점 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 세심하게 설계되었습니다. **따라서 정확한 결과가 필요할 때는 사용하면 안됩니다. float과 double타입은 특히 금융 관련 계산과는 맞지 않습니다.** 0.1 혹은 10의 음의 거듭제곱 수(10^-1,10^-2 등)를 표현할 수 없기 때문입니다. 6 | 7 | ```swift 8 | print(1.03 - 0.42) 9 | 10 | // 출력 결과 11 | // 0.6100000000000001 12 | ``` 13 | => 실제로 계산해보면 결과는 0.61이지만 부동소수점 오차 때문에 0.6100000000000001이 출력됩니다. 14 | 15 | ```swift 16 | print(1.00 - 9 * 0.10) 17 | 18 | // 출력 결과 19 | // 0.09999999999999998 20 | ``` 21 | => 실제로 계산해보면 결과는 0.1이지만 부동소수점 오차 때문에 0.09999999999999998이 출력됩니다. 22 | 23 | 결과값을 출력하기 전에 반올림하면 해결되리라 생각할지 모르지만, 반올림을 해도 틀린 답이 나올 수 있습니다. 24 | 25 | * 참고로 Swift에서 소수(ex. 1.03, 0.42)를 타입을 명시하지 않고 사용하면 타입 추론으로 Double 타입이 됩니다. 26 | 27 | ## 금융 계산에 부동소수 타입을 사용한 경우 28 | 29 | 주머니에는 1달러가 있고, 선반에 10센트, 20센트, 30센트, ... 1달러짜리의 맛있는 사탕이 놓여 있다고 해봅시다. 10센트짜리부터 하니씩, 살 수 있을 때까지 사봅시다. 사탕을 몇 개나 살 수 있고, 잔돈은 얼마나 남을까요? 다음은 이 문제의 답을 구하는 '어설픈' 코드입니다. 30 | 31 | ```swift 32 | var funds: Double = 1.00 33 | var itemsBought: Int = 0 34 | 35 | var price = 0.1 36 | while funds >= price { 37 | funds -= price 38 | itemsBought += 1 39 | price += 0.1 40 | } 41 | 42 | print("\(itemsBought) 개 구입") 43 | print("잔돈(달러): \(funds)") 44 | 45 | // 출력 결과 46 | // 3 개 구입 47 | // 잔돈(달러): 0.3999999999999999 48 | ``` 49 | 50 | 프로그램을 실행해보면 사탕 3개를 구입한 후 잔돈은 0.3999999999999999 달러가 남게 됩니다. 물론 잘못된 결과입니다! 51 | 이 문제를 올바로 해결하려면 어떻게 해야 할까요? 52 | 53 | **금융 계산에는** 54 | 55 | * **자바의 경우 `BigDecimal`, 스위프트의 경우 `Decimal` 사용하기** 56 | * **자바의 경우 `int` 혹은 `long`, 스위프트의 경우 `Int` 사용하기** 57 | 58 | **이렇게 두 가지 방법이 있습니다.** 59 | 60 | ## 금융 계산에 Decimal 타입을 사용한 경우 61 | 62 | 코코아 Foundation 프레임워크는 10진수 관련 계산할 때 유용한 `NSDecimalNumber`(swift 로는 Decimal) 클래스를 제공합니다. 63 | 64 | ```swift 65 | public init?(string: String, locale: Locale? = nil) 66 | ``` 67 | 68 | 자바의 BigDecimal 처럼 스위프트도 Decimal 도 문자열을 받는 생성자가 있습니다. 문자열에 불필요한 값이 포함된다면 생성자는 nil을 반환합니다. 69 | 70 | ```swift 71 | guard let tenCents = Decimal(string: "0.1") else { return } 72 | guard var funds = Decimal(string: "1.00") else { return } 73 | 74 | var itemsBought: Int = 0 75 | var price: Decimal = tenCents 76 | while price.isLessThanOrEqualTo(funds) { 77 | funds -= price 78 | itemsBought += 1 79 | price += tenCents 80 | } 81 | 82 | print("\(itemsBought) 개 구입") 83 | print("잔돈(달러): \(funds)") 84 | 85 | // 출력 결과 86 | // 4 개 구입 87 | // 잔돈(달러): 0 88 | ``` 89 | => 이 프로그램을 실행하면 사탕 4개를 구입한 후 잔돈은 0달러가 남습니다. 드디어 올바른 답이 나왔습니다. 90 | 91 | 하지만 Decimal을 이용한 십진 계산에도 장단점은 있습니다. 92 | 93 | * 장점: 정수와 소수를 정확하게 나타내며, 부동소수 타입보다는 느리지만 그럼에도 빠릅니다. 94 | * 단점: 부동 소수 타입보다는 한 자리수 이상 느립니다. 95 | 96 | 이 [글](https://forums.swift.org/t/provide-native-decimal-data-type/4003/4)을 참고했습니다. 97 | 98 | ## 금융 계산에 정수타입을 사용한 경우 99 | 100 | Decimal의 대안으로 Int 타입을 쓸 수 있습니다. 그럴 경우 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해야 합니다. 101 | 이번 예에서는 모든 계산을 달러 대신 센트로 수행하면 이 문제가 해결됩니다. 다음은 이 방식으로 구현해본 코드입니다. 102 | 103 | ```swift 104 | var funds: Int = 100 105 | var itemsBought: Int = 0 106 | var price: Int = 10 107 | 108 | while funds >= price { 109 | funds -= price 110 | itemsBought += 1 111 | price += 10 112 | } 113 | 114 | print("\(itemsBought) 개 구입") 115 | print("잔돈(달러): \(funds)") 116 | 117 | // 출력결과 118 | // 4 개 구입 119 | // 잔돈(달러): 0 120 | ``` 121 | 122 | ## Decimal의 반올림 모드: NSDecimalNumber.RoundingMode 123 | 124 | Decimal도 자바의 BigDecimal처럼 반올림 모드(RoundingMode)를 제공합니다. 125 | 자바의 BigDecimal은 반올림 모드가 8가지이지만, 스위프트의 Decimal은 4가지 모드를 제공합니다. 126 | 127 | ```swift 128 | public enum RoundingMode : UInt { 129 | case plain = 0 130 | case down = 1 131 | case up = 2 132 | case bankers = 3 133 | } 134 | ``` 135 | 136 | ## Decimal의 반올림 메소드 137 | 138 | Deciamal는 또한 반올림 모드(RoundingMode)를 인수로 사용하는 반올림 메소드를 제공합니다. 139 | 140 | > `NSDecimalNumber rounding(accordingToBehavior:) method` 을 사용하는 경우 141 | 142 | ```swift 143 | let scale: Int16 = 3 144 | 145 | let behavior = NSDecimalNumberHandler(roundingMode: .plain, scale: scale, raiseOnExactness: false, raiseOnOverflow: false, raiseOnUnderflow: false, raiseOnDivideByZero: true) 146 | 147 | let roundedValue1 = NSDecimalNumber(value: 0.6844).rounding(accordingToBehavior: behavior) 148 | let roundedValue2 = NSDecimalNumber(value: 0.6849).rounding(accordingToBehavior: behavior) 149 | 150 | print(roundedValue1) 151 | print(roundedValue2) 152 | 153 | // 출력 결과 154 | // 0.684 155 | // 0.685 156 | ``` 157 | 158 | > `NSDecimalRound(_:_:_:_:) function` 을 사용하는 경우 159 | 160 | ```swift 161 | var value1 = Decimal(0.6844) 162 | var value2 = Decimal(0.6849) 163 | 164 | var roundedValue1 = Decimal() 165 | var roundedValue2 = Decimal() 166 | 167 | NSDecimalRound(&roundedValue1, &value1, scale, NSDecimalNumber.RoundingMode.plain) 168 | NSDecimalRound(&roundedValue2, &value2, scale, NSDecimalNumber.RoundingMode.plain) 169 | 170 | print(roundedValue1) 171 | print(roundedValue2) 172 | 173 | // 출력 결과 174 | // 0.684 175 | // 0.685 176 | ``` 177 | 178 | ### 참고 179 | 180 | * [Provide native Decimal data type](https://forums.swift.org/t/provide-native-decimal-data-type/4003/1) 181 | * [Decimal](https://developer.apple.com/documentation/foundation/decimal) 182 | * [NSDecimalNumber](https://developer.apple.com/documentation/foundation/nsdecimalnumber) 183 | * [NSDecimalNumber.RoundingMode](https://developer.apple.com/documentation/foundation/nsdecimalnumber/roundingmode) 184 | * [Rounding a double value to x number of decimal places in swift 185 | ](https://stackoverflow.com/questions/27338573/rounding-a-double-value-to-x-number-of-decimal-places-in-swift) 186 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item62.md: -------------------------------------------------------------------------------- 1 | # Item 62 다른 타입이 적절하다면 문자열 사용을 피하라 2 | 3 | 4 | 5 | ## 문자열을 쓰지 않아야 할 사례들 6 | 7 |
8 | 9 | ### 1. 문자열은 다른 값 타입을 대신하기에 적합하지 않습니다. 10 | 11 | 입력받는 데이터가 문자열일 때만 문자열을 사용하도록 합니다. 12 | 13 | **데이터가 수치형일 때** 14 | 15 | ```swift 16 | // 잘못된 예 17 | struct Person { 18 | let name: String 19 | let age: String 20 | } 21 | 22 | let gwonii: Person = .init(name: "gwonii", age: "29") 23 | 24 | // 올바른 예 25 | struct Person { 26 | let name: String 27 | let age: Int 28 | } 29 | 30 | let gwonii: Person = .init(name: "gwonii", age: 29) 31 | 32 | **데이터가 "예/아니오" 로 구분될 때** 33 | 34 | ```swift 35 | // 잘못된 예 36 | struct Car { 37 | let type: CarType 38 | let name: String 39 | 40 | // 2륜, 4륜 등의 구동방식 41 | let drivingSystem: String 42 | } 43 | 44 | let dreamCar: Car = .init(type: .convertible, name: "911", drivingSystem: "2wd") 45 | 46 | // 올바른 예 47 | struct Car { 48 | let type: CarType 49 | let name: String 50 | let is4WD: Bool 51 | } 52 | 53 | let dreamCar: Car = .init(type: .convertible, name: "911", is4WD: false) 54 | 55 | **데이터가 열거형으로 구분될 때** 56 | 57 | ```swift 58 | struct Car { 59 | let type: CarType 60 | let name: String 61 | let drivingSystem: DrivingSystem 62 | } 63 | 64 | enum DrivingSystem: CustomStringConvertible { 65 | case FF 66 | case FR 67 | case MR 68 | case RR 69 | case 4WD 70 | 71 | var description: String { 72 | switch self { 73 | case .FF: 74 | return "frontEngineFrontWheel" 75 | case .FR: 76 | return "frontEngineRearWheel" 77 | //... 이하 생략 78 | } 79 | } 80 | } 81 | let dreamCar: Car = .init(type: .convertible, name: "911", drivingSystem: .FR) 82 | 83 | 84 |
85 | 86 | ### 2. 문자열은 혼합 타입을 대신하기에 적합하지 않다. 87 | 88 | 혼합된 데이터를 하나의 문자열로 표현하는 것은 대체로 좋지 않은 생각입니다. 89 | 90 | ``` 91 | String compoundKey = className + "#" + i.next() 92 | 93 | ``` 94 | 95 | 일하면서 직접 작성하게 된 코드... 96 | 97 | ``` 98 | // LocalNotification ID 99 | let identifier: String = "\\(commuteType.description)_\\(weekday)_\\(count)" 100 | 101 | ``` 102 | 103 | **단점** 104 | 105 | - 직접 출력된 문자열만 봐서는 내용을 정확히 인지할 수 없습니다. 106 | - 요소를 개별적으로 접근하려고 할 때는 불필요한 파싱 작업이 생깁니다. 107 | - 오류의 가능성이 큽니다. 108 | 109 | **개선방안** 110 | 111 | ```swift 112 | struct LocalNotificationIdentifer { 113 | static func isEqualCommuteType(lhs: Self, rhs: Self) -> Bool { 114 | return lhs.commuteType.rawValue == rhs.commuteType.rawValue 115 | } 116 | 117 | static func isEqualWeekday(lhs: Self, rhs: Self) -> Bool { 118 | return lhs.weekday.rawValue == rhs.weekday.rawValue 119 | } 120 | 121 | let commuteType: CommuteType 122 | let weekday: Weekday 123 | let number: Int 124 | } 125 | 126 | 이런식으로 객체를 만들어서 활용할 수 있습니다. 127 | 128 |
129 | 130 | ### 3. 문자열은 권한을 표현하기에 적합하지 않습니다. 131 | 132 | ```java 133 | public class ThreadLocal { 134 | private ThreadLocal() { } // 외부에서 객체 생성 불가 135 | 136 | public static void set(String key, Object: value); 137 | 138 | public static Object get(String key); 139 | } 140 | 141 | 142 | `String` 키값을 이용하여 쓰레드를 구분하고 있습니다. 143 | 144 | 위와 같은 방식은 두 클라이언트가 서로 소통하지 않고 키값을 사용하게 된다면, 두 클라이언트는 제대로 기능을 하지 못할 것입니다. 145 | 146 | ```java 147 | public class ThreadLocal { 148 | private ThreadLocal() { } 149 | 150 | public static class Key { 151 | Key() {} 152 | } 153 | 154 | public static Key getKey() { 155 | return new Key(); 156 | } 157 | 158 | public static void set(Key key, Object: value); 159 | public static Object get(Key key); 160 | } 161 | 162 | // ThreadLocal 클래스 자체를 Key값으로 사용하기 163 | public final class ThreadLocal { 164 | public ThreadLocal(); 165 | public void set(T value); 166 | public T get(); 167 | } 168 | 169 | 위의 코드에서는 Thread 별로 고유한 키값을 맵핑시킬 수 있습니다. 이로써 Thread의 권한을 적절하게 표현할 수 있게 되었습니다. -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item63.md: -------------------------------------------------------------------------------- 1 | # item 63. 문자열 연결은 느리니 주의하라 2 | 3 | 문자열 연결 연산자(+)는 여러 문자열을 하나로 합쳐주는 편리한 수단입니다. 한 줄짜리 출력값이나 작고 크기가 고정된 객체의 문자열 표현을 만들 때라면 괜찮지만, 많은 양의 반복적인 사용을 하는 경우 성능 저하를 감내하기 어렵습니다. 그러므로 `Effective Java` 저자 조슈아 블로크는 성능에 신경을 써야 한다면 많은 문자열 연결 시 문자열 연결 연산자(+)를 피하라고 말하고 있습니다. Swift에서도 동일하게 적용되는 제안인지 확인해보았습니다. 4 | 5 | Swift로 문자열 연결을 할 수 있는 방법에는 몇 가지가 있습니다. 그 중 가장 많이 사용되는 방법들을 골라 아래와 같이 일정 갯수만큼 반복하게 해 걸리는 시간을 측정해보았습니다. 6 | 7 | 두가지 방법으로 테스트를 해보았는데, 첫번째는 아래처럼 `CFAbsoluteTimeGetCurrent` 를 이용해 시간을 측정했고, 두번째는 하단 이미지처럼 테스트 코드 내 `measure` 메서드를 이용했습니다. 8 | 9 | ```swift 10 | let lineForItem = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" 11 | let number = 1000 12 | 13 | func bench(function: () -> Void, number: Int) { 14 | let startTime = CFAbsoluteTimeGetCurrent() 15 | function() 16 | let processTime = CFAbsoluteTimeGetCurrent() - startTime 17 | print("process time\(number) = \(processTime)") 18 | } 19 | 20 | func test1() { 21 | var result = "" 22 | for _ in 1...number { 23 | result += lineForItem 24 | } 25 | } 26 | 27 | func test2() { 28 | var result = "" 29 | for _ in 1...number { 30 | result.append(lineForItem) 31 | } 32 | } 33 | 34 | func test3() { 35 | var result = "" 36 | for _ in 1...number { 37 | result = "\(result)\(lineForItem)" 38 | } 39 | } 40 | ``` 41 | 42 | 43 | 44 | 두가지 방법으로 테스트 해 본 결과 각각 `+ 연산자` 는 0.0015139597, `append()` 는 0.0005130767, `string interpolation` 은 0.003707051277만큼 걸려 `+ 연산자` , `string interpolation`, `append()` 방법으로 빨랐습니다. 하지만 `test2()` 에서 `append()` 메서드를 이용한 경우 오차범위가 ±185%이어서 신뢰할만한 측정값은 아니라는 결론을 내렸습니다. (스크린샷 참조) 45 | 46 | 하지만 책에서 말하는 것처럼 많은 양의 문자열을 합칠 때 + 연산자를 사용하는 것이 `string interpolation`이나 `append` 메서드를 이용하는 것과 크게 차이가 나지 않으므로 딱히 지양하지는 않아도 될 것 같습니다. 47 | 48 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item64.md: -------------------------------------------------------------------------------- 1 | # item64. 객체는 인터페이스를 사용해 참조하라 2 | 3 | > 객체는 상위 타입의 객체를 사용해 참조하라 4 | 5 | `객체는 클래스가 아닌 프로토콜로 혹은 상위 타입의 객체를 이용하여 참조` 하십시오. 적합한 프로토콜만 있다면 가능한 매개변수뿐 아니라 반환값, 변수, 필드를 전부 프로토콜 타입으로 선언하십시오. 객체의 실제 클래스를 사용해야 할 상황은 **오직** 생성자로 생성할 때 뿐입니다. 프로토콜을 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해지는 장점이 있습니다. 나중에 구현 클래스를 교체하고자 한다면 그저 새 클래스의 생성자나 다른 정적 팩터리를 호출해주기만 하면 됩니다. 6 | 7 | ```swift 8 | protocol Vehicle { 9 | func transport() 10 | } 11 | 12 | class Bicyle: Vehicle { 13 | func transport() { 14 | //... 15 | } 16 | } 17 | 18 | 19 | // 좋은 예. 프로토콜을 타입으로 사용함 20 | let sonVehicle: Vehicle = Bicycle() 21 | 22 | // 나쁜 예. 클래스를 타입으로 사용함 23 | let sonVehicle: Bicycle = Bicycle() 24 | ``` 25 | 26 | 27 | 28 | 그러면 앞선 선언은 아래처럼도 다른 코드는 전혀 손대지 않고 새로 구현한 클래스로 교체가 가능합니다. 한가지 주의 할 점은, 원래의 클래스가 프로토콜의 일반 규약 이외의 특별한 기능을 제공하며, 주변 코드가 이 기능에 기대어 동작한다면 새로운 클래스도 반드시 같은 기능을 제공해야 합니다. 29 | 30 | ```swift 31 | class Car: Vehicle { 32 | func transport() { 33 | //... 34 | } 35 | } 36 | 37 | let sonVehicle: Vehicle = Car() 38 | ``` 39 | 40 | 41 | 42 | 하지만 이러한 제안이 모든 상황에 적용되지는 않고, 아래와 같은 상황은 프로토콜이 아닌 클래스로 참조해야 합니다. 43 | 44 | 1. String, Int와 같은 타입처럼 적합한 인터페이스가 없는 경우 45 | 2. 프레임워크가 제공하는 객체들 중 클래스 기반으로 작성된 경우 46 | 3. 인터페이스에는 없는 별개의 메서드를 제공하는 클래스인 경우 47 | 48 | 49 | 50 | 실전에서는 주어진 객체를 표현할 적절한 프로토콜 혹은 상위 타입의 객체가 있는지 찾아서 해당 객체로 참조하면 더 유연하고 세련된 프로그램을 만들 수 있습니다. **적합한 객체가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인 (상위의)클래스를 타입으로 사용합니다.** -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item65.md: -------------------------------------------------------------------------------- 1 | # Item 65. 리플렉션보다는 인터페이스를 사용하라 2 | 3 |
4 | 5 | ### 리플렉션이란? 6 | 7 | 객체를 통해 클래스 정보를 분석하는 프로그램 기법입니다. 8 | 9 | 구체적인 클래스 타입을 알지 못하더라도, 컴파일된 코드를 분석하여 역으로 클래스 정보를 알아내어 클래스를 사용할 수 있게 합니다. 10 | 11 | 리플렉션은 정적/동적으로 클래스를 생성하고 교체하는 방식으로 프레임워크의 유연성을 높히기 위해 사용된다고 합니다. 12 | 13 | ```java 14 | Class testClass = TestClass.class 15 | 16 | // 클래스 이름을 통해서 클래스를 동적 로딩하는 메소드입니다. 17 | Class testObejct = Class.forName(TestClass) 18 | 19 | ``` 20 | 21 | `runtime` 에 동적으로 원하는 라이브러리를 로딩할 수 있지만, 컴파일타임에는 오류를 감지할 수 없어 런타임에 에러가 발생할 수도 있습니다. 22 | 23 |
24 | 25 | ### 리플렉션의 단점 26 | 27 | - 컴파일타임 타입 검사가 주는 이점을 사용할 수 없습니다. 28 | 29 | 동적으로 클래스를 생성하고 사용하려고 했을 때, 개발자의 실수로 없는 클래스를 생성하거나 `deprecated` 클래스를 생성하여 에러가 발생할 수 있습니다. 30 | 31 | - 리플렉션을 이용하면 코드가 지저분하고 장황해집니다. 32 | 33 | 리플렉션을 통해 클래스 객체를 생성하거나 클래스 메소드를 사용하려고 할 때 수많은 예외처리를 해야합니다. 34 | 35 | - 성능이 떨어집니다. 36 | 37 | 리플렉션을 통한 메소드 호출은 일반 메소드 호출보다 훨씬 느립니다. 38 | 39 |
40 | 41 | ### 리플렉션을 사용해야 하는 경우 42 | 43 | 리플렉션은 코드 분석 도구나 의존관계 주입 프레임워크에 사용됩니다. 44 | 45 | - 리플렉션을 사용할 때는 아주 제한된 형태로만 사용되어야 단점을 피하고 이점을 얻을 수 있습니다. 46 | 47 |
48 | 49 | ### 리플렉션과 인터페이스의 사용 50 | 51 | 위에서도 알 수 있듯이 리플렉션을 사용하는 것에는 여러 패널티들이 있습니다. 그렇기 때문에 꼭 사용해야 되는 상황이라면 최소한으로 사용하고 인터페이스를 함께 사용하도록 해야 합니다. 52 | 53 |
54 | 55 | **리플렉션으로 생성하고 인터페이스를 참조하는 경우** 56 | 57 | ```java 58 | public static void main(String[] args) { 59 | 60 | // Class 객체 61 | Class targetClass = null; 62 | 63 | try { 64 | targetClass = (Class>) 65 | Class.forName(args[0]); 66 | } catch (ClassNotFoundException e) { 67 | fatalError("클래스를 찾을 수 없습니다.") 68 | } 69 | 70 | // 생성자 71 | Constructor> constructor = null; 72 | 73 | try { 74 | constructor = targetClass.getDeclaredConstructor(); 75 | } catch (NoSuchMethodException e) { 76 | fatalError("메소드를 찾을 수 없습니다."); 77 | } 78 | 79 | // 인스턴스 80 | Set instance = null; 81 | 82 | try { 83 | instance = constructor.newInstance(); 84 | } catch (IllegalAccessException e) { 85 | fatalError("생성자에 접근할 수 없습니다.") 86 | } 87 | . 88 | . 89 | . // 여러 error 처리 90 | 91 | instance.addAll(Arrays.asList(args).sublist(1, args.length)); 92 | system.out.println(instance); 93 | } 94 | 95 | ``` 96 | 97 | 위의 코드는 인스턴스를 생성할 때만 리플렉션을 사용하고 98 | 99 | 이후 메소드들은 리플렉션을 통해 호출하는 것이 아니라 인터페이스의 메소드를 이용하여 호출하고 있습니다. 100 | 101 | 이렇게 리플렉션의 사용은 제한적으로 하고 인터페이스를 활용하는 것이 좋습니다. 102 | 103 | - **(Mirror Document)** **https://developer.apple.com/documentation/swift/mirror** 104 | - NSClassFromString 105 | 106 | -------------------------------------------------------------------------------- /9장_일반적인_프로그래밍_원칙/item67.md: -------------------------------------------------------------------------------- 1 | # item 67. 최적화는 신중히 하라 2 | 3 | ##### *(맹목적인 어리석음을 포함해) 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다(심지어 효율을 높이지도 못하면서).* *\- 윌리엄 울프(Wulf72)* 4 | 5 | ##### *(전체의 97% 정도인) 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다.* *\- 도널드 크누스(Knuth74)* 6 | 7 | ##### *최적화를 할 때는 다음 두 규칙을 따르라.* *첫 번째, 하지 마라.* *두 번째, (전문가 한정) 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.* *\- M. A. 잭슨(Jackson75)* 8 | 9 | 최적화는 좋은 결과보다는 해로운 결과로 이어지기 쉽고, 섣불리 진행하면 특히 더 그렇습니다. 빠르지도 않고 제대로 동작하지도 않으면서 수정하기는 어려운 소프트웨어를 탄생시키는 것입니다. 10 | 11 |
12 | 13 | ## 설계 시 고려할 점 14 | 15 | ### 빠른 프로그램보다는 좋은 프로그램을 작성하라. 16 | 17 | 성능 때문에 견고한 구조를 희생하지 맙시다. 좋은 프로그램이지만 원하는 성능이 나오지 않는다면 그 아키텍처 자체가 최적화할 수 있는 길을 안내해줄 것입니다. 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있습니다. 따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있습니다.([아이템 15 - 클래스와 멤버의 접근 권한을 최소화하라](https://github.com/TheSwiftists/effective-swift/blob/main/4장_클래스와_인터페이스/item15.md)) 18 | 19 | 프로그램을 완성할 때까지 성능 문제를 무시하라는 뜻이 아닙니다. 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하기 불가능할 수 있습니다. 완성된 설계의 기본 틀을 변경하려다 보면 유지보수하거나 개선하기 어려운 꼬인 구조의 시스템이 만들어지기 쉽기 때문입니다. 그래서 설계 단계에서 반드시 성능을 염두에 두어야 합니다. 20 | 21 | ### 성능을 제한하는 설계를 피하라. 22 | 23 | 완성 후 변경하기가 가정 어려운 설계 요소는 바로 컴포넌트끼리, 혹은 외부 시스템과 하는 소통 방식입니다. API, 네트워크 프로토콜 등이 대표적입니다. 이런 설계 요소들은 완성 후에는 변경하기 어렵거나 불가능 할 수 있고 시스템 성능을 심각하게 제한할 수 있습니다. 24 | 25 | ### API를 설계할 때 성능에 주는 영향을 고려하라. 26 | 27 | public 타입을 가변으로 만들면, 즉 내부 데이터를 변경할 수 있게 만들면 불필요한 방어적 복사를 수없이 유발할 수 있습니다(아이템 50 - 적시에 방어적 복사본을 만들라). 비슷하게, 컴포지션으로 해결할 수 있음에도 상속 방식으로 설계한 public 클래스는 상위 클래스에 영원히 종속되며 그 성능 제약까지도 물려받게 됩니다([아이템 18 - 상속보다는 컴포지션을 사용하라](https://github.com/TheSwiftists/effective-swift/blob/main/4장_클래스와_인터페이스/item18.md)) 인터페이스도 있는데 굳이 구현타입을 사용하는 것도 좋지 않습니다. 특정 구현체에 종속되게 하여 나중에 더 빠른 구현체가 나오더라도 이용하지 못하게 됩니다(아이템 64 - 객체는 인터페이스를 사용해 참조하라). 28 | 29 | ### 성능을 위해 API를 왜곡하지 말라. 30 | 31 | 잘 설계된 API는 보통 성능도 좋습니다. 그러므로 성능을 위해 API를 왜곡하는 건 매우 좋지 않습니다. API를 왜곡하도록 만든 그 성능 문제는 해당 플랫폼이나 소프트웨어의 다음 버전에서 사라질 수도 있지만, 왜곡된 API와 이를 지원하는 데 따르는 고통은 더 오래 지속될 것입니다. 32 | 33 | 신중한 설계를 바탕으로 한 명확한 구조를 가진 프로그램을 완성한 다음에야 최적화를 고려하십시오. 물론 성능에 만족하지 못할 경우에만입니다. 34 | 35 | ### 각각의 최적화 시도 전후로 성능을 측정하라 36 | 37 | 아마 측정 결과에 놀랄지도 모릅니다. 시도한 최적화 기법이 성능을 눈에 띄게 높이지 못하는 경우가 많고, 심지어 더 나빠지게 할 수도 있습니다. 주요 원인은 우리의 프로그램에서 시간을 잡아먹는 부분을 추측하기 어렵기 때문입니다. 느릴 거라고 생각한 부분이 사실은 성능에 별 영향을 주지 않는 곳이라면 시간만 허비한 셈입니다. 일반적으로 90%의 시간을 단 10%의 코드에서 사용한다는 사실을 기억하십시오. 38 | 39 | 프로파일링 도구(profiling tool)는 최적화 노력을 어디에 집중해야 할지 찾는 데 도움을 줍니다. 이런 도구는 개별 메서드의 소비 시간과 호출 횟수 같은 런타입 정보를 제공해, 집중 할 곳과 알고리즘을 변경해야 한다는 사실을 알려줍니다. 40 | 41 | 42 | 43 |
44 | 45 | ## 정리 46 | 47 | 좋은 프로그램을 작성하다 보면 성능은 따라오게 마련입니다. 하지만 시스템을 설계할 때, 특히 API, 네트워크 프로토콜등을 설계 할 때는 성능을 염두에 두십시오. 이것들이 완료 된 이후에는 성능을 측정해보고 개선 할 필요성이 있는 경우 프로파일러를 사용해 문제의 원인이 되는 지점을 찾아 최적화를 수행하면 됩니다. 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 swiftyus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | --------------------------------------------------------------------------------