개발을 하다보면 어떤 데이터의 값이 변경되었는지 알고 싶을 때가 있습니다. 예를 들어, 버튼이 눌려 어떤 옵션이 변경되었을 때 그 옵션에 맞게 다른 처리를 해야 할 때가 있습니다. 이럴 때 적합한 것이 옵저버 패턴이고, 코틀린의 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가 생성한 클래스를 타입으로 갖고 있습니다. 이 클래스 내부에 정의한 변수를 갖고 있으며 이 변수가 변경될 때 구현된 함수를 콜백해줍니다.
참고
Related Posts
- Kotlin - 배열에서 최소 값, 최대 값 찾기
- Kotlin - 2차원 배열 선언, 초기화 방법
- Kotlin - 배열 선언, 초기화 방법
- Kotlin - 리스트, 배열 길이 가져오기
- Kotlin - 리스트에서 최대, 최소 값 찾기
- Kotlin - for 반복문, 배열/리스트 순회
- Kotlin - Timer, 주기적으로 함수 실행
- Kotlin - sleep, 쓰레드 몇 초 지연
- Kotlin - Thread 생성 및 실행
- Kotlin에서 정규표현식 사용하기
- Kotlin - 문자열 길이 계산
- Kotlin - 문자열 비교 방법(equals, ==, compareTo)
- Kotlin - 2개의 배열 하나로 합치기
- Kotlin - 2개의 List 하나로 합치기
- Kotlin - 디렉토리의 모든 파일 리스트 출력
- Kotlin - 리스트 정렬 방법 (sort, sortBy, sortWith)
- Kotlin - 문자열 뒤집기 (Reverse String)
- Kotlin - 랜덤 숫자 생성 (Random, SecureRandom)
- Kotlin - Range, 숫자 범위 표현
- Kotlin - 음수를 양수로 변환, math.abs()
- Kotlin - List를 Set로 변환
- Kotlin - Set를 List로 변환
- Kotlin - 문자열에서 숫자(int)만 추출하는 방법
- Kotlin - Map을 List로 변환하는 방법
- Kotlin - File, Directory가 존재하는지 확인
- Kotlin - List를 Map으로 변환
- Kotlin - List의 중복 요소 제거
- Kotlin - List를 Array로 변환
- Kotlin - 엘비스 연산자 (Elvis Operation)
- Kotlin - Array를 List로 변환
- Kotlin - String을 Float으로 변환
- Kotlin - String을 Double으로 변환
- Kotlin - String을 Int로 변환
- Kotlin - String을 Long으로 변환
- Kotlin - String Null 또는 Empty 체크