minstudio

1. Hello World와 프로그램 구조

코틀린(Kotlin)은 Java 가상 머신(JVM) 위에서 돌아가며 100% 호환되는 간결하고 안전한 현대적인 언어입니다.

자바와 달리 최상위 레벨(Top-level) 함수를 지원하므로, 불필요한 클래스 선언 없이 바로 fun main() 함수만 작성하여 프로그램을 시작할 수 있습니다. 문장 끝의 세미콜론(;)도 필요 없습니다.

🚀 코틀린 Hello World의 간결함 기존 Java 방식 public class Main { public static void main(String[] args) { System.out.println("Hello"); ; } } 모던 Kotlin 방식 fun main() { println("Hello") }
// 코틀린 프로그램의 진입점은 main 함수입니다.
fun main() {
    println("Hello, Kotlin World!")
    
    // 세미콜론이 없어도 됩니다.
    // System.out.println 대신 간결한 println()을 사용합니다.
    println("코틀린은 자바보다 간결합니다.")
}
2. 변수와 기본 타입 (val vs var)

코틀린은 강력한 타입 추론(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")
}
3. 컬렉션 (List, Set, Map)

코틀린의 컬렉션은 자바와 달리 불변(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["아메리카노"])
}
4. 조건문과 제어 흐름 (if, when)

코틀린에서 ifwhen은 문장(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("기타 등등")
    }
}
5. 함수 (Functions)

코틀린에서 함수는 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 = "이순신")
}
6. 클래스와 인스턴스 (Classes)

코틀린은 클래스 선언과 동시에 주 생성자(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)
}
7. 널 안전성 기초 (Null Safety)

자바 개발자들을 가장 괴롭히는 오류는 바로 NullPointerException(NPE)입니다. 코틀린은 아예 컴파일 단계에서 Null 가능성을 차단합니다.

기본적으로 모든 변수는 null을 가질 수 없습니다. 만약 null이 허용되어야 한다면 타입 뒤에 물음표(?)를 붙여야 하며, 이를 Nullable 타입이라고 부릅니다. 널러블 변수는 안전한 호출 연산자(?.)나 엘비스 연산자(?:)를 통해서만 안전하게 다룰 수 있습니다.

🛡️ 엘비스 연산자 (?:) Nullable 변수 name ?: Default 값 "익명" name이 null이 아니면 name 반환, null이면 "익명" 반환
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
}
8. 확장 함수 (Extension Functions)

코틀린에서는 내가 만들지 않은 기존 클래스(예: 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"
}
9. 스코프 함수 (Scope Functions)

스코프 함수(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)
}
10. 고차 함수와 람다식 (Lambda & Higher-Order Functions)

코틀린에서 함수는 일급 시민(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)
}
11. 인터페이스와 상속 (Interfaces & Inheritance)

코틀린의 클래스는 기본적으로 상속 불가(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() // 디폴트 메서드 사용
}
12. 싱글톤과 동반 객체 (Object & Companion Object)

코틀린은 귀찮게 싱글톤 패턴(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)
}
13. 특수 목적 클래스 (Data, Enum, Sealed Class)

데이터만 담는 용도의 클래스(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("로딩 중...")
    }
}
14. 프로퍼티와 지연 초기화 (lateinit, lazy)

코틀린은 필드(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) // 이미 로딩된 값을 반환 (로그 안 찍힘)
}
15. 고급 Null 제어와 스마트 캐스트 (Smart Cast)

코틀린 컴파일러는 매우 똑똑합니다. 변수가 특정 타입인지 is 연산자로 검사하거나, null이 아님을 if문으로 체크했다면, 이후 블록에서는 자동으로 해당 타입(또는 Non-Null)으로 형변환을 적용해 줍니다. 이를 스마트 캐스트(Smart Cast)라고 합니다.

반면, !! 연산자(Not-null Assertion)는 "이건 절대 null이 아니니까 무조건 통과시켜!" 라고 컴파일러에게 강제하는 위험한 연산자입니다. 런타임 시 NPE가 터질 수 있어 사용을 지양해야 합니다.

🧠 스마트 캐스트 원리 val obj: Any if (obj is String) 컴파일러: "아! 여긴 확실히 String이네!" obj.length (자동 String 캐스팅)
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 폭발 (앱 크래시 발생)
}
16. 라이브러리와 API (Standard Library)

코틀린 표준 라이브러리는 자바 컬렉션 위에서 동작하지만, 어마어마하게 유용한 함수들을 제공합니다.

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)
}
1. Hello World와 프로그램 구조
2. 변수와 기본 타입 (val vs var)
3. 컬렉션 (List, Set, Map)
4. 조건문과 제어 흐름 (if, when)
5. 함수 (Functions)
6. 클래스와 인스턴스 (Classes)
7. 널 안전성 기초 (Null Safety)
8. 확장 함수 (Extension Functions)
9. 스코프 함수 (Scope Functions)
10. 고차 함수와 람다식 (Lambda & Higher-Order Functions)
11. 인터페이스와 상속 (Interfaces & Inheritance)
12. 싱글톤과 동반 객체 (Object & Companion Object)
13. 특수 목적 클래스 (Data, Enum, Sealed Class)
14. 프로퍼티와 지연 초기화 (lateinit, lazy)
15. 고급 Null 제어와 스마트 캐스트 (Smart Cast)
16. 라이브러리와 API (Standard Library)

목차