본문 바로가기

Kotlin/안전성, 가독성을 효과적인 향상시키는 사용법

👋Kotlin : 5️⃣예외를 활용해 코드에 제한을 걸어라

728x90

코틀린은 2010년에 처음 개발되었지만 2016년 2월에 첫 번째 안정 버전(stable version)이 공식적으로 배포되었을 정도로 굉장히 오랜 시간 동안 만들어지고 있고, 처음부터 대규모 애플리케이션을 실용적으로 만들기 위한 프로그래밍 언어로 설계되었고 현재는 모바일 애플리케이션, 웹 애플리케이션의 백엔드, 웹 애플리케이션의 프론트엔드 등 다양한 영역에서 활용되고 있습니다.

 

5️⃣예외를 통한 코드 제한

확실하게 동작해야 하는 코드가 있는 경우 예외를 통해 코드에 제한을 주는 것이 좋습니다. 

 

require() : 매개변수에 제한을 걸 수 있고, 제한을 만족하지 못하면 예외 던집니다.

check() : 상태와 관련된 동작에 제한을 걸 수 있고, 제한을 만족하지 못하면 예외 던집니다.

assert() : 테스트 모드에서만 동작하며, 어떤 것이 true인지 확인할 수 있고, 제한을 만족하지 못하면 예외 던집니다.

return 또는 throw와 함께 활용하는 ?: (Elvis 연산자)

 

위와 같이 제한을 거는 이유는 뭘까요? 

만약 문제가 생겨 메서드가 동작하지 못하고 종료되거나, 예상하지 못한 동작을 하게된다면, 메서드를 동작시키지 않는 것 보다 더 한 문제를 가져올 수 있습니다.

제한을 걸면 다른 개발자들이 보고 파악하기 수월합니다.

코드가 어느 정도 자체적으로 검사될 수 있으니, 관련된 단위 테스트를 줄일 수 있습니다.

스마트 캐스트 기능을 활용할 수 있게 되므로, 캐스트(타입 변환)를 적게 할 수 있습니다.

 

require

매개변수에 제한을 거는 require 함수를 활용하는 예를 들겠습니다.

1. 매개변수로 받은 Int 타입의 정수가 양의 정수여야 한다.

2. 매개변수로 받은 List 콜렉션의 리스트가 빈 값이 아니어야 한다.

3. 매개변수로 받은 String 타입의 문자열이 null이여선 안되고, 올바른 형식이어야 한다.

 

require(n >= 0)

require(list.isNotEmpty())

requireNotNull(value)
require(isCheckedForm(value)) { "$value의 형식이 만족하지 못했습니다." }

이와 같은 유효성 검사 코드는 함수의 가장 앞부분에 배치하므로 코드를 읽을 때쉽게 파악할 수 있고 람다를 활용해 예외를 던질 때 메세지를 정의할 수도 있습니다.

 

check

상태에 제한을 거는 check 함수를 활용하는 예를 들겠습니다.

1. 객체가 초기화 되어있는 경우에만 처리를 할 수 있다.

2. 로그인을 한 경우에만 처리를 할 수 있다.

3. 객체를 사용할 수 있는 시점에 처리할 수 있다.

 

check(this::isInitialized)

checkNotNull(token)

check(this::mediaPlayer.isOpen)

이와 같은 상태 검사 코드는 require 함수 뒤에 배치합니다. check를 나중에 하는 것 입니다.

 

nullability와 스마트 캐스팅

코틀린에서 require과 check 함수로 조건에 대해 true가 나왔다면, 해당 조건은 이후에도 true라고 가정하기 때문에 이를 활용해 타입 비교를 한다면, smart cast가 작동됩니다. 

fun getFood(food: Food) {
    require(food.type is FoodType.MILK)
    val milk : FoodType.MILK = food.type
    
     require(food.protein != null)
     val protein: Long = food.protein
}

 

그리고 requireNotNull과 checkNotNull 모두 스마트 캐스트를 지원하기 때문에 변수를 unpack(객체에서 데이터를 꺼내 다른 변수에 저장하는 것)하는 용도로 사용할 수 있습니다.

val protein = requireNotNull(food.protein)

 

Elvis 연산자

Elvis 연산자를 활용해 throw 또는 return 등의 사용하여 코드를 읽기 쉽고 유연하게 사용할 수 있습니다.

fun cooking(food: Food) {
	val name : String = food.name ?: return
    
     또는 
     
     val name : String = food.name ?: run {
     	log.d(TAG, "name이 NULL 입니다.")
        return
     }
}

fun cooking(food: Food) {
	val name : String = food.name ?: throw IllegalArgumentException("name is null")
}

 

이와 같이 예외를 활용해 코드를 제한하므로 얻을 수 있는 장점이 많습니다.