DHistory

[Kotlin] Kotlin In Action - 02장 코틀린 기초 본문

Programming/Kotlin

[Kotlin] Kotlin In Action - 02장 코틀린 기초

ddu0422 2021. 8. 29. 21:37

기본 요소: 함수와 변수

타입 선언 생략 가능!

불변 데이터 장려!

Hello, World!

fun main(args: Array<String>) {
    println("Hello, World!")
}

함수를 선언할 때 fun 키워드를 사용한다.

파라미터 이름 뒤에 그 파라미터의 타입을 타입을 쓴다.

함수를 최상위 수준에 정의할 수 있다.

배열도 일반적인 클래스와 마찬가지다.

코틀린 표준 라이브러리는 여러 가지 표준 자바 라이브러리 함수를 간결하게 사용할 수 있게 감싼 래퍼를 제공한다.

세미콜론을 붙이지 않아도 된다.

 

함수

함수의 반환 타입은 파라미터 목록의 닫는 괄호 다음에 오는데, 괄호와 반환 타입 사이를 콜론(:)으로 구분해야 한다.

코틀린 if는 문장(statement)이 아니고 결과를 만드는 식(expression)이다.

Statement vs Expression

문(Statement)은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다.

식(Expression)은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있다.

식이 본문인 함수

식이 하나로 이루어진 경우 중괄호를 없애고 return을 제거하면서 등호(=)를 식 앞에 붙이면 더 간결하게 함수를 표현할 수 있다.

fun max(a: Int, b: Int): Int = if (a > b) a else b // 식이 본문인 함수
fun max(a: Int, b: Int): Int {                     // 블록이 본문인 함수
    return if (a > b) a else b
}

식이 본문인 함수가 자주 쓰인다.

if, when, try 등의 더 복잡한 식도 자주 쓰인다.

반환 타입을 생략할 수 있다. (1.4 버전 이상에서 explicitApi를 사용한다면 생략할 수 없다.)

 

변수

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

val question = "삶은 무엇인가" // String
val answer = 42             // Int
val yearsToCompute = 7.5e6  // Double

 

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

val(value) - 변경 불가능한(immutable) 참조를 저장하는 변수다. val 참조 객체는 불변일지라도 그 참조가 가리키는 객체의 내부 값은 변경될 수 있다.

val languages = arrayListOf("Java")
languages.add("Kotlin")

var(variable) - 변경 가능한(mutable)참조를 저장하는 변수다. 변수의 값은 바뀔 수 있다. 단, 변수의 타입은 고정되어 있다.

→ 컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론하며, 변수 선언 이후 변수 재대입이 이뤄질 때는 이미 추론한 변수의 타입을 염두에 두고 대입문의 타입을 검사한다.

var answer = 42
answer = "no answer" // Error: type mismatch (컴파일 오류 발생)

 

기본적으로는 모든 변수를 val 키워드를 사용해 불변 변수로 선언하고, 나중에 꼭 필요할 때만 var로 변경하라.

변경 불가능한 참조와 변경 불가능한 객체를 부수 효과가 없는 함수와 조합해 사용하면 코드가 함수형 코드에 가까워진다.

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

val message: String

if (canPerfromOperation()) {
    message = "Success"
} else {
    message = "Failed"
}

더 쉽게 문자열 형식 지정: 문자열 템플릿

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    println("Hello, $name!")
}

문자열 리터럴의 필요한 곳에 변수를 넣되 변수 앞에 $를 추가해야 한다.

존재하지 않는 변수를 문자열 템플리 안에서 사용하면 컴파일 오류가 발생한다.

중괄호를 둘러싼 식 안에서 큰 따옴표를 사용할 수도 있다.

 

클래스와 프로퍼티

코틀린에서는 자바보다 더 적은 양의 코드로 클래스와 관련 있는 대부분의 작업을 수행할 수 있다.

public class Person {
    private final String name;

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

    public String getName() {
        return name;
    }
}

코틀린의 기본 가시성은 public이므로 이런 경우 변경자를 생략해도 된다. (1.4 버전 이상에서 explicitAPI를 사용하면, 생략이 불가능하다.)

 

프로퍼티

클래스라는 개념의 목적은 데이터를 캡슐화하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.

자바에서는 필드와 접근자를 묶어 프로퍼티라고 부른다.

코틀린 프로퍼티는 자바의 필드와 접근자 메서드를 완전히 대신한다. val로 선언한 프로퍼티는 읽기 전용이며, var로 선언한 프로퍼티는 변경 가능하다.

class Person(
    val name: String,
    var isMarried: Boolean
)
// 자바에서 Person 클래스 사용하기
Person person = new Person("Duho", false);
System.out.println(person.getName());   // Duho
System.out.println(person.isMarried()); // false
// 코틀린에서 Person 클래스 사용하기
val person = Person("Duho", false)
println(person.name)       // Duho
println(person.isMarried)  // false

 

커스텀 접근자

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

val rectangle = Rectangle(41, 43)
println(rectangle.isSaure) // false

구현이나 성능상 차이는 없다. 차이가 나는 부분은 가독성 뿐이다.

클래스의 특성을 정의하고 싶다면 프로퍼티로 그 특성을 정의해야 한다.

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

코틀린도 자바와 마찬가지로 파일의 맨 앞에 package문을 넣을 수 있다. 패키지를 선언하면 모든 선언이 해당 패키지에 들어간다.

같은 패키지에 속해 있다면 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다.

다른 패키지에 정의한 선언을 사용하려면 임포트를 통해 선언을 불러와야 한다.

package geometry.shapes

import java.util.Random

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

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

코틀린에서는 클래스 임포트와 함수 임포트에는 차이가 없다.

모든 선언을 import 키워드로 가져올 수 있다.

최상위(Top - level) 함수는 그 이름을 써서 임포트 할 수 있다.

package geometry.example

import geometry.shapes.createRandomRectangle

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
}

코틀린에서는 어느 디렉터리에 소스코드 파일을 위치시키든 관계없다. 하지만 대부분의 경우 자바와 같이 패키지별로 디렉터리를 구성하는 편이 낫다.

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

 

선택 표현과 처리: enum과 when

when은 자바의 switch를 대치하되 훨씬 강력하다.

enum을 선언하는 방법과 스마트 캐스트에 대해서도 살펴본다.

enum 클래스 정의

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

enum은 소프트 키워드이다. enum은 class 앞에 있을 때는 특별한 의미를 지니지만 다른 곳에서는 이름에 사용할 수 있다.

→ 그래도 키워드이므로 다른 곳에서 사용하지 않는다.

enum class Color(
    val r: Int, val g: Int, b: Int
) {

    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
    INDIGO(75, 0, 130), VIOLET(238, 130, 238);

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

println(Color.BLUE.rgb()) // 255

whem으로 enum 클래스 다루기

if와 마찬가지로 when도 값을 만들어내는 식이다.

식이 본문인 함수에 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.VIOLET -> "Vain"
}

앞의 코드는 color로 전달된 값과 같은 분기를 찾는다.

한 분기 안에서 여러 값을 매치 패턴으로 사용할 수도 있다. 그럴 경우 값 사이를 콤마(,)로 분리한다.

fun getWarmth(color: Color) = when (color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

println(getWarmth(Color.ORANGE)) // warm

Color.YELLOW처럼 Color라는 enum 클래스 이름을 enum 상수 이름 앞에 붙인 전체 이름을 사용했다.

상수 값을 임포트 하면 더 간단하게 만들 수 있다.

import Color.*

fun getWarmth(color: Color) = when (color) {
    RED, ORANGE, YELLOW -> "warm"
    GREEN -> "neutral"
    BLUE, INDIGO, VIOLET -> "cold"
}

println(getWarmth(Color.ORANGE)) // warm

when과 임의의 객체를 함께 사용

코틀린에서 when은 자바의 switch보다 훨씬 강력하다.

코틀린의 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")
    }

 

분기 조건에 있는 객체 사이를 매치할 때 동등성을 사용한다.

모든 분기 식에서 만족하는 조건을 찾을 수 없다면 else 분기의 문장을 계산한다.

when의 분기 조건 부분에 식을 넣을 수 있기 때문에 많은 경우 코드를 간결하고 아름답게 작성할 수 있다.

인자 없는 when 사용

함수가 자주 호출된다면 불필요한 가비지 객체가 늘어나는 것을 방지하기 위해 함수를 고쳐 쓰는 편이 낫다.

인자가 없는 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")
}

println(mixOptimized(BLUE, YELLOW)) // GREEN

추가 객체를 만들지 않는다는 장점이 있지만 가독성은 떨어진다.

스마트 캐스트: 타입 검사와 타입 캐스트를 조합

코틀린에서는 is를 사용해 변수 타입을 검사한다. is검사는 자바의 instanceof와 비슷하다.

자바에서는 instanceof로 확인한 다음에 명시적으로 변수 타입을 캐스팅해야 한다.

코틀린에서는 컴파일러가 캐스팅을 해준다. is로 검사하고 나면 굳이 변수를 원하는 타입으로 캐스팅하지 않아도 마치 처음부터 그 변수가 원하는 타입으로 선언된 것처럼 사용할 수 있다.

→ 이를 스마트 캐스트라고 부른다.

interface Expr
class Num(val value: Int) : Expr
class Sum()

fun eval(e: Expr): Int {
    if (e is Num) {
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

println(eval(Sum(Sum(Num(1), Num(2)), Num(4)))) // 7

리팩터링: if를 when으로 변경

fun eval(e: Expr): Int =
    if (e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.right) + eval(e.left
    } else {
        throw IllegalArgumentException("Unknown expression")
    }

코틀린의 if (a > b) else b는 자바의 a > b ? a : b처럼 동작한다. if가 값을 만들어내기 때문에 자바와 달리 3항 연산자가 따로 없다.

when을 사용해 더 다듬을 수 있다.

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else -> throw IllegalArgumentException("Unknown expression")
    }

when 식을 동등성 검사가 아닌 다른 기능에도 쓸 수 있다.

if와 whem의 분기에서 블록 사용

if나 when 모두 분기에 블록을 사용할 수 있다. 블록의 마지막 문장이 블록 전체의 결과가 된다.

fun evlWithLoggin(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right;
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }

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

함수는 이 규칙이 성립하지 않는다. 식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수를 내부에 return문이 반드시 있어야 한다.

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

코틀린의 while은 자바의 while과 동일하다.

코틀린의 for는 for <아이템> in <원소들> 형태를 취한다.

while 루프

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

수에 대한 이터레이션: 범위와 수열

초깃값, 증가 값, 최종 값을 사용한 루프를 대신하기 위해 코틀린에서는 범위를 사용한다.

.. 연산자로 시작 값과 끝 값을 연결해서 범위를 만든다. (폐구간)

어떤 범위에 속한 값을 일정한 순서로 이터레이션 하는 경우를 수열이라고 부른다.

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz"
    i % 3 == 0 -> "Fizz"
    i % 5 == 0 -> "Buzz"
    else -> "$i "
}

// 1..100 범위에 정수에 대해 이터레이션한다.
for (i in 1..100) {
    print(fizzBuzz(i))
}

// 100 downTo 1은 역방향 수열을 만든다.
// step2를 붙이면 증가 값의 절댓값이 2로 바뀐다.
for (i in 100 downTo 1 step 2) {
    print(fizzBuzz(i))
}

끝 값이 개구간인 범위를 사용하려면 until을 사용한다.

val sum = 0

for (i in 1 until 10) {
    sum += 1
}

prinnt(sum) // 45

맵에 대한 이터레이션

val binaryReps = TreeMap<Char, String>();

for (c in 'A'..'F') {
    val binary = Integer.toBinaryString(c.toInt())
    binaryReps[c] = binary
}

// 맵에 대해 이터레이션한다.
// 맵의 키와 값을 두 변수에 각각 대입한다. (구조분해)
for ((letter, binary) in binaryReps) {
    println("$letter = $binary")
}

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

in으로 컬렉션이나 범위의 원소 검사

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

!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
println("Kotlin" in "Java" .. "Scala") // true 문자열 범위안에 속한다.
println("Kotlin" in setOf("Java", "Scala")) // false 해당 집합에 속하지 않는다.

코틀린의 예외 처리

함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.

함수를 호출하는 쪽에서는 그 예외를 잡아 처리할 수 있다.

발생한 예외를 함수 호출 단에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던진다.

try, catch, finally

예외를 처리하려면 try와 catch, finally 절을 함께 사용한다.

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)) // 239

코틀린은 Checked Exception과 Unchecked Exception을 구별하지 않는다.

함수가 던지는 예외를 지정하지 않고 발생한 예외를 잡아내도 되고 잡아내지 않아도 된다.

코틀린은 자바의 try-with-resource를 위한 문법을 제공하지 않는다.

try를 식으로 사용

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

val reader = BufferedReader(StringReader("not a number"))
readerNumber(reader)

try 키워드는 if나 when과 마찬가지로 식이다.

if와 달리 try의 본문을 반드시 중괄호 {}로 둘러싸야한다.

마지막 식의 값이 전체 결과 값이다.

catch 블록도 그 안의 마지막 식이 블록 전체의 값이 된다.

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

val reader = BufferedReader(StringReader("not a number"))
readerNumber(reader) // null

요약

- 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.

- 문자열 템플릿을 사용하면 문자열을 연결하지 않아도 되므로 코드가 간결해진다. 변수 이름 앞에 $를 붙이거나, 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.

- 코틀린에서는 값 객체 클래스를 아주 간결하게 표현할 수 있다.

- 다른 언어에도 있는 if는 코틀린에서 식이며, 값을 만들어낸다.

- 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.

- 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 그런 경우 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.

- for, while, do-while 루프는 자바가 제공하는 같은 키워드의 기능과 비슷하다. 하지만 코틀린의 for는 자바의 for보다 더 편리하다. 특히 맵을 이터레이션 하거나 이터레이션 하면서 컬렉션의 원소와 인덱스를 함께 사용해야 하는 경우 코틀린의 for가 더 편리하다.

- 1..5와 같은 식의 범위를 만들어낸다. 범위와 수열은 코틀린에서 같은 문법을 사용하며, for 루프에 대해 같은 추상화를 제공한다. 어떤 값이 범위 안에 들어있거나 들어있지 않은지 검사하기 위해서 in이나 !in을 사용한다.

- 코틀린 예외 처리는 자바와 비슷하다. 다만 코틀린에서는 함수가 던질 수 있는 예외를 선언하지 않아도 된다.