Kotlin in Action: 7.4-7.5 구조 분해 선언과 component 함수, 프로퍼티 접근자 로직 재활용: 위임 프로퍼티
7.4 구조 분해 선언과 component 함수
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화 할 수 있다.
data class Point(val x: Int, val y: Int)
fun main(args: Array<String>) {
val p = Point(10, 20)
val (x, y) = p
println(x)
println(y)
}
구조 분해 선언은 = 의 좌변에 여러 변수를 괄호로 묶어 사용한다.
내부에서 구조 분해 선언은 다시 관례를 사용한다.
각 변수를 초기화하기 위해 componentsN 이라는 함수를 호출한다. 여기서 N 은 구조 분해 선언에 있는 변수 위치에 따라 붙는 번호다. data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentsN 함수를 만들어준다.
구조 분해 선언은 함수에서 여러 값을 반환할 때 유용하다. 여러 값을 한꺼번에 반환해야 하는 모든 값이 들어갈 데이터 클래스를 정의하고 함수의 반환 타입을 그 데이터 클래스로 바꾼다.
구조 분해 선언 구문을 사용하면 이런 함수가 반환하는 값을 쉽게 풀어서 여러 변수에 넣을 수 있다.
data class NameComponents(val name: String, // 값을 저장하기 위한 데이터 클래스를 선언한다.
val extension: String)
fun splitFilename(fullName: String): NameComponents {
val result = fullName.split('.', limit = 2)
return NameComponents(result[0], result[1]) // 함수에서 데이터 클래스의 인스턴스를 반환한다.
}
fun main(args: Array<String>) {
val (name, ext) = splitFilename("example.kt") // 구조 분해 선언 구문을 사용해 데이터 클래스를 푼다.
println(name)
println(ext)
}
물론 무한히 componentN 을 선언할 수는 없으므로 이런 구문을 무한정 사용할 수는 없다. 그럼에도 여전히 컬렉션에 대한 구조 분해는 유용하다.
코틀린 표준 라이브러리에서는 맨 앞의 다섯 원소에 대한 componentN 을 제공한다.
표준 라이브러리의 Pair 나 Triple 클래스를 사용하면 함수에서 여러 값을 더 간단하게 반환할 수 있다.
이 둘은 그 안에 담겨있는 원소의 의미를 말해주지는 않으므로 경우에 따라 가독성이 떨어질 수는 있지만 직접 클래스를 작성할 필요가 없으므로 코드는 더 단순해진다.
1. 구조 분해 선언과 루프
함수 본문 내의 선언문뿐 아니라 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용할 수 있다.
특히 맵의 원소에 대해 이터레이션할 때 구조 분해 선언이 유용하다.
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
fun main(args: Array<String>) {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
}
위의 예제는 두 가지 코틀린 관례를 활용한다. 하나는 객체를 이터레이션하는 관례고, 다른 하나는 구조 분해 선언이다.
코틀린 표준 라이브러리에는 맵에 대한 확장 함수로 iterator 가 들어있다. 그 iterator 는 맵 원소에 대한 이터레이터를 반환한다. 따라서 자바와 달리 코틀린에서는 맵을 직접 이터레이션할 수 있다.
또한 코틀린 라이브러리는 Map.Entry 에 대한 확장 함수로 component1 과 component2 를 제공한다.
7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티
위임 프로퍼티를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다. 또한 그 과정에서 접근자 로직을 매번 재구현할 필요도 없다.
예를 들어 프로퍼티는 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다.
위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체(위임 객체)가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다.
이 절에서는 그 패턴을 프로퍼티에 적용해서 접근자 기능을 도우미 객체가 수행하게 위임한다. 도우미 객체를 직접 작성할 수도 있지만 더 나은 방법은 코틀린 언어가 제공하는 기능을 활용하는 것이다.
1. 위임 프로퍼티 소개
위임 프로퍼티의 일반적인 문법은 다음과 같다.
class Foo {
var p: Type by Delegate()
}
이때 p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다. 여기서는 Delegate 클래스의 인스턴스를 위임 객체로 사용한다. by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다. 프로퍼티 위임 객체가 따라야하는 관례를 따르는 모든 객체를 위임에 사용할 수 있다.
class Foo {
private val delegate = Delegate () // 컴파일러가 생성한 도우미 프로퍼티다.
var p: Type // "p" 프로퍼티를 위해 컴파일러가 생성한 접근자는 "delegate"의 getValue와 setValue 메서드를 호출한다.
set (value: Type) = delegate. setValue (..., value)
get () = delegate.getValue (...)
}
위와 같이 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다. p프로퍼티는 바로 그 위임 객체에게 자신의 작업을 위임한다.
설명을 편하게 하기 위해 이 감춰진 프로퍼티 이름을 delegate라고 하자.
프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue와 setValue(변경 가능한 프로퍼티의 경우) 메서드를 제공해야 한다.
관례를 사용하는 다른 경우와 마찬가지로 getValue와 setValue는 멤버 메서드이거나 확장 함수일 수 있다. 두 메서드의 파라미터를 생략하고 Delegate 클래스를 단순화하면 아래와 같다.
class Delegate {
operator fun getValue (...) { ... } // gelValue는 게터를 구현하는 로직을 담는다.
operator fun setValue (..., value: Type) { ... } // selValue 메서드는 세터를 구현하는 로직을 담는다.
}
class Foo {
var p: Type by Delegate () // "by" 키워드는 프로퍼티와 위임 객체를 연결한다.
}
실제로 p의 게터나 세터는 Delegate타입의 위임 프로퍼티 객체에 있는 메서드를 호출한다.
2. 위임 프로퍼티 : by lazy()를 사용한 프로퍼티 초기화 지연
지연 초기화는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴이다.
초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때 마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.
class Person (val name: String) {
private var emails: List <Email>? = null
val emails: List‹Email>
get () {
if (_emails = null) {
_emails = loadEmails (this)
}
return _emails!! // 저장해 둔 데이터가 있으면 그 데이터를 반환한다.
}
}
위는 이메일을 불러오기 전에는 null을 저장하고, 불러온 다음에는 이메일 리스트를 저장하는 _emails프로퍼티를 추가해서 지연 초기화를 구현한 클래스를 보여준다.
_emails 라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails 는 _emails 라는 프로퍼티 값에 대한 읽기 연산을 제공한다. _emails 는 널이 될 수 있는 타입인 반면 emails 는 널이 될 수 없는 타입이므로 프로퍼티를 두 개 사용해야 한다.
위임 프로퍼티를 사용하면 위 코드가 훨씬 더 간단해진다. 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화준다. 예제와 같은 경우를 위한 위임 객체를 반환하는 표준 라이브러리 함수가 바로 lazy 다.
class Person (val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메서드가 들어있는 객체를 반환한다. 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.
lazy 함수의 인자는 값을 초기화할 때 호출할 람다다. lazy 함수는 기본적으로 스레드 안전하다. 하지만 필요에 따라 동기화에 사용할 락을 lazy 함수에 전달할 수도 있고, 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 lazy 함수가 동기화를 하지 못하게 막을 수도 있다.
3. 위임 프로퍼티 구현
어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다. 이런 기능이 유용할 때가 많다. 예를 들어 어떤 객체를 UI에 표시하는 경우 객체가 바뀌면 자동으로 UI도 바뀌어야 한다. 이 경우 코틀린에서는 위임 프로퍼티를 이용해 기능을 구현할 수 있다.
4. 위임 프로퍼티 컴파일 규칙
class C {
var prop: Type by MyDelegate ()
}
val c = c()
컴파일러는 MyDelegate 클래스의 인스턴스를 감춰진 프로퍼티에 저장하며 그 감춰진 프로퍼티를 <delegate> 라는 이름으로 부른다. 또한 컴파일러는 프로퍼티를 표현하기 위해 KProperty 타입의 객체를 사용한다. 이 객체를 <property> 라고 부른다.
컴파일러는 아래 코드를 생성한다.
class c {
private val <delegate> = MyDelegate ()
var prop: Type
get () = <delegate›. getValue (this, <property>)
set (value: Type) = <delegates>.setValue(this, <property>, value)
}
다시 말해 컴파일러는 모든 프로퍼티 접근자 안에 getValue와 setValue 호출 코드를 생성해준다.
5. 프로퍼티 값을 맵에 저장
자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 활용하는 경우가 자주 있다. 그런 객체를 확장 가능한 객체 라고 부르기도 한다
예를 들어 연락처 관리 시스템에서 연락처별로 임의의 정보를 저장할 수 있게 허용하는 경우를 살펴보자. 시스템에 저장된 연락처에는 특별히 처리해야 하는 일부 필수 정보 (이름 등)가 있고, 사람마다 달라질 수 있는 추가 정보가 있다.
그린 시스템을 구현하는 방법 중에는 정보를 모두 맵에 저장하되 그 맵을 통해 처리 하는 프로퍼티를 통해 필수 정보를 제공하는 방법이 있다.