이직 후 코틀린을 새로 배우면서 익히는 정보들을 기억하기 위한 글입니다.
아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.

변수의 타입 선언

자바에서는 변수를 선언할 때 타입이 맨 앞에 온다. 코틀린에서는 타입 지정을 생략하는 경우가 흔하다. 타입으로 변수 선언을 시작하면 타입을 생략할 경우 식과 변수 선언을 구별할 수 없다. 그런 이유로 코틀린에서는 키워드로 변수 선언을 시작하는 대신 변수 이름 뒤에 타입을 명시하거나 생략하게 허용한다.

- kotlin IN ACTION 64p -

초기화에 따른 변수 타입 선언

초기화 식을 사용하지 않고 변수를 선언하려면 변수 타입을 반드시 명시해야 한다.
초기화 식이 없다면 변수에 저장될 값에 대해 아무 정보가 없기 때문에 컴파일러가 타입을 추론할 수 없다. 따라서 그런 경우 타입을 반드시 지정해야 한다.

- kotlin IN ACTION 65p -

변경 가능한 변수와 변경 불가능한 변수

val(값을 뜻하는 value에서 따옴) - 변경 불가능한(immutable) 참조를 저장하는 변수다. val로 선언된 변수는 일단 초기화하고 나면 재 대입이 불가능하다. 자바로 말하자면 final 변수에 해당한다.
var(변수를 뜻하는 variable에서 따옴) - 변경 가능한(mutable) 참조다. 이런 변수의 값은 바뀔 수 있다. 자바의 일반 변수에 해당한다.

- kotlin IN ACTION 65p -

타입 추론의 방법

컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론하며, 변수 선언 이후 변수 재 대입이 이뤄질 때는 이미 추론한 변수의 타입을 염두에 두고 대입문의 타입을 검사한다.
어떤 타입으로 변환하거나, 값을 변수에 대입할 수 있는 타입으로 강제 형 변환해야 한다.

- kotlin IN ACTION 67p -

클래스의 기본 제어자

자바 클래스의 기본 제어자는 default이지만, 코트린 클래스의 기본 제어자는 public이다.
기본 제어자란 클래스의 각 변수에 제어자를 따로 설정하지 않았을 경우 설정되는 제어자를 말한다.

- kotlin IN ACTION 70p -

프로퍼티

클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다. 자바에서는 데이터를 필드에 저장하며, 멤버 필드의 가시성은 보통 비공개다. 클래스는 자신을 사용하는 클라이언트가 그 데이터에 접근하는 통로로 쓸 수 있는 접근자 메서드를 제공한다. 보통은 필드를 읽기 위한 게터를 제공하고 필드를 변경하게 허용해야 할 경우 세터를 추가 제공한다.
자바에서는 필드와 접근자를 한데 묶어 프로퍼티라고 부르며 프로퍼티라는 개념을 활용하는 프레임워크가 많다.

코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. 클래스에서 프로퍼티를 선언할 때는 앞에서 살펴본 변수를 선언하는 방법과 마찬가지로 val이나 var를 사용한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.

class Person(
       val name: String, // 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
       var isMarried: Boolean // 쓸 수 있는 프로퍼티로, 코틀린은 (비공개) 필드, (공개) 게터, (공개) 세터를 만들어낸다.
)


게터와 세터의 이름을 정하는 규칙에는 예외가 있다. 이름이 is로 시작하는 프로퍼티의 게터에는 get이 붙지 않고 원래 이름을 그대로 사용하며, 세터에는 is를 set으로 바꾼 이름을 사용한다.

코틀린에서는 게터와 세터 대신 프로퍼티를 사용할 수 있다. 

>>> val person = Person("NAME", true) // new 키워드를 사용하지 않고 생성자를 호출한다.
>>> println(person.name) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
NAME
>>> println(person.isMarried) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
true

- kotlin IN ACTION 71 ~ 72p -

커스텀 접근자

class Rectangle(val height: Int, val width: Int) {
        val isSquare: Boolean
            get() { // 프로퍼티 게터 선언
                return height == width
            }

           or
           get() = height == width
}

클라이언트가 프로퍼티에 접근할 때마다 게터가 프로퍼티 값을 매번 다시 계산한다.

- kotlin IN ACTION 73 ~ 74p -

코틀린 소스코드 구조: 디렉터리와 패키지

자바 패키지의 경우 하나의 패키지 아래에 여러 파일이 있어서 해당 패키지를 import 하면 하위 파일들에 접근할 수 있다.
코틀린은 한 파일에 여러 개의 클래스를 넣을 수 있어 파일마다 패키지를 지정하여 사용할 수 있다.

만약 자바와 코틀린을 같이 사용하게 된다면 자바 소스코드 구조를 따르는 게 맞다. 자바 방식을 따르지 않으면 자바 클래스를 코틀린 클래스로 마이그레이션 하는데 문제가 발생할 수 있다. 하지만 여러 클래스를 한 파일에 넣는 것을 주저해서는 안된다. 코틀린의 경우 클래스 소스코드 크기가 작은 경우가 자주 있다.

- kotlin IN ACTION 74 ~ 77p -

Enum 클래스

enum class Color (val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0), ORANGE(255, 165, 0), YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255), INDOGO(75, 0, 130), VIOLET(238, 130, 238);

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

코틀린에서 유일하게 세미콜론을 사용해야 하는 부분이다.
각 상수를 정의할 때 클래스에 선언한 프로퍼티 값을 지정해야 한다.

- kotlin IN ACTION 78p -

when

코틀린의 if문과 마찬가지고 식이 본문인 함수에 when을 바로 사용할 수 있다.

fun getMnemonic(color: Color) = 
    when(color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Gave"
        Color.BLUE ->  "Battle"
        Color.INDIGO ->  "In"
        Color.BIOLET -> "Vain"
    }
========================================================================
fun getMnemonic(color: Color) = 
    when(color) {
        Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
        Color.GREEN -> "neutral"
        Color.BLUE, Color.INDIGO, Color.BIOLET  ->  "cold"
    }
========================================================================
import colors.Color

fun getMnemonic(color: Color) = 
    when(color) {
       RED, ORANGE, YELLOW -> "warm"
       GREEN -> "neutral"
       BLUE, INDIGO, BIOLET  ->  "cold"
    }
========================================================================
자바와 달리 코틀린은 when의 분기 조건에 임의의 객체를 사용할 수 있다.

fun mix(c1: Color, c2: Color) = 
    when(setOf(c1, c2)) {
        setOf(RED, YELLOW) -> ORANGE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(BLUE, VIOLET) -> INDIGO
        else -> throw Exception("Dirty Color")
    }

set 컬렉션을 이용하여 c1이 RED 거나 YELLOW 둘 중 순서는 상관이 없다. 분기 조건이 맞는 게 없다면 else 구문을 실행한다.
위의 예제는 분기를 검사할 때마다 set 객체가 생성되어 효율적이지 않다. 그렇다고 성능에 큰 영향을 미치지는 않는다.
위의 예제보다 조금 더 효율적인 when은 아래와 같다.

fun mixOptimized(c1: Color, c2: Color) = 
    when { // 인자 값이 없음
        (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) -> ORANGE
        (c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
        (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
        else -> throw Exception("Dirty Color")
    }

위의 예제 보다 효율적이긴 하지만 가독성이 떨어진다는 단점이 있다.

- kotlin IN ACTION 77 ~ 86p -

블록이 본문인 함수 Return

"블록의 마지막 식이 블록의 결과"라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립한다.

if(e is Num) {
    println("e is Number")
    e.value // 마지막 식이 블록의 결과로 반환이 된다.
}

위의 예제처럼 블록 가장 마지막 식이 결과로 반환이 된다.
식이 본문인 함수는 블록을 본문으로 가질 수 없고 본문인 함수는 내부에 return문이 반드시 있어야 한다.

- kotlin IN ACTION 88 ~ 89p -

대상을 이터레이션: while과 for 루프

코틀린 특성 중 자바와 가장 비슷한 것이 이터레이션이다.
코틀린의 for는 C#과 마찬가지로 for <아이템> in <원소들> 형태를 취한다.
코틀린에는 while과 do-while 루프가 있다

while(조건) {
     /*...*/
}

do {
     /*...*/
} while(조건)


코틀린 for문은 범위를 사용한다.
범위는 기본적으로 두 값으로 이뤄진 구간이다. 보통은 그 두 값은 정수 등의 숫자 타입의 값이며, .. 연산자로 시작 값과 끝 값을 연결해서 범위를 만든다.

ex) for (x in 1..10)

코틀린의 범위는 폐구간(닫힌 구간) 또는 양끝을 포함하는 구간이다.

증가 값을 같은 범위도 있다.

ex) for (x in 100 downTo 1 step 2)

증가 값 step을 갖는 수열에 대해 이터레이션한다. 증가 값을 사용하면 수를 건너뛸 수 있다. 증가 값을 음수로 만들면 정방향 수열이 아닌 역방향 수열을 만들 수 있다. 이 예제에서 100 downTo 1은 역방향 수열을 만든다(역방향 수열의 기본 증가 값은 -1이다)
그 뒤에 step 2를 붙이면 증가 값의 절댓값이 2로 바뀐다(이때 증가 값의 방향은 바뀌지 않는다. 따라서 증가 값은 실제로는 -2와 같다).

또한 마지막 값을 포함하지 않는 범위도 있다.

ex) for(x in 0 until size) == for(x in 0..size-1)

위의 예제에서 size가 100이라고 했을 때 99까지가 범위가 된다.

======================================
for(x in 1..10) print("${x}, ") // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
for(x in 1 until 10) print("${x}, ") // 1, 2, 3, 4, 5, 6, 7, 8, 9, 
for(x in 1..10 step 2) print("${x}, ") // 1, 3, 5, 7, 9,
for(x in 10 downTo 1) print("${x}, ") // 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,
for(x in 10 downTo 1 step 2) print("${x}, ") // 10, 8, 6, 4, 2, 
======================================

- kotlin IN ACTION 90 ~ 92p -

맵에 대한 이터레이션

fun main() {
    val binaryReps = TreeMap<Char, String>()

    for(c in 'A'..'F'){ // 문자 타입도 이터레이션할 수 있다.
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary // mpa[key] = value 형식으로 자바로 바꾸면 map.put(key, value)와 같다.
    }

    for((letter, binary) in binaryReps) { // 객체에 대한 key와 value를 가져올 수 있다.
        println("${letter} = ${binary}")
    }
}

>>> 
A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110

===============================================================
fun main() {
    val list = arrayListOf("10", "11", "12")

    for((idx, ele) in list.withIndex()) { // withIndex()를 이용하여 list의 인덱스 값을 구할 수 있다.
        println("${idx}: ${ele}")
    }
}

- kotlin IN ACTION 92 ~ 94p -

in  연산자

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

println(isLetter('q')) // true
println(isNotDigit('x')) // true

in 연산자로 범위 안에 속해 있는지, !in 연산자로 범위 안에 속해 있지 않은지를 확인할 수 있으면 when에도 사용할 수 있다.

fun recognize(c: Char)
    = when(c) {
        in '0'..'9' -> "It's a digit!"
        in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
        else -> "I don't konw..."
}

println(recognize('8')) // It's a digit!

c in 'a'..'z' ===> 'a' <= c && c <= 'z'와 동일

ex) println("Kotlin" in "Java".."Scala") // true
위의 예제에서는 String에 있는 Comparable 구현이 두 문자열을 알파벳 순서로 비교하기 때문에 여기 있는 in 검사에서도 문자열을 알파벳 순서로 비교한다.

ex) println("Kotlin" in setOf("Java", "Scala") // false
이 집합에는 "Kotlin"이 들어있지 않아서 false가 출력된다.

범위는 문자에만 국한되지 않는다. 비교가 가능한 클래스라면(java.lang.Comparable 인터페이스를 구현한 클래스라면) 그 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다.

- kotlin IN ACTION 94 ~ 95p -

코틀린의 예외 처리

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

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

val percentage
    = if(number in 0..100)
           number
       else 
           throw IllegalArgumentException("A percentage value must be between 0 and 100: $number")

if의 조건이 참이면 percentage 변수가 number의 값으로 초기화되지만, 조건이 거짓이면 초기화 되지 않는다.

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

    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}

자바 코드와 가장 큰 차이는 throws(이 경우 s가 붙어있다) 절이 코드에 없다는 점이다. 자바에서는 함수를 작성할 때 함수 선언 뒤에 throws IOexception을 붙여야 한다. 이유는 IOException이 체크 예외(checked exception)이기 때문이다. 자바에서는 체크 예외를 명시적으로 처리해야 한다. 어떤 함수가 던질 가능성이 있는 예외나 그 함수가 호출한 다른 함수에서 발생할 수 있는 예외를 모두 catch로 처리해야 하며, 처리하지 않은 예외는 throws 절에 명시해야 한다.

다른 최신 JVM 언어와 마찬가지로 코튼린도 체크 예외와 언체크 예외를 구별하지 않는다. 코틀린에서는 함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다. 실제 자바 프로그래머들이 체크 예외를 사용하는 방식을 고려해 이렇게 코틀린 예외를 설계했다.

BufferedReader.close는 IOException을 던질 수 있는데, 그 예외는 체크 예외이므로 자바에서는 반드시 처리해야 한다. 하지만 실제 스트림을 닫다가 실패하는 경우 특별히 스트림을 사용하는 클라이언트 프로그램이 취할 수 있는 의미 있는 동작은 없다. 그러므로 이 IOException을 잡아내는 코드는 그냥 불필요하다.

try도 if와 when과 마찬가지고 본문을 식으로 사용할 수 있다.

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

        println(number
    }

    val reader = BufferedReader(StringReader("239"))
    readNumber(reader)
}

- kotlin IN ACTION 96 ~ 100p -

 

요약

  • 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
  • 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
  • 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.
  • 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.
  • 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.
  • for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for보다 더 편리하다. 특히 맵을 이터레이션 하거나 이터레이션 하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리하다.
  • 1..5와 같은 식은 범위를 만들어낸ㄴ다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어 있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.
  • 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.

'개발 언어 > Kotlin' 카테고리의 다른 글

Kotlin 문자열과 정규식 다루기  (0) 2022.01.19
Kotlin 함수 정의와 호출  (0) 2022.01.11

+ Recent posts