조건과 반복

조건과 반복 (Conditions and loops)

코틀린은 프로그램의 흐름을 제어할 수 있는 유연한 도구를 제공해요. if, when, 그리고 반복문을 활용하면 조건에 따라 또렷하고 표현력 있는 로직을 만들 수 있습니다.

출처: Kotlin 공식 문서 — Conditions and loops

본문

if 표현식

코틀린에서 if를 쓰려면 괄호 () 안에 확인할 조건을 넣고, 결과가 참일 때 실행할 동작을 중괄호 {} 안에 넣으면 돼요. 추가적인 분기나 확인이 필요하면 elseelse if를 쓸 수 있습니다.

if는 표현식(expression)으로도 쓸 수 있어요. 그러면 반환된 값을 변수에 바로 할당할 수 있죠. 이 형태에서는 else 분기가 필수입니다. if 표현식은 다른 언어에서 볼 수 있는 삼항 연산자(condition ? then : else)와 같은 역할을 해요.

예를 들어 볼게요.

fun main() {
    val heightAlice = 160
    val heightBob = 175

    //sampleStart
    var taller = heightAlice
    if (heightAlice < heightBob) taller = heightBob

    // Uses an else branch
    if (heightAlice > heightBob) {
        taller = heightAlice
    } else {
        taller = heightBob
    }

    // Uses if as an expression
    taller = if (heightAlice > heightBob) heightAlice else heightBob

    // Uses else if as an expression:
    val heightLimit = 150
    val heightOrLimit = if (heightLimit > heightAlice) heightLimit else if (heightAlice > heightBob) heightAlice else heightBob

    println("Taller height is $taller")
    // Taller height is 175
    println("Height or limit is $heightOrLimit")
    // Height or limit is 175
    //sampleEnd
}

if 표현식의 각 분기는 블록이 될 수 있고, 이때 블록의 마지막 표현식 값이 결과가 돼요.

fun main() {
    //sampleStart
    val heightAlice = 160
    val heightBob = 175

    val taller = if (heightAlice > heightBob) {
        print("Choose Alice\n")
        heightAlice
    } else {
        print("Choose Bob\n")
        heightBob
    }

    println("Taller height is $taller")
    //sampleEnd
}

when 표현식과 문

when은 여러 가능한 값이나 조건에 따라 코드를 실행하는 조건 표현식이에요. Java, C 등의 언어에 있는 switch 문과 비슷하죠. when은 인자를 평가하고, 그 결과를 순서대로 각 분기의 조건과 비교하다가 어느 하나가 충족되면 멈춥니다. 예를 들어 볼게요.

fun main() {
    //sampleStart
    val userRole = "Editor"
    when (userRole) {
        "Viewer" -> print("User has read-only access")
        "Editor" -> print("User can edit content")
        else -> print("User role is not recognized")
    }
    // User can edit content
    //sampleEnd
}

when은 표현식으로도, 문(statement)으로도 쓸 수 있어요. 표현식으로 쓰면 값을 반환해서 나중에 코드에서 사용할 수 있고, 문으로 쓰면 결과를 반환하지 않고 동작만 수행합니다.

// Returns a string assigned to the 
// text variable
val text = when (x) {
    1 -> "x == 1"
    2 -> "x == 2"
    else -> "x is neither 1 nor 2"
}
// Returns no result but triggers a 
// print statement
when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> print("x is neither 1 nor 2")
}

한편 when은 피대상(subject)이 있거나 없을 수 있는데, 동작은 어느 쪽이든 같아요. 보통 피대상을 쓰면 무엇을 확인하는지가 분명히 드러나서 코드가 더 읽기 쉽고 유지보수하기 좋습니다.

when(x) { ... }
when { ... }

when을 어떻게 쓰느냐에 따라 분기에서 가능한 모든 경우를 다루어야 할지가 달라져요. 가능한 모든 경우를 모두 다루는 것을 **완전성(exhaustive)**을 갖췄다고 말합니다.

문 (Statements)

when을 문으로 쓰면 모든 경우를 다 다룰 필요가 없어요. 아래 예시에서는 일부 경우만 다루어서 아무 분기도 실행되지 않지만, 오류가 발생하지는 않습니다.

fun main() {
    //sampleStart
    val deliveryStatus = "OutForDelivery"
    when (deliveryStatus) {
        // Not all cases are covered
        "Pending" -> print("Your order is being prepared")
        "Shipped" -> print("Your order is on the way")
    }
    //sampleEnd
}

if와 마찬가지로 각 분기는 블록이 될 수 있고, 그 분기의 값은 블록의 마지막 표현식 값이에요.

표현식 (Expressions)

when을 표현식으로 쓰면 반드시 모든 가능한 경우를 다루어야 해요. 첫 번째로 일치하는 분기의 값이 전체 표현식의 값이 됩니다. 모든 경우를 다루지 않으면 컴파일러가 오류를 던져요.

피대상이 있는 when 표현식이라면 else 분기를 써서 모든 경우를 다루도록 보장할 수 있지만, 이는 필수가 아니에요. 예를 들어 피대상이 Boolean, enum 클래스, sealed 클래스, 또는 이들의 null 허용 대응형이라면 else 분기 없이도 모든 경우를 다룰 수 있어요.

import kotlin.random.Random
//sampleStart
enum class Bit {
    ZERO, ONE
}

fun getRandomBit(): Bit {
    return if (Random.nextBoolean()) Bit.ONE else Bit.ZERO
}

fun main() {
    val numericValue = when (getRandomBit()) {
        // No else branch is needed because all cases are covered
        Bit.ZERO -> 0
        Bit.ONE -> 1
    }

    println("Random bit as number: $numericValue")
    // Random bit as number: 0
    //sampleEnd
}

when 표현식을 단순화하고 반복을 줄이려면, (현재 미리보기 단계인) 문맥 민감 해석(context-sensitive resolution)을 시도해 볼 수 있어요. 이 기능을 쓰면 기대 타입이 알려져 있을 때 when 표현식에서 enum 항목이나 sealed 클래스 멤버를 쓸 때 타입 이름을 생략할 수 있습니다.

자세한 내용은 문맥 민감 해석 미리보기 또는 관련 KEEP 제안을 참고하세요.

피대상이 없는 when 표현식이라면 else 분기가 반드시 있어야 해요. 아니면 컴파일러가 오류를 냅니다. else 분기는 다른 분기 조건이 하나도 충족되지 않을 때 평가됩니다.

fun main() {
    //sampleStart
    val localFileSize = 1200
    val remoteFileSize = 1200

    val message = when {
        localFileSize > remoteFileSize -> "Local file is larger than remote file"
        localFileSize < remoteFileSize -> "Local file is smaller than remote file"
        else -> "Local and remote files are the same size"
    }

    println(message)
    // Local and remote files are the same size
    //sampleEnd
}

when을 활용하는 다른 방법

when 표현식과 문은 코드를 단순화하고, 여러 조건을 다루고, 타입 검사를 수행하는 다양한 방법을 제공해요.

쉼표를 사용해 여러 조건을 하나의 분기로 묶을 수 있습니다.

fun main() {
    val ticketPriority = "High"
    //sampleStart
    when (ticketPriority) {
        "Low", "Medium" -> print("Standard response time")
        else -> print("High-priority handling")
    }
    //sampleEnd
}

true 또는 false로 평가되는 표현식을 분기 조건으로 사용할 수 있어요.

fun main() {
    val storedPin = "1234"
    val enteredPin = 1234
  
    //sampleStart
    when (enteredPin) {
        // Expression
        storedPin.toInt() -> print("PIN is correct")
        else -> print("Incorrect PIN")
    }
    //sampleEnd
}

in 또는 !in 키워드를 사용해서 값이 범위나 컬렉션에 포함되는지(혹은 포함되지 않는지) 확인할 수 있습니다.

fun main() {
    val x = 7
    val validNumbers = setOf(15, 16, 17)

    //sampleStart
    when (x) {
        in 1..10 -> print("x is in the range")
        in validNumbers -> print("x is valid")
        !in 10..20 -> print("x is outside the range")
        else -> print("none of the above")
    }
    //sampleEnd
}

is 또는 !is 키워드로 값의 타입을 확인할 수 있어요. 스마트 캐스트 덕분에 해당 타입의 멤버 함수와 속성에 바로 접근할 수 있습니다.

fun hasPrefix(input: Any): Boolean = when (input) {
    is String -> input.startsWith("ID-")
    else -> false
}

fun main() {
    val testInput = "ID-98345"
    println(hasPrefix(testInput))
    // true
}

전통적인 if-else if 체인 대신 when을 쓸 수도 있어요. 피대상이 없으면 분기 조건은 단순한 불리언 표현식이 되고, true인 첫 번째 분기가 실행됩니다.

fun Int.isOdd() = this % 2 != 0
fun Int.isEven() = this % 2 == 0

fun main() {
    //sampleStart
    val x = 5
    val y = 8

    when {
        x.isOdd() -> print("x is odd")
        y.isEven() -> print("y is even")
        else -> print("x+y is odd")
    }
    // x is odd
    //sampleEnd
}

마지막으로, 아래 문법으로 피대상을 변수에 담아 둘 수도 있습니다.

fun main() {
    val message = when (val input = "yes") {
        "yes" -> "You said yes"
        "no" -> "You said no"
        else -> "Unrecognized input: $input"
    }

    println(message)
    // You said yes
}

피대상으로 도입된 변수의 스코프는 when 표현식 또는 문의 본문으로 제한돼요.

가드 조건 (Guard conditions)

가드 조건을 쓰면 when 표현식이나 문의 분기에 조건을 여러 개 담을 수 있어서, 복잡한 제어 흐름을 더 명확하고 간결하게 만들 수 있어요. when에 피대상이 있으면 가드 조건을 사용할 수 있습니다.

가드 조건은 같은 분기 안에서 주 조건(primary condition) 뒤에 if로 구분해서 넣어요.

sealed interface Animal {
    data class Cat(val mouseHunter: Boolean) : Animal
    data class Dog(val breed: String) : Animal
}

fun feedDog() = println("Feeding a dog")
fun feedCat() = println("Feeding a cat")

//sampleStart
fun feedAnimal(animal: Animal) {
    when (animal) {
        // Branch with only primary condition
        // Calls feedDog() when animal is Dog
        is Animal.Dog -> feedDog()
        // Branch with both primary and guard conditions
        // Calls feedCat() when animal is Cat and not mouseHunter
        is Animal.Cat if !animal.mouseHunter -> feedCat()
        // Prints "Unknown animal" if none of the above conditions match
        else -> println("Unknown animal")
    }
}

fun main() {
    val animals = listOf(
        Animal.Dog("Beagle"),
        Animal.Cat(mouseHunter = false),
        Animal.Cat(mouseHunter = true)
    )

    animals.forEach { feedAnimal(it) }
    // Feeding a dog
    // Feeding a cat
    // Unknown animal
}
//sampleEnd

쉼표로 구분된 여러 조건이 있을 때는 가드 조건을 쓸 수 없어요. 예를 들어 아래 형태는 안 됩니다.

0, 1 -> print("x == 0 or x == 1")

하나의 when 표현식이나 문 안에서, 가드 조건이 있는 분기와 없는 분기를 섞어 쓸 수 있어요. 가드 조건이 있는 분기의 코드는 주 조건과 가드 조건이 모두 true로 평가될 때만 실행됩니다. 주 조건이 일치하지 않으면 가드 조건은 평가되지 않아요.

when 문은 모든 경우를 다 다룰 필요가 없으므로, else 분기 없이 가드 조건을 쓴다면 일치하는 조건이 없을 때 아무 코드도 실행되지 않습니다.

문과 달리 when 표현식은 모든 경우를 다 다루어야 해요. else 분기 없이 가드 조건을 쓴다면, 컴파일러는 런타임 오류를 피하기 위해 가능한 모든 경우를 처리하도록 요구합니다.

하나의 분기 안에서 불리언 연산자 &&(AND)나 ||(OR)로 여러 가드 조건을 결합할 수 있어요. 불리언 표현식에는 혼동을 피하기 위해 괄호를 둘러 주세요.

when (animal) {
    is Animal.Cat if (!animal.mouseHunter && animal.hungry) -> feedCat()
}

가드 조건은 else if도 지원합니다.

when (animal) {
    // Checks if `animal` is `Dog`
    is Animal.Dog -> feedDog()
    // Guard condition that checks if `animal` is `Cat` and not `mouseHunter`
    is Animal.Cat if !animal.mouseHunter -> feedCat()
    // Calls giveLettuce() if none of the above conditions match and animal.eatsPlants is true
    else if animal.eatsPlants -> giveLettuce()
    // Prints "Unknown animal" if none of the above conditions match
    else -> println("Unknown animal")
}

JVM에서의 바이트코드 생성

JVM 21 이상을 대상으로 코틀린 코드를 컴파일하면, 컴파일러는 조건에 맞는 when 표현식에 대해 invokedynamic 명령어를 생성해요. 이렇게 하면 Java switch 문이 만드는 것처럼 더 작은 바이트코드가 만들어집니다.

컴파일러는 아래 조건이 모두 충족될 때 SwitchBootstraps.typeSwitch() 메서드와 함께 invokedynamic을 사용해요.

  • else를 제외한 모든 조건이 is 또는 null 검사인 경우.
  • when 표현식에 가드 조건(if)이 없는 경우.
  • 조건이 직접 타입 검사할 수 없는 타입(예: 변경 가능한 코틀린 컬렉션 MutableList, 함수 타입 kotlin.Function1, kotlin.Function2 등)을 포함하지 않는 경우.
  • when 표현식이 else 외에 최소 두 개 이상의 조건을 가지는 경우.
  • 모든 분기가 when 표현식의 같은 피대상을 검사하는 경우.

예를 들어 볼게요.

open class Shape

class Circle : Shape()
class Rectangle : Shape()
class Triangle : Shape()

fun countCorners(shape: Shape) = when (shape) {
    is Circle -> 0
    is Rectangle -> 4
    is Triangle -> 3
    else -> -1
}

여기서 when (shape) 표현식은 바이트코드에서 여러 instanceof 검사 대신 단일 invokedynamic 타입 스위치로 컴파일됩니다.

for 반복문

for 반복문은 컬렉션, 배열, 범위를 순회할 때 사용해요.

for (item in collection) print(item)

for 반복문의 본문은 중괄호 {}로 감싼 블록일 수 있습니다.

fun main() {
    val shoppingList = listOf("Milk", "Bananas", "Bread")
    //sampleStart
    println("Things to buy:")
    for (item in shoppingList) {
        println("- $item")
    }
    // Things to buy:
    // - Milk
    // - Bananas
    // - Bread
    //sampleEnd
}

범위 (Ranges)

숫자 범위를 순회하려면 ....< 연산자를 사용한 범위 표현식을 쓰세요.

fun main() {
//sampleStart
    println("Closed-ended range:")
    for (i in 1..6) {
        print(i)
    }
    // Closed-ended range:
    // 123456
  
    println("\nOpen-ended range:")
    for (i in 1..<6) {
        print(i)
    }
    // Open-ended range:
    // 12345
  
    println("\nReverse order in steps of 2:")
    for (i in 6 downTo 0 step 2) {
        print(i)
    }
    // Reverse order in steps of 2:
    // 6420
//sampleEnd
}

배열 (Arrays)

배열이나 리스트를 인덱스와 함께 순회하고 싶다면 indices 속성을 사용할 수 있어요.

fun main() {
    val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee")
    //sampleStart
    for (i in routineSteps.indices) {
        println(routineSteps[i])
    }
    // Wake up
    // Brush teeth
    // Make coffee
    //sampleEnd
}

대신 표준 라이브러리의 .withIndex() 함수를 쓸 수도 있습니다.

fun main() {
    val routineSteps = arrayOf("Wake up", "Brush teeth", "Make coffee")
    //sampleStart
    for ((index, value) in routineSteps.withIndex()) {
        println("The step at $index is \"$value\"")
    }
    // The step at 0 is "Wake up"
    // The step at 1 is "Brush teeth"
    // The step at 2 is "Make coffee"
    //sampleEnd
}

이터레이터 (Iterators)

for 반복문은 iterator를 제공하는 모든 것을 순회해요. 컬렉션은 기본적으로 이터레이터를 제공하고, 범위와 배열은 인덱스 기반 반복문으로 컴파일됩니다.

iterator()라는 멤버 또는 확장 함수를 제공해서 나만의 이터레이터를 만들 수도 있어요. iterator() 함수는 next() 함수와 Boolean을 반환하는 hasNext() 함수를 가진 Iterator<>를 반환해야 합니다.

클래스에 대한 나만의 이터레이터를 만드는 가장 쉬운 방법은 Iterable<T> 인터페이스를 상속하고, 이미 있는 iterator(), next(), hasNext() 함수를 오버라이드하는 거예요. 예시를 보겠습니다.

class Booklet(val totalPages: Int) : Iterable<Int> {
    override fun iterator(): Iterator<Int> {
        return object : Iterator<Int> {
            var current = 1
            override fun hasNext() = current <= totalPages
            override fun next() = current++
        }
    }
}

fun main() {
    val booklet = Booklet(3)
    for (page in booklet) {
        println("Reading page $page")
    }
    // Reading page 1
    // Reading page 2
    // Reading page 3
}

인터페이스상속에 대해 더 알아보세요.

아니면 함수를 처음부터 직접 만들 수도 있어요. 이 경우 함수에 operator 키워드를 붙여야 합니다.

//sampleStart
class Booklet(val totalPages: Int) {
    operator fun iterator(): Iterator<Int> {
        return object {
            var current = 1

            operator fun hasNext() = current <= totalPages
            operator fun next() = current++
        }.let {
            object : Iterator<Int> {
                override fun hasNext() = it.hasNext()
                override fun next() = it.next()
            }
        }
    }
}
//sampleEnd

fun main() {
    val booklet = Booklet(3)
    for (page in booklet) {
        println("Reading page $page")
    }
    // Reading page 1
    // Reading page 2
    // Reading page 3
}

while 반복문

whiledo-while 반복문은 조건이 충족되는 동안 본문에 있는 코드를 계속 실행해요. 둘의 차이는 조건을 확인하는 시점에 있습니다.

  • while은 조건을 확인하고, 충족되면 본문 코드를 실행한 뒤 다시 조건 확인으로 돌아가요.
  • do-while은 본문 코드를 먼저 실행한 다음 조건을 확인합니다. 조건이 충족되면 반복이 다시 진행돼요. 그래서 do-while의 본문은 조건과 무관하게 최소 한 번은 실행됩니다.

while 반복문은 확인할 조건을 괄호 ()에, 본문을 중괄호 {}에 넣어요.

fun main() {
    var carsInGarage = 0
    val maxCapacity = 3
//sampleStart
    while (carsInGarage < maxCapacity) {
        println("Car entered. Cars now in garage: ${++carsInGarage}")
    }
    // Car entered. Cars now in garage: 1
    // Car entered. Cars now in garage: 2
    // Car entered. Cars now in garage: 3

    println("Garage is full!")
    // Garage is full!
//sampleEnd
}

do-while 반복문은 확인할 조건을 괄호 ()에 넣기 전에, 먼저 중괄호 {} 안에 본문을 놓아요.

import kotlin.random.Random

fun main() {
    var roll: Int
//sampleStart
    do {
        roll = Random.nextInt(1, 7)
        println("Rolled a $roll")
    } while (roll != 6)
    // Rolled a 2
    // Rolled a 6
    
    println("Got a 6! Game over.")
    // Got a 6! Game over.
//sampleEnd
}

반복문에서의 break와 continue

코틀린은 반복문에서 전통적인 breakcontinue 연산자를 지원합니다. Returns and jumps를 참고하세요.

더 알아보기 (Learn more)