
코틀린 delegation을 이해할 때 가장 먼저 보게 되는 장면이 class Derived(b: Base) : Base by b 같은 문장입니다.
처음 보면 문법이 짧아서 편해 보이지만, 왜 이 문법이 필요한지 감이 바로 오지는 않습니다.
핵심은 간단합니다.
delegation은 상속을 더 화려하게 만드는 문법이 아니라, 조합으로 협력시키는 코드를 덜 장황하게 만드는 문법입니다.
이번 글에서는 by 문법이 실제로 무엇을 대신해 주는지, 상속보다 위임이 더 읽기 쉬워지는 순간은 언제인지, 그리고 override를 섞을 때 어디서 많이 헷갈리는지까지 단계적으로 정리하겠습니다.
코틀린 delegation을 한 줄로 먼저 이해하기


Kotlin 공식 문서는 delegation pattern이 implementation inheritance의 좋은 대안이 될 수 있다고 설명합니다.
이 말의 뜻은 “상속을 다 버리자”가 아닙니다.
더 정확히 말하면, 인터페이스 구현을 다른 객체에 맡기는 구조를 언어 차원에서 짧게 쓸 수 있다는 뜻입니다.
보통 위임 구조를 직접 쓰면 이런 전달 코드가 반복됩니다.
interface Printer {
fun print(message: String)
}
class ConsolePrinter : Printer {
override fun print(message: String) {
println(message)
}
}
class LoggingPrinter(
private val delegate: Printer,
) : Printer {
override fun print(message: String) {
println("[log] start")
delegate.print(message)
println("[log] end")
}
}이 구조 자체는 나쁘지 않습니다.
문제는 메서드가 많아질수록 전달 코드가 계속 늘어난다는 점입니다.
delegation은 바로 이 반복을 줄여 줍니다.
by 문법은 무엇을 대신해 줄까
코틀린의 interface delegation은 컴파일러가 전달 메서드를 대신 만들어 줍니다.
interface Printer {
fun print(message: String)
fun close()
}
class ConsolePrinter : Printer {
override fun print(message: String) {
println(message)
}
override fun close() {
println("closed")
}
}
class LoggingPrinter(
private val inner: Printer,
) : Printer by inner {
override fun print(message: String) {
println("[log] before")
inner.print(message)
println("[log] after")
}
}여기서 close()는 직접 구현하지 않았지만 Printer by inner 덕분에 자동으로 inner.close()에 전달됩니다.
즉 by 문법은 내가 굳이 건드리지 않을 기본 전달 메서드를 자동 생성해 주고, 내가 바꾸고 싶은 일부만 override하게 도와줍니다.
상속보다 위임이 더 읽기 쉬워지는 순간
상속은 “이 타입은 저 타입의 한 종류인가”를 표현할 때 잘 맞습니다.
반면 delegation은 “이 객체는 저 객체의 기능을 빌려서 일부만 덧붙이는가”를 표현할 때 더 잘 맞습니다.
예를 들어 로깅, 권한 검사, 캐시, 측정, 포맷 변환 같은 기능은 원래 기능 위에 얹히는 경우가 많습니다.
이런 상황에서 상속으로 계속 파생 클래스를 늘리면 종류 분류와 부가 기능 확장이 섞여서 코드가 무거워집니다.
delegation을 쓰면 관심사가 더 잘 보입니다.
- 기본 기능은 원래 구현체가 담당한다
- 부가 기능은 래퍼 객체가 담당한다
- 둘의 협력은 인터페이스로 맞춘다
이 구조는 Decorator, Proxy, Bridge 같은 패턴을 이해할 때도 매우 유용합니다.
delegation이 특히 잘 맞는 장면
여기서 하나 더 구분해야 할 것이 있습니다. 코틀린에서 delegation이라고 할 때 많은 사람이 by lazy, Delegates.observable 같은 property delegation과 지금 설명하는 interface delegation을 한 덩어리로 기억합니다. 둘 다 by를 쓰지만, 하나는 인터페이스 구현 전달이고 다른 하나는 프로퍼티 접근 제어라는 점에서 목적이 다릅니다.
즉 이번 글의 중심은 “행위를 다른 객체에 맡기는 interface delegation”입니다. 같은 by 문법이라도 역할이 다르다는 점을 분리해 두면 훨씬 덜 헷갈립니다.
delegation이 자주 빛나는 장면은 아래와 같습니다.
- 인터페이스 메서드가 많은데 일부만 덧붙이고 싶을 때
- 상속 계층을 늘리기보다 래퍼 조합으로 기능을 붙이고 싶을 때
- 구현 재사용은 하고 싶지만 “is-a” 관계는 어색할 때
- 테스트에서 가짜 구현이나 감시 래퍼를 빠르게 만들고 싶을 때
예를 들어 결제 SDK 호출기, 메시지 전송기, 저장소 래퍼, 네트워크 클라이언트 래퍼 같은 곳에서 자주 잘 맞습니다.
override를 섞을 때 가장 많이 헷갈리는 부분
Kotlin 공식 문서가 강조하는 중요한 포인트가 하나 있습니다.
delegate 객체 내부에서 자기 메서드를 호출할 때는, 바깥 래퍼에서 override한 구현이 자동으로 다시 끼어들지 않습니다.
이 부분이 처음에는 꽤 헷갈립니다.
interface Base {
val message: String
fun print()
}
class BaseImpl(x: Int) : Base {
override val message = "BaseImpl: x = $x"
override fun print() {
println(message)
}
}
class Derived(b: Base) : Base by b {
override val message: String = "Message of Derived"
}Derived.print()를 호출해도 BaseImpl.print() 안에서는 BaseImpl 자신의 message를 봅니다.
즉 “밖에서 override했으니 delegate 내부 호출도 바뀌겠지”라고 기대하면 틀릴 수 있습니다.
이 점은 delegation을 마법처럼 생각하지 않게 해 주는 중요한 경계입니다.
delegation이 설계를 대신해 주는 것은 아니다
by 문법이 편리하다고 해서 자동으로 좋은 구조가 되는 것은 아닙니다.
예를 들어 아래 질문은 여전히 사람이 판단해야 합니다.
- 정말 위임이 맞는 상황인가
- 인터페이스 경계가 자연스러운가
- 래퍼가 너무 많은 책임을 먹고 있지 않은가
- 단순 전달만 하는 껍데기가 과하게 늘어나고 있지 않은가
delegation은 좋은 구조를 더 짧게 적는 도구이지, 구조 판단 자체를 대신해 주는 도구는 아닙니다.
상속, 조합, delegation을 어떻게 구분하면 좋을까
실무에서는 이렇게 생각하면 판단이 빨라집니다.
상속이 더 자연스러운 경우
- 진짜로 하위 타입 관계가 분명할 때
- 부모 타입의 개념을 자식이 그대로 이어받을 때
- 행위 확장이 아니라 타입 분류가 중심일 때
조합이 더 자연스러운 경우
- 기능을 붙였다 뗐다 해야 할 때
- 역할 교체 가능성이 있을 때
- 구현 재사용은 필요하지만 계층 상속은 어색할 때
delegation이 특히 좋은 경우
- 조합을 쓰기로 이미 결정했는데 전달 메서드가 너무 많을 때
- 인터페이스를 그대로 노출하면서 일부 동작만 바꾸고 싶을 때
- 래퍼 객체를 여러 개 붙이는 구조를 Kotlin답게 간결하게 쓰고 싶을 때
작은 실전 예시로 감각 잡기
저장소 인터페이스가 있다고 해보겠습니다.
interface UserRepository {
fun findById(id: Long): String?
fun save(name: String)
}
class DefaultUserRepository : UserRepository {
private val store = mutableMapOf<Long, String>()
override fun findById(id: Long): String? = store[id]
override fun save(name: String) {
store[store.size.toLong() + 1] = name
}
}
class LoggingUserRepository(
private val inner: UserRepository,
) : UserRepository by inner {
override fun findById(id: Long): String? {
println("findById: $id")
return inner.findById(id)
}
}여기서 save()는 기본 동작을 그대로 쓰고, findById()만 가볍게 감시하고 싶다면 delegation이 아주 잘 맞습니다.
직접 모든 메서드를 포워딩할 필요가 없고, 독자도 “이 클래스는 저장소를 대체하는 새 종류라기보다 감싼 래퍼구나”라고 바로 읽을 수 있습니다.
언제 오히려 과할까
모든 상황에 delegation을 넣으면 오히려 복잡해질 수 있습니다.
특히 아래 경우는 과할 수 있습니다.
- 인터페이스 메서드가 1~2개뿐이라 전달 코드가 거의 없을 때
- 래퍼가 하나도 아니고 둘도 아니고 계속 중첩되어 디버깅이 불편해질 때
- 실제로는 위임보다 명확한 별도 구현이 더 읽기 쉬울 때
짧게 말하면, delegation은 보일러플레이트가 문제일 때 강하고, 구조 자체가 불명확할 때는 해답이 아닙니다.
정리
코틀린 delegation을 이해할 때 가장 중요한 포인트는 세 가지입니다.
by문법은 인터페이스 구현 전달 코드를 컴파일러가 대신 만들어 준다- 상속보다 위임이 읽기 쉬운 장면은 기능 덧붙이기와 조합 중심 확장이다
- delegate 내부 호출과 override의 경계는 자동으로 마법처럼 합쳐지지 않는다
그래서 delegation은 “상속을 없애는 코틀린 신기능”보다, 조합 중심 설계를 Kotlin답게 짧게 쓰게 해 주는 실용 도구로 이해하는 편이 훨씬 오래 남습니다.
같이 보면 좋은 글은 상속보다 조합이 더 나은 순간, 코틀린 Decorator 패턴, 코틀린 Bridge 패턴입니다.

관련 맥락으로는 상속보다 조합이 더 나은 순간, 코틀린 Decorator 패턴 글을 함께 보면 delegation의 위치가 더 또렷해집니다.
공식 참고 자료로는 Kotlin Delegation Documentation, Kotlin Interfaces Documentation, Refactoring.Guru – Decorator를 함께 보면 좋습니다.