Kotlin

Kotlin을 시작하며

언어를 새로 배울 땐 항상 강의나 책에 의존했었는데 버전도 계속 변하고 언어별 Doc만큼 확실한 학습법이 없다는 것을 느껴서, 이번에는 Doc을 기반으로 학습을 시작하게 되었습니다. 이 글의 ‘이론적인 부분’ 내용과 예시는 Kotlin Document 를 기반으로 작성되었습니다. (Learning materials and Kotlin Basics track 순서로 학습)


Kotlin 기본문법

Java와 비교로 보는 기본문법

코틀린은 자바의 가상머신 JVM 상에서 실행되는 언어입니다. 코틀린 컴파일러는 JVM이 이해할 수 있는 바이트 코드로 변환해 자바 클래스 파일과 동일한 형식을 가지고 실행할 수 있게 합니다. 그리고 자바와의 호환성이 뛰어난 언어이며 비슷한 문법, 라이브러리 상호운용성을 보장합니다. 코틀린과 자바 코드를 비교하면서 기초 문법을 작성했습니다. (자바와 동일한 부분은 제외)


함수 - fun
코틀린은 자바와 달리 함수 선언이 fun으로 간단하며, 출력메서드도 간단합니다. 코드마다 ;도 없습니다. (파이썬과 자바 혼용의 느낌이 나기 시작합니다.)
여기서 가장 다르다고 느낀 부분은 변수 부분인데, 아래 코드에서도 알 수 있듯이 코틀린의 경우는 변수명이 먼저 오고 : 다음에 타입 순으로 작성됩니다. 코틀린 1.3 이전의 버전에서는 파라미터로 Arrays<String>이 반드시 필요했습니다. 최신 버전의 경우 main 함수에 파라미터가 필요하지 않습니다.

// kotlin main 함수
fun main(args: Array<String>) {
    println(args.contentToString())
}
// java main 함수
public static void main(String[] args) {
    System.out.println(Arrays.toString(args));
}

코틀린은 리턴타입을 마지막에 표시하는데 유추될 수 있는 타입이나 Unit 리턴타입은 생략이 가능합니다.

// kotlin 함수
fun sum(a: Int, b: Int): Int {
    return a + b
}
fun sum(a: Int, b: Int) = a + b

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}
fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}")
}
// java 함수
public int sum(int a, int b){
    return a + b;
}

[ Infix notation ]
한국말로는 중위표기법이라고 합니다. 즉 두 개의 값 사이의 특정한 표현을 넣는 것을 의미합니다. 여기서는 Pairto 를 사용했는데, Pair 생성자를 이용도 같은 결과가 나옵니다.

val pair = "Ferrari" to "Katrina"
println(pair)

3개의 변수를 합치는 경우에는 Triple을 사용해서 변수를 초기화할 수 있습니다. 각각의 변수는 first, second, third로 접근할 수 있습니다.

val (a, b, c) = Triple(2, "x", listOf(null))
println(a) // 2
println(b) // x
println(c) // [null]

Infix 함수infix fun으로 나타내는데, infix fun dispatcher.함수이름(receiver): 리턴타입 { 구현부 }과 같은 형식으로 되어 있습니다. dispatcher는 Infix 전에 오는 객체이고, 뒤에 receiver는 Infix 다음에 오는 객체입니다.

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "Bye ")


변수 - val, var, const
코틀린은 변수 타입을 적지 않아도 유추되기 때문에 생략이 가능합니다. var 변수는 재할당이 가능한 변수이고, val는 선언할 데이터의 값이 변경되지 않을 경우에 사용됩니다. const는 java의 static final가 붙은 상수처럼 고정된 값을 갖는 변수를 의미합니다.

// kotlin 변수
var x: Int = 5
var x = 5   // `Int` 타입이 유추됨
val y = 5   // 상수
// java 변수
int x  = 5;
final int y  = 5;

val과 const val의 차이점
val은 불완전한 불변성을 가지며, 런타임시에 결정되는 상수입니다. 함수를 받는 val 변수를 생각해보면, 함수의 파라미터 값에 따라 val 변수의 값은 변할 수 있습니다.
반면, const val컴파일시에 결정되는 상수로 일반적으로 함수나 클래스의 상태에 상관없이 항상 동일한 값을 갖습니다. 그리고 변수명은 대문자_를 사용해서 나타냅니다. 함수 내 지역변수나 클래스 속성으로 사용할 수 없어 다음과 같이 compaion object를 같이 사용해야 합니다.

런타임시 결정되는 상수와 컴파일시 결정되는 상수
런타임시에 결정되는 상수는 실행 중 메모리에 동적 할당되고, 컴파일시 결정되는 상수는 컴파일된 코드의 데이터 영역에 할당(= 정적 할당)됩니다. 즉, 컴파일시 결정되는 상수는 데이터 영역에 할당되므로 값을 변경할 수 없습니다. 따라서 보안과 관련된 상수를 이용할 때는 런타임시 결정되는 상수를 사용하는 것이 적절합니다.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}


문자열 템플릿
이 부분은 문자열 내에서 변수 변환을 주는 문법을 사용하고 있다는 점에서 약간 Spring Thymeleaf의 문법과 유사한 느낌을 받았습니다.
왜 s1을 상수로 설정했을까?
Doc에서 제공한 코드를 사용했는데 왜 s1을 상수로 설정했을까요? 혹시 a를 다시 설정하면 값이 변경되는지 확인해봤습니다. 하지만 자바 String 타입에 들어간 a 값은 변동이 없는 것처럼 변화가 없다는 것을 확인했습니다. 그럼에도 코드에서 상수를 명시하는 것은 불변성을 표시하므로써 유지보수와 가독성의 편리함을 제공합니다.

// kotlin 문자열 템플릿
var a = 1
val s1 = "a is $a"
a = 2
val s2 = "${s1.replace("is", "was")}, but now is $a"
// java 문자열 템플릿
int a = 1;
String s1 = "a is " + a;
a = 2;
String s2 = s1 + ", but now is "+ a;


조건문
코틀린에서는 아래 코드처럼 조건문의 값을 대입하는 것처럼 간결하게 나타낼 수 있습니다.

// kotlin 조건문
fun maxOf(a: Int, b: Int) = if (a > b) a else b
// java 조건문
public int maxOf(int a, int b){
    return a>b? a:b;
}

자바의 switch문과 유사한 when 표현식이 있는데, 표현방법도 거의 유사합니다. 이 코드도 위 if 조건문처럼 마치 대입하는 형식으로 표기가 가능합니다.

fun describe(obj: Any): String =
    when (obj) {
        1          -> "One"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }


반복문
자바에서는 for 반복문, 향상된 for 반복문에서도 인덱스를 제공하는 별도의 방법이 없어서 불편했지만, 코틀린에서는 .indices로 유효한 인덱스 범위 0..list.lastIndex를 제공하고 있습니다. 그 외에도 collection으로 forEachIndex를 가지고 제공하고 있습니다.
(while문의 경우에는 자바와 코틀린이 거의 유사하기 때문에 생략했습니다.)

// kotlin 반복문
val items = listOf("apple", "banana", "kiwifruit")
// 방법1
for (index in items.indices) {
    println("item at $index is ${items[index]}")
}
// 방법2
list.forEachIndexed{index, element ->
    println("item at ${index} is ${element}")
}
// java 반복문
List<String> list = List.of("apple", "banana", "kiwifruit");
for(int i=0; i<list.length; i++){
    System.out.println("item at " + i + " is " + list.get(i););
}

Range와 관련해서는 step처럼 값을 점프하면서 범위를 반복하거나, 수를 줄여가면서 반복하기 위해 사용되는 downTo가 있습니다.

for (x in 1..10 step 2) {
    print(x)
}
println()
for (x in 9 downTo 0 step 3) {
    print(x)
}


Collections
코틀린에서도 콜렉션을 제공하는데 대표적으로 람다 표현식이 있습니다. 자바와 달리 { }를 사용합니다.

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
    .filter { it.startsWith("a") }
    .sortedBy { it }
    .map { it.uppercase() }
    .forEach { println(it) }


Null-Safety
자바에서 코틀린으로 넘어간 개발자들이 가장 편리하다고 말하는 기능입니다. 코틀린은 null 참조로 인한 위험을 막기 위해 Null-Safety 기능을 제공합니다. 코틀린에서 null을 사용하기 위해서는 var b: String? = "abc"과 같은 방식으로 타입 뒤에 ?를 붙여 허용해줘야 합니다.
만약 null을 허용한 변수를 가지고 다른 메서드를 사용하려고 하면, 컴파일러 에러가 발생해 null 가능성을 알려줍니다. 이 경우 Safe calls로 해결할 수 있습니다. 앞서 예시로 nullable 변수로 null 값을 넣은 변수 b의 길이를 구하기 위해서 b?.length와 같은 표현이 가능합니다. 이렇게 Safe calls를 이용하면, 변수가 null인 경우 그 값도 null을 리턴합니다.
여기서 추가로 let을 사용하면 null이 아닌 경우만 { } 안의 메서드를 실행합니다. 즉, 아래 코드에 리스트 안에 null이 있더라도 null이 아닌 Kotlin만 출력합니다. 자바였다면, 조건문을 사용해서 출력했을텐데 확실히 코드가 간결해진 것을 볼 수 있습니다.

val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}


타입 체크
코틀린에서는 is를 이용해서 타입을 체크합니다. 자바에서의 instanceof와 동일한 기능을 수행합니다.

if (obj !is String) return null

results matching ""

    No results matching ""