코틀린 입문 스터디 (17) Types

mook2_y2
16 min readFeb 23, 2019

--

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

목차

(1) Introduction

(2) From Java to Kotlin

(3) Basics

(4) Control Structures

(5) Extensions

(6) 실습 : Mastermind game

(7) Nullability

(8) Functional Programming

(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park

(10) Properties

(11) Object-oriented Programming

(12) Conventions

(13) 실습 : Rationals, Board

(14) Inline functions

(15) Sequences

(16) Lambda with Receiver

(17) Types

(18) 실습 : Game 2048 & Game of Fifteen

1. Basic types (6m)

  • Java에서는 개발자가 integer, boolean 등을 할당하는 변수를 선언할 때 목적에 맞게 primitive type과 reference type 중에서 선택해야했습니다. 하지만 Kotlin에서는 이러한 구분이 존재하지 않으며, Kotlin 컴파일러가 컴파일하는 과정에서 코드에 적절한 것으로 변환합니다. (ex: nullable Int인 Int?java.lang.Integer로 변환, generic argument 로서 Int인 List<Int>List<java.lang.Integer>로 변환, 함수 인자 타입을 Any로 선언하고 정수를 넣는 경우 autoboxing에 의해 일괄 java.lang.Integer 로 변환 등)-> 동일한 종류의 자료형에 대해 primitive type과 reference type이 존재하는 int, boolean, double 등의 경우 둘 중 어떤 것을 선택해야하는지 유스케이스가 명확하므로 이를 컴파일러 레벨에서 처리하여 편의성을 개선
  • Java에서는 개발자가 변수 타입을 선언할 때 nullable/non-null 여부를 의무적으로 선언할 필요가 없었지만, Kotlin에서는 타입을 명시하는 경우 nullability 명시를 강제합니다. (ex: 정수의 경우 Int, Int? 중에서 선택 필요) -> 런타임 NPE 이슈와 연관된 Nullability는 개발자가 직접 명시적으로 구분하도록 하고, nullable 타입에 대해 null operator를 제공하여 안전성을 개선
  • String 타입의 경우 Java의 java.lang.String 에서 String 타입을 인자로 받지만 이를 regular expression으로 사용하여 혼동의 여지가 있었던 replaceAll() 함수를 숨기고, String타입과 Regex타입 각각에 대해 오버로딩한 replace() 함수를 제공합니다.
// Java의 replaceAll() 함수
// 인자로 String 타입을 받지만 regular expression으로 쓰임
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
// Kotlin의 replace() 함수
// 인자의 타입이 String인 경우와 Regex인 경우로 오버로딩됨 @kotlin.internal.InlineOnly
public inline fun CharSequence.replace(regex: Regex, replacement: String): String = regex.replace(this, replacement)
expect fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String
  • primitive type과 reference type의 구분이 있는 Java에서는 java.lang.Object 가 reference type만의 최상위 타입이었으나, 위에 언급한 처럼 primitive/reference type의 구분이 없는 Kotlin에서 Any 는 모든 타입의 최상위 타입으로 동작합니다. 이 역시 컴파일 단계에서 상황에 맞게 적절히 자동 변환됩니다. (아래 코드 예시 참고)
// Koltin 코드 
// nullable Int의 경우 Java의 reference type에 대응되는 것이 명확하지만,
// primitive Type으로 대응되는 Non-nullable Int는 Any에 대한 타입체크시
// 어떻게 동작할까요?
val nullableInt:Int? = 1
println(nullableInt)
println(nullableInt is Any)
val nonNullInt:Int = 1
println(nonNullInt)
println(nonNullInt is Any)
// 디컴파일된 Java 코드
// 아래와 같이 primtive type으로 그대로 변환되며
// Kotlin의 "nonNullInt is Any" 는 상수 true로 변환됩니다.
Integer nullableInt = 1;
System.out.println(nullableInt);
boolean var2 = nullableInt instanceof Object;
System.out.println(var2);
int nonNullInt = 1;
System.out.println(nonNullInt);
boolean var3 = true;
System.out.println(var3);
  • 전반적으로 Kotlin 타입 구조는 편의성을 개선하기 위한 의도로 Java에서 개발자가 굳이 명시할 필요가 없거나 (ex: primitive/reference type 구분), 혼동의 여지가 있던 요소(ex: String의 replaceAll() 함수 생략)는 생략하고, 안전성을 개선하기 위한 의도로 필요한 요소 (ex: Nullable/Non-nullable 타입 구분 추가)를 새롭게 추가한 것으로 보입니다.

2. Kotlin type hierarchy (11m)

  • 함수의 반환값이 없는 경우에 대해 Java에서는 Void 타입 1개로만 명시가 가능했습니다. 하지만, 실질적인 프로그래밍 관점에서 생각해보면 함수의 반환값이 없는 경우는 1) 함수 자체는 정상적으로 완료 되었으나 반환할만한 의미있는 정보가 없어 반환값이 없는 경우와, 2) 함수 자체가 정상적으로 완료되지 못하여 정보를 반환할 수 없는 경우로 나뉩니다. 이러한 경우를 구분하기 위해 Kotlin에서는UnitNothing 이라는 2개의 타입을 제공합니다.

Unit 타입

  • “No meaningful value is returned from this function”을 의미합니다.
  • Kotlin Unit은 기존 Java에서 Void 타입으로 명시했던 모든 상황에 대응될 수 있습니다.
// 함수 반환 타입 명시 생략 (가장 일반적인 방식)
fun unit1(){
println("unit")
}
// 함수 반환 타입으로 Unit 명시
fun unit2():Unit{
println("unit2")
}
// return Unit 명시
fun unit3():Unit{
println("unit3")
return Unit
}
  • Unit 타입의 경우 return 을 생략할 수 있고, 함수 반환 타입 명시를 생략할 수 있습니다. (expression body의 경우 함수 반환 타입 생략이 가능하지만 block body 방식의 경우 함수 반환 타입 명시는 의무적입니다. 그러나 Unit 타입에 대해서만 생략이 가능합니다.)

Nothing 타입

  • “this function never returns”을 의미합니다.
  • 예시 상황은 오직 예외를 던지는 함수, TODO()로 선언되어 구현이 되지 않은 함수, 무한 루프를 발생시키는 함수 등입니다. 비정상적으로 종료되거나 절대 종료되지 않는 경우를 의미합니다.
fun fail(message:String): Nothing{
throw IllegalStateException(message)
}
  • 함수가 반환값을 가지지 않을 수 도 있는 Java와 달리 Kotlin은 함수형 프로그래밍의 영향을 받아 모든 함수가 반환값을 가지는 구조입니다. (관련 링크 : 일급 함수란?) 그리고 Unit 타입은 Int, User (custom class) 등에 대응되는 독립적인 타입이고, Nothing 타입은 모든 타입의 subtype으로 정의됩니다.
// fail 함수의 반환값을 Unit으로 명시하는 경우 
// answer 변수는 Int 또는 Unit이 할당될 수 있어
// 독립적인 타입들을 포괄하기 위해 Any 타입으로 명시되야 합니다.
// 그러나 이는 answer을 효율적으로 조작하기에 방해가 되며,
// answer은 오직 Int 타입만 저장되고
// Int 타입이 저장되지 않는 경우는 예외 발생으로 비정상 상태인 것이므로
// 의미적으로도 부적합합니다.
val answer: Any = if (timeHasPassed()) {
42
} else {
fail("No answer yet")
}
}

fun fail(message:String): Unit{
throw IllegalStateException(message)
}
============================// fail 함수의 반환값을 Nothing으로 명시하는 경우
// Nothing은 모든 타입의 subtype이기 때문에
// answer를 Int 타입으로 명시할 수 있습니다.
val answer: Int = if (timeHasPassed()) {
42
} else {
fail("No answer yet")
}
}

fun fail(message:String): Nothing{
throw IllegalStateException(message)
}
  • 이러한 설계 구조로 인한 코드상의 이점은 위 코드 사례를 참고 부탁드립니다.

Type Hierarchy

  • Any 타입은 모든 타입의 super type (top type) 입니다.
  • Nothing 타입은 모든 타입의 subtype (bottom type) 입니다.
  • 한편 모든 타입에 대해 nullable 타입 (A?)은 non-null 타입(B?)의 super type 입니다. 이에 따라 Kotlin의 최상위 super type은 Any? 입니다.

3. Nullable Types (12m)

  • 앞서 언급한 것 처럼 Java 타입 구조는 nullable과 non-null을 구분하지 않지만, Kotlin은 안전성 개선을 위해 nullable과 non-null 타입을 구분하였습니다. 한편 Kotlin은 Java와의 상호운용성을 지원하는 언어입니다. 이에 따라 (nullable/non-null 구분이 없는) Java의 함수를 (nullable/non-null 구분이 있고 이에 따라 처리 방식이 다른) Kotlin에서 호출하는 경우 해당 함수의 타입을 무엇으로 처리해야할지에 대한 이슈가 발생합니다.
  • Kotlin의 nullable / non-nullable 구분은 바이트코드 레벨에서는 어노테이션을 이용하는 방식으로 구현되므로 Java 코드에서도 @Nullable , @NotNull 어노테이션을 선언해줄 경우 Nullability 여부가 명시되어 문제가 없습니다. 하지만 명시가 없는 경우가 대부분이며 이에 대한 가능한 대응 방안은 크게 1) 안전성을 위해 일괄 nullable 타입으로 해석하고 null operator 사용을 강제한다, 2) Nullability 여부를 알 수 없음이라는 정의를 가지는 제3의 타입을 사용한다. 로 2가지인데 Kotlin은 2) 방식을 선택하였습니다.

Platform Type

  • “type that came from Java type of unkown nullability”를 의미합니다.
  • 앞서 언급한 것처럼 안전성 측면에서는 일괄 nullable type으로 처리하여 null operation 사용을 강제하는 것이 런타임 NullPointerException을 막을 수 있어 안전할 수 있고, Kotlin 개발팀도 처음에는 이러한 접근법을 취하고자 했으나 이 경우 사실 null을 반환할 가능성이 없는 함수임에도 무조건 null operation 사용이 강제되어 코드 가독성이 떨어지는 문제와 추후 설명할 Generic 타입 이슈 (unknown mutability) 처리를 위해서도 제 3의 타입 정의가 필요하여 platfrom type을 도입하였습니다.
  • Nullable type을 A? 로 선언하듯이, platform type은 A! 로 선언됩니다. 하지만 이는 syntax가 아니라 notation으로 실제 코딩시에는 사용되지 않고 에러, 경고 메시지 등에만 노출됩니다.

Platform Type에 대한 Nullability 처리

  • Java의 함수를 Kotlin 코드에서 호출하여 반환값을 할당할 때 발생하는 platform type은 nullability 관련하여 기존 Java 코드와 동일하게 동작합니다. null operation을 사용해도 무방하지만 사용하지 않더라도 경고나 컴파일시에 오류 메시지가 노출되지 않습니다. 이에 대해 nullability 이슈를 대응 하는 방법은 크게 아래 2가지와 같습니다.
  • 1) Java 코드에 @Nullable , @NotNull 어노테이션을 붙여 platform type이 아니게 되도록 변경하는 방법입니다. 어노테이션을 붙일 경우 Kotlin 컴파일러가 Nullable/Not-null을 분류할 수 있게 됩니다. 이를 위해서는 라이브러리 설치가 필요합니다. (관련 링크 : Anootations — IntelliJ IDEA) 한편 각 메소드 별로 어노테이션을 붙이는 것이 비효율적인 경우 커스텀 어노테이션을 사용하여 패키지 또는 클래스 단위로 디폴트 어노테이션을 @NotNull 로 선언해두고, null이 발생할 가능성이 있는 메소드에만 @Nullable 을 붙이는 식으로 처리할 수 도 있습니다. Nullability 여부를 명시하는 것이 보다 안전한 코드를 만드는 방향이므로 이 방식을 지향하는 것이 좋습니다.
  • 2) 하지만 외부 라이브러리여서 코드 추가가 불가능하거나, 그 외 어떠한 이유로 Java 코드에 추가하는 것을 원하지 않을 때는 Kotlin에서 Java 함수를 호출하여 반환 값을 저장하는 Kotlin 코드상의 변수에 어떠한 nullability 타입이여야 하는지를 고려하여 명시적인 nullability 타입을 선언해주는 방법이 있습니다. 이 경우 Java 함수를 호출하는 곳마다 선언해줘야 하는 문제는 있지만 할당이 된 후부터는 nullability에 대한 안전성이 보장됩니다. 또한 nullability 타입을 잘못 선언한 경우 (실제로는 null이 반환되는데 not-null 타입으로 선언한 경우) 에는 런타임시 NullPointerException이 아니라, Kotlin 컴파일러가 null이 할당되는지 여부를 체크하여 컴파일 단계에서 IllegalStateException을 발생시켜줍니다.

Intrinsic checks

  • Nullability 타입이 구분된 Kotlin 코드를 Java에서 호출하여 사용하는 경우 모든 변수 할당 지점에 Intrinsics.checkExpressionValueIsNotNull 이 호출되어 변수의 null 여부를 확인한 뒤에 로직을 수행합니다.
  • 한편, 퍼포먼스 개선 목적 또는 해당 Kotlin 프로젝트를 Java 코드에서 호출할 가능성이 없는 경우 컴파일러 옵션에서 이 기능을 제외시킬 수 있습니다.

4. Collection types (5m)

  • 앞서 안전성을 목표로 Java에는 없었으나 Kotlin에서는 nullability 타입 구분이 도입되었습니다. 마찬가지로 collection을 다루는 과정에서 안전성과 코드의 의도를 효과적으로 표현하기 위해 collection types에 read-only/mutable type 구분이 도입되었습니다.
  • Mutable type은 add(), remove(), clear()함수 등 mutating 함수 기능이 제공되며, read-only type은 제공되지 않습니다. Java는 모든 Collection에 대해 mutating 함수가 제공됩니다. Kotlin은 이러한 collection type 구분을 통해 mutating이 발생될 의도가 없는 변수에 대해 안전성을 보장할 수 있습니다.
  • 한편, read-only가 immutable을 보장하지는 않습니다. read-only는 해당 변수에 대해 mutating 함수를 사용할 수 없음이 보장되지만, 동일한 collection을 mutable 타입으로 선언된 다른 변수에 할당하여 수정을 하면 변경될 수 있습니다.
// 동일한 리스트를 mutable 타입인 mutableList와
// read-only 타입인 list에 할당한 경우입니다.
val mutableList = mutableListOf(1, 2, 3)
val list: List<Int> = mutableList
// 이 경우 list에 대해서는 mutating 함수를 사용할 수 없으며,
// 아래 코드는 컴파일 에러가 발생합니다.
list.add(4)
// 하지만 동일한 객체를 할당한 mutableList는 mutating 함수 사용이 가능하며
// 이 경우 아래 println 호출시 [1,2,3,4]가 호출되어
// list의 immutable이 보장되지는 않습니다.
mutableList.add(4)
println(list) // [1,2,3,4] 출력
  • 한편, 앞서 java에서 구분하지 않았지만 Kotlin에서 nullability type을 구분함으로 인해 java 메소드를 Kotlin 코드에서 사용하는 경우 unknown nullability 이슈가 발생했습니다. 마찬가지로 read-only/mutable type 역시 java에서는 구분하지 않았지만 Kotlin에서 구분이 도입되어 java의 collection 반환 메소드를 Kotlin 코드에서 사용하는 경우 unknown mutability 이슈가 발생합니다. 이에 따라 collection type에 대해서는 platform type이 unknown nullability와 unknown mutability의 2가지 의미를 가집니다. 한편 원래 java의 collection은 mutable하여 Kotlin에서 read-only/mutable 중 어떤쪽으로 처리해도 동작 가능하므로 큰 문제는 없고 로직에 맞게 적절히 선언하면 됩니다.

--

--