본문 바로가기
2023년 이전/kotlin

kotlin - 널이 될수 있는 타입(?), 안전 호출 연산자(?.), 엘비스 연산자(?:), 안전 캐스트(as?)

by JeongUPark 2020. 3. 18.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 


코틀린은 자바에서 흔히 볼 수 있는 NullPointerException 오류를 피할 수있게 돕는 널 가능성이라는 코틀린 타입 시스템 특성이 있습니다. 즉, 타입 시스템에 널이 될 수 있는지 여부를 추가함으로써 컴파일러가 여러가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생 할 수 있는 예외의 가능성을 줄여줍니다.

 

널이 될 수 있는 타입

널이 될 수 있는 타입이란 프로그램안의 프로퍼티나 변수에 null을 허용하게 만드는 방법을 말합니다. 어떤 변수가 null을 허용한다면 이 변수는 NullPointerException이 발생할 수 있다는 의미입니다. 코틀린은 이런 null이 될 수있는 호출을 금지함으로서 NullPointerExeption에 대한 오류를 많이 방지해 줍니다. (그럼다고 null을 쓸 수 없는 것은 아닙니다.)

int strLen(String s){
	return s.length();
}

위의 자바코드는 s에 null 값이 들어오면 NullPointerException이 발생합니다. 그럼 NullPointerException을 방지하기 위해 s를 체크해야할까요? 그것은 필요에 따라서 달라질 것입니다. 하지만 코틀린에서 위 코드를 작성하면

fun strLen(s:String) : int = s.length

이렇게 작성되며, 이때는 s에는 null이 될 수 잇는 인자를 넘기는 것은 금지 되거나 컴파일시 오류가 발생합니다.

만일 s에 null이 들어오도록 하고싶다면

fun strLen(s:String?) : int = s.length

s뒤에 ?를 붙여주면 null을 인자로 받을 수 있습니다.

위 처럼 타입뒤에 물음표(?)를 붙이면 그 타입의 변수나 프로퍼티에 null을 참조 저장할 수 있다는 뜻이 됩니다. 반대로 물음표가 없다면 그 타입은 null을 참조 할 수 없다는 뜻입니다. 그러므로 코틀린의 모든 타입의 기본은 null을 참조 할 수 없습니다.

 

그리고 위의 fun strLen(s:String?) : int = s.length 를 실행하면 error가 발생합니다. 그 이유는 s가 null을 인자로 받을 수 있기 때문에 변수 s에 대한 메소드 .length를 직접 호출 할 수 없기 때문입니다. 그러므로 다음과 같이

fun strLen(s:String?) : int =
	if(s !== null) s.length else 0

null일 때 아닐 때를 구분하여 동작시켜야 합니다.

 

그럼 이렇게 제약이 많은데 null을 사용해야 할까? 일반적으로 null이 필요한 이유는 null과 비교를 위해 많이 사용 됩니다.

 

안전한 호출 연산자 ?.

코틀린이 제공하는 기능중 유용한 기능은 안전 호출 연산자인 ?. 이다. ?.은 자바로 따지면 (s는 String 입니다.)

if( s != null){
	s.toUpperCase()
}else{
	null 
}

으로 볼 수 있습니다.

위의 자바 코드를 코틀린에서 안전한 호출 연산자(?.)를 사용하면

s?.toUpperCase()

로 간략하게 작성할 수 있습니다.

즉 s가 null이 아니면 toUpperCase()를 실행하게 되고, null이면 null이 됩니다. 그리고 안전한 호출 연산자의 결과는 그 호출 인자가 null일 경우 null이 되기 때문에 안전 호출 연산자 사용시 그 값이 null이 될 수 있음을 인지하고 사용해야 합니다.

val result : String? = s?.toUpperCase()

이 처럼 안전호출연산자를 사용하여 값을 받을 경우 그 프로퍼티의 타입은 물음표(?)를 붙여서 널이 가능하다는 것을 명시해야 합니다.( result : String?)

 

그리고 안전 호출 연산자를 연속으로 사용하여 널체크를 편리하게 할 수 도 있습니다.

class Person(val name:String, val company : Company)

fun Person.countryName() : String{
	val country = this.company?.address?.country
	return if(country != null) country else "unKnow"
}

위와 같이 안전호출연산자를 연속으로 사용하여 데이터에 대한 null 체크를 연속으로 할 수 도 있습니다.

 

엘비스 연산자 ?:

코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공합니다. 그 연산자의 이름은 엘비스 연산자(?:)라고 합니다.

이 연산자를 사용하면 null일 때의 디폴트 값을 지정할 수 있습니다.

fun strLenSafe(s: String?): Int = s?.length ?: 0

위와 같이 작성하면 s가 null이면 안전연산호출에 의해 s?.length가 null을 반환하고 엘비스 연산자(?:)에 의해 그 값이 0을 반환하게 됩니다.

이 부분이 조금 복잡하다면 간단하게

fun test(s:String?) : String = s?: ""

test를 호출 했는데 s가 null이면 빈 string을 반환합니다.

즉, 엘비스 연산자(?:)는 좌항(위 코드에서는 s가 되겠습니다.)의 값을 계산하여 null이면 우항 값으로 null이 아니면 좌항값으로 계산하도록 해줍니다.

이런 엘비스연산자(?:)를 사용하여 우항에 return이나 throw등의 연산을 넣어 null일 경우에 대한 예외를 처리할 수 있게 대줍니다.

fun printShippingLabel(person: Person) {
    val address = person.company?.address
      ?: throw IllegalArgumentException("No address")
    with (address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

printprintShippingLabel 함수는 모든 정보(Person, Company, Address)가 제대로 있으면 그 주소를 반환하지만 아닐 경우 NullPointerException 대신 아래와 같이 의미 있는 Exception을 보여줍니다.

Exception in thread "main" java.lang.IllegalArgumentException: No address
	at ElvisOperatorKt.printShippingLabel(ElvisOperator.kt:12)
	at ElvisOperatorKt.main(ElvisOperator.kt:24)

 

여기까지 배운 안전한 호출 연산자(?.)와 엘비스 연산자(?:)를 통하여 자바의 "if not - null" 검사를 더 간편하게 수행할 수 있는 것을 알 수있습니다.

 

안전한 캐스트 as?

 자바에서 보면 instanceof 가 있습니다. 이는 어떤 타입이 이 타입으로 Cast 될 수 있는지를 체크하는데, 이런 체크 없이 Cast 될 수 없을 경우에 Cast를 요청하면 ClassCastException을 반환 합니다. 이런 기능을 코틀린에서는 instanceof를 is가 대신 하고 Cast의 경우에는 as가 대신하고 있습니다.

여기서 Cast하고 싶은 인자에 대하여 as를 사용하려면 is를 통하여 타입 체크를 해야 할 것 입니다.(그렇지 않으면 cast 할 수 없을 경우 위에서 말한  ClassCastException가 발생할 것입니다.)하지만 코틀린은 이를 더 간결하게 처리할 수 있습니다.

test as? Type

이렇게 test라는 인자를 안전 캐스트하면, test가 Type으로 Cast 가능하면(test is Type == true) test as Type이 되고  아닐 경우 (test is Type == false) null을 반환합니다.

그리고 안전 캐스트(as?)뒤에 엘비스 연산자(?:)을 사용하면 캐스트를 수행 할 수 없는 경우 그에 대한 디폴트를 정할 수 있습니다.

class Person(val firstName: String, val lastName: String) {
   override fun equals(o: Any?): Boolean {
      val otherPerson = o as? Person ?: return false

      return otherPerson.firstName == firstName &&
             otherPerson.lastName == lastName
   }

   override fun hashCode(): Int =
      firstName.hashCode() * 37 + lastName.hashCode()
}

위의 코드에서 o가 Person으로 캐스트 되면 otherPerson에 그 값이 들어 가고, 캐스트 될 수 없으면 as?가 null을 반환하고 ?:에 의해 return false가 반환 됩니다.

반응형

'2023년 이전 > kotlin' 카테고리의 다른 글

코루틴(Coroutine)이란 -2  (0) 2020.09.13
코루틴(Coroutine)이란? -1  (0) 2020.09.12
kotlin - 수신 객체 지정 람다  (0) 2020.03.10
kotlin - 자바 함수형 인터페이스 활용  (2) 2020.03.10
kotlin - 시퀀스(Sequence)  (0) 2020.03.02