반응형

 

Kotlin Delegation 이해하기① - 왜 상속보다 추천되는 걸까?

 

[Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까?

상속과 델리게이션(위임) 모두 객체지향 프로그래밍의 디자인 방식입니다. 두 방식 모두 클래스를 다른 클래스로부터 확장하는데요. 개발자는 종종 두 방식 사이에서 선택을 해야 하는데, 언어

readystory.tistory.com

Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기

 

[Kotlin] Kotlin Delegation 이해하기② - 생성자 파라미터와 프로퍼티에서 사용하기

2021.07.25 - [Kotlin] - [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? [Kotlin] Kotlin Delegation 이해하기① - 왜 상속보다 추천되는걸까? 상속과 델리게이션(위임) 모두 객체지향 프로..

readystory.tistory.com

 

이 글은 Kotlin Delegation 이해하기 시리즈 3편으로 마지막 포스트가 되겠습니다.

 

지금까지의 예제에서 우리는 클래스 수준의 델리게이션에 집중했습니다. 코틀린은 클래스 수준의 델리게이션뿐만 아니라 객체의 속성(property)과 지역 변수에 접근하기 위한 델리게이션 역시 설정하고, 사용할 수 있습니다.

 

속성이나 지역 변수를 읽을 때, 코틀린 내부에서는 getValue() 함수를 호출합니다. 이와 유사하게 속성이나 변수를 설정할 때 코틀린은 setValue() 함수를 호출합니다. 객체의 델리게이션을 위의 두 메소드와 함께 제공함으로써 우리는 객체의 속성과 지역변수를 읽고 쓰는 요청을 가로챌 수 있습니다.

 

변수 델리게이션

우리는 지역 변수의 읽기와 쓰기에 대한 접근을 모두 가로챌 수 있습니다. 그리고 리턴되는 것을 변경할 수 있고 데이터를 언제, 어디에 저장하는지도 변경할 수 있습니다. 이런 기능을 묘사하기 위해서 String 변수에 대한 접근을 가로채는 커스텀 델리게이션을 만들어 보겠습니다.

 

우리가 유저의 댓글을 받는 어플리케이션을 만든다고 가정해 보겠습니다. 유저가 입력하는 텍스트는 다른 유저들에게 보여야 하기 때문에 비속어가 없어야 합니다.

 

이제 예의 없는 단어인 "stupid" 를 필터링하는 델리게이션을 작성해보겠습니다.

 

val comment = "this is stupid"
println(comment)  // this is stupid

 

우리의 목표는 "stupid" 라는 단어를 변경하여 문장이 출력되었을 때 무례하지 않게 만드는 것입니다. 이를 위해서 PoliteString 이라는 클래스를 만들고, 서론에서 언급한 setValue() 와 getValue() 를 갖도록 하겠습니다.

 

import kotlin.reflect.KProperty
class PoliteString(var content: String) {  
  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    content = value
  }
  
  operator fun getValue(thisRef: Any?, property: KProperty<*>) = content.replace("stupid", "s*****")
}

 

PoliteString 클래스는 델리게이션으로만 동작하도록 되어있습니다. 코틀린에서는 인터페이스를 구현할 필요도 없고 관습적인 코드도 없이 단지 메소드만 구현하면 됩니다.

 

델리게이션이 가변(mutable)인 속성이나 변수를 타깃으로 한다면 위 코드처럼 setValue() 도 구현해줘야 하고, 불변이라면 getValue() 만 선언해주면 되겠습니다.

 

다시 예제로 돌아와, PoliteString 클래스는 content 라는 뮤터블한 속성을 받습니다. getValue() 함수에서 문제가 되는 단어인 "stupid" 를 정리하고 문자열을 반환해 줍니다. 각각의 함수들은 operator 표기로 작성되어 "=" 기호를 통해 사용할 수 있게 됩니다. operator 에 대해 생소하신 분들은 코틀린 공식 문서를 참고하시면 되겠습니다.

 

이제 PoliteString 을 통해 테스트해보도록 하겠습니다.

 

fun main() {
  var comment: String by PoliteString("nice message")
  println(comment)  // nice message
  comment = "this is stupid"
  println(comment)  // this is s*****
}

 

보시다시피 comment 변수를 선언하면서 PoliteString 클래스로 델리게이션을 사용하였고 그 결과 문자열에 "stupid" 가 포함될 경우 "s*****" 로 대체되어 가져올 수 있도록 적용되었습니다. 만약 by 키워드 뒤에 클래스의 생성자를 사용하고 싶지 않다면 해당 델리게이션 인스턴스를 리턴해주는 함수를 사용할 수도 있습니다.

 

속성 델리게이션(Property Delegation)

이전에 사용한 접근법으로 우리는 지역변수뿐만 아니라 객체의 속성에도 델리게이션 접근을 할 수 있습니다. 속성을 정의할 때 값을 할당하는 게 아니라 by 를 사용하고 그 뒤에 델리게이션을 위치하면 됩니다. 다시 말하자면 델리게이션은 getValue() 를 구현하거나 getValue() 와 setValue() 를 모두 사용하는 속성이라면 어떤 객체에서든 사용 가능합니다.

 

코틀린 스탠다드 라이브러리의 디자인을 보면 Map 과 MutableMap 은 델리게이션을 사용할 수 있습니다. Map 은 val 속성, MutableMap 은 var 속성으로 사용할 수 있습니다. 왜냐하면 Map 은 getValue() 메소드도 가지고 있고, MutableMap 은 getValue() 와 setValue() 메소드 모두 가지고 있기 때문입니다. 

 

일단 우리는 데이터 소스로 사용될 MutableMap 에 comment 의 값을 저장하기 위해서 PoliteString 의 변형을 만듭니다.

class PoliteString(val dataSource: MutableMap<String, Any>) {
  operator fun getValue(thisRef: Any?, property: KProperty<*>) =
    (dataSource[property.name] as? String)?.replace("stupid", "s*****") ?: ""
    
  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    dataSource[property.name] = value
  }
}

 

PoliteString 을 수정하여서 String 파라미터를 받는 대신에 MutableMap<String, Any> 를 받아서 comment 의 값을 저장합니다. getValue() 메소드 안에서 속성의 이름을 key 로 사용해서 map 에 있는 값을 리턴해줍니다. 그리고 값이 존재한다면 쉽게 String 으로 캐스팅하고, 아까와 마찬가지로 "stupid" 라는 텍스트는 "s*****" 으로 대체해줍니다. setValue() 에서는 값을 map 에 저장하기만 합니다.

 

이제 PostComment 클래스를 만들어 보겠습니다. PostComment 클래스는 블로그 게시물의 comment 를 나타냅니다. PostComment 의 프로퍼티는 로컬 필드에 저장하는 대신 map 에 get/set 동작으로 위임합니다.

 

class PostComment(dataSource: MutableMap<String, Any>) {
  val title: String by dataSource
  var likes: Int by dataSource
  val comment: String by PoliteString(dataSource)
  
  override fun toString() = "Title: $title Likes: $likes Comment: $comment"
}

프라이머리 생성자가 MutableMap<String, Any> 타입의 기존 데이터 dataSource 를 파라미터로 받습니다. dataSource 는 이 클래스의 속성을 위임받아서 처리해 주는 델리게이션입니다.

 

title 은 읽기 전용 프로퍼티이고, likes 는 Int 타입에 읽기-쓰기 가능한 프로퍼티입니다. 그리고 이 둘은 모두 dataSource 에 델리게이션 됩니다.

반면에 comment 프로퍼티는 PoliteString 에 위임됩니다. PoliteString 은 동일한 dataSource 에서 데이터가 저장되고 검색됩니다. 물론 PoliteString 또한 동일한 dataSource 에서 데이터가 저장되고 검색됩니다.

 

comment 속성의 읽기와 쓰기는 각각 PoliteString 델리게이션의 getValue() 와 setValue() 을 호출합니다. 이 위임을 통해서 comment 값을 dataSource 에서 읽거나 dataSource 에 저장합니다.

 

이제 샘플 데이터를 통해 어떻게 동작하는지 확인해보겠습니다.

 

val data = mutableMapOf(
        "title" to "Using Delegation",
        "likes" to 2,
        "comment" to "Keep it simple, stupid")

val post = PostComment(data)
post.likes++
println(post)  // Title: Using Delegation Likes: 3 Comment: Keep it simple, s*****

 

지금까지 직접 델리게이션을 만드는 방법을 확인해봤습니다.

 

Lazy Delegation

단축 평가란 지금까지 진행한 식의 평가가 결과를 도출하기에 충분할 경우 식의 실행을 건너뛰는 것을 말합니다. 대부분의 프로그래밍 언어는 이 기능을 지원하고, 코틀린의 Lazy Delegation 은 이러한 접근의 영역을 넓혀줍니다. 함께 살펴보겠습니다.

 

도시의 현재 온도를 얻을 수 있는 함수가 있다고 가정해보겠습니다.

 

fun getTemperature(city: String): Double {
  println("fetch from webservic for $city")
  return 30.0
}

 

이 기상 함수 getTemperature() 는 웹 서비스에 원격 접속을 해야 하기 때문에 약간의 시간이 걸리고, 웹 서비스를 사용할 때 사용요금을 내야 한다고 가정해보겠습니다. 따라서 가능하다면 호출을 안 하는 게 이득입니다. 단축 평가는 자연스럽게 호출을 피하게 해 줍니다.

 

val showTemperature = false
val city = "Seoul"
if (showTemperature && getTemperature(city) > 20) {
  println("Warm")
} else {
  println("Nothing to report")
}

 

showTemperature 변수의 값은 false 입니다. 단축 평가식 덕분에 getTemperature() 메소드는 생략되어 호출되지 않습니다. 하지만 이 코드를 약간 리팩토링 하면 효율성이 떨어져 버립니다.

 

val showTemperature = false
val city = "Seoul"
val temperature = getTemperature(city)

if (showTemperature && temperature > 20) {
  println("Warm")
} else {
  println("Nothing to report")
}

 

우리는 getTemperature() 의 결과를 지역 변수에 저장했습니다. 그리고 if 문을 사용해서 결과를 평가했습니다. 하지만 이런 변화 때문에 오버헤드가 발생했습니다. 그리고 단축 평가 때문에 temperature 변수는 사용되지도 않았습니다.

 

개발자들은 Boolean 식의 단축 평가에 아주 익숙합니다. 다만 코틀린은 이런 Boolean 식 외에도 실행을 스킵할 수 있는 방법을 지원합니다. 개발자는 직접 컴파일러에게 식의 결과가 정말로 필요하기 전까지는 식을 실행하지 않도록 지연 연산을 요청할 수 있습니다. 식의 결과가 필요하지 않으면 식 전체를 스킵해 버립니다.

 

이젠 이전 코드를 Lazy Delegation 을 사용하도록 수정해보겠습니다. 그리고 이때 Lazy Delegation 클래스를 직접 사용하는 대신 편리하게 lazy 라는 wrapper 함수를 사용하겠습니다.

 

val showTemperature = false
val city = "Seoul"
val temperature by lazy { getTemperature(city) }

if (showTemperature && temperature > 20) {
  println("Warm")
} else {
  println("Nothing to report")
}

 

변수 temperature 를 by 키워드를 사용해서 델리게이션 속성으로 변경했습니다. lazy 함수는 연산을 실행할 수 있는 람다 표현식을 인자(argument)로 받습니다. 그리고 요청 즉시 실행하지 않고, 필요한 순간에만 실행합니다.

 

즉, lazy 뒤에 오는 람다 표현식의 연산은 변수의 값이 필요할 때만 수행됩니다. 변수의 값이 필요하기 전까지는 실행이 연기되고, 영원히 실행되지 않을 가능성이 있습니다. 이번 예제에서는 영원히 실행되지 않습니다.

 

위 예제에서는 우리가 showTemperature 변수의 값을 true 로 변경했을 때 getTemperature() 가 실행됩니다. 이는 temperature 가 정의되는 시점에 실행되는 게 아니라 showTemperature 의 평가 이후에 "temperature > 20" 의 평가가 필요한 시점에 실행됩니다.

 

그리고 중요한 점은 일단 람다 표현식이 실행되면 델리게이션은 결과를 저장하고 있다가 미래에 요청이 있으면 저장된 값을 알려줍니다. 람다 표현식이 다시 실행되는 게 아닙니다.

 

lazy 함수는 기본적으로 람다 표현식의 실행과 동기화됩니다. 그래서 하나의 스레드만 실행됩니다. 만약에 멀티 쓰레드에서 코드를 동시에 실행하는 게 안전한 경우나 안드로이드 UI 쓰레드처럼 싱글 쓰레드만 쓸 수 있는 경우라면 enum 타입인 LazyThreadSafetyMode 인자 값을 lazy 함수로 전달해서 다른 종류의 동기화 옵션을 선택할 수 있습니다.

 

재밌게도 게으름(Lazy)이 보다 효율적인 코드를 만들어 냈는데요.

코틀린 델리게이션에 대해서 1~3편을 통해 알아봤고, 이 외에도 코틀린 스탠다드 라이브러리에 빌트인 되어 있는 observable() 델리게이션 함수나 vetoable() 함수도 추가로 알아보시면 좋을 것 같습니다.

 

긴 글 읽어 주셔서 감사합니다.

반응형
반응형