[Kotlin in Action] 02. Kotlin Basic

주영진·2025년 7월 21일

Kotlin 스터디

목록 보기
2/4

1. 기본 요소: 함수와 변수

fun main(){
    println("Hello World!")
}

다음은 코틀린으로 "Hello World!"를 출력하는 함수이다. 여기서 찾을 수 있는 코틀린 문법의 특성은 다음과 같다.

  • 함수를 선언할 때 fun 키워드를 사용한다.
  • 함수를 모든 코틀린 파일의 최상위 수준에 정의할 수 있으므로 클래스 안에 함수를 넣을 필요는 없다. 즉, 자바차럼 꼭 클래스 안에 메서드처럼 넣을 필요가 없다.
  • main 함수를 애플리케이션의 진입점으로 지정할 수 있기에, main에는 인자가 굳이 없어도 된다
  • 간결성을 강조하는 언어로서, 텍스트를 표시하고 싶을 때 그냥 println만 쓰면된다(코틀린 라이브러리는 자바 라이브러리 함수에 대해 더 간결한 구문을 사용할 수 있게 해주는 wrapper를 제공한다. println도 그중 하나)
  • 세미콜론 필요없다. 이는 최신 프로그래밍 언어의 트렌드.

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

fun max(a: Int, b: Int): Int {
    return if(a > b) a else b
}

fun main() {
    println(max(1,2)) // 2
}

코틀린에서는 파라미터 이름이 먼저 오고, 그 뒤에 파라미터의 타입을 지정하며, 타입과 이름을 '콜론(:)'으로 구분한다. 함수의 반환 타입은 파라미터 목록을 닫는 괄호 다음에 오며, 이 또한 콜론으로 구분한다. 또한, 코틀린의 if식은 자바의 삼항 연산자와 비슷하다.

  • main 함수는 파라미터가 있는 경우든, 없는 경우든, 어떤 경우에서도 아무 값도 반환하지 않는다(자바의 void와 비슷한 형태)
  • 코틀린에서 if는 식이지 문이 아니다. 식은 값을 만들어내며 다른 식의 하위 요소로 계산에 참여할 수 있는 반면, 문은 자신을 둘러 싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않는다.

// if, when(자바의 switch, try-catch 모두 식으로 값을 반환하므로 바로 결과를 val에 할당 가능!
val x = if(myBoolean) 3 else 5
val direction = when (inputString) {
    "u" -> UP
    "d" -> DOWN
    else -> UNKNOWN
}

val number = try {
    inputString.toInt()
} catch (e: NumberFormatException){
    -1
}

반면에, 코틀린에서는 자바와 다르게 대입을 식이 아닌 문으로 취급한다. 즉, 대입 연산이 값을 반환하지 않기에 다른 식에 끼워넣을 수 없다.

val a = 10      // val: 변경 불가능한 값
var b = 20      // var: 변경 가능한 값
b = 30          // var만 다시 대입 가능

이런식으로 코틀린에서 대입은 그냥 위와 같이 변수에 값을 ‘할당’하는 형태로 사용하면 된다.

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

앞서 나왔던 함수를 등호를 사용해 다음과 같이 더 간결하게 나타낼 수도 있다.

fun max(a: Int, b: Int): Int = if(a > b) a else b
  • 본문이 중괄호로 둘러 쌓인 함수: block body function(블록 본문 함수)
  • " 등호와 식으로 " : expression body function(식 본문 함수) // 코틀린에서는 식 본문 함수가 더 자주 쓰인다

위의 함수 식에서 반환 타입 지정하는 부분을 제거하여 더욱 함수를 간추릴 수 있다.

fun max(a: Int, b: Int) = if(a > b) a else b

식 본문 함수의 경우 굳이 사용자가 반환 타입을 적지 않아도 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 지정해준다. 이게 앞서 1장에서도 다뤘던 타입 추론(Type inference)다. 타입 추론은 식 본문 함수에만 적용 가능하다는 것을 기억하자.

변수 선언

위의 이미지가 제일 기본적인 코틀린 변수 선언의 형태이다. 데이터 타입을 빼는 경우에는 앞서 언급했듯이 컴파일러에 의해 타입 추론이 발생한다.

변수 선언의 종류에는 다음 두 가지가 존재한다.

  • val: 읽기 전용 참조(read-only reference) 선언. val로 선언된 변수는 단 한번만 대입될 수 있다. 일단 초기화하고 나면 다른 값을 대입할 수 없다.
  • var: 재대입 가능한 참조(reassignable reference)를 선언.

val 자체가 읽기 전용이어서 한 번 대입된 다음에 그 값을 바꿀 수 없더라도, 그 참조가 가리키는 객체 내부의 값은 변경될 수 있다. 또, 컴파일러 자체가 똑똑해 잠재적인 두 가지 대입 중(if-else 식) 하나만 실행될 수 있는 경우도 사용 가능하다.

fun main(){
    val languages = mutableListOf("Java")
    languages.add("Kotlin") //참조가 가리키는 객체에 원소를 하나 추가하는 변경 수행
}

var의 경우, 변수의 값을 변경할 수 있지만 변수의 타입은 고정된다.

문자열 템플릿($)

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

위는 문자열 템플릿(string template)을 기능을 보여주고 간단한 사용자 입력을 읽는 방법을 보여준다. 이는 다른 스크립트 언어와 비슷하게 변수 이름 앞에 $를 붙이면 변수를 문자열 안에 참조할 수 있다.
*$를 문자열에 넣으려고 하는 경우에는 백슬래쉬()를 사용해 escape시킨다.

문자열 템플릿과, if가 코틀린에서 식이라는 사실을 조합해 다음과 같은 코드의 형태를 만들어낼 수 있다.

fun main(){
   val input = readln()
    println("Hello, ${if (input.isBlank()) "someone" else input}!")
}

2. 행동과 데이터 캡슐화: Class & Property

다른 객체지향 언어와 마찬가지로 코틀린도 클래스라는 추상화를 제공한다. 하지만 코틀린은 자바에 비해 훨씬 더 적은 양의 코드로 이를 구현할 수 있다. 아래는 자바 코드의 예시이다.

public class Person {
    private final String name;
    
    public Person(String name){
        this.name = name;
    }
    
    public String getName{
        return name;
    }
}

이를 코틀린으로 변환하면 다음과 같다.

class Person(val name: String)

훨씬 간단해지는 걸 확인할 수 있다. 이제 자바 안쓸ㄹ

클래스와 데이터를 연관시킨다: Property(속성)

자바에서는 필드와 접근자를 한데 묶어 property라고 부르며, 이를 활용하는 다양한 프레임워크가 있다. 코틀린은 프로퍼티를 언어 기본 기능으로 제공하며, 코틀린 프로퍼티는 자바의 필드와 접근자 메서드(e.g. getter, setter)를 '완전히' 대신한다.
변수와 동일하게 val이나 var을 활용하여 선언한다.

class Person(
    val name: String,
    var isStudent: Boolean //쓸 수 있는 프로퍼티, 비공개 필드, 공개 게터,세터를 만들어냄 
)

자바의 긴 getter와 setter 코드가 코틀린의 저 한줄에 다 숨어 들어가있다. 즉, 코틀린으로 저렇게 정의한 클래스를 자바에서도 똑같이 원래 클래스를 사용하는 방식대로 똑같이 사용할 수 있다. 아래는 코틀린으로 Person 클래스를 사용하는 예시이다.

fun main() {
    val person = Person("Bob", true) //new 키워드 사용X
    println(person.name) //Bob
    println(person.isStudent) //true
    
    person.isStudent = false //졸업
    println(person.isStudent) //false
}

위의 코드에서 확인할 수 있듯이, getter를 명시적으로 호출하는 대신 프로퍼티를 직접 사용함으로써 코틀린이 자동으로 getter를 호출하게 한다. 로직은 동일하지만 코드는 더 간단해졌다.

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

프로퍼티 접근자의 커스텀 구현이 필요한 일반적인 경우는 어떤 프로퍼티가 같은 객체 안의 다른 프로퍼티에서 계산된 직접적인 결과인 경우가 있다. 다음은 프로퍼티에 접근할 때 마다 정사각형인지 여부를 계산하는 Rectangle 클래스이다.

class Rectangle(val height: Int, val width: Int){
    val isSquare: Boolean
    get() = height == width //커스텀 getter
}

이 클래스에서 height와 width, isSquare은 모두 클래스의 프로퍼티지만, isSquare은 나머지 두 개와 다르게 단순히 값을 저장하지 않고, 항상 계산해서 돌려줘야 하는 커스텀 접근자이므로, 따로 중괄호로 싸서 정의해준다.

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

코틀린은 클래스를 조직화하기 위해 자바와 동일한 개념의 패키지를 사용한다. package 문이 있는 파일에 들어있는 모든 선언(클래스, 함수, 프로퍼티)는 해당 패키지 안으로 들어간다.

다른 패키지에 정의한 선언을 사용하려면 해당 선언을 불러오는 import 키워드를 사용해야 한다. 코틀린은 함수 import와 클래스 import를 구분하지 않는다. 패키지 이름 뒤에 .*를 추가하면 패키지 안의 모든 선언을 임포트할 수 있다. 이를 star import라고 한다.

자바에서는 패키지의 구조와 일치하는 디렉터리 계층 구조를 만들고, 클래스의 소스코드를 그 클래스가 속한 패키지와 같은 디렉터리에 위치시켜야 한다. 이에 반해 코틀린에서는 여러 클래스를 같은 파일에 넣을 수 있어서, 파일의 이름도 마음대로 정해서 디스크 상의 어느 디렉터리에든 위치시킬 수 있지만, 연동성을 고려해 자바의 패키지 구조를 따르는 것이 바람직하다.

가장 두드러지는 자바와의 디렉터리 구조의 차이점은, '여러 클래스를 한 파일에 넣는 것을 주저하지 말아야 한다'는 것이다.

3. 선택과 표현과 처리: enum & when

enum은 자바 선언보다 코틀린 선언에 더 많은 키워드를 써야 하는 흔치 않은 예다. 코틀린에서는 enum class를 사용하지만, 자바에서는 그냥 enum으로만 쓴다.

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

enum은 코틀린에서 소프트 키워드이다. class 앞에 붙을 때만 특별한 이넘의 의미를 지니고, 다른 곳에는 일반적인 이름으로 사용할 수 있다. 이와 반대되는 하드 키워드에는 class가 있다.

enum class 또한 일반적인 클래스와 마찬가지로 생성자와 프로퍼티 선언 문법을 사용할 수 있다. 여기서 코틀린에서 유일하게 세미콜론이 나오는 부분이 등장한다. 이넘 클래스 안에 메서드를 정의하는 경우 반드시 이넘 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야 한다.

when 식은(얘도 식이다), 자바의 switch문과 같은 기능을 한다.

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))
}

자바와 다른 점은 각 분기의 끝에 break를 넣지 않아도 된다. 또한, 상수 값들을 Import하면 이넘 클래스 이름을 굳이 붙이지 않아도 된다. 그에 대한 예시는 아래와 같다.

import ch02.colors.Color.*

fun measureColor() = ORANGE

fun getWarmthFromSensor(): String {
    val color = measureColor() //color = orange가 됨
    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())
}

추가적으로 위의 예시에서 정의한 color 변수의 정의가 when의 조건안으로 정의되서 해당 변수가 when 식 안에서만 정의되도록 할 수도 있다.

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") //일치하는 분기 조건이 없을 경우
        }

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

코틀린 표준 라이브러리는 인자로 전달받은 여러 객체들을 포함하는 집합(Set)을 생성해주는 setOf함수를 제공한다. 따라서 위의 코드에서 setOf의 안의 순서는 중요하지 않다.

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

본 책은 본격적으로 스마트 캐스트에 대해 알아보기 전, 마커 인터페이스라는 개념을 다룬다. 책에서는 이것을 "여러 타입의 식 객체를 아우르는 공통 타입 역할만 수행하는 인터페이스"라고 표현하지만 옮긴이 누구냐, 좀 더 이해하기 쉽게 표현하자면, 메서드는 없지만, 어떤 클래스를 특정한 용도로 쓸 수 있다는 “표시” 또는 “태그”를 붙이는 도구 정도로 생각하면 된다.

interface Expr //마커 인터페이스
class Num(val value: Int) : Expr //Expr 인터페이스를 구현
class Sum(val left: Expr, val right: Expr): Expr

Expr 인터페이스를 구현한다는 말은, Num과 Sum 클래스가 Expr이라는 스티커를 붙였다 정도로 이해하면 이해가 빠르다. Num이든 Sum이든 공통 타입으로 처리하기 위해 기능은 없고 '표시'역할만 하는 마커 인터페이스를 붙여 한 타입처럼 다루게 만드는 것이다.

인터페이스에 대해서는 4장에서 조금 더 본격적으로 다룬다. 마커 인터페이스가 갑자기 스마트 캐스트 얘기하는데 왜 튀어나오는지는.. 곧 다뤄보도록 하겠다.

코틀린에서는 is 검사를 사용해 어떤 변수의 구체적 타입을 검사할 수 있다.

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("Unkown Expression")
}

위의 코드에서 스마트 캐스트를 확인할 수 있다. 스마트 캐스트를 먼저 정의하고 가자면, "타입 체크(is) 후에 자동으로 형변환(cast)이 되는 코틀린의 기능"이 스마트 캐스트이다. 위의 코드에서 val n = e as Num 줄을 굳이 적지 않아도, e.value라고 적어도 코틀린이 알아서 e를 Num 타입으로 캐스팅해준다. 이것이 바로 스마트 캐스트이다. 밑의 e.right와 e.left에서는 스마트 캐스트를 활용한 것이다. 다시 한번 쉽게 설명하자면, "is는 단지 타입을 검사하는 문법일 뿐인데, 코틀린이 똑똑하게 그걸 감지해서 자동으로 캐스트까지 해주는 것"이다.

참고로 명시적으로 타입 캐스팅하려면 as 키워드를 활용한다.

val n = e as Num

위의 스마트 캐스트가 활용된 코드를 더욱 간단하게 다음과 같이 만들 수 있다.

fun eval(e: Expr): Int =
    if(e is Num) e.value
    else if(e is Sum) eval(e.right) + eval(e.left)
    else throw IllegalArugmentException("Unknown Expression")

이를 when을 활용한 코드로 변환할 수 있는데, 그 코드는 다음과 같다.

fun evalWithLogging(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")
    }

참고로, 각 블록의 맨 마지막에 있는 식이 그 블록이 반환할 값이 된다.

4. iteration: while & for 루프


코틀린의 반복문은 자바, C# 등의 다른 언어에서 사용하는 방법과 아주 비슷하다.

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

코틀린에는 while과 do-while 루프가 있다. while은 자바와 똑같이 조건이 참인 동안 본문을 반복 실행하고, break로 중단시키고, do-while문의 경우에는 처음에 무조건 본문을 한 번 실행하고, 그 다음부터는 조건이 참인 동안 본문을 반복 실행한다. 레이블도 지정해서 특정 루프로 빠져나가거나 진행되게 하여 가독성을 향상시킬 수도 있다.

수에 대해 iteration: 범위와 순열

코틀린에는 자바에 그 흔한 for(int i = 0; i < 10; i++) 와 같은 고전 루프의 형태는 없다. 대신해서 '범위'를 사용하는데, '..'연산자를 활용해서 다음과 같이 작성한다. 여기서 범위 폐구간(양쪽 끝 값이 범위에 포함)이다.

val oneToTen = 1..10

정수 범위를 갖고 수행할 수 있는 가장 단순한 작업은 범위에 속한 값을 일정한 순서로 반복하는 경우, 순열(progression)이다. 피즈버즈라는 게임을 코드로 구현했을 때, 순열이 다음과 같이 활용된다.

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

fun main() {
    for (i in 1..100) { //1..100 사이 iteration
        print(fizzBuzz(i))
    }
}

거꾸로 세는 것도 가능하다.

for(i in 100 downTo 1 step2) // 100부터 2씩 감소

in으로 Collection이나 범위의 원소 검사

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

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

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 know..."
}

fun main() {
    println(recognize('8'))
}

범위는 단순히 문자에만 국한되지 않고, 비교가 가능한 클래스의 인스턴스 객체를 사용해 범위를 만들 수 있다. 이에 대해 자세한 내용은 9장에서 다룬다.

코틀린의 예외 던지고 잡아내기

코틀린의 예외 처리는 자바나 다른 언어의 예외 처리와 비슷하다. 오류가 발생하면 던질(throw) 수 있고, 호출하는 쪽에서 그 예외를 잡아(catch) 처리할 수 있다. 자바와 달리 코틀린의 throw는 '식'이므로 다른 식에 포함될 수 있다는 점을 기억하자.

자바에서는 함수 선언 뒤에 'throws IOException'을 붙여서 명시적으로 처리해야만 하는 유형의 예외, 체크 예외를 모두 잡아 처리해야 한다. 하지만, 최신 JVM 언어의 트렌드와 마찬가지로 코틀린 또한 체크 예외와 언체크 예외를 구별하지 않는다. 이는 자바 프로그래머들이 의미 없이 예외를 다시 던지거나, 예외를 처리하지는 않고 그냥 무시하는 코드를 작성하는 경우가 자주 있어, 실제 오류 발생을 방지하지 못하는 경우를 막고자 코틀린을 이런식으로 개발하였다고 한다.

이러한 코틀린 언어의 설계 결정으로 코드를 작성하는 사람이 직접 잡아내고 싶은 예외와 그렇지 않은 예외를 결정하여 try-catch로 처리할 수 있다. try는 다른 문법들처럼 식으로 처리할 수 있어서 코드를 보다 더 간결하게 만들 수 있다.

profile
'개발사(社)' (주)영진

0개의 댓글