코틀린 스터디/이펙티브 코틀린

아이템8) 적절하게 null을 처리하라

막이86 2024. 2. 23. 11:11
728x90

이펙티브 코루틴을 요약한 내용입니다

  • 함수가 null을 리턴한다는 것은 함수에 다라 여러 의미를 가질 수 있음
    • String.toIntOrNull()은 String을 Int로 적절하게 변환할 수 없는 경우 null 리턴
    • Iterable<T>.firstOrNull(() → Boolean) 은 주어진 조건에 맞는 요소가 없을 경우 null 리턴
  • null은 최대한 명확한 의미를 갖는 것이 좋음
val printer: Printer? = getPrinter()
printer.print() // 오류

printer?.print() // 안전한 호출
if (printer != null) printer.print() // 스마트 캐스팅
printer!!.print() // not-null assertion
  • nullable 타입은 세 가지 방법으로 처리
    • ?., 스마트 캐스팅, Elvis 연산자 활용
    • throw
    • 함수 또는 프로퍼티를 리팩터링해서 nullable 타입이 나오지 않게 변경

null을 안전하게 처리하기

  • 안전 호출(safe call), 스마트 캐스팅(smart casting)
    • 애플리케이션 사용자 관점(개발자)에서 가장 안전한 방법
    • 편리하여 가장 많이 활용
    printer?.print() // 안전한 호출
    if (printer != null) printer.print() // 스마트 캐스팅
    
  • 대표적으로 인기 있는 다른 방법은 Elvis 연산자 사용
    • 연산자 오른쪽에 return 또는 throw를 포함한 모든 표현식이 허용
    val printerName1 = printer?.name ?: "Unnamed"
    val printerName2 = printer?.name ?: return
    val printerName3 = printer?.name ?: throw Error("Printer must be named")
    
  • 스마트 캐스팅은 코틀린의 규약 기능(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) }
}

방어적 프로그래밍과 공격적 프로그래밍

  • 모든 가능성을 올바른 방식으로 처리하는 것을 방어적 프로그래밍(defensive programming)
    • 프로덕션 환경에서 발생할 수 있는 것들로부터 프로그램을 방어해서 안정성을 높이는 방법을 나타내는 포괄적인 용어
    • 상황을 처리할 수 있는 올바른 방법이 있을 때 좋음
  • 모든 상황을 안전하게 처리하는 것은 불가능, 이런 경우 공격적 프로그래밍(offensive programming) 방법 사용
    • 예상하지 못한 상황이 발생했을 때 문제를 개발자에게 알려서 수정하게 만듬
    • require, check, assert가 공격적 프로그래밍을 위한 도구
  • 코드의 안전을 위해서는 두가지 방법 모두 필요

오류 throw하기

  • printer가 null일때 개발자에게 알리지 않고 코드가 그대로 진행 됨
    • 개발자가 오류를 찾기 어렵게 만듬
    • 문제가 발생한 경에는 개발자에게 오류를 강제로 발생시켜 주는 것이 좋음
    • 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(!!)과 관련된 문제

  • nullability는 어떻게든 적절하게 처리해야 하므로 추가 비용이 발생
  • 필요한 경우가 아니라면 nullability 자체를 피하는 것이 좋음
  • null은 중요한 메시지를 전달하는 데 사용될 수 있음
  • 이유 없이 null을 사용했다면 다른 개발자들이 코드를 작성할 때
    • !!연산자를 사용하게 됨
    • 의미 없이 더럽히는 예외 처리를 해야함
  • nullability를 피할 때 사용할 수 있는 몇가지 방법
    • 클래스에서 nullability에 따라 여러 함수를 만들어 제공
      • List → get, getOrNull 함수
    • 클래스 생성 이후에 확실하게 설정된다는 보장이 있다면 lateinit 프로퍼티와 notNull 델리게이트 사용
    • 빈 컬렉션 대신 null을 리턴하지 않기
      • List<Int>?와 Set<String?>과 같은 컬렉션을 빈 컬렉션으로 둘 때와 null로 둘 때는 의미가 완전 다름
      • 요소가 부족하다는 것을 나타내려면 빈 컬렉션 사용
    • nullable enum과 None enum값은 완전 다른 의미
      • null enum은 별도 처리 필요
      • None enum 정의에 없으므로 필요한 경우에 사용하는 쪽에서 추가해서 활용

lateinit 프로퍼티와 notNull 델리게이트

  • 클래스 생성 중에 초기화할 수 없는 프로퍼티를 가지는 것은 드문 일이 아니지만 분명 존재
    • 프로퍼티는 사용 전에 반드시 초기화해서 사용해야 함
    • ex) JUnit의 @BeforEach
    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과 nullable 비교
    • !! 연산자로 언팩 하지 않아도 됨
    • 어떤 의미를 나타내기 위해서 null 사용하고 싶을 때, nullable로 만들 수 있음</aside>
    • <aside> ❓ nullable이 아닌데???
    • 프로퍼티가 초기화된 이후에는 초기화되지 않은 상태로 돌아갈 수 없음
  • lateinit을 사용할 수 없는 경우
    • JVM에서 Int, Long, Double, Boolean과 같은 기본 타입과 연결된 타입으로 프로퍼티 초기화해야 하는 경우
    • lateinit 보다는 약간 느린 Delegates.notNull을 사용
    class DoctorActivity: Activity() {
    	private var doctorId: Int by Delegates.notNull()
    	private var fromNotification: Boolean by Delegates.notNull()
    
    	override fun onCreate(saveInstanceState: Bundle?) {
    		super.onCreate(saveInstanceState)
    		doctorId = intent.extras.getInt(DOCTOR_ID_ARG)
    		fromNotification = intent.extras.getBoolean(FROM_NOTIFICATION_ARG)
    	}
    }
    
  • 초기화하는 프로퍼티는 지연 초기화하는 형태로 프로퍼티 위임을 사용할 수 있음
class DoctorActivity: Activity() {
	private var doctorId: Int by arg(DOCTOR_ID_ARG)
	private var fromNotification: Boolean by arg(FROM_NOTIFICATION_ARG)
}
728x90