[android] eventAPI vs Runnable Callback

최근 새로운 프레임웍을 제작하면서 최신 API를 되돌아볼 기회가 생겼습니다. 그러면서 나름 중요한 변화를 감지했는데, 전통적인 이벤트 모델의 변화입니다.

기존의 이벤트 API

기존(젤리빈이전)의 API는 대부분 다음과 같은 형태로 이벤트를 지정하는 시그니처를 갖게 되어있습니다.

void setOnXXXListener( XXXListener )

이러한 이벤트 모델은 장단점을 갖는데, 원래부터 안드로이드 이벤트는 다음과 같은 이유로 콜백모델에 비해 큰 장점이 없었습니다.

  1. 보통 이벤트 시스템은 리스너를 다수 등록하고 관리할 수 있는 옵져버모델을 따르고 있으나 안드로이드 이벤트 대부분은 하나의 리스너만 받는다.
  2. 등록된 이벤트리스너를 get할 수 있는 방법이 없다.
  3. UI와 관련된 이벤트가 자동으로 Handler와 연동되지 않아 리스너가 다시 Handler를 호출하여 메인UI쓰레드와 통신해야하는 책임을 진다.

하지만 이러한 편의(?)적인 단점 외에도 큰 단점이 하나 더 존재하는데, 너무 많은 이벤트클래스가 존재한다는 점입니다. 각각의 리스너가 서로 다른 타입의 이벤트객체를 받아들이므로 각 상황에 맞는 이벤트 클래스를 정의해야 한다는 것이죠.

반대로 이러한 상황별 이벤트 클래스가 존재하므로 각 상황에 맞는 다양한 필드값을 채워서 리스너에게 보내줄 수 있다는 점은 장점이라 할 수 있습니다. 쨌든 이러한 API는 이벤트시스템으로는 불완전하므로 본격적인 옵져버패턴을 이용하는 이벤트는 결국 Window.Callback 이라는 다른 시스템에 의존하게 됩니다.

하지만 리스너가 그저 상황이 발생했다는 점만 인지하고 딱히 이벤트 객체 따위를 받지 않아도 괜찮은 경우가 얼마든지 발생할 수 있습니다.

Runnable을 이용하기

사실 이것은 새로운 것이라 보기는 힘듭니다. 왜냐면 초창기부터 Handler에는 post메서드가 존재하는데 이 시그니처는 다음과 같습니다.

public final boolean post (Runnable r)

결국 메인UI쓰레드에서 실행되기만 하면 되지 딱히 다른 참조객체가 필요없으므로 콜백할 인터페이스만 있으면 되기 때문에 Runnable을 받는 형태가 되어있습니다. 사실 핸들러는 쓰레드와 큐가 연결되어있는 형태지만 딱히 이 시점에 Runnable을 써야하는 이유가 쓰레드와 관련되어있지는 않습니다.

단지 java코어수준에서 제공하는 범용적인 API를 활용하면서 쓰레드환경내라는 암시도 줄 수 있으니 괜찮은 선택이었다고 생각합니다. 그래서 이 때는 별 생각없이 그런가보다하고 넘어갔습니다.

근데 애니메이션 레이어를 만들면서 ViewPropertyAnimator 를 다시 보고 있자니 재밌는 점을 발견했습니다.

젤리빈 이후부터 지원되는 withStartAction withEndAction 이란 메서드를 보면 Runnable을 받고 있습니다. 이 메서드는 애니메이션이 시작되거나 끝나면 호출되는 콜백을 지정하게 되어있는데 여기에 Runnable을 사용한 것입니다.

public ViewPropertyAnimator withStartAction (Runnable runnable)

만약 이전의 개념이었다면 이것은 아마도 다음과 같은 API가 되어야 합당했을 것입니다.

public ViewPropertyAnimator setStartListener (ValueAnimator.AnimatorStartListener listener)

public ViewPropertyAnimator setEndListener (ValueAnimator.AnimatorEndListener listener)

이게 바로 기존의 setXXXListener 형태이자 XXXListener 전용 클래스 체제입니다. 하지만 젤리빈에 추가된 withXXXAction은 그러한 형태를 취하지 않았습니다. 여기엔 기존 API에 비해서 단점도 존재합니다.

Runnable로 리스너를 받기 때문에 인자에 아무것도 전달할 수 없다는 것입니다. 근데 그럼에도 불구하고 상관없다라고 생각한건지 Runnable을 취하고 있습니다.

실제 코드로 환원하면 어떻게 될까요?

View view = findById( R.id.test );

ViewPropertyAnimator  ani = v.withEndAction( new Runnable(){
    public void run(){
        view.setVisibility( View.GONE );
        Log.i( ani.getDuration() );
    }
} );

즉 run안에 필요한 객체는 이미 컨텍스트를 통해 view, ani등 지역변수로  참조할 수 있는 상황이므로 구지 리스너로 인자를 받지 않아도 된다고 할 수 있습니다. 젤리빈 이전의 인터페이스라면 다음과 같이 작성되었겠죠.

View view = findById( R.id.test );

ViewPropertyAnimator  ani = v.setListener( new Animator.AnimatorListener(){
    public void onAnimationEnd( Animator ani ){
        view.setVisibility( View.GONE );
        Log.i( ani.getDuration() );
    }
});

결국 리스너 코드가 같으니

  1. 전용클래스보다는 범용클래스로 (  Animator.AnimatorListener → Runnable )
  2. 범용등록메서드보다는 전용등록메서드로 ( setListener → withStartAction, withEndAction )

이전시킨 게 아닐까 싶습니다(할라면 withUpdateAction도 만들란 말이지!)

결론

이벤트 시스템을 범용클래스 콜백으로 바꾸는 것은 작업하는 입장에서는 환영할 일입니다. 짜피 다중 리스너를 처리해주는 것도 아니었고 투명한 옵져버모델도 아니었습니다. 단지 바라는게 있다면 withXXXAction이라는 이상한 규칙의 이름을 또 만들어내기보다는 callBackXXX 처럼 좀 알기 쉽게 작동을 설명하는 API이름도 괜찮았을텐데 하는 정도입니다. SDK를 포괄하는 이벤트 리스너를 만들어보는걸로 마무리하죠.

void withStart( ViewPropertyAnimator  ani, Runnable action ){

    if( Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN ){

        ani.setListener( new Animator.AnimatorListener(){
            public void onAnimationEnd( Animator ani ){
                action.run();
            }
        });

    }else ani.withStartAction( action );
}

//host
View view = findById( R.id.test );

withStart( view.animate(), new Runnable(){
    public void run(){
        view.setVisibility( View.VISIBLE);
    }
);