Kotlin에는 SAM Conversions이 제공됩니다. Single Abstract Method의 약자로 SAM이라고 합니다.
이름에서 알 수 있듯 SAM은 하나의 추상 메서드에 대해서 lambdas 식을 제공합니다. 단, Java에서 작성한 Interface 정의와 이를 활용하는 setOnClickListener
를 kotlin에서 부르는 경우에만 이에 해당합니다.
코틀린에서 유용해야 하는데 실제로는 kotlin에서 작성한 interface와 setOnClickListener
구현체가 있을 경우에는 SAM이 동작하지 않습니다.
이번 글에서는 Java에서 만들어진 interface 정의와 이를 활용하는 setOnClickListener
을 java와 Kotlin에서 구현할 때 다른 점을 알아보고, Anonymous class와 Higher-Order Functions 정의를 이번 글에서 살펴보겠습니다.
OnClickListener 샘플 코드
이번 글에서는 Android SDK에 포함되어 있는 OnClickListener
인터페이스와 setOnClickListener
의 내부 코드를 활용하기 위해서 원 코드를 옮겨보았습니다.
// Interface는 아래와 같습니다.
public interface OnClickListener {
void onClick(View var1);
}
// setOnClickListener을 아래와 같이 구현되어 있습니다.
public void setOnClickListener(View.OnClickListener l) {
throw new RuntimeException("Stub!");
}
Java에서 OnClickListener 구현
먼저 Java 7 기준
으로 아래와 같이 setOnClickListener을 구현하게 되는데 이때 new OnClickListener
을 통해 interface 상속을 구현합니다.
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 구현 부
}
});
Java 8
에서는 기본 람다식을 제공하고 있으므로 아래와 같이 컨버팅 할 수 있습니다.
Button btn = findViewById(R.id.btn);
btn.setOnClickListener(view -> {
// 구현부
});
kotlin에서 setOnClickListener을 구현
setOnClickListener
을 구현하였는데 입력 중 자동완성은 아래와 같이 확인할 수 있습니다.
자동완성에서 2개의 setOnClickListener
을 확인할 수 있는데 lambdas({}
)이 포함되는 경우와 기존과 마찬가지로 일반 구현 식)(()
)이 포함되는 경우 2개입니다.
이중 람다식({}
)으로 만들어진 코드는 아래와 같습니다.
btn.setOnClickListener {
// 구현부
}
()
으로 처리하는 경우는 아래와 같이 setOnClickListener
을 구현할 수 있는데 View.OnClickListener
부분은 생략 가능하여, 결국 람다식(({})
) 표현이 가능합니다.
btn.setOnClickListener(View.OnClickListener {
// 구현부
})
SAM은 언제 동작할까?
선언부(interface, setOnClickListener)가 Java에 있고, kotlin에서 setOnClickListener
을 호출하였을 때만 SAM이 동작합니다.
그래서 아래와 같이 Java에서 SAM Sample을 구현하고,
class JavaSAMSample {
public static void setSAM(SAMInterface sam) {
}
interface SAMInterface {
void onClick(int position);
}
}
이를 kotlin에서 호출하면 아래와 같이 SAM과 기본 메소드 2개가 노출됩니다.
역시 setOnClickListener을 구현할 때와 마찬가지로 2가지 모두 아래와 같이 구현이 가능하죠.
JavaSAMSample.setSAM { }
JavaSAMSample.setSAM(JavaSAMSample.SAMInterface { })
위의 코드의 디컴파일 결과는 아래와 같습니다.
JavaSAMSample.setSAM((SAMInterface)null.INSTANCE);
JavaSAMSample.setSAM((SAMInterface)null.INSTANCE);
결국 setSAM과 SAMInterface 모두가 kotlin 클래스 내부가 아닌 Java class 내부에 있을 경우에만 SAM을 사용할 수 있는데, 2개 중 하나라도 kotlin(.kt)에 포함되어 있을 경우에는 SAM을 사용할 수 없어 아래와 같이 오류가 발생합니다.
kotlin에서 Abstract Method 구현하기
결국 SAM Conversions이 동작하면 lambdas 식을 사용할 수 있습니다. 하지만 kotlin class에 interface와 setter
가 존재하면 lambdas 식을 적용받을 수 없습니다.
lambdas을 대신해서 구현을 하기 위한 Anonymous inner classes을 사용할 수 있습니다.
Anonymous classes을 정의하기 위해서는 Java SAM 샘플 코드에 포함되어 있는 SAMInterface
정의에 1개 이상이 Method을 구현하거나,
class JavaSAMSample {
// SAM 설정을 위한 클래스
public static void setSAM(SAMInterface sam) { }
interface SAMInterface {
void onClick(int position);
void onClickSAM();
}
}
kotlin에서는 그냥 interface 정의만 하더라도 Anonymous class을 이용하여 구현해야 합니다.
interface SAMInterface {
fun onClick(position: Int)
}
먼저 Java에서 setSAM을 아래와 같이 구현할 수 있는데, java 8에서도 lambdas 식이 아닌 아래와 같이 구현해야 합니다.
JavaSAMSample.setSAM(new JavaSAMSample.SAMInterface() {
@Override
public void onClick(int position) {
}
@Override
public void onClickSAM() {
}
});
kotlin에서도 구현을 시도하면, 아래와 같이 SAM을 제외한 setSAM 하나만 노출됩니다.
하나만 노출되어 anonymous classes을 이용해서 구현해야 하는데 영상을 추가하였습니다.
생성한 코드는 아래와 같습니다.
JavaSAMSample.setSAM(object : JavaSAMSample.SAMInterface {
override fun onClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onClickSAM() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
})
이번에는 kotlin에서 작성한 interface을 kotlin의 setSAM에 지정하는 경우도 아래와 같이 사용해야 합니다. 이 경우는 Abstract Method의 수와 무관하게 아래와 같이 구현합니다.
class ExampleUnitTest {
private fun setSAMInterface(onClick: SAMInterface) { }
@Test
fun test() {
setSAMInterface(object : SAMInterface {
override fun onClick(position: Int) {
}
})
}
}
interface SAMInterface {
fun onClick(position: Int)
}
kotlin에서 Abstract Method에 Lambdas 식 적용하기
SAM을 사용하기 위해서는 Java에서 만든 Interface와 1개의 Abstract Method 구현체만 있어야 하고, 자바 클래스에 setter
이 구현되어야 함을 확인하였고, 그 외 모든 경우는 Anonymous classes 정의가 필요함을 확인하였습니다.
이러한 부분을 보완하기 위해서 Higher-Order Functions을 이용해볼 수 있습니다.
아래와 같이 Higher-Order Functions을 setSAM에 2개 넘겨주고, 이를 Anonymous classes을 이용하여 구현하였습니다.
그리고 lateinit에 samInterface 변수를 구현했습니다.
override fun onCreate(savedInstanceState: Bundle?) {
setSAM({}) {}
}
private lateinit var samInterface: JavaSAMSample.SAMInterface
private fun setSAM(onClick: (Int) -> Unit, onClickSAM: () -> Unit) {
samInterface = object : JavaSAMSample.SAMInterface {
override fun onClick(position: Int) {
onClick(position)
}
override fun onClickSAM() {
onClickSAM()
}
}
}
그런데 전혀 쉬워 보이지 않습니다. lambdas 식을 사용하기 위해서 setSAM에 2개의 Higher-Order Functions의 정의가 들어가 있고, 이를 Interface 정의를 쉽게 하기 위해서 또 Anonymous classes을 구현까지 했습니다.
기존 인터페이스에 Anonymous classes만 구현해서 쓰는 게 더 편해보입니다. 또는 Higher-Order Functions만 구현해서 쓰는 게 더 편해 보입니다.
Higher-Order Functions으로 lambdas 식 적용하기
SAM을 알아보려고 했는데 결국 Higher-Order Functions까지 왔습니다. kotlin만 사용한다면 Higher-Order Functions을 적용하는 편이 더 편합니다.
굳이 Anonymous classes와 Higher-Order Functions을 적용해가면서 복잡하게 Java처럼 구현해줄 필요가 없습니다. 그냥 Anonymous classes 정의만 사용하거나, Higher-Order Functions을 따로 사용하는 편이 더 유용합니다.
위에서 봤던 코드에서 Anonymous classes을 제거하고, 아래와 같이 짧은 코드를 만들고, Higher-Order Functions을 사용하는 편이 더 유용합니다.
private fun setSAM(onClick: (Int) -> Unit, onClickSAM: () -> Unit) {
// 구현
}
여기에서 한 단계 더 나아가 lateinit var
을 사용하는 게 가능합니다. 저는 최근에 많이 사용하고 있는 기법인데 RecyclerView Adapter에서 onClick이 발생하면 이를 Higher-Order Functions에 넘기고, 이를 구현하는 쪽에서 받아서 처리하는 방법을 사용하고 있습니다.
그래서 lateinit var
을 아래와 같이 정의하고, 구현할 수 있습니다.
onClick = {
// 구현
}
onClickSAM = {
// 구현
}
private lateinit var onClick: (Int) -> Unit
private lateinit var onClickSAM: () -> Unit
@Test
fun test() {
onClick(0)
onClickSAM()
}
마무리
결국 SAM Conversions을 쓰는 것도 좋겠지만 1개 이상의 Abstract Method가 생길 확률이 높기 때문에 Anonymous classes을 구현하거나, kotlin에서만 사용한다면 Higher-Order Functions을 이용하는 편이 훨씬 유용합니다.