Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 이펙티브자바
- 알고리즘정렬
- Kotlin
- kubernetes
- 함수형프로그래밍
- 스프링핵심원리
- 스프링
- 이펙티브 자바
- 김영한
- 스프링 핵심원리
- 알고리즘
- Spring
- 티스토리챌린지
- JavaScript
- Sort
- Effective Java 3
- 예제로 배우는 스프링 입문
- Effective Java
- 스프링부트
- ElasticSearch
- 자바스크립트
- java
- 자바
- k8s
- 클린아키텍처
- 엘라스틱서치
- 이차전지관련주
- 오블완
- effectivejava
- 카카오
Archives
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin Delegation 완벽 가이드 | by 키워드로 보일러플레이트 제거하기 본문
반응형
이 글을 읽으면: Java의 반복적인 위임 코드를 Kotlin의 by 키워드로 한 줄로 해결하는 방법을 배울 수 있습니다. 클래스 위임, 프로퍼티 위임, 커스텀 위임까지 실전 예제로 완벽하게 마스터하세요.
📌 목차
- 들어가며 - Java 위임 패턴의 불편함
- 클래스 위임 - by 키워드
- 프로퍼티 위임 - lazy, observable
- Map 위임 - 동적 프로퍼티
- 커스텀 위임 프로퍼티
- 실전 위임 패턴
- 마무리 - 다음 편 예고

들어가며 - 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: 반복적인 패턴이 있고, 재사용 가능한 로직일 때 만듭니다.
관련 글
💬 댓글로 알려주세요!
- 어떤 위임 패턴을 자주 사용하시나요?
- 커스텀 위임을 만들어본 경험이 있나요?
- 이 글이 도움이 되셨나요?
반응형
'개발 > java basic' 카테고리의 다른 글
| Kotlin 상속과 인터페이스 완벽 가이드 | open, abstract, sealed (1) | 2025.12.27 |
|---|---|
| Kotlin 제네릭스 완벽 가이드 | in, out, reified 마스터하기 (0) | 2025.12.27 |
| Kotlin 람다와 고차 함수 완벽 가이드 | 함수형 프로그래밍 심화 (0) | 2025.12.26 |
| Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍 (1) | 2025.12.24 |
| Kotlin Null Safety 완벽 가이드 | ?, ?., ?:, !! 마스터하기 (0) | 2025.12.23 |
Comments
