코틀린 타입 시스템 Type System
?
Java
와 Kotlin
의 큰 차이점 중 하나는 변수 타입을 널이 될 수 있는 타입 과 널이 될 수 없는 타입 으로 나눌 수 있다는 점이다. 그리고, 컬렉션 Collection
을 다룰 때에도 읽기 전용 컬렉션 과 변경 가능 컬렉션 으로 나눠진다.
이 두 가지 특성은 Kotlin
프로그래밍에 안정성과 가독성을 향상시키는 데 도움이 될 것 같다.
이번 6장에서는 아래 항목을 소개하고 있다.
- 널이 될 수 있는
Nullability
타입과 널 처리 방식 - 코틀린 원시 타입
- 코틀린 컬렉션
널이 될 수 있는 Nullability
타입과 널 처리 방식
Java
프로그래밍을 하면 범하기 쉬운 실수 중 하나는 분명 NullPointerException (NPE)
처리일 것이다. 매번 if
절을 통해 null
을 확인하는 개발 로직은 피로감을 높아질 수 밖에 없다. Kotlin
에서는 이런 NPE
를 최대한 개발자가 자연스럽게 처리할 수 있도록 도와주고 있다.
널이 될 수 있는 타입
Type?
: 타입 뒤에?
를 붙이면, 해당 변수의 타입은 널이 될 수 있는 타입 이 된다.Type?
=Type
ornull
s
변수의 타입이 그냥 String
으로만 되어있고, null
이 전달된다면, s.length
실행 시 NPE
에러가 발생할 것이다. 하지만 Kotlin
에서는 널이 될 수 있는 타입 을 통해서 안전하게 변수의 확장함수를 호출하여 NPE
를 피할 수가 있게 된다.
널이 될 수 있는 타입 이 무조건
null
를 모두 포함하기 때문에 항상 널이 될 수 있는 타입 으로 선언하는 것은 좋은 방법이 아니다. 최대한 널이 될 수 없는 타입 으로 선언한 후에 필요시에만 변경하여 사용하는 것을 권장하고 있다.
안전한 호출 연산자 : ?.
안전한 호출 연산자 는 함수 호출 전에 null
검사와 함수 호출을 한 번의 연산으로 수행하는 연산자이다.
엘비스 연산자: ?:
엘비스 연산자 는 null
대신 사용할 Default
값을 지정할 때 편리하게 사용할 수 있는 연산자이다.
Kotlin
에서는 return
이나 throw
등의 연산도 식이기 때문에, 엘비스 연산자 의 우항에 return
과 throw
등의 연산을 넣을 수 있다.
안전한 캐스트 : as?
안전한 캐스트 는 대상 타입으로 변환할 수 없다면 null
을 반환한다.
- 타입이 일치하지 않으면,
false
반환된다. - 안전한 캐스트를 하고 나면 스마트 캐스트 처리된다.
널 아님 단언 : !!
널 아님 단언 은 !!
처리를 통해 널이 될 수 있는 타입 의 변수이더라도 널이 될 수 없는 타입 으로 변환한다. 하지만 null
에 대하여 !!
를 적용하면 NPE
가 발생한다.
!!
의 사용 유의점으로, 혹시라도 예외가 발생할 경우 스택 트레이스stack trace
에는 어떤 파일의 몇 번째 줄인지에 대한 정보가 들어있을 뿐이다. 그래서 한 줄에 !!
를 중첩되기 사용하면, 디버깅이 힘들어질 수 있다. (e.g person.company!!.address!!.country)
!!
기호로 정의한 이유는 마치 컴파일러에게 소리 지르는 것 같은 느낌을 주기 위해 정해진 표기법이라고 한다.
let
함수
let
함수는 Kotlin
에서 null
이 될 수 있는 식을 더 쉽게 다룰 수 있는 방법이다.
나중에 초기화할 프로퍼티 : lateinit
Kotlin
에서는 클래스 안의 널이 될 수 없는 타입 의 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화할 수 없다. Kotlin
에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 하고, 널이 될 수 없는 타입 의 프로퍼티는 널이 아닌 값으로 반드시 초기화해야 한다.
위 코드처럼 구현하고, myService
프로퍼티를 여러 번 사용해야한다면 어떨까? 이를 해결하기 위해 lateinit
를 사용한다.
- 나중에 초기화하는 프로퍼티 는 항상
var
여야 한다. val
프로퍼티는final
필드이기 때문에, 생성자 안에서 반드시 초기화해야하기 때문이다.
널이 될 수 있는 타입의 확장
널이 될 수 있는 타입 의 확장 함수를 정의하면 null
를 다루는 강력한 도구로 활용할 수 있다.
예제로, String?
의 타입의 isNullOrEmpty
이나 isNullOrBlank
함수들은 수신 객체에 대해 null
검사를 먼저하고, 다음 처리를 진행한다.
타입 파라미터의 널 가능성
Kotlin
에서는 기본적으로 함수의 파라미터의 타입은 널이 될 수 있는 타입 으로 정의된다. ?
가 없더라도 함수가 받는 파라미터의 타입은 널이 될 수 있는 타입 이기 때문에 함수 안에서는 해당 파라미터 정보를 활용할 때 안전한 함수 호출 ?.
를 활용하는 것을 추천한다.
널 가능성
Nullability
과Java
플랫폼 타입
- 플랫폼 타입 은
Kotlin
에서null
관련 정보를 알 수 없는 타입을 뜻한다. 그 타입은 널이 될 수 있는 타입 으로 처리해도 되고, 널이 될 수 없는 타입 으로 처리해도 무방한다.Kotlin
은Java
에서 전달받은 반환값에 대한 타입은 플랫폼 타입 으로 전달 된다.- 따라서,
Java
와 함께 사용할 때는 항상 널이 될 수 있는 타입 으로 간주하여 개발을 해야할 것 같다.
코틀린의 원시 타입
Kotlin
에는 int
, boolean
과 같은 Java
의 원시 타입 primitive type
과는 다른 형태의 원시 타입을 지원한다.
원시 타입 : Int, Boolean 등
Kotlin
에서는 원시 타입과 참조 타입을 따로 구분하지 않는다. 이 방식은 개발자들에게 편리함을 많이 가져다줄 수 있다.
Java
에서 Collection
에 원시 타입의 정수값을 저장하고자 한다면, Collection<int>
가 아니라 Collection<Integer>
로 선언했어야 했다. 하지만 Kotlin
에서는 구분하지 않기 때문에 Collection<Int>
로 선언이 가능하다.
그렇다고 해서, Kotlin
에서는 원시 타입도 참조 타입처럼 항상 객체로 저장되는 것은 아니다. 대부분의 경우에는 원시 타입은 Java
의 원시 타입으로 변환되어 컴파일된다. 하지만 참조 타입이 필요한 경우 (eg. Collection
의 타입 선언) Kotlin
의 원시 타입은 참조 타입으로 컴파일된다.
널이 될 수 있는 원시 타입
Koltin
의 원시 타입은 객체처럼 취급되기 때문에 null
이 될 수 도 있다.
숫자 변환
Kotlin
의 숫자 변환은 직접 변환은 어렵지만, 다양한 확장 함수를 통하여 변환이 가능하다.
Any, Any? : 최상위 타입
Any
타입은 Java
의 Object
클래스의 최상위 타입처럼 널이 될 수 없는 타입 의 최상위 조상 타입이다.
Any?
타입은 널이 될 수 있는 타입 의 최상위 타입이 된다.
Unit 타입 : 코틀린 void
Unit
타입은 Java
의 void
같은 기능을 한다. 하지만 Kotlin
의 Unit
타입은 보다 강력한 역할을 할 수 있다.
Nothing 타입 : 이 함수는 결코 정상적으로 끝나지 않았다.
Nothing
타입은 함수를 호출하는 코드를 분석하는 경우 함수가 정상적으로 끝나지 않는다는 사실을 알기 위해 사용하면 유용하다. 따라서 Nothing
타입은 함수의 반환 타입 또는 반환 타입으로 쓰일 타입 파라미터로만 사용 가능하다.
컬렉션과 배열
Kotlin
컬렉션은 Java
의 컬렉션을 활용하지만 보다 다양하고 강력한 확장 함수를 제공하고 있다. 그리고 null
처리에 용이한 컬렉션 타입과 읽기 전용과 변경 가능한 컬렉션 타입 구분에 대해 알아보고, Java
와 어떻게 상호 운용이 가능한지 살펴보고자 한다.
널 가능성과 컬렉션
컬렉션의 널 처리 확장 함수
filterNotNull
함수 :filter
함수의 확장 함수로 수신 객체의null
여부를 감사하고 필터 처리하여 반환한다.
읽기 전용과 변경 가능한 컬렉션
읽기 전용 컬렉션 Collection
Kotlin
의 제일 기조적인 Collection
인터페이스 (eg. List
, MAP
) 를 사용하면 읽기만 가능한 읽기 전용 컬렉션 이다.
변경 가능한 컬렉션 MutableCollection
기본적인 Collection
인터페이스에 Mutable
(변할 수 있는
)이란 키워드를 붙여주면, 변경 가능한 컬렉션 이 된다.
Collection
을 선언할 때는 먼저 읽기 전용 컬렉션 으로 선언하고, 필요시에만 변경 가능한 컬렉션 으로 변환하는 것을 추천한다.
코틀린 컬렉션과 자바
Kotlin
의 Collection
은 모두 Java
의 Collection
이다. 이는 Java
에게 Kotlin
의 변경 가능한 컬렉션 을 전달하더라도 변환이 필요없이 동작 가능하다. 단지 Kotlin
에서는 Java
의 Collection
을 2가지 표현으로 제공할 뿐이다.
코틀린 Collection
의 계층 구조
위 그림 처럼 Kotlin
은 Java
와의 호환성을 제공하면서, 읽기 전용 인터페이스와 변경 가능 인터페이스를 분리하여 제공한다.
읽기 전용 컬렉션 은 곧 불변 컬렉션 으로 변경될 예정이라고 한다.
컬렉션을 플랫폼 타입으로 다루기
Java
에서 Collection
을 Kotlin
으로 넘길 때는 다른 타입들처럼 플랫폼 타입 으로 전달된다. 그래서 해당 객체는 읽기 전용 상태와 변경 가능한 상태 모두 해당되기 때문에 유의해서 다룰 필요가 있다.
- 컬렉션이 널이 될 수 있는가?
- 컬렉션의 원소가 널이 될 수 있는가?
- 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?
객체의 배열과 원시 타입의 배열
Java
원시 타입 배열의 형태는 int[]
, char[]
와 같은 형태이지만, Kotlin
은 Collection
과 비슷한 Array<T>
형태를 가지고 있다.
코틀린 원시 타입 배열 형태
- 기본 :
Array<T>
- 별도 클래스 :
IntArray
,BooleanArray
,CharArray
등- 위와 같은 클래스 선언하여 배열 생성할 경우, 람다를 통해서 초기화 가능
하지만 위와 같은 배열 형태일지라도,
Java
로 변환되면int[]
,char[]
로 컴파일된다.