Kotlin "by" 키워드와 위임 이해하기

JaeEun Lee·2024년 11월 23일

SwiftUI & Jetpack compose

목록 보기
9/10

Kotlin에서 by 키워드는 위임(delegation)을 쉽게 구현할 수 있도록 지원합니다. 위임은 클래스나 인터페이스의 기능을 다른 객체에 넘겨 실행하게 만드는 디자인 패턴입니다

위임의 기본개념

위임(delegation)

  • 클래스가 해야 할 작업을 다른 객체에게 위임하여 처리.
  • 코드 재사용성을 높이고 클래스가 여러 책임을 가지지 않도록 만듦.
    Kotlin의 "by"
  • 인터페이스의 구현을 다른 객체에게 넘기는 것을 간단히 처리.
  • 보일러플레이트 코드를 제거하고 코드 가독성을 향상.
// 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의 기능을 위임받아 실행합니다.

by를 활용한 간소화

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())
    /*
    출력:
    암호화 후 저장: 암호화된 중요한 정보
    파일에 데이터를 저장: 암호화된 중요한 정보
    복호화된 파일에서 데이터를 읽음
    */
}

요약

  • by 키워드는 인터페이스 구현을 다른 객체에 위임할 때 사용합니다.
  • 보일러플레이트 코드를 줄일 수 있음.
  • 클래스가 해야 할 작업을 다른 객체에 위임하여 책임을 분리
  • 코드 가독성과 유지보수성 향상
  • 추가 기능 삽입(로깅, 암호화 등)
  • 다양한 구현을 조합해 유연한 설계

Jetpack Compose에서의 활용

Kotlin에서 by 키워드는 Jetpack Compose와 같은 최신 기술에서도 자주 사용됩니다. Compose에서 by는 주로 상태관리지연초기화를 효율적으로 처리하는 데 사용됩니다.

by와 collectAsState

Jetpack Compose에서는 StateFlowLiveData를 Compose 상태로 변환하기 위해 bycollectAsState를 함께 사용하는 경우가 많습니다. 이 패턴은 Compose UI를 상태 변화에 반응하도록 만듭니다.

StateFlow와 collectAsState

@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

by lazy

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는 처음 호출될 때만 초기화되며 이후에는 캐싱된 데이터를 사용한다.

by remember

Compose의 remember는 컴포저블 함수 내에서 상태를 유지하기 위해 사용됩니다. by를 사용하면 상태를 간결하게 관리할 수 있습니다.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

remember는 count 상태를 컴포저블 함수가 재구성될 때 상태를 유지시킨다.

by와 Delegates.observable

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") }
    )
}

by와 ViewModel 의존성주입

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와 함께 StateFlowLiveData를 상태로 변환
    • remembermutableStateOf로 Compose 상태를 간결하게 관리
  • 지연 초기화
    • by lazy로 데이터나 객체의 초기화를 지연
  • 상태 변화 감지
    • Delegates.observable로 상태 변화를 감지하고 추가 작업 실행
  • 의존성 주입
    • by viewModels 또는 hiltViewModel()ViewModel을 간단히 주입

by는 Kotlin의 강력한 기능으로 Compose에서 코드의 가독성과 효율성을 높이는 데 필수적인 요소입니다.

profile
공업철학프로그래머

0개의 댓글