자바의 함수형 인터페이스
만약 코틀린 프로젝트에서 코틀린의 라이브러리를 사용하면, 당연히 코틀린 람다를 이용하여 라이브러리들을 사용할 수 있을 것이다.
코틀린 프로젝트에서 자바로 작성된 라이브러리를 사용하지 않는 것은 쉽지 않은 일일 것이다. 사실 이전까지 살펴본 코틀린의 람다는 상당히 편리한 기능을 많이 제공하도록 설계된 만큼 자바의 라이브러리에 바로 적용할 수 있을까라는 의문이 들긴했다.
하지만 그럼에도 코틀린 람다가 자바 라이브러리를 사용할 때도 똑같이 사용할 수 있다고 책에서 소개하여 꽤 놀랐다. 우선 이러한 호환의 원리를 정리해보려고 한다.
button.setOnClickListener {
println("I was clicked")
}
setOnClickListener 함수는 OnclickListener 타입의 인자를 받아 새 리스너를 설정하는 함수이다.

그런데 여기서 OnClickListener 인터페이스에는 onClick() 메서드밖에 존재하지 않는다.
https://developer.android.com/reference/android/view/View.OnClickListener

위와 같은 인터페이스는 단일 추상 메서드 (SAM, Single Abstract Method) 인터페이스, 혹은 함수형 인터페이스라고 한다.
함수형 인터페이스를 파라미터로 받는 자바 메서드에 람다 전달하기
코틀린에서는 함수형 인터페이스를 파라미터로 받는 모든 자바 메서드에 대해 람다를 전달할 수 있다.
다음과 같은 자바 코드 예시가 있다고 했을 때
void postphoneComputation(int delay, Runnable computation);
마지막 매개변수가 Runnable 이므로, 코틀린에서 위 함수를 호출할 때 다음과 같이 호출할 수 있을 것이다.
postphoneComputation(1000) { println(42) }
컴파일러는 위 코드를 보고 람다를 Runnable을 구현하는 익명 클래스의 인스턴스로 만들고, 람다를 그 인스턴스의 유일한 추상 메서드의 본문으로 만들어준다.
하지만 람다를 함수형 인터페이스로 직접 변환해줘야하는 경우가 있다.
SAM 변환: 람다를 함수형 인터페이스로 명시적 변환
fun createAllDoneRunnable(): Runnable {
return { println("All done!") }
}
이전의 경우에는 Runnable 객체로 변환해야함이 명확하였으나, 위 같은 경우에는 단지 람다만으로는 Runnable 인스턴스가 되지 않으므로, 명시적으로 람다를 SAM 생성자에 전달해줘야 한다.
SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 동일하다. 때문에 위의 코드는 다음과 같이 수정해야 잘 동작한다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
fun main() {
createAllDoneRunnable().run()
}
코틀린에서 SAM 인터페이스 정의
코틀린에서는 fun interface를 이용하여 커스텀 함수형 인터페이스를 정의할 수 있다.
주의해야할 부분은 단일 추상 메서드 인터페이스 이기 때문에 SAM 인터페이스는 비추상 메서드를 여러 개 가질 수 있다.
fun interface IntCondition {
fun check(i: Int): Boolean
fun checkString(s: String) = check(s.toInt())
fun checkChar(c: Char) = check(c.digitToInt())
}
fun main() {
val isOdd = IntCondition { it % 2 != 0 }
println(isOdd.check(1))
// true
println(isOdd.checkString("2"))
// false
println(isOdd.checkChar('3'))
// true
}
위의 예제를 통해 SAM 인터페이스의 동작이 좀 더 잘 와닿았던 것 같다. 우선 Interface임에도 불구하고, SAM 생성자에 추상 메서드의 구현을 정의한 람다를 전달하여 익명 객체를 만들어 isOdd에 저장하고, 문제없이 잘 동작하는 것을 확인할 수 있었다.
수신 객체 지정 람다: with, apply, also
위 함수들은 코틀린 표준 라이브러리의 함수이다. 이 책의 이전 장에서 소개됐던 수신 객체의 개념을 가져왔듯이, 수신 객체를 자동으로 해석해주기 때문에 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출할 수 있게 해주는 라이브러리 함수이다.
with() 함수
with를 사용하면 중복되는 객체의 이름을 획기적으로 줄일 수 있다고 한다. 책에서는 다음과 같은 초기 예시를 제공한다.
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()
}
fun main() {
println(alphabet())
}
사실 이정도만 되어도 특히 로직적으로는 크게 반복되는 부분이 없어보인다.
하지만, 유독 result 변수를 매번 사용해야 한다는 점이 약간 걸린다.
개인적인 생각으로 특히 result가 긴 변수이거나, 클래스의 멤버였다면 더욱 코드를 타이핑하는데 불편함을 느낄 것이다.
코틀린의 with를 사용하면 다음과 같이 리팩터링 할 수 있다!
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) {
for (letter in 'A'..'Z') {
this.append(letter)
}
this.append("\nNow I know the alphabet!")
this.toString()
}
}
fun main() {
println(alphabet())
}
with()는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다. 마지막 인자가 람다라면 괄호 밖으로 빼낼 수 있는 규칙이 있기 때문에 사실 코드는 with(stringBuilder, {...})가 원형이라고 할 수 있겠다.
결론적으로 첫 번째 인자가 람다의 수신 객체가 되므로, this를 이용하여 쉽게 수신객체로 접근할 수 있다.
심지어 책에서는 this를 아예 생략하는 것도 가능하다고 소개한다.
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
}
fun main() {
println(alphabet())
}
간결하긴 하지만, 뭔가 가독성 측면에서는 애매해 보인다는 생각이 든다. 이 기능은 상황에 따라 잘 사용해야 할 것 같다는 생각이 든다.
마지막으로 좀 더 나아가서 식 본문 함수를 이용하면 위 코드를 좀 더 간결하게 만들수도 있다.
fun alphabet() = with(stringBuilder) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
apply() 함수
with가 반환하는 값은 람다 코드가 실행된 결과 즉 람다식의 본문의 마지막 식의 값이다. 하지만 마지막 식의 값이 아닌 수신 객체가 필요한 경우가 있을 것이다.
apply 함수는 with와 동작은 동일하지만, 자신에게 전달된 수신객체를 반환한다는 점이 다르다.
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
fun main() {
println(alphabet())
}
여기서 apply는 확장 함수로 호출되므로, apply 함수의 수신 객체는 StringBuilder이다.
때문에 alphabet 함수의 리턴값은 나중에 toString() 함수가 적용된 String 객체임을 유추해볼 수 있을 것이다.
apply의 활용, 인스턴스 만들고 프로퍼티 초기화하기
fun createViewWithCustomAttributes(context: Context) =
TextView(context).apply {
test = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}
인스턴스를 만드는 즉시 프로퍼티 값을 초기화 하는 경우에 apply를 유용하게 사용할 수 있다!
수신 객체를 인자로 전달하는 also()
apply 함수와 비슷하게 also를 받으며, 수신 객체에 대한 동작을 수행한 후 다시 수신 객체를 돌려준다. apply와의 차이는 also는 수신 객체를 인자로 참조한다.
fun main() {
val fruits = listOf("Apple", "Banana", "Cherry")
val uppercaseFruits = mutableListOf<String>()
val reversedLongFruits = fruits
.map { it.uppercase() }
.also { uppercaseFruits.addAll(it) }
.filter { it.length > 5 }
.also { println(it) }
.reversed()
println(uppercaseFruits)
println(reversedLongFruits)
}
예제코드에서 확인할 수 있듯이 인자로 참조하기 때문에 this가 아닌 it으로 수신 객체를 사용하는 것을 확인할 수 있다.