[Kotlin] Kotlin In Action을 읽고나서.
0. 들어가며🏃🏻♂️
최근 여러 회사들의 기술 스택을 보면 자바 대신 코틀린을 사용하는 경우가 많이 있는 것 같습니다. 저 또한 회사에서 코틀린을 사용하고 있는데 과연 내가 코틀린스럽게 사용하고 있는걸까? 라는 의문이 드는 순간들이 많았던 것 같습니다. 이에 코틀린 컴파일러 개발자가 직접 썼다는 Kotlin In Action을 읽어보았고, 이에 대한 내용과 생각을 정리해보려 합니다.
1. 코틀린에 대하여😎
많은 자바 개발자들이 자바에 익숙했음에도 불구하고 코틀린으로 기술 스택을 변경해나가는 이유가 무엇일까 궁금했습니다. 이를 알기 위해서는 코틀린의 특징, 철학 등에 대해 먼저 알아보아야 할 것 같은데요. 코틀린 공식문서와 Kotlin In Action에서 설명하는 코틀린을 제 나름대로 요약, 정리해보면 아래와 같습니다.
코틀린❓
코틀린은 IDE로 유명한 JetBrains에서 개발한 오픈소스이고 자바 플랫폼에서 돌아가는 새로운 프로그래밍 언어입니다. 따라서 자바를 사용하는 곳이라면 거의 대부분 코틀린을 사용할 수 있죠. 대표적으로 서버 개발, 안드로이드 앱 개발 등의 분야가 있습니다. 또한 기존 자바 라이브러리나 스프링과 같은 프레임워크와도 함께 잘 작동하며 성능도 자바와 같은 수준이라고 합니다.
코틀린의 목적은 현재 자바가 사용되고 있는 모든 용도에 더 적합하면서 더 간결하고 생상적이며, 안전한 대체 언어를 제공하는 것이라고 합니다. 코틀린의 문법들을 살펴보면 더 적은 코드 또는 더욱 가독성 있는 문법으로 자바를 대체할 수 있게끔 해주죠.
코틀린 철학🥕
이런 코틀린은 크게 4가지 철학을 지니고 있습니다. 실용성, 간결성, 안전성, 상호운용성 입니다. 각각에 대해 알아보도록 하죠.
1) 실용성
코틀린을 사용하다보면 Java, C++, JavaScript의 문법이 뭔가 섞여있다..? 라는 느낌을 종종 받습니다. 이는 코틀린이 다른 프로그래밍 언어가 채택한 것 중 성공적으로 검증된 해법과 기능을 차용했기 때문입니다. 즉, 여러 언어들의 매력적이고 성공적인 기능들을 뽑아 코틀린에 도입한 것이죠. 이는 코틀린을 처음 접하더라도 익숙한 문법 덕에 언어를 빠르게 배울 수 있다는 장점을 주기도 합니다. 또한 코틀린을 사용하는 대부분이 자바에 익숙할 것인데 코틀린 입문시 자바와 별반 다르지 않게 작성하더라도 큰 문제가 없고 점차 코틀린에 익숙해짐에 따라 코틀린이 제공하는 기능을 통해 더욱 간결하게 사용할 수 있게 된다는 흐름을 가질 수 있습니다.
2) 간결성
언어의 간결성은 유지보수, 가독성 등에 도움을 주고 이는 생산성 향상에 중요한 요소입니다. 실제로 코틀린을 써보면 자바에 비해 간결하게 쓸 수 있는 부분이 많은데요. Data Class를 통해 보일러플레이트 코드를 대폭 줄이고 Getter, Setter 등을 쓰지 않아도 언어적 차원에서 자동으로 만들어지는 부분이 대표적인 예시입니다. 또한 컬렉션을 다룰 때 코틀린이 제공해주는 다양한 API를 통해 단순화가 가능하며, 람다를 지원해서 더욱더욱 간결하게 코드를 작성할 수 있게 됩니다.
3) 안전성
코틀린에서는 안전성도 내세우고 있습니다. 프로그램이 안전하다는 것은 일부 유형의 오류를 프로그램 설계가 원천적으로 방지할 수 있는가와도 관련이 있는데, 안전성과 생산성은 대게 트레이드 오프 관계에 있곤 합니다. 개발자가 컴파일러에게 더 많은 정보를 제공해야 안전성을 챙길 수 있기 때문입니다. 대표적으로 자바와 파이썬으로 짠 같은 기능의 코드만 비교해보아도 쉽게 알 수 있죠. 사실 JVM과 정적 타입 언어를 사용한다는 것만해도 메모리 안전성과 타입 안전성을 챙길 수 있습니다. 하지만 코틀린은 자바보다 더 높은 수준의 안정성을 원했고, 그와중에 비용도 적게 지불하고 싶었습니다. 이러한 사상은 코틀린 문법에 자연스럽게 녹아있는데 대표적인 예시로 Nullable 타입을 ? 키워드 하나만 가지고 만들었으며, is 라는 키워드를 통해 타입 검사 및 타입 캐스팅을 동시에 가져왔습니다.
4) 상호운용성
코틀린에서는 상호운용성 역시 중요하게 생각합니다. 자바를 쓰던 사람들은 기존 라이브러리를 그대로 쓸 수 있는지, 스프링에서도 잘 동작하는지에 대해 궁금할 수 밖에 없을 것 같은데 Kotlin In Action에 따르면 대답은 Yes라고 합니다. 기존 자바 클래스를 상속하거나 구현도 가능하며, 자바 애노테이션 역시 적용 가능하고, 같은 프로젝트 안에서도 자바와 코틀린을 같이 사용할 수 있다고 합니다. 또한 IntelliJ만 봐도 자바 코드를 복사 붙여넣기하면 코틀린 코드로 짠 변환해주는 것을 볼 수 있습니다.
2. 코틀린과 자바에서 달랐던 점❗️
위에서 코틀린이 어떤 언어이고, 어떤 철학을 가지고 있는지에 대해 알아보았는데요. 코틀린을 공부하다보면 자연스럽게 자바와 다른점이 무엇인지에 대해 집중하게 되는 것 같습니다. Kotlin In Action 역시 읽으면서 "이 부분은 자바와 어떻게 다르구나!" 라는 느낌으로 읽으면 좀 더 와닿았던 것 같은데요. 그런 부분을 중심적으로 내용을 정리해보려합니다. 물론 코틀린과 자바의 다른 점을 전부 설명하기에는 너무 방대하고, 크게 의미가 있을 것 같지도 않아 몇 가지 인상깊었던 내용을 위주로 써보려합니다. 또한 추후 코틀린에 점점 더 익숙해지면서 코틀린에 대한 이해가 깊어졌을 때 추가적으로 글을 작성해보도록 하겠습니다. 우선 시작해보죠!
확장함수와 최상위 함수
코틀린에서는 확장함수와 최상위 함수라는 개념이 있습니다. 자바의 경우 모든 메서드는 클래스 안에 정의되어있어야하는데 코틀린에서는 꼭 그럴 필요가 없습니다. 코틀린에서 이런 개념은 왜 나온것일까요? 자바를 사용하다보면 객체에 대한 연산 혹은 처리를 위한 정적 메스드를 모아두는 유틸리티 클래스들이 생기곤합니다. 대표적으로 Collections 클래스가 있고 이 외에도 직접 코드를 작성하다보면 XXXUtils 라는 클래스를 만들곤 합니다. 코틀린에서는 이런 상황을 확장함수와 최상위 함수를 사용해 해결할 수 있습니다.
코틀린은 자바와도 상호 운용된다고 했는데 자바에서는 제공하지 않는 확장 함수와 최상위 함수 정의는 어떻게 가능한 것일까요? 먼저 최상위 함수부터 살펴보도록 하겠습니다. TopLevelFunction.kt라는 코틀린 파일을 만들고 최상위 함수로 topLevelFunction을 정의해보도록 하겠습니다.
// TopLevelFunction.kt 파일
fun topLevelFunction() {
println("topLevelFunction")
}
이렇게 메서드를 정의하면 자바와는 다르게 잘 동작하는데 이게 어떻게 가능한건지 알아보기 위해 자바로 디컴파일 해보도록 하겠습니다. 자바로 디컴파일하는 방법은 인텔리제이를 사용하고 있다면 간단한데 먼저 코틀린 파일을 컴파일 한 뒤 out 폴더로 들어가 TopLevelFunctionKt.class를 찾아줍니다. 그 후 Tools ➡️ Kotlin ➡️ Decompile to Java 를 선택하면 아래와 같은 코드를 살펴볼 수 있습니다.
public final class TopLevelFunctionKt {
public static final void topLevelFunction() {
System.out.println("topLevelFunction");
}
}
자바로 디컴파일한 결과를 살펴봅시다. 우선 우리가 정의했던 최상위 함수를 덮고 있는 하나의 클래스가 생성된 것을 확인할 수 있고, 클래스 이름은 .kt파일의 이름 뒤에 Kt만 붙은 것을 알 수 있습니다. 추가로 우리가 정의했던 메서드는 public static final로 만들어져있네요. 이를 통해 코틀린의 최상위 함수도 결국 정적 메서드 클래스를 만들었던 것처럼 동작한다는 것을 알 수 있습니다.
다음은 확장함수입니다. ExtentionFunction.kt라는 코틀린 파일을 만들고 그 안에 아래와 같이 확장함수를 정의해보고 실행해봅시다!
// 확장함수 정의
fun String.lastChar(): Char = this[length - 1]
fun main() {
println("Kotlin".lastChar()) // 결과 : "n"
}
확장 함수 역시 잘 동작하는데, 도대체 이게 어떻게 가능한 것일까요..? 이번에도 역시 자바로 디컴파일 해보도록 하겠습니다.
public final class ExtentionFunctionKt {
public static final char lastChar(@NotNull String $this$lastChar) {
Intrinsics.checkNotNullParameter($this$lastChar, "<this>");
return $this$lastChar.charAt($this$lastChar.length() - 1);
}
}
자바로 디컴파일한 결과를 보면 이번에도 역시 ExtentionFunction에 Kt 라는 postfix를 붙여 클래스를 생성하였고, public static final 로 메서드를 만들어낸 것을 알 수 있습니다. 또한 lastChar 인자로 String 객체를 받는 것을 알 수 있으며, 이 객체에 마지막 char를 가져오는 동작을 하는 메서드를 정의하는 방식을 사용했다는 것을 알 수 있습니다. 결국 확장함수에서도 클래스를 만들고, 그 안에 수신 객체 타입에 맞는 수신 객체를 인자로 받아 이를 처리했음을 알 수 있습니다. 확장함수도 사실 특별한 것이 없고 어떻게 보면 syntatic sugar 정도로 이해하면 될 것 같습니다. 또한 확장함수가 클래스의 캡슐화를 깨는 것은 아닐까라는 생각도 했었는데 위와 같은 방식으로 구현되어 있기 때문에 String 클래스 내부에 private, protected 메서드나 필드를 사용할 수 없어 캡슐화를 깨지 않습니다!
클래스
코틀린의 클래스는 기본적으로 final 입니다. 이는 자바와 다르죠. 저는 이 부분이 인상깊었는데 이펙티브 자바를 읽으면서 상속은 어렵고 위험한 작업이다 라는 내용을 본 적이 있습니다. 부모 클래스에 대한 내용을 충분히 이해하지 않고 상속을 하는 경우 자식 클래스가 부모 클래스의 가정을 깨버리는 경우가 생기기 때문이죠. 또한 이러한 위험성 때문에 상속을 허락한 부모 클래스도 철저한 설계와 문서화를 갖추어야한다고 조언합니다. 코틀린에서는 이러한 문제점을 인식하고 기본적으로 클래스에 아무것도 적용하지 않을 시 final로, 즉 상속을 금지하는 방향을 기본값으로 설정하고 있습니다. 코틀린에서는 클래스의 final과 같이 자바에서 좀 더 좋은 코드를 쓰기 위한 방법들을 언어적 차원에서 기본 값으로 제공하고 있는 것 같습니다.
또한 코틀린에서는 자바에는 없는 Data Class라는 기능을 제공하는데 이를 통해 equals, hashCode, toString과 같은 기계적으로 구현해야하는 코드를 생략할 수 있도록 도와줍니다. Data Class는 개발할때도 굉장히 자주 쓰는 기능이라 정말 편한 것 같습니다.
람다
람다는 다른 함수에 넘길 수 있는 작은 코드 조각 정도로 이해하면 될 것 같은데 코틀린에서는 이런 람다를 적극 사용하고 있습니다. 대표적으로 컬렉션 처리에서 그러한 모습을 볼 수 있죠. 자바 8 이전에는 익명 클래스를 통해 람다와 비슷한 기능을 썼지만 이는 코드 상으로 지저분합니다. 자바 8 이후로 내부적으로는 익명클래스를 사용하되 함수를 넘기는 것처럼 람다를 사용할 수 있게 되었죠. 이렇듯 객체지향 언어인 자바에서도 람다라는 함수형 프로그래밍 개념을 도입할 정도로 람다는 강력한데, 코틀린에서는 자바보다 훨씬 람다를 풍부하게 사용할 수 있도록 해줍니다. 코틀린을 사용하다보면 람다를 자주 사용하게 되는데 아래 코드를 통해 코틀린에서 어떻게 람다를 표현할 수 있는지 간단하게 살펴보도록 하겠습니다.
val people = listOf(Person("Alice", 29), Person("Bob", 32))
// 여기서 이제 가장 나이가 많은 Person 인스턴스를 찾아보자.
// 기본 방식 (함수 인자로 람다를 줄 수 있다.)
1. people.maxBy({ p: Person -> p.age })
// 마지막 인자가 람다라면 밖으로 빼는 것이 가능하다.
2. people.maxBy() { p: Person -> p.age }
// 람다가 유일한 인자라면 () 생략이 가능하다.
3. people.maxBy { p: Person -> p.age }
// 컴파일러가 타입 추론이 가능하다면 생략할 수 있다.
4. people.maxby { p -> p.age }
// 람다 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론 가능하다면 it로 대체할 수 있다.
// 다만 람다가 중첩되다보면 그냥 람다의 파라미터를 명시하는 편이 낫다.
5. people.maxby { it.age }
// 멤버 참조를 사용하여 처리하는 방식
6. people.maxby(Person::age)
Null을 다루는 방식
코틀린은 자바와 타입 시스템이 비슷하면서도 여러 포인트에서 다른 점이 있습니다. 대표적으로 Null에 대한 처리가 자바와 가장 크게 차이가 나죠. 사실 NPE는 개발을 하다보면 정말 많이 마주치게 되는 Exception 중 하나입니다. 그렇기에 자바에서도 null을 처리하는 코드를 if 문을 통해 자주 쓰곤하고, 이를 간단하게 하기 위해 라이브러리를 도입하기도 합니다. 코틀린에서는 null에 대한 접근을 최대한 런타임에서 컴파일 타임으로 옮기려했습니다. 이를 위해 널이 가능한지 불가능한지에 대한 타입 두가지로 나누어서 타입 시스템에 추가했고, 이를 통해 컴파일러가 컴파일 시 널 가능성을 미리 감지해 NPE 가능성을 낮추었습니다. 자바에서도 Optional이라는 기능을 제공하지만 코틀린에서는 단순히 ? 키워드를 붙임으로써 훨신 깔끔하게 해당 문제를 해결했습니다. 또한 런타임에는 널 가능, 불가능 타입 모두 같은 객체이기에 성능 저하 이슈도 없습니다. 추가적으로 null을 다루는 여러 도구를 제공하는데 대표적으로 ?., ?:, !!. ?.let{} 과 같은 방식으로 자바보다 더 깔끔하게 null을 처리할 수 있습니다.
이외에도..
코틀린에서 앞서 말한 내용 이외에도 연산자 오버로딩, DSL, 고차함수 등등 다양한 개념들이 있었습니다. 이러한 것들은 앞으로 코드를 코틀린스럽게 작성해나가는데 도움이 되도록 알고 있어야할 것 같고, 추후 실제 적용해볼 수 있을때 다시 글을 정리해보도록 하겠습니다.
3. 나가며 💨
아직 코틀린 경험이 많진 않지만 그동안 코틀린을 써보고, Kotlin In Action 책을 읽어보며 코틀린에 대해 느낀점은 다음과 같습니다.
1) 기존 자바 코드의 불편함을 개선하고 코드를 간소화하는 방향으로 만들어진 언어같다.
2) 자바에서 좋은 코드를 쓰기 위한 관행들이 코틀린에는 언어적 기능으로써 들어온 것 같다. (기본적으로 상속 불가, 불변객체, Null 불가 타입 등)
3) Data Class 처럼 보일러 플레이트 코드를 생략하는데 도움을 많이 준다.
4) 코틀린은 좀 더 문법적으로 간략하게 표현하는 것을 많이 도와주고, 이를 통해 코드가 간결해지는 것 같다.
5) 자바보다 커뮤니티 사이즈가 작아서 불편한 점이 있다.
위 정도의 느낌이지만 코틀린을 쓰면 쓸수록 자바로 돌아가기 어렵겠다라는 생각이 들 정도로 매력적인 언어인 것 같고, 많은 회사에서도 코틀린을 도입하는 것으로 보아 공부해볼 가치가 충분히 있는 언어라는 생각이 드는 것 같습니다.
이펙티브 코틀린까지 달려보자,,