Kotlin 자바를 알고 있어도 코들린에서 막히는 부분

자바 개발자가 코틀린을 사용하면서 생기는 문제점이나 막히는 부분들에 대해 찾아보았습니다.

코틀린은 자바와 연동하여 사용할 수 있기 때문에 똑같을 거라는 생각을 할 수 있습니다.

하지만 코틀린도 하나의 프로그램 언어이기 때문에 코틀린이라는 언어가 가진 특성을 이해하고 개념을 공부하면서 기능들을 어떻게 사용해야 할지 알아가야 합니다.

코틀린언어를 사용하면서 중요하게 생각되는 부분들을 작성했습니다.

 

Smart Cast (스마트 캐스트)

자바에서 인스턴스의 실제 타입을 알아보기 위해 사용하는 연산자로 instanceof를 사용하는 경우가 많습니다.

하지만 여러 가지 이유로 인해 instanceof를 사용하는 것을 추천하지 않는 경우가 많더군요.

그중 하나가 자바에서 instanceof는 확인하려는 대상의 타입의 체크와 캐스트(cast)가 동시에 되지 않는다는 것입니다.

// Java
Animal animal = new Cat();

if (animal instanceof Cat) {
	Cat cat = (Cat)animal; // 다운 캐스팅
	cat.catMethod(); // Cat 함수를 사용하여 처리
}

 

instanceof로 animal이 Cat에 해당되는지 확인을 하였지만, 조건문 안을 보면 Cat에 다운 캐스팅을 해줘야 합니다.

다운 캐스팅은 안전한 처리가 아니기 때문에 가능하면 사용하고 싶지는 않습니다.

만약에 (Dog) animal 라고 코딩을 했다면 실행할 때에 ClassCastException 이 발생했을 겁니다.

if 문에서 aninal이 Cat에 해당되는 것을 확인하였지만, 명시적으로 캐스팅을 해주지 않으면 안 됩니다.

즉 instanceof로 확인을 하였다 해도 개발자가 잘못해서 코딩을 하게 되면 에러가 발생할 수도 있게 됩니다.

이러한 에러를 방지할 수 있는 것으로 코틀린에서는 Smart Cast가 있습니다.

//Kotlin
val animal = Cat()

if (animal is Cat) { // Smart Cast
	animal.catMethod() // Cat 함수를 사용하여 처리
}

Smart Cast는 대단한 기능을 가진 것처럼 보이지는 않지만 개발자에 코딩 미스를 방지해 줄 수 있습니다.

 

 

Null Safety

코틀린을 공부하면서 가장 좋은 기능 중 하나가 아닌가 생각이 듭니다.

자바에서는 아래와 같은 코드를 작성하면 NullPointerException이 발생할 가능성이 있습니다.

// Java
String s = foo(); // null 일 가능성이 있음
int l = s.length(); // 실행중 null일 경우 NullPointerException

 

코틀린에서는 실행할 때 에러가 발생하지 않고 컴파일 에러가 발생합니다.

// Kotlin
val s = foo() // null 일 가능성이 있음
val l = s.length // 컴파일 에러

 

실행할때 에러가 발생할지도 모르는 코드를 코틀린에서는 컴파일러가 검출하여 컴파일 에러를 발생시킵니다.

프로그램을 만들 때에 에러를 발견하는 타이밍이 늦으면 늦을수록 수정하기가 힘들어지는 경우가 많습니다.

실행할때 에러가 발생하여 버그를 찾았다는 것은 테스트 진행 중에 발견되었을 가능성이 큽니다.

또는 릴리스를 하여 실제 사용 중에 버그로 발견될 수도 있습니다.

테스트 중이나 실제 사용 중에 NullPointerException이 발생했다면 개발자 입장으로서는 많은 잔소리를 들어야 하는 경우가 생길지도 모릅니다.

코틀린에서 컴파일 에러가 발생하여 null 체크를 해줘야 한다면 다음과 같이 할 수 있습니다.

// Kotlin
val s = foo() // null 일 가능성이 있음
if (s != null) {
	val l = s.length // OK
}

 

이러한 경우에도 Smart Cast가 용이합니다.

if 문의 null 체크에서 s가 null이 아닌 경우 s는 String으로 취급됩니다.

의문점으로 코틀린의 Smart Cast는 어떠한 타입을 어떻게 캐스팅하게 되는 것일까요?

코틀린에서는 null일 가능성이 있는 값의 타입과 null이 아닌 값의 타입을 구별해줍니다.

null일 가능성이 있는 값의 타입을 Nullable Type

null이 아닌 값의 타입을 Non-null Type

이라고 부릅니다.

자바와 동일하게 String이라고 작성하면 Non-null Type이 됩니다.

Non-null Type일 경우 null을 대입할 수 없습니다.

// Kotlin
val s: String = null // 컴파일 에러

 

변수 선언시 타입에 ?를 붙여 String? 라고 작성하면 Nullable Type이 됩니다.

Nullable Type은 null을 대입할 수 있습니다.

// Kotlin
val s: String? = null // OK

 

주의해야 할 것으로는 String과 String?은 전혀 다른 타입이라는 것입니다.

String? 타입의 값에 String에 있는 함수를 사용할 수 없습니다.

예를 들어 String에는 length라는 함수가 있어 s.length라고 사용할 수 있지만 String?에는 length 함수가 없기 때문에 s.length를 작성하면 컴파일 에러가 발생합니다.

그리고 중요한 것으로 String은 String?의 서브 타입입니다.

즉 String에서 String?에 대입할 수 있지만 String?에서 String으로는 대입할 수 없습니다.

// Kotlin
val a: String = "abc"
val b: String? = a // OK

val c: String? = "xyz"
val d: String = c // 컴파일 에러

 

다시 null 체크로 돌아와서 Smart Cast는 어떠한 타입으로 어떻게 캐스팅을 하는지의 답변으로, String?을 String으로 캐스팅했다고 볼 수 있습니다.

// Kotlin
val s: String? = foo() // 여기서는 String?
if (s != null) { // 여기서 s 는 String 캐스팅
	val l = s.length
}

 

즉, s != null은 s is String처럼 움직입니다.

다음 샘플을 보도록 하겠습니다.

// Kotlin
val s: String? = "abc"
if (s is String) { // null 체크를 하지 않고 is 를 사용 Smart Cast
	val l = s.length
}

 

이러한 구조로 코틀린에서는 NullPointerException을 방지하고 있습니다.

자바에서는 NullPointerException이 발생하는 경우가 많지만, 코틀린에서는 Null Safety로 인해 발생이 줄어들게 됩니다.

 

!!

코틀린의 List(Iterable )에는 max라는 함수가 있습니다.

이 함수는 List 안에 있는 요소 중 가장 큰값을 반환합니다.

// Kotlin
listOf(2, 7, 5, 3).max() // 7

 

하지만 List<Int>에 max 함수의 반환 값은 Int가 아닙니다.

Int?입니다.

이유는 List에 요소가 없는 경우도 있기 때문입니다.

// Kotlin
listOf<Int>().max() // null

 

예제를 보도록 하겠습니다.

반드시 1개 이상의 요소를 가지고 있는 List<Int>가 있고, 그 List에 요소 중 최대값을 제곱을 하려고 합니다.

자바로 작성을 한다면 max 함수의 반환값을 제곱하면 됩니다.

// Java
List<Integer> list = ...; // 반드시 1개 이상의 요소를 가지 있음
int maxValue = Collections.max(list);
int square = maxValue * maxValue; // OK

 

하지만 코틀린에서는 컴파일 에러가 발생합니다.

// Kotlin
val list: List<Int> = ...; // 반드시 1개 이상의 요소를 가지 있음
val maxValue = list.max()
val square = maxValue * maxValue // 컴파일 에러

 

이유는 maxValue의 타입은 Int가 아닌 Int?이기 때문입니다.

Int?는 계산식인 * 연산자를 사용할 수 없습니다.

이것을 해결하기 위해서는 null체크를 해줘야 합니다.

// Kotlin
val list: List<Int> = ...; // 반드시 1개 이상의 요소를 가지 있음
val maxValue = list.max() // maxValue 는Int? 타입
if (maxValue != null) { // maxValue 를 Int 타입으로 캐스팅
	val square = maxValue * maxValue // OK
} else {
	throw RuntimeException("Never reaches here.") // 예외 처리
}

 

사실 else 부분으로 들어가는 경우는 없다고 생각이 들지만 만약의 버그를 위해 예외 처리를 작성하였습니다.

위의 샘플처럼 반드시 1개 이상의 요소를 가진 List에서 값을 취득하는데 null 체크를 해줘야 하는 것은 필요 없는 소스가 늘어날 뿐입니다.

그리고 매번 작성해줘야 하는 것도 번거롭습니다.

코틀린의 Null Safety로 인해 이렇게 필요 없는 코드를 작성해야 한다고 하면 Null Safety는 그다지 도움이 안 된다고 할 수 도 있습니다.

이러한 문제를 해결하기 위해 있는 것이 !! 연산자 입니다.

!!는 T?를 T로 변환해줍니다.

단 값이 null인 경우에는 예외 처리로 빠집니다.

// Kotlin
val list: List<Int> = ...; // 반드시 1개 이상의 요소를 가지 있음
val maxValue = list.max()!! // maxValue 를 Int 타입으로 변환
val square = maxValue * maxValue // OK

 

주의 점으로 !!는 Non-null 타입으로 변환에 실패하면 NullPointerException 을 발생시킵니다.

Null Safety의 안정성을 무시하게 됩니다.

즉, 반드시 null이 아닌 것을 알 수 있는 값에만 !!를 사용해야 합니다.

 

 

?.

?.를 사용하면 T? 타입이라도 값이 null이 아닌 경우에는 T 함수를 사용할 수 있습니다.

// Kotlin
val s: String? = ...
val l: Int? = s?.length

 

변수 l을 Int? 로 선언하였습니다.

s가 null일 경우 s?.length의 결과는 null이 됩니다.

그렇기 때문에 length의 반환 타입은 Int가 되지만,

s?.length는 Nullable Type이기 때문에 Int?로 설정해줘야 합니다.

.?는 null을 가질지도 모르는 변수를 사용해야 하는 경우에 사용하면 편리합니다.

?:

값이 없는 경우에 지정한 초기값으로 설정해주는 경우가 있습니다.

이러한 경우에 ?: 연산자를 사용하면 편리합니다.

예를 들어 사용자 이름이 설정되어 있지 않는 경우 No Name 이라고 표시해주는 샘플을 보겠습니다.

val name: String? = ...
val displayName: String = name ?: "No Name"

 

여기서 중요한 것은 displayName 타입이 String?가 아닌 String으로 설정했다는 것입니다.

?:은 name 변수가 null일 경우에 문자열 “No Name”을 대입해주기 때문에 null이 들어가는 경우가 없습니다.

그렇기 때문에 String?가 아닌 String으로 설정하였습니다.

?:가 편리한 점으로는 ?: 우측에 return 등의 문법을 작성할 수 있습니다.

// Kotlin
fun foo(list: List<String>) {
	// list 에 요소가 없는 경우 return
	val length: Int = list.min()?.length ?: return 
	// list를 이용한 처리
	...
}

 

코틀린을 사용하면서 지금까지 사용해왔던 자바와 다른 점들과 코틀린의 특성에 대해 알아봤습니다.

적은 내용 외에도 많은 것들이 있습니다.

못적은 부분들은 추가적으로 포스팅하도록 하겠습니다.

아직 공부하는 단계인지라 이상한 내용이 있으면 코멘트 남겨주세요.

댓글