Delegation
- 위임 패턴은 어떤 객체가 자신의 역할 중 일부를 다른 객체에 위임하여 처리하는 디자인 패턴
- 기능 일부를 직접 구현하지 않고 다른 객체에게 맡김
- by 키워드를 사용하여 간단하게 구현
사용 이유
- 코드의 재사용성
- 역할 분리로 책임 분산
- 클래스 상속의 복잡도 감소
예시
interface Printer {
fun printMessage()
}
class RealPrinter: Printer{
override fun printMessage() {
println("실제 출력 중입니다.")
}
}
class Manager(printer: Printer): Printer by printer
fun main() {
val realPrinter = RealPrinter()
val manager = Manager(realPrinter)
manager.printMessage()
}
위임 클래스
전통적인 위임 (Manual Delegation)
기존의 위임은 다른 객체의 기능을 직접 호출하여 처리
interface Sound {
fun makeSound()
}
class DogSound : Sound {
override fun makeSound() = println("멍멍!")
}
class Animal : Sound {
private val dogSound = DogSound()
override fun makeSound() {
dogSound.makeSound() // 위임
}
}
→ Animal이 Sound를 구현하지만 실제 작업은 DogSound에 위임
Kotlin의 위임 (Delegation by by 키워드)
class Animal2(sound: Sound): Sound by sound
→ Animal2가 Sound를 구현하지만 구현을 sound 객체에 위임하여 자동으로 처리
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) {
println("Console: $message")
}
}
class FileLogger : Logger {
override fun log(message: String) {
println("File: $message")
}
}
class Service(logger: Logger) : Logger by logger
fun main() {
val service1 = Service(ConsoleLogger())
service1.log("서비스 시작됨") // Console: 서비스 시작됨
val service2 = Service(FileLogger())
service2.log("로그 저장됨") // File: 로그 저장됨
}
- 위임할 구현체만 바꾸면 다른 방식도 동작 가능
- 반복 코드(boilerplate)를 줄일 수 있음
클래스 상속과 차이점
- 상속은 재사용을 위한 벙법이지만 위임을 활용하면 더 유연하고 안전하게 코드 재사용 가능
상속 | 위임 |
단일 상속만 가능 | 여러 객체로부터 기능 위임 가능 |
부모 클래스 변경에 영향을 받음 | 위임 대상만 교체하면 유연함 |
강한 결합 | 느슨한 결합 |
위임 프로퍼티
- 메서드 뿐만 아닌 프로퍼티 위임도 가능
위임 프로퍼티 비교
위임 프로퍼티 | 설명 | 사용 대상 | 초기화 시점 | 특징/제약사항 |
by lazy { ... } | 최초 접근 시 1회만 초기화되고 이후 캐시됨 | val | 첫 사용 시 | Thread-safe (기본은 synchronized) |
by Delegates.observable(...) | 값이 변경될 때마다 콜백 실행 | var | 선언과 동시에 초기값 필요 | 이전값과 새값을 모두 다룰 수 있음 |
by Delegates.vetoable(...) | 값 변경 전에 조건에 따라 거부 가능 | var | 선언과 동시에 초기값 필요 | false 반환 시 값 변경 안 됨 |
by Delegates.notNull() | null 불가한 값을 지연 초기화할 때 사용 | var | 명시적으로 할당해야 함 | 초기화 전 접근 시 IllegalStateException 발생 |
상황별 추천
상황 | 추천 위임 프로퍼티 |
한 번만 초기화하고 계속 재사용 | by lazy |
값이 바뀔 때마다 특정 동작(로그 등) 하고 싶을 때 | observable |
값이 바뀌기 전에 검증하고 싶을 때 | vetoable |
나중에 꼭 초기화되지만 null은 쓰기 싫을 때 | notNull() |
by lazy
- 지연 초기화(Lazy Intialization)을 위해 사용
- 객체가 최초로 접근될 때 초기화되며 그 이후 캐싱된 값을 반환
- Thread-safe하게 동작
- 기본적으로 LayThreadSafetyMode.SYNCHRONIZED 사용
- 초기화 시점: 최초 접근시
- 주 용도: 무거운 객체 지연 초기화
- val만 사용 가능
- 예시: 싱글턴 초기화, 무거운 계산
val message: String by lazy {
println("초기화 중...")
"Hello Delegation"
}
fun main() {
println("프로그램 시작")
println(value)
println(value)
프로그램 시작
초기화 중...
Hello Delegation
Hello Delegation
⇒ value에 처음 접근할 때만 블록이 실행되고 이후에는 캐시된 값만 사용
Delegates.observable
- 값이 변경될 때마다 특정 동작을 수행하고 싶을 때 사용
- 초기화 시점: 선언 시 기본값 / 이후에 값 변경마다 작동
- 주용도: 값 변경 추적 및 리액선 처리
- 사용 대상: var만 사용 가능
- 예시: 값이 바뀌면 로그를 남기거나, UI 갱신, 데이터 바인딩 등
import kotlin.properties.Delegates
var name: String by Delegates.observable("초기값") { prop, old, new ->
println("name이 '$old'에서 '$new'로 변경됨")
}
fun main() {
name = "희우"
name = "철수"
}
name이 초기값에서 희우로 변경됨
name이 희우에서 철수로 변경됨
⇒ 값이 바뀔 떄마다 Lambda 블록이 실행되면서 변경 내용이 추적
Delegates.vetoable
- 프로퍼티의 값이 변경되기 전에 실행되는 로직을 정의 가능
- 리스너 블록에서 true를 반환하면 변경 허용 / false를 반환하면 변경 거부
- 호출 시점: 값 변경 전
- 리턴값: Boolean
- 주용도: 값 변경 제어
- 예시: 값 제한, 조건부 설정, 유효성 검사
import kotlin.properties.Delegates
var score: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
newValue >= 0
}
fun main() {
println("현재 점수: $score") // 0
score = 10
println("변경된 점수: $score") // 10
score = -5
println("변경 시도 후 점수: $score") // 여전히 10
}
현재 점수: 0
변경된 점수: 10
변경 시도 후 점수: 10
1. 유효성 검증
var age: Int by Delegates.vetoable(0) { _, _, new ->
new in 0..150 // 사람 나이 범위만 허용
}
2. 조건 만족
var isLoggedIn: Boolean by Delegates.vetoable(false) { _, old, new ->
!old && new // 최초 로그인만 허용
}
Delegates.notNull()
- 초기값 없이 선언 후 나중에 반드시 한 번 초기화 해야하는 not-null 프로퍼티를 만들때 사용
- 초기화 전에 접근하면 예외(IllegalStateException) 발생
- var만 사용 가능
- 기본 타입에서 지연 초기화를 사용하고 싶을때 사용
- lateinit var은 프리미티브 타입(Int, Double 등)에는 사용할 수 없음
- nullable(?)를 사용하지 않고 초기화 시점을 명확히 컨트롤하고 싶을 때 사용
import kotlin.properties.Delegates
class MyActivity {
var username: String by Delegates.notNull90
fun onCreate() {
//초기화
username = "huiwoo"
}
fun printUser() {
println("Username: $username") // 초기화가 안된 경우 예외 발생
}
}
- 초기화가 안된 경우
fun main() {
val activity = MyActivity()
activity.printUser()
}
Exception in thread "main" java.lang.IllegalStateException: Property username should be initialized before get.
문제
개념 체크
1. Kotlin에서는 클래스 상속을 통해 하나의 인터페이스를 여러 클래스에 위임할 수 있다.
답: O, 인터페이스는 여러 클래스에 위임이 가능하다. 보충: 클래스는 하나만 상속이 가능하지만, 여러 인터페이스를 위임 가능
2. class A : Interface by obj에서 obj는 Interface를 구현한 객체여야 한다.
답: O, 인터페이스 구현을 다른 객체에 맡기는 것이므로 obj는 구현 객체여야한다.
3. 위임 패턴은 코드 재사용성과 유연성을 높여주는 데 도움을 준다.
답: O, 다른 객체에게 인터페이스 구현을 맡기므로 재사용성과 유연성이 높아진다.
4. 위임 프로퍼티는 by 키워드를 사용해서 getter와 setter를 직접 정의하는 방식이다.오답 노트: X, by 키워드는 getter/setter를 직접 정의하는 것이 아닌 위임 객체에 위임하는 것이다!
답: O, 값 설정에 대한 지연과 방법을 설정한다.
5. lazy 위임은 해당 프로퍼티가 선언과 동시에 즉시 초기화된다.
답: X, 최초 접근 시에 초기화된다.
6. 위임 클래스는 생성자에서 받은 객체의 함수를 오버라이드할 수 없다.
답: O, 오버라이드가 가능하다.
7. Kotlin의 위임은 컴파일러가 자동으로 위임 메서드를 생성해주는 기능이다.오답 노트: O, by 키워드를 이용하여 선언하면 컴파일러가 자동으로 위임 메서드를 생성해준다.
답: X, 사용자가 지정해야한다.
8. observable 위임 프로퍼티는 값이 변경될 때마다 특정 동작을 수행할 수 있다.
답: O, 값이 변경될때마다 처리된다.
9. Kotlin의 by 키워드는 클래스 위임에만 사용할 수 있다.
답: X, 프로퍼티 위임에도 사용 가능하다.
10. Kotlin의 위임 패턴은 자바보다 간결하게 구현할 수 있다.
답: O, by 키워드를 사용하여 쉽게 구현할 수 있다.
빈칸 채우기
1. Kotlin에서 클래스 위임은 ____ 키워드를 사용하여 인터페이스의 구현을 다른 객체에 맡길 수 있다.
답: by
2. 클래스 위임은 클래스 상속보다 더 높은 ____과(와) ____을 제공할 수 있다.
답: 유연성, 재사용성
3. 프로퍼티 위임에서 lazy는 프로퍼티가 ____ 될 때까지 초기화를 ____한다.
답: 최초 접근, 지연
4. observable 위임은 프로퍼티 값이 변경될 때마다 ____를 실행할 수 있다.보충: 공식적인 용어로는 콜백(callback)이 정확한 표현이다.
답: 동작
5. 다음 코드에서 사용된 위임 패턴은 ____이다
interface Printer {
fun print()
}
class RealPrinter : Printer {
override fun print() = println("Real Printer")
}
class PrinterProxy(printer: Printer) : Printer by printer
답: 클래스 위임
6. 위임 프로퍼티를 직접 구현할 때는 ReadOnlyProperty 또는 ____ 인터페이스를 사용할 수 있다.
답:
오답 노트: readWriteProperty
- ReadOnlyProperty: 읽기 전용 프로퍼티 위임을 만들때 사용
- ReadWriteProperty: 읽기 + 쓰기 가능한 프로퍼티 위임을 만들때 사용
→ 추가 학습 필요
7. 위임 패턴은 GoF 디자인 패턴 중 하나로, 다른 객체에게 ____을 위임하여 동작하게 한다.
답: 구현
보충: 구현도 가능하지만 GoF 디자인 패턴에서는 보통 책임(respnsibility)라고 표현
→ GoF 디자인 패턴 학습 필요
구현
1. 클래스 위임 기초
[문제 설명]
SoundMachine 인터페이스를 만들고, 이를 구현하는 Radio 클래스와 Mp3Player 클래스를 만든다. 그리고 Device 클래스는 SoundMachine 인터페이스의 구현을 위임받아 소리를 재생한다.
interface SoundMachine {
fun playSound()
}
class Radio : SoundMachine {
override fun playSound() {
println("📻 라디오에서 소리가 재생됩니다.")
}
}
class Mp3Player : SoundMachine {
override fun playSound() {
println("🎵 MP3 플레이어에서 음악이 재생됩니다.")
}
}
// 이 부분을 위임을 통해 작성하자!
class Device(...) : SoundMachine by ...
[요구사항]
- Device는 생성자에서 어떤 SoundMachine을 사용할지 받아야 함
- Device.playSound()를 호출하면 위임된 객체의 playSound()가 실행되어야 함
[실행 예시]
fun main() {
val radio = Radio()
val mp3 = Mp3Player()
val device1 = Device(radio)
val device2 = Device(mp3)
device1.playSound() // 📻 라디오에서 소리가 재생됩니다.
device2.playSound() // 🎵 MP3 플레이어에서 음악이 재생됩니다.
}
답:
class Device(device: SoundMachine): SoundMachine by device
2. 커스텀 위임 프로퍼티
아래처럼 TrimDelegate라는 위임 프로퍼티를 구현해보세요.
- TrimDelegate는 문자열 프로퍼티에 위임돼서, 값을 저장할 때 앞뒤 공백을 자동으로 제거해서 저장해야 합니다.
- 값을 읽을 때는 저장된 공백이 제거된 값을 반환해야 합니다.
import kotlin.reflect.KProperty
class TrimDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
// TODO: 저장된 값을 반환
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
// TODO: newValue의 앞뒤 공백을 제거해서 저장
}
}
class User {
var name: String by TrimDelegate()
}
fun main() {
val user = User()
user.name = " Hello Kotlin! "
println(user.name) // 출력 결과: "Hello Kotlin!"
}
[요구사항]
- getValue와 setValue 오퍼레이터 함수 구현
- User 클래스의 name 프로퍼티에 TrimDelegate를 위임하여 사용
답:
class TrimDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue.trim()
}
3. 다음 요구사항을 만족하는 클래스를 작성하세요.
3-1. val greeting 프로퍼티는 lazy 위임 프로퍼티로 선언하여, 최초 접근 시 "Hello, Kotlin!" 문자열을 생성하도록 한다.
답:
val greeting by lazy {
"Hello, Kotlin!"
}
3-2. var name 프로퍼티는 observable 위임 프로퍼티로 선언하여, 값이 변경될 때마다 이전 값과 새로운 값을 출력한다.
답:
var name: String by Delegates.observable("초기값") { _, oldValue, newValue ->
println("$oldValue -> $newValue")
}
3-3. var age 프로퍼티는 vetoable 위임 프로퍼티로 선언하여, 나이가 0 이상인 경우에만 값이 변경되도록 한다. (0 미만이면 변경 거부)
답:
var age: Int by Delegates.vetoable(0) { _, _, newValue ->
newValue >= 0
}
'개발새발 > 코틀린' 카테고리의 다른 글
[코틀린/Kotlin] 재귀(Recursion) (1) | 2025.09.01 |
---|---|
[코틀린/Kotlin] 인라인 함수 (1) | 2025.09.01 |
[코틀린/Kotlin] 코루틴(Coroutine) (3) | 2025.08.28 |
[코틀린/Kotlin] sealed class/when (0) | 2025.08.22 |
[코틀린/Kotlin] 스코프 함수 (0) | 2025.08.21 |