본문 바로가기

Android/Tech

LiveData 를 선언하는 방법 (get Keyword)

LiveData 및 MutableLiveData 가 안드로이드 플랫폼에서의 MVVM Architecture 구현에 큰 역할을 해왔다는 사실에는 누구도 부정할 수 없습니다. 구현이 복잡하고 보일러 플레이트 코드가 많은 Observer Pattern 을 대체하여 간편하게 사용할 수 있기 때문입니다.

 

StateFlow 와 SharedFlow 가 추가되면서 그 입지가 많이 줄었지만, 그래도 여전히 수많은 안드로이드 앱에 사용되고 있습니다.


저는 개발 공부를 할 때 다른 개발자들의 코드를 많이 참고하는 편입니다.

그 개발자들을 선택하는 기준은 두 가지인데,

- 유명한 개발자

- 좋은 품질의 코드를 개발할 수 있는 환경에 있는 개발자 

가 그 것입니다.

 

그러한 개발자들의 여러 코드를 쭉 살펴보면, 보통 소속 집단이나 성향에 따라 코드가 천차만별입니다. 하지만 공통적으로 사용하는 패턴이나 비슷한 부분들이 분명히 있는데, 오늘은 LiveData 를 선언하는 방식에서의 공통점을 발견했고, 이에 대해 기록하고자 합니다.


잘못된 방식 먼저 소개하고, 옳은 방식을 설명해 보겠습니다.

잘못된 방식

class MainViewModel {
    val numData = MutableLiveData<Int>()
}

ViewModel 에 단순히 MutableLiveData() 만 구현해두는 방식입니다. 단 하나만 구현하기 때문에 가독성은 굉장히 높다고 할 수 있습니다. 깔끔하긴 하지만, 이렇게 선언을 해두면 Activity 에서 값을 변경할 수 있어, 문제 발생의 소지가 있습니다.

 

또한, 아래와 같은 방식으로 직접적인 접근이 가능하여 Elegant 하지 못한 느낌도 듭니다.

viewModel.numData.observe(this@MainActivity) {
	// todo
}

 

그런데, 이 방식을 디컴파일 하면 아래와 같습니다.

 

public final class MainViewModel {
   @NotNull
   private final MutableLiveData numData = new MutableLiveData();

   @NotNull
   public final MutableLiveData getNumData() {
      return this.numData;
   }
}

 

getter 메서드가 생성되었고, 객체에 private 을 선언하지 않았음에도 불구하고 자동으로 객체를 은닉시킵니다. 언어 차원에서 그렇게 해주지만, 그래도 우리는 명시적으로 private 을 선언해주는 게 좋긴 합니다.

 

아무튼, 코틀린으로 필드를 작성하면 getter 및 setter 를 만들어줍니다. (물론 위 예시는 var 이 아닌 val 이기 때문에 getter 만 생성) 그래서, viewModel.numData 와 같은 방식으로 접근하는 것은 사실 getter 메서드를 사용하는 것과 같습니다. 

 

즉,  위 코드는 아래 코드와 정확히 같습니다.

 

class MainViewModel {
    private val numData = MutableLiveData<Int>()

    fun getNumData() = numData
}

 

디컴파일 해봅시다.

 

public final class MainViewModel {
   private final MutableLiveData numData = new MutableLiveData();

   @NotNull
   public final MutableLiveData getNumData() {
      return this.numData;
   }
}

 


 

아무튼 위 방식에는 문제가 있습니다. View 에서 ViewModel 에 대한 직접적인 조작이 '가능'하다는 점입니다. "직접 조작 안 하면 되는 거 아니냐?" 할 수 있지만, 애초에 그러한 가능성을 제거하는 것이 중요합니다.

 

그렇다면, 그 가능성을 제거한 코드를 작성해 보겠습니다. 그럼 다음과 같을 것입니다.

 

class MainViewModel {
    private val _numData = MutableLiveData<Int>()
    val numData: LiveData<Int> = _numData
}

 

이렇게 작성하게 되면 _numData 는 private 으로 은닉되어 있고, numData 를 통해 외부에서 접근할 수 있기 때문에, 외부로부터 상태에 대한 관찰은 가능하지만 값에 대한 변경이 불가능합니다. 디컴파일 해봅시다.

 

public final class MainViewModel {
   private final MutableLiveData _numData = new MutableLiveData();
   @NotNull
   private final LiveData numData;

   @NotNull
   public final LiveData getNumData() {
      return this.numData;
   }

   public MainViewModel() {
      this.numData = (LiveData)this._numData;
   }
}

 

_numData 에 대한 은닉과 접근을 위해 numData 라는 새로운 객체가 생성됩니다. 이는 불필요한 메모리를 사용하는 것이기 때문에 효율이 좋다고 말할 수 없습니다.

 

이러한 경우를 위해 코틀린에서는 get() 키워드를 제공하고 있으며, 이를 통해 MutableLiveData 의 객체만 생성하고 접근 경로만 새롭게 구성할 수 있습니다.

[올바른 방식]

class MainViewModel {
    private val _numData = MutableLiveData<Int>()
    val numData: LiveData<Int>
        get() = _numData
}

 

프로퍼티는 처음과 같이 구현하지만 numData 의 타입만을 지정해주고 get() 키워드를 삽입하여 _numData 를 할당해 줍니다.

 

public final class MainViewModel {
   private final MutableLiveData _numData = new MutableLiveData();

   @NotNull
   public final LiveData getNumData() {
      return (LiveData)this._numData;
   }
}

 

바이트 코드로 변환하면 이렇게 됩니다. 더 없이 깔끔하고 효율적입니다. _numData 는 private 을 통해 외부로부터의 직접적인 접근이 불가능하게 하고, 이를 LiveData 타입으로 전달받을 수 있도록 합니다.

 


get() = 

get() { }

 

get 키워드는 두 가지 방식을 지원합니다.

첫째로는 이때까지 기술했던 것과 같이 단순한 할당입니다.

둘째가 특이한데, 블럭에 특정한 액션이 가능합니다. map{} 과 같은 기능을 한다고 볼 수 있습니다.

예시는 다음과 같습니다.

 

class MainViewModel {
    private val justInt = 5
    val sumInt: Int
    get() {
        return justInt + 5
    }
    
    init {
    	print(sumInt)
    }
}

 

 


 

최근 코틀린 학습을 시작했는데, 굉장히 매력적인 언어인 것 같습니다.