Kotlin이란?
Kotlin은 JetBrains에서 개발한 JVM 기반 프로그래밍 언어입니다. Java와 100% 상호 운용이 가능하면서도 간결한 문법, null 안전성, 함수형 프로그래밍 지원 등 현대적인 기능을 제공합니다. Google이 Android 개발의 공식 언어로 채택한 이후 빠르게 성장하고 있습니다.
이 글에서는 Java 개발자가 Kotlin으로 전환할 때 알아야 할 핵심 문법 차이를 정리합니다.
변수 선언과 타입 추론
Kotlin은 val(불변)과 var(가변)로 변수를 선언합니다. 타입 추론이 강력하여 대부분의 경우 타입을 명시하지 않아도 됩니다.
fun main() {
// val — 불변 (Java의 final)
val name = "홍길동" // String으로 추론
val age: Int = 25 // 명시적 타입 선언
// name = "김영희" // 컴파일 에러! val은 재할당 불가
// var — 가변
var score = 85
score = 92 // 재할당 가능
println("$name ($age세), 점수: $score")
// 실행 결과: 홍길동 (25세), 점수: 92
// 문자열 템플릿 — ${표현식}
val items = listOf("사과", "바나나", "포도")
println("과일 ${items.size}종: ${items.joinToString(", ")}")
// 실행 결과: 과일 3종: 사과, 바나나, 포도
// 타입 변환 — Java와 달리 명시적 변환 필수
val intValue = 42
val longValue: Long = intValue.toLong() // 자동 변환 없음
val doubleValue: Double = intValue.toDouble()
println("Int: $intValue, Long: $longValue, Double: $doubleValue")
// 실행 결과: Int: 42, Long: 42, Double: 42.0
}
Kotlin에서는 val을 기본으로 사용하고, 재할당이 꼭 필요한 경우에만 var을 사용하는 것이 권장됩니다.
Null 안전성
Kotlin의 가장 중요한 특징은 타입 시스템 수준의 null 안전성입니다. 기본적으로 모든 타입은 null을 허용하지 않으며, null이 가능한 타입은 ?를 붙여 명시합니다.
fun main() {
// 기본 타입은 null 불가
var name: String = "홍길동"
// name = null // 컴파일 에러!
// nullable 타입은 ?를 붙임
var nickname: String? = "길동이"
nickname = null // 허용
// 안전 호출 연산자 (?.) — null이면 null 반환
println("닉네임 길이: ${nickname?.length}")
// 실행 결과: 닉네임 길이: null
// 엘비스 연산자 (?:) — null일 때 기본값
val displayName = nickname ?: "이름 없음"
println("표시명: $displayName")
// 실행 결과: 표시명: 이름 없음
// 안전 호출 체이닝
val city: String? = getUser()?.address?.city
println("도시: ${city ?: "알 수 없음"}")
// 실행 결과: 도시: 알 수 없음
// let — null이 아닐 때만 블록 실행
val email: String? = "user@example.com"
email?.let {
println("이메일 발송: $it")
// 실행 결과: 이메일 발송: user@example.com
}
// 비강제 단언 (!!) — NullPointerException 가능, 사용 자제
// val length = nickname!!.length // NPE 발생!
}
// 예시용 함수
data class Address(val city: String?)
data class User(val name: String, val address: Address?)
fun getUser(): User? = null
!! 연산자는 null이면 NullPointerException을 발생시키므로 가능한 사용을 피하고, ?.과 ?:를 조합하여 안전하게 처리합니다.
데이터 클래스와 when 표현식
Kotlin의 data class는 equals(), hashCode(), toString(), copy()를 자동 생성합니다. Java에서 수십 줄이 필요한 DTO/VO 클래스를 한 줄로 정의할 수 있습니다.
// 데이터 클래스 — equals, hashCode, toString, copy 자동 생성
data class Product(
val name: String,
val price: Int,
val category: String = "일반" // 기본값 파라미터
)
// sealed class — 제한된 계층 구조
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
data object Loading : Result()
}
fun handleResult(result: Result): String {
// when — Java switch의 강화판 (표현식으로 사용 가능)
return when (result) {
is Result.Success -> "성공: ${result.data}"
is Result.Error -> "에러 [${result.code}]: ${result.message}"
is Result.Loading -> "로딩 중..."
// sealed class이므로 else 불필요 (모든 케이스 커버)
}
}
fun main() {
// 데이터 클래스 사용
val product1 = Product("키보드", 89000)
val product2 = Product("키보드", 89000)
println(product1) // toString 자동 생성
// 실행 결과: Product(name=키보드, price=89000, category=일반)
println("동등성: ${product1 == product2}") // equals 자동 생성
// 실행 결과: 동등성: true
// copy로 일부 필드만 변경한 복사본 생성
val discounted = product1.copy(price = 79000)
println("할인: $discounted")
// 실행 결과: 할인: Product(name=키보드, price=79000, category=일반)
// 구조 분해 선언
val (name, price, category) = product1
println("$name — ${price}원 ($category)")
// 실행 결과: 키보드 — 89000원 (일반)
// when 표현식
val results = listOf(
Result.Success("데이터 로드 완료"),
Result.Error("네트워크 오류", 500),
Result.Loading
)
results.forEach { println(handleResult(it)) }
// 실행 결과:
// 성공: 데이터 로드 완료
// 에러 [500]: 네트워크 오류
// 로딩 중...
}
확장 함수
확장 함수는 기존 클래스를 수정하지 않고 새 함수를 추가하는 기능입니다. Java의 유틸리티 클래스를 대체하며, 코드의 가독성을 높입니다.
// String에 확장 함수 추가
fun String.toSlug(): String {
return this.lowercase()
.replace(Regex("[^a-z0-9가-힣\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
}
// List에 확장 함수 추가
fun <T> List<T>.secondOrNull(): T? {
return if (size >= 2) this[1] else null
}
// Int에 확장 프로퍼티 추가
val Int.won: String
get() = "${String.format("%,d", this)}원"
fun main() {
// 확장 함수 사용 — 마치 원래 메서드처럼
val title = "Hello World! Kotlin 가이드"
println(title.toSlug())
// 실행 결과: hello-world-kotlin-가이드
val items = listOf("첫째", "둘째", "셋째")
println("두 번째: ${items.secondOrNull()}")
// 실행 결과: 두 번째: 둘째
val emptyList = emptyList<String>()
println("두 번째: ${emptyList.secondOrNull()}")
// 실행 결과: 두 번째: null
// 확장 프로퍼티 사용
println(89000.won)
// 실행 결과: 89,000원
println(1500000.won)
// 실행 결과: 1,500,000원
}
정리
Kotlin의 핵심 장점을 Java와 비교하면 다음과 같습니다.
- Null 안전성:
?,?.,?:연산자로 NPE를 컴파일 타임에 방지합니다. - 간결함:
data class한 줄로 DTO를 정의하고, 타입 추론으로 보일러플레이트를 줄입니다. - when 표현식:
switch보다 강력하며,sealed class와 조합하면 패턴 매칭처럼 사용할 수 있습니다. - 확장 함수: 기존 클래스에 메서드를 추가하여 유틸리티 클래스를 제거합니다.
- 기본값 파라미터: Java의 메서드 오버로딩 대신 기본값으로 처리합니다.
기존 Java 프로젝트에 Kotlin을 점진적으로 도입할 수 있으므로, 새 파일부터 Kotlin으로 작성하는 것을 권장합니다.