코틀린

내위키
Dennis (토론 | 기여)님의 2020년 9월 20일 (일) 12:53 판

Kotlin.

공식 웹사이트.

통합 개발환경인 인텔리J로 유명한 제트브레인에서 내놓은 프로그래밍 언어. 자바 가상머신 위에서 동작하며 자바와 100% 호환된다는 것이 제트브레인 측의 설명이다. 즉 자바가 돌아가는 환경이라면 코틀린도 돌아간다. 그 대표적인 예가 안드로이드로, 안드로이드 스튜디오 2.x 대에서도 코틀린을 사용해서 안드로이드 프로그래밍을 하는 것이 가능하며[1] 안드로이드 스튜디오 3.0부터는 아예 자바와 함께 코틀린도 기본으로 지원한다. 2019년부터는 아예 안드로이드 관련 공식문서에 코틀린을 우선 사용하기 시작했다. 물론 자바 라이브러리도 제약 없이 갖다 쓸 수 있다. 자바 때문에 오라클과 계속해서 분쟁을 겪고 있는 구글로서는 코틀린이 하나의 대안으로 부상하고 있는데, 한때 스위프트로 돌아선다는 얘기도 있었지만 그럴 가능성은 거의 사라졌고, 일단 자바와 100% 호환되는 코틀린을 지원함으로써 개발자들을 이쪽으로 유도한 후 자바 가상머신을 대체해서 LLVM과 같은 방식으로 코틀린을 네이티브 지원하고 자바는 자연스럽게 놓지 않겠느냐는 전망이 대두되었고, 실제로 LLVM 기반으로 JVM을 필요로 하지 않는 코틀린 네이티브(Kotlin Native) 프로젝트가 진행되고 있다. 안드로이드만이 아니라 iOS, 윈도우, macOS, 리눅스도 지원한다. 아파치 톰캣과 같은 자바 서블릿 기반 웹 서버 환경에서도 쓸 수 있으며 자바 서버 환경에서 널리 널리 쓰이는 Spring Framework도 Spring Boot를 통해 코틀린을 잘지원한다.

특징

간결함

자바에 비해서 훨씬 간결하게 코드를 짤 수 있으며, 현대 프로그래밍의 여러 개념들을 적극 수용했다. 실제로 똑같은 일을 하는 프로그램을 자바와 코틀린으로 짜 보면 코틀린의 코드 양이 훨씬 적다. 예를 들면 아래의 코드는 단 한 줄로 name, email, company 세 개의 필드를 가지고 이에 대한 getters와 setters는 물론 equals(), hashCode(), toString() 그리고 copy()까지 모두 지원하는 POJO 클래스를 정의한다.

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

이걸 자바로 짜려면 몇 줄이나 나올까? 말도 하지 말자. 그런데 자세히 보면 변수 혹은 매개변수를 정의하는 방법이 자바와 많이 다르다. val이라는 키워드가 먼저 나오고, 변수 이름이 나온 다음 콜론을 찍고 변수의 타입을 쓴다. 변수 타입 → 이름 순서인 자바[2]와는 반대다. [3] 다만 약간 헷갈릴 수 있는 부분이 있는데, 매개변수의 이름에 val이나 var를 사용하면 이를 프로퍼티로 간주하여 클래스 안에 변수는 물론 getter와 setter도 만들어주는 반면, val이나 var가 없으면 그냥 생성자(constructor)에 쓸 매개변수로 취급한다.

또한 과도하게 엄격한 예외 처리를 강요하는 자바와는 달리 모든 예외 처리를 강제하지 않는다. 자바에서는 이른바 checked error라고 해서 모든 예외를 다 프로그래밍으로 처리함으로써 오류 문제에 단단한 코드를 만들려고 했지만 프로그래머들은 지나친 엄격함에 시달리다 못해 그냥 안 받아도 되는 RuntimeException 같은 걸로 다 때워버리든가 해버려서 오히려 프로그램을 부실하게 만들었다. 코틀린은 이런 문제점을 해소한 셈이다.

자바와는 달리 줄 끝에 세미콜론을 쓰지 않는 것도 차이. 요즘 인기 있는 언어들인 파이썬, Go(응?)[4], 스위프트도 역시 줄끝에 세미콜론을 쓰지 않는다.

안전함

코틀린이 강조하는 또 하나의 특징은 안전함이다. 코틀린은 null 값이 들어가도 되는 변수(nullable)와 그렇지 않은 변수를 구분한다. 기본은 nullable이 아니며 nullable로 지정하려면 변수의 타입 뒤에 ? 기호를 붙여 줘야 한다. 이로 인해 null 값을 잘못 사용해서 생길 수 있는 널 참조 문제[5]를 컴파일 단계에서 더 많이 잡아준다.

var output: String
output = null   // Compilation error

val name: String? = null    // Nullable type
println(name.length())      // Compilation error

또한 null이 아닐 때에만 어떤 코드를 실행할 수 있도른 간편한 방법을 제공한다.

something?.let {
    println("Not-null")
} ?: run { println("Null") }

변수 something이 null이면 println("Null")이, null이 이나면 println("Not-null")이 실행된다.

현대적

최근에 유행하는 고차함수클로저, 람다식 같은 개념들을 지원한다. 자바도 버전 8부터는 클로저람다식을 지원하지만 사용하려면 좀 복잡한데, 코틀린으로 하면 훨씬 간단하다.

fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    }
    finally {
        lock.unlock()
    }
}

고차함수의 예. lock 함수의 두 번째 매개변수는 body: () -> T로 되어 있는데, 이는 매개변수가 없고 T 유형의 반환값을 가지는 함수를 매개변수로 받는다는 뜻이며, 함수 안에서는 이를 body로 참조해서 실행시킬 수 있다.

기본 문법

일단 기본적인 문법은 다음과 같다.

변수 선언

변수를 선언하려면 var(variable), 또는 val(value) 키워드를 사용한다. val은 한번 선언하고 초기값을 부여하면 변경할 수 없는, 즉 읽기만 가능한 변수로 자바의 final과 같은 기능을 한다. 반면 var는 값을 변경할 수 있는 변수다. 가장 기본이 되는 형식은 다음과 같다.

// val [변수 이름]:[변수 유형] = [대입할 값]
// var [변수 이름]:[변수 유형] = [대입할 값]

var num: Int = 1
val num: Int = 2
num = 3 // Error!

C자바와는 달리 변수 이름을 먼저 쓰고 : 기호 다음에 변수의 유형을 쓴다. Go가 이와 비슷한 방식을 사용하고 있다. 또한 자바는 기본 유형은 int, float처럼 모두 소문자지만 코틀린은 Int, Float와 같이 모두 대문자로 시작한다. 자바는 기본 유형인 int와 클래스인 Integer 사이, float와 Float 사이에 박싱 및 언박싱이 일어나지만 코틀린은 그런 거 없다.

코틀린은 var든 val이든 선언할 때 초기화를 해 주는 게 원칙이다. 다만 기본 형식이 아닌 클래스 var이라면 lateinit 키워드를 줘서 다른 곳에서 초기화를 하되 그 전에는 쓰지 않을 것이라고 알려준다. 만약 이렇게 해 놓고 초기화 하기 전에 사용하면 컴파일 오류를 일으킨다. 또한 val은 초기화하고 나면 값을 변경할 수 없지만 객체를 대입했다면 객체의 필드에 다른 값을 넣을 수는 있다. 물론 변경할 수 있는 필드 한정이다. 리스트나 맵 같은 유형도 클래스이기 때문에 객체를 val로 써도 역시 내용을 바꿀 수 있지만 기본적으로 List, Map과 같은 클래스들은 immutable이기 때문에 내용을 바꿀 수 없다. 내용을 바꾸고 싶다면 MutableList, MutableMap과 갈이 앞에 Mutable이 붙는 클래스를 써야 한다.

초기화 대입값에 따라서 추측하는 기능이 있어서 변수 선언 때 타입을 안 쓸 수도 있다.

var name = "John Doe" // name은 자동으로 String 타입이 된다.
name = 1 // Error!
var unknown // Error!

주의할 것은, 일단 초기화를 하고 나면 파이썬처럼 name에 다른 타입의 값을 집어넣을 수 없다. 코틀린은 자바와 마찬가지로 정적 타이핑 언어다. 선언 시점에서 초기 대입값에 따라 변수 타입이 결정되므로 다른 타입의 값을 넣으려고 하면 컴파일 단계에서 오류가 난다. 또한 변수를 선언만 하고 초기 대입값이 없으면 타입을 추정할 수 없으므로 이 역시 에러가 난다. 변수를 선언할 때 값을 대입하지 않을 거라면 타입을 반드시 지정해 줘야 한다.

기본적으로 코틀린의 변수에는 null을 대입할 수 없다. 어떤 변수에 null을 대입할 수 있게 하려면, 즉 nullable로 선언하려면 반드시 타입 뒤에 ? 표시를 붙여줘야 한다.

var he: String = "John Doe" // non-nullable
var she: String? = "Jane Doe" // nullable
he = null // Error!
she = null // Okay.

물론 기본값이 nullable이 아니라는 것은 될 수 있으면 nullable을 쓰지 말라는 것이다. 자바와 호환성 때문에 안 쓸 수 없다든가 하는 어쩔 수 없는 때에만 쓰도록 하자.

함수 선언

함수를 선언하려면 fun 키워드를 사용한다. 함수는 즐겁다. 변수 선언 때처럼 반환값 타입을 함수 매개변수 정의 뒤 : 다음에 써야 한다.

fun sum(a: Int, b: Int): Int {
    return a + b
}

// 위 함수를 다음과 같이 축약할 수도 있다.
fun sum(a: Int, b: Int) = a + b

코틀린은 함수 선언에서만이 아니라 여기 저기서 중괄호 쓸 일이 많이 줄어든다.

변수와 마찬가지로 함수도 null 값을 반환값으로 쓰려면 함수 정의의 반환값 타입 뒤에 ?를 붙여줘야 한다.


조건문 및 반복문

자바처럼 if-else, when 같은 조건문과 while, for 같은 반복문을 제공한다. 그러나 두드러진 차이가 있다.

val i: Int = 1
val test: String = if (i == 0) "ok" else "not ok"
println(test)

이렇게 if-else 문을 대입문에 사용할 수 있다. C나 자바는 Elvis (?:) 연산자로 비슷한 일을 할 수 있는데, 코틀린은 ? 연산자를 null-safe 기능을 위해 쓰기 때문에 이 연산자를 쓸 수 없다. 대신 if-else 문을 쓰면 된다.

코틀린은 C나 자바의 switch를 대체하는 when을 제공한다. 기본 기능은 switch와 비슷하지만 조건을 테스트하는 유연성이 훨씬 좋다. 다만 break 문이 없으므로 어떤 조건에 해당하는 명령들을 실행하고 나면 무조건 when 바깥으로 빠져나간다.

val i: Int = 101;
when(i) {
    1 -> println("one")
    2 -> println("two")
    in 3 .. 10 -> println("three to ten")
    !in 0 .. 100 -> println("more than 100")
    else -> println("I don't know")
}

when 안에서는 case 대신 그냥 평가값만 써 주면 되고 범위를 지정할 수도 있다. 함수도 쓸 수 있고 객체의 필드 또는 메서드를 사용할 수도 있고, 평가식도 사용할 수 있다.

val i: List<Int> = listOf(1, 2, 3, 4)
when {
    (i.size % 2 == 1) -> println("The size is odd.")
    (i.size % 2 == 0) -> println("The size of even.")
}

클래스와 객체

클래스 정의하기

클래스 정의는 자바와 비슷하다.

class Sample {
}

그런데 상속 관련 문법은 extends를 안 쓰고 : 기호를 쓰기 때문에 오히려 C++C#와 비슷해 보인다.

open class Base(num: Int)

class Derived(num: Int) : Base() {
}

코틀린에서 모든 클래스는 기본값이 자바final이다. 즉 상속 불가다. 상속을 허용하려면 open 키워드를 써 줘야한다. 즉, 자바는 final로 지정하지 않으면 어떤 클래스든 상속 가능이지만 코틀린은 반대로 open으로 지정하지 않으면 어떤 클래스든 상속 불가다. 이는 클래스 안의 함수에도 마찬가지다. 즉 open을 붙이지 않으면 함수 오버로딩이 안 된다. 이렇게 상속을 기본으로 막아놓고 지정한 것만 허용하는 정책은 C++또는 C#과 같은 개념이다.[6]

클래스 생성자

보통 클래스 생성자가 있을 때에는 매개변수를 받아서 필드를 설정하는데, 코틀린은 이를 간편하게 할 수 있다.

open class Person(private val name: String, private val age: Int) {
}

val john = Person("Jonn Doe", 42)

위와 같이 하면 Person 클래스 안에 nameage 두 가지 필드가 생기고, 객체를 만들 때 생성자를 사용해서 이들 필드를 설정할 수 있다.

object

자바와 구별되는 것으로는 클래스가 아니라 객체를 정의해 버리는 것도 가능하다. 이게 무슨 뜻이냐 하면, 싱글톤 클래스, 다시 말해서 이 클래스를 이용한 객체(인스턴스)가 딱 하나만 존재하는 클래스를 정의할 수 있는 것.

object Singleton {
    var count = 0;
    fun addCount(delta: Int = 1) {
        count += delta;
    }
}

object 이름이 꼭 Singleton일 필요는 없다. 이렇게 정의한 싱글톤 클래스의 객체는 바로 클래스 이름과 함께 쓰면 된다.

Singleton.addCount()
println("count is ${Singleton.count}")

단, object는 컨스트럭터를 가질 수 없다는 점에 유의하자.

companion object

코틀린에는 static이 없다. 즉 같은 클래스의 인스턴스들끼리 공유하는 변수나 함수를 만들 수 없다. 대신 companion object로 같은 일을 할 수 있다.

class Example {
    companion object {
        val name = "Example class"
    }
}

println("The class name is: ${Example.name}")

자바에서는 static과 그렇지 않은 멤버들이 뒤엉켜 있기 쉬운데, 코틀린은 static에 해당되는 멤버들이 모두 companion object 블록 안에 들어가야 하므로 엄격히 구분된다는 장점이 있다.

확장

기존 클래스에 새로운 메서드를 넣고 싶다면 자바에서는 클래스를 상속 받아서 추가해야 했다. 코틀린은 그럴 필요 없이 그냥 기존 클래스를 확장해서 새로운 메서드를 추가할 수 있는 방법을 제공한다. 또한 변수나 함수가 꼭 클래스 안에 있지 않아도 된다.

package com.example

val NAME = "John Doe"
fun myName() : String {
    println("$NAME")
}

이렇게 해 놓고,

com.example.myName()

다른 패키지에서 쓰려면 이렇게 패키지 이름을 붙여서 참조하면 된다.

각주

  1. 안드로이드 스튜디오에서 코틀린 플러그인을 설치해서 쓸 수 있다.
  2. 자바만이 아니라 대부분 프로그래밍 언어, 그 중에서도 C를 계승한 언어들이 그렇다. 반면 Go는 코틀린처럼 변수의 타입을 나중에 쓴다. 이쪽을 지지하는 주장은 보통 사람은 '변수 name은 String 유형'와 같은 순서로 생각하는 게 자유롭다는 것.
  3. 코틀린의 변수 선언과 가장 비슷한 언어는 놀랍게도 지금 별로 쓰이지 않고 있는 파스칼이다! 코틀린의 변수 선언 방식은 val a: Int = 10000인데 파스칼도 var a : integer = 10000;.
  4. Go는 정확히 말하면 세미콜론을 없앤 것은 아니고 컴파일할 때 줄 끝에 자동으로 세미콜론을 붙여 준다. 그때문에 주의할 부분들이 있다. 예를 들어 함수를 정의할 때 여는 중괄호를 함수 형식 정의와 같은 줄에 쓰지 않으면 컴파일 오류를 일으킨다.
  5. 이른바 The Billion Dollar Mistake라고 부른다. 토니 호어가 알골 언어를 설계하는 과정에서 좀 쉽게 가자는 마음에 널 참조를 허용했는데 이 때문에 수십 년 동안 일어난 문제가 'billion dollar' 수준으로 손해가 될 만큼 엄청나다는 것을 뜻하는 말.
  6. 코틀린을 잘 보다 보면 은근히 C#을 닮은 구석이 많다. 사실 C#이 사용성으로는 최강이라는 평가도 많으니 참고할 만도 하다.