코틀린의 by 키워드는 Delegate Pattern을 쉽게 구현할 수 있도록 도와주는 키워드입니다.

Delegate Pattern

Delegate Pattern이란 객체의 메소드를 다른 객체(Helper Object)에 위임하여 처리하는 패턴입니다. 즉 어떤 기능을 자신이 수행하지 않고 다른 객체가 수행하도록 하는 패턴입니다.

특히 이 Delegate Pattern과 항상 붙어다니는 "상속 vs 조합" 이라는 단어입니다. 자식클래스에서 부모클래스의 요소를 사용할 때 상속(Ingeritance) 또는 조합(Composition)을 이용할 수 있습니다. 상속은 모든 요소를 물려받기 때문에 변수나 메소드 등을 다시 구현할 필요가 없어 편리하지만, 객체의 유연성이 떨어진다는 치명적인 단점이 있습니다. 만약 부모클래스가 수정된다면, 하위클래스도 수정되거나 override로 대응해야합니다.

조합(Composition) / 상속(Inheritance)

따라서 객체의 유연성을 높이기 위해 객체 조합(Composition)을 통해 부모클래스를 이용합니다. 상속이 아닌 객체의 조합으로 상위 클래스의 요소를 활용하는 것입니다.

Delegate Pattern은 Composition을 이용하는 일반적인 패턴입니다. Composition 객체의 함수가 많아지면 형식적인 코드(boilerplate code)를 많이 작성해야 할 수 있습니다. 코틀린은 by라는 키워드를 이용하여 적은 코드로 적용할 수 있도록 지원하고 있습니다. 먼저 Delegate Pattern에 대해서 간단히 예제로 알아보고, by를 이용하여 쉽게 적용하는 방법을 알아보겠습니다.

 

Delegate Pattern 예제

interface IBase {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : IBase {
    override fun printMessage() {
        print(x)
    }

    override fun printMessageLine() {
        println(x)
    }
}

class MessageService(val base: IBase) {
    fun printMessage() {
        base.printMessage()
    }

    fun printMessageLine() {
        base.printMessageLine()
    }
}

위 코드에서 BaseImpl 클래스는 IBase 인터페이스를 상속받아 구현했습니다. MessageService는 BaseImpl을 상속받지 않고, BaseImpl 클래스를 property로 가지고 있습니다. 그리고 MessageService에 존재하는 메소드들은 BaseImpl의 메소드들을 호출해주고 있습니다.

이 구조에서 MessageService 클래스는 BaseImpl의 메소드(기능)를 내부 변수 base에 위임하였습니다. 여기서 형식적인 코드(boilerplate code)는 MessageService.printMessage()MessageService.printMessageLine() 입니다. 만약 IBase 인터페이스의 메소드가 20개라면 20개에 대한 wrapper 메소드를 모두 작성해야합니다.

fun main() {
    val messageService = MessageService(BaseImpl(10))
    messageService.printMessage() // 10
    messageService.printMessageLine() // 10\n
}

위 코드처럼 MessageService의 생성자로 BaseImpl 클래스를 넘겨주고, messageService.printMessage()가 호출되면 MessageService 내부의 BaseImpl 클래스에 모든 일을 위임하게 됩니다.

 

by 키워드를 이용한 Delegate Pattern 예제

위에서 직접 Delegate Pattern을 구현하고자 형식적인(boilerplate) 코드를 직접 작성하였습니다. 코틀린은 by 키워드로 Delegate Pattern을 쉽게 구현하도록 도와주며 이 과정에서 boilerplate code를 생략할 수 있습니다.

interface IBase {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : IBase {
    override fun printMessage() {
        print(x)
    }

    override fun printMessageLine() {
        println(x)
    }
}

class MessageService(val base: IBase) : IBase by base {
    
}

위 코드를 확인했을 때 MessageService 클래스는 거의 동일하지만 IBase by base 부분이 다른걸 확인할 수 있습니다. by 키워드는 MessageService의 생성자로 IBase 인터페이스의 구현체를 전달받아 Delegate Pattern 코드를 자동으로 작성해줍니다.

fun main() {
    val messageService = MessageService(BaseImpl(10))
    messageService.printMessage() // 10
    messageService.printMessageLine() // 10\n
}

위 코드가 정상적으로 수행되므로 MessageService가 상속받은 IBase의 인터페이스를 구현하지 않아도 생성자로 주입받은 base 프로퍼티가 역할을 위임받아 수행하게 됩니다.