Kotlin (코틀린)

[Kotlin] 코틀린의 null / null 처리 연산자 ? ?. !! / 엘비스 연산자 ?:

Oscar:) 2024. 5. 11. 19:43

 

 

이번 포스팅에서는 코틀린에서의 null에 대해 알아보자.

 

 


 

코틀린에서의 null 처리

 

 

앞선 코틀린 특징 포스팅에서도 언급했지만,

코틀린은 NPE로부터 안정성을 보장할 정도로 null을 예민하게 다룬다.

 

 

코틀린이 null을 어떻게 처리하는지 알아보기 전에,

우리가 null을 주로 어떠한 경로로 만나는지부터 간단히 생각해 보자.

 

객체를 null과 관련지어 다음 3가지로 구분할 수 있다.

- 구조 상 절대 null이 될 수 없는 객체
- 구조 상 무조건 null인 객체
- null이 될 수도 있는 객체

 

 

개발을 하다보면 느끼겠지만, 무조건 null인 객체는 코드 내 경고로서 미리 확인할 수 있다.

여기서 중요한 점은, null이 될 수도 있는 객체다.

 

코틀린은 개발자가 작성한 코드 중 이미 null인 객체 뿐만 아니라,

null이 될 수 있는 가능성이 있는 객체까지 추적한다.

 

 

대부분의 NPE는 개발자가 의도적으로 발생시키는 것이 아니다.

null이 될 수 있는 가능성이 있는 객체에 대한 미흡한 처리때문에 발생한다고 볼 수 있다.

 

 

코틀린에서는 위와 같은 상황에서 사용할 수 있는 null 처리 연산자를 제공한다.

 

 

 


null 처리 연산자

 

 

null 처리 연산자는 간단한 예제를 통해 알아볼 예정이다.

 

아래 대부분의 예제는 매개변수로 받아온 값을 처리하는 방식으로 작성했다.

null 처리가 필요한 대부분의 경우가 외부에서 받아온 값을 정의하는 상황이기 때문이다.

 

 

✅ ? 연산자

 

위에서 언급했던 null이 될 수도 있는 객체를 명시할 수 있다.

// null 허용 명시
var str: String? = null

// 명시 안하면 컴파일 에러 발생
var str: String = null

 

위 처럼 타입 뒤에 ? 연산자를 붙여서 사용한다.

 

 

여기서 재미있는 점은, 코틀린은 String과 String?을 엄연히 다르게 취급한다는 것이다.

간단한 예제를 보자.

// String 객체를 매개변수로 받는 함수
fun nullTest(str: String) {
	println(str)
}

// String? 객체를 위 함수의 인자로 넣음
val str : String? = "Oscar"
nullTest(str)

 

String을 매개변수로 받는 함수에 String? 객체를 넣으면

 

 

위와 같이 Type mismatch 에러를 확인할 수 있다.

 

이처럼 같은 String이라도 null이 될 수 있냐 · 없냐 만으로

타입이 다르다고 못 박아버릴 만큼 코틀린은 null 처리에 예민하다.

 

 

 


 

✅ ?. 연산자

 

위에서 설명했던 ? 연산자에 다른 함수나 객체를 호출(.)하는 연산자를 합친 형태이다.

fun strLength(str: String?) {
	println(str?.length)
}

 

출력문 내에 str?. 를 보면 된다.

 

?. 연산자를 기준으로 왼쪽의 객체가 null이라면 null을 반환하며, 오른쪽의 함수는 동작하지 않는다.

반면 왼쪽의 객체가 null이 아니라면 오른쪽의 함수가 동작한다.

 

fun strLength(str: String?) {
	println(str?.length)
}

// 출력 : null
var str : String? = null
strLength(str)


// 출력 : 5
var str : String? = "Oscar"
strLength(str)

 

첫 번째 예제에서는 null을 인자로 넣었기에

String.length() 메서드를 호출했음에도 null이 출력되는 것을 확인할 수 있다.

 

두 번째 예제에서는 null이 아닌 객체를 넣었으므로 글자 수를 출력했다.

 

 


 

 

하지만 다음과 같이 ?. 연산자를 코드 한 줄에 과하게 사용하는 것은 지양해야 한다.

val str = a?.b?.c?.d

 

만약 str 객체가 null이라면, a~c 중 어디에서 null이 발생했는지 추적하기 어렵기 때문이다.

 

따라서 ?. 연산자는 되도록 작은 단위에서 사용할 것을 추천한다.

 

 

 


 

✅ !! 연산자

 

위에서 설명했던 nullable 객체를 사용하다보면,

분명히 null이 될 수 없는 상황인데도 귀찮게 반복적으로 null 처리를 해줘야 한다.

 

이는 컴파일러가 null이 될 수 있는 상황은 인식하지만,

반대로 null이 될 수 없는 상황은 명확히 인식하지 못하기 때문이다.

 

 

이러한 상황에서는 !! 연산자를 사용하여 강제로 not null을 명시할 수 있다.

fun strLength(str: String?) {
	println(str!!.length) // !!연산자로 not null 명시
}

strLength(null) // NPE 발생

 

당연히 해당 매개변수에 null을 대입하면 NPE가 발생한다.

 

그렇기에 정말 확실하게 not null일 때만 사용해야 한다.

 

 

 


 

✅ ?: 연산자

 

엘비스 연산자라고 불린다.

 

?: 연산자를 기준으로 좌측의 값이 null이라면 우측의 값이 대입되는 원리이다.

즉, 해당 객체가 null일 때 대체될 Default 값을 지정할 수 있는 것이다.

 

어떠한 상황에서 사용될 수 있는지 과정을 통해 알아보자.

 

 

 

다음 예제는 ?. 연산자를 사용했을 때, null이 출력되는 상황이다.

fun strLength(str: String?) {
	val length = str?.length
	println(length)
}

strLength(null) // 출력 : null

 

 

위와 같은 상황에서 null이 아닌 다른 값을 대입해주고 싶다면?

fun strLength(str: String?) {
	var length : Int
	if (str == null) {
		length = -1
	} else {
		length = str.length
	}
	println(length)
}

strLength(null) // 출력 : -1

 

우리는 위와 같이 null check 조건문을 만들어서 해결할 수 있다.

 

 

● 하지만 위 null check 대신 엘비스 연산자를 이용하면?

fun strLength(str: String?) {
	val length = str?:-1
	println(length)
}

strLength(null) // 출력 : -1

 

위 처럼 간소화된 코드를 사용할 수 있다.

 

 

코틀린이 null을 예민하게 다루는 만큼 null check가 필연적인 상황이 많은데,

그럴 때마다 엘비스 연산자를 정말 유용하게 사용하곤 한다.

 

 

 


 

 

 

코틀린에서의 null에 대해 알아보았다.

 

null 처리 연산자를 자주 사용할수록 런타임 시 발생할 수 있는

NPE에 대해 확실히 예방한다는 느낌을 받곤 한다.