├── .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 | 
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 extends E> 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 extends E> 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 extends E> 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 | 
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