Post

Kotlin

Kotlin 문법, 개념 정리

Kotlin

공식 문서를 참고하여 작성한 글입니다.

기본 문법

패키지 정의 & import

패키지 정의는 소스 코드 가장 윗줄에 해야하고, 패키지 경로와 실제 디렉터리는 일치하지 않아도 됩니다.

1
2
3
4
package my.demo

import kotlin.text.*
// ~~~

entry point

코틀린 애플리케이션의 엔트리 포인트는 main 함수입니다.

1
2
3
4
5
6
7
8
9
fun main() {
  println("Hello world")
}

// 다음과 같이 String 인자들을 받을 수도 있습니다.

fun main(args: Array<String>) {
  println(args.contentToString())
}

입출력하기

자바와 유사하게 print, println 함수로 출력하고, readln 함수로 입력받을 수 있습니다.

1
2
3
4
5
6
7
8
fun main() {
  println("Enter any word : ")

  val word = readln()

  print("Entered word : ")
  print(word)
}

함수

함수는 다음과 같이 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun sum(a: Int, b: Int): Int {
  // 파라미터로 전달되는 변수는 기본적으로 val입니다.
  return a+b
}

// 다음처럼 작성할 수 도 있습니다.

fun sum(a: Int, b: Int) = a+b

// 리턴 값이 없는 함수는 다음 처럼 작성할 수 있습니다.

fun printSum(a: Int, b: Int): Unit {
  println("sum of $a and $b is ${a+b}")
}

// Unit 리턴 타입은 다음처럼 생략 가능합니다.

fun printSum(a: Int, b: Int) {
  println("sum of $a and $b is ${a+b}")
}

변수

코틀린에서 변수는 val, var 키워드를 이용해 선언할 수 있습니다.

  • val
    • 값을 오직 한번만 할당할 수 있고, 초기화 이후에 변수의 값을 변경할 수 없습니다.
  • var
    • 재할당 가능한 변수를 선언합니다.
1
2
3
4
5
6
7
8
val x: Int = 5
var y: Int = 5
y += 1
// 다음처럼 타입을 명시하지 않을 수 있습니다.
val z = 5
// 다음처럼 초기화 없이 변수를 선언할 수도 있습니다, 타입 명시는 필수적입니다.
val c: Int
c = 3

Class & Instance

class 키워드로 클래스를 생성할 수 있습니다.

클래스의 프로퍼티는 다음과 같이 선언하거나 바디에 작성할 수 있습니다. 클래스 선언에 포함된 파라미터들을 포함한 생성자가 자동으로 생성됩니다.

1
2
3
4
5
6
7
8
class Rectangle(val height: Double, val length: Double) {
  val perimeter: Double = (height + length) * 2
}

fun main() {
  val rectangle = Rectangle(5.0, 2.0)
  println("The perimeter is ${rectangle.perimeter}")
}

클래스간 상속은 :로 선언할 수 있습니다. 클래스는 기본적으로 final 이고, 상속 가능한 클래스를 선언하기 위해서는 open 키워드를 추가해야합니다.

1
2
3
4
5
open class Shape

class Rectangle(val height: Double, val length: Double): Shape() {
  val perimeter: Double = (height + length) * 2
}

반복문

for loop

1
2
3
4
5
6
7
8
9
10
11
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
  println(item)
}

// 다음과 같이 작성할 수도 있습니다.

val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
  println("item at $index is ${items[index]}")
}

while loop

1
2
3
4
5
6
val items = listOf("apple", "banana", "kiwifruit")
var index: Int = 0
while (index < items.size) {
  println("item at $index is ${items[index]}")
  index++
}

Ranges

in 오퍼레이터를 이용해 숫자가 범위 안에 있는지 판단할 수 잇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
val x = 10
val y = 9
if (x in 1..y+1) {
  println("fits in range")
}

val list = listOf("a", "b", "c")
if (-1 !in 0..list.lastIndex) {
  println("-1 is out of range")
}
if (list.size !in list.indices) {
  println("list size is out of valid last indices range")
}

다음처럼 range를 순회할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (x in 1..5) {
  println(x)
}
/*
1
2
3
4
5
*/
// 다음 처럼 스텝을 지정하거나 역순으로도 가능합니다.
for (x in 1..10 step 2) {
  print(x)
}

for (x in 9 downTo 0 step 3) {
  print(x)
}

Collections

기본적 컬렉션 순회는 다음과 같이 작성할 수 있습니다.

1
2
3
for (item in items) {
  println(item)
}

컬렉션이 특정 오브젝트를 가지는지 in 오퍼레이터로 확인 가능합니다.

1
2
3
4
when {
  "orange" in items -> println("juicy")
  "apple" in items -> println("apple is fine too")
}

다음과 같이 람다를 이용해서 filter, map를 처리할 수도 있습니다.

1
2
3
4
5
6
val fruits = listOf("banana", "avocado",  "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") }
  .sortedBy { it }
  .map { it.uppercase() }
  .forEach { println(it) }

Null

nullable 변수는 반드시 ?를 포함해서 null이 가능하다는 것을 표현해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// parseInt 함수는 주어진 str이 int를 포함하지 않으면 null을 리턴합니다.
fun parseInt(str: String): Int? {
  // implementation ~~~
}

fun printProduct(arg1: String, arg2: String) {
  val x = parseInt(arg1)
  val y = parseInt(arg2)
  // parseInt가 null을 리턴할 수 있기에 null check가 필요합니다.
  if (x != null && y != null) {
    println("x * y = ${x * y}")
  } else {
    println("`$arg1` or `$arg2` is not number")
  }
}

Type check & automatic casts

is 오퍼레이터는 인스턴스가 주어진 클래스 타입인지 확인합니다. 만약 불변 로컬 변수 혹은 프로퍼티가 특정 타입으로 확인 된다면, 명시적 타입 할당이 없어도 자동으로 해당 타입으로 할당됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun getStringLength(obj: Any): Int? {
  if (obj is String) {
    // obj는 자동으로 String으로 할당됩니다.
    return obj.length
  }
  return null
}

fun getStringLength(obj: Any): Int? {
  if (obj !is String) {
    return null
  }
  // 이경우에도 자동으로 String으로 할당됩니다.
  return obj.length
}

// 타입 확인 즉시 할당되므로 다음과 같이 작성도 가능합니다.
fun getStringLength(obj: Any): Int? {
  if (obj is String && obj.length > 0) {
    return obj.length
  }
  return null
}

Idiom

코틀린에서 관습적으로 사용하는 표현들 정리입니다.

DTO 생성

1
data class Customer(val name: String, val email: String)

다음과 같은 기능을 포함한 Customer클래스를 생성합니다.

  • getter, setter(프로퍼티가 var인 경우)
  • equals
  • hashCode
  • toString
  • copy
  • component1(), component2()

함수 파라미터 기본값 설정

1
fun foo(a: Int = 0, b: String = "") {...}

List filter

1
2
3
val positives = list.filter { x -> x > 0 }
// 혹은 다음과 같이 선언할 수 있다.
val positives = list.filter { it > 0 }

Collection 내부 엘리멘트 존재 확인

1
2
3
if ("abc@test.com" in emailList) {...}

if ("abc@test.com" !in emailList) {...}

String interpolation

1
println("name $name")

입력 안전하게 읽기

1
2
3
4
5
6
// int로 변환되지 않으면 null이 할당됩니다.
val wrongInt = readln().toIntOrNull()
println(wrongInt)
// int로 변환 가능한 경우 int가 할당됩니다.
val correctInt = readln().toIntOrNull()
pritln(correntInt)

인스턴스 값 확인

1
2
3
4
5
when (x) {
  is Foo -> ...
  is Bar -> ...
  else -> ...
}

읽기 전용 list

1
val list = listOf("a", "b", "c")

읽기 전용 map

1
val map = mapOf("a" to 1, "b" to 2, "c" to 3)

map entry 접근

1
2
println(map["key"])
map["key"] = value

map 내부 탐색하기

1
2
3
for ((k, v) in map) {
  println("$k $v")
}

range를 이용해 순회하기

1
2
3
4
5
for (i in 1..100) {...} // 100 포함
for (i in 1..<100) {...} // 100 미포함
for (i in 2..10 step 2) {...}
for (i in 9 downTo 3 step 3) {...}
(1..10).forEach {...}

if-not-null

1
2
val files = File("Test").listFiles()
println(files?.size) // 만약 files가 null이 아니면 size가 출력된다

if-not-null-else

1
2
3
4
5
6
7
8
9
10
11
val files = File("Test").listFiles()

// 만약 null이면 empty가 출력된다
println(files?.size ?: "empty")

// run 키워드를 사용해서 좀더 복잡한 처리를 할 수도 있다
val filesSize = files?.size ?: run {
  val someSize = getSomeSize()
  someSize * 2
}
println(filesSize)

null이면 실행할 statement 지정

1
2
val values = ...
val email = values["email"] ?: throw IllegalStateException("email is null")

비어있을 수 있는 컬렉션에서 첫번째 값 가져오기

1
2
val emails = ... // 비어있을 수 있다
val mainEmail = emails.firstOrNull() ?: ""

null이 아니라면 특정 코드 실행하기

1
2
3
4
val value = ...
value?.let {
  // value가 null이 아니면 실행된다
}

nullable value 값 매핑하기

1
2
3
val value = ...
val mapped = value?.let { transformValue(it) } ?: defaultValue
// value가 null이면 defaultValue가 리턴되고 아니면 transformValue 값이 리턴된다

when 절에서 값 return 하기

1
2
3
4
5
6
7
8
fun transform(color: String): Int {
    return when (color) {
        "Red" -> 0
        "Green" -> 1
        "Blue" -> 2
        else -> throw IllegalArgumentException("Invalid color param value")
    }
}

try-catch

1
2
3
4
5
6
7
fun test() {
  val result = try {
    count()
  } catch(e: ArithmeticException) {
    throw IllegalStateException(e)
  }
}

if

1
2
3
4
5
6
7
val y = if (x == 1) {
  "one"
} else if (x == 2) {
  "two"
} else {
  "other"
}

single expression functions

아래 두 함수는 동일한 함수 입니다.

1
2
3
4
5
fun theAnswer() = 42

fun theAnswer(): Int {
  return 42
}

다음과 같이 더 짧은 함수를 작성하는데 도움을 줄 수 있습니다.

1
2
3
4
5
6
fun transform(color: String):Int = when (color) {
  "red" -> 0
  "green" -> 1
  "blue" -> 2
  else -> throw IllegalArgumentException("invalid color param")
}

한 오브젝트의 메소드 다수 호출하기-with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Turtle {
  fun penDown()
  fun penUp()
  fun turn(degrees: Double)
  fun forward(pixels: Double)
}

val myTurtle = Turtle()
with(myTurtle) {
  penDown()
  for (i in 1..4) {
    forward(100.0)
    turn(90.0)
  }
  penUp()
}

오브젝트 프로퍼티 정의하기-apply

생성자에 포함되지 않은 프로퍼티를 정의할 때 유용합니다.

1
2
3
4
5
val myRectangle = Rectangle.apply {
  length = 4
  breadth = 5
  color = 0xFAFAFA
}

제네릭 타입 정보가 필요한 제네릭 함수

1
2
3
4
5
6
//  public final class Gson {
//     ...
//     public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException {
//     ...

inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)

변수 스왑하기

1
2
3
4
var a = 1
var b = 2

a = b.also { b = a }

TODO

TODO 함수는 NotImplementedError를 발생시킵니다. fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting")

코드 컨벤션

개념 정리

Types

코틀린에서 사용되는 기본적인 타입들은 다음과 같습니다.

  • Numbers (byte, int, long, float, double)
  • Boolean
  • Characters
  • String
  • Array

추가적으로 다음과 같은 타입도 존재합니다.

  • Any: 자바에서 Object처럼 모든 클래스의 상위 클래스이다.
  • Nothing : 절대 존재하지 않음을 의미, 리턴 타입이 Nothing이면 리턴하지 않는 다는 의미입니다.
  • Unit: 자바에서 void 타입과 일치합니다.

Numbers

자료형 변환

숫자 타입은 암시적으로 더 큰 숫자 타입으로 형변환되지 않습니다.

1
2
3
val b: Byte = 1
// val a: Int = b -> ERROR
val a: Int = b.toInt()

모든 넘버 타입은 다음 함수들을 통해 명시적으로 형 변환을 할 수 있고, 대부분의 경우 컨텍스트에서 내부적으로 변환되기에 사용할 일이 많지는 않다

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()
1
val l = 1L + 3 // Long + Int -> Long

Array

배열은 고정된 수의 값들을 담을 수 있는 자료구조이다. 코틀린에서는 특별한 요구사항이 있지 않는 경우 collections를 사용하는 것이 권장된다.

  • collections은 read-only로 사용할 수 있다.
  • element를 삭제하고 추가하기 더 용이하다.

create array

arrayOf(), arrayOfNulls(), emptyArray() 같은 함수 혹은 배열 생성자를 이용해서 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// [1, 2, 3]
val simpleArray = arrayOf(1, 2, 3)
println(simpleArray.joinToString())
// 1, 2, 3

// [null, null, null]
val nullArray: Array<Int?> = arrayOfNulls(3)
println(nullArray.joinToString())
// null, null, null

var exampleArray = emptyArray<String>()

// 생성자를 이용해 생성하기
val initArray = Array<Int>(3) {0}
println(initArray.joinToString())
// 0, 0, 0

// ["0", "1", "4", "9", "16"]
val asc = Array(5) { i -> (i * i).toString() }
asc.forEach { println(it) }

// 다음과 같이 2차원, 3차원 배열도 생성할 수 있습니다.
val twoDArr = Array(2) { Array<Int>(2) {0} }

val threeDArr = Array(3) { Array(3) { Array<Int>(3) {0} } }

다음과 같은 primitive type array를 사용하면 boxing으로 인한 오버헤드를 줄일 수 있습니다.

Primitive-type arrayEquivalent in Java
BooleanArrayboolean[]
ByteArraybyte[]
CharArraychar[]
DoubleArraydouble[]
FloatArrayfloat[]
IntArrayint[]
LongArraylong[]
ShortArrayshort[]

control flow

Returns and jumps

코틀린은 3가지 jump expression이 있습니다.

  • return : 디폴트로 실행중인 함수에서 리턴하거나, 익명 함수에서 리턴합니다.
  • break : 가장 내부에 있는 loop에서 벗어납니다.
  • continue : 가장 내부에 있는 loop에서 다음 값으로 진행합니다.

Break and continue labels

코틀린에서 label로 마킹할 수 있습니다. label은 다음과 같이 사용가능합니다.

1
2
3
4
5
6
7
8
9
10
11
loop@ for (i in 1..100) {
  // ...
}
// j == 10에서 바깥 loop도 벗어납니다.
loop@ for (i in 1..100) {
  for (j in 1..100) {
    if (j == 10) {
      break @loop
    }
  }
}

Exception

throw IllegalArgumentException() 자바와 동일하게 예외를 던질 수 있습니다.

Precondition functions

Precondition functionUse caseException thrown
require()유저 입력 값 검증IllegalArgumentException
check()오브젝트 혹은 변수 상태 검증IllegalStateException
error()illegal state or conditionIllegalStateException

다음과 같이 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fun getIndices(count: Int): List<Int> {
    require(count >= 0) { "Count must be non-negative. You set count to $count." }
    return List(count) { it + 1 }
}

fun main() {
    // illegalArgumentException 발생
    println(getIndices(-1))
    
}
---------
fun main() {
    var someState: String? = null

    fun getStateValue(): String {

        val state = checkNotNull(someState) { "State must be set beforehand!" }
        check(state.isNotEmpty()) { "State must be non-empty!" }
        return state
    }
    // null 스트링 값이 주어지면 예외 발생
    // getStateValue()

    someState = ""
    
    // 빈 스트링 값이 주어지면 예외 발생 
    // getStateValue()
    someState = "non-empty-state"

    println(getStateValue())
}

Custom exception

Exception 클래스를 상속한 클래스를 만든다.

1
2
3
class CustomException: Exception("My message")
class NumberTooLargeException: ArithmeticException("My message")

Class

1
class Person { /*..*/ }

클래스 선언은 클래스명, 클래스 헤더(파라미터, 기본 생성자) 그리고 클래스 바디로 구성됩니다. 클래스 헤더와 바디는 옵셔널하고, 클래스 바디가 없다면 curly braces는 다음과 같이 생략될 수 있습니다.

1
class Empty

생성자

코틀린에서 생성자는 주 생성자를 하나 가지고, 하나 혹은 그 이상의 보조 생성자를 가진다. 주 생성자는 클래스 헤더에 선언된다.

1
2
3
class Person constructor(firstName: String) { /*..*/ }

// 주 생성자가 어노테이션이나 visibility modifier를 포함하지 않는다면 constructor 키워드는 생략 가능합니다.

오브젝트 생성 과정에서 코드를 실행하고 싶으면, init 키워드를 사용해서 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints $name")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

/*
First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5
*/

주 생성자의 파라미터는 initializer block에서 사용 가능합니다.

1
2
3
class Customer(name: String) {
    val customerKey = name.uppercase()
}

constuctor 키워드를 이용해 보조 생성자를 다음과 같이 선언할 수 있다.

1
2
3
4
5
6
7
class Pet {

  constructor(owner: Person) {
    owner.pets.add(this)
  }

}

만약 클래스에 주 생성자가 존재한다면, 보조 생성자들은 직접 혹은 다른 보조 생성자를 통해서라도 주 생성자를 호출해야합니다.

1
2
3
4
5
6
class Person(val name: String) {
  val children: MutableList<Person> = mutableListOf()
  constructor(name: String, parent: Person): this(name) {
    parent.children.add(this)
  }
}

initializer block는 주 생성자와 함께 실행됩니다. 보조 생성자에 바디를 실행하기 전에 주 생성자를 호출하고, 주 생성자의 실행 이후에 initializer block이 실행되고 보조 생성자의 바디가 실행됩니다.

주 생성자가 없는 경우에도 initializer block이 먼저 실행되고 보조 생성자가 실행됩니다.

아무런 생성자를 명시하지 않으면 기본적으로 empty public 생성자가 생성되고, 이를 원치 않으면 private한 빈 생성자를 이용해 막을 수 있습니다.

1
class DontCreateMe private constructor() { /*...*/ }

abstract class

추상 클래스는 open 키워드가 필요하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
abstract class Polygon {
    abstract fun draw()
}

class Rectangle : Polygon() {
    override fun draw() {
      /*
      ~~~
      */
    }
}

Inheritance

기본적으로 코틀린 클래스들은 final 입니다. 상속을 위해서는 open 키워드를 추가해야합니다.

1
2
3
open class Base(p: Int)

class Derived(p: Int): Base(p)

만약 상속한 클래스가 주 생성자를 가진다면, 상위 클래스 역시 주 생성자를 가져야하고, 상위 클래스 역시 하위 클래스의 주 생성자로 초기화되야합니다.

만약 주 생성자가 없다면, 각 보조 생성자는 직접 super로 상위 클래스의 생성자를 호출하거나, 상위 클래스의 생성자를 호출하는 다른 보조 생성자를 호출해야합니다.

1
2
3
4
5
class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

기본적으로 메소드에 open 키워드가 없으면 재정의 불가이다. 재정의를 할 때는 override 키워드가 필수이며, overrideopen 키워드를 포함하고 있기에 자식 클래스에서의 재정의를 막기 위해서는 final 키워드를 추가해야한다.

property

프로퍼티를 정의하는 전체 문법은 다음과 같습니다.

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

initializer, getter, setter는 옵셔널입니다. initializer 혹은 getter의 리턴 타입으로 부터 타입을 확정 지을 수 있으면, 타입을 명시하지 않아도 됩니다.

1
2
3
var initialized = 1 // getter, setter
// var allByDefault -> ERROR
val simple: Int? // 생성자에서 초기화되야한다, getter

커스텀 getter, setter는 다음과 같이 작성 가능합니다.

1
2
3
4
5
var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }

Backing Fields

다음 코드는 에러를 발생시킨다.

1
2
3
4
5
6
var counter = 0 // the initializer assigns the backing field directly
    set(value) {
        if (value >= 0)
            field = value
            // counter = value // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
    }

코틀린에서 프로퍼티란 필드와 접근자를 합친개념이다. 코틀린에서 프로퍼티에 값을 할당할 때 내부적으로 세터를 호출하기에 주석친 부분을 해제하면 무한 재귀 상황이 발생하게 된다.

이를 해결하기 위해 field라는 식별자를 이용해 값을 할당할 수 있다.

BackingFields는 field 식별자를 커스텀 접근자에서 사용하거나, 하나 이상의 접근자가 기본 생성된 경우 자동으로 생성된다. 다음 같은 경우에는 생성되지 않는다.

1
2
val isEmpty: Boolean
    get() = this.size == 0 // val 프로퍼티이기에 get 접근자만 생성가능한데, 이것을 재정의하였다.

Backing Properties

BackingProperties를 사용하면 외부에 불변 변수를 노출하고 내부적으로는 변경 가능한 값을 사용할 수 있다.

1
2
3
4
5
6
7
8
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

interface

코틀린에서 인터페이스는 추상 메서드를 선언하거나, 메소드 구현을 할 수 있습니다. 추상 클래스와의 차이점은 인터페이스는 상태를 저장할 수 있습니다. 인터페이스는 프로퍼티를 가질 수 있지만, 추상 프로퍼티만을 가지고 접근자 구현만을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface MyInterface {
  fun bar()
  fun foo() {
    // optional body
  }
}

class Child : MyInterface {
  override fun bar() {
    // body
  }
}

----

interface MyInterface {
  val prop: Int // 추상 프로퍼티
  val propertyWithImplementation: String
    get() = "foo"
    fun foo() {
      print(prop)
    }
}

class Child : MyInterface {
  override val prop: Int = 29
}

함수형 인터페이스

오직 하나의 추상 멤버 함수를 가지고 있는 인터페이스를 함수형 인터페이스 혹은 Single Abstract Method (SAM) interface라고 부릅니다. 코틀린에서는 다음과 같이 함수형 인터페이스를 선언할 수 있습니다.

1
2
3
fun interface KRunnable {
  fun invoke()
}

SAM Conversion

SAM Conversion을 이용해서 좀 더 가독성 좋은 코드를 작성할 수 있습니다.

1
2
3
fun interface IntPredicate {
  fun accept(i: Int): Boolean
}

SAM Conversion을 사용하지 않으면 다음처럼 작성해야합니다.

1
2
3
4
5
val isEven = object : IntPredicate {
  override fun accept(i: Int): Boolean {
    return i % 2 == 0
  }
}

SAM Conversion을 이용하면 다음과 같이 작성 가능합니다.

1
2
3
4
5
6
7
8
9
fun interface IntPredicate {
  fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
  println("Is 7 even ? ${isEven.accept(7)}")
}

Visibility modifiers

코틀린에서는 4가지 공개 범위가 있고 기본은 public입니다.

  • private : 선언한 파일 내에서만 사용 가능
  • protected : private + subclass에서 사용 가능
  • internal : 같은 모듈 안에서 사용 가능
  • public : 모든 범위에서 공개

Extensions

코틀린에서는 함수를 상속할 필요 없이 새로운 함수를 추가할 수 있는 extension이라는 기능이 있습니다. extension 함수는 다음과 같이 작성할 수 있습니다. 다음 예시는 MutableList<Int>swap함수를 추가하는 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
fun MutableList<Int>.swap(index1: Int, index2: Int) {
  val tmp = this[index]
  this[index1] = this[index2]
  this[index2] = tmp
}
// 제네릭으로는 다음과 같이 작성가능합니다.
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
  val tmp = this[index1]
  this[index1] = this[index2]
  this[index2] = tmp
}

익스텐션 함수는 static하게 결정되고, 동일한 멤버 함수가 존재하면 멤버 함수가 실행됩니다.

Nullable receiver

extension 함수를 작성할 때, 오브젝트 변수가 null이어도 실행이 되기에 this == null 을 이용해 null 체크를 해주는 것이 좋습니다.

1
2
3
4
5
6
fun Any?.toString(): String {
    if (this == null) return "null"
    // After the null check, 'this' is autocast to a non-nullable type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

Extension properties

extension 함수를 지원하는 것처럼, properties도 extension에 쓸 수 있습니다.

1
2
3
4
5
val <T> List<T>.lastIndex: Int
    get() = size - 1
// 멤버를 클래스에 추가할 수는 없기에 backing field를 가지게 작성할 수는 없습니다.

val House.number = 1 // error: backing field가 생성되기에 에러 발생

Data classes

코틀린에서 데이터 클래스는 주로 데이터를 저장하기 위해서 사용합니다. 데이터 클래스에 대하여 컴파일러는 자동으로 출력 함수, 비교 함수, 등등을 추가해줍니다.

1
data class User(val name: String, val age: Int)

주 생성자에 명시된 파라미터에 대하여 다음 함수들이 생성됩니다.

  • equals(), hashcode()
  • toString()
  • componentN()
  • copy()

생성된 코드들이 유효하기 위해서 데이터 클래스들은 다음 요구사항을 만족해야합니다.

  • 주 생성자는 하나 이상의 파라미터를 가져야한다.
  • 모든 주 생성자는 val 혹은 var이어야한다.
  • 데이터 클래스는 추상, 오픈 클래스일 수 없다.
1
2
3
data class Person(val name: String) {
  var age: Int = 0
}

Person 클래스를 위와 같이 선언하면, equals 비교 결과는 다음과 같습니다.

1
2
3
4
5
6
7
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20

println("person1 == person2 : ${person1 == person2}")
// person1 == person2 : true

Sealed class & interface

Sealed class의 서브 클래스들은 컴파일 타임에 컴파일러가 알 수 있습니다. 서브 클래스들은 같은 모듈 혹은 패키지에서만 등장할 수 있습니다.

Sealed class는 다음과 같은 경우에 유용하게 사용할 수 있습니다.

  • 클래스 상속의 제한이 요구될 때
  • type safe 디자인이 요구될 때
1
2
3
4
5
6
7
8
9
10
11
12
// Create a sealed interface
sealed interface Error

// Create a sealed class that implements sealed interface Error
sealed class IOError(): Error

// Define subclasses that extend sealed class 'IOError'
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

// Create a singleton object implementing the 'Error' sealed interface
object RuntimeError : Error

Generics

코틀린 클래스도 자바와 유사하게 타입 파라미터를 받을 수 있습니다.

1
2
3
4
5
class Box<T>(t: T) {
  var value = t
}

val box: Box<Int> = Box<Int>(1)

Variance

코틀린은 와일드카드가 없는 대신 type projection과 declaration-site variance가 있습니다.

코틀린의 declaration-site variance에 대해서 알기 위해서는 자바의 가변성에 대해서 먼저 알아봐야합니다.

먼저 자바에서 제네릭 타입은 invariance, 무공변입니다.

List 은 List의 서브 타입이 아니라는 말입니다.

만약 리스트가 무공변이 아니라면, 아래 코드는 정상적으로 컴파일될 것이고, 런타임에 에러를 발생할 것입니다.

1
2
3
4
5
6
7
8
9
10
List<String> strs = new ArrayList<String>();

// 타입 미스매치 에러를 컴파일 타임에 발생시킵니다.
List<Object> objs = strs;

// 타입 미스매치 에러를 발생시키지 않는다면?
objs.add(1);

// 이때 타입 미스 매치로 인해 ClassCastException이 발생하게 됩니다.
String s = strs.get(0);

자바는 런타임 에러를 방지하기 위해 이런 것들을 금지합니다. 하지만 이런 것들이 영향을 미치는 경우가 있습니다. Collection 인터페이스의 addAll() 메소드를 생각해보면, 아래와 같이 작성할 수 있을 것 입니다.

1
2
3
4
interface Collection<E> ... {
  void addAll(Collection<E> items);

}

하지만, 위와 같이 작성된 코드는 다음과 같은 경우에 문제가 발생합니다.

1
2
3
void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from);
}

그렇기에 실제 addAll() 메소드 시그니처는 다음과 같이 작성되었습니다.

1
2
3
interface Collection<E> ... {
  void addAll(Collection<? extends E> items);
}

와일드카드 타입 ? extends E은 E 뿐만 아니라 E의 서브 타입도 허용합니다. 하지만 이 경우, E의 서브 타입이 무엇인지 확정 지을 수 없기때문에 값을 쓸 수는 없습니다.

이런 제한사항 때문에 Collection<String>Collection<? extends Object>의 하위 타입으로 가능하게 하는 다시 말해 타입 covariant, 공변적으로 만드는 것을 고려하게 됩니다.

만약 컬렉션으로 부터 아이템을 빼오기만한다면, String 컬렉션을 사용하여 Object를 읽는 것은 문제가 없습니다. 만약 컬렉션에 데이터를 추가하기만 한다면, Object 컬렉션을 사용하여 String을 추가하는 것은 문제가 없습니다. 후자에서 설명하는 개념은 반공변 개념입니다.

Delcaration-site variance

다음과 같은 자바 제네릭 인터페이스가 있습니다.

1
2
3
interface Source<T> {
  T nextT();
}

그렇다면, Source<String>Source<Object>로 레퍼런스하는 것은 무조건 안전하지만, 자바에서는 이것을 금지합니다.

1
2
3
void demo(Source<String> strs) {
  Source<Object> objects = strs; // 금지되었습니다.
}

이 문제를 해결하기 위해서는 Source<? extends Object>로 선언하는 방법이 있습니다. 하지만, 선언하여도 컴파일러는 알지못하고 여전히 문제가 됩니다.

코틀린에서는 이런 상황을 컴파일러에 설명하는 방법이 있습니다. 이것을 declaration-site variance 라고 합니다. 코틀린에서는 out를 이용해 공변으로 만들 수 있고, in을 이용해 반공변으로 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Source<out T> {
  fun nextT(): T
}

fun demo(strs: Source<String>) {
  val objects: Source<Any> = strs // T가 out 파라미터이기에 문제 되지 않습니다.
}

interface Comparable<in T> {
  operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
  x.compareTo(1.0)
  val y: Comparable<Double> = x
}

코틀린 제네릭 in, out 참고 블로그

Enum

enum은 다음과 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
9
enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

더불어서 enum 클래스에서 인터페이스를 구현할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
    PLUS {
        override fun apply(t: Int, u: Int): Int = t + u
    },
    TIMES {
        override fun apply(t: Int, u: Int): Int = t * u
    };

    override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}

inline value classes

inline class는 클래스명 앞에 value를 붙이는 것으로 선언할 수 있습니다.

1
2
3
4
5
value class Password(private val s: String)

// JVM backend에서는 아래와 같이 작성해야합니다.
@JvmInline
value class Password(private val s: String)

inline value 클래스를 사용해서 객체 생성시 힙 할당으로 인해 발생하는 오버헤드를 줄일 수 있습니다.

Object

object는 익명 클래스의 오브젝트를 생성하는 표현입니다. 오브젝트를 이용한 익명 오브젝트는 다음과 같이 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
val helloWorld = object {
    val hello = "Hello"
    val world = "World"
    // object expressions extend Any, so `override` is required on `toString()`
    override fun toString() = "$hello $world"
}

print(helloWorld)
// Hello World

다음과 같이 인터페이스를 상속하는 익명 클래스를 생성할 수도 있습니다.

1
2
3
4
5
window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*...*/ }

    override fun mouseEntered(e: MouseEvent) { /*...*/ }
})

상속을 받을 수도 있고, 상속을 받는다면, 다음처럼 적합한 생성자와 함께 사용해야 합니다.

1
2
3
4
5
6
7
8
9
open class A(x: Int) {
    public open val y: Int = x
}

interface B { /*...*/ }

val ab: A = object : A(1), B {
    override val y = 15
}

또한 object를 이용해서 간편하게 싱글톤 오브젝트를 생성할 수도 있다.

1
2
3
4
5
6
7
8
object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

companion object라는 하나의 클래스가 갖는 동반 객체를 선언할 수도 있다.

1
2
3
4
5
6
7
8
9
class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

Myclass.Factory.create()
MyClass.create()
MyClass.Companion.create() // 동반 객체의 이름이 없는 경우에만 사용가능하다

동반 객체 참고 블로그

function

코틀린에서 함수는 다음과 같이 선언합니다.

1
2
3
fun double(x: Int): Int {
  return x * 2
}

멤버 함수 호출은 .가 필요합니다.

1
Stream().read() // Stream 인스턴스를 생성하고, read()를 호출합니다.

default parameter

파라미터는 기본 값을 가질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun read(
  b:ByteArray,
  off: Int = 0,
  len: Int = b.size,
)
fun foo(
    bar: Int = 0,
    baz: Int = 1,
    qux: () -> Unit,
) { /*...*/ }

foo(1) { println("hello") }     // Uses the default value baz = 1
foo(qux = { println("hello") }) // Uses both default values bar = 0 and baz = 1
foo { println("hello") }        // Uses both default values bar = 0 and baz = 1

메소드 오버라이딩시, 베이스 메소드의 파라미터 기본값을 사용합니다.

1
2
3
4
5
6
7
open class A {
    open fun foo(i: Int = 10) { /*...*/ }
}

class B : A() {
    override fun foo(i: Int) { /*...*/ }  // 기본값이 허용되지 않습니다.
}

named arguments

함수를 호출할 때, 인자의 이름을 지정해서 호출할 수 있습니다.

다음과 같은 함수가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun reformat(
    str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHumps: Boolean = false,
    wordSeparator: Char = ' ',
) { /*...*/ }

// 모든 인자에 이름을 붙이지 않아도 됩니다.
reformat("String!", false, upperCaseFirstLetter = false, divideByCamelHumps = true, '_')

// 디폴트 값이 있는 인자들은 스킵할 수 있습니다.
reformat("this is long string")

single expression function

다음과 같이 간단하게 함수를 작성할 수도 있습니다.

1
2
fun double(x: Int): Int = x * 2
fun double(x: Int) = x * 2 // 리턴타입을 명시하지 않아도됩니다.

가변 인자

함수의 파라미터(특히 마지막)을 가변인자로 받을 수 있습니다.

1
2
3
4
5
6
7
8
fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}

val list = asList(1,2,3)

Infix

Infix함수는 . 그리고 () 없이도 호출할 수 있습니다. Infix 함수는 다음 조건을 만족해야합니다.

  • 멤버 함수 혹은 extension 함수로 작성해야합니다.
  • 하나의 파라미터만을 가져야합니다.
  • 가변인자 파라미터가 존재하면 안되고, 기본값 역시 존재하면 안됩니다.
1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { ... }

// calling the function using the infix notation
1 shl 2

// is the same as
1.shl(2)

lambda

코틀린 함수는 일급 함수입니다.

일급 함수란 의미는 함수가 변수에 저장될 수 있고, 함수의 인자로 전달 될 수 있고, 다른 함수의 리턴 값이 될 수 있음을 의미합니다.

코틀린은 이를 가능하게 하기 위해서 함수 타입의 패밀리를 사용하며, 람다 표현식과 같은 특수한 언어 구성 요소들을 제공합니다.

High-order functions

higher-order function이란 함수를 인자로 받고, 함수를 리턴하는 함수를 의미합니다. fold 함수가 이에 대한 좋은 예시입니다.

1
2
3
4
5
6
7
8
9
10
fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

초기 accumulator 값을 입력 받고, combine할 함수를 전달 받습니다. 컬렉션의 각 엘리멘트를 순회하면서 연속적으로 combine 함수를 실행하고, 결과를 리턴합니다.

위 코드에서 combine 파라미터는 함수 타입(R, T) -> R으로 R 타입과 T 타입을 입력 받아 R 타입을 리턴하는 함수를 인자로 받을 수 있습니다.

fold 함수를 호출하기 위해서는 함수 타입의 인스턴스를 전달해야 합니다. 그리고 람다 함수가 이를 위해 종종 쓰입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val items = listOf(1, 2, 3, 4, 5)

// {}로 감싸진 블럭이 람다에 해당합니다.
items.fold(0, { 
    // 람다가 파라미터가 있다면, 가장 먼저 작성하고, 그 이후 -> 를 작성합니다.
    acc: Int, i: Int -> 
    print("acc = $acc, i = $i, ") 
    val result = acc + i
    println("result = $result")
    // 람다의 마지막 줄은 리턴 값으로 여겨집니다.
    result
})

// 타입의 유추가 가능하다면, 타입은 생략 가능합니다.
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

// 함수의 레퍼런스를 이용할 수도 있습니다.
val product = items.fold(1, Int::times)

Function types

코틀린은 (Int) -> String과 같은 함수 타입을 이용합니다. 함수 타입들은 함수의 서명, 즉 함수의 매개변수와 반환값에 해당하는 특별한 표기법을 가지고 있습니다.

  • 모든 함수 타입은 괄호안에 파라미터 타입과 리턴 타입을 가지고 있습니다.
    • (A, B) -> C : A타입 그리고 B타입을 파라미터로 받으며, C 타입을 리턴하는 함수라는 의미,
    • () -> C : Unit 타입의 경우 생략 가능합니다.
  • 함수 타입은 추가적인 리시버 타입을 가질 수 있습니다.
    • A.(B) -> C : A 타입의 리시버 오브젝트가 파라미터 B를 인자로 받고 C를 리턴하는 함수를 의미합니다.

다음과 같이 함수를 리턴하는 타입도 작성 가능합니다. (Int) -> ((Int) -> Unit)

Instantiating function type

함수 타입의 인스턴스화에는 몇가지 방법들이 존재합니다.

  • function literal
    • 람다 : {a, b -> a+b}
    • 익명 함수 : fun(s: String): Int {return s.toIntOrNull() ?: 0}
  • callable reference
    • 탑 레벨, 로컬, 멤버, 혹은 extension function ::isOdd, String::toInt
    • 탑 레벨, 멤버, 혹은 extension property List<Int>::size
    • 생성자 ::Regex
  • 함수 타입을 인터페이스로 전달받아 구현한 커스텀 클래스
    1
    2
    3
    4
    5
    6
    
    class IntTransformer: (Int) -> Int {
      override operator fun invoke(x: Int): Int {
        //~~
      }
    }
    val intFunction: (Int) -> Int = IntTransformer()
    

수신자(receiver)가 있는 함수 타입과 없는 함수 타입의 비문자적 값(non-literal value)은 상호 교환이 가능하므로, 수신자는 첫 번째 매개변수를 대신할 수 있으며 그 반대도 가능합니다. 예를 들어, (A, B) -> C 타입의 값을 A.(B) -> C 타입이 예상되는 곳에 전달하거나 할당할 수 있으며, 그 반대도 가능합니다.

1
2
3
4
5
6
7
8
val repeatFun: String.(Int) -> String = {times -> this.repeat(times)}
val twoParam: (String, Int) -> String = repeatFun // 이렇게 교환가능합니다.

fun runTransformation(f: (String, Int) -> String): String {
  return f("hello", 3)
}

val result = runTransformation(repeatFun)

Invoking function type instance

invoke(....)를 이용해 함수 인스턴스 실행할 수 있습니다.

만약 리시버 타입이 존재한다면, 리시버 오브젝트가 첫번째 인자로 전달되어야합니다. 또 다른 방법으로는 extension function처럼 사용할 수도 있습니다.

1
2
3
4
5
6
7
8
9
val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("hello", "world"))

println(intPlus.invoke(1, 1))
println(intPlus(2, 3))
println(9.intPlus(10))

lambda expressions and anonymous function

람다와 익명 함수는 function literal입니다. function literal 이란 선언되지 않고 즉시 전달되어 사용되는 함수를 의미합니다.

1
max(strings, { a, b -> a.length < b.length })

max는 상위 레벨 함수로 함수를 두번째 인자로 입력 받습니다. 두번째 인자로 전달된 함수는 function literal로 다음 함수와 동일합니다.

1
fun compare(a: String, b: String): Boolean = a.length < b.length

람다 표현식의 전체 구문 형식은 다음과 같습니다.

1
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x+y }
  • 람다 표현식은 항상 {}로 감싸진다.
  • 파라미터 구문은 {} 안에 항상 들어가고, 타입 파라미터는 필수적이지 않습니다.
  • 바디는 -> 이후에 작성됩니다.
  • 리턴 타입이 Unit이 아니라면, 가장 마지막 줄에 해당하는 값이 리턴됩니다.

trailing lambda

만약 함수의 마지막 파라미터가 함수라면, 다음과 같이 파라미터 바같에 함수를 전달 가능합니다.

1
val product = items.fold(1) { acc, e -> acc * e }

만약 람다가 호출에 필요한 유일한 변수라면, 다음처럼 작성도 가능합니다.

1
run { println("...") }

it : 단일 파라미터 암시적 표현

람다가 하나의 파라미터만 가지는 경우는 매우 흔합니다. 람다가 하나의 파라미터만 가진다면, 다음과 암시적으로 it을 이용해서 파라미터에 접근 가능합니다.

1
ints.filter {it > 0}

underscore for unused variables

만약 람다 파라미터가 사용되지 않는다면, 변수명 대신 _를 사용가능합니다.

1
map.forEach {(_, value) -> println(value)}

익명 함수

람다 표현식에서는 한가지가 빠져있습니다 : 리턴 값의 타입 명시

대부분의 경우 내부적으로 리턴 값의 타입을 알 수 있지만, 리턴 값 타입의 명시가 필요하다면, 익명 함수를 사용할 수 있습니다.

1
fun(x: Int, y: Int): Int = x + y

익명 함수의 선언은 보통 함수의 선언과 매우 유사합니다.

Closures

람다 표현식이나 익명 함수(또는 로컬 함수와 객체 표현식)는 외부 범위에서 선언된 변수들을 포함하는 클로저에 접근할 수 있습니다. 클로저에서 캡처된 변수들은 람다 내부에서 수정될 수 있습니다.

1
2
3
4
5
var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

inline functions

high-order function들을 사용하는 것은 런타임 패널티를 부과하기도 합니다. 각 함수는 객체로 간주되며, 클로저를 캡처합니다. 클로저는 함수 본문에서 접근할 수 있는 변수의 범위(scope)입니다. 함수 객체와 클래스에 대한 메모리 할당과 가상 호출(virtual calls)은 런타임 오버헤드를 초래합니다.

대부분의 경우 이런 오버헤드는 람다 표현을 in-line하는 것으로 해결될 수 있습니다.

1
lock(l) {foo()}

파라미터에 해당하는 함수 오브젝트를 생성하고 호출을 생성하는 대신, 컴파일러는 다음과 같은 코드를 생성할 수 있습니다.

1
2
3
4
5
6
l.lock()
try {
    foo()
} finally {
    l.unlock()
}

컴파일러가 이렇게 동작하기 위해서는 lock()함수를 inline함수로 만들어줘야합니다.

1
inline fun <T> lock(lock: Lock, body: () -> T): T { ...  }

inline처리를 하면 바이트코드 양은 증가하게 되지만, 무의미하게 객체를 생성하는 것으로 발생하는 오버헤드를 줄일 수 있다.

noinline

noinline으로 람다 표현식이 inline되는 것을 막을 수 있습니다.

1
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

inline 함수 관련 참고 블로그

Type-safe builders

타입-세이프 빌더(Type-safe builders)는 복잡한 계층적 데이터 구조를 반선언적 방식으로 구축하기에 적합한 Kotlin 기반 도메인 특화 언어(DSL)를 생성할 수 있게 해줍니다. 빌더의 샘플 사용 사례는 다음과 같습니다:

  • Kotlin 코드를 사용하여 마크업 생성 (예: HTML 또는 XML)
  • 웹 서버의 라우트를 구성하기 (예: Ktor)

html 마크업 생성 과정

null safety

코틀린의 타입 시스템은 null참조로 인한 문제를 제거하는 것을 목표로 만들어졌습니다.

코틀린에서 NPE가 발생하는 경우는 다음 경우 뿐입니다.

  • 명시적으로 NPE를 던지는 경우
  • !! 연산자
  • 초기화와 관련된 데이터 불일치 문제는 다음과 같은 상황에서 발생할 수 있습니다:
    • 생성자에서 초기화되지 않은 this가 전달되어 다른 곳에서 사용되는 경우 (“leaking this” 문제).
    • 상위 클래스의 생성자가 호출되면서, 파생 클래스에서 초기화되지 않은 상태를 사용하는 오픈 멤버를 호출하는 경우.

코틀린 타입 시스템에서는 null을 담을 수 있는 타입과, 담을 수 없는 타입을 구분합니다. String 타입의 변수는 null을 담을 수 없습니다.

1
2
var a: String = "abc"
a = null // 컴파일 에러

null을 허용하기 위해서는 String? 자료형으로 작성해야합니다.

1
2
3
var b: String? = "abc"
b = null
print(b)

a가 NPE을 발생시키지 않는 것이 보장되면, 다음과 같이 사용 가능합니다.

1
val l = a.length

하지만, NPE을 발생시킬 수 있다면 위와 같이 사용할 수 없습니다. 가장 간단하게 문제를 해결하는 방법은 명시적으로 확인하는 것입니다.

1
val l = if (b != null) b.length else -1

또다른 방법은 ?. 를 사용하는 방법입니다.

1
2
3
val a = "Kotlin"
val b: String? = null
println(b?.length)

b?.length 연산은 b가 null이 아니라면 b.length를 리턴합니다.

non-null 값에 대한 특정 연산을 처리하고 싶으면 let을 사용할 수 있습니다.

1
2
3
4
val listWithNulls: List<String?> = listOf("kotlin", null)
for (item in listWithNulls) {
  item?.let { println(it) }
}

nullable receiver

extension 함수는 nullable receiver에 적용될 수 있습니다. toString()은 nullable receiver을 만족할 수 있게 정의됐습니다.

1
2
val person: Person? = null
logger.debug(person.toString()) // 에러를 발생하지 않고, null이 출력됩니다.

elvis operator

idiom 섹션에서 확인했던 것 처럼, if not null을 다음과 같이 간편하게 작성 가능합니다.

1
val l = b?.length ?: -1

!! operator

다음 코드는 b가 null인 경우 NPE을 발생시킵니다.

1
val l = b!!.length

equality

  • == : equals 로 값 동일성 판단
  • === : 같은 객체를 참조하고 있는지 확인

asynchronous programming technique

수십년동안 개발자들은 애플리케이션이 blocking 되는 문제를 해결하기 위해 노력해왔습니다. blocking 문제에 대한 해결책으로 다음과 같은 방법들이 주로 사용됐습니다.

  • threading
  • callback
  • futures, promises and others
  • reactive extensions
  • coroutines

threading

thread가 가장 널리 알려진 블로킹을 피하는 방법 중 하나 입니다.

1
2
3
4
5
6
7
8
9
10
fun postItem(item: Item) {
  val token = preparePost()
  val post = submitPost(token, item)
  processPost(post)
}

fun preparePost(): Token {
  // 메인 스레드를 block하는 작업 발생
  return token
}

preparePost가 길게 소요되는 작업일 경우, ui를 block하게 됩니다. 이를 해결하기 위해서 preparePost를 다른 스레드에서 실행할 수 있습니다. 이는 매우 흔한 작업이지만, 다음과 같은 단점이 있습니다.

  • 스레드는 싼 리소스가 아닙니다. 컨텍스트 스위칭이 발생하게 되고 이는 비용이 발생하는 작업입니다.
  • 스레드는 무한히 생성할 수 없습니다.
  • 스레드는 항상 사용가능하지 않습니다. 자바스크립트 같은 플랫폼에서는 스레드를 지원하지 않습니다.
  • 스레드는 사용하기 쉽지 않습니다. 스레드를 디버깅하고, race condition을 피하기 쉽지 않습니다.

callbacks

콜백은 함수를 파라미터로 다른 함수에 전달해서 프로세스가 끝나면 실행될 수 있게 하는 개념입니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun postItem(item: Item) {
  preparePostAsync { token ->
    submitPostAsync(token, item) { post ->
      processPost(post)
    }
  }
}

fun preparePostAsync(callback: (Token) -> Unit) {
  // make request and return immediately
  // arrange callback to be invoked later
}

하지만 콜백도 여러가지 문제가 있습니다.

  • 콜백이 중첩된 경우, 이해하기 힘든 코드가 작성될 수 있습니다.
  • 에러 처리가 어렵습니다.

콜백 사용 예시 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
    catchPrimeHigh(range = 100) { num ->
        println("CatchPrimeNumber: $num")
    }
}

fun catchPrimeHigh(range: Int, callback: (Int) -> Unit) {
    (1..range).forEach { num ->
        if (isPrime(num)) callback(num)
    }
}

fun isPrime(num: Int): Boolean {
    for (i in 2..num / 2) {
        if (num % i == 0) return false
    }
    return true
}

Futures, promises, and other

Futures, promises는 어떤 함수를 호출하면 특정 시점에서 Promise라는 오브젝트를 리턴하고, 이를 이용해 작업을 이어나갈 것이라는 아이디어에서 기반했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun postItem(item: Item) {
  preparePostAsync()
    .thenCompose { token ->
      submitPostAsync(token, item)
    }
    .thenAccept { post ->
      processPost(post)
    }
}

fun preparePostAsync(): Promise<Token> {
  // makes request and returns a promise that is completed later
  return promise
}

이런 접근 법은 다음과 같은 변경사항들을 필요로 합니다.

  • 다른 프로그래밍 모델
    • 콜백과 유사하게 프로그래밍 모델을 top-down에서 chained call로 변경해야합니다.
    • 전통적 프로그래밍 구조의 반복문, exception handling은 이 모델에서 유효하지 않습니다.
  • 다른 api
    • thenCompose, thenAccept와 같은 새로운 api에 대한 학습이 필요합니다.
  • 리턴 타입
    • 실 데이터를 리턴하지 않고 Promise를 리턴해야합니다.
  • Error handling이 복잡해집니다.

Reactive extensions

Reactive Extensions (Rx)은 Erik Meijer가 소개한 개념입니다. Netflix가 자바에 적용하며 RxJava로 메인스트림으로 쓰이기 시작했습니다. Rx에서 중요한 개념중 하나는 observable streams로 데이터를 무한히 발생하는 스트림으로 여기고 관찰할 수 있다는 개념입니다.

Rx를 간단하게 Observer pattern이라고 할 수 있습니다. https://en.wikipedia.org/wiki/Observer_pattern

Futures와 유사하게 보이지만, Rx는 스트림을 리턴한다는 차이점이 있습니다.

원문 링크

coroutine

코틀린의 비동기적 코드 작성 방법은 coroutine을 사용하는 것입니다. 코루틴은 실행 중인 함수가 특정 지점에서 실행을 일시 중지하고 나중에 다시 실행을 재개할 수 있는 개념, 즉 일시 중지 가능한 연산의 개념입니다.

그러나 코루틴의 장점 중 하나는 개발자가 비차단(non-blocking) 코드를 작성하는 것이 본질적으로 차단(blocking) 코드를 작성하는 것과 거의 동일하다는 점입니다. 즉, 프로그래밍 모델 자체는 크게 변하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
fun postItem(item: Item) {
  launch {
    val token = preparePost()
    val post = submitPost(token, item)
    processPost(post)
  }
}

suspend fun preparePost(): Token {
  // makes a request and suspends the coroutine
  return suspendCoroutine { /* .. */ }
}

코드는 메인 스레드를 차단하지 않고 오래 실행되는 작업을 시작합니다. preparePost는 일시 중지 가능한 함수라고 불리며, 이 때문에 suspend라는 키워드가 함수 앞에 붙습니다. 이는 앞서 설명한 것처럼, 함수가 실행을 시작하고 중간에 일시 중지되었다가 나중에 다시 실행을 재개할 수 있음을 의미합니다.

  • 함수의 시그니처는 완전히 동일하게 유지됩니다. 유일한 차이점은 suspend가 추가된다는 것입니다. 그러나 반환 타입은 우리가 반환받기를 원하는 타입 그대로 유지됩니다.
  • 코드는 여전히 동기 코드처럼 위에서 아래로 작성됩니다. 특별한 문법이 필요 없으며, 코루틴을 시작하는 launch 함수의 사용만 있으면 됩니다
  • 프로그래밍 모델과 API는 동일하게 유지됩니다. 반복문, 예외 처리 등을 계속 사용할 수 있으며, 새로운 API 세트를 배울 필요가 없습니다.
  • 이것은 플랫폼에 독립적입니다. 우리가 JVM, JavaScript 또는 다른 어떤 플랫폼을 대상으로 하든, 작성하는 코드는 동일합니다. 컴파일러가 내부적으로 각 플랫폼에 맞게 코드를 적절히 변환해줍니다.

코루틴은 새로운 개념이 아니며, Kotlin이 발명한 것도 아닙니다. 코루틴은 수십 년 동안 존재해왔고 Go와 같은 다른 프로그래밍 언어에서도 인기가 있습니다. 그러나 중요한 점은 Kotlin에서 구현된 방식입니다. 대부분의 기능이 라이브러리에 위임됩니다. 실제로 suspend 키워드 외에는 언어에 추가된 키워드가 없습니다. 이는 C#과 같은 언어와 약간 다릅니다. C#에서는 async와 await가 문법의 일부이지만, Kotlin에서는 이들이 단지 라이브러리 함수일 뿐입니다.

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

자바스크립트 비동기 방식 참고 블로그

자바 스크립트 promise, async/await 흐름 제어 참고 블로그

Annotations

어노테이션은 코드에 메타 데이터를 추가하는 것을 의미합니다. 어노테이션을 선언하기 위해서는 annotation을 클래스 앞에 포함하면 됩니다.

1
annotation class Fancy

어노테이션의 추가적인 정보들은 아래 어노테이션들을 이용해 포함시킬 수 있습니다.

  • @Target : 어노테이션을 사용가능한 element 명시 (classes, functions, propeties, …)
  • @Retention : 어노테이션이 컴파일 클래스 파일에 저장될지, 런타임에 reflection을 통해 visible할지 여부를 명시합니다.
  • @Repeatable : 같은 어노테이션을 단일 엘리멘트에 여러번 사용가능한지를 명시합니다.
  • @MustBeDocumented : 어노테이션이 public API의 일부이고, API 문서에 문서화되야함을 명시합니다.
1
2
3
4
5
6
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
        AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
        AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

usage

선언한 어노테이션은 다음과 같이 사용가능합니다.

1
2
3
4
5
@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

생성자

어노테이션은 파라미터를 입력받는 생성자를 가질 수 있습니다.

1
2
3
annotation class Special(val why: String)

@Special("example") class Foo {}

허용된 파라미터 타입은 다음과 같습니다.

  • java primitive types
  • Strings
  • Classes
  • Enums
  • 다른 어노테이션들
  • 위 타입의 배열

어노테이션 파라미터들은 null 가능한 타입일 수 없습니다. 만약 어노테이션이 다른 어노테이션의 파라미터로 쓰이면, 클래스 명 앞에 @가 쓰이면 안됩니다.

1
2
3
4
5
6
7
annotation class ReplaceWith(val expression: String)

annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""))

@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))

만약 클래스를 어노테이션의 인자 중 하나로 써야한다면, KClass를 사용하는 것이 권장됩니다. 코틀린 컴파일러가 자동으로 자바 클래스로 변환해줄 것 이고, 자바 코드가 어노테이션과 인자를 문제 없이 사용할 수 있게 해줍니다.

1
2
3
4
5
import kotlin.reflect.KClass

annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)

@Ann(String::class, Int::class) class MyClass

Instantiation

자바에서 어노테이션 타입은 인터페이스의 형태로 사용되어서 어노테이션을 구현하고 인스턴스를 생성할 수 있습니다. 이런 메커니즘의 대안으로 코틀린에서는 어노테이션 클래스의 생성자를 호출하는 것으로 인스턴스를 사용할 수 있게 지원하고 있습니다.

1
2
3
4
5
6
7
8
9
10
annotation class InfoMarker(val info: String)

fun processInfo(marker: InfoMarker): Unit = TODO()

fun main(args: Array<String>) {
    if (args.isNotEmpty())
        processInfo(getAnnotationReflective(args))
    else
        processInfo(InfoMarker("default"))
}

lambda

어노테이션은 람다에서도 사용가능합니다. Quasar같은 프레임워크에서는 어노테이션을 이용해서 동시성을 제어합니다.

1
2
3
annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

destructuring declarations

어떤 경우에는 오브젝트를 다수의 변수로 분해하는 것이 편한 경우가 있습니다.

1
val (name, age) = person

위 문법을 destructuring declaration(분해 선언)이라고 부릅니다. 분해 선언은 다수의 변수를 한번에 생성하고, 독립적 사용을 가능하게 합니다.

분해선언은 다음과 같은 코드를 통해 실행됩니다.

1
2
val name = person.component1()
val age = person.component2()

분해선언은 for 반복문에서도 적용가능합니다.

1
for ((a, b) in collection) {....}

예시) 함수에서 두 가지 값을 리턴하는 경우

1
2
3
4
5
6
7
8
data class Result(val result: Int, val status: Status) 

fun function(...): Result {
  // 연산처리
  return Result(result, status)
}

val (result, status) = function(...)

예시) 맵에서의 분해선언

1
2
3
for ((key, value) in map) {
   // do something with the key and the value
}
This post is licensed under CC BY 4.0 by the author.