일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 클래스
- Rxjava Observable
- 서비스 쓰레드 차이
- jvm이란
- 큐 자바 코드
- 안드로이드 유닛테스트란
- 안드로이드 레트로핏 crud
- 자바 다형성
- rxjava hot observable
- 2022 플러터 설치
- jvm 작동 원리
- 객체
- 안드로이드 레트로핏 사용법
- 멤버변수
- 안드로이드 유닛 테스트 예시
- 안드로이드 라이선스 종류
- 스택 큐 차이
- 2022 플러터 안드로이드 스튜디오
- android ar 개발
- ar vr 차이
- android retrofit login
- 안드로이드 유닛 테스트
- 스택 자바 코드
- rxjava cold observable
- 안드로이드 os 구조
- 플러터 설치 2022
- 서비스 vs 쓰레드
- rxjava disposable
- ANR이란
- 안드로이드 라이선스
- Today
- Total
나만을 위한 블로그
[이펙티브 코틀린] 아이템 8. 적절하게 null을 처리하라 본문
null : 값이 부족하다(lack of value)
프로퍼티가 null = 값이 설정되지 않았거나 제거됐음을 나타냄
함수가 null을 리턴하는 건 함수에 따라 여러 의미를 가질 수 있다.
- String.toIntOrNull() : String -> Int로 적절하게 변환할 수 없을 경우 null 리턴
- Iterable<T>.firstOrNull(() -> Boolean) : 주어진 조건에 맞는 요소가 없을 경우 null 리턴
null은 최대한 명확한 의미를 갖는 게 좋다. nullable 값을 처리해야 하기 때문인데 이걸 처리하는 사람은 API 사용자(API 요소를 사용하는 개발자)다
val printer: Printer? = getPrinter()
printer.print() // 컴파일 오류
printer?.print() // 안전 호출
if (printer != null) printer.print() // 스마트 캐스팅
printer!!.print() // not-null assertion
기본적으로 nullable 타입은 3가지 방법으로 처리한다.
- safe call(?.), 스마트 캐스팅, 엘비스 연산자 등을 활용해서 안전하게 처리한다
- 오류를 throw한다
- 함수 or 프로퍼티를 리팩토링해서 nullable 타입이 안 나오게 바꾼다
null을 안전하게 처리하기
null을 안전하게 처리하는 방법 중 널리 쓰이는 방법 : safe call, 스마트 캐스팅
printer?.print() // safe call
if (printer != null) printer.print() // 스마트 캐스팅
둘 모두 printer가 null이 아닐 때 print()를 호출한다. 앱 사용자 관점에서 가장 안전한 방법이다. 개발자에게도 편해서 nullable 값을 처리할 때 이 방법을 가장 많이 활용한다.
다른 인기있는 방법은 엘비스 연산자를 쓰는 것이다. 엘비스 연산자는 오른쪽에 return 또는 throw를 포함한 모든 표현식이 허용된다.
val printerName1 = printer?.name ?: "Unnamed"
val printerName2 = printer?.name ?: return
val printerName3 = printer?.name ?: throw Error("Printer must be named")
컬렉션 처리를 할 때 뭔가 없다는 걸 나타낼 때는 null이 아닌 빈 컬렉션을 쓰는 게 일반적이다. 따라서 Collection<T>.orEmpty() 확장 함수를 쓰면 nullable이 아닌 List<T>를 리턴받는다.
스마트 캐스팅은 코틀린의 규약 기능(contracts feature)을 지원한다. 이 기능을 쓰면 아래 코드처럼 스마트 캐스팅할 수 있다.
println("What is your name?")
val name = readLine()
if (!name.isNullOrBlank()) {
println("Hello ${name.toUpperCase()}")
}
val news: List<News>? = getNews()
if (!news.isNullOrEmpty()) {
news.forEach { notifyUser(it) }
}
지금까지 본 null을 처리하기 위한 방법들은 반드시 알아 둬야 한다.
오류 throw하기
이전 코드에선 printer가 null일 때 개발자에게 알리지 않고 코드가 진행된다. 하지만 printer가 null이 될 거라 예상 못 했다면 print()가 호출되지 않는다. 이는 개발자가 오류를 찾기 어렵게 만든다. 따라서 다른 개발자가 어떤 코드를 보고 선입견처럼 당연히 그럴 거라고 생각되게 되는 부분이 있고 그 부분에서 문제가 발생할 경우 개발자에게 오류를 강제로 발생시키는 게 좋다.
오류를 강제 발생시킬 때는 throw, !!, requireNotNull, checkNotNull 등을 활용한다.
fun process(user: User) {
requireNotNull(user.name)
val context = checkNotNull(context)
val networkService = getNetworkService(context) ?: throw NoInternetConnection()
networkService.getData { data, userData ->
show(data!!, userData!!)
}
}
not-null assertion(!!) 관련 문제
nullable을 처리하는 가장 간단한 방법은 not-null assertion(!!)을 쓰는 것이다. 그런데 !!을 쓰면 자바에서 nullable을 처리할 때 발생할 수 있는 문제가 똑같이 발생한다. !!은 사용하기 쉽지만 좋은 해결법은 아니다. 예외가 발생할 때 어떤 설명도 없는 제네릭 예외가 발생한다. 코드가 짧고 너무 쓰기 쉬워서 남용하게 되는 문제도 있다.
!!의 타입은 nullable이지만 null이 안 나온다는 것이 거의 확실한 상황에서 많이 쓰인다. 하지만 현재 확실하다고 미래에 확실한 건 아니다.
파라미터로 숫자를 4개 받아서 가장 큰 것을 찾는 함수를 생각하라. 모든 파라미터를 리스트에 넣은 뒤에 max()를 써서 가장 큰 값을 찾게 설계하기로 했다고 가정한다. 문제는 컬렉션 안에 아무것도 없을 경우 null을 리턴하므로 최종적으로 nullable을 리턴한다는 것이다. 이 리턴값이 null일 수 없단 걸 아는 개발자는 아래처럼 !! 연산자를 쓸 것이다.
fun largestOf(a: Int, b: Int, c: Int, d: Int): Int = listOf(a, b, c, d).max()!!
이런 간단한 함수에서도 !!는 NPE로 이어질 수 있다. 미래의 누군가가 함수를 리팩토링하면서 컬렉션이 null일 수 있다는 걸 놓칠 수 있기 때문이다.
fun largestOf(vararg nums: Int): Int = nums.max()!!
largestOf() // NPE
nullability(null일 수 있는지)와 관련된 정보는 숨겨져 있으므로 쉽게 놓칠 수 있다. 변수와 비슷하다.
변수를 일단 선언하고 이후 사용하기 전에 값을 할당해서 쓰기로 하고 아래 코드를 작성했다고 가정한다. 이렇게 변수를 null로 선언하고 이후에 !! 연산자를 쓰는 것은 좋은 방법이 아니다.
class UserControllerTest {
private var dao: UserDao? = null
private var controller: UserController? = null
@BeforeEach
fun init() {
dao = mockk()
controller = UserController(dao!!)
}
@Test
fun test() {
controller!!.doSomeThing()
}
}
이렇게 코드를 짜면 이후 프로퍼티를 계속 언팩해야 하므로 쓰기 귀찮다. 또한 해당 프로퍼티가 실제로 이후에 의미 있는 null 값을 가질 가능성 자체를 차단한다. 이런 코드를 작성하는 올바른 방법은 lateinit 또는 Delegates.notNull을 쓰는 것이다.
!! 연산자를 쓰거나 명시적으로 예외를 발생시키는 형태로 설계하면 미래 어느 시점에 해당 코드가 오류를 발생시킬 수 있다는 걸 염두에 둬야 한다. 예외는 예상하지 못한 잘못된 부분을 알려주기 위해 쓰는 것이다.
하지만 명시적 오류는 제네릭 NPE보단 더 많은 정보를 제공해줄 수 있어서 !! 연산자를 쓰는 것보단 훨씬 좋다.
!! 연산자가 의미 있는 경우는 굉장히 드물다. 일반적으로 nullability가 제대로 표현되지 않는 라이브러리를 사용할 때 정도에만 써야 한다. 코틀린 대상으로 설계된 API를 활용한다면 !! 연산자를 쓰는 걸 이상하게 생각해야 한다.
일반적으로 !! 사용을 피해야 한다. 이런 제안은 코틀린 커뮤니티 전체에서 널리 승인되고 있는 제안이다. Detekt 같은 정적 분석 도구는 !! 연산자를 쓰면 아예 오류가 발생하게 설정돼 있다. !! 연산자를 보면 반드시 조심하고 뭔가 잘못돼 있을 가능성을 생각하라.
의미 없는 nullability 피하기
nullability는 어떻게든 적절하게 처리해야 해서 추가 비용이 발생한다. 따라서 필요한 경우가 아니면 nullability 자체를 피하는 게 좋다. null은 중요한 메시지를 전달하는 데 쓰일 수 있다. 따라서 다른 개발자가 보기에 의미가 없을 때는 null을 안 쓰는 게 좋다. 아래는 nullability를 피할 때 쓸 수 있는 몇 가지 방법이다.
- 클래스에서 nullability에 따라 여러 함수를 만들어 제공할 수도 있다. 대표적인 예로 List<T>와 get, getOrNull()이 있다
- 어떤 값이 클래스 생성 이후에 확실하게 설정된단 보장이 있으면 lateinit 프로퍼티와 notNull 델리게이트를 써라
- 빈 컬렉션 대신 null을 리턴하지 마라. List<Int>?와 Set<String?>과 같은 컬렉션을 빈 컬렉션으로 둘 때와 null로 둘 때는 의미가 다르다. null은 컬렉션 자체가 없다는 걸 나타낸다. 요소가 부족하다는 걸 나타내려면 빈 컬렉션을 써라.
- nullable enum, None enum 값은 완전히 다른 의미다. null enum은 별도 처리해야 하지만 None enum 정의에 없으므로 필요한 경우에 사용하는 쪽에서 추가해서 활용할 수 있단 의미다
lateinit 프로퍼티와 notNull 델리게이트
클래스가 클래스 생성 중에 초기화할 수 없는 프로퍼티를 갖는 건 분명 존재하는 일이다. 이런 프로퍼티는 사용 전에 반드시 초기화해서 써야 한다. 예로 JUnit의 @BeforeEach처럼 다른 함수들보다 먼저 호출되는 함수에서 프로퍼티가 설정되는 경우가 있다.
class UserControllerTest {
private var dao: UserDao? = null
private var controller: UserController? = null
@BeforeEach
fun init() {
dao = mockk()
controller = UserController(dao!!)
}
@Test
fun test() {
controller!!.doSomething()
}
}
프로퍼티를 쓸 때마다 nullable에서 null이 아닌 것으로 타입 변환하는 것은 바람직하지 않다. 이런 값은 테스트 전에 설정될 거라는 게 명확하므로 의미 없는 코드가 쓰인다고 할 수 있다.
이런 코드에 대한 바람직한 해결책은 나중에 속성을 초기화할 수 있는 lateinit 한정자를 쓰는 것이다. lateinit 한정자는 프로퍼티가 이후에 설정될 것임을 명시하는 한정자다.
class UserControllerTest {
private lateinit var dao: UserDao?
private lateinit var controller: UserController?
@BeforeEach
fun init() {
dao = mockk()
controller = UserController(dao)
}
@Test
fun test() {
controller.doSomething()
}
}
lateinit을 쓸 때도 비용이 발생한다. 초기화 전에 값을 쓰려고 하면 예외가 발생한다. 처음 사용하기 전에 반드시 초기화가 되어 있을 때만 lateinit을 붙여라. 그런 값이 사용되서 예외가 발생하면 그 사실을 알아야 하므로 예외가 발생하는 건 오히려 좋은 일이다.
lateinit은 nullable과 비교해서 아래와 같은 차이가 있다.
- !! 연산자로 언팩하지 않아도 된다
- 이후 어떤 의미를 나타내기 위해 null을 쓰고 싶을 때 nullable로 만들 수도 있다
- 프로퍼티 초기화 이후에는 초기화되지 않은 상태로 돌아갈 수 없다
lateinit : 프로퍼티를 처음 쓰기 전에 반드시 초기화될 거라고 예상되는 상황에 활용함(lifecycle을 갖는 클래스처럼 메서드 호출에 명확한 순서가 있을 경우 - 안드로이드 액티비티의 onCreate, iOS UIViewController, 리액트 컴포넌트의 componentDidMount 등
lateinit을 쓸 수 없는 경우 : JVM에서 Int, Long, Double, Boolean 같은 기본형과 연결된 타입으로 프로퍼티를 초기화해야 하는 경우. 이 경우 lateinit보다 느리지만 Deletages.notNull을 사용한다.
class DoctorActivity: Activity() {
private var doctorId: Int by Delegates.notNull()
private var fromNotification: Boolean by Delegates.notNull()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
doctorId = intent.extras.getInt(DOCTOR_ID_ARG)
fromNotification = intent.extras.getBoolean(FROM_NOTIFICATION_ARG)
}
}
위 코드처럼 onCreate 때 초기화하는 프로퍼티는 지연초기화하는 형태로 아래처럼 프로퍼티 위임을 쓸 수도 있다.
class DoctorActivity: Activity() {
private var doctorId: Int by arg(DOCTOR_ID_ARG)
private var fromNotification: Boolean arg(FROM_NOTIFICATION_ARG)
}
프로퍼티 위임을 쓰는 패턴은 아이템 21에서 다룬다. 프로퍼티 위임을 쓰면 nullability로 발생하는 여러 문제를 안전하게 처리할 수 있다.
'책 > Effective Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 아이템 10. 단위 테스트를 만들어라 (0) | 2022.06.05 |
---|---|
[이펙티브 코틀린] 아이템 9. use를 써서 리소스를 닫아라 (0) | 2022.06.02 |
[이펙티브 코틀린] 아이템 7. 결과 부족이 발생할 경우 null, Failure를 사용하라 (0) | 2022.05.26 |
[이펙티브 코틀린] 아이템 6. 사용자 정의 오류보다는 표준 오류를 사용하라 (0) | 2022.05.26 |
[이펙티브 코틀린] 아이템 5. 예외를 활용해 코드에 제한을 걸어라 (0) | 2022.05.22 |