안녕하세요! Betalabs 백엔드 개발팀의 Woogie입니다.
이번에는 코틀린 공식 문서와 Kotlin In Action을 참고해서 코틀린 람다에 대해 알아보겠습니다. :)
아래에 대해서 알아볼 것입니다.
첫째, 람다란?
둘째, 람다를 왜 사용하는가?
셋째, 람다 표기법
넷째, 컬렉션에서 람다 활용하기
다섯째, Receiver 지정 람다 그리고
여섯째, 마치며
Kotlin In Action
람다 식lambda expression
또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.
저는 간단한 표현식으로 함수를 전달할 수 있는 표현 방법이라고 이해했습니다.
기본적으로 코틀린의 함수는 일급 함수(first-class)입니다.
간단히 일급 함수라는 것은 함수를 변수나 자료구조로 담아낼 수 있고, 파라미터로 다른 함수로 전달하거나 함수를 리턴 가능한 것입니다.
쉽게 예를 들면,
// 함수를 변수로 저장
val apple = { fruit: String -> println("fruit: $fruit") }
apple.invoke("🍎")
// 함수를 파라미터로 전달
val bucket = listOf("🍏", "🍌", "🍊", "🍇").fold("🍎") { acc: String, next: String ->
"$acc, $next"
}.also { println(it) }
와 같이 활용할 수 있겠습니다.
람다를 사용하는 이유는 무엇보다 간결함에 있다고 생각합니다.
간결하게 표현함으로써 가독성이 좋아지고 그것은 곧 생산성과도 직결될 수 있다는 생각입니다.
우리는 읽기 쉽고 이해하기 좋은 코드를 선호합니다. 그렇기 때문에 람다를 적극적으로 사용하고 있습니다.
아래는 오라클 공식 문서에서 발췌한 내용입니다.
One issue with anonymous classes is that if the implementation of your anonymous class is very simple, such as an interface that contains only one method, then the syntax of anonymous classes may seem unwieldy and unclear.
간단한 구현을 위해서 익명 클래스를 이용하는 건 장황하고 그런 구문은 불분명한다는 것을 이야기하고 있습니다.
코틀린의 람다 표기법은 다음과 같습니다.
// 매개변수 없음
() -> A
// 매개변수 있음
(A, B) -> C
// 생략 전
(a: Int, b: Int) -> Int -- O
// 생략 후
(a, b) -> Int -- O
// 생략 불가능
(a, b) -> -- X
// 람다식
{a, b -> a + b}
// 확장함수
String::toInt
// block 이 마지막 파라미터 아닐 때
fun greeting(block: (String) -> Any, value: String) {
block(value)
}
greeting({ println(it) }, "hello world!!!") -- X
// block이 마지막 파라미터 일 때
fun greeting(value: String, block: (String) -> Any) {
block(value)
}
greeting("hello world!!!") { println(it) }
Java 8부터 도입된 stream
을 이용하면, 컬렉션을 명령형이 아닌 선언형으로 제어할 수 있어 더욱 가독성 좋은 코드 작성이 가능해졌습니다.
코틀린 은 컬렉션을 더욱 편하게 제어할 수 있게 기본으로 많은 확장 함수를 제공하고 있고, 람다를 이용해 더욱 간결하게 표현 가능합니다.
class Person {
private final String name;
private final int age;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
}
final Person billGates = new Person("Bill Gates", 12);
final Person markZuckerberg = new Person("Mark Zuckerberg", 15);
final Person elonMusk = new Person("Elon Musk", 4);
final int max = Stream.of(billGates, markZuckerberg, elonMusk)
.map(Person::getAge)
.max(Comparator.comparingInt(age -> age)).orElse(0);
System.out.println(max); -- 15
// kotlin
class Person(
val name: String,
val age: Int
)
val billGates = Person("Bill Gates", 12)
val markZuckerberg = Person("Mark Zuckerberg", 15)
val elonMusk = Person("Elon Musk", 4)
val maxAge = listOf(billGates, markZuckerberg, elonMusk).maxOf { person -> person.age }
println(maxAge) -- 15
위와 같이 코틀린은 컬렉션을 제어하는 유용한 확장 함수들을 제공하고 있습니다.
Kotlin In Action
수신객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출 할 수 있게 하는 것이다.
그런 람다를 수신객체 지정 람다lambda with receiver
라고 부른다.
수신 지정 객체 람다를 선언하는 방법은 람다의 파라미터를 감싸던 소괄호를 수신 객체 타입의 오른쪽으로 빼낸 후 마침표(.)를 넣는 형태입니다.
예시로 보면, 다음과 같습니다.
T.() -> R
이런 lambda with receiver
는 코틀린이 제공하는 스코프 함수 with
과 apply
를 알아보면 자세히 알 수 있습니다.
"hello world".apply {
println("$this!!") -- "hello world!!"
}
apply
와 with
의 비슷하지만 한 가지 다른 점이 있습니다.
예시를 통해 알아보겠습니다.
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
위와 같이 apply
는 수신 객체를 return
합니다.
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
with
는 람다를 호출해서 얻은 결과를 return
합니다.
위와 같은 receiver
를 이용해 계층 형태의 데이터 구조를 손쉽게 만들어 낼 수 있고, 이를 builder
라고 합니다.
빌더를 이용해 Kotlin DSL(Domain Specific Language)
를 만들어 낼 수 있고, 이를 이용한 대표적인 예시가 바로 gradle
입니다.
아래는 Kotlin DSL
사용의 예입니다.
dependencies {
testImplementation(kotlin("test"))
}
import kotlinx.browser.*
import kotlinx.html.*
import kotlinx.html.dom.*
fun main() {
document.body!!.append.div {
h1 {
+"Welcome to Kotlin/JS!"
}
p {
+"Fancy joining this year's "
a("https://kotlinconf.com/") {
+"KotlinConf"
}
+"?"
}
}
}
Kotlin DSL
을 이용하면, 간결하고 타입 안전한 builder
를 손쉽게 만들어 낼 수 있고, 구조화된 집합을 만들어내기 용이합니다.
개발을 하며 대표적으로 람다가 쓰이는 범위는 역시 컬렉션과 스코프 함수라는 생각이 들었습니다. 물론, 컬렉션과 스코프 함수를 사용할 때에는 람다에 대해 별생각 없이 접근해도 충분히 사용 가능합니다.
다만, 람다의 특성과 활용 범위에 대해 알고 있다면 더욱 간결하고 생산성 높은 코드 생성을 위한 도구를 만들어 낼 수 있겠다는 생각이 들었습니다.
참고로 베타 랩스에서는 JDSL이라는 JPA Criteria API
를 만들어주는 DSL 오픈소스를 적극 활용하고 있습니다.
그 편리함과 간결함 덕에 생산성과 코드 품질 두 마리 토끼를 잘 잡고 있습니다.
잘 보고 갑니다!