본문 바로가기

Kotlin

코틀린, 코틀린답게 사용하기 - 4. Lambda

자바에서는 메서드를 파라미터로 넘길 수가 없어서, 메서드가 하나만 존재하는 특별한 인터페이스를 만들고 무명클래스를 이용하여 전달하곤합니다. 하지만 함수형언어인 코틀린에서는 함수를 일반 값처럼 다룰 수 있어서 변수에 저장할 수 있고, 다른 함수에 전달할 수 있습니다!

 

람다는 이 특별한 인터페이스를 대체할 수 있고, 함수형 API 와 확장함수가 더해져 많은 기능을 할 수 있습니다.

이번 포스팅에서는 코틀린에서 람다의 특징과 관례들에 대해서 알아보려고합니다.

 

1. Closure

코틀린에서는 람다 밖 함수에 있는 변수에 접근할 수 있고, 변경할 수도 있습니다.

자바와 달리 코틀린에서는 람다 외부에 있는 final이 아닌 변수에 접근할 수 있습니다. 

fun printProblemCounts(responses: Collection<String>) {
  var clientErrors = 0
  var serverErrors = 0

  responses.forEach {
    if (it.startsWith("4")) {
      clientErrors++
    } else if (it.startsWith("5")) {
      serverErrors++;
    }
  }  
}

 

2. Lazy Evaluation

코틀린의 람다는 기본적으로 Eager evaluation 입니다. 많은 경우에 이를 Lazy evaluation으로 동작하게하여 효율적으로 만들 수 있는 방법이 있습니다. 

 

불필요한 연산을 피하기위해 연산을 미루는 것을 Lazy evaluation 이라고 합니다.

 

간단하게 예제를 살펴보죠.

 

val findFirst = listOf(1, 2, 3, 4, 5, 6).filter { it % 2 == 0 }.map { value -> value * 10 }.first()

 

  1. 1~6까지 원소를 포함하고있는 리스트를 생성하고  [1, 2, 3, 4, 5, 6]
  2. 원소 중 짝수만 걸러낸 후                                      [2, 4, 6]
  3. 원소에 10을 곱하여                                              [20, 40, 60]
  4. 첫번째 원소를 반환한다.                                            20

 

기본적으로 코틀린의 람다는 위와같이 동작합니다. 각 단계가 끝나고 다음 단계로 넘어가게되는 것이죠. 이를 Eager Evaluation 이라고 합니다. 

 

위와 달리 자바의 스트림의 동작은 조금 다릅니다.

 

List.of(1,2,3,4,5,6).stream()
  .filter(i -> i % 2 ==0)
  .map(value -> value * 10)
  .findFirst();

 

  1. 1~6까지 원소를 포함하고있는 리스트를 생성하고  [1, 2, 3, 4, 5, 6]
  2. filter의 조건에 충족하는 첫 번째 원소를 찾은 후          2
  3. 10을 곱하고 반환한다.                                              20 

첫번째 filter 연산과 두번째 map 연산은 처음 요소를 제외하고는 불필요한 연산입니다.

자바의 스트림은 연산을 미루다가 필요한 순간에만 연산을 실행합니다.

 

단계마다 진행되는 람다와 달리 다음과 같이 수직적으로 실행됩니다.

  1. 원소 1에 대해 짝수 판단 -> 짝수가아니므로 다음단계 x
  2. 원소 2에 대해 짝수 판단 -> 짝수이므로 10을 곱하고 리턴 (이후 3~6의 원소에 대하여 연산을 무시함)

이는 수평적으로 수행되던 Eager evaluation 과 달리 많은 연산을 줄일 수 있습니다.

 

 

코틀린의 Sequence 는 자바의 스트림의 개념과 일치합니다. 다음과 같이 List 를 시퀀스로 만들어 Lazy evaluation 으로 수행되게 바꿀 수 있습니다.

  val findFirst = listOf(1, 2, 3, 4, 5, 6).asSequence()
          .filter { it % 2 == 0 }.map { value -> value * 10 }.first()

 

또한 map, filter 등의 함수형 API 는 리스트를 반환합니다. 이는 중간단계에 임시로 리스트가 생기는 것을 의미하는데, 시퀀스를 사용하면 불필요한 중간단계의 리스트가 생성되지 않기때문에 연산을 효율적으로 실행할 수 있습니다.

 

사실 위의 람다식은 다음과같이 시퀀스로 만들지 않고 간단하게 할 수 있습니다.

listOf
(1, 2, 3, 4, 5, 6).first { it % 2 == 0 } * 10

불필요한 연산도 불필요한 컬렉션도 생성이 안되겠네요~
많은 경우에 함수형 API 를 적절하게 사용하는것만으로도 성능과 가독성을 모두 충족시킬 수 있습니다!

 

 

 

3. With & Apply

수신 객체 지정 람다인 apply 를 통해 빌더 스타일의 API를 사용해 생성하고 초기화할 수 있습니다.

 

코틀린에서는 어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있습니다.

 

fun alphabet (): String {
  val stringBuilder = StringBuilder()
  return with(stringBuilder) {
    for (letter in 'A'..'Z') {
      append(letter)
    }
      append("\nNow I know the alphabet!")
      toString()
  }
}

 

with 함수를 이용하여 다음과 같이 StringBuilder의 인스턴스인 stringBuilder를 참조하지 않고 내장 메서드를 호출하는 것처럼 호출할 수 있습니다.

 

저는 이 with 함수를 보자마자 아~ System.out.println() 을 코틀린에서 간단히 println() 으로 쓸 수 있었던게 이거였구나! 라고 생각했는데 그건 아니더라구요..

public actual inline fun println() { System.out.println() }

이렇게 inline 함수로 선언돼있었네요!

 

이렇게 단순히 Stringbuilder 의 인스턴스를 with로 참조하는 상황에서는 Apply 함수를 이용하여 객체의 인스턴스를 만들면서 같은 동작을 할 수 있습니다.

 

fun alphabet () = StringBuilder().apply { 
  for (letter in 'A'..'Z') {
    append(letter)
  }
    append("\nNow I know the alphabet!")
}.toString()

 

apply 를 통해 StringBuilder 의 인스턴스가 반환되고 다시 toString 메서드를 통해 스트링 객체가 반환되는 모습입니다!