Kim-Baek 개발자 이야기

Kotlin Delegation 완벽 가이드 | by 키워드로 보일러플레이트 제거하기 본문

개발/java basic

Kotlin Delegation 완벽 가이드 | by 키워드로 보일러플레이트 제거하기

김백개발자 2025. 12. 28. 09:44
반응형

이 글을 읽으면: Java의 반복적인 위임 코드를 Kotlin의 by 키워드로 한 줄로 해결하는 방법을 배울 수 있습니다. 클래스 위임, 프로퍼티 위임, 커스텀 위임까지 실전 예제로 완벽하게 마스터하세요.


📌 목차

  1. 들어가며 - Java 위임 패턴의 불편함
  2. 클래스 위임 - by 키워드
  3. 프로퍼티 위임 - lazy, observable
  4. Map 위임 - 동적 프로퍼티
  5. 커스텀 위임 프로퍼티
  6. 실전 위임 패턴
  7. 마무리 - 다음 편 예고

들어가며 - Java 위임 패턴의 불편함

Java로 위임 패턴을 구현할 때 이런 경험 있으신가요?

// Java - 수동 위임 (지루하고 반복적)
interface Repository {
    void save(String data);
    String load();
    void delete();
}

class DatabaseRepository implements Repository {
    public void save(String data) {
        System.out.println("DB 저장: " + data);
    }
    
    public String load() {
        return "DB 데이터";
    }
    
    public void delete() {
        System.out.println("DB 삭제");
    }
}

// 위임 클래스 - 모든 메서드를 수동으로 전달
class CachedRepository implements Repository {
    private final Repository delegate;
    
    public CachedRepository(Repository delegate) {
        this.delegate = delegate;
    }
    
    public void save(String data) {
        System.out.println("캐시 업데이트");
        delegate.save(data);  // 수동 위임
    }
    
    public String load() {
        System.out.println("캐시 확인");
        return delegate.load();  // 수동 위임
    }
    
    public void delete() {
        System.out.println("캐시 삭제");
        delegate.delete();  // 수동 위임
    }
}

실제 프로젝트에서 겪은 문제:

상황: 기존 클래스에 로깅 기능 추가
문제: 20개 메서드를 모두 수동으로 위임 코드 작성
결과: 500줄의 보일러플레이트 코드 발생

Kotlin의 해결책:

// Kotlin - by 키워드로 한 줄 해결
interface Repository {
    fun save(data: String)
    fun load(): String
    fun delete()
}

class DatabaseRepository : Repository {
    override fun save(data: String) = println("DB 저장: $data")
    override fun load() = "DB 데이터"
    override fun delete() = println("DB 삭제")
}

// 자동 위임 - 보일러플레이트 제로!
class CachedRepository(
    private val delegate: Repository
) : Repository by delegate {
    // 필요한 메서드만 오버라이드
    override fun save(data: String) {
        println("캐시 업데이트")
        delegate.save(data)
    }
}

fun main() {
    val repo = CachedRepository(DatabaseRepository())
    repo.save("데이터")
    // 캐시 업데이트
    // DB 저장: 데이터
    
    println(repo.load())  // 자동 위임
    // DB 데이터
}

오늘은 Kotlin의 위임을 완전히 마스터해보겠습니다!


클래스 위임 - by 키워드

기본 클래스 위임

interface Printer {
    fun print(message: String)
    fun printBold(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println(message)
    }
    
    override fun printBold(message: String) {
        println("**$message**")
    }
}

// by로 모든 메서드 자동 위임
class Logger(printer: Printer) : Printer by printer

fun main() {
    val logger = Logger(ConsolePrinter())
    
    logger.print("일반 메시지")        // 일반 메시지
    logger.printBold("굵은 메시지")    // **굵은 메시지**
}

일부 메서드만 오버라이드

interface DataSource {
    fun read(): String
    fun write(data: String)
    fun close()
}

class FileDataSource : DataSource {
    override fun read() = "파일 데이터"
    override fun write(data: String) = println("파일 쓰기: $data")
    override fun close() = println("파일 닫기")
}

class CachedDataSource(
    private val source: DataSource
) : DataSource by source {
    private var cache: String? = null
    
    // read만 오버라이드 (캐싱 추가)
    override fun read(): String {
        return cache ?: source.read().also { cache = it }
    }
    
    // write, close는 자동 위임
}

fun main() {
    val cached = CachedDataSource(FileDataSource())
    
    println(cached.read())   // 파일 데이터 (캐시 없음)
    println(cached.read())   // 파일 데이터 (캐시 사용)
    
    cached.write("새 데이터")  // 파일 쓰기: 새 데이터 (자동 위임)
    cached.close()            // 파일 닫기 (자동 위임)
}

실전 예제 - 데코레이터 패턴

interface Coffee {
    fun cost(): Int
    fun description(): String
}

class SimpleCoffee : Coffee {
    override fun cost() = 1000
    override fun description() = "커피"
}

class MilkDecorator(coffee: Coffee) : Coffee by coffee {
    override fun cost() = coffee.cost() + 500
    override fun description() = "${coffee.description()} + 우유"
}

class SugarDecorator(coffee: Coffee) : Coffee by coffee {
    override fun cost() = coffee.cost() + 200
    override fun description() = "${coffee.description()} + 설탕"
}

fun main() {
    val coffee = SimpleCoffee()
    println("${coffee.description()}: ${coffee.cost()}원")
    // 커피: 1000원
    
    val milkCoffee = MilkDecorator(coffee)
    println("${milkCoffee.description()}: ${milkCoffee.cost()}원")
    // 커피 + 우유: 1500원
    
    val sweetMilkCoffee = SugarDecorator(MilkDecorator(coffee))
    println("${sweetMilkCoffee.description()}: ${sweetMilkCoffee.cost()}원")
    // 커피 + 우유 + 설탕: 1700원
}

실전 예제 - 로깅 래퍼

interface UserService {
    fun createUser(name: String): User
    fun getUser(id: Long): User?
    fun deleteUser(id: Long)
}

data class User(val id: Long, val name: String)

class UserServiceImpl : UserService {
    private val users = mutableMapOf<Long, User>()
    private var nextId = 1L
    
    override fun createUser(name: String): User {
        val user = User(nextId++, name)
        users[user.id] = user
        return user
    }
    
    override fun getUser(id: Long) = users[id]
    
    override fun deleteUser(id: Long) {
        users.remove(id)
    }
}

class LoggingUserService(
    private val service: UserService
) : UserService by service {
    
    override fun createUser(name: String): User {
        println("[LOG] 사용자 생성 시작: $name")
        val user = service.createUser(name)
        println("[LOG] 사용자 생성 완료: $user")
        return user
    }
    
    override fun deleteUser(id: Long) {
        println("[LOG] 사용자 삭제 시작: $id")
        service.deleteUser(id)
        println("[LOG] 사용자 삭제 완료: $id")
    }
    
    // getUser는 자동 위임
}

fun main() {
    val service = LoggingUserService(UserServiceImpl())
    
    val user = service.createUser("규철")
    // [LOG] 사용자 생성 시작: 규철
    // [LOG] 사용자 생성 완료: User(id=1, name=규철)
    
    println(service.getUser(1))  // 자동 위임
    // User(id=1, name=규철)
    
    service.deleteUser(1)
    // [LOG] 사용자 삭제 시작: 1
    // [LOG] 사용자 삭제 완료: 1
}

프로퍼티 위임 - lazy, observable

lazy - 지연 초기화

class HeavyObject {
    init {
        println("무거운 객체 생성 중...")
        Thread.sleep(1000)
    }
    
    fun doWork() = println("작업 수행")
}

class Service {
    // 처음 사용할 때까지 초기화 안 함
    val heavyObject: HeavyObject by lazy {
        println("lazy 초기화 시작")
        HeavyObject()
    }
}

fun main() {
    println("Service 생성")
    val service = Service()
    
    println("첫 번째 접근")
    service.heavyObject.doWork()
    // lazy 초기화 시작
    // 무거운 객체 생성 중...
    // 작업 수행
    
    println("두 번째 접근")
    service.heavyObject.doWork()
    // 작업 수행 (초기화 안 함)
}

observable - 값 변경 감지

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("초기값") { property, oldValue, newValue ->
        println("${property.name} 변경: $oldValue -> $newValue")
    }
    
    var age: Int by Delegates.observable(0) { _, old, new ->
        if (new != old) {
            println("나이 변경: $old -> $new")
        }
    }
}

fun main() {
    val user = User()
    
    user.name = "규철"
    // name 변경: 초기값 -> 규철
    
    user.name = "영희"
    // name 변경: 규철 -> 영희
    
    user.age = 30
    // 나이 변경: 0 -> 30
    
    user.age = 31
    // 나이 변경: 30 -> 31
}

vetoable - 값 변경 검증

import kotlin.properties.Delegates

class Product {
    var price: Int by Delegates.vetoable(0) { _, old, new ->
        new >= 0  // 음수면 변경 거부
    }
    
    var stock: Int by Delegates.vetoable(0) { _, _, new ->
        new in 0..1000  // 0~1000 범위만 허용
    }
}

fun main() {
    val product = Product()
    
    product.price = 10000
    println("가격: ${product.price}")  // 10000
    
    product.price = -5000  // 거부됨
    println("가격: ${product.price}")  // 10000 (변경 안 됨)
    
    product.stock = 500
    println("재고: ${product.stock}")  // 500
    
    product.stock = 2000  // 거부됨
    println("재고: ${product.stock}")  // 500 (변경 안 됨)
}

실전 예제 - 설정 관리

import kotlin.properties.Delegates

class AppConfig {
    var maxRetry: Int by Delegates.vetoable(3) { _, _, new ->
        new in 1..10
    }
    
    var timeout: Long by Delegates.observable(5000L) { _, old, new ->
        println("타임아웃 변경: ${old}ms -> ${new}ms")
        // 설정 파일에 저장
    }
    
    val apiKey: String by lazy {
        println("API 키 로드 중...")
        // 파일이나 환경변수에서 읽기
        "secret-api-key-12345"
    }
}

fun main() {
    val config = AppConfig()
    
    config.maxRetry = 5
    println("재시도 횟수: ${config.maxRetry}")  // 5
    
    config.maxRetry = 20  // 거부됨 (1~10 범위 벗어남)
    println("재시도 횟수: ${config.maxRetry}")  // 5
    
    config.timeout = 10000
    // 타임아웃 변경: 5000ms -> 10000ms
    
    println(config.apiKey)
    // API 키 로드 중...
    // secret-api-key-12345
}

Map 위임 - 동적 프로퍼티

기본 Map 위임

class User(map: Map<String, Any>) {
    val name: String by map
    val age: Int by map
    val email: String by map
}

fun main() {
    val user = User(mapOf(
        "name" to "규철",
        "age" to 30,
        "email" to "user@example.com"
    ))
    
    println(user.name)   // 규철
    println(user.age)    // 30
    println(user.email)  // user@example.com
}

가변 Map 위임

class MutableUser(map: MutableMap<String, Any>) {
    var name: String by map
    var age: Int by map
}

fun main() {
    val map = mutableMapOf<String, Any>(
        "name" to "규철",
        "age" to 30
    )
    
    val user = MutableUser(map)
    
    println(user.name)  // 규철
    
    user.name = "영희"
    println(user.name)  // 영희
    println(map)        // {name=영희, age=30}
}

실전 예제 - JSON 파싱

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken

class JsonObject(private val map: Map<String, Any?>) {
    val id: Long by map
    val title: String by map
    val completed: Boolean by map
}

fun main() {
    val json = """
        {
            "id": 1,
            "title": "Kotlin 공부하기",
            "completed": false
        }
    """
    
    val type = object : TypeToken<Map<String, Any?>>() {}.type
    val map: Map<String, Any?> = Gson().fromJson(json, type)
    
    val obj = JsonObject(map)
    println("ID: ${obj.id}")              // ID: 1
    println("제목: ${obj.title}")          // 제목: Kotlin 공부하기
    println("완료: ${obj.completed}")      // 완료: false
}

실전 예제 - 설정 파일 파싱

class DatabaseConfig(properties: Map<String, String>) {
    val host: String by properties
    val port: Int by lazy { 
        properties["port"]?.toInt() ?: 5432 
    }
    val username: String by properties
    val password: String by properties
    val database: String by properties
    
    fun getConnectionString(): String {
        return "postgresql://$username:$password@$host:$port/$database"
    }
}

fun main() {
    val props = mapOf(
        "host" to "localhost",
        "port" to "5432",
        "username" to "admin",
        "password" to "secret",
        "database" to "mydb"
    )
    
    val config = DatabaseConfig(props)
    println(config.getConnectionString())
    // postgresql://admin:secret@localhost:5432/mydb
}

커스텀 위임 프로퍼티

ReadOnlyProperty 구현

import kotlin.reflect.KProperty

class UpperCaseDelegate {
    private var value: String = ""
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value.uppercase()
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        value = newValue
    }
}

class User {
    var name: String by UpperCaseDelegate()
}

fun main() {
    val user = User()
    user.name = "kotlin"
    println(user.name)  // KOTLIN
    
    user.name = "java"
    println(user.name)  // JAVA
}

실전 예제 - NotNull 위임

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

class NotNullDelegate<T : Any> : ReadWriteProperty<Any?, T> {
    private var value: T? = null
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("${property.name}이 초기화되지 않았습니다")
    }
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        this.value = value
    }
}

fun <T : Any> notNull() = NotNullDelegate<T>()

class Configuration {
    var apiKey: String by notNull()
    var timeout: Int by notNull()
}

fun main() {
    val config = Configuration()
    
    try {
        println(config.apiKey)  // 예외 발생
    } catch (e: IllegalStateException) {
        println(e.message)  // apiKey이 초기화되지 않았습니다
    }
    
    config.apiKey = "secret-key"
    println(config.apiKey)  // secret-key
}

실전 예제 - 검증 위임

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

class ValidatedDelegate<T>(
    private val validator: (T) -> Boolean,
    private val errorMessage: String
) : ReadWriteProperty<Any?, T> {
    private var value: T? = null
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value ?: throw IllegalStateException("값이 설정되지 않았습니다")
    }
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        if (!validator(newValue)) {
            throw IllegalArgumentException(errorMessage)
        }
        value = newValue
    }
}

fun <T> validated(errorMessage: String, validator: (T) -> Boolean) =
    ValidatedDelegate(validator, errorMessage)

class User {
    var email: String by validated("올바른 이메일 형식이 아닙니다") {
        it.contains("@")
    }
    
    var age: Int by validated("나이는 0~150 사이여야 합니다") {
        it in 0..150
    }
}

fun main() {
    val user = User()
    
    try {
        user.email = "invalid-email"
    } catch (e: IllegalArgumentException) {
        println(e.message)  // 올바른 이메일 형식이 아닙니다
    }
    
    user.email = "user@example.com"
    println(user.email)  // user@example.com
    
    user.age = 30
    println(user.age)  // 30
    
    try {
        user.age = 200
    } catch (e: IllegalArgumentException) {
        println(e.message)  // 나이는 0~150 사이여야 합니다
    }
}

실전 예제 - 로깅 위임

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

class LoggingDelegate<T>(private var value: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("[GET] ${property.name} = $value")
        return value
    }
    
    override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        println("[SET] ${property.name}: $value -> $newValue")
        value = newValue
    }
}

fun <T> logged(initialValue: T) = LoggingDelegate(initialValue)

class ViewModel {
    var username: String by logged("")
    var isLoading: Boolean by logged(false)
}

fun main() {
    val viewModel = ViewModel()
    
    viewModel.username = "규철"
    // [SET] username:  -> 규철
    
    println(viewModel.username)
    // [GET] username = 규철
    // 규철
    
    viewModel.isLoading = true
    // [SET] isLoading: false -> true
    
    println(viewModel.isLoading)
    // [GET] isLoading = true
    // true
}

실전 패턴 모음

패턴 1: 프록시 패턴

interface Image {
    fun display()
}

class RealImage(private val filename: String) : Image {
    init {
        loadFromDisk()
    }
    
    private fun loadFromDisk() {
        println("$filename 로딩 중...")
        Thread.sleep(1000)
    }
    
    override fun display() {
        println("$filename 표시")
    }
}

class ProxyImage(private val filename: String) : Image {
    private val realImage: RealImage by lazy {
        RealImage(filename)
    }
    
    override fun display() {
        realImage.display()
    }
}

fun main() {
    println("프록시 이미지 생성")
    val image = ProxyImage("photo.jpg")
    
    println("\n첫 번째 display")
    image.display()
    // photo.jpg 로딩 중...
    // photo.jpg 표시
    
    println("\n두 번째 display")
    image.display()
    // photo.jpg 표시 (로딩 안 함)
}

패턴 2: 캐싱 위임

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

class CachedDelegate<T>(private val initializer: () -> T) : ReadOnlyProperty<Any?, T> {
    private var cache: T? = null
    private var lastUpdate = 0L
    private val ttl = 5000L  // 5초
    
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        val now = System.currentTimeMillis()
        
        if (cache == null || now - lastUpdate > ttl) {
            println("캐시 갱신")
            cache = initializer()
            lastUpdate = now
        } else {
            println("캐시 사용")
        }
        
        return cache!!
    }
}

fun <T> cached(initializer: () -> T) = CachedDelegate(initializer)

class ApiClient {
    val users: List<String> by cached {
        println("API 호출 중...")
        Thread.sleep(1000)
        listOf("규철", "영희", "철수")
    }
}

fun main() {
    val client = ApiClient()
    
    println(client.users)
    // 캐시 갱신
    // API 호출 중...
    // [규철, 영희, 철수]
    
    println(client.users)
    // 캐시 사용
    // [규철, 영희, 철수]
    
    Thread.sleep(6000)
    
    println(client.users)
    // 캐시 갱신
    // API 호출 중...
    // [규철, 영희, 철수]
}

패턴 3: 변경 추적 위임

import kotlin.properties.Delegates

class Entity {
    private var isDirty = false
    
    var name: String by Delegates.observable("") { _, old, new ->
        if (old != new) isDirty = true
    }
    
    var email: String by Delegates.observable("") { _, old, new ->
        if (old != new) isDirty = true
    }
    
    fun hasChanges() = isDirty
    
    fun save() {
        if (isDirty) {
            println("변경사항 저장: name=$name, email=$email")
            isDirty = false
        } else {
            println("변경사항 없음")
        }
    }
}

fun main() {
    val entity = Entity()
    
    entity.save()  // 변경사항 없음
    
    entity.name = "규철"
    entity.email = "user@example.com"
    
    println("변경됨? ${entity.hasChanges()}")  // true
    
    entity.save()
    // 변경사항 저장: name=규철, email=user@example.com
    
    entity.save()  // 변경사항 없음
}

마무리 - 다음 편 예고

오늘 배운 것 ✅

  • 클래스 위임 - by로 자동 위임
  • 프로퍼티 위임 - lazy, observable, vetoable
  • Map 위임 - 동적 프로퍼티
  • 커스텀 위임 - ReadWriteProperty 구현
  • 실전 패턴 - 프록시, 캐싱, 변경 추적

다음 편에서 배울 것 📚

13편: 예외 처리 완벽 가이드 | try-catch, runCatching, Result

  • try-catch-finally 기본
  • 체크 예외가 없는 Kotlin
  • runCatching - 함수형 예외 처리
  • Result 타입 활용
  • Custom Exception 설계
  • 실전 에러 핸들링 패턴

실습 과제 💪

// 1. 클래스 위임 활용
// - 트랜잭션 래퍼
// - 성능 측정 래퍼

// 2. 프로퍼티 위임 만들기
// - 암호화 위임
// - 포맷팅 위임

// 3. 커스텀 위임
// - 범위 검증 위임
// - 타입 변환 위임

// 4. 실전 패턴
// - 지연 로딩 캐시
// - 변경 추적 시스템

자주 묻는 질문 (FAQ)

Q: 클래스 위임과 상속의 차이는?
A: 상속은 "is-a" 관계, 위임은 "has-a" 관계입니다. 위임이 더 유연하고 안전합니다.

Q: lazy는 언제 사용하나요?
A: 초기화 비용이 크고, 사용되지 않을 수도 있는 프로퍼티에 사용합니다.

Q: observable vs vetoable 차이는?
A: observable은 변경 후 알림, vetoable은 변경 전 검증입니다.

Q: 커스텀 위임은 언제 만드나요?
A: 반복적인 패턴이 있고, 재사용 가능한 로직일 때 만듭니다.


관련 글


💬 댓글로 알려주세요!

  • 어떤 위임 패턴을 자주 사용하시나요?
  • 커스텀 위임을 만들어본 경험이 있나요?
  • 이 글이 도움이 되셨나요?

 

반응형
Comments