Backing Properties 분석하기
1.Java의 필드
필드(field): 클래스의 속성값을 저장하기 위해 선언하는 변수들을 필드라고 합니다. 필드는 다른 말로 멤버변수나 전역변수라고 합니다.
필드는 클래스 안에서 선언 위치에 따라 3가지로 구분됩니다.
지역 변수 (local variable)
인스턴스 변수 (instance variable)
클래스 변수 (class variable)
class class_name {
static data_type variable_name; // 클래스 변수
data_type variable_name; // 인스턴스 변수
data_type method_name() {
data_type variable_name; // 지역 변수
}
}
지역 변수(메서드나 생성자 내부 선언)
메서드 안에 선언된 변수를 의미합니다. 메서드가 호출될 때 생성되고 메서드가 종료될 때 삭제됩니다. stack 메모리에 저장되며 접근 지정자를 사용할 수 없습니다. 기본적으로 변수가 존재하는 블록에서만 사용할 수 있기 때문에 블록변수라고도 합니다. 반드시 사용하기 전에 초기화해야합니다.
(멤버 변수 : 인스턴스 변수, 클래스 변수 -> 선언 위치가 클래스 영역)
인스턴스 변수
메서드 밖에서 선언된 변수 중 static 키워드를 사용하지 않고 선언된 변수입니다. 인스턴스(객체)가 생성될 때 생성되며 객체가 삭제될 때 삭제됩니다. 인스턴스 별로 다른 값을 가질 수 있으므로, 각각의 인스턴스마다 고유의 값을 가져야할 때는 인스턴스 변수로 선언합니다. heap 메모리에 저장되며 각 객체의 정보를 저장하는데 사용되어 멤버 변수라고도 합니다.
클래스 변수
클래스 변수는 메서드 밖에서 선언된 변수 중 static 키워드를 사용하여 선언한 변수입니다. 프로그램이 실행될 때 생성되고 프로그램이 종료될 때 삭제됩니다. 단 한 번만 생성되고 객체 생성 없이 클래스명.변수명으로 접근할 수 있습니다. 메서드 영역에 할당되고 객체(인스턴스) 간에 공유되기 때문에 공유변수라고도 합니다.
2. Kotlin 프로퍼티와 필드
프로퍼티는 일반 변수처럼 보이지만 함수가 내장된 변수입니다. 접근자(Accessor)로 불리는 함수가 내장되어 있습니다. 코틀린에서는 필드 뿐만아니라 접근자(Accessor) 메서드도 자동으로 생성해줍니다. property는 사용은 필드처럼 하지만, 호출하게 되면 함수처럼 호출됩니다.
(프로퍼티 문법)
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
initializer, getter, setter 는 optional 입니다. initializer 로부터 타입을 추론하는 것이 가능하다면 프로퍼티의 타입을 생략하는것이 가능합니다.
val : 읽기전용으로 사용
var : 읽기와 수정이 사용
(Backing Field)
var counter = 0 // the initializer assigns the backing field directly
get() = field
set(value) {
if (value >= 0)
field = value
// counter = value // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
}
프로퍼티는 get(), set() 함수를 가지고 있고 프로퍼티가 가진 값은 field에 저장되어 있습니다. get(),set()으로 데이터를 읽기 및 수정을 할 수 있습니다.
클래스 내의 프로퍼티의 getter, setter 역할을 하는 get(), set() 함수가 접근하는 field(필드)는 프로퍼티에 저장된 값 자체를 지칭하는 예약어 입니다. 이 필드라는 개념은 get(), set() 에서만 사용 가능합니다.
(프로퍼티에 대한 자세한 내용은 추후에 따로 다루겠습니다.)
HomeMainViewModel.kt
private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState> = _marketData
private val _itemData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val itemData: LiveData<HomeMainState> = _itemData
위 코드에서 MutableLiveData와 LiveData를 사용한 것을 알 수 있습니다. MutableLiveData의 경우 var과 같이 읽기 및 수정이 가능합니다. LiveData의 경우 val과 같이 읽기만 가능합니다.
그래서 MutableLiveData의 경우 private으로 캡슐화되어 _marketData와 _itemData로 선언되어 HomeMainState의 value를 ViewModel 내부에서 읽고 수정하게 할 수 있습니다.
그리고 _marketData value는 marketData 프로퍼티의 Backing Properties(지원 속성)로 get() = _marketData / val marketData: LiveData = _marketData 와 같은 형태로 _marketData 프로퍼티의 값을 반환 하게 됩니다.
(Backing Properties 공식문서 발췌)
If you want to do something that does not fit into this “implicit backing field” scheme, you can always fall back to having a backing property:
코틀린 공식문서에 의거하여 위와 같이 암시적 지원 필드에 맞지 않는 경우 우리는 Backing Properties로 Property를 용도에 맞게 생성하여 LiveData의 back field를 커스텀하여 쓸 수 있음을 알 수 있습니다.
그리고 LiveData의 경우는 private가 아니고 marketData와 itemData로 선언되어 ViewModel 외부 클래스에서 value를 읽을 수 있고, 수정하는 것을 막을 수 있게 됩니다.
(LiveData에 대한 자세한 내용은 다른 글에서 다루겠습니다.)
marketData와 itemData는 각각 근처 마켓 정보와 새로운 할인 상품을 나타낼 LiveData 입니다. 위의 설명과 같이 Backing Properties로 구현하여 LiveData를 필요한 곳에서 observe(읽을 수 있게)하면서 value의 수정은 ViewModel에서만 가능하게 됩니다.
(LiveData를 observe)
//HomeMainFragment.kt
marketData.observe(viewLifecycleOwner) {
when (it) {
...
is HomeMainState.Success<*> -> {
nearbyMarketAdapter.submitList(it.modelList)
}
...
}
}
HomeMainFragment에서 marketData를 observe하고 있다가 Success 상태가 되면 성공적으로 불러온 데이터를 nearbyMarketAdapter에 리스트를 submitList(it.modelList)을 이용하여 업데이트 하여 RecyclerView에 정보를 출력하게 됩니다.
(ViewModel에서 value의 변경)
//HomeMainViewModel.kt
private suspend fun fetchMarketData() {
if (marketData.value !is HomeMainState.Success<*>) {
_marketData.value = HomeMainState.Loading
// 거리가 가까운 순으로 정렬
// 임시로 ViewModel에서 type을 HOME_CELL로 변경
_marketData.value = HomeMainState.Success(
modelList = homeRepository.getAllMarketList().map {
it.copy(type = CellType.HOME_CELL)
}.sortedBy { it.distance }
)
}
}
homeRepsitory에서 근처 마켓의 정보를 가져와서 거리가 가까운 순으로 정렬하는 method 입니다. Success가 아닐 때만 정보를 가져오도록 하여 화면 이동과 같은 상황에서 불필요하게 계속 정보를 불러오는 것을 방지했습니다.
(Backing Properties 구현 방법)
(1) private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState> = _marketData
(2) private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState>
get() = _marketData
Backing Properties는 이와 같이 2가지 방법으로 구현을 할 수 있습니다. Backing field의 생성 유무의 차이점이 존재합니다. Kotlin은 backing field를 필요할 때만 생성하게 됩니다.
1. Setter를 이용한 값 설정
private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState> = _marketData
위와 같은 경우 marketData의 backing field가 생성되어 _marketData의 reference를 저장하고 있습니다.
private final MutableLiveData _marketData;
@NotNull
private final LiveData marketData;
...
@NotNull
public final LiveData getMarketData() {
return this.marketData;
}
위 Kotlin의 코드를 Java로 decompile해보면 marketData의 field가 생성되어 있는 것을 확인할 수 있습니다.
2. Custom getter를 이용한 값 설정
private val _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState>
get() = _marketData
하지만 getter만 설정해주게되면 backing field가 생성되지 않고 getMarketData()만 생성됩니다.
private final MutableLiveData _marketData;
...
@NotNull
public final LiveData getMarketData() {
return (LiveData)this._marketData;
}
getMarketData()에서 _marketData를 LiveData로 변환하여 가져오는 것을 볼 수 있습니다.
3. 예제 코드로 확인하기.
(1) private var _marketData = MutableLiveData<HomeMainState>(HomeMainState.Uninitialized)
val marketData: LiveData<HomeMainState> = _marketData
위의 예시와 달리 _marketData이 val이 아니라 var이고 재생성 되었다고 가정해보겠습니다. LiveData로 이미 생성된 객체는 동일한 이름으로 재생성이 불가능 함으로 다른 예로서 설명하겠습니다.
class Backing_Properties {
data class LiveData(var data: String)
companion object {
var _marketData = LiveData("첫번째 데이터")
val marketData1 = _marketData
val marketData2
get() = _marketData
fun start() {
println("marketData1: ${marketData1}")
println("marketData2: ${marketData2}")
println("_marketData를 재생성")
_marketData = LiveData("두번째 데이터")
println("marketData1: ${marketData1}")
println("marketData2: ${marketData2}")
}
}
}
(컴파일 결과)
marketData1: LiveData(data=첫번째 데이터)
marketData2: LiveData(data=첫번째 데이터)
_marketData를 재생성
marketData1: LiveData(data=첫번째 데이터)
marketData2: LiveData(data=두번째 데이터)
Setter를 이용한 값 설정방식에서는 초기화 당시의 _marketData을 가지고 있기 때문에 재생성된 프로퍼티는 얻을 수 없습니다.
그리고 Custom getter를 이용한 값 설정방식에서는 marketData을 가져올 때 _marketData 자체를 가져오기 때문에 재생성된 프로퍼티를 얻을 수 있습니다.
결과론 적으로 우리는 LiveData 프로퍼티를 재생성하지는 않기 때문에 get을 쓰나 setter을 이용하나 크게 차이가 없음을 알 수 있습니다.
이로써 우리는 Kotlin Property에 대한 기본적인 내용과 더불어 이 개념을 확장적으로 활용할 때 Backing Field를 확장한 backing properties의 방법에 대한 이해를 할 수 있었습니다.
다음 내용은 AAC ViewModel과 일반적인 ViewModel의 차이점에 대한 내용으로 돌아오겠습니다. 감사합니다.