HOME > kotlin > basic

Kotlin - Delegates로 프로퍼티를 Observerable로 만들기

By JS|15 Jun 2019

개발을 하다보면 어떤 데이터의 값이 변경되었는지 알고 싶을 때가 있습니다. 예를 들어, 버튼이 눌려 어떤 옵션이 변경되었을 때 그 옵션에 맞게 다른 처리를 해야 할 때가 있습니다. 이럴 때 적합한 것이 옵저버 패턴이고, 코틀린의 Delegates는 프로퍼티를 Observerable로 쉽게 만들어주는 기능을 제공합니다.

이 글에서는 Delegates를 이용하여 프로퍼티를 Observerable로 만드는 방법에 대해서 알아봅니다. 그래서 프로퍼티의 값이 변경될 때마다 Callback을 받아 다른 처리를 하는 코드를 구현해볼 것입니다.

Delegates.observable()로 프로퍼티를 observable로 만들기

위에서 말씀드린 것처럼 Delegates는 프로퍼티를 Observerable로 쉽게 만들 수 있게 도와줍니다. Delegates는 observable()을 제공합니다. 이것을 이용하면 프로퍼티의 데이터가 변할 때마다 callback을 받을 수 있습니다.

Delegates.observable()의 함수 인자 및 리턴 타입은 다음과 같습니다. 프로퍼티의 값이 변경되면, 인자 onChange로 전달된 함수가 콜백됩니다

inline fun <T> observable(
    initialValue: T,
    crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
): ReadWriteProperty<Any?, T>

코틀린에서 Delegates.observable()은 아래처럼 사용할 수 있습니다.

var observed = false
var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
    println("Changing max to $newValue")
    observed = true
}

fun main() {
    println(max) // 0
    println("observed is ${observed}") // false
    max = 10
    println(max) // 10
    println("observed is ${observed}") // true
}

실행해보면 다음과 같이 결과를 출력합니다. max를 10으로 변경할 때 Delegates.observable에 구현한 코드가 호출되었고, 로그와 함께 observed = true로 설정하였습니다.

0
observed is false
Changing max to 10
10
observed is true

위에서 보신 것처럼 var max : Int로 프로퍼티를 선언할 때 by Delegates.observable(0) { ... }를 선언하면 Delegates가 프로퍼티를 Observable로 만듭니다. 그 이후 프로퍼티의 값이 변경될 때, Delegates는 { ... }의 코드를 Callback해줍니다. Callback과 함께 인자로 Property, 이전 값(oldValue), 새로운 값(newValue)이 전달됩니다.

이렇게 우리는 Delegates에 프로퍼티를 Observerable로 만드는 것을 위임할 수 있습니다. 코틀린 코드상에서 눈으로 보이지 않지만 Delegates는 프로퍼티를 Observable로 만들어주는 코드를 생성합니다. 자바로 변환해서 보면 어떤 코드가 생성되는지 명확히 알 수 있습니다.

Delegates.observable의 동작 원리

아래는 위의 코드를 자바로 디컴파일한 코드입니다. 가장 중요한 포인트는 Delegates가 Test7Kt$$special$$inlined$observable$1라는 클래스를 만들어 냈다는 것입니다. 이 클래스는 ObservableProperty를 상속하고 있습니다.

public final class Test7Kt {
  static {
     Delegates var0 = Delegates.INSTANCE;
     Object initialValue$iv = 0;
     int $i$f$observable = false;
     max$delegate = (ReadWriteProperty)(new Test7Kt$$special$$inlined$observable$1(initialValue$iv, initialValue$iv));
  }

  public static final boolean getObserved() {
     return observed;
  }

  public static final void setObserved(boolean var0) {
     observed = var0;
  }

  public static final int getMax() {
     return ((Number)max$delegate.getValue((Object)null, $$delegatedProperties[0])).intValue();
  }

  public static final void setMax(int var0) {
     max$delegate.setValue((Object)null, $$delegatedProperties[0], var0);
  }
}

public final class Test7Kt$$special$$inlined$observable$1 extends ObservableProperty {
   final Object $initialValue;

   public Test7Kt$$special$$inlined$observable$1(Object $captured_local_variable$1, Object $super_call_param$2) {
      super($super_call_param$2);
      this.$initialValue = $captured_local_variable$1;
   }

   protected void afterChange(@NotNull KProperty property, Object oldValue, Object newValue) {
      int newValue = ((Number)newValue).intValue();
      int oldValue = ((Number)oldValue).intValue();
      String var8 = "Changing max to " + newValue;
      System.out.println(var8);
      Test7Kt.setObserved(true);
   }
}

public static final void main() {
   int var0 = getMax();
   System.out.println(var0);
   String var2 = "observed is " + observed;
   System.out.println(var2);
   setMax(10);
   var0 = getMax();
   System.out.println(var0);
   var2 = "observed is " + observed;
   System.out.println(var2);
}

자바 코드에서 변수 max$delegate는 Delegates가 생성한 Observable 객체이며, 이 클래스의 내부 변수가 변경될 때 afterChange를 호출하도록 구현되어있습니다. 자바의 afterChange()를 보면 코틀린에서 콜백에 구현한 코드들이 있습니다.

정리하면, var max : Int by Delegates.observable()는 내부적으로 max를 Observable로 만들어줍니다. 구현된 코드를 보면 max는 Int 타입이 아니고, Observable 클래스가 Int 타입을 wrapping하고 있는 구조입니다.

클래스에서 사용하는 Delegates.observable

위의 예제는 파일의 Top level의 프로퍼티에서 Delegates.observable을 사용했습니다. Class 내에서도 동일하게 사용할 수 있습니다. 아래는 클래스에서 프로퍼티를 Observable로 만든 코드입니다.

class MyObservable {
    var value: Int by Delegates.observable(0) { property, oldNew, newVal ->
        onValueChanged()
    }
    private fun onValueChanged() {
        println("value has changed:$value")
    }
}

fun main() {
    val observable = MyObservable()
    observable.value = 10
    observable.value = -20
}

Delegates는 Observable 프로퍼티를 갖고 있는 클래스의 Inner class로 Observable 클래스를 구현해줍니다. Delegates가 생성하는 코드들은 위와 동일하기 때문에 자바로 변환된 코드는 붙이지 않았습니다.

정리

Delegates.observable()을 사용하면 프로퍼티를 Observable로 만들 수 있습니다. 개발자가 구현해야 하는 내용들을 Delegates에 위임하여 boilerplate 코드를 작성하지 않아도 됩니다. Delegates는 Observable 코드를 자동 생성하는데, 코틀린에서는 보이지 않고 바이트코드나 자바코드로 변환해야 알 수 있습니다. 실제로 프로퍼티는 코틀린에서 정의한 타입이 아니며, Delegates가 생성한 클래스를 타입으로 갖고 있습니다. 이 클래스 내부에 정의한 변수를 갖고 있으며 이 변수가 변경될 때 구현된 함수를 콜백해줍니다.

참고