101. (Java to Kotlin) 1. 자바에서 코틀린으로

1. 코틀린 소개

원제 : Introduction

1.1. 함수형 프로그래밍 측면에서의 자바 발전사

원제 : An Opinionated History of Java Programming Style

모든 프로그래밍 언어에는 일종의 결이 존재한다.

이 결을 무시하는 경우, 목적을 달성하기 위해 더 많은 코드를 작성해야하고, 성능이 저하되거나 실수할 여지가 늘어나게 된다.

아래 코드를 보자.

import kotlinx.coroutines.*
import kotlin.collections.shuffle

fun main() {
    val numbers = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    val sum = numbers.fold(0, Int::plus)

    println("sum : $sum")
}

코틀린의 fold()는 컬렉션의 확장 함수로 컬렉션을 순회하면 주어진 동작을 수행한다.

1
2
3
4
5
public inline fun <R> IntArray.fold(initial: R, operation: (acc: R, Int) -> R): R {
var accumulator = initial
for (element in this) accumulator = operation(accumulator, element)
return accumulator
}

만약에 자바 1.0을 이용하여 위의 코드를 구현한다면 어떻게 될까?

자바 1.0(좀 더 정확히는 자바 8 이전)에는 함수가 1급 객체가 아니었기 때문에 클래스를 직접 작성하고 인터페이스를 작성했어야 했다.

참고 1급 객체(First-class citizen) 이란 무엇인가?
함수형 프로그래밍에서 1급 객체란 1급 시민의 3가지 조건을 모두 충족하는 객체를 말한다.
1급 시민의 3가지 조건은 아래와 같다.

  1. 변수나 데이터에 할당할 수 있어야 한다.
  2. 객체의 인자로 넘길 수 있어야 한다.
  3. 객체의 반환값으로 반환할 수 있어야 한다.

아래가 그 예시이다.

1
2
3
public interface Function2 {
Object apply(Object arg1, Object arg2);
}

이후 fold() 함수에 대한 고차 함수를 작성해주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
public class Vectors {
public static Object fold(Vector l, Object initial, Function2 f) {
Object resut = initial;
for (int i = 0; i < l.size(); i++) {
result = f.apply(result, l.get(i))
}
return result;
}

// ...
}

이때 fold() 함수에 넘기는 함수 타입 개수가 N개라면 N개의 클래스를 전부 별개로 작성해야 한다.

자바 1.0은 제네릭, 오토박싱, 클로저 등도 없었기에 모든 인자의 타입을 직접 캐스팅하고 참조 타입과 원시 타입을 반환하도록 박싱 코드까지 작성해야 한다.

1
2
3
4
5
6
7
public class AddIntegers implements Function2 {
public Object apply(Ojbect arg1, Object arg2) {
int i1 = ((Integer)arg1).intValue();
int i2 = ((Integer)arg2).intValue();
return new Integer(i1 + i2);
}
}

여기까지 작성하고 나서야 목적을 달성할 수 있다.

1
int sum = ((Integer)Vectors.fold(counts, new Integer(0), new AddInegers())).intValue();

이처럼 함수형 프로그래밍을 지원하지않는 언어에서는 많은 비용을 투입해야 하는 경우가 잦았다.

물론 이게 이유의 전부는 아니다.

자바에는 표준 함수 타입이 없으므로, 여러 라이브러이에 정의된 함수 타입간의 변환을 수행하는 어댑터도 일일이 작성해야했고,

JVM도 단순한 가비지 컬렉터만 가지고 있었고, JIT 컴파일러가 없었기때문에 아래의 명령문 스타일보다 성능이 떨어졌다.

1
2
3
4
int sum = 0;
for (i = 0; i < counts.size(); i++) {
sum += ((Integer)counts.get(i)).intValue();
}

즉, 함수형 프로그래밍은 자바 1.0의 결을 거스르는 행위라고 볼 수 있다.

이후 자바가 버전 업 되면서 점진적으로 편하게 코드를 작성할 수 있게 되었다.

자바 1.1에서 익명 내부 클래스(anonymous inner class)가, 자바 2에서 컬렉션 프레임워크가 추가 되면서

아래와 같이 코드를 작성할 수 있게 되었다.

1
2
3
4
5
6
7
8
9
int sum = ((Integer)List.fold(counts, new Integer(0), 
new Function2() {
public Ojbect apply(Ojbect arg1, Object arg2) {
int i1 = ((Integer)arg1).intValue();
int i2 = ((Integer)arg2).intValue();
return new Integer(i1 + i2);
}
}
)).intValue();

하지만 여전히 자바 2의 결을 거스른다.

이후 자바 5에서 제네릭이 추가되면서 보일러 플레이트를 획기적으로 줄일 수 있게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
public interface Function2<A, B, R> {
R apply(A arg1, B arg2);
}

int sum = List.fold(counts, 0,
new Function2<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer arg1, Integer arg2) {
return arg1 + arg2;
}
}
);

아직 완벽하게 자바 5의 결을 따르진 않지만, 함수형 프로그래밍의 트렌드가 시작된 순간이기도 하다.

이후 자바 8에 람다 식과 메서드 참조 및 Stream API가 추가되면서 비로소 한 줄로 코드를 작성할 수 있게 되었다.

1
int sum = counts.stream().reduce(0, Integer::sum);

결과적으로 자바라는 언어는 발전을 거듭해 자바 8 이후부터는 함수형 스타일을 적용할 수 있게 되었다.

1.2. 코틀린의 결

원제 : The Grain of Kotlin

코틀린은 비교적 최근에 나온 언어이지만, 자바와는 그 결이 다르다.

코틀린은 크게 4가지 목표를 가지고 있으며, 그 목표는 아래와 같다.

간결성
코틀린은 간단하다.

자바에서의 반복적인 작업들을 코틀린은 묵시적으로 제공한다.

안정성
코틀린은 자바와 마찬가지로 JVM에서 실행되기때문에 메모리 안정성이 뛰어나며,

뛰어난 타입 추론 능력을 가지고 있어, 타입 안정성도 보장해준다.

호환성
자바로 작성된 메서드의 호출이나 클래스의 삭송, 인터페이스의 구현 등

자바에서 적용하던 모든 것들을 코틀린에서도 수행할 수 있다.

도구 친화성
코틀린을 개발하는 곳과 IDE의 개발사는 JetBrains로 동일하다.

이미 IntelliJ를 사용하고 있다면 별다른 IDE에 대한 적응없이 바로 코틀린을 시작할 수 있다.

위 목표를 달성하기 위해 코틀린과 표준 라이브러리는 아래와 같은 선호도를 기준으로 코틀린을 개발하였다.

  1. 가변 상태를 변경 < 불변 데이터를 변환
  2. 암시적 동작 < 명시적 동작
  3. 동적 바인딩 < 정적 바인딩
  4. 불확실성 < 통제 범위
  5. 코드의 규칙과 정책 < 마이그레이션의 용이성 (Java only)

결과적으로 코틀린에 능숙해지는 방법은, 자바와 코틀린 두 언어의 결을 이해하는 것이다.