이 글을 쓰게 된 계기는 두 가지이다.
먼저 by lazy 의 동작방식을 제대로 알아보고 싶다는 것이다.
그리고 다른 글에서 코틀린 연산자 관례에 대해 소개하고 여러 관례를 정리하였는데 (링크)
getValue 와 setValue 메서드를 정리하지 않아서 찝찝했다. (invoke 는 여기서 마지막에 추가 정리했다..)
그래서 추가로 정리하고 위임 프로퍼티에 대해 알아보려고한다.
(이해한대로 쓰기 때문에 읽다가 틀린 부분이 있다면 댓글로 말씀해주시면 매우 감사입니다.)
그럼 다시 리마인드 해야할 것 들을 알아보자.
배경지식 리마인드
관례?
코틀린에서 관례라고 하는 것은 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법이다.
예를 들면 코틀린에서 흔하게 사용하던 == 로 값을 비교하는 기능은 equals 호출로 컴파일 된다.
아래 형식처럼 말이다.
a == b 는 a?.equals(b) ?: (b == null)
이외에도 산술 연산(+, -, *, / , ++, !, ..), 순서 연산(<, >, <=, >=), 인덱스 연산([]), 범위, 포함 등이 있었다.
정해진 함수 이름을 가진 함수 앞에 operator 키워드를 붙인다.
예를들면 아래와 같다.
operator fun equals(other: Any?): Boolean
추가로 이 글에서 프로퍼티 위임을 설명하면서 중점적으로 다뤄질 getValue 와 setValue 관례가 있다.
프로퍼티 접근자?
getter 와 setter 를 말한다.
일단 프로퍼티는 필드와 접근자를 묶어 말한다.
코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 대한 접근자 구현을 언어 기본 기능으로 제공한다.
또한 get, set 으로 커스텀 접근자를 만들 수 있고, 이는 접근할 때마다 호출된다.
위임?
위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다.
이때 도우미 객체를 위임 객체(delegate) 라고 말한다.
위임 프로퍼티는 무엇이고 왜 쓰나?
위임 프로퍼티를 사용하면 뒷받침 필드(프로퍼티의 값을 저장하기 위한 필드)에 단순히 저장하는 것보다
더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현하는 기능이다.
짧게 말하면 접근자 로직을 다른 객체에게 위임하는 기능이다.
그 과정에서 접근자 로직을 매번 재구현할 필요 없이 재활용할 수 있는 것이 장점이다.
또한 by 키워드를 사용하여 위임 객체를 지정하면 컴파일러가 위임 객체를 감춰진 프로퍼티에 저장하고 주 객체의 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue 와 setValue 를 호출해주므로, 접근자를 위임하기위한 보일러플레이트 코드를 줄일 수 있다.
즉 재사용성을 높이고 코드를 간결하게 작성할 수 있다고 생각한다.
이따가 예시를 보면 확 와닿을 것이다.
문법
형식은 아래와 같다.
val|var 프로퍼티이름 by 위임객체
by 위임객체에 집중하자.
by 뒤에 프로퍼티 위임 관례를 따르는 클래스의 인스턴스를 사용한다.
by 뒤에 함수나 식 등을 사용할 수도 있다. 그 함수나 식이 적절한(getValue, setValue 를 제공하는) 위임객체를 반환하면 된다.
즉, by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다고 이해하면 된다.
그러면 어떤 클래스의 인스턴스가 위임객체가 될 수 있는걸까?
프로퍼티 위임 관례를 따르는 클래스는 getValue 와 setValue 를 제공해야한다.
(setValue 는 변경 가능한 프로퍼티만 즉, setValue 까지 제공하는 위임객체라면 var 로 선언할 수 있을 것이다.)
그러면 컴파일러가 위임객체를 생성해주고, 프로퍼티 접근자 get, set 을 위임객체의 getValue 와 setValue 로 대체해준다..
저 문법을 따르는 프로퍼티는 접근자 로직을 다른 객체에게 위임한다고 표현할 수 있다.
getValue 와 setValue 는 어디서 나온 것인가? 했을 때 앞서 설명한 배경 중에 '관례' 를 다시 떠올리며 아래 리스트를 확인한다.
- 코틀린 관례 에서 사용하는 다른 함수와 마찬가지로 getValue 와 setValue 함수에도 operator 변경자가 붙음
- getValue 에는 프로퍼티가 포함된 객체와 프로퍼티를 표현하는 객체를 파라미터로 받는다. (*프로퍼티를 표현하는 객체는 KProperty 타입 객체를 말하는데 프로퍼티에 대한 정보를 담고있다. 자세한 설명은 주제에 벗어나므로 생략. )
- setValue 에는 프로퍼티가 포함된 객체와 프로퍼티를 표현하는 객체 그리고 새롭게 값을 변경해줄 객체를 파라미터로 받는다.
아래는 내가 대충 만들어본 예시이다. (당장 설명하는데만 무리 없을 정도로 많이 대충..)
data class Engine(val name: String, val old: Int)
class EngineDelegate(private val initEngine: Engine) {
private var _engine: Engine? = null
operator fun getValue(car: Car, property: KProperty<*>): Engine {
println("엔진 대신 가져오는 중...")
return _engine ?: run {
_engine = initEngine
initEngine
}
}
operator fun setValue(car: Car, property: KProperty<*>, newEngine: Engine) {
println("새로운 엔진으로 교체중...")
_engine = newEngine
}
}
class Car {
var engine: Engine by EngineDelegate(Engine("구형 엔진", 9))
}
fun main() {
Car().run {
println(engine)
println(engine)
engine = Engine("신형 엔진", 0)
println(engine)
}
}
위 예시의 출력값은 예상대로 아래와 같다.
엔진 대신 가져오는 중...
Engine(name=구형 엔진, old=9)
엔진 대신 가져오는 중...
Engine(name=구형 엔진, old=9)
새로운 엔진으로 교체중...
엔진 대신 가져오는 중...
Engine(name=신형 엔진, old=0)
위의 예시에서는 단순히 같은 engine 이라는 같은 타입에 저장했지만 저장 장소를 컬렉션으로 바꿀 수도 있고 데이터 베이스 테이블로도 바꿀 수 있고 print 문 대신 값 검증이나 변경 통지 등의 다양한 일을 해낼 수 있을 것이다.
그리고 코틀린을 사용하는 개발자는 그 다양한 일을 감추고 간결한 코드를 작성할 수 있다!
+ 참고로 IntelliJ 에서는 아래사진처럼 by 위임객체를 썼는데 getValue, setValue 뭐라도 없으면 에러 띄우고, 생성해달라고 클릭하면 자동 생성도 도와준다.
이렇게 말로만 하면 이게 무엇인가 헷갈리니 실제 예시를 찾아서 추가한다.
위임 프로퍼티 사용 예시
지연 초기화 lazy
getValue 관례를 이용해 위임 프로퍼티를 사용하는 대표적인 예시가, 코틀린 표준 라이브러리 함수인 lazy 이다.
잠깐만 lazy 가 무엇인지 짚고 넘어가면
lazy 는 지연초기화에 사용한다.
지연 초기화는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분이 필요한 경우 초기화할 때 흔히 쓰이는 패턴이라고 한다.
보통 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연초기화 패턴을 사용한다. 예를 들면 디비 데이터를 가져오는 일이다.
안드로이드 개발자는 by 뒤에 viewModels(), activityViewModels() 같은 확장함수를 사용한 기억이 있을 것이다.
이 때 반환하는 것이 Lazy 의 구현 ViewModelLazy 이다!
구현은 비공개로 두는 프로퍼티와 get 접근자를 제공해주는 프로퍼티를 따로두어 최초에만 가져오는 동작을 하고 이미 그 동작을 해서 값이 있다면 그 값을 반환하는 방식이다. 구현은 여기서 중요하지 않으니 생략한다.
당장 중요한 것은 lazy 도 결국 getValue 를 제공하는가이다. 찾아보니 아래와 같이 제공한다.
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
value 를 반환하는데 이는 Lazy 인터페이스의 프로퍼티이고 이를 구현하는 SynchronizedLazyImpl, SafePublicationLazyImpl, UnsafeLazyImpl 에 위에 말한 구현 내용이 있다.
Lazy 의 구현 클래스의 인스턴스가 위임객체였던 것이다!
_value 를 따로 두어 초기화 동작을 한번만 한다. 심지어 SynchronizedLazyImpl 는 동기화도 제공해준다.
아래는 SynchronizedLazyImpl 의 구현 일부이다.
value 에 대한 접근자 get() 은 위 getValue 가 호출될 때마다 실행될 것임을 리마인드한다.
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
...
}
위임 프로퍼티가 없었다면 지연초기화가 필요할 때마다 이 구현을 해야할지도 모른다..
아래는 by lazy 를 출력으로 실험해본 코드이다.
class LazyTest {
val test: String by lazy {
runBlocking {
println("초기화 로직...")
delay(1000L)
"값"
}
}
}
fun main() = runBlocking {
val lazyTest = LazyTest().test
repeat(3) {
println(lazyTest)
}
}
예상대로 아래와 같이 출력된다.
초기화 로직...
값
값
값
마무리
이렇게 관례 두 가지를 더 알아보며 위임 프로퍼티 기능에 대해 예를 들어 알아보았다.
하지만 의문인 점이 있을 것이다.
컴파일러는 어떻게 위임 프로퍼티를 구현했을지? -> 코틀린 인 액션 7.5.4 를 읽어보자.
KProperty 는 뭐지? -> 코틀린 인 액션 10장을 읽어보자.
언젠가 리플렉션 API 실전에서 잘 써먹는 시즌이 오면 정리해보려고 한다.
코틀린 인 액션에 Map, MutableMap 인터페이스에 대해 getValue, setValue 확장함수에 대해 간략히 소개하는데 이 링크에서 관련된 내용을 추가 확인해도 좋을 것 같다.
틀린 내용을 발견하셔서 댓글 써주시면 매우 감사합니다.
출처: Kotlin in Action, 구글링, Jetbrains/Kotlin 레포, 가끔 나의 뇌
'Kotlin' 카테고리의 다른 글
[Kotlin] 멤버참조 문법과 예시, 람다의 컴파일에 대해 알아보자! (0) | 2023.01.23 |
---|---|
[Kotlin] 코틀린의 연산자 관례 정리, 어떤 클래스든 연산시켜보자! (0) | 2023.01.08 |
[Kotlin] 흥미돋는 코틀린의 타입을 정리해보자 (0) | 2023.01.05 |
[Kotlin] 람다함수는 무엇이고 왜 쓰고 어떻게 쓸까? (1) | 2023.01.01 |
[Kotlin] Pair , Triple 과 Destructure (0) | 2021.08.05 |