├── LICENSE ├── README.md ├── SUNDAY-SADANG.md ├── chapter 4 └── wickedev-ch4-summary.md └── iammert-AndroidArchitecture.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kotlin Korea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin-Study 2 | 3 | ## Branch 4 | 5 | * SundaySadang 6 | * CandleSquare 7 | 8 | ## Links 9 | 10 | * [StudyLog](https://github.com/kotlin-korea/Study-Log/issues?utf8=%E2%9C%93&q=is%3Aissue) 11 | -------------------------------------------------------------------------------- /SUNDAY-SADANG.md: -------------------------------------------------------------------------------- 1 | # 스터디 방식 2 | 3 | * 각자 원하는 소스 코드를 선택한다. 4 | * 참여한 소스에 star를 눌러 업데이트를 구독한다. 5 | * 다른 사람의 PR에 대해 최대한 반응(리뷰, 이모지 등)한다. :) 6 | 7 | # 대상 소스 선정 8 | 9 | * 자세한 후보 리스트는 [이슈 #2](https://github.com/kotlin-korea/SundaySadang-Log/issues/2)에서 확인 가능합니다. 10 | 11 | ## 초급 12 | - [x] https://github.com/ganadist/AndroidApiDemo 13 | - [x] https://github.com/firebase/quickstart-android 14 | 15 | ## 중급 16 | - [x] https://github.com/googlesamples/android-architecture/tree/dev-todo-mvvm-live 17 | - [x] https://github.com/googlecodelabs/android-testing 18 | 19 | ## 고급 20 | - [x] https://github.com/iammert/AndroidArchitecture 21 | - 사전 작업 안내 : [iammert-AndroidArchitecture](https://github.com/kotlin-korea/Study-Log/blob/master/iammert-AndroidArchitecture.md) 22 | 23 | # Rule 24 | 25 | ## 오프라인 26 | 27 | 0. 오프라인 참석 전 PR을 반드시 등록하여야 합니다. 28 | 1. 반드시 오프라인 모임에 참여하여야 합니다. 29 | 2. 오프라인 참여가 불가능할 경우 최소 이틀 전 깃헙 모임 관련 이슈에 참여 불가 의사를 밝히셔야 합니다. 30 | 3. 오프라인 불참 여부를 전달하지 않으신 경우 자동으로 온라인 멤버로 전환됩니다. 31 | 4. 오프라인 참여가 2회를 초과하여 불가능할 경우 자동으로 온라인 멤버로 전환됩니다. 32 | 5. 모임 장소에서 마시는 유료 음료 등의 비용은 개인이 직접 계산/부담하시면 됩니다. 33 | 6. 모임 장소에 대한 비용 발생 시 참여자 간 1/n로 계산하며, 편의를 위해 천원 단위 이하는 절삭하여 제가 부담하도록 하겠습니다. 34 | 35 | ## 오프라인 멤버로의 전환 36 | 37 | 1. 온라인 멤버는 해당 회차에 대한 결원이 발생할 경우 오프라인 참가가 가능합니다. 38 | 2. 오프라인 TO가 발생할 경우 랜덤-_-하게 추첨하여 다음분께 참여 의사를 확인하도록 하곘습니다. 39 | 40 | ## 기타 41 | 42 | 1. 코틀린 문법 등은 필요에 따라 본인이 직접 학습하셔야 합니다. 43 | 2. 도움되는 내용에 대해 공유해주시는 것은 언제든지 환영합니다. 44 | -------------------------------------------------------------------------------- /chapter 4/wickedev-ch4-summary.md: -------------------------------------------------------------------------------- 1 | 4\. 클래스, 객체,인터페이스 2 | ====================== 3 | 4 | ## 4.1 클래스 계층 정의 5 | ------------------- 6 | 7 | ### 4.1.1 코틀린 인터페이스 8 | 9 | ```kotlin 10 | interface Clickable { 11 | // 일반 메소드 선언 12 | fun click() 13 | // 디폴트 구현이 있는 메소드 14 | fun showOff() = println("I'm clickable") 15 | } 16 | 17 | interface Focusable { 18 | fun setFocus(b: Boolean) = 19 | println("I ${if (b) "got" else "lost"} focus.") 20 | fun showOff() = println("I'm fucusable!") 21 | } 22 | 23 | class Button : Clickable, Focusable { 24 | override fun click() = println("I was clicked") 25 | 26 | // showOff 는 Clickable 과 Focusable 양쪽에 27 | // 구현을 가지므로 반드시 override 해서 구현해야 함! 28 | override fun showOff() { 29 | super.showOff() 30 | super.showOff() 31 | } 32 | } 33 | 34 | fun main(args: Array) { 35 | val button = Button() 36 | button.showOff() 37 | button.setFocus(true) 38 | button.click() 39 | } 40 | 41 | /* 42 | I'm clickable! 43 | I'm focusable! 44 | I got focus. 45 | I was clicked. 46 | */ 47 | ``` 48 | 49 | ```java 50 | public final class Button implements Clickable { 51 | public void showOff() { 52 | Clickable.DefaultImpls.showOff(this); 53 | } 54 | } 55 | 56 | public interface Clickable { 57 | void showOff(); 58 | 59 | public static final class DefaultImpls { 60 | public static void showOff(Clickable $this) { 61 | String var1 = "I'm clickable"; 62 | System.out.println(var1); 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | 69 | - 코틀린의 인터페이스는 자바 8과 마찬가지로 구현 메소드(디폴트 메소드)를 정의할 수 있다. 70 | - 코틀린은 자바 6와 호환되도록 설계 되었으므로 자바에서 디폴트 메소드가 있는 코틀린 인터페이스를 사용할 수 없음 71 | 72 | ### 4.1.2 open, final abstract 변경자: 기본적으로 final 73 | 74 | ```kotlin 75 | open class RichButton: Clickable { 76 | fun disable() { } 77 | open fun animate() { } 78 | // final 이 없는 override 매소드나 프로퍼티는 기본적으로 열려있다. 79 | final override fun click() { } 80 | } 81 | ```` 82 | 83 | - 코틀린의 class 는 기본적으로 상속 금지인 자바의 final class 84 | - 취약한 기반 클래스(fragile base class) 문제: 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버리는 경우 85 | - [GoF Design Pattern: "Program to an interface, not an implementation."](https://www.artima.com/lejava/articles/designprinciplesP.html) 86 | - 에릭 감마: "클래스에 의존성을 추가하는 것은 너무나도 쉽지만, 흥미롭게도 역은 쉽지 않습니다. 인터페이스에만 의존하면 구현과 분리됩니다. 이는 구현이 다를 수 있음을 의미하며 이는 건전한 의존 관계입니다." 87 | - 조슈아 블로크: "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라" 88 | 89 | ### 의문(P. 148) 90 | 91 | # 열린 클래스와 스마트 캐스트 92 | 클래스의 기본적인 상속 가능 상태를 final로 함으로써 얻을 수 있는 큰 이익은 다양한 경우에 스마트 캐스트가 가능하다는 점이다. 2.3.5절에서 말한 것처럼 스마트 캐스트는 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다. 클래스 프로퍼티의 경우 이는 val이면서 커스텀 접근자가 없는 경우에만 스마트 캐스트를 쓸 수 있다는 의미이다. 이 요구 사항은 또한 프로터피가 final이어야만 한다는 뜻이기도 하다. 프로퍼티가 fianl이 아니라면 그 프로퍼티를 다른 클래스가 상속하면서 커스텀 접근자를 정의함으로써 스마트 캐스트의 요구사항을 깰 수 있다. 프로퍼티는 기본적으로 final이기 때문에 따로 고민할 필요 없이 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있다. 이는 코드를 더 이해하기 쉽게 만든다. 93 | 94 | - 표 4.1 클래스 내에서 상속 제어 변경자의 의미 95 | 96 | | 변경자 | 이 변경자가 붙은 맴버는... | 설명 | 97 | | --- | --- | --- | 98 | | `fianl` | 오버라이드 할 수 없음 | 클래스 맴버의 기본 변경자다. | 99 | | `open` | 오버리이드 할 수 있음 | 반드시 open을 명시해야 오버리이드 할 수 있다. | 100 | | `abstract` | 반드시 오버리이드해야 함 | 추상 클래스의 맴버에만 이 변경자를 붙일 수 있다. | 101 | | `override` | 상위 클래스나 상위 인스턴스의 맴버를 오버리이드하는 중 | 오버라이드하는 맴버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다. | 102 | 103 | ### 4.1.3 가시성 변경자: 기본적으로 공개 104 | 105 | | 변경자 | 클래스 맴버 | 최상위 선언 | 106 | | --- | --- | --- | 107 | | `public`(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. | 108 | | `internal` | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서 볼 수 있다. | 109 | | `protected` | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) | 110 | | `private` | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. | 111 | 112 | - 코틀린의 `private` 클래스는 바이트코드 상 패키지-전용 클래스로 컴파일한다. 113 | - 코틀린의 `internal` 은 컴파일 될 때 바이트코드 상 `public` 이 된다. 그러므로 Java 코드에서 코틀린 `internal` 맴버를 접근 할 수 있지만 맹글(mangle)되므로 보기 불편하고 못생겨 보인다. 114 | 115 | ### 4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스 116 | 117 | | 클래스 B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 | 118 | | --- | --- | --- | 119 | | 중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | `static class A` | `class A` | 120 | | 내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | `class A` | `inner class A` | 121 | 122 | ```kotlin 123 | class Outer { 124 | inner class Inner { 125 | fun getOuterReferences(): Outer = this@Outer 126 | } 127 | } 128 | ``` 129 | 130 | ### 4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한 131 | 132 | - Without sealed 133 | 134 | ```kotlin 135 | interface Expr 136 | class Num(val value: Int) : Expr 137 | class Sum(val left: Expr, val right: Expr) : Expr 138 | 139 | fun eval(e: Expr) :Int { 140 | return when(e) { 141 | is Num -> e.value 142 | is Sum -> eval(e.right) + eval(e.left) 143 | else -> // 반드시 else 분기가 있어야 한다. 144 | throw IllegalArgumentException("Unknown expression") 145 | } 146 | } 147 | ``` 148 | 149 | - With sealed 150 | 151 | ```kotlin 152 | sealed class Expr 153 | class Num(val value: Int) : Expr() 154 | class Sum(val left: Expr, val right: Expr) : Expr() 155 | 156 | fun eval(e: Expr) :Int { 157 | return when(e) { 158 | is Expr.Num -> e.value 159 | is Sum -> eval(e.right) + eval(e.left) 160 | } 161 | } 162 | ``` 163 | 164 | ## 4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 165 | 166 | ```kotlin 167 | class User(val nickname: String) 168 | 169 | class User constructor(_nickanme String) { 170 | val nickname: String 171 | init { 172 | nickname = _nickname 173 | } 174 | } 175 | 176 | // val은 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다. 177 | class User(val nickname: String) 178 | // 생성자 파라미터에 대한 디폴트 값을 제공한다. 179 | class User(val nickname: String, 180 | val isSubscribed: Boolean = true) 181 | // isSubscribed 파라미터에는 디폴트 값이 쓰인다. 182 | >> val hyun = User("현석") 183 | >> println(hyun.isSubscribed) // true 184 | >> val hyun = User("혜원", isSubscribed = false) 185 | >> println(hyun.isSubscribed) // false 186 | ``` 187 | - 아래 클래스의 호출 순서는 어떨까? 188 | ```kotlin 189 | class Class { 190 | private val value: String 191 | constructor(value: String) { 192 | this.value = value 193 | println("constructor") 194 | } 195 | 196 | init { 197 | println("init") 198 | } 199 | } 200 | ``` 201 | 202 | 203 | ```java 204 | public final class Class { 205 | private final String value; 206 | 207 | public Class(@NotNull String value) { 208 | Intrinsics.checkParameterIsNotNull(value, "value"); 209 | super(); 210 | String var2 = "init"; 211 | System.out.println(var2); 212 | this.value = value; 213 | var2 = "constructor"; 214 | System.out.println(var2); 215 | } 216 | } 217 | ``` 218 | 219 | - 실행 순서: 생성자 호출 -> init 블록 호출 -> 생성자 안쪽 블록 호출 220 | 221 | ```kotlin 222 | class Secretive private constructor() { 223 | companian object { 224 | fun create(): Secretive = Secretive() 225 | } 226 | } 227 | ``` 228 | - 위 처럼 팩토리 메소드 생성 방법을 통해서 객체를 생성하도록 생성자를 `private` 으로 선언할 수도 있음 229 | 230 | ```kotlin 231 | open class View { 232 | // 주 생성자 233 | constructor(ctx: Context) { 234 | TODO() 235 | } 236 | 237 | // 부 생성자 238 | constructor(ctx: Context, attr: AttributeSet) { 239 | TODO() 240 | } 241 | } 242 | 243 | class MyButton : View { 244 | constructor(ctx: Context) : this(ctx, MY_STYLE) { 245 | TODO() 246 | } 247 | 248 | constructor(ctx: Context, attr: AttributeSet) 249 | : super(ctx, attr) { 250 | TODO() 251 | } 252 | } 253 | ``` 254 | 255 | ## 4.2.3 인터페이스에 선언된 프로퍼티 구현 256 | 257 | ```kotlin 258 | // 인터페이스에 추상 프로퍼티 선언을 넣을 수 있음 259 | interface User { 260 | val email: String 261 | // 프로퍼티에 뒷받침하는 필드가 없다. 262 | // 대신 매번 결과를 계산해 돌려준다. 263 | // 인터페이스는 상태를 저장할 수 없다. 264 | val nickname: String 265 | get() = email.substringBefore('@') 266 | } 267 | 268 | class PrivateUser( 269 | override val email: String, 270 | override val nickname: String 271 | ) : User 272 | 273 | class SubscribingUser( 274 | override val email: String 275 | ): User { 276 | override val nickname: String 277 | get() = email.substringBefore('@') 278 | } 279 | 280 | class FacebookUser( 281 | val accountId: Int, 282 | override val email: String 283 | ) : User { 284 | override val nickname = getFacebookName(accountId) 285 | } 286 | ``` 287 | 288 | ## 4.2.4 게터와 세터에서 뒷받침하는 필드에 접근 289 | 290 | ```kotlin 291 | class User(val name: String) { 292 | var address: String = "unspecified" 293 | set(value: String) { 294 | println(""" 295 | Address was changed for $name: 296 | "$field" -> "$value". 297 | """.trimIndent()) // 뒷받침하는 필드 값 읽기 298 | field = value // 뒷받침하는 필드 값 변경하기 299 | } 300 | } 301 | 302 | >>> val user = User("Alice") 303 | >>> user.address = "Elsenheimerstrasse 47, 80687 Muenchen" 304 | Address was changed for Alice: 305 | "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen" 306 | ``` 307 | 308 | ### 4.2.5 접근자의 가시성 변경 309 | 310 | ```kotlin 311 | class LengthCounter { 312 | // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다. 313 | var counter: Int = 0 314 | private set 315 | 316 | fun addWord(word: String) { 317 | counter += word.length 318 | } 319 | } 320 | 321 | >>> val lengthCounter = LengthCounter() 322 | >>> lengthCounter.addWord("Hi!") 323 | >>> println(lengthCounter.counter) 324 | // 3 325 | ``` 326 | 327 | ## 4.3 컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임 328 | ----------------------------------------------- 329 | 330 | 자바 플랫폼에서는 `equals`, `hashCode`, `toString` 등의 메소드를 직접 구현해야 함. (Lombok 같은 프로젝트도 있지만 언어 표준이 아니기 때문에 고통스러움) 331 | 332 | ### 4.3.1 모든 클래스가 정의해야 하는 메소드 333 | 334 | ```kotlin 335 | class Client(val name: String, val postalCode: Int) { 336 | override fun toString(): String = 337 | "Client(name=$name, postalCode=$postalCode)" 338 | 339 | override fun equals(other: Any?): Boolean { 340 | if (other == null || other !is Client) 341 | return false 342 | return name == other.name && 343 | postalCode == other.postalCode 344 | } 345 | 346 | override fun hashCode(): Int = 347 | name.hashCode() * 31 + postalCode 348 | } 349 | 350 | >>> val client = Client("오현석", 4122) 351 | >>> println(client) 352 | // Client(name=오현석, postalCode=4122) 353 | 354 | >>> val client1 = Client("오현석", 4122) 355 | >>> val client2 = Client("오현석", 4122) 356 | >>> println(client1 == client2) 357 | // client1.equals(client2) 358 | // true 359 | 360 | >>> val processed = hashSetOf(Client("오현석", 4122)) 361 | >>> println(processed.contains(Client("오현석", 4122))) 362 | // true 363 | ``` 364 | 365 | ### 4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성 366 | 367 | ```kotlin 368 | data class Client(val name: String val postalCode: Int) 369 | ``` 370 | 371 | ## 데이터 클래스와 불변성: copy() 메소드 372 | 373 | HashMap 등의 컨테이너에 데이터 클래스를 담거나, 다중스레드 프로그램에서 불변 객체를 사용하면 프로그램의 오류를 줄일 수 있다. `data class`는 `copy` 메소드를 구현해 준다. 374 | 375 | ```kotlin 376 | class Client(val name: String, val postalCode: Int) { 377 | fun copy(name: String = this.name, 378 | postalCode = this.postalCode) = 379 | Client(name, postalCode) 380 | } 381 | 382 | >>> val lee = Client("이계영", 4122) 383 | >>> println(lee.copy(postalCode = 4000)) 384 | Client(name=이계영, postalCode=4000) 385 | ``` 386 | 387 | ### 4.3.3 클래스 위임: by 키워드 사용 388 | 389 | ```kotlin 390 | // 데코레이터를 만들 때 원래 작성해야하는 코드양 391 | class DelegationCollection : Collection { 392 | private val innerList = arrayListOf() 393 | override fun size: int get() = innserList.size 394 | override fun isEmpty(): Boolean = innerList.isEmpty() 395 | ... 396 | override fun ... 397 | } 398 | 399 | // 클래스 위임을 사용했을 때 코드양 400 | class DelegationCollection( 401 | innsrList: Collection = ArrayList() 402 | ) : Collection by innerList() 403 | ``` 404 | 405 | ## 4.4 object 키워드: 클래스 선언과 인스턴스 생성 406 | ---------------------------------------- 407 | 408 | - 객체 선언(object declaration)은 싱글턴을 정의하는 방법 중 하나 409 | - 동반 객체(companion object)는 자바에서 static method 410 | - 객체 식은 자바의 무명 내부 클래스(anonymous inner class) 대신 쓰임 411 | 412 | ## 4.4.1 객체 선언: 싱글턴을 쉽게 만들기 413 | 414 | ```kotlin 415 | object Payroll { 416 | val allEmployess = arrayListOf() 417 | fun calculateSalary() { 418 | TODO() 419 | } 420 | } 421 | 422 | Payroll.allEmployess.add(Person(...)) 423 | Payroll.calculateSalary() 424 | 425 | object CaseInsensitiveFileComparator : Comparator { 426 | override fun compare(f1:File, f2:File): Int { 427 | ... 428 | } 429 | } 430 | >>> println(CaseInsensitiveFileComparator.compare(f1, f2)) 431 | 432 | // 자바에서 433 | >>> CaseInsensitiveFileComparator.INSTANCE.compare(f1, f2); 434 | ``` 435 | 436 | - 대규모 소프트웨어 개발에서는 싱글턴이 적합하지 않을 수도 있다: 생성을 제어할 수 없고 생성자 파라미터를 지정할 수 없으므로 단위 테스트를 하거나 의존 관계를 바꿀때 곤란하다. 437 | 438 | ## 4.4.2 동반 객체: 팩토리 메소드와 정적 맴버가 들어갈 장소 439 | 440 | ```kotlin 441 | class A { 442 | companion object { 443 | fun bar() { 444 | TODD() 445 | } 446 | } 447 | } 448 | >>> A.bar() 449 | // 자바에서 450 | >>> A.Companion.bar() 451 | ``` 452 | 453 | ## 4.4.3 동반 객체를 일반 객체처럼 사용 454 | 455 | ```kotlin 456 | class Person(val name: String) { 457 | companion object Loader { 458 | fun fromJSON(jsonText: String) : Person = { TODO() } 459 | } 460 | } 461 | >>> person = Person.Loader.fromJSON("{name: 'Dmitry'}") 462 | 463 | interface JSONFactory { 464 | fun fromJSON(jsonText: String): T 465 | } 466 | 467 | class Person(val name: String) { 468 | companion object : JSONFactory { 469 | override fun fromJSON(jsonText: String): Person = ... 470 | } 471 | } 472 | ``` 473 | 474 | ## 4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 작성 475 | 476 | ```kotlin 477 | var clickCount = 0 478 | window.addMouseListener { 479 | object : MouseAdapter() { 480 | override fun mouseClicked(e: MouseEvent) { 481 | clickCount++ // 로컬 변수의 값을 접근 할 수 있음 482 | } 483 | override fun mouseEntered(e: MouseEvent) = ... 484 | } 485 | } 486 | ``` 487 | -------------------------------------------------------------------------------- /iammert-AndroidArchitecture.md: -------------------------------------------------------------------------------- 1 | # 사전 작업 2 | 3 | ## 1. Resouce 빌드 실패 4 | 5 | ### 문제 6 | 7 | 해당 프로젝트 빌드시 아래와 같은 파일이 없는 것으로 나옵니다 8 | 9 | - ic_language_green_24dp 10 | - ic_stats_green_24dp 11 | 12 | ### 쉽게 해결방안 13 | 14 | 1. https://github.com/konifar/android-material-design-icon-generator-plugin 를 설치한다 15 | 2. 사용 컬러를 `#00E175` 으로 설정 16 | 3. 24dp 와 적당한 이름으로 변경한다 17 | 4. 끝 18 | 19 | ![default](https://user-images.githubusercontent.com/1534926/28421392-07250eb0-6da0-11e7-823b-2ac868a56e39.PNG) 20 | 21 | ## 2. API 키 누락으로 NPE 발생 22 | 23 | ### 문제 24 | 25 | 현재 프로젝트에는 API가 누락되어 있는 상태로 아래와 같은 에러 발생 26 | 27 | ``` 28 | FATAL EXCEPTION: AsyncTask #1 29 | Process: iammert.com.androidarchitecture, PID: 10988 30 | java.lang.RuntimeException: An error occurred while executing doInBackground() 31 | at android.os.AsyncTask$3.done(AsyncTask.java:325) 32 | at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354) 33 | at java.util.concurrent.FutureTask.setException(FutureTask.java:223) 34 | at java.util.concurrent.FutureTask.run(FutureTask.java:242) 35 | at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243) 36 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133) 37 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 38 | at java.lang.Thread.run(Thread.java:761) 39 | Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.List iammert.com.androidarchitecture.data.remote.model.MoviesResponse.getResults()' on a null object reference 40 | at iammert.com.androidarchitecture.data.MovieRepository$1.saveCallResult(MovieRepository.java:36) 41 | at iammert.com.androidarchitecture.data.MovieRepository$1.saveCallResult(MovieRepository.java:32) 42 | at iammert.com.androidarchitecture.data.NetworkBoundResource$2.doInBackground(NetworkBoundResource.java:71) 43 | at iammert.com.androidarchitecture.data.NetworkBoundResource$2.doInBackground(NetworkBoundResource.java:67) 44 | at android.os.AsyncTask$2.call(AsyncTask.java:305) 45 | at java.util.concurrent.FutureTask.run(FutureTask.java:237) 46 | at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)  47 | at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)  48 | at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)  49 | ``` 50 | 51 | ### 해결 방안 52 | 53 | - iammert.com.androidarchitecture.data.remote.ApiConstants.java 파일 연다 54 | - API_KEY 항목에 `8476a7ab80ad76f0936744df0430e67c` 입력 (https://github.com/chonamdoo 님이 발급하셨어요) 55 | --------------------------------------------------------------------------------