Kotlin에서 by 키워드는 위임(delegation)을 쉽게 구현할 수 있도록 지원합니다. 위임은 클래스나 인터페이스의 기능을 다른 객체에 넘겨 실행하게 만드는 디자인 패턴입니다
위임(delegation)
// 1. 인터페이스 정의
interface Greeter {
fun greet(): String
}
// 2. 구현 클래스
class EnglishGreeter : Greeter {
override fun greet(): String = "Hello!"
}
// 3. 위임 클래스
class GreeterDelegate(private val greeter: Greeter) : Greeter {
override fun greet(): String = greeter.greet()
}
// 사용
fun main() {
val englishGreeter = EnglishGreeter()
val greeter = GreeterDelegate(englishGreeter)
println(greeter.greet()) // 출력: Hello!
}
이 코드는 GreeterDelegate 클래스가 EnglishGreeter의 기능을 위임받아 실행합니다.
Kotlin에서는 위 코드를 by 키워드를 활용해 간단히 작성할 수 있습니다.
// 1. 인터페이스 정의
interface Greeter {
fun greet(): String
}
// 2. 구현 클래스
class EnglishGreeter : Greeter {
override fun greet(): String = "Hello!"
}
// 3. by 키워드를 활용한 위임
class GreeterDelegate(private val greeter: Greeter) : Greeter by greeter
// 사용
fun main() {
val englishGreeter = EnglishGreeter()
val greeter = GreeterDelegate(englishGreeter)
println(greeter.greet()) // 출력: Hello!
}
by 키워드를 사용하면 GreeterDelegate에 greet 메서드를 다시 구현할 필요가 없습니다. Greeter의 모든 구현이 englishGreeter로 자동 위임됩니다.
위임 패턴을 사용해 추가적인 기능을 쉽게 구현할 수 있습니다.
interface DataSource {
fun readData(): String
fun writeData(data: String)
}
class FileDataSource : DataSource {
override fun readData(): String = "파일에서 데이터를 읽음"
override fun writeData(data: String) {
println("파일에 데이터를 저장: $data")
}
}
// 암호화를 위한 위임
class EncryptedDataSource(dataSource: DataSource) : DataSource by dataSource {
override fun writeData(data: String) {
val encrypted = "암호화된 $data"
println("암호화 후 저장: $encrypted")
dataSource.writeData(encrypted)
}
override fun readData(): String {
val data = dataSource.readData()
return "복호화된 $data"
}
}
// 사용
fun main() {
val fileDataSource = FileDataSource()
val encryptedDataSource = EncryptedDataSource(fileDataSource)
encryptedDataSource.writeData("중요한 정보")
println(encryptedDataSource.readData())
/*
출력:
암호화 후 저장: 암호화된 중요한 정보
파일에 데이터를 저장: 암호화된 중요한 정보
복호화된 파일에서 데이터를 읽음
*/
}
Kotlin에서 by 키워드는 Jetpack Compose와 같은 최신 기술에서도 자주 사용됩니다. Compose에서 by는 주로 상태관리나 지연초기화를 효율적으로 처리하는 데 사용됩니다.
Jetpack Compose에서는 StateFlow나 LiveData를 Compose 상태로 변환하기 위해 by와 collectAsState를 함께 사용하는 경우가 많습니다. 이 패턴은 Compose UI를 상태 변화에 반응하도록 만듭니다.
@Composable
fun MyScreen(viewModel: MyViewModel) {
// StateFlow를 Compose의 상태로 변환
val uiState by viewModel.uiState.collectAsState()
// UI 렌더링
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> Text("Data: ${(uiState as UiState.Success).data}")
is UiState.Error -> Text("Error: ${(uiState as UiState.Error).message}")
}
}
만약 by를 사용하지 않으면 아래와 같이 작성해야 합니다.
val uiState = viewModel.uiState.collectAsState().value
Compose에서도 by lazy를 활용해 초기화 비용이 높은 객체를 필요할 때만 생성하는 방식으로 최적화할 수 있습니다.
by lazy를 활용한 데이터 초기화
class MyViewModel : ViewModel() {
// 데이터 초기화를 지연
val expensiveData: List<String> by lazy {
loadExpensiveData()
}
private fun loadExpensiveData(): List<String> {
// 무거운 작업 시뮬레이션
return listOf("Item1", "Item2", "Item3")
}
}
@Composable
fun MyScreen(viewModel: MyViewModel) {
val data = viewModel.expensiveData
LazyColumn {
items(data) { item ->
Text(item)
}
}
}
by lazy를 사용함으로써 expensiveData는 처음 호출될 때만 초기화되며 이후에는 캐싱된 데이터를 사용한다.
Compose의 remember는 컴포저블 함수 내에서 상태를 유지하기 위해 사용됩니다. by를 사용하면 상태를 간결하게 관리할 수 있습니다.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
remember는 count 상태를 컴포저블 함수가 재구성될 때 상태를 유지시킨다.
Compose에서 Delegates.observable은 상태 변화를 감지하고 추가 작업을 실행하는 데 사용할 수 있습니다.
import kotlin.properties.Delegates
class MyViewModel : ViewModel() {
var query: String by Delegates.observable("") { _, old, new ->
println("Query changed from '$old' to '$new'")
// 상태 변화에 따른 추가 작업
}
}
@Composable
fun SearchScreen(viewModel: MyViewModel) {
var query by remember { mutableStateOf("") }
TextField(
value = query,
onValueChange = {
query = it
viewModel.query = it
},
label = { Text("Search") }
)
}
Compose에서 by를 사용하여 Hilt 또는 Koin과 같은 DI라이브러리와 ViewModel을 결합할 수 있습니다.
@Composable
// hiltViewModel() -> Hilt에서 제공하는 함수로, Compose 내에서 ViewModel을 주입.
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
// by 키워드를 사용해 간결하게 ViewModel 주입
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> Text("Data loaded!")
else -> Text("Error!")
}
}
Compose에서 by 키워드는 다음과 같은 상황에서 자주 사용됩니다.
collectAsState와 함께 StateFlow나 LiveData를 상태로 변환remember와 mutableStateOf로 Compose 상태를 간결하게 관리by lazy로 데이터나 객체의 초기화를 지연Delegates.observable로 상태 변화를 감지하고 추가 작업 실행by viewModels 또는 hiltViewModel()로 ViewModel을 간단히 주입by는 Kotlin의 강력한 기능으로 Compose에서 코드의 가독성과 효율성을 높이는 데 필수적인 요소입니다.