Kotlin in Action 2/e 서적을 읽으면서 정리하고자 쓴 글입니다.

왜 JetBrains는 Kotlin을 만들었는가
JetBrains는 IntelliJ IDEA를 만든 회사로 내부 서비스와 툴 대부분 Java로 이루어져 있으며 다음과 같이 불편이 누적되었습니다.
- Java의 장황한 문법 (Boilerplate)
- 안전하지 않는 Null 처리
- 더 생산적이고 간결한 코드에 대한 실용적인 요구 증가 (모던한 방식)
C#처럼 모던한 방식을 원했고, 기존 Java로 이뤄진 코드를 호환할 수 있는 언어를 찾아보았으나 그러한 언어가 없었습니다. 그렇게 JetBrains는 Java와 호환성이 좋고, 모던함을 갖추고자 실용적인 언어 Kotlin을 만들기로 결정합니다.
Kotlin은 코틀린 개발 팀이 대부분 살고 있는 러시아의 상트페테르부르크 근처에 있는 섬이름을 따서 만든 이름입니다. Java나 Ceylon 같이 언어 기원의 전통을 따른 것인데, 고향에 가까운 섬의 이름을 따왔습니다.
Kotlin이 무엇이고, 왜 써야하는가
Java보다 더 모던한 방식이며서 Java로 만든 라이브러리, 프레임워크, SDK를 이용할 수 있는 언어가 필요하였고, 위 조건을 충족할 수 있는 목표로 만들어진 언어가 Kotlin입니다. 아래는 Java 코드를 Kotlin으로 변환하면 어떻게 되는지 알 수 있습니다.
Java 코드
public class Main {
public static void main(String[] args) {
List<Person> persons = List.of(
new Person("owen"),
new Person("bella", 29)
);
Person oldPerson = persons.stream()
.max(Comparator.comparingInt(p -> p.getAge() != null ? p.getAge() : 0))
.orElse(null);
System.out.println("나이가 가장 많은 사람: " + oldPerson);
}
}
class Person {
private String name;
private Integer age; // nullable
public Person(String name) {
this(name, null);
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
@Override
public String toString() {
return "Person(name=" + name + ", age=" + age + ")";
}
}
Kotlin 코드
fun main() {
val persons = listOf(
Person("owen"),
Person("bella", 29)
)
val oldPerson = persons.maxBy { it.age ?: 0 }
println("나이가 가장 많은 사람: $oldPerson")
}
data class Person(val name: String, val age: Int? = null)
40줄이 넘는 코드가 10줄 정도로 줄어드는 마술을 보면 왜 Kotlin을 써야 하는지 알 수 있습니다.
Kotlin은 다음과 같은 특성들이 있습니다.
1) 활용 플랫폼
Kotlin은 JVM 위에 동작하는 언어지만 여러 플랫폼에서 사용할 수 있습니다.
JVM 기반 활용
Spring Boot, Android 등 Java가 동작하는 곳이라면 그대로 Koltin을 사용할 수 있습니다.
멀티 플랫폼 지원
Koltin/JS → React, Node.js 환경에서 동작
Kotlin/Native → IOS, Windows, macOS, Linux에서 실행 가능
Kotlin Multiplatform → 비즈니스 로직 공유가 가능하여 mobile/web/back-end 공통 로직을 하나로 유지 가능
2) 정적 타입 언어 (+ 타입 추론)
Kotlin은 정적 언어이기 때문에 안정성을 확보하면서도 타입 추론을 지원해 코드가 간결합니다.
타입 추론
타입을 직접 쓰지 않아도 Kotlin은 타입을 추론합니다. 하지만 추론이 과도하면 상위 타입이 강제로 할당되거나 의도한 타입이 모호해지는 경우가 있을 수 있어 주의가 필요합니다.
val age = 20 // Int 추론
val name = "Kim" // String 추론
Unit 타입 (Java의 void와 차이)
Kotlin에서는 반환값을 명시하지 않은 함수도 내부적으로는 항상 `Unit`타입을 반환합니다. 이 `Unit`은 Java의 `void`처럼 “아무것도 없다”는 의미가 아니라, 하나의 값만 존재하는 정상적인 타입입니다. 개념적으로는 Java의 `void`보다는 `Void` 클래스에 가깝기 때문에 값처럼 전달하거나 파라미터로 사용할 수 있습니다.
// Kotlin에서는 반환값이 없는 함수도 "Unit" 타입을 반환합니다.
// 즉, 이 함수의 실제 시그니처는 fun test1(): Unit 입니다.
fun test1() {
println("Hello")
}
// Unit을 파라미터 타입으로 받는 함수 정의가 가능합니다.
fun test2(value: Unit) {
value // Unit은 실제로 객체처럼 전달이 가능하다.
}
// test1()을 호출하면 println은 수행되지만, 반환값으로 Unit 인스턴스가 반환됩니다.
// 이 Unit 값이 test2의 파라미터로 정상 전달됩니다.
fun main() {
test2(test1())
}
3) 객체 지향(OOP) + 함수형 프로그래밍(FP)을 모두 갖춘 멀티 패러다임
Kotlin은 객체지향과 함수형 두 접근 방식을 상황에 따라 혼합할 수 있습니다.
일급 함수
함수가 다른 일반적인 값(변수, 객체 등)과 동일한 자격을 갖는 것을 의미합니다.
// 1. 함수 정의 (A: 일반 함수)
val sum: (Int, Int) -> Int = { a, b -> a + b }
// 2. 함수 타입 정의 및 변수에 함수 할당 (Function Reference 사용)
// ::sum 은 sum 함수 자체를 참조(Reference)하는 것을 의미합니다.
// (Int, Int) -> Int 는 이 함수의 타입(signature)을 의미합니다.
val myOperation: (Int, Int) -> Int = ::sum
// 3. 변수에 할당된 함수 실행
val result = myOperation(10, 5)
println("결과: $result") // 결과: 15
// 4. 함수를 리스트에 저장
val functionList = listOf(::sum)
고차 함수
다른 함수를 인자로 받거나 함수를 결과로 반환하는 함수를 말합니다. 일급 함수 개념이 있기 때문에 고차 함수를 만들 수 있습니다.
fun operate(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)
val result = operate(3, 5) { x, y -> x + y } // Kotlin은 람다 함수를 뒤로 뺄 수 있다.
println("결과: $finalResult") // 결과: 8
선언형 프로그래밍
Java가 `stream()`을 통해 컬렉션 파이프라인을 구축하는 반면, Kotlin은 컬렉션 API의 확장 함수들을 바로 연결하여 데이터의 변환 및 필터링 흐름을 명령형 코드보다 더욱 직관적인 파이프라인 구조로 설계할 수 있습니다.
// Java21
var names = List.of("A", "B", "C")
var result = names.stream()
.map(String::toLowerCase)
.filter(s -> s.startWith("a"))
.toList();
System.out.println(result); // [a]
// Kotlin
val names = listOf("A", "B", "C")
val result = names
.map { it.lowerCase() }
.filter { it.startsWith("a") }
println(result) // [a]
4) 안드로이드 프로그래밍
Kotlin은 Android 공식 언어로 선정되었으며, 많은 최적화가 적용되어 있습니다.
Jetpack Compose
Google의 최신 UI 프레임워크인 Jetpack Compose는 Kotlin API를 활용하여 선언 UI를 구축하는 대표 사례입니다. 이는 Kotlin의 DSL(Domain-Spectofoc Language)적인 특성을 살린 모던한 개발 방식입니다.
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name")
}
Kotlin과 Java와 비교했을 때 성능 차이가 없다.
Kotlin으로 작성된 애플리케이션은 최종적으로 JVM 바이트코드로 컴파일되므로, 기존 Java와 성능 차이가 거의 없습니다. 또한 런타임 시스템의 용량이 작기 때문에 패키지(APK/AAB) 크기에 미치는 영향이 미미합니다.
Kotlin의 고차 함수는 인자로 받은 람다를 `inline`처리합니다. (함수에 함수를 호출하지만 마치 하나의 함수로 호출되는 마술) 이를 통해 람다를 호출할 때 새로운 객체가 생성되는 것을 방지합니다. 이는 GC 활동을 줄여 성능의 효율을 높일 수 이습니다.
5) Kotlin의 철학: 실용성 · 간결성 · 안정성 · 상호 운용성
실용성 (Practicality)
- 학술적 언어가 아닌 실전 문제를 해결하기 위한 언어
- IntelliJ 기반이라 IDE 지원이 매우 뛰어남 (자동 변환, 리팩토링, 빠른 인스펙션)
간결성 (Conciseness)
getter/setter, 생성자, 데이터 객체 등 모두 간결하게 작성할 수 있습니다.
// Java
class Person {
private String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
public String setName(String name) { this.name = name; }
}
// Kotlin: data class로 선언하면 기본적으로 getter/setter를 생성해준다.
data class Person(val name: String)
fun main() {
val person = Person("hun")
println("이름은 ${person.name}입니다.") // 이름은 hun입니다.
}
안정성 (Safety)
다양한 문법적 안전장치(Null 안정성, 스마트 캐스트 등)를 제공합니다.
// 1. Non-nullable Type: 변수를 선언할 때 기본적으로 `null`값을 허용하지 않습니다.
var name: String = "hun"
name = null // 컴파일 오류 발생!!
// 2. Nullable Type: 변수에 `null`을 저장하려면 타입 뒤에 물음표(?)를 명시적으로 붙여야 합니다.
var age: Int? = 20
name = null // 허용
// 3. 안전 호출 연산자(?.): `null` 아닌 경우 메서드나 프로퍼티 호출을 보장합니다.
val length = name?.toString()?.length // age가 null이면 전체 결과는 null
// 4. elvis 연산자(?:): 안전 호출 결과가 `null`인 경우 대신 사용할 값을 지정합니다.
val safeLength = age?.toString()?.length ?: 0 // age가 null이면 0 사용
// 5. 스마트 캐스트 (Smart Casts): 타입 검사 후 명시적 형 변환 없이 바로 해당 타입을 사용할 수 있습니다.
fun printLength(x: Any) { // Any: Java의 Object 클래스처럼 Kotlin의 최상위 타입이다.
if (x is String) { // x instanceOf String
print(x.length) // 타입 검사가 유효하다면 x는 Any -> String으로 캐스팅된다.
}
}
// 6. 예외 안전 처리 runCatching 함수: try-catch를 함수형 스타일로 사용할 수 있는 표준 라이브러리 함수
// 성공하면 결과를, 예외가 발생하면 "fallback value"를 반환
val result = runCatching {
riskOp(); // 예외가 발생할 수 있는 함수
}.getOrDefault("fallback value")
상호 운용성 (Interoperability)
프로젝트 내 Java와 Kotlin으로 작성된 클래스가 각각 있을 때 서로 의존해도 아무런 호환 문제가 없습니다.
단, Java에서 가져온 클래스를 Kotlin에서 사용할 때, Java는 기본적으로 `null` 가능성을 명시적으로 처리하지 않기 때문에 해당 타입은 코틀린에서 플랫폼 타입으로 인식됩니다. 플랫폼 타입은 `null` 허용 여부 정보가 없어 코틀린의 `null` 안정성 시스템을 보장받지 못하기 때문에 이를 주의해서 사용해야 합니다.
Kotlin Build 과정

1. 소스 코드 작성 (.kt 파일)
- 입력: 개발자가 작성한 Kotlin 소스 파일 (.kt)
2. Kotlin Compiler (kotlinc)
- 역할: 코틀린 컴파일러(kotlinc)는 `.kt` 파일을 JVM이 이해할 수 있는 형태로 변환합니다.
- 출력: 표준 자바 클래스 파일 포맷인 `.class` 파일(바이트코드)이 생성됩니다.
- 상호 운용성 핵심: 이 바이트코드는 자바 컴파일러가 생성한 바이트코드와 완벽히 호환됩니다. 따라서 코틀린과 자바 코드가 서로를 호출할 수 있으며, 기존 자바 라이브러리를 그대로 사용할 수 있습니다.
3. 패키징 및 최종 아티팩트 생성
- JVM (back-end/desktop): `.class` 파일들을 모아 `META-INF/services` 등의 메타 데이터와 함께 압축 → `.jar` (Java Archive)
4. 애플리케이션 실행 환경 및 런타임 의존성
최종 아티팩트가 실행될 때, 해당 애플리케이션은 코틀린 런타임(Kotlin Runtime) 라이브러리에 의존합니다.
- Kotlin Runtime: 코틀린의 표준 라이브러리(kotlin-stdlib.jar)를 의미합니다. 이 라이브러리는 확장 함수, 기본 함수, 코틀린만의 컬렉션 인터페이스 등 코틀린 언어의 고유한 기능을 지원하는 데 필요한 클래스들을 포함하고 있습니다.
- 의존성: 이 런타임 라이브러리는 최종 `.jar`에 포함되어 배포되어야 애플리케이션이 실행될 수 있습니다.
JVM 바이트코드 사용: 코틀린은 자바와 동일한 바이트코드를 사용하므로, 코틀린 파일과 자바 파일은 한 프로젝트 내에서 함께 컴파일되고 실행될 수 있습니다.
Kotlin Runtime 포함: 코틀린 고유의 문법을 지원하기 위해 소형의 표준 라이브러리(런타임)가 최종 애플리케이션에 포함됩니다.
'Kotlin' 카테고리의 다른 글
| [Kotlin in action 2/e] 코틀린 타입 시스템 (0) | 2026.01.07 |
|---|---|
| [Kotlin in action 2/e] 람다로 프로그래밍 (0) | 2025.12.31 |
| [Kotlin in Action 2/e] 클래스, 객체, 인터페이스 (0) | 2025.12.30 |
| [Kotlin in Action 2/e] 함수 정의와 호출 (1) | 2025.12.30 |
| [Kotlin in Action 2/e] 코틀린의 기초 (0) | 2025.12.12 |