|
| 1 | +# 변화로부터 코드를 보호하려면 추상화를 사용하라 |
| 2 | + |
| 3 | +- 추상화란, 코드의 세부 구현을 감추고, 외부에는 필요한 부분만 노출하는 것을 의미 |
| 4 | + |
| 5 | +함수, 클래스를 추상화하고 실질적인 코드를 숨기면 |
| 6 | +사용자는 세부 사항을 몰라도 괜찮고, 원하는 대로 수정할 수도 있다 |
| 7 | + |
| 8 | +서론에서 언급했던 자동차 인터페이스 처럼 |
| 9 | +운전대, 페달 등을 조작할 줄만 알면 그 자동차의 내부 구조, 작동 원리에 대해 몰라도 된다 |
| 10 | + |
| 11 | +그래서 자동차 제조업체는 더 친환경적이고, 더 많은 센서를 추가한 자동차를 안전하게 만들 수 있다 |
| 12 | + |
| 13 | +이번 장에서는 추상화를 통해 코드를 보호하는 세 가지 사례를 살펴본다 |
| 14 | + |
| 15 | +## 상수 |
| 16 | + |
| 17 | +첫 번째로 상수이다. |
| 18 | + |
| 19 | +우리가 개발할 때 비밀번호 최소 길이 등을 상수로 빼는 것이 이와 관련되어 있다 |
| 20 | + |
| 21 | +```kotlin |
| 22 | +const val MIN_PASSWORD_LENGTH = 7 |
| 23 | + |
| 24 | +fun isPasswordValid(text: String) : Boolean { |
| 25 | + if(text.length < MIN_PASSWORD_LENGTH) return false |
| 26 | + if(text.length < 7) return false |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +직접 “7”을 입력하는 것보다 “MIN_PASSWORD_LENGTH” 라는 상수로 뺌으로써 |
| 31 | +“7”의 의미를 빠르고 쉽게 이해할 수 있게 된다 |
| 32 | + |
| 33 | +비밀번호 최소 길이를 변경하기도 쉬워진다 |
| 34 | +함수 내부 로직은 몰라도, 상수 값만 변경하면 된다 |
| 35 | + |
| 36 | +따라서, 이렇게 두 번 이상 사용되는 값은 상수로 추출하자 |
| 37 | + |
| 38 | +> 상수로 추출하면, |
| 39 | +- 의미있는 이름을 붙여 이해가 쉽다 |
| 40 | +- 나중에 해당 값을 쉽게 변경할 수 있다 |
| 41 | +> |
| 42 | +
|
| 43 | +## 함수 |
| 44 | + |
| 45 | +```kotlin |
| 46 | +fun Context.toast( |
| 47 | + message: String, |
| 48 | + duration: Int = Toast.LENGTH_LONG |
| 49 | +) { |
| 50 | + Toast.makeText(this, message, Toast.LENGTH_LONG).show() |
| 51 | +} |
| 52 | + |
| 53 | +// 일반적인 사용 |
| 54 | +context.toast(message) |
| 55 | + |
| 56 | +// 액티비티 또는 Context 서브 클래스에서 사용 |
| 57 | +toast(message) |
| 58 | +``` |
| 59 | + |
| 60 | +토스트 메시지를 띄우는 함수를 위와 같은 toast 확장 함수로 만들어서 사용할 수 있다 |
| 61 | + |
| 62 | +`Toast.makeText(this, message, Toast.LENGTH_LONG).show()` |
| 63 | +위의 공통 코드는 확장 함수를 만들어서 일반적인 알고리즘으로 추출하자. |
| 64 | + |
| 65 | +이렇게 하면 토스트를 출력하는 자세한 코드를 기억하지 않아도, |
| 66 | +toast 라는 확장 함수를 통해 간단하게 구현할 수 있다 |
| 67 | + |
| 68 | +토스트를 출력하는 방법이 변경된다고 해도 확장 함수 내부 코드만 수정하면 되므로 유지보수성도 향상된다 |
| 69 | + |
| 70 | +하지만, 토스트가 아니라 스낵바를 출력하는 것으로 변경된다면 어떻게 될까? |
| 71 | + |
| 72 | +Context.toast를 Context.snackbar로 변경하면 될까? |
| 73 | + |
| 74 | +함수의 이름을 직접 바꾸는 것은 좋은 방법이 아니다. (아이템 28) |
| 75 | + |
| 76 | +함수의 파라미터까지 한 번에 변경하는 것 또한 쉽지 않은 일이라, Toast.LENGTH_LONG이 계속 사용된다 |
| 77 | + |
| 78 | +그렇다면 우리는 메시지의 출력 **방법**이 아니라, **메시지를 출력하고 싶다는 의도** 자체를 나타내야 한다 |
| 79 | + |
| 80 | +```kotlin |
| 81 | +fun Context.showMessage( |
| 82 | + message: String, |
| 83 | + duration: MessageLength = MessageLength.LONG |
| 84 | +) { |
| 85 | + val toastDuration = when(duration) { |
| 86 | + SHORT -> Length.LENGTH_SHORT |
| 87 | + LONG -> Length.LENGTH_LONG |
| 88 | + } |
| 89 | + Toast.makeText(this, message, toastDuration).show() |
| 90 | +} |
| 91 | + |
| 92 | +enum class MessageLength {SHORT, LONG} |
| 93 | +``` |
| 94 | + |
| 95 | +이렇게 showMEssage라는 높은 레벨의 함수로 바꿀 수 있다. |
| 96 | + |
| 97 | +여기서 가장 큰 변화는 이름이다 |
| 98 | +큰 차이가 없다고 생각할 수 있지만, 추상화를 표현하는 함수는 의미있는 함수 이름을 나타내는 것이 가장 중요하다 |
| 99 | + |
| 100 | +함수는 단순한 추상화지만 제한도 많고, 상태를 유지하지 않는다 |
| 101 | + |
| 102 | +또한 함수 이름을 변경하면 프로그램 전체에 큰 영향을 준다 |
| 103 | + |
| 104 | +구현을 추상화하는 더 강력한 방법은 클래스이다 |
| 105 | + |
| 106 | +## 클래스 |
| 107 | + |
| 108 | +```kotlin |
| 109 | +Class MessageDisplay(val context : Context) { |
| 110 | + fun show( |
| 111 | + message: String, |
| 112 | + duration: MessageLength = MEssageLenth.LONG |
| 113 | + ) { |
| 114 | + val toastDuration = when(duration) { |
| 115 | + SHORT -> Length.LENGTH_SHORT |
| 116 | + LONG -> Length.LENGTH_LONG |
| 117 | + } |
| 118 | + Toast.makeText(this, message, toastDuration).show() |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +enum class MessageLength {SHORT, LONG} |
| 123 | + |
| 124 | +// 사용 |
| 125 | +val messageDisplay = MessageDisplay(context) |
| 126 | +messageDisplay.show("Message") |
| 127 | +``` |
| 128 | + |
| 129 | +클래스가 더 강력한 이유는 상태를 가질 수 있다는 점, 많은 함수를 가질 수 있다는 점이다 |
| 130 | + |
| 131 | +위 코드에서 클래스 상태인 context는 기본 생성자를 통해 주입되는데, |
| 132 | +의존성 주입 프레임워크로 클래스 생성을 위임할 수도 있다 |
| 133 | + |
| 134 | +```kotlin |
| 135 | +@Inject lateinit var messageDisplay : MessageDisplay |
| 136 | +``` |
| 137 | + |
| 138 | +mock 객체를 활용해 이 클래스에 의존하는 다른 클래스를 테스트 할 수도 있다 |
| 139 | + |
| 140 | +```kotlin |
| 141 | +val messageDisplay: MessageDisplay = mockk() |
| 142 | +``` |
| 143 | + |
| 144 | +메시지를 출력하는 더 다양한 종류의 메서드를 만들 수 있어 확장성에서도 유리하다 |
| 145 | + |
| 146 | +```kotlin |
| 147 | +messageDisplay.setChristmasMode(true) |
| 148 | +``` |
| 149 | + |
| 150 | +## 인터페이스 |
| 151 | + |
| 152 | +- `listOf` 함수는 `List`를 리턴하는데, 이 `List`는 인터페이스 |
| 153 | +- 컬렉션 처리 함수는 `Iterable`, `Collection`의 확장 함수로 `List`, `Map`과 같은 인터페이스를 리턴 |
| 154 | +- 함수 `lazy`는 `Lazy` 인터페이스를 리턴 |
| 155 | + |
| 156 | +라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고 인터페이스를 노출한다 |
| 157 | + |
| 158 | +사용자가 클래스를 직접 사용하지 못하므로, |
| 159 | +인터페이스 뒤에 객체를 숨겨 걱정없이 인터페이스의 구현을 변경하거나 유지할 수 있기 때문이다 |
| 160 | + |
| 161 | +그렇게 하여 사용자는 추상화된 것에만 의존하게 되고, 결합도가 낮아진다 |
| 162 | + |
| 163 | +또한 선언과 사용을 분리함으로써 실제 구현을 자유롭게 변경할 수 있게 된다 |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +지금까지 추상화 하는 방법을 정리하면 다음과 같다 |
| 168 | + |
| 169 | +- 상수로 추출한다 |
| 170 | +- 동작은 함수로 래핑한다 |
| 171 | +- 함수를 클래스로 래핑한다 |
| 172 | +- 인터페이스 뒤에 클래스(구현)을 숨긴다 |
| 173 | +- 보편적인 객체를 특수한 객체로 래핑한다 |
| 174 | + |
| 175 | +이를 구현 할 때에는 다음과 같은 도구를 활용할 수 있다 |
| 176 | + |
| 177 | +- 제네릭 타입 파라미터 |
| 178 | +- 내부 클래스 추출 |
| 179 | +- 생성을 제한한다 |
| 180 | + |
| 181 | +### 추상화에 문제는 없을까? |
| 182 | + |
| 183 | +추상화는 자유로운 대신 코드를 이해하고 수정하기엔 어렵다 |
| 184 | +이 코드를 읽는 사람이 코드 개념을 배우고, 잘 이해해야 한다 |
| 185 | + |
| 186 | +큰 프로젝트에서는 모듈화를 잘 해야하는 등의 비용이 발생하기 때문에 극단적인 추상화는 지양하자 |
| 187 | + |
| 188 | +너무 많은 것을 숨기게 되면 결과를 이해하는 것 자체가 어렵다 |
| 189 | + |
| 190 | +누군가는 showMessage를 토스트 출력 함수로 생각한다든지, |
| 191 | +Toast.makeText를 따로 찾는다든지 할 수 있기 때문에 코드를 이해하기 어렵게 만들 수 있다 |
| 192 | + |
| 193 | +### 균형을 맞추자 |
| 194 | + |
| 195 | +- 팀의 크기 |
| 196 | +- 팀의 경험 |
| 197 | +- 프로젝트의 크기 |
| 198 | +- feature set |
| 199 | +- 도메인 지식 |
| 200 | + |
| 201 | +이렇게 프로젝트에 따라 균형을 적절히 찾아가보자. |
0 commit comments