uncategorized

코틀린 타입 시스템

널 가능성

널이 될 수 있는 타입

어떤 타입이든 타입뒤에 ?를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다. 널 가능성이 있는 타입은 호출 가능한 메서드가 제한된다. 또한 파라미터에 널 가능성이 없는 타입이 선언된 경우 널 가능성이 있는 타입은 인자로 전달할 수 없다. 널 가능성이 있는 타입은 널 체크 이후 널 가능성이 없는 타입처럼 사용할 수 있다.

자바에서 NPE 오류를 다루는 방법
어노테이션을 활용해서 널이 될 수 있는 여부를 표시.(@Nullable, NotNull) 하지만 이런 방식이 정식 자바 컴파일에 해당하는 것도 아니고 모든 NPE 발생 가능 지점에 사용하기도 어렵다. null 대신 optional을 사용할 수 있는데 성능이 저하될 수 있고 외부 라이브러리에서 Null 반환을 막을 수 없다.

널 가능성이 있는 타입과 널 가능성이 없는 타입의 객체는 같다.
널이 될 수 있는 타입이 특별한 래퍼 클래스는 아니다. 모든 검사는 컴파일 시점에 수행되고 런타임에 부가 비용이 들지 않는다.

안전한 호출 연산자 : ?.

?.은 null 검사와 메서드 호출을 한번의 연산으로 수행한다.

1
2
3
4
val s: String? = null
val upperS = s?.uppercase()
// 즉 이 코드와 같다.
if (s != null) s.uppercase() else null

이때 안전한 호출의 반환값도 널 가능성이 있는 타입임을 명심하자.

엘비스 연산자 : ?:

특정 객체가 null 인 경우 디폴트값을 줄 때 엘비스 연산자를 사용하면 된다.

1
val result : String = s?: "it's null"

안전한 호출과 연쇄해서 사용하기도 한다.

1
val result : String = s?.uppercase()?: "it's null"

엘비스 연산자 우항에는 식이 올 수 있다. 그래서 return이나 throw도 활용할 수 있다.

안전한 캐스트: as?

코틀린은 타입 캐스트를 as로 하는데, 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생한다. 그래서 as를 하기 전에 is로 해당 타입으로 검사가 가능한지 확인할 수 있다. 하지만 이보다 더 좋은 방법이 as?이다
as?는 해당 타입으로 변경할 수 없으면 null을 반환한다.

1
2
3
4
5
fun equals(o1: Any?, o2: Any?): Boolean {
val person1: Person = o1 as? Person ?: return false
val person2: Person = o2 as? Person ?: return false
return person1.name == person2.name && person1.age == person2.age
}

널 아님 단언 : !!

널이 될 수 있는 타입을 명시적으로 널이 될 수 없는 타입으로 바꾸는 연산자가 !! 이다. 널 아님 단언을 사용했는데 대상 객체가 null이면 NPE가 발생하니 다른 연산자를 통해 처리하도록 하자.

let 함수

let은 널 가능성이 있는 타입이 null이 아닌 경우에만 람다를 실행하도록 하는 함수이다.

1
2
3
4
5
fun sayHello(name: String?) {
name?.let {
println("hi my name is $name")
}
}

만약 여러 값이 널인지 검증해야 된다면 let을 중첩시켜서 할 수는 있지만 코드가 복잡해지니 if로 여러값을 한번에 검사하는 게 낫다.

나중에 초기화할 프로퍼티

코틀린에서는 클래스 안에 널이 될 수 없는 프로퍼티는 생성자 안에서 초기화되어야 한다. 만약 초기값으로 널이 아닌 값을 주지 못하면 널이 될 수 있는 프로퍼티로 선언해야 한다. 하지만 널이 될 수 있는 타입을 쓰면 모든 프로퍼티 접근에 널 검사를 하거나 !! 연산자를 사용해야 되는 불편함이 생긴다. 이를 해결하기 위해 나중에 초기화하는 기능을 사용할 수 있다. 나중에 초기화는 lateinit 변경자를 붙여서 특정 프로퍼티를 나중에 초기화 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Manitto {
private lateinit var manitto: Person

fun setManitto(person: Person) {
this.manitto = person
}

fun hello() = println("hi my name is ${manitto.name}")
}

val manitto = Manitto()
manitto.setManitto(Person("Bob", 11))
manitto.hello()

널이 될 수 있는 타입 확장

널 가능성이 있는 타입에 확장 함수를 정의할 수 있다. 확장 함수는 해당 객체를 통해 디스패치된 메서드가 아니기 때문에 해당 변수가 널인지 검사를 내부에서 할 수 있다. 이렇게 되면 안전한 호출을 사용하지 않아도 된다.

1
2
3
4
5
6
7
8
fun String?.printHello() {
if (this == null) println("it's null") else println("hello $this")
}

var str: String? = "hey"
str.printHello()
str = null
str.printHello()

타입 파라미터의 널 가능성

코틀린의 모든 타입 파라미터는 기본적으로 널이 될 수 있다. 그래서 안전한 호출을 써야한다. (물론 예시코드는 안전한 호출없이도 된다.)

1
2
3
fun <T> printHashCode(t: T) {
println(t?.hashCode())
}

만약 타입에 널이 될 수 없게 하려면 상한을 걸어두자.

1
2
3
fun <T: Any> printHashCode(t: T) {
println(t.hashCode())
}

널 가능성과 자바

자바에서 타입을 코틀린으로 옮길 때는 어노테이션의 유무에 따라 달라진다.
먼저 어노테이션이 있는 경우(@Notnull, @Nullable)는 자연스럽게 널 가능성이 없거나 널 가능성이 있는 타입으로 치환된다.

문제는 어노테이션이 없는 경우인데, 이 경우 코틀린에서 플랫폼 타입을 사용한다.

플랫폼 타입

플랫폼 타입은 널 가능성을 판단할 수 없는 타입을 말한다. 이 경우 널 가능성이 없다고 판단하고 사용해도 되고, 널 가능성이 있다고 판단하고 널 처리를 해도 된다. 개발자에게 판단을 넘긴 셈이다.

자바 메서드를 오버라이딩할 때 널 주의

코틀린에서 자바 메서드를 오버라이딩할 때 그 메서드의 파라미터와 반환 타입을 널이 될 수 있는 타입으로 선언할 지 널이 될 수 없는 타입으로 선언할 지 결정해야 한다. 만약 널 가능성이 있는 파라미터로 개발하면 코틀린 컴파일러가 널이 아님을 검사하는 단언문을 만들어줁다. 내부에서 사용하지 않는 파라미터도 널 체크가 되기 때문에 이를 유의하자.

코틀린의 원시 타입

코틀린은 숫자나 불린을 래퍼타입으로 구분하지 않는다. 다만 런타임에 숫자 타입은 가장 효율적인 방식으로 표현된다. 원시타입으로 컴파일이 안되는 제네릭 클래스 같은 경우에 래퍼타입이 들어간다.

널이 될 수 있는 원시 타입: Int?, Boolean?

코틀린의 Int가 자바의 원시타입 int로 대부분 변환되고 제네릭 같은 경우에만 래퍼 클래스로 활용된다는 건 자연스럽다. 둘다 널 가능성이 없기 때문이다.

한편 코틀린에서 널 가능성이 있는 원시타입을 사용하면 자바에서는 래퍼 타입으로 변환된다. 또한 제네릭 클래스의 경우 래퍼 타입을 사용한다.

1
val listOfInts = listOf(1, 2, 3)

위 예시는 자바에서 리스트 안에 원시 타입이 오지 못하므로 래퍼 클래스인 Integer가 오는 게 자연스럽다.

숫자 변환

코틀린은 한 숫자 타입이 정해지면 다른 숫자 타입으로 자동으로 변환되지 않는다. 대신 직접 변환 메서드를 호출해야 한다.

1
2
val i = 1
val l: Long = i.toLong()

Any, Any? : 최상위 타입

자바에 Object가 있다면 코틀린에는 Any가 있다. 자바에서는 모든 객체의 조상이 Object이지만 코틀린은 원시 타입도 포함한 모든 값과 객체의 조상이 Any이다.
다만 널 가능성에 따라 두 종류로 나뉘며, toString, equals, hashCode 외에 다른 Object 메서드는 지원하지 않는다. 만약 그런 메서드를 호출해야 하는 경우엔 캐스트해서 호출해야 한다.

Unit 타입 : 코틀린의 void

코틀린의 Unit은 자바의 void와 매우 흡사하다. 다만 Unit은 타입 인자로 사용할 수 있다. Unit도 값이 있다. 단 하나 Unit에 속하는 값은 이름도 Unit이다. 엄밀히 말하면 Unit은 반환값이 없는 것이 아니다. 기본 반환값으로 이해하면 좋다.
코틀린에서 인터페이스에 반한값을 적지 않으면 묵시적으로 Unit을 반환하는 것을 의미하며 return Unit을 하지 않아도 컴파일러가 알아서 넣어준다.

Nothing 타입 : 이 함수는 결코 정상적으로 끝나지 않는다.

예외를 던져서 항상 제대로 값을 돌려주지 않는 함수에 Nothing을 사용한다.

1
2
3
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}

Nothing의 진가는 엘비스 연산자의 우항으로 쓰일 때 진면목을 보인다.

1
2
val address = Company(null).address ?: fail("no address")
println(address.length)

null 인 경우 Nothing인 메서드가 실행된다면, 이후 address에는 반드시 null이 아니라는 것을 확신할 수 있어서 널 가능성이 없는 타입으로 취급하고 사용할 수 있다.

컬렉션과 배열

읽기 전용과 변경 가능한 컬렉션

코틀린 컬렉션은 자바 컬렉션과는 다르게 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션안의 데이터를 변경하는 인터페이스를 분리했다는 점이 다르다.
kotlin.collections.Collection 은 이터레이션, 사이즈 조회, 값 유무 검사, 데이터 조회 등을 지원하지만 컬렉션에 추가 및 제거는 지원하지 않는다. 변경 가능한 컬렉션은 이를 확장한 kotlin.collections.MutableCollection 인터페이스를 활용한다.

하지만 컬렉션 인터페이스를 활용할 때 주의점은 읽기 전용 인터페이스로 컬렉션을 활용하다고 그 컬렉션 객체가 실제로 불변 객체는 아니라는 점이다. 즉 다른 변경 가능한 컬렉션 인터페이스를 참조 변수로 접근하면 충분히 해당 객체에 변경이 생길 수 있고 이는 읽기 전용 컬렉션이 스레드 안전하지는 않음을 의미한다.

코틀린 컬렉션과 자바

코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스 두가지 표현을 제공한다.
코틀린은 코틀린 컬렉션 인터페이스가 마치 자바 컬렉션 클래스의 상위 타입인 것처럼 취급한다. 이를 통해 자바 호환성을 제공한다.

컬렉션 생성 함수와 자바 객체
일반적으로 listOf는 읽기 전용을, mutableListOf는 변경 가능한 객체를 반환하는 식으로 작동한다. 이때 setOfmapOf는 자바 표준 라이브러리에 속한 클래스의 인스턴스를 반환한다. 즉 내부는 변경 가능한 컬렉션 객체인 셈이지만 절대 그 사실에 의존하지 않도록 하자

지비 메서드에서 Collection을 매개변수로 코틀린 컬렉션 받을 때 주의 사항

코틀린에서 읽기 전용과 변경 가능한 컬렉션을 구분해도 자바 메서드에서 Collection을 매개변수로 받을 때는 이 구분이 의미가 없어지게 된다. 즉 읽기 전용으로 제한하던 인스턴스를 Collection을 매개변수로 갖는 자바 메서드에 전달하면 해당 메서드에서 변경 가능하게 할 수 있다는 의미다. 그래서 자바에서 올바른 파라미터 타입을 사용하고, 자바에서 코틀린에서 온 컬렉션을 사용해야 하는 경우 제한을 잘 확인하고 사용하자.

컬렉션을 플랫폼 타입으로 다루기

자바에서 코틀린으로 오면 플랫폼 타입으로 온다. 이는 컬렉션도 마찬가지다. 플랫폼 타입인 컬렉션은 기본적으로 읽기 전용과 변경 가능 컬렉션 모두 사용할 수 있도록 한다.

문제는 플랫폼 타입은 그렇듯이 자바의 시그니처를 오버라이딩 할 때가 문제가 된다. 즉 자바 인터페이스의 시그니처에 컬렉션이 있는 경우 null 가능성과 컬렉션의 변경 가능성을 토대로 타입을 구체화해야 한다.

  • 컬렉션이 null 가능한가
  • 컬렉션의 원소가 null 가능한가
  • 오버라이딩할 메서드가 컬렉션을 수정할 수 있는가

객체의 배열과 원시값의 배열

코틀린 배열은 타입 파라미터를 받는 클래스다. 배열의 원소 타입은 타입 파라미터에 의해 정해진다.
문제는 코틀린 배열이 항상 제네릭 타입을 받다보니 자바 코드로 바뀔 때 코틀린에서 원시 타입 배열이어도 자바에서는 래퍼 타입 배열이 되버린다. Array<Int>Intger[]이 되어버린다.

이럴 경우 박싱하지 않은 원시 타입 배열을 의미하는 특별한 클래스를 활용해야 한다. IntArray, ByteArray 등 원시 타입마다 제공해준다.

Share