Kotlin

[Kotlin] 흥미돋는 코틀린의 타입을 정리해보자

노소래 2023. 1. 5. 01:43

오늘은 Kotlin in Action 을 읽고나서, 코틀린의 타입 시스템에 대해서 정리해보려고 한다.

타입은 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정하는 것이다.

 

코틀린으로 코딩하는데 문제가 없고 편하다고 생각했던 사람에게도 큰 도움이 되는가 물을 수 있겠지만

나는 코틀린을 쓰면서 단순히 편하다고 느꼈던 것들에 대해, 코틀린 컴파일러 개발자가 왜 이렇게 만들었는지 말해주는 설계 의도를 보며 굉장한 재미를 느끼고있다.

그래서 단순한 사용 방법보다는 키워드와 클 틀로 정리해보려고한다.

(틀린 부분이나 이견이 있다면 댓글 남겨주시면 매우 감사합니다ㅎㅎ)

 

코틀린의 원시타입

코틀린에서는 원시타입과 래퍼타입을 구분하지 않는다!

그래서 개발자는 항상 같은 타입을 사용하게 된다.

자바를 떠올려보면 long, int, double, float, boolean ... 같은 원시타입(primitive type)과 참조타입(reference type)이 있다는 것을 기억할 것이다. 그리고 래퍼타입 이라 불리는 것이 있다.

원시타입은 변수에 그 값이 직접 들어가는 타입이고

참조타입은 변수에 메모리상의 객체 위치(주소)가 들어가는 타입이다.

래퍼타입은 Collection 의 타입 파라미터처럼 원시타입에 대해 참조타입이 필요할 때는 원시타입 값을 감싸서 사용하는데 이를 래퍼타입이라고 한다. (참고로 원시타입 -> 래퍼클래스 변화를 Boxing 이라고 하고 그 반대를 UnBoxing 이라고 한다.) 예를 들면 int 를 Collection 원소로 담을 때는 Integer 로 담는 경우를 말한다. (JVM 은 타입 인자로 원시 타입을 허용하지 않기 때문)

코틀린에서는 원시타입과 래퍼타입을 구분하지 않는다는 말의 예시는 일반적으로 쓰든 컬렉션 원소로 쓰든 Int 타입으로 사용한다는 것이다. 

원시타입조차 항상 객체로 표현하면 비효율적이지 않을까 고민했는데 책에 답이 있었다.

컴파일시에는 대부분의 경우 자바 int 로 컴바일되고 제네릭 클래스를 사용하는 경우에만 자바의 int 래퍼 클래스 Integer 객체가 들어간다고 한다. Int? 도 int 로 컴파일될까?  아니다. 자바 원시 타입은 null 이 될 수 없으므로 자바의 래퍼 타입으로 컴파일 된다.

자바 원시 타입에 해당하는 코틀린의 타입

  • 정수 타입 : Byte, Short, Int, Long
  • 부동소수점 수 타입: Float, Double
  • 문자 타입: Char
  • 불리언 타입: Boolean

코틀린 개발자 입장에서는 자바 원시타입에 대해 유용한 메서드(ex. .to~ 류의 타입 변환 같은 것)를 사용할 수 있고 항상 같은 타입을 사용할 수 잇어서 유용한 한편,실제로는 대부분 원시타입으로 컴파일하여 효율을 챙기기때문에 안심이되었다.

배열이 없다!

하지만 컴파일이 배열로 되는 것은 있다. 

바로 Array 이다.

얼핏보면 Array 클래스는 일반 제네릭 클래스로 보인다.

Array 는 타입 파라미터가 있고, 컬렉션에 사용할 수 잇는 모든 확장 함수를 제공해주기 때문이다.

기본적으로는 배열보다 컬렉션을 더 먼저 사용해야하지만, 여러 자바 API 가 여전히 배열을 사용하므로 배열을 써야하는 경우가 생겨서 사용하게된다.

나같은 경우는 조금이라도 성능을 높이기 위해 알고리즘 풀이에 자주 사용했던 것 같다.

아래는 Array 클래스와 관련해서 기억할만한 정보이다.

  • 컬렉션을 Array 로 변환하고 싶다면 toTypedArray 를 사용
  • Array 객체 앞에 스프레드 연산자(*) 을 붙이면  vararg 인자로 넘길수 있다.
  • 박싱하지 않은 원시타입(박싱한 원시타입의 예는 Int, Boolena 등)에 대한 배열이 필요하다면 IntArray, BooleanArray 등의 원시타입배열을 사용한다. 이들은 int[], boolean[] 등으로 컴파일 되는것이다. 그래서 박싱하지 않고 가장 효율적인 방식으로 저장되는 것이다!

코틀린은 'Nullable 타입'  지원한다!

코틀린은 'Null 이 될 수 있는 타입' 을 지원한다!

그래서 NullPointerException 오류를 컴파일 시점에서 알 수 있다!

코틀린을 처음 소개받을 때부터 귀에 피딱지 나게 듣는 말인데도 가장 재밌게 읽은 부분이다. 이유는 너무 편하고 이제는 당연하게 사용하고 있는 것에 대한 얼마나 깊게 고민했을지 감탄했기 때문이다.

문법적으로는 타입 뒤에 ? 을 붙이면된다.

Int 혹은 null 이 될 수 있는 타입을 Int? 로 표현할 수 있는 것이다.

즉, 어떤 타입이든 타입 이름 뒤에 물을표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있는 것!

하지만 반대로 물음표가 없으면 절대 null 을 저장할 수 없다는 것이다.

그럼 왜 이렇게 구분하는 것일까?

그것은 자바의 타입 시스템은 Null 으르 제대로 다루지 못했기 때문이다. 

자바로 개발할 때 null 이 아닐 거라는 확신에 if (변수 != null) 과 같은 검사를 해주지 않았다가 실수로 틀리면 NullPointerException 을 런타임에 마주하게된다.

코틀린은 Null 이 될 수 있는 타입과 그렇지 않은 타입을 나눔으로써 컴파일 시점에 NPE 를 방지한다.

 

추가로 아래는 간결하게 nullable 타입을 다루는 도구들이다.

안전한 호출 연산자  ?.

엘비스 연산자 ?:

널 아님 단언 !!

null 관련한 기능을 제공함으로써 널이 될 수 잇는 값을 안전하게 다루게 도와준다.

안전한 호출 연산자  ?.

val str: String? 이란 변수가 있다고 가정해보자

toUpperCase 을 호출할 때 str?.toUpperCase로 호출하면 

호출하려는 값이 null 이면 이 호출은 무시되고 null 이 결과값이 된다.

null 이 아닐 때만 호출하는 것은 간결하게 null 검사를 한 호출을 가능하게 한다.

엘비스 연산자 ?:

null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수있는 이항 연산자이다.

(언제들어도 어이없는 이름의 유래는 연산자가 가수 엘비스 머리 눈모양이라서...ㅋ)

val str: String? = null 이란  예시로 생각했을 때

str ?: "디폴트 값" 으로 사용할 수 있다는 말이다!

좌항이 널이면 우항을 좌항이 널이 아니면 좌항을 결과로한다.

코틀린에서는 return 이나 throw 등의 연산도 식이므로

str ?: return 이나 str ?: throw ~Exception 으로 함수의 전제 조건을 검사하는 경우에 유용하다.

그리고 ?. 나 as? 와 연쇄하여 사용할 때도 간결하게 호출이나 캐스트에 실패한 경우에 대한 코드를 작성할 수 있다.

널 아님 단언 !! (not-null assertion)

때때로 코틀린의 널 처리 지원을 활용하는 대신 직접 컴파일러에게 어떤 값이 널이 아니라는 사실을 알려주고 싶을 경우 사용한다.

내가 !! 을 사용한 변수가 알고보니 null 이면 당연히 런타임에 NPE 다..

그래서 웬만하면 쓰지 않고 다른 방법을 찾아보기를 권장하므로 진짜 널이 아닌 경우임이 확실할 때 사용하는 것이 좋을 것 같다.

타입 검사 is 와 안전한 캐스트 as?

as? 는 해당 타입으로 변환할 수 있으면 null 을 반환하고 그렇지 않으면 null 을 반환한다.

자바의 타입 캐스트와 마찬가지로 대상 값을 as로 지정한  타입으로 바꿀 수 없으면 ClassCastException 이 발생한다.

if 문과 is (자바의 instanceof, !을 앞에 붙여 !is 로 사용할 수도 있다.) 연산자로 타입을 검사해볼 수도 있지만 역시 코틀린은 안전하면서 간결한 언어를 지향하므로 as? 연산자를 제공하는 것이다.

null 을 반환하니 이 역시 엘비스 연산자와 연쇄하여 사용하기 좋다.

자바와의 상호운용에서 발생하는 플랫폼 타입

플랫폼 타입이란 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.

자바와 상호운용시 자바 API 에 @Nullable (코틀린에선 ? 붙은 타입에 해당) 또는  @NonNull (코틀린 ? 안 붙은 타입에 해당) 이 붙지 않은 (원시타입이 아닌) 타입이 플랫폼 타입이 된다.

이 경우 그 타입을 널이 될 수 있는 타입으로 처리해도 되고 널이 될 수 없는 타입으로 처리해도 된다.

이 때문에 주의할 것이 생긴다.

자바 API 를 사용할 때 Null 가능성 여부 애노테이션이 없다면 nullable 타입으로 생각할지 말지를 매번 염두해야한다는 것이다.

왜냐하면 자바에서 가져온 이 변수가 널이 아님을 확신할 수 없다면 예외 방지를 위해 추가검사를 해야할 것이기 때문이다.

따라서 자바 API 를 다룰 때는 문서를 잘 읽어봐야 한다.

 

한편, 코틀린에서는 플랫폼 타입을 선언할 수 없는데, IDE 나 컴파일러 오류 메시지에서는 타입뒤에 ! 가 붙은 플랫폼 타입을 볼 수 있다.

나는 처음에 오류메시지에 String! 이런식으로 나오면 non-null assertion 과 관련있는 것인가 하는 오해를 했던 기억이 난다..ㅎㅎ

Any

Any 는 최상위 타입이다.

즉, 코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다.

Int 와 같은 원시타입 래퍼클래스도 포함해서 말이다!

내부에서 자바의 Object 와 대응한다. 

코틀린의 모든 클래스에는 toString, equals, hashCode 라는 세 메서드가 들어있는데 이 메서드들은 Any 에 정의된 메서드를 상속한 것이다.

Unit

자바의 void 와 같은 기능을 한다.

함수 반환타입이 Unit 인 경우 :반환타입을 생략할 수 있고 return 값을 써줄 필요도 없다.

컴파일러가 묵시적으로 return Unit 을 넣어준다.

자바의 void 와 다른 점은 Unit은 void 와 달리 타입 인자로 쓸 수 있다. 

제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환타입으로 Unit 을 쓸 때 유용하다.

내 경우 단순한 이벤트를 관찰할 때 값이 필요하지 않은 경우 Unit 을 쏴줄 때도 유용했다.

Nothing

Nothing 은 함수가 정상적으로 끝나지 않는다는 경우를 표현하기 위해 사용한다.

Nothing 타입은 아무 값도 포함하지 않아서 함수의 반환 타입이나 

?: throw ~ 이나 ?: return ~ 처럼 ?: Nothing반환하는 함수로 전제조건을 검사할 수 있다.

컴파일러는 Nothing 이 반환 타입인 함수가 결코 정상 종료되지 않음을 알고 코드 분석할 때 사용한다.

 

드는 생각

코틀린 타입 시스템을 보며, Kotlin in Action 초장에 소개했던 코틀린의 철학들이 떠올랐다.

(물론 타입 말고 다른 코틀린 특징도 그렇지만..)

간결성, 안전성, 상호운용성 세 가지있다.

간결하게 널을 안전하게 다루고 자바와 상호운용성까지 챙기는 일을 합리적이게 해낸 컴파일러 개발자가 존경스럽다.

나름 코틀린 잘 사용하고있다고 얼핏 생각했는데 책을 읽을 수록 어떤 철학으로 만들어졌는지, 어떻게 컴파일되는지 알게되는 게 흥미돋는다.

 

 

출처: Kotlin in Action