이펙티브 코틀린 Item 8: 적절하게 Null을 처리하자.

woga·2023년 3월 19일
0

코틀린 공부

목록 보기
11/54
post-thumbnail

코틀린에는 Null로 반환해서 처리할 수 있는 함수들이 있다

  • String.toIntOrNull()
  • Iterable<T>.firstOrNull()

이렇게 null을 반환하게 하고 null을 처리하는 몇 가지 방법들이 코틀린에 있는데 아래와 같다.

  • ?., 스마트 캐스팅, Elvis 연산자 등을 활용해서 처리

  • 오류를 throw

  • 함수 또는 프로퍼티를 리팩토링해서 (orEmpty() 등) nullable 타입이 안나오게 처리

null을 안전하게 처리

주로 null을 안전하게 처리하는 거엔 safe call스마트 캐스팅이 있다

printer?.print() // safe call
if (printer != null) printer.print() // smart casting

그 중 엘비스는 오른쪽에 return 또는 throw을 포함한 모든 표현식이 허용된다.

val printerName1 = printer?.name ?: "Unnamed"
val printerName2 = printer?.name ?: return
val printerName3 = printer?.name ?: throw Error("Printer must be named")

그리고 이는 returnthrow 모두 Nothing(모든 타입의 서브타입)을 리턴하게 설계되서 가능하다

아래처럼 스마트캐스팅도 사용할 수 있다.

println("What is your name?")
val name = readLine()
if (!name.isNullOrBlank()) {
	println("Hello ${name.toUpperCase()}")
}

val news: List<News>? = getNews()
if (!new.isNullOfEmpty()) {
	news.forEach { notifyUser(it) }
}

방어적 프로그래밍: 모든 가능성으로 올바른 방식으로 처리
공격적 프로그래밍: 예상하지 못한 상황이 발생했을 때, 개발자에게 알려 수정하게 하기 (require, check, assert 가 그런 경우)
이 둘은 코드 안전을 위해 모두 필요하다

오류 throw

이전에서 봤던 코드는 printer가 null일 때 개발자에게 알리지 않고 코드가 그대로 진행되는데 이게 null이 되리라 예상하지 못했다면 print 메서드가 호출되지 않아서 이상할 것이다.
그래서 그런 부분은 개발자에게 오류를 강제로 발생시켜주는 것이 좋다.

이럴 땐, throw, !!, requireNotNull, checkNotNull 등을 활용하자

not-null assertion (!!) 문제

nullable을 처리하는 가장 간단한 방법은 !!을 사용하는 것이다. 그치만 이는 좋은 해결 방법이 아니다.
현재 확실하다고 미래에 확실한 것이 아니기 때문이다 (실제로 코드 까딱하고 잘못썼다가 해당 문제가 발생하기 쉽상이다 현재 포스팅하는 나도 겪어본 적 있다)

또한, 컬렉션 함수에서 nullability(널일 수 있는지) 관련된 정보를 숨긴 채 값을 반환하는데 !!를 썼다가 NPE가 나올 수 있으니 확실히 좋은 방법은 아니다

(글에서도 좋은 방법이 아니라고 2번이나 반복해서 이야기한다 ㅋㅋ.. 쓰면 진짜진짜 안될듯 이정도면 거의 비는 수준)

그러므로 올바른 방법은 lateinit, Delegates.notNull을 사용하는 것이다.

예외는 예상하지 못한 잘못된 부분을 알려주기 위해서 발생하는 것이다. 하지만 명시적 오류는 제네릭 NPE보다 훨씬 더 많은 정보를 제공해 줄 수 있으므로 !! 연산자를 사용하는 것보다 훨씬 좋다

대부분 팀이 이를 쓰지않자고 정책을 갖고 있기도 하고 Detekt 같은 정적 분석 도구는 !!를 쓰면 오류를 발생하도록 설정되어 있기도 하다고 한다.

모쪼록 조심하자!

의미 없는 nullability 피하기

nullability는 적절하게 처리해야해서 신경쓸게 많다 그래서 따라서 필요한 경우가 아니라면 nullability 자체를 피하는 것이 좋다.
왜냐면 null 자체로 중요한 메세지로 쓰기도 하기 때문이다.

nullability를 그럼 어떻게 피할 수 있을까?

  • 클래스에서 nullability에 따라 여러 함수를 만들어서 제공할 수 있다.

  • 어떤 값이 클래스 생성 이후에 확실하게 설정된다는 보장이 있다면 lateinit이나 notNull 델리게이트를 사용하자

  • 빈 컬렉션 대신 null을 리턴하지 말자. null은 컬렉션 자체가 없다는 것을 나타낸다 요소가 부족하다는 것을 나타내려면 빈 컬렉션을 뱉자

  • nullable enum과 None enum 값은 완전 다르다. null enum은 별도로 처리해야 하지만 None enum은 정의에 없으므로 필요한 경우에 추가해서 쓸 수 있다

lateinit과 notNull 델리게이트

반드시 처음 사용하기 전에 반드시 초기화가 되어 있을 경우에 lateinit을 쓰자

그러다 초기화 전에 사용되어 예외가 발생한다면 그 사실을 알아야 하므로 예외가 발생하는 것은 오히려 좋다

  • !! 연산자로 언팩(unpack)하지 않아도 된다.
  • 추후에 nullable로 쓰고 싶으면 nullable로 만들 수도 있다.
  • 프로퍼티가 초기화된 이후에는 초기화되지 않은 상태로 돌아갈 수 없다

그래서 lateinit은 라이프 사이클을 갖는 클래스처럼 메서드 호출에 명확한 순서가 있을 경우 잘 쓰일 수 있다

반면 이 키워드를 사용할 수 없는 경우가 있다. JVM에서 Int, Long, Double, Boolean과 같은 기본 타입과 연결된 타입으로 프로퍼티를 초기화해야 하는 경우다. 이런 경우는 Delegates.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)
    }
}

그리고 프로퍼티 위임하는 패턴은 추후 21장에서 더 자세하게 다룬다고 하니 투비 컨티뉴..

마무리

개인적으로 코틀린은 오히려 null을 잘 쓰고 잘 활용할 수 있게 지원하는 느낌이다 자바는 약간 Null에 속수무책이었는데.. 개발자는 이런 점이 오히려 개발하기 수월하다 느껴진다. 심지어 저 위의 키워드나 내용 족족 재밌어서 이번 편은 쉽게 이해하고 코드로 활용할 수 있을듯 하다.

profile
와니와니와니와니 당근당근

0개의 댓글