(kotlin) 002. Kotlin Essentials for the Java Eyes

Kotlin Essentials for the Java Eyes

단순한 프로그램을 만들 때는 만들기 쉬워야하고, 복잡한 프로그램을 만들 때는 비용이 적게 드는 것이 이상적이다.

kotlin(이하 코틀린 혼용)은 이러한 이상을 충족하는 언어이다.

본 포스팅에서는 Java 유저 입장에서 코틀린을 사용할 때 적응하기 위한 여러가지 사항들을 나열해보았다.

1. Less Typing

코틀린으로 애플리케이션을 개발할 땐 Java 대비 더 적은 타이핑이 요구된다.

이는 코틀린의 많은 부분이 Optional이기 때문이다.

Good bye semicolon
코틀린은 모든 표현식과 명령문을 끝내는 곳에 세미콜론이 필요없다.

코틀린에서 세미콜론이 필요한 경우엔 한 줄에 두 개 이상의 표현식이나 명령문을 사용할 떄 뿐이다.

아래 예제를 참고하자.

1
2
3
4
// java case
public int sum(int a, int b) {
return a + b;
}
1
2
// kotlin case
fun sum(a: Int, b: Int) : Int = a + b

실제로 세미콜론을 치더라도 IDE에서 무시된 것처럼 Syntax가 적용된 것을 확인할 수 있다.

Variable type specification

코틀린은 이전 포스팅에서 언급했듯 정적 타입 언어다.

보통 정적 타입 언어는 변수의 타입을 명시해야하지만, 코틀린은 우월한 컴파일러의 추론 능력을 통해 명백하게 추론 가능한 변수의 경우 타입을 명시적으로 작성하지 않아도 된다.

아래 예제를 보자.

1
2
3
4
5
// Integer case
val number = 1
println(number)
println(number::class)
println(number.javaClass)
1
2
3
4
# Output
1
class kotlin.Int
int
1
2
3
4
5
// String case
val greet = "hello"
println(greet)
println(greet::class)
println(greet.javaClass)
1
2
3
4
# Output
hello
class kotlin.String
class java.lang.String

코틀린의 타입 추론은 런타임이 아닌 컴파일 타임에 발생하기 때문에 아래와 같이 코드를 수정하면 컴파일 오류가 나온다.

1
2
val number = 1
number = "Is this number?"
1
2
# Output
Val cannot be reassigned

상술했듯 코틀린의 타입 추론은 타입이 명확한 경우에만 타입의 생략이 가능하도록 하고 있기때문에 개발자는 안심하고 타입을 생략할 수 있다.

Classes and Functions

변수의 타입은 물론 클래스와 함수마저 생략할 수 있는 경우가 있다.

이는 최소한 현재 개발하고 있는 소스코드 안에서는 명령문이나 표현식이 함수에 속할 필요가 없고, 함수는 클래스에 속할 필요가 없다.

아래 예제를 통해 이해해보자.

1
2
3
4
5
6
7
8
9
10
11
fun standAlone() {
println("I'm ALIVE!!")
throw RuntimeException("Sorry, Time to DIE!")
}

println("Not in a function")
try {
standAlone()
} catch(e: Exception) {
e.printStackTrace()
}

위의 코드에서 standAlone() 함수는 어느 클래스에도 속해있지않고, 이를 호출하는 코드 또한 어느 함수에도 속해있지않다.

위와 같은 코드가 동작이 가능한 이유는 코틀린이 JVM에서 동작할 수 있도록 Wrapping했기 때문이다.

정리하자면 코틀린은 코드가 컴파일 혹은 스크립트로 실행될 시점에 JVM의 기대치에 맞는 wrapper 클래스나 메서드를 만들어 처리해준다.

Optional try-catch

Java는 예외가 발생할 수 있는 코드에 대해 try-catch 블록을 통해 명시적으로 처리하거나 throw를 통해 외부로 전달해야한다.

이 명시적 처리에 대한 호불호를 떠나 코틀린의 예외 처리 방식에 대해 알아보자.

코틀린은 런타임 계열의 Unchecked Exception이든 Checked Exception이든 예외 처리에 대해 강제하지 않는다.

만약 try-catch 없이 함수를 호출하였는데, 예외가 발생한다면 자동으로 해당 예외를 호출한 함수 혹은 코드로 전달하고, 전달받은 함수 혹은 코드에서 별도의 핸들링이 없는 경우 프로그램이 종료되는 방식으로 전달한다.

스레드의 대표적인 메서드인 sleep() 메서드를 통해 차이점을 살펴보도록 하자.

Java는 sleep()을 쓰기 위해 아래와 같이 작성해야 한다.

1
2
3
4
5
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Do Something.
}

InterruptedException 예외 처리가 없으면 컴파일 오류를 계속해서 출력하게 된다.

반면 코틀린으로 작성하면 아래와 같이 작성할 수 있다.

1
Thread.sleep(1000)

물론 무조건적인 생략은 물론 좋지않으므로, 코틀린을 개발할땐 예외가 호출 함수와 코드에 전파되는 것에 대해 인지하고 방어적인 프로그래밍을 지향하는 것이 좋다.

2. Sensible Warnings

코드가 문법에 맞추어 작성되었다고 하더라도, 잠재적인 문제는 남아있을 수 있다.

코틀린의 컴파일러는 코드 안의 다양한 잠재적 문제들을 찾아내 개발자들에게 경고해준다.

아래의 예제를 보자.

1
fun notUseParameter(n: Int) = 0

위의 코드는 문법적으로 이상은 없지만 주어진 파라미터 n을 사용하지 않고 있음을 알 수 있다.

1
println(notUseParameter(4))

위와 같이 호출하더라도 무조건 0을 출력하게 되는 것이다.

코틀린의 컴파일러는 위의 코드에 대해 아래와 같은 경구 문구를 출력한다.

1
warning: parameter 'n' is never used

이처럼 코틀린은 코드의 잠재적인 문제까지 찾아내 개발자들에게 경고해준다.

3. Prefer val over var

코틀린은 immutable 변수를 선언하기 위한 키워드로 val을 사용한다.

아래의 예제를 보자.

1
2
val explicitTypePi: Double = 3.14
val implicitTypePi = 3.14

위의 두 변수 explicitTypePiimplicitTypePi는 모두 val로 선언되었기때문에 값을 초기화할 수 없다.

코틀린은 val로 선언된 값을 변경하려는 시도에 대해서 컴파일 오류를 발생시키기 때문이다.

즉, Java의 final과 비슷한 역할을 한다.

값의 재할당을 위해선 val이 아니라 var 키워드를 사용하여 변수를 사용해야 한다.

개발자는 변경 가능성에 대해서 val 혹은 var을 선택적으로 사용하여 오류를 회피할 수 있다.

4. Improved Equality Check

Java처럼 코틀린도 두 가지의 동일성 체크를 지원한다.

Java의 equals() 메서드와 코틀린의 == 연산자는 을 비교한다. 이를 Structural equality(구조상의 동일성) 이라 한다.

Java의 == 연산자와 코틀린의 === 연산자는 참조 대상을 비교한다. 이를 Referential equality(참조상의 동일성) 이라 한다.

참조 대상의 비교란, 두 비교 대상이 같은 객체를 참조하는 지 여부를 검증함을 뜻한다.

이렇게 보면 연산자 혹은 메서드의 차이일 뿐이라고 볼 수 있지만, 코틀린의 ==equals()보다 더욱 좋다.

코드 레벨로 비교해보는 것이 이해하는 데 도움이 된다.

먼저 Java 코드를 보자.

1
2
3
4
String base = null;
String compare = "string";

base.equals(compare);

위의 코드는 NullPointerException을 발생시킨다.

코틀린 코드를 보자.

1
2
3
4
var base: String? = null
var compare = "string"

base == compare

코틀린은 base 혹은 compare 둘 중 하나가 null인 경우 false를 반환하고, 둘 다 null인 경우 true를 반환한다.

모든 케이스에 대해서 비교해보자.

1
2
3
4
5
println("string" == "string") // true
println("string" == "String") // false
println(null == "string") // false
println("string" == null) // false
println(null == null) // true

보기 쉽게 주석으로 바로 출력 결과를 추가해두었다.

코틀린은 이렇게 끝나지않고 아래와 같이 컴파일러를 통해 경고 메시지도 출력해준다.

1
condition 'null == "string"' is always 'false'

5. String Templates

Java를 개발하며 + 연산자를 통해 문자열을 연결해본 경험이 있다면, 이러한 방식이 얼마나 헷갈리고 유지보수가 어려워지는 지 경험해보았을 것이다.

코틀린은 문자열 템플릿을 통해 이러한 문제를 해결해준다.

큰 따옴표로 정의한 문자열 내에서는 $ 심볼을 통해 어떤 변수든지 포함시킬 수 있으며 ${} 심볼을 통해 명령문도 포함시킬 수 있다.

만약 $만 단독으로 존재하는 경우 혹은 \가 앞에 붙어서 오는 형태인 경우 심볼이 아닌 문자로 판정한다.

아래의 예제를 보자.

1
2
val name = "Namhoon Kim"
println("My name is $name")
1
2
var number = 1
println("$number + 1 = ${number + 1)

6. Raw Strings

코틀린은 이스케이프 문자를 사용하는 대신 시작과 끝의 세 개의 큰 따옴표 """를 이용해 raw한 문자열을 사용할 수 있고, 멀티라인도 그대로 출력할 수 있다.

예제를 통해 파악해보자.

1
val escaped = "아인슈타인이 말했다. \"내가 천재인 게 아니라 니들이 바보인 거다.\""

Java처럼 큰 따옴표를 문자열 내에서 표현하기 위해 위와 같이 쓸 수도 있지만 raw string을 쓰면 아래와 같이 쓸 수 있다.

1
val rawString = """아인슈타인이 말했다. "내가 천재인 게 아니라 니들이 바보인거다."""

멀티라인도 예제를 통해 파악해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
val escaped = "제1의아해가무섭다고그리오.\n" +
"제2의아해도무섭다고그리오.\n" +
"제3의아해도무섭다고그리오.\n" +
"제4의아해도무섭다고그리오.\n" +
"제5의아해도무섭다고그리오.\n" +
"제6의아해도무섭다고그리오.\n" +
"제7의아해도무섭다고그리오.\n" +
"제8의아해도무섭다고그리오.\n" +
"제9의아해도무섭다고그리오.\n" +
"제10의아해도무섭다고그리오.\n" +
"제11의아해가무섭다고그리오.\n" +
"제12의아해도무섭다고그리오.\n" +
"제13의아해도무섭다고그리오."

raw string을 적용하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
val rawString = """제1의아해가무섭다고그리오.
제2의아해도무섭다고그리오.
제3의아해도무섭다고그리오.
제4의아해도무섭다고그리오.
제5의아해도무섭다고그리오.
제6의아해도무섭다고그리오.
제7의아해도무섭다고그리오.
제8의아해도무섭다고그리오.
제9의아해도무섭다고그리오.
제10의아해도무섭다고그리오.
제11의아해가무섭다고그리오.
제12의아해도무섭다고그리오.
제13의아해도무섭다고그리오."""

raw한 그대로 나가기 때문에 들여쓰기까지 그대로 포함되는 경우 아래와 같이 처리하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val rawString = """제1의아해가무섭다고그리오.
|제2의아해도무섭다고그리오.
|제3의아해도무섭다고그리오.
|제4의아해도무섭다고그리오.
|제5의아해도무섭다고그리오.
|제6의아해도무섭다고그리오.
|제7의아해도무섭다고그리오.
|제8의아해도무섭다고그리오.
|제9의아해도무섭다고그리오.
|제10의아해도무섭다고그리오.
|제11의아해가무섭다고그리오.
|제12의아해도무섭다고그리오.
|제13의아해도무섭다고그리오."""
println(rawString.trimMargin())

위와 같이 처리하면 | 앞단의 공백을 모두 소거 시킬 수 있다.

trimMargin()의 코드는 아래를 참고하자.

1
public fun String.trimMargin(marginPrefix: String = "|"): String = replaceIndentByMargin("", marginPrefix)

7. More Expressions, Fewer Statements

Expression(표현식) 은 결과를 반환해주고 어떠한 상태도 변경하지 않는 반면에, Statement(명령문) 은 아무것도 반환하지않고, 상태나 변수를 변하게 하거나 파일을 쓰고 데이터베이스를 업데이트한다던지, 서버에 데이터를 전송시키는 등의 동작을 수행한다.

프로그래밍의 수많은 언어들은 표현식 보다는 명령문을 더 가지고 있거나, 그 반대로 명령문 보다는 표현식을 더 많이 가지고 있는 경우로 분류할 수도 있다.

참고
표현식 < 명령문인 언어 : Java, C#, Javascript
표현식 > 명령문인 언어 : Ruby, F#, Haskell

따라서, 명령문보다는 표현식이 많은 것이 변경의 위험성에 대비하여 좀 더 유리하다고 볼 수 있다.

아래 예제를 통해 이해해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun isConscriptionTarget(grade: Int): String {
var result: String
if (grade < 4) {
result = "$grade 급은 현역 입영 대상입니다."
} else {
result = "$grade 급은 보충역 대상입니다."
}
return result
}
println(isConscriptionTarget(1))
println(isConscriptionTarget(2))
println(isConscriptionTarget(3))
println(isConscriptionTarget(4))

isConscriptionTarget() 메소드는 if문을 명령문 형태로 사용한다.

명령문은 아무런 반환값을 주지않기때문의 별도의 result 변수를 반들어 해당 변수를 수정하여 반환해야한다.

하지만 코틀린은 if문을 표현식 형태로 사용할 수도 있다.

1
2
3
4
5
6
7
8
fun isConscriptionTarget(grade: Int): String {
val result: String = if (grade < 4) "$grade 급은 현역 입영 대상입니다." else "$grade 급은 보충역 대상입니다."
return result
}
println(isConscriptionTarget(1))
println(isConscriptionTarget(2))
println(isConscriptionTarget(3))
println(isConscriptionTarget(4))

초기화 이후 result에 대한 변경이 없으므로 var가 아닌 val을 사용하여 변경에 대한 여지를 없애버릴 수 있다.

이번엔 try-catch-finally도 표현식으로 처리해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun tryCatchFinally(causeException: Boolean): Int {
return try {
if (causeException) {
throw RumtimeException("Exception")
}
1
} catch(e: Exception) {
2
} finally {
3
}
}

println(tryCatchFinally(true)) // 2
println(tryCatchFinally(false)) // 1

이 경우 catch 블록의 마지막 부분이 결과값이 되어 반환된다.