코틀린 vs 자바 식을 줄 모르는 떡밥
결론부터 말씀드리면 성능은 코드 작성 방식에 따라 달라집니다. 코틀린의 이점을 적절히 활용한다면 자바보다 성능상 이점을 누릴 수 있고요 반대로 자바처럼 코틀린을 사용했다면 느려질 수도 있습니다.
상황에 따라 다르다는 말은 여전히 애매모호하죠? 그런 의미에서 자바 대비 코틀린의 성능을 좌우할 수 있는 몇 가지 포인트를 정리해 봤습니다.
참고로 이번 글은 Baeldung 포스트에서 많은 도움을 받은 점을 밝힙니다.
인라인 함수는 자바에선 볼 수 없는 코틀린의 고유한 기능입니다. 람다를 포함하는 함수를 인라인 함수의 형태로 사용하면 코틀린은 함수 실행 시 매번 람다 객체를 생성하는 대신 람다 함수를 실행시킵니다.
텍스트 설명 만으로는 이게 무슨 소리인지 이해하기 어려울 것 같아서 짧은 예제를 준비했습니다.
fun callFunction(action: () -> Unit) {
action()
}
fun main() {
repeat(1000) {
callFunction { println("Hello") }
}
}
위 코드의 callFunction 은 람다 함수를 실행하는 간단한 함수입니다. main 함수에선 callFunction을 1000회 실행시키는데 이때 람다 객체 action 인자가 1,000 번 생성되게 됩니다.
inline fun callFunction(action: () -> Unit) {
action()
}
fun main() {
repeat(1000) {
callFunction { println("Hello") } // 객체 생성 없이 코드가 삽입됨
}
}
반면에 인라인 함수를 사용하면 객체가 생성되지 않고 함수가 1000번 실행됩니다. 객체를 매번 생성하지 않아도 되기 때문에 GC 대상이 되지 않고 시스템에 무리를 주지 않게 되죠.
자바에는 inline 함수 같은 기능이 없기 때문에 람다 함수를 사용한다면 매번 객체를 새로 생성해야 하므로 추가 GC 작업이 발생하게 됩니다. 코틀린을 이용해 성능상의 이점을 볼 수 있는 부분이죠.
실험에선 인라인 함수를 사용한 경우와 그렇지 않은 경우 결과 값을 비교해 보니 44% 정도 빨랐다고 합니다. 예제 코드처럼 고차원의 함수를 사용하는 경우 (higher order function) 인라인의 유무가 성능에 영향을 미치는 정도가 크다는 것을 알 수 있습니다.
자바의 경우 list에서 map이나 filter 함수를 사용하려면 매번 Stream으로 변환해서 처리해야 하므로 추가적인 객체 생성 작업이 필요합니다. 하지만 코틀린에서는 자체 내장된 기능이 있어 추가 객체 생성이 없기 때문에 성능 상에서 이점을 가져갈 수 있을 것이라고 가정할 수 있습니다.
하지만 테스트 결과는 예상과 다릅니다. 자바가 코틀린보다 12% 정도 높은 성능을 보였는데요 코틀린의 배열 내장 함수에서 null check 같은 과정이 포함되어 있기 때문이었습니다. 코틀린에 있는 추가 기능 때문에 성능상에 페널티가 생기고 있었죠. 하지만 배열 길이가 길지 않다면 무시하고 넘어가도 될 수준이라고 합니다.
자바에서는 가변인자를 배열의 형태로 사용할 수 있습니다. 객체가 배열의 형태 데이터라면 가변 인자를 이용해서 함수 인자로도 사용할 수 있죠.
public class VarargsExample {
public static int sum(int... numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
public static void main(String[] args) {
System.out.println("Sum of 1, 2, 3: " + sum(1, 2, 3));
}
}
하지만 코틀린의 경우에는 가변 인자가 특수한 케이스입니다. 그리고 자바와 달리 배열이 자동으로 가변인자로 변환되지 않기 때문에 함수 인자로 실행하려는 경우 배열을 개별 요소로 분할해서 전달해야 합니다.
fun printNumbers(vararg numbers: Int) {
for (number in numbers) {
println(number)
}
}
fun main() {
val data = intArrayOf(1, 2, 3)
printNumbers(*data)
}
이렇게 스프레드 연산자를 활용하는 경우 성능에 17% 정도 페널티가 발생하게 됩니다. 그러므로 코틀린으로 코드를 작성한다면 가능하면 가변인자를 사용하지 않는 게 좋습니다.
자바와 차별화되는 코틀린의 가장 큰 특성은 코루틴이라고 할 수 있겠죠? 코루틴은 자바 스레드의 경량화된 버전이라고 말할 수 있습니다.
자바 스레드는 OS 스레드를 사용하기 때문에 컨텍스트 스위칭 시 오버헤드도 많고 개별 동작에 필요한 리소스도 많습니다. 하지만 코루틴은 OS 스레드처럼 작동하면서도 OS 스레드의 경량화된 버전이라 적은 리소스로 작동합니다. 자바에선 여러 개의 스레드를 동시에 운영해야 하는 부담이 있었다면 코루틴은 코루틴 디스패쳐에게 일임하게 되기 때문에 책임에서도 자유롭습니다.
메모리 사용량뿐만 아니라 스레드 관리에서 자유롭고 추가로 비동기 작업 + 병렬처리 작업까지 가독성 높은 코드를 작성해서 간단히 처리할 수 있기 때문에 성능뿐만 아니라 생산성 측면에서도 좋은 기능이라고 할 수 있죠.
참고로 최근에는 자바에도 기존 스레드의 경량화된 버전인 가상스레드라는 기능이 도입되면서 성능에선 많은 개선점을 보이고 있습니다.