[Kotlin] SAM(Single Abstract Method) 이란?

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을 구현하였는데 입력 중 자동완성은 아래와 같이 확인할 수 있습니다.

kotlin-setOnClickLisetner

자동완성에서 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개가 노출됩니다.

java-sam

역시 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을 사용할 수 없어 아래와 같이 오류가 발생합니다.

Unresolved-reference

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 하나만 노출됩니다.

java-sam-02

하나만 노출되어 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을 이용하는 편이 훨씬 유용합니다.