devseop08 님의 블로그

[Basic] 1. 코틀린 기초 본문

Language/Kotlin

[Basic] 1. 코틀린 기초

devseop08 2025. 6. 1. 20:51

2.1 기본 요소 : 함수와 변수

  • 코틀린이 어떻게 변경 가능한 데이터보다 변경할 수 없는 불변 데이터 사용을 권장하는지와 왜 불변 데이터가 더 좋은 것인지 살펴본다.2.1.1 첫 번째 코틀린 프로그램 작성 : Hello, World!
fun main(){
    println("Hello, world!") 
}
  • 함수 선언 시, fun 키워드를 선언한다.
  • 함수를 모든 코틀린 파일의 최상위 수준에 정의할 수 있으므로 클래스 안에 함수를 넣어야 할 필요는 없다.
  • 최상위에 있는 main 함수를 어플리케이션의 진입점으로 지정할 수 있다.
  • 코틀린 표준 라이브러리는 수많은 표준 자바 라이브러리 함수에 대해 더 간결한 구문을 사용할 수 있게 해주는 래퍼를 제공한다.
  • println도 래퍼 중 하나이다.

2.1.2 파라미터와 반환값이 있는 함수 선언

  • 코틀린 함수의 기본 구조
fun max(a : Int, b: Int) : Int {
    return if (a > b) a else b
}
  • 코틀린에서 if는 식이지 문이 아니다. 코틀린 if 식은 자바의 삼항 연산자와 비슷하다.
  • 식은 값을 만들어내며, 다른 식의 하위 요소로 계산에 참여할 수 있다.
  • 코틀린에 루프(for, while, do/while)를 제외한 대부분의 제어 구조가 식이라는 점이 자바와 다른 점이다.
  • 함수 호출
fun main(){
    println(max(1, 2))
    // 2
}
  • 코틀린에서는 대입을 항상 문(구문)으로 취급한다. 이는 값을 변수에 대입해도 그 대입 연산 자체는 아무값도 돌려주지 않는다는 뜻이다.
  • 대입이 항상 문으로 취급되면, 대입 연산과 비교 연산 시의 혼동을 피할 수 있다. 이런 혼동은 자바나 C/C++과 같이 대입을 식으로 취급하는 언어에서 자주 발생하는 버그의 원인이다.
val number : Int
val alsoNumber = i = getNumber()

// 컴파일 에러 
// ERROR : Assignments are not expressions

2.1.3 식 본문을 사용해 함수를 더 간결하게 정의

  • max 함수 본문이 if 식 하나로만 이뤄져 있기 때문에 이 식을 함수 본문 전체로 하고 중괄호를 없앤 후 return을 제거할 수 있다.
fun max(a : Int, b : Int) : Int = if (a > b) a else b
fun max(a : Int, b : Int) = if (a > b) a else b // 반환 타입 생략 가능
  • 코틀린은 정적 타입 지정 언어이므로, 컴파일 시점에 모든 식의 타입을 지정해야 하지 않는가?
    • 실제로 모든 변수나 모든 식에는 타입이 있으며, 모든 함수는 반환 타입이 정해져야 한다.
    • 단, 식 본문 함수의 경우에는, 굳이 사용자가 반환 타입을 지정하지 않아도, 컴파일러가 함수
      본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다.
    • 이러한 분석을 타입 추론이라 한다.
    • 블록 본문 함수의 경우에는, 반드시 반환 타입을 지정하고, return 문을 사용해 반환값을 명시해야 한다.
    • 블록 본문 함수가 긴 경우, return 문이 여럿 들어있는 경우가 있기 때문에, 어떤 타입의 값을 반환하고 어느 위치에서 반환하는지 반드시 명시해야 하는 것이다.

2.1.4 데이터를 저장하기 위해 변수 선언

val question : String = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer : Int = 42
  • 타입 추론 이용
val question = "삶, 우주, 그리고 모든 것에 대한 궁극적인 질문"
val answer = 42
  • 변수를 선언하면서 즉시 초기화하지 않으면 컴파일러가 변수의 타입을 추론할 수 없다.
fun main{
    val answer
    answer = 42 // 타입 추론 불가

    val answer : Int
    answer = 42
}

2.1.5 변수를 읽기 전용 변수나 재대입 가능 변수로 표시

  • val은 읽기 전용 참조를 선언한다. val로 선언된 변수는 단 한 번만 대입될 수 있다. 일단 초기화하고 나면 다른 값을 대입할 수 없다(자바의 final 변경자)
  • 블록 안에 val로 선언된 변수에 대한 대입문이 여러 개 존재할 수도 있는데, 블록이 실행될 때 블록 안의 val로 선언된 변수에 대한 대입문들 중 오직 하나의 대입문만 실행된다면, 컴파일러는 이를 인지할 수 있기 때문에, val로 선언된 변수에 대한 대입문이 여러 개 존재할 수 있는 것이다.
fun canPerformOperation() : Boolean {
    return true
}

fun main() {
    val result: String
    if(canPerformOperation()){
        result = "Success"
    }
    else{
        result = "Can't perform operation"
    }
}
  • var은 재대입 가능한 참조를 선언한다. 초기화가 이뤄진 다음이라도 다른 값을 대입할 수 있다.
  • val로 선언된 변수가 가리키는 객체의 내부 값은 변경 가능할 수도 있음을 잊어선 안 된다.
fun main(){
    val languages = mutableListOf("Java")
    languages.add("Kotlin")
}
  • var 키워드를 사용하면 변수의 값을 변경할 수 있지만, 변수의 타입은 고정된다.
  • 컴파일러는 변수 선언 시점의 초기화 식으로부터 변수의 타입을 추론한다.
  • 어떤 타입의 변수에 다른 타입의 값을 저장하고자 한다면, 변환 함수를 사용하거나, 강제 형 변환 연산을 사용해야 한다.

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

  • 변수 이름 앞에 $를 덧붙이면 변수를 문자열 안에 참조할 수 있다.
fun main(){
    val input = readln()
    val name = if (input.isNotBlank()) input else "Kotlin"
    println("Hello, $name!");
}
  • $ 문자를 문자열에 넣고 싶으면 백슬래시()를 사용해 $를 이스케이프시켜야 한다.
fun main(){
    println("\$x")
}
  • 문자열 템플릿 안에 식을 사용할 수도 있다.
fun main(){
    val name = readln()
    if(name.isNotBlank()){
        println("Hello, ${name.length}-letter person")
    }
}
  • 한글을 문자열 템플릿에서 사용할 때 주의할 점
    • 코틀린에서는 한글도 변수명으로 사용 가능하기 때문에 문자열 템플릿 작성이 영문 변수명 바로 뒤에 한글명이 붙는 경우를 주의해야 한다.
    • 이 문제를 해결하는 방법은 "${name}님 반가워요!"처럼 변수 이름을 {}로 감싸는 것이다.
  • 문자열 템플릿 안에 if 식과 같은 더 복잡한 식을 사용할 수도 있다.
fun main(){
    val name = readln()
    println("Hello ${if (name.isBlank()) "someone" else name}!")
}

2.2 행동과 데이터 캡슐화: 클래스와 프로퍼티

다른 객체 지향 언어와 마찬가지로 코틀린도 클래스라는 추상화를 제공한다. 하지만 코틀린을 활용하면 다른 객체 지향 언어를 사용할 때보다 더 적은 양의 코드로 대부분의 공통적인 작업을 수행할 수 있다.

/* 자바 */
public class Person{
    private final String name;

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

    public String getName(){
        return name;
    }
}
/* 코틀린 */
class Person(val name : String)

2.2.1 클래스와 데이터를 연관시키고, 접근 가능하게 만들기: 프로퍼티(필드와 접근자: 게터, 세터)

클래스라는 개념은 데이터를 캡슐화시키고 캡슐화한 데이터를 다루는 코드를 한 주체 안에 가두는 것이다.

  • 코틀린 프로퍼티는 자바의 필드와 접근자 메소드를 완전히 대신한다.
  • 코틀린에서 프로퍼티를 선언하는 방식은 프로퍼티와 관련있는 접근자를 선언하는 것이다.
class Person(
    val name : String,      // 읽기 전용 프로퍼티로, 코틀린은 (비공개) 필드와 
                            // 필드를 읽는 단순한 (공개) 게터를 만들어낸다.
    var isStudent : Boolean // 재대입 가능 프로퍼티로, 코틀린은 (비공개) 필드와 
                            // 필드를 읽는 (공개) 게터, (공개) 세터를 만들어낸다.
)
  • 접근자의 디폴트 구현은 뻔하다. 값을 저장하기 위한 필드가 생성되고, 게터는 그 필드 값을 반환하며 세터는 그 필드의 값을 변경한다.
  • 원한다면 다른 로직을 사용해 프로퍼티 값을 계산하거나 변경하는 커스텀 접근자를 선언할 수도 있다.
  • 코틀린에서 선언된 Person 클래스를 자바 코드에서 사용
public class Demo{
    public static void main(String[] args){
        Person person = new Person("Bob", true);

        System.out.println(person.getName());

        System.out.println(person.isStudent()); 

        person.setStudent(false); 

        System.out.println(person.isStudent());
    }
}
  • is로 시작하는 프로퍼티의 게터 접근자 메소드에는 get이 붙지 않고 원래 이름을 사용한다.
  • is로 시작하는 프로퍼티의 세터 접근자 메소드는 is를 set으로 바꾼 이름을 사용한다.
  • 코틀린에서 선언된 Person 클래스를 코틀린 코드에서 사용
fun main(){
    val person = Person("Bob", true) // 생성 시 new 연산자 미사용

    println(person.name)             // name 필드 게터 getName() 호출

    println(person.isStudent)        // isStudent 필드 게터 isStudent() 호출

    person.isStudent = false         // isStudent 필드 세터 setStudent() 호출

    println(person.isStudent)        // isStudent 필드 게터 isStudent() 호출
}

2.2.2 프로퍼티 값을 저장하지 않고 계산 : 커스텀 접근자

프로퍼티 접근자의 커스텀 구현이 필요한 일반적인 경우는 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 직접적인 결과인 경우이다.(on the go 프로퍼티: 프로퍼티에 접근할 때 계산할 수 있는 프로퍼티)

class Rectangle(val height: Int, val width: Int){
    val isSquare: Boolean // on the go '프로퍼티' 
        get(){  // 프로퍼티 커스텀 게터 선언
            return height == width
        }
}

식 본문 함수 구문을 사용해 작성할 수도 있다.

class Rectangle(val height: Int, val width: Int){
    val isSquare: Boolean
        get() = height == width  // 프로퍼티 커스텀 게터 선언, 식 본문 함수 구문 사용
}

프로퍼티 접근자를 커스텀 구현하더라도 프로퍼티 호출과 접근 방식은 이전과 같다.

fun main(){
    val rectangle = Rectangle(41, 43)
    println(rectangle.isSquare) // isSquare 프로퍼티 게터 isSquare() 호출
}

커스텀 게터를 정의하는 방식과 클래스 안에 파라미터가 없는 함수를 정의하는 방식 모두 구현이나 성능에는 차이가 없다.

일반적으로 클래스의 특성을 기술하고 싶다면 프로퍼티로 그 특성을 정의해야 하고, 클래스의 행동을 기술하고 싶다면 프로퍼티가 아니라 멤버 함수를 선택한다.

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

  • 코틀린은 클래스를 조직화하기 위해 패키지라는 개념을 사용한다.
  • 모든 코틀린 파일엔 맨 앞에 package 문이 올 수 있다.
  • 같은 패키지에 속해 있다면, 다른 파일에서 정의한 선언일지라도 직접 사용할 수 있다. 반면 다른 패키지에 정의한 선언을 사용하려면 해당 선언을 불러와야 한다.
  • 파일 맨 앞의 package 문 아래에 import 키워드를 사용해서 다른 패키지를 불러올 수 있다.
  • 자바에서는 패키지의 구조와 일치하는 디렉터리 계층 구조를 만들고 클래스의 소스 코드를 그 클래스가 속한 패키지와 같은 디렉터리에 위치시켜야 한다.
  • 코틀린에서는 여러 클래스를 같은 파일에 넣을 수 있고, 이름도 마음대로 정할 수 있다.
  • 코틀린에서는 디스크 상의 어느 디렉터리에 소스코드 파일을 위치시키든 관계없다.

2.3 선택 표현과 처리: 이넘과 when

2.3.1 이넘 클래스와 이넘 상수의 정의

package ch02.colors

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
  • 코틀린에서 enum은 소프트 키워드다. 변수명을 enum으로 지을 수 있다. class는 하드 키워드로 변수명으로 사용할 수 없다.
  • 이넘 상수도 일반적인 클래스와 마찬가지로, 생성자와 프로퍼티 선언 문법을 사용할 수 있다.
package ch02.colors

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), 
    INDIGO(75, 0, 130), 
    VIOLET(238, 130, 238);  // 이넘 상수 목록과 메서드 선언 사이에 ; 필수

    fun rgb() = (r * 256 + g) * 256 + b   
    fun printColor() = println("$this is $rgb")
}

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

    Color.GREEN.printColor()
}

2.3.2 when으로 이넘 클래스 다루기

  • if와 마찬가지로 when도 값을 만들어내는 식이다. => 식 본문 함수에 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"     
    }
fun main(){
    println(getMnemonic(Color.BLUE)) // Battle
}
  • 자바와 달리 각 분기의 끝에 break를 넣지 않아도 된다.
  • 한 분기 안에서 콤마로 분리하면 여러 값을 패턴으로 사용할 수도 있다.
fun measureColor() = Color.ORANGE

fun getWarmthFromSensor() : String {
    val color = measureColor()
    return when (color){
        Color.RED, Color.ORANGE, Color.YELLOW -> "warm (red = ${color.r})"
        Color.GREEN -> "neutral (green = ${color.g})"
        Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold (blue = ${color.b})"     
    }
}

fun main(){
    println(getWarmthFromSensor()) // warm (red = 255)
}
  • 상수 값들을 임포트하면, 이넘 클래스 이름의 반복을 피할 수 있다.
package ch02.colors.Color.*

fun measureColor() = ORANGE

fun getWarmthFromSensor() : String {
    val color = measureColor()
    return when (color){
        RED, ORANGE, YELLOW -> "warm (red = ${color.r})"
        GREEN -> "neutral (green = ${color.g})"
        BLUE, INDIGO, VIOLET -> "cold (blue = ${color.b})"     
    }
}

fun main(){
    println(getWarmthFromSensor())
}

2.3.3 when 식의 대상을 변수에 캡처

  • when 바깥의 코드를 불필요한 변수(color)로 더럽히는 일을 방지하기 위해 when 식의 대상 값을 변수에 넣어주고 사용할 수 있다.(캡처)
  • 이런 경우 캡처한 변수의 영역이 when 식의 본문으로 제한되면서 when 식의 각 가지 안에서 변수가 가리키는 값의 프로퍼티에 접근할 수 있다.
package ch02.colors.Color.*

fun measureColor() = ORANGE

fun getWarmthFromSensor() = 
    when (val color = measureColor()){
        RED, ORANGE, YELLOW -> "warm (red = ${color.r})"
        GREEN -> "neutral (green = ${color.g})"
        BLUE, INDIGO, VIOLET -> "cold (blue = ${color.b})"     
    }

fun main(){
    println(getWarmthFromSensor())
}

2.3.4 when의 분기 조건에 임의의 객체 사용

package ch02.colors.Color.*

fun mix(c1:Color, c2:Color) =
    when (setOf(c1, c2)){ // 표준 라이브러리의 setOf 함수 => 객체 집합 Set 객체 반환
        setOf(RED, YELLOW) -> ORANGE 
        setOf(YELLOW, BLUE) -> GREEN 
        setOf(BLUE, VIOLET) -> INDIGO  
        else -> throw Exception("Dirty color")
    }

fun main(){
    println(mix(BLUE, YELLOW))
}

2.3.5 인자 없는 when 사용

  • 분기마다 Set 인스턴스를 생성하는 것은 비효율적이다.
  • 인자가 없는 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) ->
        ORANGE

    (c1 == BLUE && c2 == VIOLET) ||
    (c1 == VIOLET && c2 == BLUE) ->
        ORANGE

    else -> throw Exception("Dirty color")

}

fun main(){
    println(mixOpimized(BLUE, YELLOW))
}

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

  • 코틀린의 is 연산자는 자바의 instanceof 연산자와 같다고 할 수 있지만, 거기에 약간의 편의를 더했다.
  • 코틀린에서는 is 연산자를 통해 어떤 임의의 변수의 타입을 확인한 후 그 타입에 속한 멤버에 접근하기 위한 명시적인 변수 타입 변환이 따로 필요하지 않다는 것이다.
  • 타입을 검사한 변수를 마치 검사한 그 타입의 변수인 것처럼 사용 가능
  • 단, 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다.
  • 클래스 프로퍼티의 경우, val로 선언돼있으면서, 커스텀 접근자가 없는 경우에만 스마트 캐스트가 가능하다는 것이다.
  • val로 선언되지 않았거나 val로 선언했지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문이다.
  • 원하는 타입으로 명시적으로 타입 캐스팅하려면 as 키워드를 사용한다. val n = e as Num
  • if와 when의 분기에서 블록 사용
  • 코틀린에서는 if 나 when 식 모두 분기에 블록을 사용할 수 있다.
  • 그런 경우 블록의 마지막 문장이 블록 전체의 결과가 된다.
fun evalWithLogging(e: Expr) : Int = 
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value // e의 타입 Num이면 이 식의 값이 반환된다.
        }
        is Sum -> {
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right // e의 타입이 Sum이면 이 식의 값이 반환된다.
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }

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

2.4.1 조건이 참인 동안 코드 반복: while 루프

  • 코틀린 while, do-while 문 형식
while(조건){
    /*...*/
    if(shouldExit) break
}

do{
    if(shouldSkip) continue
    /*...*/
}while(조건)
  • 내포된 루프의 경우 코틀린에서는 레이블을 지정할 수 있다.
outer@ while(outerCondition){    // 바깥 루프에 outer라는 레이블 붙인다. 
    while(innerCondition){
       if(shouldExitInner) break // 레이블을 지정하지 않으면 가장 안쪽 루프 동작 
       if(shouldSkipInner) continue // 레이블을 지정하지 않으면 가장 안쪽 루프 동작 
       if(shouldExit) break@outer  // 레이블 지정하면 지정한 루프를 빠져나가거나 동작
       if(shouldSkip) continue@outer // 레이블 지정하면 지정한 루프를 빠져나가거나 동작
    }
}

2.4.2 수에 대해 이터레이션: 범위와 순열

  • 코틀린에서는 C와 같은 for 루프, 즉 어떤 변수를 초기화하고 루프를 한번 실행할 때마다 그 변수를 갱신하고, 값이 어떤 경계에 도달하면 중단시켜주는 루프에 해당하는 요소가 없다.
  • 이런 루프의 흔한 용례를 대신하기 위해 코틀린에서는 범위를 사용한다.
  • 순열의 범위를 쓸 때는 .. 범위 연산자를 사용한다.
val oneToTen = 1..10 // 순열
  • 코틀린의 범위는 폐구간, 즉 양 끝을 포함하는 구간이다. 이는 두 번째 값이 항상 범위에 포함된다는 것이다.
  • when을 사용해 피즈버그 게임 구현하기
fun fizzBuzz(i: Int) = when{
    i % 15 == 0 -> "피즈버즈"
    i % 3 == 0 -> "피즈"
    i % 5 == 0 -> "버즈"
    else -> "$i"
}

fun main(){
    for (i in 1..100){ // in 연산자
        print(fizzBuzz(i))
    }
}
  • 증가 값을 갖는 순열을 이터레이션
fun main(){
    for(i in 100 downTo 1 step 2){
        print(fizzBuzz(i))
    }
}

/*
100 downTo 1 => 100부터 1까지의 역방향 순열 
step 2 => 방향은 그대로 유지하고 증가 값을 2로 한다. 역방향이므로 결국엔 증가값은 -2.
step 값은 양수여야 한다. 
*/
  • 범위에서 끝 부분을 반만 닫힌 구간으로 하려면 범위에 ..< 연산자 사용하면 된다.
  • 2.4.3 맵에 대해 이터레이션
  • 컬렉션에 대해 이터레이션하기
fun main(){
    val collection = listOf("red", "green", "blue");
    for(color in collection){
        print("$color")
    }
}
  • 맵을 초기화하고 이터레이션
fun main(){
    val binaryReps = mutableMapOf<Char, String>()
    for(char in 'A'..'F'){
        val binary = char.code.toString(radix=2)
        binaryReps[char] = binary
    }

    for((letter, binary) in binaryReps){
        println("$letter = $binary")
    }
}
  • 맵에 사용한 구조 분해 구문을 맵이 아닌 컬렉션의 현재 인덱스를 유지하면서 컬렉션을 이터레이션 할때도 쓸 수 있다.
  • 컬렉션의 withIndex 함수를 사용
fun main(){
    val list = listOf("10", "11", "1001")
    for((index, element) in list.withIndex()){
        println("$index : $element")
    }
}    

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

  • in 연산자를 사용해 어떤 값이 범위에 속하는지 검사할 수 있다.
  • 반대로 !in을 사용하면 어떤 값이 범위에 속하지 않는지 검사할 수 있다.
fun isLetter(c:Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c:Char) = c!in '0'..'9'

fun main(){
    println(isLetter('q'))
    println(isNotDigit('x'))
}
  • when 가지에서 in 검사 사용하기
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 know..."
}

fun main(){
    println(recognize('8'))
}
  • 검사 범위는 문자에 국한되지 않는다.
  • 비교가 가능한 클래스라면, 그 클래스의 인스턴스 객체를 사용해, 범위를 만들 수 있다.
  • 이렇게 만든 범위의 경우 그 범위 내의 모든 객체를 항상 이터레이션하지는 못한다.
  • 하지만 in 연산자를 사용하여 값이 그 범위 안에 속하는지는 항상 결정할 수 있다.
fun main(){
    println("Kotln" in "Java".."Scala")
}

2.5 코틀린에서 예외 던지고 잡아내기

  • 함수는 정상적으로 종료할 수 있지만 오류가 발생하면 예외를 던질 수 있다.
  • 함수를 호출하는 쪽에서는 그 예외를 잡아 처리할 수 있다.
  • 발생한 예외를 그 함수를 호출한 쪽에서 처리하지 않으면 함수 호출 스택을 거슬러 올라가면서 예외를 처리하는 부분이 나올 때까지 예외를 다시 던진다.
  • 자바와 달리 코틀린은 new 연산자가 필요없이 그냥 throw 키워드를 사용해 예외를 던진다.
  • 자바와 달리 코틀린에서 throw는 구문이 아닌 식이므로 다른 식에 포함될 수 있다.
val percentage = 
    if(number in 0..100)
        number
    else 
        throw IllegalArgumentException("exception")
        // throw는 식이다.

2.5.1 try, catch, finally를 사용한 예외 처리와 오류 복구

  • 예외를 발생시키는 쪽이 아닌 예외를 잡아 해결해야 하는 쪽에서는 자바와 마찬가지로 예외를 처리하려면 try와 catch, finally 절을 함께 사용해 예외를 처리한다.
  • 자바와 코틀린의 예외 처리에 있어서 가장 큰 차이점은 코틀린은 호출한 함수에서 던져질 가능성이 있는 예외를 함수 선언부에 throws 키워드를 사용해서 명시해주지 않아도 된다는 점이다.
  • 자바의 경우엔 throws 절을 메서드 선언부에 명시하여 발생 가능한 예외를 알려줘야 했다.
fun readNumber(reader:BufferedReader): Int? { // throws IOException이 필요 X
    try{
        val line = reader.readLine()
        return Integer.parseInt(line)
    }catch(e: NumberFormatException){
        return null
    }finally{
        reader.close()
    }
}

fun main(){
    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}
  • 자바에서는 체크 예외가 메서드 시그니처의 일부이다.
  • 자바에서는 명시적으로 처리해줘야만 하는 유형의 예외를 체크 예외라고 한다.
  • 함수가 다른 함수를 호출할 경우 그 함수에 선언된 체크 예외는 반드시 모두 잡아 처리하거나 아니면 그 예외를 다시 던질 수 있다고 선언해야만 한다.
  • 하지만 코틀린과 같은 최신 JVM 언어에서는 체크 예외와 언체크 예외를 구분하지 않는다.
  • 코틀린은 함수가 던지는 명시적으로 처리해줘야만 하는 예외를 따로 지정해두지 않고, 예외를 잡을지 말지만 결정하면 된다.
  • 코틀린에서는 컴파일러가 예외 처리를 강제하지 않는다.
  • 2.5.2 try를 식으로 사용
  • 지금까지 try를 문으로만 사용했다. 하지만 try는 식이기도 하다.
fun readNumber(reader: BufferedReader){
    val number = try {
        Integer.parseInt(reader.readLine())
    }catch(e: NumberFormatException){
        return
    }

    println(number)
}

fun main(){
    val reader = BufferedReader(StringReader("239"))
    readNumber(reader)  // 아무것도 출력하지 않는다.
}
fun readNumber(reader: BufferedReader){
    val number = try {
        Integer.parseInt(reader.readLine())
    }catch(e: NumberFormatException){
        null
    }

    println(number)
}

fun main(){
    val reader = BufferedReader(StringReader("239"))
    readNumber(reader)  // null 출력
}
  • try를 식으로 사용하면 중간 변수를 도입하는 것을 피함으로써 코드를 좀 더 간결하게 만들고, 더 쉽게 예외에 대비한 값을 대입하거나 try를 둘러싼 함수를 반환시킬 수 있다.

2장 요약

  • 함수를 정의할 때 fun 키워드를 사용한다. val과 var는 각각 읽기 전용 변수와 변경 가능한 변수를 선언할 때 쓰인다.
  • val 참조는 읽기 전용이지만, val 참조가 가리키는 객체의 내부 상태는 여전히 변경 가능할 수 있다.
  • 문자열 템플릿을 사용하면 지저분하게 문자열을 연결하지 않아도 된다. 변수 이름 앞에 $를 붙이거나 식을 ${식}처럼 ${}로 둘러싸면 변수나 식의 값을 문자열 안에 넣을 수 있다.
  • 코틀린에서는 클래스를 아주 간결하게 표현할 수 있다.
    • 프로퍼티 선언으로 자동으로 접근자, (비공개) 필드, (공개) getter, (공개) setter가 선언된다.
    • 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 직접적인 결과인 경우, 해당 프로퍼티의 접근자는 커스텀으로 구현이 필요하다.
  • if는 코틀린에서 구문이 아닌 식이며, 값을 만들어낸다.
  • 코틀린 when은 자바의 switch와 비슷하지만 더 강력하다.
  • 어떤 변수의 타입을 검사하고 나면 굳이 그 변수를 캐스팅하지 않아도 검사한 타입의 변수처럼 사용할 수 있다. 컴파일러가 스마트 캐스트를 활용해 자동으로 타입을 바꿔준다.
    • 단, val로 선언되고 커스텀 접근자가 구현되지 않은 프로퍼티여야 한다.
  • .. 연산자를 이용한 식은 범위를 만들어낸다.
  • 코틀린에서는 자바와 달리 체크 예외와 언체크 예외를 구분하지 않으며 던질 가능성이 있는 예외를 throws 절을 이용해 명시해주지 않아도 된다.