몰?.루();

안드로이드 코틀린 람다 정리 (SAM) 본문

프로그래밍/안드로이드, 코틀린

안드로이드 코틀린 람다 정리 (SAM)

toonraon 2022. 1. 3. 21:58

자바8 이전의 옛날 문법이 익숙하다보니 코틀린은 물론이고 최근의 자바 문법들도 익숙하지 않습니다.

그 중에서 특히 적응 안 되는 게 람다라서 정리해보기로 했습니다.

 

특히 많이 쓰이는 setOnClickListener를 통해서 람다와 SAM(Single Abstract Method)에 대해 정리해보겠습니다.

 

        // 코틀린에서 가장 흔하게 볼 수 있는 코드
        val myButton = Button(this)
        myButton.setOnClickListener {
            println(it.id) // 버튼이 눌리면 버튼 자신의 id를 출력
        }

버튼을 하나 선언하고 그 버튼을 누르면 버튼 자신의 id를 출력하는 아주 간단한 코드입니다.

아주 간단하지만 제 입장에서는 너무 간단해서 오히려 이해할 수가 없었습니다. 제가 익숙하던 옛날 자바 코드와 너무 다르기 때문입니다.

 

        // 자바의 코드
        Button myButton = new Button(this);
        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(v.getId());
            }
        });

똑같은 코드임에도 상당히 깁니다. 하지만 저에겐 굉장히 익숙한 코드입니다. 이런 코드에 익숙해져있다보니 코틀린의 짧은 코드에 적응이 안 되었습니다. 물론 자바8에서는 자바 역시 람다를 지원하므로 저런 식으로 코드를 짜놓으면 안드로이드 스튜디오에서 람다식을 쓰라고 알려줍니다.

 

 

자바도 요즘은 람다 다 지원합니다.

Replace with lambda를 눌러 위 JAVA 코드를 코틀린으로 변환하면 이렇게 바뀝니다.

        // 자바8의 람다
        myButton.setOnClickListener(v -> System.out.println(v.getId()));

역시 굉장히 짧습니다. 그러나 요즘에 솔직히 안드로이드 개발하는데 누가 자바를 쓰겠습니까. 자바8 쓸바엔 그냥 코틀린을 쓰지. 그래서 그냥 코틀린 람다에 대해서만 정리하기로 했습니다.

 

 

먼저 코틀린의 가장 긴 코드를 보겠습니다.

        // 람다식을 사용하지 않은 코틀린 코드
        myButton.setOnClickListener(object: View.OnClickListener {
            override fun onClick(v: View) {
                println(v.id)
            }
        })

제가 익숙한 구식 자바의 코드와 대체적으로 비슷합니다. (세미콜론을 코틀린은 안 쓴다든가, 함수 선언을 fun으로 한다든가, 자바는 getId()라는 Getter를 쓰는데 코틀린은 그냥 바로 v.id로 접근한다든가 하는 건 물론 많이 다른 점이긴 하지만 오늘 이야기할 것과는 거리가 있으므로 무시합니다.)

가장 두드러지는 차이점은 일단 object: 입니다.

여기서 object는 자바에서 흔히 말하는 object(객체)와는 좀 다릅니다.

 

코틀린에서 익명 클래스를 생성할 때 object: 를 씁니다.

즉 자바에서 new를 이용해서

        // 자바 익명 클래스
        View.OnClickListener l = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(v.getId());
            }
        };

이렇게 쓰던 익명 클래스가 코틀린에서는

        // 코틀린의 익명 클래스
        val l = object: View.OnClickListener {
            override fun onClick(v: View) {
                println(v.id)
            }
        }

이렇게 바뀐 것입니다. 코틀린에서는 new가 없으니까 object: 를 통해서 익명 클래스를 만든다고 생각하면 됩니다.

 

 

코틀린의 가장 기본 뼈대 코드에 대해서는 이 정도면 이해를 했다고 생각하고 람다를 이용해서 코드를 한 번 줄여봅시다.

        // 코틀린 1차 축약
        myButton.setOnClickListener(View.OnClickListener {
            v: View -> print(v.id)
        })

아까 코틀린 기본 코드와 비교해보면

        // 람다식을 사용하지 않은 코틀린 코드
        myButton.setOnClickListener(object: View.OnClickListener {
            override fun onClick(v: View) {
                println(v.id)
            }
        })

2번째 줄의 override fun onClick(v: View) { ... } 이 부분이 통째로 날아간 것을 알 수 있습니다.

그리고 그 부분이 v: View -> print(v.id)로 바뀌었습니다.

또한 object도 사라졌습니다. 익명 클래스가 아니라 람다식으로 바뀌었기 때문입니다. (단순히 코드가 짧아진 것뿐만 아니라 메모리 관리 방법도 다르다고 합니다. 람다식쪽이 더 좋다는데 자세한 건 아직 모르겠으니 패스)

 

v: View -> print(v.id)

여기서 v: View 부분이 파라미터 부분, 그니까 (v: View)이고 print(v.id)는 뭐 말 안해도 알다시피 onClick의 몸체부분입니다.

 

아니 그럼 onClick이라는 함수 이름은 어디로 갔지?

람다식으로 줄이면서 함수의 파라미터와 몸체 부분은 있는데 정작 중요한 함수 이름이 없습니다. 제가 onClick을 호출하려고 하는 건지 onLongClick를 호출하려는 것인지 그걸 컴파일러가 어떻게 알고 함수 이름을 생략해도 되는거지? 뭐 컴파일러가 요샌 독심술이라도 쓰나?

 

이유는 View.OnClickListener 때문입니다.

    public interface OnClickListener {
        void onClick(View v);
    }

OnClickListner는 onClick이라는 단 하나의 추상 메소드(=함수)만을 가집니다. 이걸 SAM(Single Abstract Method)라고 합니다. onClick 앞에 abstract가 없는데 어떻게 onClick이 추상 메소드냐고 할 수도 있지만 OnClickListener가 interface이므로 그 안에 있는 함수는 자연스레 abstract가 붙은 것과 똑같으므로 onClick은 자연스레 추상 메소드가 됩니다.

 

그리고 알다시피 추상 메소드는 반드시 override 해서 사용해야합니다. 따라서 제가 OnClickListener를 호출했다는 것은 반드시 onClick을 override해서 쓸 거란 걸 나도 알고 쟤도 알고 컴파일러도 안다는 것입니다. 따라서 OnClickListener 안에서 굳이 override를 쓸 필요가 없으니 줄여 쓰는 것입니다.

 

다만 만약에 OnClickListener 안에 onClick만 있는 게 아니라 만약에

    public interface OnClickListener {
        void onClick(View v);
        void onClick2(View v)
    }

이렇게 여러 메소드가 있었다면 컴파일러 입장에서 프로그래머가 OnClickListener를 쓰겠다고 말해도

"너 그럼 onClick이랑 onClick2 둘 다 오버라이딩 해서 써야하는데 함수 2개를 람다로 어케 줄이냐?"라고 하기 때문에 이 경우엔 람다식을 사용할 수 없습니다. SAM 이라는 이름처럼 추상 메소드가 딱 1개(Single Abstract Method)만 있어야 가능한 것입니다.

 

 

실제론 OnClickListener 안에는 추상 메소드가 딱 1개이므로

컴파일러는 우리가 onClick을 override해서 쓸 거란 걸 알고 있기 때문에 그냥 onClick을 생략해버린 게 아까 본 그 코드입니다.

        // 코틀린 1차 축약
        myButton.setOnClickListener(View.OnClickListener {
            v: View -> print(v.id)
        })

 

 

그런데 위 코드를 써보면 안드로이드 스튜디오가 View.OnClickListener를 새까만 글씨로 표시하면서 "이거 쓸 필요 없으니 지우는 게 어때?"라고 강력히 어필합니다.

Remove redundant SAM-constructor라고 하는데 즉 불필요한 SAM 생성자는 지우라는 말입니다.

 

 

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

생각해보면 JAVA의 setOnClickListner 함수를 쓴 순간 인자엔 OnClickListener를 쓸 게 뻔합니다. 저기에다가 뭐 뜬금없이 int를 집어넣거나 할 순 없으니까.

그럼 컴파일러 입장에서는 우리가 myButton.setOnClickListener( 라고 쓴 순간 "이 프로그래머는 이제 파라미터로 OnClickListener를 써야하고, 또 OnClickListener는 가지고 있는 추상 메소드가 onClick 하나 밖에 없으니까 onClick 메소드까지 쓰겠지?"라고 빠르게 눈치를 챌 수 있기 때문에 사실 우린 OnClickListener를 쓸 필요도, onClick을 쓸 필요도 없습니다.

 

        // 코틀린 2차 축약
        myButton.setOnClickListener({
            v: View -> print(v.id)
        })

그래서 OnClickListener도 지웠습니다.

그런데 여기서 또 생각해보면 v: View라는 것은 onClick(v: View)에서 파라미터 부분을 따온 것입니다. onClick에 들어갈 파라미터는 View뿐인데 우리가 굳이 v: View라고 또 써야할 필요가 있을까요?

 

생각해보면 무조건 v의 클래스는 View인 게 확실합니다. v가 Int일리도 없고 String 일리도 없죠. onClick의 파라미터가 View라는 건 컴파일러가 더 잘 아는 사실이니까요.

 

따라서 굳이 v: View라고 v가 View라고 알려줄 필요가 없습니다.

그래서 이렇게 써버리면... 에러가 납니다.

왜냐하면 1개뿐인 파라미터의 변수명은 사람마다 다 다르게 할 수 있기 때문입니다. 난 v라고 하고 싶은데 누군가는 view라고 할 수도 있고 누구는 superDuperKingAwsomeInsaneView 라고 하고 싶을 수도 있고 아니 애초에 onClick의 경우에나 파라미터가 View인 것이지 다른 SAM에서는 하나 뿐인 파라미터가 View가 아니라 전혀 다른 걸 수도 있습니다. 그럼 v로 통일하자는 억지도 불가능해지죠. 다른 SAM에선 파라미터가 String일 수도 있는데 그걸 v로 해버리는 건 이상하니까요.

 

그러기 때문에 하나 뿐인 파라미터 이름을 생략했을 때 그 파라미터 이름은 it으로 하기로 통일했습니다. 뭐 한국어로 치자면 '그거' 같은 느낌이라고 볼 수 있습니다. 만능 단어 '그거'

        // 코틀린 3차 축약
        myButton.setOnClickListener({
            print(it.id)
        })

아무튼 v: View -> 부분도 사라지고 그냥 바로 it으로 호출하는 것으로 바뀌었습니다.

 

그런데 이건 또 이렇게 바꿀 수 있습니다.

        // 코틀린 4차 축약
        myButton.setOnClickListener() {
            print(it.id)
        }

뭐가 바뀐 건가 싶겠지만 )의 위치가 바뀌었습니다.

아니 저게 뭔 이상한 문법이지 싶지만 이게 파라미터가 람다식 하나 뿐이라서 그렇습니다. 원래는 파라미터가 여러 개 있어도 마지막 파라미터가 람다식이면 그 람다식만 소괄호 바깥으로 빼낼 수 있는 규칙이 있습니다.

 

예를 들어 setOnClickListener에 파라미터로 3개를 받는 함수였다면

        // 마지막 파라미터가 람다식이면 바깥으로 빼낼 수 있다
        myButton.setOnClickListener(333, "aaa", { print(it.id) })

여기서 마지막 람다식만 바깥으로 빼내서

        // 마지막 파라미터가 람다식이면 바깥으로 빼낼 수 있다
        myButton.setOnClickListener(333, "aaa") { print(it.id) }

이렇게 쓸 수 있는 것입니다.

이렇게 보면 확실히 바깥으로 람다식을 빼낸 게 좀 더 코드가 예쁩니다.

 

아무튼 다시 setOnClickListener 이야기로 돌아와서, setOnClickListener는 파라미터가 1개뿐이라 람다식을 바깥으로 빼내고 나니 소괄호가 혼자 남아버렸습니다.

 

()가 굉장히 초라해보입니다. 그리고 실제로 안드로이드 스튜디오에서도 ()를 어둡게 표시합니다. 다크모드라 잘 안 보이는데 직접 보면 확실히 다른 건 흰색인데 () 혼자 회색입니다.

 

저렇게 람다식을 빼내고 나서 () 안에 아무 것도 없다면 ()를 그냥 생략할 수 있습니다..

        // 코틀린 5차 축약
        myButton.setOnClickListener {
            print(it.id)
        }

포스팅 제일 처음에 보았던 그 모양입니다.

 

참고로 중요한 것 하나를 빼놓고 계속 이야기 했는데 이렇게 SAM을 쓸 수 있는 것은 JAVA 코드를 코틀린에서 쓸 때 뿐입니다. 즉, 코틀린으로 된 코드를 코틀린에서 사용할 때는 해당 코드가 interface에 단 하나의 추상 메소드를 가지고 있어도 SAM을 이용해 축약할 수 없습니다. 왜 그렇게 했는지는 모르겠다만 아무튼 그렇습니다.

**수정**

이 부분은 코틀린 1.4에서 업데이트 되어 이제 코틀린 to 코틀린에서도 SAM을 지원합니다. 자세한 건 포스팅 아래에.

 

 

따라서 다음과 같이 JAVA 파일을 만들었을 때

// MySuperAmazingButton.java
public class MySuperAmazingButton {

    public void setOnSuperAmazingClickListener(OnSuperAmazingClickListener listener) {
    }

    public interface OnSuperAmazingClickListener {
        void onSuperAmazingClick();
    }

}

이렇게 setOnSuperAmazingClickListener에 OnSuperAmazingClickListener 파라미터 1개만 들어가고, 그 리스너가 interface이고 1개의 추상 메소드를 가진다면 우리가 지금까지 본 자바의 setOnClickListener와 똑같으므로 코틀린에서 SAM이 잘 됩니다.

 

하지만 만약 똑같은 코드를 자바가 아니라 코틀린에서 썼다면

// MySuperAmazingButton.kt
class MySuperAmazingButton {
    
    fun setOnSuperAmazingClickListener(listener: OnSuperAmazingClickListener?) {}
    
    interface OnSuperAmazingClickListener {
        fun onSuperAmazingClick()
    }
    
}

분명히 똑같은 구조임에도 불구하고

오류를 뿜어내게 됩니다.

 

 

 


글 내용 수정

 

코틀린 1.4부터는 자바코드 뿐만 아니라 코틀린 코드에 대해서도 SAM Conversion이 가능하다고 합니다.

구현하는데 제한 사항이 있어서 개발하는데 시간이 좀 걸렸다고 하네요.

대신 interface가 아니라 fun interface로 해줘야합니다.

 

따라서 위의 코드를

// MySuperAmazingButton.kt
class MySuperAmazingButton {
    
    fun setOnSuperAmazingClickListener(listener: OnSuperAmazingClickListener?) {}
    
    fun interface OnSuperAmazingClickListener {
        fun onSuperAmazingClick()
    }
    
}

이렇게 했다면

이 코드가 오류 없이 사용되어집니다.

옛날 사진 재활용해서 그렇지 저 빨간 밑줄도 없이 잘 됩니다.

Comments