[코틀린 인 액션] 2장, 코틀린 기초(함수와 변수, ..., 코틀린의 예외 처리)

kindof

·

2023. 8. 14. 21:02

[코틀린 인 액션] 책을 읽으면서 이해한 내용을 정리합니다.

책의 설명을 기반으로 하되, Java와의 비교나 주관적인 생각들도 써보려고 하는데요. 코드를 작성하면서 궁금한 부분이나 다른 기본적인 내용들은 최대한 공식 문서를 참고해서 작성해보겠습니다.

 

지난 글 [코틀린 인 액션] 1장, 코틀린이란 무엇이며, 왜 필요한가? 에서는 코틀린의 기본적인 철학과 소스 코드 빌드 과정에 대해 간략하게 소개했습니다.

 

이번 시간에는 코틀린의 기초적인 내용 중에서 함수와 변수, 클래스와 프로퍼티, enum/when, while/for loop, 예외 처리에 대해서 정리해보겠습니다.

 

다만, 책의 뒷부분에서 각 내용에 대한 자세한 설명이 이어질 예정이라 이번 글에서는 짧은 예시 위주로 가볍게 정리하겠습니다.

 

설명하는 코드는 책에서 소개한 코드에 기반하여 설명을 위해 조금씩 각색한 부분이 있을 수 있습니다.

 

 

1. 함수와 변수

지난 글에서 소개했던 코드를 다시 살펴보겠습니다.

data class Person(val name: String, val age: Int? = null)

fun main(args: Array<String>) {
    val persons = listOf(Person("영희"), Person("철수", age = 29))
    val oldest = persons.maxBy { it.age ?: 0 }
    println("나이가 가장 많은 사람: $oldest")
}

 사실 이 코드에서 코틀린 함수의 많은 부분을 이해할 수 있는데요.

 

먼저, 1) 함수를 선언할 때는 fun 키워드를 사용하며 2) 파라미터 이름 뒤에 그 파라미터의 타입을 지정합니다. 또한, 반환값이 없는 함수일 때 void 같은 키워드를 붙일 필요가 없으며, 4) 변경 불가능한 참조를 저장하는 변수를 가리킬 때 val를 사용합니다. 마지막으로 5) 문자열의 형식을 지정할 때 변수 앞에 $를 추가합니다.

 

한편, 위 함수에서 사용한 listOf() 메서드와 maxBy() 메서드에서도 코틀린 함수의 특징을 살펴볼 수 있습니다.

public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
@Deprecated("Use maxByOrNull instead.", ReplaceWith("this.maxByOrNull(selector)"))
@DeprecatedSinceKotlin(warningSince = "1.4", errorSince = "1.5", hiddenSince = "1.6")
@Suppress("CONFLICTING_OVERLOADS")
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
    return maxByOrNull(selector)
}

listOf() 함수는 List<T>를 리턴 타입으로 지정하고 있으며, 메서드 자체가 등호와 식으로 이루어진 '식이 본문인 함수'입니다. 코틀린에서는 식이 본문인 함수가 자주 쓰입니다.

 

반면, maxBy() 메서드는 반환 타입으로 T?, null이 될 수 있는 제네릭 타입을 반환하고 있으며 {}로 감싸진 '블록이 본문인 함수' 특성을 보이고 있습니다.

 


 

자바에서는 변수를 선언할 때 타입이 맨 앞에 옵니다.

 

코틀린에서는 타입 지정을 생략하는 경우가 흔한데요. 예시를 보겠습니다.

// [1] 
val question = "삶, 우주 ..."
val a = 42
val b: Int = 42
val yearsToCompute = 7.5e6

// [2]
val c: Int
c = 1

// [3]
val langs = arrayListOf<String>("Java")
langs.add("Java")

// [4]
var d = 42
d = "string!"

 

[1] 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하는 것을 허용합니다.

[2] 초기화 식이 없다면 변수에 저장될 값에 대해 컴파일러가 타입 추론을 할 수 없기 때문에 타입을 반드시 지정해야 합니다.

[3] val 참조 자체는 불변이라도 참조가 가리키는 객체의 내부 값은 변경될 수 있습니다.

[4] var 키워드를 사용하면 변수의 값을 변경할 수 있지만 타입은 고정되어 변경할 수 없습니다.

 

이정도로 코틀린 변수의 특징은 가볍게 이해하고 넘어가면 될 것 같습니다.

 

 

2. 클래스와 프로퍼티

코틀린 클래스는 이후 4장에서 자세하게 소개할 예정이므로 여기서는 간단한 예시만 하나 보고 넘어가도록 하겠습니다.

 

아래는 자바로 작성된 JavaPerson 클래스이며 name, age 두 개의 필드를 갖는 읽기 전용 클래스입니다. 

public class JavaPerson {
    private final String name;
    private final Integer age;

    public JavaPerson(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

}

 

이 코드를 Intellij의 자바-코틀린 변환기를 통해 변환하면 아래와 같은 결과물을 볼 수 있습니다.

class JavaPerson(val name: String, val age: Int)

아주 단순한 코드로 변환이 되었는데요. 코틀린은 기본적으로 public을 접근 지시자로 사용하므로 변경자를 생략할 수 있습니다. 또한, 데이터만 유통하기 위한 클래스는 위와 같이 한 줄로 축약할 수 있게 되는데, 이를 값 객체(value object)라고 합니다.

 


 

한편, 자바에서는 데이터를 필드에 저장하며, 멤버 필드의 가시성은 보통 private으로 설정합니다.

 

클래스는 자신을 사용하는 클라이언트가 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드(Accessor method)를 제공하는데요.

보통은 Getter, Setter 메서드가 이러한 역할을 합니다.

 

자바에서는 필드와 접근자를 한데 묶어 프로퍼티(Property)라고 부르며, 코틀린은 프로퍼티를 언어 기본 기능으로 제공하여 자바의 필드와 접근자 메서드를 완전히 대체합니다.

class Person(
    val name: String,
    var isMarried: Boolean
)

여기서 val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능합니다.

 

아래 코드를 보면 Person2 클래스의 name 필드는 val, isMarried는 var로 선언했습니다.

 

class Person2(
    val name: String,
    var isMarried: Boolean
)

fun main() {
    val person = Person2("josh", true)

    println("person's name:${person.name} age:${person.isMarried}")
    person.isMarried = false
    person.name = "jjj"
}

이 때, 코틀린에서는 Getter 대신에 프로퍼티에 직접 접근하여 값을 가져올 수 있고 변경 가능한 프로퍼티의 세터도 마찬가지 방식으로 동작합니다.

 

단, 여기서 name 필드는 val 읽기 전용 필드이므로 person.name = "jjj" 에서는 컴파일 에러가 발생합니다.

 

3. enum/when

enum class RGB (
    val r: Int, val g: Int, val b:Int
)
{
    RED(255, 0, 0),
    GREEN(0, 255, 0),
    BLUE(0, 0, 255)
    ;

    fun rgb() = (r * 256 + g) * 256 + b
}

inline fun <reified T : Enum<T>> printAllValues() {
    println(enumValues<T>().joinToString { it.name })
}

fun main() {
    printAllValues<RGB>() // RED, GREEN, BLUE
    println("Color red's rgb = ${RGB.RED.rgb()}") // Color red's rgb = 16711680
}

enum에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 선언하고, 각 enum 상수를 정의할 때는 그 상수에 해당하는 프로퍼티 값을 지정해야만 합니다. 

 

그리고 위 코드의 rgb() 메서드처럼 enum 클래스 안에서 메서드를 정의할 수 있습니다.

 

또한, 여기서는 책에서 제시한 코드와 조금 달리 printAllValues()라는 코드도 추가했는데요.

 

printAllValues() 함수는 reified 타입 매개변수 T를 통해 함수 안에서 제네릭 타입의 실제 타입 정보를 사용할 수 있도록 합니다. enumValues<T>() 메서드를 통해 제네릭 타입 T의 enum 값 배열을 가져오고, enum 필드들의 name을 String 형태로 join합니다.

 


한편, JDK17에서는 Switch문 Pattern Matching / Guarded Pattern을 사용할 수 있었는데요.

 

[JAVA] JDK 17에서 제공하는 새로운 기능들 정리해보기

1. 빨라진 JDK Release 작년 9월, JDK 17이 최초로 공개되었고 바로 얼마 전에는 JDK 19 버전이 출시되었습니다. 비록 JDK 19가 LTS 버전은 아니지만, 내년 하반기에는 JDK 21 LTS 버전이 출시된다고 하는데요.

studyandwrite.tistory.com

 

코틀린에서는 when 키워드를 사용해 분기 조건에 상수 뿐만 아니라 임의의 객체까지 허용하여 코드를 작성할 수 있습니다. 

fun processColor(color: RGB) {
    when (color) {
        RGB.RED -> println("The color is RED")
        RGB.GREEN -> println("The color is GREEN")
        RGB.BLUE -> println("The color is BLUE")
    }
}

fun mixColor(c1: RGB, c2: RGB) {
    when (setOf(c1, c2)) {
        setOf(RGB.RED, RGB.GREEN) -> println("RED + GREEN")
        setOf(RGB.RED, RGB.BLUE) -> println("RED + BLUE")
        setOf(RGB.GREEN, RGB.BLUE) -> println("GREEN + BLUE")
    }
}

fun main() {
    processColor(RGB.RED) // The color is RED
    mixColor(RGB.RED, RGB.GREEN) // "RED + GREEN"
}

 

4. while/for loop

[1] 코틀린의 while Loop는 자바와 동일하고 for Loop는 자바의 for-each Loop에 해당하는 형태만 존재합니다. 

fun main() {
    val items = listOf<String>("a", "b", "c")
    for (item in items) { println(item) }
}

 

[2] 코틀린은 수에 대한 이터레이션에 대해 기본적인 범위를 폐구간으로 합니다.

val oneToTen = 1..10
for (num in oneToTen) {
    println(num)
}

// output
1
2
...
10

 

[3] 코틀린의 Map에 대한 이터레이션은 아래와 같이 가능합니다.

fun main() {
    val map = mutableMapOf(
        "apple" to 5,
        "banana" to 3,
        "orange" to 7
    )

    val bananaCount = map["banana"]
    println("Banana count: $bananaCount") // Output: Banana count: 3

    map["banana"] = 10
    println("Updated banana count: ${map["banana"]}") // Output: Updated banana count: 10

    map["grape"] = 8
    println("Grape count: ${map["grape"]}") // Output: Grape count: 8

    println("Iterating through the Map:")
    for ((key, value) in map) {
        println("Key: $key, Value: $value")
    }
}

to 키워드를 통해 Map의 원소를 초기화할 수 있고 for Loop를 사용해 맵의 Key, Value 쌍을 순회할 수 있습니다.

 

또한, get과 put 대신에 map[key]나 map[key] = value를 사용하여 값을 가져오고 설정할 수 있습니다.

 

5. 코틀린의 예외 처리

코틀린 예외 처리가 자바와 가장 다른 부분은 바로 throws 절이 코드에 없다는 점입니다.

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

만약 위 코드를 자바로 작성했다면 BufferedReader가 던질 수 있는 IOException을 처리하기 위해 함수 선언 뒤에 throws IOException이 필요했을 것입니다.

 

이는 IOException이 컴파일 시점에 명시적으로 처리해야 하는 Checked Exception이기 때문인데, 코틀린은 Checked Exception, Unchecked Exception을 구분하지 않기 때문입니다.

 

책에서 저자는 "프로그래머들이 의미 없이 예외를 다시 던지거나, 예외를 잡되 처리하지는 않고 그냥 무시하는 코드를 작성하는 경우가 흔하다. 그로 인해 예외 처리 규칙이 실제로는 오류 발생을 방지하지 못하는 경우가 자주 있다"라고 말하는데요.

 

코틀린은 이러한 철학을 바탕으로 함수에서 던지는 예외를 지정하지 않고, 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다고 합니다.

 

이 부분이 인상깊은 부분인 것 같은데, 느낌 상 나중에 다시 조금 더 구체적인 글을 쓸 것 같습니다.

 

6. 정리 / Reference

이번 글에서는 코틀린의 기초가 되는 변수와 함수부터 예외 처리의 기본적인 컨셉에 대해 가볍게 정리해봤습니다.

 

생략한 내용도 꽤 많지만, 책의 뒷부분에서 각 내용에 대한 설명을 깊게 할 예정이라고 하니 그 때 더 많은 예시 코드를 가지고 설명해보도록 하겠습니다.

 

감사합니다.

 

 

 

Kotlin in Action | 드미트리 제메로프 - 교보문고

Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀

product.kyobobook.co.kr