Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

19장. 위임

일을 혼자 다 하지 않고,
잘하는 누군가에게 대신 맡기는 방법이 있습니다.

회사에서 팀장이 모든 일을 직접 하지 않는 것과 같습니다.
자료 정리는 정리 잘하는 팀원에게,
발표는 발표 잘하는 팀원에게 맡깁니다.

이렇게 “내 일을 다른 사람에게 대신 맡기는 것“을
위임(delegation)이라고 합니다.

코틀린은 이 위임을 문법으로 아주 쉽게 지원합니다.
by라는 짧은 키워드 하나면 됩니다.

이 장에서는 위임이 무엇인지,
그리고 코틀린에서 위임을 어떻게 쓰는지 배웁니다.

처음에는 낯설 수 있지만,
알고 나면 반복 코드를 크게 줄여 주는 아주 편리한 도구입니다.


19.1 Delegation이란 무엇인가

일을 대신 맡긴다는 것

위임(delegation)이란
“내가 할 일을 다른 대상에게 대신 맡기는 것“입니다.

일상에서도 우리는 위임을 자주 합니다.

이사할 때 짐을 내가 다 나르지 않고
이삿짐센터에 맡기는 것,
이것이 위임입니다.

나는 “이사한다“는 책임만 지고,
실제 일은 전문가에게 넘기는 것입니다.

프로그래밍에서도 똑같습니다.
어떤 객체가 자기 일을 직접 처리하지 않고,
그 일을 잘하는 다른 객체에게 넘깁니다.

위임하는 쪽과 위임받는 쪽

위임에는 두 역할이 있습니다.

  • 위임하는 쪽 : 일을 맡기는 대상
  • 위임받는 쪽 : 일을 대신 처리하는 대상

코드로 아주 간단하게 표현하면 이렇습니다.

class 음악플레이어 {
    private val 스피커 = 스피커()

    fun 소리내기() {
        스피커.출력()   // 실제 일은 스피커에게 맡긴다
    }
}

음악 플레이어는 소리를 직접 만들지 않습니다.
스피커라는 객체에게 그 일을 넘깁니다.

이렇게 어떤 객체가
자기 안에 다른 객체를 두고,
그 객체에게 일을 넘기는 방식이 위임의 기본 모습입니다.

왜 위임을 쓸까

위임을 쓰면 좋은 점은 크게 두 가지입니다.

첫째, 잘 만들어진 기능을 그대로 재사용합니다.
이미 있는 객체에게 일을 맡기면,
같은 코드를 다시 만들 필요가 없습니다.

둘째, 역할을 나눌 수 있습니다.
각 객체가 자기가 잘하는 일에만 집중하게 됩니다.

이런 생각은 상속(8장)과도 자주 비교됩니다.
상속이 “부모의 기능을 물려받는 것“이라면,
위임은 “다른 객체에게 일을 넘기는 것“입니다.

둘 중 어느 쪽이 나은지는
이 장 마지막(19.7)에서 다시 정리하겠습니다.

지금은 이렇게만 기억해 둡시다.

위임은
“직접 하지 않고 대신 맡기는 것“이다.


19.2 Interface Delegation과 by

반복되는 전달 코드

먼저 자바에서 흔히 겪는 불편함부터 봅시다.

인터페이스(interface)란
“이런 기능을 갖춰야 한다“고 정해 놓은 약속입니다.
(인터페이스는 8장에서 배웠습니다.)

예를 들어 소리를 내는 기능을
인터페이스로 정해 봅시다.

interface Speaker {
    fun playSound()
    fun stopSound()
}

그리고 이 약속을 지키는 기본 스피커를 만듭니다.

class BasicSpeaker : Speaker {
    override fun playSound() {
        println("소리를 냅니다")
    }

    override fun stopSound() {
        println("소리를 멈춥니다")
    }
}

이제 음악 플레이어도
Speaker처럼 동작하게 만들고 싶습니다.

하지만 소리 내는 기능은
이미 BasicSpeaker가 잘 만들어 두었습니다.
그러니 그 객체에게 맡기면 됩니다.

자바 방식으로 쓰면 이렇게 됩니다.

class MusicPlayer(
    private val speaker: Speaker
) : Speaker {
    override fun playSound() {
        speaker.playSound()   // 그냥 넘겨주기만 함
    }

    override fun stopSound() {
        speaker.stopSound()   // 그냥 넘겨주기만 함
    }
}

여기서 문제가 보입니다.
playSound, stopSound가 하는 일은
그냥 speaker에게 그대로 넘기는 것뿐입니다.

메서드가 열 개, 스무 개라면?
이런 “넘기기만 하는 코드“를
그 수만큼 반복해서 써야 합니다.

이런 코드를 전달 코드(forwarding code)라고 합니다.
지루하고, 실수하기도 쉽습니다.

by로 위임하기

코틀린은 이 반복을 by 키워드 하나로 해결합니다.

class MusicPlayer(
    speaker: Speaker
) : Speaker by speaker

이게 전부입니다.
앞의 길었던 코드와 완전히 같은 일을 합니다.

여기서 by speaker의 뜻은 이렇습니다.

Speaker가 해야 할 일은
speaker에게 맡겨라(by).

이렇게 인터페이스의 기능을
다른 객체에게 통째로 넘기는 것을
인터페이스 위임(interface delegation)이라고 부릅니다.

코틀린 컴파일러가
앞에서 우리가 손으로 쓰던 전달 코드를
자동으로 만들어 줍니다.

일부만 바꾸기

위임하면서 일부 기능만
직접 바꾸고 싶을 때도 있습니다.

그럴 때는 그 메서드만 override로 다시 적으면 됩니다.

class MusicPlayer(
    speaker: Speaker
) : Speaker by speaker {

    override fun playSound() {
        println("음악을 재생합니다")   // 이 기능만 새로 정의
    }
    // stopSound()는 speaker에게 그대로 위임됨
}

playSound는 새로 만든 내용대로 동작하고,
stopSound는 여전히 speaker에게 맡겨집니다.

실행해 보면 이렇습니다.

fun main() {
    val player = MusicPlayer(BasicSpeaker())
    player.playSound()
    player.stopSound()
}
음악을 재생합니다
소리를 멈춥니다

필요한 부분만 바꾸고,
나머지는 그대로 재사용하는 것입니다.

이것이 인터페이스 위임의 큰 매력입니다.


19.3 Property Delegation

프로퍼티도 위임할 수 있다

앞에서는 인터페이스(기능)를 위임했습니다.
코틀린은 프로퍼티(property)도 위임할 수 있습니다.

프로퍼티란
클래스가 가진 값, 즉 변수를 말합니다.
(프로퍼티는 4부에서 다뤘습니다.)

보통 프로퍼티는 이렇게 씁니다.

class User {
    var name: String = "이름 없음"
}

name은 값을 그냥 자기 안에 저장합니다.

그런데 값을 읽고 쓰는 그 동작 자체를
다른 객체에게 맡길 수 있습니다.
이것이 프로퍼티 위임(property delegation)입니다.

by로 프로퍼티 위임하기

프로퍼티에서도 똑같이 by를 씁니다.

class User {
    var name: String by 어떤위임객체
}

by 뒤에 오는 객체가
name의 값을 읽고 쓰는 일을 대신 처리합니다.

즉, name을 읽으면
그 위임 객체가 대신 “값을 꺼내 주고”,
name에 값을 넣으면
그 위임 객체가 대신 “값을 저장해” 줍니다.

말로만 들으면 어렵습니다.
그래서 코틀린은 자주 쓰는 위임 객체를
미리 만들어 두었습니다.

대표적인 것이 다음 절에서 배울 lazy입니다.

위임 객체가 갖춰야 할 약속

프로퍼티 위임을 이해하는 핵심은 이것입니다.

위임 객체는
“값을 읽어 주는 방법“과
“값을 저장하는 방법“을 알고 있어야 한다.

값을 읽어 주는 방법을 getValue,
값을 저장하는 방법을 setValue라고 합니다.

val(읽기 전용)이면 getValue만,
var(읽고 쓰기)이면 둘 다 필요합니다.

이 약속을 직접 만드는 방법은
19.6 커스텀 위임에서 다룹니다.

지금은 코틀린이 미리 만들어 준
편리한 위임들을 먼저 써 보겠습니다.


19.4 lazy

필요할 때 딱 한 번만 만들기

lazy는 가장 많이 쓰이는 프로퍼티 위임입니다.
사실 우리는 5장에서 이미 잠깐 만났습니다.

lazy는 우리말로 “게으른“이라는 뜻입니다.
이름처럼 “미루는” 성질을 가집니다.

값을 미리 만들어 두지 않고,
실제로 그 값이 처음 필요해질 때
그제야 딱 한 번 만든다.

이것을 지연 초기화(lazy initialization)라고 합니다.
초기화(값을 처음 만드는 일)를 뒤로 미룬다는 뜻입니다.

lazy 사용법

by lazy { } 형태로 씁니다.

class Database {
    val connection: String by lazy {
        println("연결을 만듭니다")
        "DB 연결 완료"
    }
}

lazy의 중괄호 { } 안에는
값을 만드는 코드를 적습니다.

이 코드는 connection
처음 읽는 순간에만 실행됩니다.

실행해 봅시다.

fun main() {
    val db = Database()
    println("아직 연결 안 함")

    println(db.connection)   // 여기서 처음 만들어짐
    println(db.connection)   // 이미 만든 값을 재사용
}
아직 연결 안 함
연결을 만듭니다
DB 연결 완료
DB 연결 완료

결과를 잘 봅시다.

  • db를 만든 시점에는
    “연결을 만듭니다“가 출력되지 않습니다.
  • connection을 처음 읽을 때
    비로소 값을 만듭니다.
  • 두 번째로 읽을 때는
    다시 만들지 않고 이미 만든 값을 씁니다.

lazy는 언제 쓸까

lazy는 이런 경우에 유용합니다.

  • 만드는 데 시간이 오래 걸리는 값
  • 항상 쓰이지는 않는 값
  • 무거운 자원(데이터베이스 연결 등)

만들어 두고 안 쓰면 낭비입니다.
lazy를 쓰면 정말 필요할 때만 만들므로
자원을 아낄 수 있습니다.

한 가지 기억할 점이 있습니다.

lazyval에만 쓸 수 있습니다.

한 번 만든 값을 계속 재사용하는 것이 목적이라,
값을 바꿀 수 있는 var에는 어울리지 않습니다.


19.5 Observable Property

값이 바뀌는 순간을 지켜보기

lazy 말고도 코틀린이 미리 만들어 둔
편리한 위임이 있습니다.

그중 하나가
관찰 가능한 프로퍼티(observable property)입니다.

관찰(observe)이란 “지켜본다“는 뜻입니다.
값이 바뀌는 순간을 지켜보다가,
바뀔 때마다 원하는 코드를 실행할 수 있습니다.

값이 바뀔 때마다
알림을 받는 것과 같습니다.

Delegates.observable

Delegates.observable을 씁니다.

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("이름 없음") { property, old, new ->
        println("이름이 '$old' 에서 '$new' 로 바뀜")
    }
}

조금 길어 보이니 하나씩 봅시다.

  • "이름 없음" : 처음에 넣어 둘 값(초깃값)
  • { property, old, new -> ... } : 값이 바뀔 때 실행할 코드
  • old : 바뀌기 전 값
  • new : 바뀐 후 값

실행해 봅시다.

fun main() {
    val user = User()
    user.name = "홍길동"
    user.name = "이순신"
}
이름이 '이름 없음' 에서 '홍길동' 로 바뀜
이름이 '홍길동' 에서 '이순신' 로 바뀜

값을 넣을 때마다
중괄호 안의 코드가 자동으로 실행됩니다.

값이 언제 바뀌는지 기록을 남기거나,
바뀔 때 화면을 갱신하는 데 쓰기 좋습니다.

Delegates.vetoable

값이 바뀌는 것을
막아야 할 때도 있습니다.

이때는 Delegates.vetoable을 씁니다.
vetoable은 “거부할 수 있는“이라는 뜻입니다.

import kotlin.properties.Delegates

class Account {
    var balance: Int by Delegates.vetoable(0) { property, old, new ->
        new >= 0   // true면 변경 허용, false면 거부
    }
}

중괄호 안에서
true를 돌려주면 값 변경을 허용하고,
false를 돌려주면 변경을 막습니다.

여기서는 새 값이 0 이상일 때만 허용합니다.
잔액이 음수가 되는 것을 막는 것입니다.

fun main() {
    val account = Account()

    account.balance = 1000
    println(account.balance)   // 1000 (허용됨)

    account.balance = -500
    println(account.balance)   // 1000 (거부되어 그대로)
}
1000
1000

-500을 넣으려 했지만
조건에 맞지 않아 거부되었습니다.
그래서 값은 이전 그대로 1000입니다.

잘못된 값이 들어오는 것을
프로퍼티 차원에서 막을 수 있는 것입니다.


19.6 Custom Delegate

위임을 직접 만들어 보기

지금까지는 코틀린이 미리 만든
lazy, observable 같은 위임을 썼습니다.

이제 위임 객체를 직접 만들어 보겠습니다.
이것을 커스텀 위임(custom delegate)이라고 합니다.

19.3에서 위임 객체는
“값을 읽는 방법“과 “값을 저장하는 방법“을
알고 있어야 한다고 했습니다.

  • 읽는 방법 : getValue
  • 저장하는 방법 : setValue

이 두 함수를 직접 만들면
나만의 위임 객체가 됩니다.

읽기 전용 위임 만들기

먼저 값을 읽기만 하는
간단한 위임을 만들어 봅시다.

import kotlin.reflect.KProperty

class UpperCaseDelegate {
    private val value = "hello"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value.uppercase()   // 항상 대문자로 돌려줌
    }
}

낯선 부분을 짚어 봅시다.

  • operator : 위임 규칙을 따르겠다는 표시
  • getValue : 값을 읽을 때 불리는 함수
  • thisRef : 이 프로퍼티를 가진 객체
  • property : 프로퍼티 자신의 정보

지금은 thisRefproperty
꼭 쓰지 않아도 됩니다.
정해진 형식이라 적어 두는 것입니다.

이제 이 위임을 프로퍼티에 붙여 봅시다.

class Message {
    val text: String by UpperCaseDelegate()
}

fun main() {
    val message = Message()
    println(message.text)
}
HELLO

text를 읽으면
UpperCaseDelegategetValue가 실행되어
값을 대문자로 바꿔 돌려줍니다.

읽고 쓰기 위임 만들기

var처럼 값을 저장도 하려면
setValue도 함께 만들면 됩니다.

값을 넣을 때 앞뒤 공백을 없애 주는
위임을 만들어 보겠습니다.

import kotlin.reflect.KProperty

class TrimDelegate {
    private var value = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue.trim()   // 앞뒤 공백을 없애서 저장
    }
}

setValue
값을 저장할 때 불리는 함수입니다.
마지막 자리(newValue)로 넣으려는 값이 전달됩니다.

여기서는 trim()으로
앞뒤 공백을 없앤 뒤 저장합니다.

class Form {
    var name: String by TrimDelegate()
}

fun main() {
    val form = Form()
    form.name = "   홍길동   "
    println("[${form.name}]")
}
[홍길동]

공백이 잔뜩 붙은 값을 넣었지만,
저장될 때 공백이 사라졌습니다.

이렇게 “값을 다루는 규칙“을
위임 객체 안에 한번 만들어 두면,
여러 프로퍼티에서 재사용할 수 있습니다.

표준 인터페이스로 정리하기

getValuesetValue
매번 손으로 적는 대신,
코틀린이 준비한 약속을 쓸 수도 있습니다.

  • ReadOnlyProperty : 읽기 전용(val)용
  • ReadWriteProperty : 읽고 쓰기(var)용

이 약속을 따르면
함수 형식을 실수 없이 맞출 수 있습니다.

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class TrimDelegate : ReadWriteProperty<Any?, String> {
    private var value = ""

    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value.trim()
    }
}

class Form {
    var name: String by TrimDelegate()
}

동작은 앞의 예와 똑같습니다.
다만 정해진 약속을 따르므로
형식이 더 안전하고 분명해집니다.

지금 이 내용이 어렵게 느껴져도 괜찮습니다.
“위임은 이렇게 직접 만들 수도 있구나”
정도만 기억해 두면 충분합니다.


19.7 상속보다 위임이 적절한 경우

상속의 한계

마지막으로 위임과 상속(8장)을
나란히 놓고 비교해 봅시다.

상속(inheritance)은
부모 클래스의 기능을 자식이 물려받는 것입니다.

강력하지만, 문제도 있습니다.

첫째, 부모에 강하게 묶입니다.
부모 코드가 바뀌면
자식이 뜻하지 않게 영향을 받습니다.

둘째, 필요 없는 기능까지 물려받습니다.
부모가 가진 모든 것을
자식이 그대로 떠안게 됩니다.

셋째, 부모는 하나만 가질 수 있습니다.
코틀린에서 클래스 상속은
부모가 오직 하나뿐입니다.

위임의 장점

위임은 이 한계들을 부드럽게 풉니다.

첫째, 느슨하게 연결됩니다.
필요한 기능을 가진 객체를
그냥 “빌려” 쓰는 방식이기 때문입니다.

둘째, 필요한 기능만 골라 씁니다.
원하는 인터페이스만 위임하면 됩니다.

셋째, 여러 대상에게 나눠 맡길 수 있습니다.
소리는 스피커에게,
화면은 디스플레이에게 각각 맡길 수 있습니다.

interface Speaker {
    fun playSound()
}

interface Display {
    fun showScreen()
}

class TV(
    speaker: Speaker,
    display: Display
) : Speaker by speaker, Display by display

TV는 소리와 화면 기능을
서로 다른 두 객체에게 나눠 맡깁니다.

상속으로는 이렇게
두 부모의 기능을 함께 물려받기 어렵습니다.
하지만 위임으로는 자연스럽습니다.

기준 정리하기

그렇다면 언제 무엇을 써야 할까요?
간단한 기준을 표로 정리해 봅시다.

상황어울리는 방식
“~는 ~의 한 종류다” 관계상속
“~는 ~기능을 빌려 쓴다” 관계위임
부모 코드에 강하게 묶여도 괜찮음상속
느슨하게 연결하고 싶음위임
여러 기능을 조합하고 싶음위임

관계를 말로 대 보면 쉽습니다.

“고양이는 동물의 한 종류다”
→ 이런 관계는 상속.

“자동차는 엔진 기능을 빌려 쓴다”
→ 이런 관계는 위임.

프로그래밍 세계에는 오래된 조언이 있습니다.

상속보다 조합(위임)을 먼저 고려하라.

무조건 위임이 옳다는 뜻은 아닙니다.
다만 상속부터 떠올리기 쉬우니,
“위임으로 풀 수는 없을까?“를
한 번 더 생각해 보라는 조언입니다.

코틀린은 이 위임을
by 한 단어로 아주 쉽게 만들어 줍니다.
그래서 위임을 고르는 것이
전혀 부담스럽지 않습니다.


19장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • 위임은 “일을 다른 대상에게 대신 맡기는 것“이라는 점
  • by 키워드로
    인터페이스 기능을 통째로 위임하는 방법
  • 프로퍼티의 읽고 쓰기 동작도
    다른 객체에게 위임할 수 있다는 점
  • 값을 미뤄 두었다가 처음 쓸 때 만드는 lazy
  • 값이 바뀌는 순간을 지켜보는 observable,
    값 변경을 막는 vetoable
  • getValuesetValue
    위임을 직접 만드는 방법
  • 상속과 위임을 비교하고,
    어느 쪽이 어울리는지 고르는 기준

특히 bylazy
앞으로 코틀린 코드에서 자주 만나게 됩니다.

위임을 잘 쓰면
반복 코드를 줄이고,
유연한 구조를 만들 수 있습니다.

다음 장에서는
또 다른 코틀린의 기능을 이어서 배워 보겠습니다.