본문 바로가기
개발새발/코틀린

[코틀린/Kotlin] 위임(Delegation)

by 조희우 2025. 8. 28.

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
}