8장 코틀린 대리자
코틀린의 대리자(delegate)에 대해 설명한다. 표준 라이브러리의 lazy, observable, vetoable, notNull 대리자 및 사용자 정의 대리자 등을 사용하는 방법을 배울 것이다. 클래스 대리자를 통해 상속을 합성으로 대체할 수 있고, 속정 대리자를 통해 어떤 속성의 획득자와 설정자를 다른 클래스에 있는 속성의 획득자와 설정자로 대체할 수 있다.
코틀린답게 대리자를 사용하는 좋은 예제로서 코틀린 라이브러리의 표준 Delegates 객체의 구현을 보여준다.
8.1 대리자를 사용해서 합성 구현하기
이슈: 다른 클래스의 인스턴스가 포함된 클래스를 만들고, 그 클래스에 연산을 위힘하고 싶다.
해법: 연산을 위힘할 메소드가 포함된 인터페이스를 만들고, 클래스에서 해당 인터페이스를 구현한 다음, by 키위드를 사용해 바깥쪽에 래퍼 클래스를 만든다.
설명: 최신 객체 지향 디자인은 강한 결합 없이 기능을 추가할때 상속보다는 합성을 선호한다. 코틀린에서 by 키워드는 포함된 객체에 있는 모든 public 함수를 이 객체를 담고 있는 컨테이너를 통해 노출할 수 있다.
예를 들어 스마트폰에는 전화와 카메라뿐만 아니라 여러 가지 부속품이 있다. 스마트폰을 래퍼 객체로, 내부 전화와 카메라 부품을 스마트폰에 포함된 객체로 대입해 생각해보면, 스마트폰 클래스의 작성 목적은 스마트폰 클래스의 함수를 호출하면 이에 상응하는 포함된 인스턴스 함수를 호출하는 것이다.
포함된 객체에 있는 모든 public 함수를 이 객체를 담고 있는 컨테이너를 통해 노출하려면, 포함된 객체의 public 메소드의 인터페이스를 생성해야 한다. 따라서 Phone과 Camera 클래스는 예제 8-1처럼 Dialable과 Snappable 인터페이스를 구현해야 한다.
이 SmartPhone 클래스가 예제 8-2처럼 생성자에서 Phone과 Camera를 인스턴스화하고 모든 public 함수를 Phone과 Camera 인스턴스에 위임하도록 정의할 수 있다.
키워드 by를 사용해서 위임
예제 8-3의 테스트에서 보듯이, 이제 SmartPhone 클래스를 인스턴스화해 Phone 또는 Camera의 모든 메소드를 호출할 수 있다.
인자 없이 SmartPhone을 인스턴스화함
위임 함수를 호출
포함된 객체는 SmartPhone을 통해 노출된 것이 아니라 오직 포함된 객체의 public 함수만이 노출된다. Phone과 Camera 클래스에는 많은 함수가 있을 수도 있지만 Dialable과 Snappable 인터페이스에 선언되어 있고, 이와 일치하는 함수만 사용 가능하다. 인터페이스를 정의하는 작업은 불필요해 보이지만 정확한 관계를 유지할 수 있게 만든다.
인텔리제이에서 사용되는 코틀린 바이트코드 보기를 수행한 다음 SmartPhone 클래스를 디컴파일했다. 이 결과에 해당하는 자바 코드 중 일부가 예제 8-4에 나와 있다.
인터페이스 타입의 필드
대리자 메소드
내부적으로 SmartPhone 클래스는 위임된 속성을 인터페이스 타입으로 정의한다. 이 인터페이스 타입에 상응하는 클래스 인스턴스는 생성자를 통해 제공된다. 그런 다음 대리자 메소드는 해당하는 메소드를 필드에서 호출한다.
8.2 lazy 대리자 사용하기
이슈: 어떤 속성이 필요할 때까지 해당 속성의 초기화를 지연시키고 싶다.
해법: 코틀린 표준 라이브러리의 lazy 대리자를 사용하자.
설명: 코틀린은 어떤 속성의 획득자와 설정자가 대리자라고 불리는 다른 객체에서 구현되어 있다는 것을 암시하기 위해 속성에 by 키워드를 사용한다. 코틀린 표준 라이브러리에는 다수의 대리자 함수가 있다. 그 중 가장 인기 있는 대리자 함수가 lazy이다. lazy 대리자를 사용하려면 예제 8-5에서 보듯이 처음 접근이 일어날 때 값을 계산하기 위해 만들어진 () -> T 형식의 초기화 람다를 제공해야 한다.
기본. 스스로 동기화
lazy 인스턴스가 다수의 스레드 간에 초기화를 동기화하는 방법을 명시
동기화 락을 위해 제공된 객체를 사용
mode 속성이 없는 lazy 대리자의 기본값은 LazyThreadSafetyMode.SYNCHRONIZED이다. lazy에 제공된 초기화 람다가 예외를 던지면, 다음 번에 접근할 때 해당 값의 초기화를 다시 시도한다. 예제 8-6에서 간단한 lazy 예제를 살펴보자.
여기서 목표는 ultimateAnswer의 값을 lazy에 제공된 람다 식이 평가되는 때, 즉 해당 변수에 처음 접근하게 될 때까지 계산하지 않는 것이다. 이 구현에서 lazy는 람다를 받고 해당 속성에 처음 접근 할 때 제공된 람다를 실행하는 lazy<Int> 타입의 인스턴스를 리턴하는 함수다. 따라서 다음 코드는 "computing the answer"를 오직 한 번만 출력한다.
println(ultimateAnswer)
println(ultimateAnswer)
첫 ultimateAnswer 호출은 lazy가 받은 람다를 실행하고 그다음 변수에 저장될 42를 리턴한다.
내부적으로 코틀린은 이 값을 캐시하는 Lazy 타입의 ultimateAnswer$delegate라는 특별한 속성을 생성한다. LazyThreadSafetyMode 타입의 인자는 다음과 같은 값의 enum을 받는다.
SYNCHRONIZED
오직 하나의 스레드만 Lazy 인스턴스를 초기화할 수 있게 락을 사용
PUBLICATION
초기화 함수가 여러 번 호출 될 수 있지만 첫 번째 리턴값만 사용됨
NONE
락이 사용되지 않음
어떤 객체를 lazy, lock의 인자로 제공하면 값을 계산할 때 이 객체가 대리자를 동기화한다. lock의 인자로 아무것도 제공되지 않았다면 대리자는 자신 스스로 동기화한다. lazy 대리자는 복잡한 객체를 인스턴스화할 때 적합안데 lazy 기본 원리는 모든 경우에 다 동일하다.
8.3 값이 널이 될 수 없게 만들기
이슈: 처음 접근이 일어나기 전에 값이 초기화되지 않았다면 예외를 던지고 싶다.
해법: notNull 함수를 이용해, 값이 설정되지 않았다면 예외를 던지는 대리자를 제공한다.
설명: 보통 코틀린 클래스의 속성은 클래스 생성 시에 초기화된다. 속성 초기화를 지연시키는 한 가지 방법은 속성에 처음 접근하기 전에 속성이 사용되면 예외를 던지는 대리자를 제공하는 notNull 함수를 사용하는 것이다. 예제 8-7에서는 속성이 사용되기 전에 어딘가에서 반드시 초기화돼야 하는 shouldNotBeNull 속성을 선언한다.
예제 8-8은 속성에 값이 제공되기 전에 접근을 시도하면 코틀린이 IllegalStateException을 던지는 테스트 케이스를 보여준다.
테스트 케이스의 동작이 너무 간당하지만 코틀린 표준 라이브러리의 Delegates.kt 내의 notNull 구현을 살펴보면 흥미로운 부분을 발견할 수 있다. 예제 8-9은 Delegates.kt 파일의 축약된 버전이다.
Delegates는 싱글톤(class 대신 object)
NotNullVar 클래스를 인스턴스화하는 팩토리 메소드
ReadWriteProperty를 구현한 private 클래스
object 키워드를 사용해서 Delegates 싱글톤 인스턴스를 정의했다. 따라서 Delegates에 포함된 notNull 함수는 마치 자바의 static처럼 동작한다. notNull 팩토리 메소드 ReadWriteProperty 인터페이스를 구현하는 private 클래스인 NotNullVar를 인스턴스화한다.
8.3 값이 널이 될 수 없게 만들기
이슈: 처음 접근이 일어나기 전에 값이 초기화되지 않았다면 예외를 던지고 싶다.
해법: notNull 함수를 이용해, 값이 설정되지 않았다면 예외를 던지는 대리자를 제공한다.
설명: 코틀린 클래스 속성은 클래스 생성시에 초기화된다. 속성 초기화를 지연시키는 한 가지 방법은 속성에 처음 접근하기 전에 속성이 사용되면 예외를 던지는 대리자를 제공하는 notNull 함수를 사용하는 것이다. 예제 8-7에서는 속성이 사용되기 전에 어딘가에서 반드시 초기화돼야하는 shouldNotBeNull 속성을 선언한다.
예제 8-8은 속성에 값이 제공되기 전에 접근을 시도하면 코틀린이 IllegalStateException을 던지는 테스트 케이스를 보여준다.
테스트 케이스의 동작이 너무 간단하지만 코틀린 표준 라이브러리의 Delegates.kt 내의 notNull 구현을 살펴보면 흥미로운 부분을 발견할 수 있다. 예제 8-9은 Delegates.kt 파일의 축약된 버전이다.
Delegates는 싱클톤(class 대신 object)
NotNullVar 클래스를 인스턴화하는 팩토리 메소드
ReadWriteProperty를 구현한 private 클래스
object 키워드를 사용해서 Delegates 싱글톤 인스턴스를 정의했다. 따라서 Delegates에 포함된 notNull 함수는 마치 자바의 static처럼 동작한다. notNull 팩토리 메소드는 ReadWriteProperty 인터페이스를 구현하는 private 클래스의 NotNullvar를 인스턴스화한다.
싱글톤 클래스, 팩토리 메소드, private 구현 클래스의 조합은 흔하게 볼 수 있는 코틀린의 구현 패턴이다. 사용자 정의 대리자를 작성하려면 싱글톤 클래스와 팩토리 메소드, private 구현 클래스의 조합 패턴을 따르는 것을 고려하자.
8.4 observable과 vetoable 대리자 사용하기
이슈: 속성의 변경을 가로채서, 필요에 변경을 거부하고 싶다.
해법: 변경 감지에는 observable 함수를 사용하고, 변경의 적용 여부를 결정할 때는 vetoable 함수와 람다를 사용하자.
설명: 레시피 8.3처럼 코틀린 표준 라이브러리에서 Delegates 객체의 observable 함수와 vetoable 함수는 사용하기 쉽다. 하지만 observable 함수와 vetoable 함수의 구현은 개발자가 대리자를 작성할 때 참고할 만한 좋은 패턴이다. 이 패턴을 설명하기 전에 observable 함수와 vetoable 함수의 사용법을 살펴보자. 코틀린 공식 문서의 observable 함수의 시그니처를 예제 8-10에서 보여준다.
두 팩토리 함수 모두 T 타입의 초기값과 람다를 인자로 받고 ReadWriteProperty 인터페이스를 구현한 클래스의 인스턴스를 리턴한다. 예제 8-11에서 보듯이 observable과 vetoable 팩토리 함수는 사용하기 쉽다.
watched 변수의 타입은 Int이고 1로 초기화됐고 이 변수의 값이 변경될 때마다 이전 값과 새로운 값을 보여주는 메시자가 출력된다. checked 변수의 값 타입도 Int이고 0으로 초기화됐지만 이번에는 오직 양수 값으로만 변경할 수 있다. vetoable의 람다 인자는 새로운 값이 0보다 크거나 같을 때만 true를 리턴한다.
예제 8-12에서 watched 변수 테스트는 예상대로 값이 변경되는지를 보여준다.
이 테스트는 다음 내용을 콘솔에 출력한다.
watched changed from 1 to 2
watched changed from 2 to 4
예제 8-13에서 vetoable checked 변수의 테스트는 오직 0보다 크거나 같은 값만 허용됨을 보여준다.
checked 값을 42또는 17로 변경하는 것 괜찮지만 -1로 변경하는 것은 거부된다. observable과 vetoable 함수 사용법은 매우 쉽다. 다시 말하지만 흥미로운 부분은 이 함수의 구현 부분이다. 예제 8-14처럼 싱글톤 Delegates 객체 안에서 observable과 notNull과 같은 팩토리 함수다.
가장 주목해야 할 사항은 observable과 vetoable 함수 모두 ObservableProperty 타입의 객체를 리턴한다는 점이다. 예제 8-15에 ObservableProperty 클래스가 나와 있다.
ObservableProperty 클래스는 모든 제네릭 타입 T의 속성을 저장하고 ReadWriteProperty 인터페이스를 구현한다. ReadWriteProperty 인터페이스를 구현했다는 것은 ObservableProperty 클래스가 ReadWriteProperty에서 보여주는 시그니처의 getValue와 setValue 함수를 제공해야 한다는 의미다. 이 예제의 경우 getValue는 value 속성만을 리턴한다.
setValue 함수는 더욱 흥미롭다. 이 함수는 현재 값을 저장한 다음 beforeChange 함수를 호출한다. beforeChange 함수가 true를 리턴한다면(기본값 true) value 속성은 변경되고 그다음 디폴트로 아무것도 하지 않는 afterChange 함수를 호출한다.
ObservableProperty는 모두 protected로 표시된 beforeChange와 afterChange라는 open 함수를 가진 추상 클래스다. 이 두 함수에는 기본 구현이 있지만 하위 클래스는 이 함수를 자유롭게 오버라이드할 수 있다. 이부분은 8-14에 나왔던 구현이다. observable 함수는 ObservableProperty를 확장하는 객체를 만들고 제공된 onChange 람다가 명시하는 작업을 수행하기 위해 afterChange 함수를 오버라이드한다. vetoable의 람다 구현은 인자로 제공되고 해당 람다는 반드시 해당 속성의 변경 여부를 결정할 수 있는 불리언을 리턴해야 한다.
다시 한번 ReadWriteProperty 인터페이스를 구현하기 위해 ObservableProperty 클래스를 생성했지만 추가 수명주기 메소드는 observable과 vetoable 함수 모두를 매우 쉽게 구현할 수 있게 해준다.
inline과 crossline
inline 키워드는 컴파일러가 함수만 호출하는 완전히 새로운 객체를 생성하는 것이 아닌 해당 호출 위치를 실제 소스 코드로 대체하도록 지시한다.
inline 함수는 지역 객체 또는 중첩 함수 같은 다른 컨텍스트에서 실행되어야 하는 파라미터로서 전달되는 람다이다. 이러한 '로컬이 아닌' 제어 흐름은 람다 내에서는 허용되지 않는다. 이 예제에서는 ObservableProperty를 확장한 클래스 대신 observable 또는 vetoable와 관련해 onChange 람다가 실행되기 때문에 crossinline 제어자가 필요하다.
싱글톤 객체 내부에서 클래스 대리자 인스턴스를 설정하는 팩리 함수 조합은 아주 유용하다.
8.5 대리자로서 Map 제공하기
이슈: 여러 값이 들어 있는 맵을 제공해 객체를 초기화하고 싶다.
해법: 코틀린 맵에는 대리자가 되는 데 필요한 getValue와 setValue 함수 구현이 있다.
설명: 객체 초기화에 필요한 값이 맵 안에 있다면 해당 클래스 속성을 자동으로 맵에 위임할 수 있다. 예를 들어 예제 8-16과 같은 Project 클래스가 있다고 가정하자.
map 인자에 위임
이 예제에서 클래스 생성자는 MutableMap을 인자로서 받고 해당 맵의 키에 해당하는 값으로 Project 클래스의 모든 속성을 초기화한다. Project 타입의 인스턴스를 생성하려면 예제 8-17에서 처럼 맵이 필요하다.
이 코드가 동작하는 이유는 Mutable에 ReadWriteProperty 대리자가 되는 데 필요한 올바른 시그니처의 setValue와 getValue 확장 함수가 있기 때문이다.
하지만 간접적인 추가 레이어가 필요한 이유가 궁금할 것이다. 다시 말해서 맵을 사용하는 대신 해당 속성을 생성자의 일부로 만들지 않는 이유가 무엇일까? 코틀린 공식 문서에서는 JSON을 파싱하거나 다른 동적인 작업을 하는 애플리케이션에서 이러한 메커니즘이 발생함을 언급한다.
예제 8-18의 테스트는 필요한 속성이 JSON 문자열에 들어 있다고 가정한다. 편의를 위해서 여기에서는 JSON을 하드코딩한다. 그다음 구글 Gdon 라이브러리를 사용해 JSON 문자열을 파싱하고 그 파싱 결과로 얻은 맵을 사용해서 Project 인스턴스를 생성한다.
JSON 문자열로 속성 맵을 가공
Project 인스턴스화을 위해 맵을 사용
Gson 라이브러리의 fromJson 함수는 문자열과 타입을 받기 때문에 코틀린에서는 이 예제 코드처럼 제네릭 타입을 명시할 수 있다. 이 getMapFromJSO의 결과 맵은 대리자로서 값을 제공하기 위해 올바른 타입이다.
8.6 사용자 정의 대리자 만들기
이슈: 어떤 클래스의 속성이 다른 클래스의 획득자와 설정자를 사용하게끔 만들고 싶다.
해법: ReadOnlyProperty 또는 ReadWriteProperty를 구현하는 클래스를 생성함으로써 직접 속성 대리자를 작성한다.
설명: 대체로 클래스의 속성은 지 필드와 함께 동작하지만 필수는 아니다. 대신 값을 획득하거나 설정하는 동작을 다른 객체에 위임할 수 있다. 사용자 정의 속성 대리자를 생성하려면 ReadOnlyProperty 또는 ReadWriteProperty 인터페이스에 존재하는 함수를 제공해야 한다. 예제 8-19에서 ReadOnlyProperty와 ReadWriteProperty 인터페이스의 시그니처를 모두 보여준다.
흥미롭게도 대리자를 만들려고 ReadOnlyProperty나 ReadWriteProperty 인터페이스를 구현할 필요가 없다. 그저 이 코드에서 보여주는 시그니처와 동일한 getValue와 setValue 함수만으로도 충분하다. 코틀린 표준 문서의 속성 대리자 절에는 Delegate 클래스가 있는 간단한 대리자 예제가 있다. 예제 8-20을 살펴보자.
방금 작성한 대리자를 사용하려면 예제 8-21처럼 위임할 클래스 또는 변수를 생성한 다음 해당 변수를 가져오거나 설정해야 한다.
이 코드는 다음을 출력한다.
delegates.Example@4c98385c, thank you for delegating 'p' to me! NEW has been assigned to 'p' in delegates.Example@4c98385c.
코틀린 표준 라이브러리에는 다수의 대리자가 들어 있다. 예를 들어 8.3에서는 예제 8-9에 나온 private NotNullVar 클래스를 인스턴스화하는 notNull 함수를 보여준다.
완전히 다른 예제 집합으로서 그레이들 빌드 도구는 위임된 속성을 통해 컨테이너와 상호작용 할 수 있게 도와주는 코틀린 DSL을 제공한다. 그레이들에는 두 개의 주요 속성 영역이 있다. 그중 하나는 프로젝트 자체와 연관되 속성의 집합이고, 다른 하나는 프로젝트 전체에서 사용할 수 있는 extra라는 속성이다.
build.gradle.kts라는 빌드 파일이 있다고 하자. 예제 8-22처럼 이 2가지 타입의 속성을 모두 만들고 접근할 수 있다.
project 속성 myProperty를 사용 가능하게 만듬
널이 될 수 있는 속성을 사용 가능하게 만듬
extra 속성 myNewProperty를 만들고 초기화
처음 접근이 일어날 때 초기화되는 속성을 생
PmyProperty=value 문법을 사용해 명령줄에서 프로젝트 속성을 설하거나 gradle.properties 파일에서 프로젝트 속성을 설정할 수 있다. 예제에서 처럼 extra 속성은 인자를 사용하거나 처음 접근 시에 평가되는 람다를 사용해 정의할 수 있다.
속성 대리자를 생성하는 방법은 꽤 간단하지만 이 장의 다른 설명을 했던 것처럼 이미 표준 라이브러리에 들어 있거나 또는 그레이들과 같은 서드파티가 제공하는 대리자를 사용하게 될 가능성이 높다.
Last updated
Was this helpful?