코틀린(Kotlin)은 Java 가상 머신(JVM) 위에서 돌아가며 100% 호환되는 간결하고 안전한 현대적인 언어입니다.
자바와 달리 최상위 레벨(Top-level) 함수를 지원하므로, 불필요한 클래스 선언 없이 바로 fun main() 함수만 작성하여 프로그램을 시작할 수 있습니다. 문장 끝의 세미콜론(;)도 필요 없습니다.
// 코틀린 프로그램의 진입점은 main 함수입니다.
fun main() {
println("Hello, Kotlin World!")
// 세미콜론이 없어도 됩니다.
// System.out.println 대신 간결한 println()을 사용합니다.
println("코틀린은 자바보다 간결합니다.")
}코틀린은 강력한 타입 추론(Type Inference)을 지원하여 자료형을 생략할 수 있습니다. 변수 선언은 크게 두 가지로 나뉩니다.
val(Value): 한 번 할당하면 바꿀 수 없는 불변(Immutable) 변수입니다. (Java의 final과 유사)var(Variable): 값을 자유롭게 변경할 수 있는 가변(Mutable) 변수입니다.
코틀린은 프로그램의 안전성을 위해 기본적으로 가급적이면 val을 사용하는 것을 강력히 권장합니다.
fun main() {
// val: 불변 변수 (Read-only)
// 우측의 값을 보고 자동으로 Int 타입으로 추론합니다.
val popcormPrice = 5000
// popcormPrice = 6000 // 에러! val은 값을 변경할 수 없습니다.
// var: 가변 변수 (Mutable)
var ticketCount = 2
println("현재 티켓 수: " + ticketCount)
ticketCount = 3 // 값 변경 가능
println("변경 후 티켓 수: " + ticketCount)
// 명시적으로 타입을 선언할 수도 있습니다 (변수명: 타입)
val movieName: String = "인터스텔라"
val rating: Double = 9.8
// 문자열 템플릿($)을 사용하면 Java의 + 연산자나 String.format보다 훨씬 편하게 문자열을 조합할 수 있습니다.
println("영화명: $movieName, 평점: $rating, 티켓 수: $ticketCount")
}코틀린의 컬렉션은 자바와 달리 불변(Read-only)과 가변(Mutable)이 엄격하게 분리되어 있습니다.
listOf(), setOf(), mapOf()로 만든 컬렉션은 값을 추가하거나 삭제할 수 없습니다. 요소를 수정하려면 반드시 mutableListOf() 처럼 mutable이 붙은 함수를 사용해야 합니다. 이는 개발자의 실수로 데이터가 변경되는 버그를 원천 차단합니다.
fun main() {
// 1. List (순서 보장, 중복 허용)
// 읽기 전용(Immutable) 리스트
val readOnlyList = listOf("Apple", "Banana", "Cherry")
println("읽기 전용: " + readOnlyList)
// readOnlyList.add("Orange") // 에러! add 메서드가 없습니다.
// 가변(Mutable) 리스트
val mutableList = mutableListOf("Apple", "Banana")
mutableList.add("Cherry")
println("수정 가능: " + mutableList)
// 2. Map (Key-Value)
// 'to' 라는 중위 연산자(Infix function)를 사용하여 직관적으로 키-값 쌍을 만듭니다.
val menuPrices = mutableMapOf(
"아메리카노" to 4000,
"카페라떼" to 4500
)
// 값 추가 및 맵 접근 (배열처럼 대괄호 사용 가능!)
menuPrices["바닐라라떼"] = 5000
println("아메리카노 가격: " + menuPrices["아메리카노"])
}코틀린에서 if와 when은 문장(Statement)이 아니라 표현식(Expression)으로 사용할 수 있습니다. 즉, 조건문의 결과를 변수에 바로 대입할 수 있습니다.
또한 자바의 switch문은 코틀린에서 when으로 진화했습니다. break가 필요 없고, 다양한 조건을 유연하게 검사할 수 있어 가독성이 압도적으로 좋습니다.
fun main() {
// 1. 표현식으로서의 if
val a = 10
val b = 20
// if의 결과(마지막 줄)가 그대로 변수에 대입됩니다! (삼항 연산자 역할)
val max = if (a > b) {
println("a가 큽니다.")
a
} else {
println("b가 크거나 같습니다.")
b
}
println("최대값: $max")
// 2. 강력한 when 표현식 (switch 대체)
val score = 85
val grade = when {
score >= 90 -> "A" // 조건 -> 결과
score >= 80 -> "B"
score >= 70 -> "C"
else -> "F" // 모든 경우를 커버하지 않으면 컴파일 에러 발생(안전함)
}
println("당신의 학점은 $grade 입니다.")
// 값 매칭도 쉽게 가능합니다.
val obj: Any = "Hello"
when (obj) {
1 -> println("숫자 1입니다.")
"Hello" -> println("인삿말입니다.")
is String -> println("문자열 타입입니다.")
else -> println("기타 등등")
}
}코틀린에서 함수는 fun 키워드로 선언합니다. 자바와 달리 파라미터에 기본값(Default arguments)을 지정할 수 있어 메서드 오버로딩(Overloading)을 여러 개 만들 필요가 없습니다.
또한 함수 본문이 단일 표현식일 경우, 중괄호와 return을 생략하고 = 등호를 사용해 매우 짧게 줄일 수 있습니다.
// 1. 기본 함수 선언 (파라미터명: 타입): 반환타입
fun sum(a: Int, b: Int): Int {
return a + b
}
// 2. 단일 표현식 함수 (중괄호와 return 생략 가능, 반환타입 추론됨)
fun multiply(a: Int, b: Int) = a * b
// 3. 기본값을 갖는 매개변수 (Default Arguments)
// 자바처럼 인자가 1개인 메서드, 2개인 메서드를 따로 만들 필요가 없습니다.
fun greet(name: String, message: String = "안녕하세요") {
println("$name 님, $message")
}
fun main() {
println("합계: " + sum(10, 20))
println("곱셈: " + multiply(5, 4))
greet("홍길동") // message는 기본값 사용
// 4. 이름 붙인 인자 (Named Arguments)
// 인자의 순서를 무시하고 이름을 명시하여 전달할 수 있어 가독성이 높습니다.
greet(message = "반갑습니다", name = "이순신")
}코틀린은 클래스 선언과 동시에 주 생성자(Primary Constructor)를 정의하여 코드를 획기적으로 줄여줍니다.
클래스 이름 옆에 val이나 var를 붙여 파라미터를 적으면, 내부 필드 선언과 생성자 할당을 한 번에 끝낼 수 있습니다. 자바의 지루한 Getter/Setter도 내부적으로 자동 생성됩니다.
// 주 생성자를 이용한 초간단 클래스 선언
// 클래스 이름 옆의 괄호가 주 생성자 역할을 하며, val/var를 붙이면 프로퍼티(필드+getter)가 됩니다.
class Person(val name: String, var age: Int) {
// 인스턴스가 생성될 때 즉시 실행되는 초기화 블록
init {
println("새로운 사람(${name})이 생성되었습니다.")
}
// 멤버 함수
fun introduce() {
println("제 이름은 $name 이고 나이는 $age 입니다.")
}
}
fun main() {
// 코틀린에서는 객체 생성 시 new 키워드를 쓰지 않습니다!
val p1 = Person("Alice", 25)
p1.introduce()
// Getter/Setter 호출 대신, 마침표(.)로 직접 프로퍼티에 접근/수정합니다. (내부적으론 메서드 호출)
println("이름 읽기: " + p1.name)
p1.age = 26 // Setter 동작
println("나이 증가: " + p1.age)
}자바 개발자들을 가장 괴롭히는 오류는 바로 NullPointerException(NPE)입니다. 코틀린은 아예 컴파일 단계에서 Null 가능성을 차단합니다.
기본적으로 모든 변수는 null을 가질 수 없습니다. 만약 null이 허용되어야 한다면 타입 뒤에 물음표(?)를 붙여야 하며, 이를 Nullable 타입이라고 부릅니다. 널러블 변수는 안전한 호출 연산자(?.)나 엘비스 연산자(?:)를 통해서만 안전하게 다룰 수 있습니다.
fun main() {
// 1. 기본 타입은 절대 null을 가질 수 없습니다.
var nonNullString: String = "Hello"
// nonNullString = null // 컴파일 에러!
// 2. Nullable 타입 (타입명 뒤에 ?)
var nullableString: String? = "Kotlin"
nullableString = null // 정상 작동
// 3. 안전한 호출 연산자 (?.)
// 변수가 null이면 뒤의 메서드를 실행하지 않고 null을 반환합니다.
val length = nullableString?.length
println("문자열의 길이는? " + length) // 출력: null
// 4. 엘비스 연산자 (?:)
// 만약 왼쪽 값이 null이라면, 오른쪽의 대체값을 반환합니다.
val safeLength = nullableString?.length ?: 0
println("안전한 문자열의 길이는? " + safeLength) // 출력: 0
}코틀린에서는 내가 만들지 않은 기존 클래스(예: String, List, 외부 라이브러리 클래스)에도 마치 내가 직접 멤버 함수를 추가한 것처럼 새로운 함수를 갖다 붙일 수 있습니다. 이를 확장 함수(Extension Function)라고 합니다.
클래스의 원본 소스코드를 건드리지 않고도 기능을 쉽게 확장할 수 있어 코틀린 표준 라이브러리를 극도로 풍부하게 만든 핵심 기술입니다.
// String 클래스에 나만의 커스텀 함수(removeFirstLast)를 갖다 붙입니다.
// 수신 객체 타입(String)에 마침표를 찍고 함수명을 정의합니다.
// 함수 내부에서 'this'는 호출하는 대상 문자열 자체를 가리킵니다.
fun String.removeFirstLast(): String {
// 길이가 2보다 작으면 그냥 원본을 반환
if (this.length <= 2) return this
// 양 끝 문자를 자르고 반환
return this.substring(1, this.length - 1)
}
fun main() {
val myText = "Hello Kotlin"
// 원래 String 클래스에 있는 메서드처럼 자연스럽게 호출 가능!
val result = myText.removeFirstLast()
println("원본: " + myText)
println("확장 함수 적용 결과: " + result) // "ello Kotli"
}스코프 함수(let, run, with, apply, also)는 객체의 컨텍스트 내에서 특정 블록(스코프)의 코드를 실행하는 함수입니다.
객체의 이름을 반복해서 쓰지 않고 초기화나 체이닝 작업을 할 때 코드를 우아하게 묶어주는 역할을 합니다. 각각 반환값(람다의 결과 vs 객체 자신)과 수신 객체 접근 방식(it vs this)에 차이가 있습니다.
class Robot(var name: String, var battery: Int) {
fun status() = "이름: $name, 배터리: $battery%"
}
fun main() {
// 1. apply: 객체 생성과 동시에 초기화 셋팅을 할 때 가장 많이 쓰입니다. (자신을 반환)
// 블록 안에서는 'this'로 접근 (생략 가능)
val r1 = Robot("R2D2", 10).apply {
battery = 100 // this.battery = 100 과 동일
name = "Super R2D2"
}
println(r1.status())
// 2. let: null 체크(?.)와 함께 연계하여 값이 있을 때만 무언가 할 때 자주 쓰입니다. (블록의 마지막 줄 반환)
// 블록 안에서는 'it'으로 접근
var nullableName: String? = "Kotlin"
val lengthMessage = nullableName?.let {
println("문자열 $it 이(가) null이 아닙니다!")
"길이는 ${it.length} 입니다." // 이 값이 반환되어 lengthMessage에 들어감
}
println(lengthMessage)
}코틀린에서 함수는 일급 시민(First-class citizen)입니다. 즉, 함수를 변수처럼 저장하고, 다른 함수의 파라미터로 넘기고, 함수에서 리턴할 수 있습니다.
함수를 파라미터로 받거나 반환하는 함수를 고차 함수(Higher-Order Function)라고 하며, 넘겨주는 익명 함수 블록을 람다(Lambda)라고 부릅니다. 맨 마지막 파라미터가 함수라면 람다 블록({})을 소괄호 밖으로 빼낼 수 있는 트레일링 람다(Trailing Lambda) 문법을 제공하여 매우 세련된 코딩이 가능합니다.
// 1. 고차 함수 선언
// action 매개변수는 (Int, Int)를 받아서 Int를 반환하는 '함수 타입'입니다.
fun calculate(a: Int, b: Int, action: (Int, Int) -> Int): Int {
val result = action(a, b) // 전달받은 함수(람다)를 여기서 실행!
return result
}
fun main() {
// 2. 람다식 사용 (변수에 함수를 저장)
val sumLambda: (Int, Int) -> Int = { x, y -> x + y }
println("람다 변수 테스트: " + calculate(10, 5, sumLambda))
// 3. 트레일링 람다 (Trailing Lambda)
// 함수의 마지막 인자가 함수라면, 소괄호 밖으로 중괄호를 뺄 수 있습니다. (코틀린의 핵심 매력)
val multiResult = calculate(10, 5) { x, y ->
x * y // 블록의 마지막 줄이 반환값이 됨
}
println("트레일링 람다(곱셈): " + multiResult)
}코틀린의 클래스는 기본적으로 상속 불가(final) 상태입니다. 상속을 허용하려면 부모 클래스에 반드시 open 키워드를 붙여야 합니다.
인터페이스는 자바 8과 유사하게 본문이 구현된 디폴트 메서드를 가질 수 있으며, 프로퍼티(추상 변수) 선언도 가능합니다. 다중 상속의 이점을 누리면서 역할과 구현을 깔끔하게 분리할 수 있습니다.
// 1. 인터페이스 정의
interface Flyable {
val maxSpeed: Int // 인터페이스 안의 프로퍼티 (구현 클래스가 오버라이드 해야 함)
fun fly() // 추상 메서드
fun landing() { // 본문이 있는 디폴트 메서드
println("안전하게 착륙합니다.")
}
}
// 2. open 클래스 (상속 허용)
open class Animal(val name: String) {
open fun makeSound() {
println("$name 가 소리를 냅니다.")
}
}
// 3. 상속과 인터페이스 다중 구현
// Animal() 을 상속받고, Flyable 을 구현함
class Bird(name: String, override val maxSpeed: Int) : Animal(name), Flyable {
// 부모 클래스의 메서드 오버라이딩 (override 키워드 필수)
override fun makeSound() {
println("짹짹!")
}
// 인터페이스의 추상 메서드 오버라이딩
override fun fly() {
println("$name 가 최대 속도 $maxSpeed 로 하늘을 납니다!")
}
}
fun main() {
val sparrow = Bird("참새", 50)
sparrow.makeSound()
sparrow.fly()
sparrow.landing() // 디폴트 메서드 사용
}코틀린은 귀찮게 싱글톤 패턴(Singleton)을 손수 짤 필요가 없습니다. class 대신 object 키워드만 사용하면, 프로그램 전역에서 단 하나만 존재하는 객체가 즉시 생성됩니다.
또한 자바의 static 멤버(클래스 레벨 변수/메서드)가 코틀린에는 없습니다. 대신 클래스 내부에 companion object를 두어 자바의 static과 완전히 동일한 역할을 하면서도 객체지향적인 유연함을 가져갑니다.
// 1. 싱글톤 선언 (object)
// DataBaseManager는 프로그램 전체에서 단 1개의 인스턴스만 보장됩니다.
object DataBaseManager {
var isConnected = false
fun connect() {
isConnected = true
println("데이터베이스에 연결되었습니다!")
}
}
// 2. 동반 객체 (Companion Object)
class User(val name: String) {
// 클래스 내부의 스태틱 영역 같은 곳입니다.
companion object {
val MAX_AGE = 120 // 공통 상수 (static final 역할)
// 팩토리 메서드 (static 메서드 역할)
fun createGuest(): User {
return User("Guest_001")
}
}
}
fun main() {
// object는 이름 그 자체로 바로 접근합니다. (생성자 호출 안함)
DataBaseManager.connect()
println("연결 상태: " + DataBaseManager.isConnected)
// companion object 멤버는 클래스 이름으로 직접 접근합니다.
println("사람의 최대 수명 한계: " + User.MAX_AGE)
val guest = User.createGuest()
println("환영합니다, " + guest.name)
}데이터만 담는 용도의 클래스(DTO 등)를 만들 때는 Data Class를 씁니다. 한 줄만 적으면 toString(), equals(), copy() 메서드가 전부 자동 생성되어 보일러플레이트 코드를 박멸합니다.
그 외에도 상태값을 열거하는 Enum Class, 상속 가능한 자식 클래스의 종류를 제한하여 when 식에서 완벽한 처리를 보장하는 Sealed Class가 있습니다.
// 1. 데이터 클래스 (Data Class)
// 데이터 보관 목적. equals, hashCode, toString, copy 가 자동 생성됨.
data class UserDto(val id: Int, val username: String, var email: String)
// 2. 봉인 클래스 (Sealed Class)
// 상속받는 자식들을 동일한 파일 내로 강제 제한하여, 종류를 한정 짓습니다.
sealed class NetworkResult {
data class Success(val data: String) : NetworkResult()
data class Error(val exceptionMsg: String) : NetworkResult()
object Loading : NetworkResult() // 상태만 나타내면 object 사용 가능
}
fun main() {
// Data Class 활용
val user1 = UserDto(1, "alice", "alice@test.com")
val user2 = UserDto(1, "alice", "alice@test.com")
// toString()이 자동으로 오버라이딩 되어 예쁘게 출력됨
println(user1)
// equals()가 내부 값들을 비교하도록 오버라이딩 되어 있음 (주소 비교 아님)
println("두 유저가 같은가? " + (user1 == user2)) // true
// copy() 메서드로 일부 값만 바꾸면서 불변 객체 복제
val user3 = user1.copy(id = 2, username = "bob")
println(user3)
// Sealed Class와 when의 찰떡 궁합
// Result의 자식이 3개뿐이라는 것을 컴파일러가 알아서, else가 필요 없음!
val response: NetworkResult = NetworkResult.Success("서버 응답 데이터")
when (response) {
is NetworkResult.Success -> println("성공: " + response.data)
is NetworkResult.Error -> println("실패: " + response.exceptionMsg)
NetworkResult.Loading -> println("로딩 중...")
}
}코틀린은 필드(Field) 대신 프로퍼티(Property)를 사용합니다. 변수처럼 보이지만, 내부적으로는 get()과 set() 메서드가 암묵적으로 호출됩니다. 커스텀 게터를 만들어 값을 동적으로 계산할 수도 있습니다.
또한 안드로이드 환경이나 의존성 주입 시 "변수를 나중에 초기화해야 할 때" 널러블(?)을 피하기 위해 lateinit(나중에 초기화)이나 lazy(처음 불릴 때 생성)를 유용하게 활용합니다.
class Rectangle(var width: Int, var height: Int) {
// 1. 커스텀 Getter (변수처럼 접근하지만 사실은 내부적으로 계산 함수가 실행됨)
val isSquare: Boolean
get() = (width == height)
val area: Int
get() = width * height
}
class Profile {
// 2. lateinit: var 에만 사용. 나중에 초기화할 것임을 약속함. (null 방지)
lateinit var nickname: String
// 3. lazy: val 에만 사용. 해당 변수가 "처음" 불릴 때 중괄호 안의 코드가 실행됨.
val heavyData: String by lazy {
println("=> 복잡하고 오래 걸리는 데이터 로딩 중...")
"거대한 용량의 데이터셋"
}
}
fun main() {
val rect = Rectangle(10, 10)
println("정사각형인가? " + rect.isSquare) // true
println("면적: " + rect.area) // 100
val p = Profile()
// println(p.nickname) // 초기화 전 호출 시 UninitializedPropertyAccessException 에러!
p.nickname = "코틀린러버"
println("설정된 닉네임: " + p.nickname)
println("lazy 데이터는 아직 로딩 안 됨.")
println("1차 호출: " + p.heavyData) // 이 때 로딩 로그가 찍힘
println("2차 호출: " + p.heavyData) // 이미 로딩된 값을 반환 (로그 안 찍힘)
}코틀린 컴파일러는 매우 똑똑합니다. 변수가 특정 타입인지 is 연산자로 검사하거나, null이 아님을 if문으로 체크했다면, 이후 블록에서는 자동으로 해당 타입(또는 Non-Null)으로 형변환을 적용해 줍니다. 이를 스마트 캐스트(Smart Cast)라고 합니다.
반면, !! 연산자(Not-null Assertion)는 "이건 절대 null이 아니니까 무조건 통과시켜!" 라고 컴파일러에게 강제하는 위험한 연산자입니다. 런타임 시 NPE가 터질 수 있어 사용을 지양해야 합니다.
fun printLength(obj: Any) {
// obj는 최상위 타입인 Any이므로 length 프로퍼티가 없습니다.
// 1. 스마트 캐스트 (is 연산자)
if (obj is String) {
// 이 중괄호 안에서는 컴파일러가 obj를 String으로 자동 인식합니다. (캐스팅 문법 필요 없음)
println("문자열 길이: " + obj.length)
} else {
println("문자열이 아닙니다.")
}
}
fun printNotNull(str: String?) {
// str은 Nullable이므로 곧바로 length를 쓸 수 없음
// null이 아님을 검사하면, 블록 내부에서는 자동으로 Non-Null (String)로 스마트 캐스트 됨
if (str != null) {
println("안전한 길이: " + str.length)
}
}
fun main() {
printLength("안녕하세요")
printLength(12345)
printNotNull("코틀린")
// 2. 단언 연산자 (!!) - 극도로 주의!
var danger: String? = "값"
// "이건 null이 절대 아니니까 검사 없이 그냥 까!" 라고 강제합니다.
println(danger!!.length)
// 만약 null이었다면? -> NullPointerException 폭발 (앱 크래시 발생)
}코틀린 표준 라이브러리는 자바 컬렉션 위에서 동작하지만, 어마어마하게 유용한 함수들을 제공합니다.
map, filter뿐만 아니라 groupBy, associate, zip 등 데이터를 파싱하고 정렬하고 조작하는 수십 가지의 컬렉션 확장 함수들을 지원하여 복잡한 로직을 단 한 줄로 끝낼 수 있게 돕습니다.
data class User(val name: String, val age: Int, val city: String)
fun main() {
val users = listOf(
User("Alice", 25, "Seoul"),
User("Bob", 30, "Busan"),
User("Charlie", 25, "Seoul"),
User("David", 40, "Jeju")
)
// 1. filter 와 map
// 나이가 30 이상인 사람들의 이름만 추출
val over30Names = users.filter { it.age >= 30 }.map { it.name }
println("30세 이상 이름: " + over30Names)
// 2. groupBy (SQL의 GROUP BY와 완벽히 동일)
// 도시(city)를 기준으로 유저들을 그룹화한 Map을 반환합니다.
val usersByCity: Map<String, List<User>> = users.groupBy { it.city }
println("Seoul 유저 수: " + usersByCity["Seoul"]?.size + "명")
// 3. maxByOrNull, minByOrNull
// 특정 조건을 기준으로 최댓값/최솟값을 가지는 객체를 찾습니다.
val oldest = users.maxByOrNull { it.age }
println("가장 나이 많은 사람: " + oldest?.name)
// 4. any, all, none (조건 검사 Boolean)
val hasSeoul = users.any { it.city == "Seoul" } // 서울 사는 사람이 하나라도 있나?
val allAdults = users.all { it.age >= 18 } // 모두가 성인인가?
println("서울 거주자 존재? " + hasSeoul)
println("모두 성인인가? " + allAdults)
}