[Kotlin In Action] (2) - basics

코틀린 기초

 

함수와 변수

 

이번 글에서는 모든 프로그램을 구성하는 기본 단위인 함수와 변수를 살펴보겠습니다.

첫 번째 코틀린 코드를 작성하고 코틀린에서 타입 선언을 생략해도 된다는 사실을 보고, 코틀린이 어떻게 변경 가능한 데이터보다 변경할 수 없는 불변 데이터 사용을 권장하는지와 왜 불변 데이터가 더 좋은 것인지 살펴보겠습니다.

 

Hello World

 

가장 기본적인 예제인 'Hello, World'를 출력하는 프로그램으로 시작해보겠습니다. 코틀린에서는 함수 하나로 이 프로그램을 만들 수 있습니다.

  • 함수를 선언할 때 fun 키워드를 사용합니다.
  • 함수를 모든 코틀린 파일의 최상위 수준에 정의할 수 있으므로 클래스 안에 넣어야 할 필요가 없습니다.
  • 최상위에 있는 main 함수를 애플리케이션의 진입점으로 지정할 수 있습니다. 이때, main에 인자가 없어도 됩니다.
  • 표준 자바 라이브러리 함수에(System.out.println) 대해 더 간결한 구문을 사용할 수 있게 해주는 래퍼(wrapper)를 제공합니다.

 

함수 선언

 

앞에서 작성했던 함수는 반환값이 없는 함수였습니다. 이번에는 반환값이 있는 함수에 대해 살펴보겠습니다.

함수 선언은 fun 키워드로 시작합니다. fun 다음에는 함수 이름이 오고, 여기에서는 max가 함수 이름입니다.

함수 이름 뒤에는 괄호 안에 파라미터 목록이 오는데, 코틀린에서는 파라미터 이름이 먼저 오고 그 뒤에 그 파라미터 타입을 지정하며, 타입과 이름을 콜론(:)으로 구분합니다. 그리고 함수의 반환 타입은 파라미터 목록을 닫는 괄호 다음에 오는데, 닫는 괄호와 반환 타입 사이를 콜론(:)으로 구분해야 합니다.

 

코틀린에서 유의해야할 점은 if는 결과를 만드는 식(expression)으로 사용할 수 있다는 것입니다. 코틀린 if식은 자바 같은 다른 언어의 삼항 연산자와 비슷합니다. (물론 일반적인 if 문(statement)처럼도 사용할 수 있지만, if를 식으로 활용하는 것이 더 코틀린스러운 코드 스타일입니다.)

프로그램의 진입점인 main 함수를 main(args: Array<String>) 라고 선언하면 커맨드라인 인자를 받을 수 있습니다.
식(expression)이란 값을 만들어내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면, 문(statement)은 자신을 둘러싸고 있는 가장 안쪽 블럭의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다는 차이가 있습니다.

코틀린에서는 루프(for, while, do/while)를 제외한 대부분의 제어 구조가 식이라는 점이 자바와 다른 점입니다. 반면, 코틀린에서는 자바와 달리 대입을 항상 문으로 취급합니다.

 

본문 함수

 

위에서 선언한 max 함수를 실제로는 더 간결하게 표현할 수도 있습니다. max 함수 본문이 if식 하나로만 이뤄져 있기 때문에 이 식을 함수 본문 전체로 하고 중괄호를 없앤 후 return을 제거할 수 있습니다. 대신, 이 유일한 식을 등호(=) 뒤에 위치시켜야 합니다.

본문이 중괄호로 둘러싸인 함수를 '블록 본문 함수(block body function)'라 부르고, 등로화 식으로 이뤄진 함수를 '식 본문 함수(expression body function)'라고 부릅니다. 코틀린에서는 식 본문 함수가 자주 쓰입니다. 조건 검사나 자주 사용되는 연산에 기억하기 쉬운 이름을 부여하는 뻔한 함수의 경우에도 물론 식 본문 함수가 편리하지만 if, when, try 등의 더 복잡한 식을 평가하는 함수에서도 식 본문 함수가 유용합니다. 

 

위 식에서 반환 타입을 생략하면 max함수를 더 간단하게 만들 수 있습니다.

코틀린은 정적 타입 지정 언어인데 어떻게 반환 타입 선언이 없는 함수가 있을 수 있을까? 라고 생각할 수 있습니다. 실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 합니다. 하지만 식 본문 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 몬둔 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해줍니다. 이런 분석을 타입 추론(type inference)이라 부릅니다.

 

코틀린에서 식 본문 함수의 반환 타입만 생략 가능하게 설계한 것은 아주 긴 함수의 경우 return 문이 여럿 들어있는 경우가 자주 있는데, 그런 경우 반환 타입을 꼭 명시하고 return을 사용하는 것이 함수가 어떤 타입의 값을 반환하고 어디서 그런 값을 반환하는지 더 쉽게 알아볼 수 있기 때문입니다.

라이브러리를 작성할 때는 반환 타입을 명시하는 것이 좋습니다. 명시적으로 함수의 반환 타입을 지정하면 실수로 함수 시그니처가 바뀌면서 라이브러리 소비자들의 코드에 오류가 발생하는 경우를 피할 수 있습니다. 코틀린에는 반환 타입을 명시적으로 지정하는지 자동으로 검사해주는 컴파일러 옵션도 있습니다.

 

변수 선언

 

변수 선언은 키워드로 시작하고 그 뒤에 변수 이름이 옵니다. 코틀린에서 여러 변수 선언에서 타입 지정을 생략할 수 있게 해주지만, 변수 이름 뒤에 타입을 명시할 수도 있습니다.

val question: String = "질문"
val answer: Int = 42

val question = "질문"
val answer = 42

식 본문 함수와 마찬가지로 타입을 지정하지 않으면 컴파일러가 초기화 식을 분석해서 초기화 식의 타입을 변수 타입으로 지정합니다.

val answer: Int
answer = 42

변수를 선언하면서 즉시 초기화하지 않고 나중에 값을 대입하고 싶을 때는 컴파일러가 변수 타입을 추론할 수 없기 때문에 이런 경우에는 명시적으로 변수의 타입을 지정해야 합니다.

 

코틀린에서는 val, var 두 개의 변수 선언 키워드를 사용합니다.

  • val(value)은 읽기 전용 참조(read-only reference)를 선언합니다.
    val로 선언된 변수는 단 한 번만 대입될 수 있기 때문에 일단 초기화하고 나면 다른 값을 대입할 수 없습니다.
    (자바에서의 final 제어자와 같습니다.)
  • var(variable)은 재대입 가능한 참조(reassignable reference)를 선언합니다.
    var로 선언된 변수는 초기화가 이뤄진 다음에도 다른 값을 대입할 수 있습니다.

기본적으로 코틀린에서 모든 변수를 val 키워드를 사용해 선언하고, 반드시 필요할 때에만 변수를 var로 선언합니다. 읽기 전용 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 함수형 프로그래밍 스타일이 제공하는 이점을 살릴 수 있습니다.

코틀린에서는 변수 선언 키워드(var, val) 없이 선언되는 변수들도 모두 val 성격입니다.
- 함수 매개변수, 람다 매개변수, for 루프 변수

 

val 변수는 그 변수가 정의된 블럭을 실행할 때 정확히 한 번만 초기화돼야 합니다. 하지만 블럭이 실행될 때 오직 하나의 초기화 문장만 실행됨을 컴파일러가 확인할 수 있다면 조건에 따라 val 값을 다른 여러 값으로 초기화할 수도 있습니다.

fun canPerformOperation(): Boolean = true

fun main() {
    val result: String
    if (canPerformOperation()) {
        result = "Success"
    } else {
        result = "Fail"
    }
}

위 코드와 같은 경우 컴파일러가 잠재적인 2가지 대입 중 단 하나만 실행될 수 있다는 것을 알 수 있기 때문에 result를 val 키워드를 사용해 읽기 전용 참조로 지정할 수 있습니다.

 

또한 val 참조 자체가 읽기 전용이어서 한 번 대입된 다음에 그 값을 바꿀 수는 없더라도, 그 참조가 가리키는 객체의 내부 값은 변경될 수 있습니다.

fun main() {
    val language = mutableListOf("Java")
    language.add("Kotlin")
}

 

문자열 템플릿

 

fun main() {
    val input = readln()
    val name = if (input.isNotBlank()) input else "Kotlin"
    println("Hello, $name!")
}

위 예제는 문자열 템플릿(string template)이라는 기능을 보여주고 간단한 사용자 입력을 읽는 방법을 보여줍니다. 표준 입력 스트립에서 readln() 함수를 통해 input을 읽습니다. 그 후 name이라는 변수를 선언하고 if식으로 초기화합니다. 표준 입력에서 받은 문자열이 비어있지 않다면 input값을, 비어있다면 "Kotlin"을 name에 저장합니다. 마지막으로 name을 println에 전달하는 문자열 리터럴 안에서 사용합니다.

 

여러 스크립트 언어와 비슷하게 코틀린에서도 변수 이름 앞에 '$'를 붙이면 변수를 문자열 안에 참조할 수 있습니다. 이렇게 만든 문자열은 자바의 문자열 접합 연산(+)과 동일한 기능을 하지만 좀 더 간결하며 똑같이 효율적입니다. 물론 컴파일러는 이 식을 정적으로 검사하기 때문에 존재하지 않는 변수를 문자열 템플릿 안에서 사용하면 컴파일 오류가 발생합니다.

코틀린의 문자열 리터럴은 컴파일 시 JVM 바이트 코드에서 StringBuilder(JDK 9이상이면 invokedynamic)로 변환됩니다.

 

$ 문자 자체를 문자열에 넣고 싶다면 백슬래시(\)를 사용해 $를 이스케이프(escape)시켜야 합니다.

println("\$input")	// input을 변수 참조로 해석 X

 

코틀린의 문자열 템플릿은 개별적인 변수만 참조하는 것으로 제한되지 않기 때문에 복잡한 식을 사용하는 경우 중괄호를 사용하면 됩니다.

println("I'll buy ${name.length} apples")

println("Hello, ${if (name.isBlank()) "someone" else name}!")
* 문자열 템플릿에서 주의할 점 
코틀린에서 모든 유니코드 문자는 식별자에 사용할 수 있기 때문에 문자열 템플릿을 해석할 때 오해가 생길 수 있습니다. 특히 문자열 템플릿 안에 $로 변수를 지정할 때 변수명 바로 뒤에 한글을 붙여 사용하면 코틀린 컴파일러는 영문자와 한글을 한꺼번에 식별자로 인식해서 unresolved reference 오류를 발생시킵니다. 예를 들어 "$name 님" 을 "$name님" 으로 사용하면 에러가 발생합니다. 이 문제는 "${name}님" 과 같이 변수 이름을 중괄호로 감싸는 것으로 해결할 수 있습니다.

 

 

클래스와 프로퍼티

 

다른 객체지향 언어와 마찬가지로 코틀린도 클래스라는 추상화를 제공합니다. 코틀린을 활용하면 다른 객체지향 언어를 사용할 때보다 더 적은 양의 코드로 대부분의 공통적인 작업을 수행할 수 있습니다. 먼저 간단한 POJO(Plain Old Java Object) 클래스 Person을 정의해보겠습니다.

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

자바에서는 이런 간단한 클래스에도 상당히 긴 코드가 필요합니다.

그러나 코틀린에서는 이런 클래스를 다음과 같이 간결하게 정의할 수 있는 문법을 제공합니다.

class Person(val name: String)

자바의 record 클래스와 유사한 개념으로, 특이한 점은 접근 제한자가 사라졌다는 것입니다. 코틀린의 경우 기본 접근 제한자가 public이므로 생략이 가능합니다.

 

클래스라는 개념은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 안에 가두는 것입니다. 자바에서는 데이터를 필드에 저장하며 멤버 필드의 접근 제한자는 보통 private 입니다. 그리고 해당 필드에 접근하기 위한 getter와 setter를 제공합니다. 코틀린에서는 필드와 getter/setter를 한데 묶어 '프로퍼티(property)'라고 부르고, 이를 언어 기본 기능으로 제공합니다.

위 클래스를 코틀린에서 사용하는 방법을 살펴보겠습니다.

코틀린에서는 getter, setter를 명시적으로 호출하는 대신 프로퍼티를 직접 사용합니다. 

자바에서 작성한 클래스에서 getXxx(), setXxx() 메서드는 자바-코틀린 변환기에 의해서 코틀린 클래스의 프로퍼티로 변환됩니다. 그러나 필드명이 'is'로 시작하는 경우에는 다음과 같은 별도의 네이밍 규칙이 있습니다.
* getIsStudent()  ->  isStudent()
* setIsStudent()  ->  setStudent()

 

커스텀 프로퍼티

 

커스텀 구현이 필요한 일반적인 경우로는 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 직접적인 결과인 경우입니다. 예를 들어 직사각형 클래스인 Rectangle에서 width와 height가 같을 때만 true를 반환하는 isSquare 프로퍼티를 제공할 수 있습니다. 이 프로퍼티는 프로퍼티에 접근할 때 계산을 하기 때문에 정보를 별도의 필드에 저장할 필요가 없고 대신 커스텀 getter를 제공할 수 있습니다.

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }
        // get() = height == width
}

위 코드의 주석과 같이 커스텀 getter를 만들 때 꼭 중괄호를 사용하지 않고 식 본문 함수 구문을 사용하여 작성해도 됩니다. 코틀린에서 커스텀 프로퍼티는 클래스의 바디에 작성하며, 커스텀 getter는 fun 키워드를 사용하지 않고 프로퍼티 정의 바로 아래에 한 칸 들여써서 작성합니다.

이때 커스텀 프로퍼티로 작성하는 것과 별도의 멤버 함수로 구현하는 것의 구현이나 성능 차이는 없습니다. 일반적으로 클래스의 특성을 기술하고 싶다면 프로퍼티로 정의하고, 클래스의 행동을 기술하고 싶다면 멤버 함수를 사용합니다.

 

소스코드 구조

 

코틀린에서는 자바와는 다르게 여러 클래스를 한 파일에 넣을 수 있고, 파일의 이름도 마음대로 정할 수 있습니다. 또한 디스크상의 어느 디렉터리에 소스코드 파일을 위치시키든 관계없습니다. 하지만 대부분의 경우 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫습니다. 특히 자바와 코틀린을 함께 사용하는 프로젝트에서는 자바의 방식을 따르는 것이 중요합니다. 

하지만 여러 클래스를 한 파일에 넣는 것을 주저해서는 안됩니다. 특히 각 클래스를 정의하는 소스코드 크기가 아주 작은 경우 더욱 그렇습니다.

 

enum과 when

 

이번엔 코틀린에서 enum을 선언하는 방법과 when에 대해 살펴보겠습니다. when은 자바의 switch를 대신하지만 훨씬 더 강력하며 더 자주 사용되는 프로그래밍 요소라고 생각할 수 있습니다. 또한 타입 겸사와 타입 변환(cast)을 하나로 묶은 개념인 스마트 캐스트(smart cast)에 대해서도 살펴보겠습니다.

 

enum class

 

enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야하는 흔치 않은 예입니다. 코틀린에서는 enum class를 사용하지만 자바에서는 enum을 사용합니다.

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE
}
soft keyword  vs.  hard keyword
코틀린에서 enum은 소프트 키워드(soft keyword)로 class 앞에 있을 때만 특별한 의미를 지나고, 다른 곳에서는 일반적인 이름으로 사용할 수 있습니다. 반면 class는 하드 키워드(hard keyword)로 해당 키워드는 식별자로 사용할 수 없어 클래스를 표현하는 변수를 정의할 때는 clazz나 aClass와 같은 이름을 주로 사용합니다.

 

위와 같이 색상을 enum 클래스에 저장하는 것뿐 아니라, 각 색상을 컴포넌트로 표현할 수 있습니다. 앞서 보았던 일반적인 클래스와 마찬가지로 생성자와 프로퍼티 선언 문법을 사용할 수 있습니다. 각 enum 상수에 대한 r, g, b 프로퍼티와 메서드를 정의해보겠습니다.

enum class Color(
        val r: Int,		// 프로퍼티
        val g: Int,
        val b: Int
) {
    RED(255, 0, 0),		// eunm 상수
    ORANGE(255, 165, 0),
    YELLOW(255, 255, 0),
    GREEN(0, 255, 0),
    BLUE(0, 0, 255);		// -> 세미콜론(;) 필수!!

    // 커스텀 프로퍼티
    val rgb: Int
        get() = (r * 256 + g) * 256 + b
    
    // 메서드
    fun printColor() = println("$this is $rgb")
}

fun main() {
    println(Color.BLUE.rgb)

    Color.GREEN.printColor()
}

위 예제에서는 코틀린에서 유일하게 세미콜론(;)이 필수인 부분을 볼 수 있습니다. enum 클래스 안에 메서드나 커스텀 프로퍼티를 정의하는 경우 반드시 상수 목록과 메서드 정의 사이에 반드시 세미콜론을 넣어야 합니다.

 

when

 

자바에서는 switch 문, JDK 13부터는 switch 식에 해당하는 코틀린의 구성 요소는 when입니다. if와 마찬가지로 when도 값을 만들어내는 식입니다. 따라서 식 본문 함수에 when을 바로 사용해서 when 식을 바로 반환할 수 있습니다.

fun getMnemonic(color: Color) = 
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "of"
        Color.YELLOW, Color.GREEN -> "York"
        else -> "Gave"
}

자바와 달리 각 분기 끝에 break를 넣지 않아도 되며, 한 분기 안에서 값 사이를 콤마(,)로 분리하면 여러 값을 패턴으로 사용할 수 있습니다.

when 식을 사용할 때마다 컴파일러는 when이 완전한지(exhaustive) 살펴봅니다. 그렇기 때문에 모든 가능한 경우를 빠짐없이 처리해야 하며, 그렇지 않은 경우 else 키워드를 사용해 디폴트 케이스를 제공해야만 합니다. 또한 when식의 각 분기를 블럭으로 만들면 여러 문장을 추가할 수 있으며, 각 블럭의 맨 마지막에 있는 식이 그 블럭이 반환할 값이 됩니다.

 

when (measureColor()) { ... }		// 함수

when (val color = measureColor()) { ... }	// 변수 캡쳐

when (setOf(c1, c2)) {			// 임의의 객체
    setOf(RED, YELLOW) -> ORANGE
    ...
}

코틀린의 when은 다른 언어의 비슷한 구성 요소보다 훨씬 더 유연하기 때문에, when 식의 인자(대상)에는 다양한 값들이 올 수 있습니다. 위 코드와 같이 함수가 올 수도 있으며, 함수의 호출 결과를 변수로 캡쳐할 수도 있습니다. 이때 캡쳐된 변수의 스코프는 when식의 본문으로 제한됩니다.

 

또한 객체들의 집합인 Set 객체를 when 식의 인자로 사용할 수도 있습니다. when식의 인자로 임의의 객체를 사용하는 경우 인자의 객체와 분기 조건에 있는 객체와 비교할 때 동등성(equility)을 비교합니다.

동등성(equility)  vs.  동일성(Identity)
* 동등성: 값이 같은지 비교 (==)
* 동일성: 주소가 같은지 비교 (===)

 

인자 없는 when

 

위 예제에서 임의의 객체 Set을 인자로 사용한 when식의 경우 호출될 때마다 분기 조건과 비교하기 위한 Set 인스턴스를 생성합니다. 보통은 이런 것이 큰 문제가 되지는 않지만, 이 함수가 아주 자주 호출된다면 가비지 컬렉터가 수거할 객체가 늘어나기 때문에 함수를 수정하는 것이 좋습니다. 이때 인자가 없는 when 식을 사용하면 불필요한 객체 생성을 막을 수 있습니다.

when {
    (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
    ...
}

코드의 가독성은 조금 떨어지지만 성능을 더 향상시키기 위해 그 정도 비용은 감수하는 경우도 자주 있습니다. 이렇게 인자가 없는 when 식의 경우 각 분기 조건이 Boolean 결과를 반환하는 식이어야 합니다.

 

스마트 캐스트

 

위 그림처럼 Apple, Banana 클래스와 Fruit 인터페이스가 있다고 가정하겠습니다. Fruit 인터페이스는 여러 타입의 공통 타입 역할만 수행하는 인터페이스인 '마커 인터페이스(marker interface)'입니다.

interface Fruit
class Apple(val color: Int) : Fruit
class Banana(val length: Int, val taste: Int) : Fruit

코틀린에서는 클래스가 구현하는 인터페이스를 지정하기 위해서 콜론(:)을 사용합니다.

 

또한 코틀린에서는 is 검사를 사용해 어떤 변수의 구체적 타입을 검사할 수 있습니다. 이는 자바의 instanceof와 같지만, 코틀린의 is 는 약간의 편의를 추가로 제공합니다. 이 편의는 어떤 변수의 타입을 확인한 다음에 그 타입에 속한 멤버에 접근할 때 명시적으로 타입 캐스팅을 하지 않아도 된다는 점입니다. 이런 경우를 '스마트 캐스트'라고 합니다.

fun buy(f: Fruit): Int = 
    if (f is Apple) {
        f.color
    } else if (f is Banana) {
        f.length + f.taste
    } else {
        throw Exception("Unknown")
    }

위 함수를 보면 is로 변수의 타입을 검사한 다음에 명시적인 캐스팅 없이 Apple과 Banana의 멤버에 접근하고 있습니다. 이러한 스마트 캐스트는 그 값이 바뀔 수 없는 경우에만 작동합니다. 위 예시에서 함수의 매개변수는 기본적으로 val 성격이기 때문에 f 변수에 대한 스마트 캐스팅이 가능합니다.

 

만약 원하는 타입으로 명시적으로 타입 캐스트을 하려는 경우 as 키워드를 사용합니다.

val a = f as Apple

 

위의 buy 함수를 다음과 같이 인자 없는 when 식으로도 표현할 수 있습니다.

fun buy(f: Fruit): Int = 
    When (f) {
        is Apple -> a.color
        is Banana -> b.length + b.taste
        else -> throw Exception("Unknown")
    }

 

while과 for 루프

 

while 루프

 

코틀린에는 whiledo-while 루프가 있습니다. 두 루프의 형태는 다음과 같습니다.

while (조건) {
    if (shouldExit) break
}

do {
    if (shouldExit) break;
} while (조건)

내포된 루프의 경우 코틀린에서는 레이블(label)을 지정할 수 있습니다. 그 후 break나 continue를 사용할 때 레이블을 참조할 수 있습니다. 

outer@ while (조건문) {
    while (조건문) {
        if (shouldExitInner) break
        if (shouldSkipInner) continue
        if (shouldExitOuter) break@outer
        if (shouldSkipOuter) continue@outer
    }
}

 

범위 연산자

 

코틀린에는 C와 같은 고전적인 루프(int i = 0; i < 3; i++ )에 해당하는 요소가 없습니다. 이런 용례를 대신하기 위해 코틀린에서는 범위를 사용합니다. 범위는 기본적으로 두 값으로 이뤄진 구간(interval)입니다. 보통 그 두 값은 숫자 타입은 시작 값과 끝 값입니다. 범위를 쓸 때는 다음과 같이 범위 연산자(..)를 사용합니다.

val oneToTen = 1..10		// 1 ≤ x ≤ 10 (폐구간)

val oneToNine = 1..<10		// 1 ≤ x < 10 (반폐구간)

 

in 연산자

 

in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있습니다.

다음 코드는 in을 사용해 어떤 문자가 정해진 문자의 범위에 속하는지 검사하는 함수입니다.

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'

fun isNotDigit(c: Char) = c !in '0'..'9'

 

이러한 범위 검사는 숫자와 문자에만 국한되지 않습니다. 비교가 가능한 클래스라면 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있습니다. 이렇게 만든 범위의 경우 그 범위 내의 모든 객체를 항상 순회할 수는 없지만, 범위 안에 속하는지는 검사할 수 있습니다.

println("Kotlin" in "Java".."Scala")		// true

위 코드의 경우  "Java"와 "Scala" 사이의 모든 문자열을 직접 순회할 수는 없지만 "Kotlin"이 그 범위에 속하는지는 확인할 수 있습니다.
String 클래스에 있는 Comparable 인터페이스가 두 문자열을 알파벳 순서로 비교하기 때문에 in 검사에서도 문자열을 알파벳 순서로 비교합니다.

 

컬렉션에서도 마찬가지로 in 연산을 사용할 수 있습니다.

println("Kotlin" in setOf("Java", "Scala"))		// false

 

for 루프

 

다음은 범위에 대한 루프를 살펴보겠습니다.

for (i in 1..10) { ... }			// 오름차순 폐구간 범위 연산자
	
for (i in 1..<10) { ... }			// 오름차순 반폐구간 범위 연산자

for (i in 10 downTo 1 step 2) { ... }		// 내림차순 폐구간 확장 함수

1행은 가장 기본적인 for 루프이며, 3행은 끝 값을 포함하지 않는 반폐구간 for 루프, 5행은 내림차순으로 간격을 지정한 for 루프입니다.

 

컬렉션 루프

 

다음은 컬렉션의 각 원소에 대한 루프를 살펴보겠습니다.

val collection = listOf(1, 2, 3)
for (number in collection) { ... }		// 원소만 루프

for ((index, number) in collection.withIndex()) { ... }	// 인덱스, 원소 루프

withIndex 함수와 구조 분해 구문을 사용하면 컬렉션의 현재 인덱스를 유지하면서 반복할 수도 있습니다. 이 기능을 사용하면 인덱스를 저장하기 위한 변수를 별도로 선언하고 루프에서 매번 그 변수를 증가시킬 필요가 없습니다.

 

맵 루프

 

다음은 맵 원소의 키/값 쌍에 대한 루프를 살펴보겠습니다.

val map = mutableMapOf(
    "a" to 10,
    "b" to 20
)

for ((letter, number) in map) { ... }

컬렉션 루프에서 간단하게 보았던 구조 분해 구문을 사용하여 키와 값을 각각 letter, number에 저장했습니다.

 

코틀린에서는 자바에서보다 더 우아하게 맵을 사용할 수 있습니다.

map["b"] = 30
println(map["b"])		// 30

코틀린에서 맵의 값을 가져오거나 키에 해당하는 값을 설정할 때에는 get과 put 같은 함수를 사용하는 대신, map[key]로 값을 가져오고 map[key] = value를 사용해 값을 설정할 수 있습니다.

 

Exception

 

코틀린의 예외(exception)처리는 자바나 다른 언어의 예외 처리와 비슷합니다. 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질(throw)수 있습니다. 함수를 호출하는 쪽에서는 그 예외를 잡아(catch) 처리할 수 있습니다. 발생한 예외를 그 함수를 호출한 쪽에서 잡아 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던집니다(rethrow).

 

다음은 throw 키워드를 사용해 예외를 던지는 예제입니다.

if (percentage !in 0..100) {
    throw IllegalArgumentException("A percentage value must be 0 ~ 100")
}

인스턴스 생성 때와 동일하게 new 키워드 없이 예외 인스턴스를 생성합니다.

 

자바와 달리 코틀린의 throw는 식이므로 다른 식에 포함될 수 있습니다.

val percentage = 
    if (number in 0..100) number
    else throw IllegalArgumentException("A percentage value must be 0 ~ 100")

 

try, catch, finally

 

예외를 처리하려면 자바와 마찬가지로 try와 catch, finally 절을 함께 사용해 예외를 처리합니다.

다음은 BufferedReader로부터 각 줄을 읽어 수로 변환하되, 올바른 수 형태가 아니면 null을 반환하고 올바른 수 형태면 수를 반환하는 코드입니다.

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}

자바 코드와 가장 큰 차이는 throws절이 코틀린에는 없다는 점입니다.

 

자바에서는 함수 선언 뒤에 'throws IOException'을 선언해야 합니다. 이렇게 해야하는 이유는 readLine 함수와 close 함수에서IOException이 발생할 수 있기 때문입니다. 

Integer readNumber(BufferedReader reader) throws IOException

자바 세계에서는 이러한 체크 예외(checked exception)들이 메서드 시그니처의 일부이기 때문에 함수를 사용할 때 해당 예외들을 모두 던져야 합니다. 그리고 해당 함수를 호출한 곳에서 예외를 모두 잡아서 처리하거나 다시 그 예외를 던져야 합니다.

 

그러나 다른 최신 JVM 언어와 마친가지로 코틀린은 체크 예외(checked exception)와 언체크 예외(unchecked exception)를 구별하지 않습니다. 함수가 던지는 예외를 선언하지 않고, 발생한 예외를 반드시 잡아내지 않아도 됩니다.

 

자바에서의 경험에 따르면 프로그래머들이 의미 없이 예외를 다시 던지거나, 예외를 잡되 처리하지는 않고 그냥 무시하는 코드를 작성하는 경우가 자주 있어, 이러한 예외 처리 규칙이 실제로는 오류 발생을 방지하지 못하는 경우가 자주 있습니다.

 

위 readNumber 함수를 예시로 들면, NumberFormatException은 언체크 예외이기 때문에 에러 처리를 강제하지 않지만 실제로는 실행시점에 발생하는 모습을 자주 볼 수 있습니다. 반면에 close 함수에서 던지는 IOException은 체크 예외로 자바에서는 반드시 처리해야 하는데, 실제로 스트림을 닫다가 실패하는 경우 프로그램이 취할 수 있는 의미 있는 동작은 없습니다. 그러므로 이 IOException을 잡아내는 코드는 불필요한 부분입니다.

 

코틀린에서는 컴파일러가 모든 예외 처리를 강제하지 않기 때문에 처리하고 싶은 예외와 그렇지 않은 예외를 직접 결정할 수 있습니다. 예외를 잡아내고 싶은 경우 try-catch를 사용하면 됩니다.

코틀린에서는 자바의 try-with-resource과 같은 특별한 문법은 제공하지 않지만, 대신 라이브러리 함수로 같은 기능을 구현합니다.

 

try 식

 

지금까지는 try를 문으로만 사용했습니다. 하지만 try는 동시에 식이기도 합니다.

위 예제를 단순화해서 finally 절을 없애고 try으로 변경해보겠습니다.

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
        // return
    }

    println(number)
}

if와 달리 try의 본문은 반드시 중괄호로 둘러싸야 합니다. 다른 문장과 마찬가지로 try의 본문도 내부에 여러 문장이 있으면 마지막 식의 값이 해당 블럭의 결괏값입니다.

위 예제는 try-catch 문 다음에 println 함수를 실행하기 때문에 catch 블록에서 null을 반환했지만, 추가적인 코드가 없다면 주석한 것처럼 return으로 함수를 종료해도 됩니다.

 

try를 식으로 사용하면 중간 변수를 도입하는 것을 피함으로써 코드를 좀 더 간결하게 만들고, 더 쉽게 예외에 대비한 값을 대입하거나 try를 둘러싼 함수를 반환시킬 수 있습니다.

'Lang > Kotlin' 카테고리의 다른 글

[Kotlin In Action] (3) - defining and calling functions  (0) 2025.10.16
[Kotlin In Action] (1) - what and why?  (0) 2025.10.09