Kotlin Delegate관례에 대하여

SSY·2023년 2월 14일
0

Kotlin

목록 보기
7/8
post-thumbnail

1. 시작하며

보통 viewModel객체를 만들땐 'ViewModelProvider'객체를 사용하여 만든다.

하지만 위 코드를 보면 알다시피 파라미터에 넘겨줘야할 인자가 너무 많을 뿐만 아니라, ViewModel객체가 많아지면 많아질수록 위와 같은 상용구 코드가 늘어난다.

또한 ViewModelProvider객체는 지연초기화를 지원해주지 않아 메모리가 불필요하게 점유되는 일이 많아진다. (사실, 요즘 모바일 또한 성능이 좋아져 이정도는 괜찮을지도 모른다.) 하지만 그럼에도 불구하고, 프로그램이 커지면서 메모리를 아끼려는 행위는 더 좋은 프로그램이 된다는 사실엔 변함이 없다.

코틀린에서 제공하는 Delegate관례는 3 가지 이점을 제공해준다.

[Delegate패턴 사용시 얻을 수 있는 이점]
1. 불필요한 상용구를 줄일 수 있다.
2. 지연초기화를 사용함으로써 불필요한 메모리 점유를 줄일 수 있다.
3. get과 set을 커스텀함으로써 원하는 객체를 핸들링할 수 있다.

해당 포스팅의 전개방식은 Google에서 제공해주는 viewModels레퍼런스를 분석하고, 그에 맞춰 필자가 소스코드를 리팩토링한 사례를 소개하려 한다.

2. Google API의 viewModels를 사용한 Delegate패턴?

viewModels를 초기화할땐 보통 아래와 같이 한다.

val myViewModel: ViewModel by viewModels()

여기서 우린 'by'키워드에 주목할 필요가 있다.

결론부터 말하자면 위 by는 키워드가 아니다. Kotlin에서 기본으로 제공해주는 operator관례 함수이다. 어떤 클래스든, setValue, getValue를 오버라이딩하면 위 by키워드를 사용하여 Delegate패턴을 사용할 수 있다.

이 글을 읽는 분들은 'var''val'의 차이정돈 알거라 생각한다. var는 varable즉, 변할 수 있는 변수이고, val는 value로 변하지 않는 상수를 의미한다.

우리가 by함수를 사용하여 Delegate를 구현하려 할때, 위 두 메서드가 오버라이딩되는 방식이 다르다. 만약 우리가 변수를 var로 선언했다면? 당연히 데이터를 '읽고 쓸 수'있다. val로 선언했다면? 당연히 데이터를 '읽을 수만'있다.

Delegate패턴을 구현하는 방식은 크게 두 가지가 있다. 하나는 [class]를 사용한 방식, 다른 하나는 [function]을 사용하는 방식이다. 필자가 첫 번째 레퍼런스로 보여준, viewModels를 통한 Delegate는 당연히 getValue만을 사용하는 방식이다. 그럼 viewModels()함수의 Delegate 관례 함수를 알아보자.

[Step1. viewModels타고 들어가기]

[Step2. ViewModelLazy타고 들어가기]

최종적으로 ViewModelLazy클래스를 타고 들어간 결과 get함수만 오버라이딩되어 있는걸 볼 수 있다. 하지만 여기서 의문이 들 수 있다.

"어...? get만 하는건 맞는데... getValue함수가 아니잖아"

좀 더 자세히 알기 위해 ViewModelLazy함수 내부를 타고 들어가볼 필요가 있다.

ViewModelLazy함수는 위 코드에서 보다시피 interface이다. 즉, 위 interface를 구현한 실제 객체를 통해 지연초기화를 진행한다는걸 알 수 있다. 그리고 해당 파일에서 스크롤을 조금만 밑으로 내려보자.

이는 아까 필자가 말했던 getValue함수가 선언되어 있는걸 볼 수 있다.

[정리]
ViewModelLazy함수는 lazy인터페이스를 구현한 객체이다. 그리고 해당 객체는 getValue메서드를 사용하여 Delegate패턴을 구현할 수 있는걸 알 수 있다.

3. 필자의 리팩토링 사례

필자는 제3자가 진행하던 프로젝트를 맡아 진행하고 있다. 하지만 불필요한 중복코드가 너무 많다. 진행시켜야만 하는 코드다

[View단에서 호출하는 코드]

[PreferenceManager단에서 호출하는 코드]

[소스코드 간단 설명]
첫 번째 사진은 View에서 PreferenceManager를 호출하는 코드다. 그리고 PreferenceManager에서는 내부 함수를 두번 더 호출하고 있다.이런 느낌의 코드가 4배정도 되어 있다. 아마 일정 압박을 크게 느낀 나머지 불필요한 중복코드를 추가할 수밖에 없던 마음이라 이해한다.

위와 같은 상용구를 줄이기 위해 필자가 사용한 방법은 바로, 'Delegate패턴을 사용한 프로퍼티의 초기화'이다.

[View단에서 호출하는 코드]

[sharePreference위임 객체 내부]

필자도 그렇게 잘 짠코드라고는 할 수 없다. 하지만 중요한 점은 1. 위임 패턴을 사용해 불필요 상용구를 제거한 점2. 지연 초기화를 가능하게 하여 불필요한 메모리 점유를 제거했다는 점이다.

4. class를 사용한 Delegate, Function을 사용한 Delegate?

필자가 작성한 코드를 클래스 버전으로 변경해봤다. 그럴 경우 아래와 같은 형태를 띈다.

inner class PreferenceDelegate<T: Any>(
    private val key: String,
    private val defaultValue: T
) {
    private val preferences: SharedPreferences by lazy {
        context.getSharedPreferences("ISIGNPASS_SETTINGS", Context.MODE_PRIVAT
    }
    operator fun getValue(
        sharedPreferencesManager: SharedPreferencesManager,
        property: KProperty<*>
    ): T {
        return when (defaultValue) {
            is Boolean -> preferences.getBoolean(key, defaultValue)
            is String -> preferences.getString(key, defaultValue)
            is Int -> preferences.getInt(key, defaultValue)
            is Long -> preferences.getLong(key, defaultValue)
            is Float -> preferences.getFloat(key, defaultValue)
            else -> throw IllegalArgumentException("no define preference type"
        } as T
    }
    operator fun setValue(
        sharedPreferencesManager: SharedPreferencesManager,
        property: KProperty<*>,
        value: T
    ) {
        when (value) {
            is Boolean -> preferences.edit().putBoolean(key, value).apply()
            is String -> preferences.edit().putString(key, value).apply()
            is Int -> preferences.edit().putInt(key, value).apply()
            is Long -> preferences.edit().putLong(key, value).apply()
            is Float -> preferences.edit().putFloat(key, value).apply()
            else -> throw IllegalArgumentException("no define preference type"
        }
    }
}

차이는 아주 간단하다.

[Class를 사용한 Delegate패턴] ->
var -> setValue, getValue오버라이딩
val -> getValue 오버라이딩

[Function을 사용한 Delegate패턴]
var -> ReadWriteProperty 무명 객체 생성 및 오버라이딩
val -> ReadOnlyProperty 무명 객체 생성 및 오버라이딩

끝이다. 해당 내용은 [Kotlin 공식 홈페이지]에서도 동일하게 설명하고 있는걸 알 수 있다.

5. 마치며

Delegate패턴 사용시 이점을 다시 정리해본다.

Delegate패턴 사용시 얻을 수 있는 이점
1. 불필요한 상용구를 줄일 수 있다.
2. 지연초기화를 사용함으로써 불필요한 메모리 점유를 줄일 수 있다.
3. get과 set을 커스텀함으로써 원하는 객체를 핸들링할 수 있다.

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글