Kotlin
고친 사람 github-actions[bot]
Kotlin 은 서버 프로그램과 Android 앱을 짜는 데 쓰는 프로그래밍 언어입니다. Java 로 짜 둔 코드와 한 프로젝트 안에서 섞어 씁니다. 값이 없을 수 있는 변수를 타입에 표시하게 해서, 실행 중에야 드러나던 오류를 컴파일할 때 걸러 줍니다.
쉽고 빠른 이해
Kotlin 은 Java 코드와 섞어 쓰면서 값이 비었는지를 컴파일러가 대신 검사해 주는 언어입니다. 값이
없을 수도 있는 변수는 타입 뒤에 물음표를 붙여 String? 이라고 적습니다.
Java 서버에서는 비어 있는 변수를 건드려 실행 도중에 멈추는 일이 잦습니다. 그렇다고 이미 쌓아 둔 Java 코드와 라이브러리를 버릴 수도 없습니다. Kotlin 은 그 둘을 같이 가져가려고 만든 언어입니다.
어떻게 도는가:
- 물음표를 붙인 타입의 변수만 값이 없을 수 있습니다
- 그런 변수는 비었는지 확인하기 전에는 쓰지 못하게 컴파일러가 막습니다
- 컴파일 결과는 Java 와 같은 형식이라 기존 Java 라이브러리를 그대로 부릅니다
대가도 있습니다. Java 쪽에서 건너온 값은 컴파일러가 비었는지 알 수 없어 검사가 거기서 끊깁니다. 배포물에는 Kotlin 전용 라이브러리가 하나 더 딸려 붙습니다.
상세
이 절은 Kotlin 을 다섯 가지로 나눠 봅니다. 소스가 실행되기까지 거치는 길, 값이 없을 수 있는 타입, 코드를 짧게 만드는 문법, 이미 있는 클래스에 함수를 덧붙이는 장치, 여러 일을 겹쳐 돌리는 방식입니다. 마지막 소절은 그 다섯이 저마다 무엇을 내주고 얻은 것인지를 한데 모읍니다.
견주는 상대는 Java 입니다. Kotlin 의 문법은 대부분 Java 에서 손이 많이 가던 일을 줄이려고 만든 것이라, Java 쪽 모습을 옆에 두면 각 문법이 왜 있는지가 드러납니다.
소스에서 실행까지
Kotlin 코드는 .kt 로 끝나는 소스 파일에 적습니다. 이 파일은 바로 실행되지 않습니다. kotlinc
라는 컴파일러가 먼저 소스를 클래스 파일로 바꿉니다.
클래스 파일 안에는 바이트코드가 들어 있습니다. 바이트코드는 실제 칩이 아니라 가상의 컴퓨터가 알아듣도록 정한 명령입니다. 그 가상의 컴퓨터를 JVM(Java Virtual Machine, 자바 가상 머신)이라고 부릅니다.
JVM 은 클래스 파일만 읽습니다. 그 파일을 만든 소스가 무슨 언어였는지는 보지 않습니다. 그래서 Kotlin 소스에서 나온 클래스 파일과 javac 가 Java 소스에서 만든 클래스 파일이 한 프로그램 안에 섞여 있어도 됩니다.
flowchart TD
K["Hello.kt · Kotlin 소스"] --> KC["kotlinc"]
J["Hello.java · Java 소스"] --> JC["javac"]
KC --> C["클래스 파일"]
JC --> C
C --> V["JVM"]
두 컴파일러는 받는 소스가 다르지만 내놓는 것은 같은 형식입니다. 이것이 Kotlin 의 상호 운용 바탕입니다. Kotlin 코드에서 Java 클래스를 부르고, Java 코드에서 Kotlin 클래스를 부르는 일이 여기서 나옵니다.
값이 없을 수 있는 타입
Java 에서는 객체를 가리키는 변수라면 무엇이든 null 일 수 있습니다. null 은 아무 객체도 가리키지 않는 상태입니다. 이런 변수로 메서드를 부르면 실행 도중에 NullPointerException 이 납니다.
Kotlin 은 그 가능성을 타입에 적게 합니다. String 타입 변수에는 null 을 넣을 수 없고, 물음표를
붙인 String? 에만 넣을 수 있습니다. 컴파일러는 이 둘을 다른 타입으로 봅니다.
var a: String = "hi"
a = null // 컴파일 오류
var b: String? = "hi"
b = null // 통과
값이 없을 수 있는 변수를 쓰려면 먼저 비었는지 확인해야 합니다. 확인하는 문장을 줄마다 적지 않아도 되게 물음표를 찍은 호출을 둡니다. 이것을 안전 호출이라고 부릅니다.
val b: String? = null
println(b?.length) // null
앞의 값이 null 이면 length 를 부르지 않고 그대로 null 을 내놓습니다. 비었을 때 쓸 값을 따로 정해
두고 싶으면 물음표와 쌍점을 붙인 엘비스 연산자를 씁니다.
val name: String? = null
println(name ?: "이름 없음") // 이름 없음
이 검사는 컴파일할 때 끝납니다. 실행 중에 따로 도는 검사가 없어서, 만들어진 클래스 파일은 Java 에서 만든 것과 다르지 않습니다.
코드를 짧게 만드는 문법
변수는 낱말 둘로 만듭니다. val 은 한 번 넣은 값을 다시 넣지 못하는 변수이고, var 은 다시 넣을
수 있는 변수입니다.
타입은 대개 적지 않아도 됩니다. 컴파일러가 오른쪽 값을 보고 타입을 정하기 때문입니다. 이렇게 값에서 타입을 알아내는 것을 타입 추론이라고 부릅니다.
val count = 3 // 정수 타입이 된다
val name = "kotlin" // 문자열 타입이다
println(name + count) // kotlin3
값만 담는 클래스에는 data 를 붙입니다. 그러면 값이 같은지 견주는 메서드, 내용을 글자로 찍는 메서드,
일부만 바꿔 새로 만드는 메서드를 컴파일러가 대신 만들어 줍니다. Java 에서 클래스마다 손으로 적던
것들입니다. 이런 클래스를 데이터 클래스라고 부릅니다.
data class User(val id: Long, val name: String)
val u1 = User(1, "eun")
val u2 = User(1, "eun")
println(u1 == u2) // true
println(u1) // User(id=1, name=eun)
Java 의 == 는 두 변수가 같은 객체를 가리키는지를 봅니다. Kotlin 의 == 는 값이 같은지를 봅니다.
그래서 위에서 u1 과 u2 는 서로 다른 객체인데도 true 가 나옵니다.
함수를 둘 곳도 덜 까다롭습니다. Java 는 모든 코드가 클래스 안에 들어가야 하지만, Kotlin 은 파일에 함수를 바로 적어도 됩니다. 컴파일러가 그 파일 이름을 딴 클래스를 만들어 그 안에 넣어 줍니다.
남의 클래스에 함수를 덧붙이는 확장 함수
확장 함수는 이미 만들어진 클래스에 메서드가 하나 더 있는 것처럼 함수를 붙이는 문법입니다. 그 클래스의 소스를 고치지 않고 붙입니다.
아래는 문자열의 첫 글자를 돌려주는 함수입니다. 문자열이 비어 있으면 붙임표를 대신 돌려줍니다.
fun String.firstOrDash(): Char =
if (isEmpty()) '-' else this[0]
println("kt".firstOrDash()) // k
println("".firstOrDash()) // -
붙인 함수가 그 클래스 안으로 들어가는 것은 아닙니다. 컴파일러는 이 함수를 그 문자열을 첫 인자로 받는 보통 함수로 바꿉니다. 그래서 클래스가 감춰 둔 내부 값에는 닿지 못합니다.
Java 에서는 같은 일을 도우미 클래스로 했습니다. 확장 함수가 바꾸는 것은 부르는 모양입니다.
StringUtils.firstOrDash(s) 가 s.firstOrDash() 가 됩니다.
여러 일을 겹쳐 돌리는 코루틴
한 프로그램 안에서 여러 일을 겹쳐 진행하는 것을 동시성이라고 합니다. 서버가 요청 여럿을 한꺼번에 처리하려면 동시성이 있어야 합니다.
보통은 스레드로 동시성을 얻습니다. 스레드는 운영체제가 번갈아 처리 시간을 나눠 주는 실행 흐름입니다. 다만 스레드 하나가 차지하는 메모리가 커서 수만 개를 띄우기 어렵습니다.
Kotlin 은 여기에 코루틴을 둡니다. 코루틴은 도중에 멈췄다가 나중에 그 지점부터 이어 도는 실행 단위입니다. 멈춰 있는 동안에는 스레드를 붙잡지 않아서, 적은 수의 스레드 위에 코루틴을 많이 얹을 수 있습니다.
멈출 수 있는 함수에는 suspend 를 붙입니다. 이 표시가 붙은 함수 안에서만 기다리는 문장을 쓸 수
있습니다.
suspend fun load(): String {
delay(100) // 스레드를 내준다
return "done"
}
delay 는 기다리는 동안 스레드를 내놓습니다. 그동안 같은 스레드가 다른 코루틴을 돌립니다. 스레드를
붙든 채 멈춰 세우는 Java 의 Thread.sleep 과 갈리는 대목입니다.
대신 suspend 는 부르는 쪽으로 번집니다. 멈출 수 있는 함수를 부르는 함수도 suspend 이거나 코루틴
안이어야 합니다. 그래서 한 곳에 코루틴을 들이면 그 호출 경로가 함께 바뀝니다.
Java 와 섞어 쓸 때 생기는 틈
Kotlin 의 null 검사는 Kotlin 코드 안에서만 완결됩니다. Java 메서드가 돌려준 값은 컴파일러가 비었는지 알 수 없습니다. Java 타입에는 그 표시가 없기 때문입니다.
이렇게 표시가 없는 채로 넘어온 타입을 플랫폼 타입이라고 부릅니다. 컴파일러는 플랫폼 타입을 검사하지 않고 지나갑니다. 물음표 없는 타입으로 받아 두었는데 실제로 null 이 오면 그 대입에서 오류가 납니다.
그래서 Java 를 부르는 경계에서는 받는 타입을 손으로 적습니다. 물음표를 붙여 받고 비었는지 확인하면 검사가 다시 이어집니다.
딸려 붙는 것도 하나 있습니다. Kotlin 코드는 Kotlin 표준 라이브러리에 기대어 돕니다. 문자열을 다루는 함수도 목록을 도는 함수도 거기서 옵니다. 그래서 배포물에 이 라이브러리를 같이 넣거나, 실행할 때 어디 있는지 알려 줘야 합니다.
무엇을 내주었나
위 다섯은 저마다 무언가를 내주고 얻은 것입니다. 한데 모으면 이렇습니다.
| 고른 것 | 얻는 것 | 내준 것 |
|---|---|---|
| 타입에 null 가능성을 적는다 | 실행 중에 나던 오류를 컴파일할 때 잡는다 | Java 에서 건너온 값에는 이 검사가 안 걸린다 |
| Java 와 같은 클래스 파일로 컴파일한다 | 기존 Java 라이브러리를 고치지 않고 쓴다 | JVM 을 켜야 하는 조건을 같이 떠안는다 |
| 문법 요소를 넉넉히 둔다 | 되풀이되는 코드가 줄어든다 | 읽는 사람이 익힐 문법이 늘어난다 |
| 표준 라이브러리를 따로 둔다 | Java 에 없는 함수를 쓴다 | 배포물에 라이브러리가 하나 더 붙는다 |
| 코루틴을 언어에 넣는다 | 동시 처리 코드를 순서대로 적는다 | suspend 가 부르는 쪽으로 번진다 |
그래서 Kotlin 은 JVM 위에 올리는 서버 애플리케이션과 Android 앱에 자주 고릅니다. Java 로 쌓아 둔 코드를 그대로 두고 새 코드만 Kotlin 으로 적는 것도 됩니다.
반대로 JVM 이 없는 환경이나, 팀이 Java 만 읽어 온 경우에는 값이 덜합니다. 앞의 경우에는 다른 갈래로 컴파일해야 하고, 뒤의 경우에는 문법을 새로 익히는 시간을 먼저 내야 합니다.
관련 항목
Kotlin 소스가 실행되기까지 거치는 구성 요소
kotlinc · 클래스 파일 · 바이트코드 · JVM · JIT 컴파일 · Kotlin 표준 라이브러리
Kotlin 언어를 이루는 문법 요소
타입 추론 · 데이터 클래스 · 확장 함수 · 널 안전성 · 안전 호출 · 엘비스 연산자 · 봉인 클래스 · 람다 표현식 · 제네릭
Kotlin 이 동시 처리에 쓰는 장치
코루틴 · Kotlin 코루틴 · 동시성 · 스레드 · 채널
Kotlin 이 Java 코드와 맞물릴 때 나오는 개념
상호 운용 · 플랫폼 타입 · null · NullPointerException · javac · 클래스패스
Kotlin 코드를 빌드하고 배포하는 도구
Gradle · Maven · Android Studio · IntelliJ IDEA
Kotlin 이 JVM 밖에서 도는 갈래
Kotlin 멀티플랫폼 · 네이티브 컴파일 · JavaScript · WebAssembly
Kotlin 으로 짓는 애플리케이션과 그 프레임워크
Android · Spring Boot · Ktor · Jetpack Compose · Gatling
서버 개발을 두고 Kotlin 과 겨루는 프로그래밍 언어
Java · Scala · Groovy · Go · Python · Swift
Kotlin 이 속하는 상위 분류
다른 이름: 코틀린 · Kotlin 언어 · 코틀린 언어