(Kotlin/코틀린) object expression (익명 객체) vs lambda 비교


1. object expression — 익명 객체

이름 없이 클래스를 즉석에서 정의하고 인스턴스를 생성합니다.

// 인터페이스를 익명 객체로 구현
interface ClickListener {
    fun onClick(view: View)
    fun onLongClick(view: View): Boolean
}

val listener = object : ClickListener {
    override fun onClick(view: View) {
        println("클릭: ${view.id}")
    }
    override fun onLongClick(view: View): Boolean {
        println("롱클릭: ${view.id}")
        return true
    }
}

button.setClickListener(listener)

2. lambda — 단일 메서드 함수

단일 메서드 인터페이스(함수형 인터페이스)를 간결하게 표현합니다.

// Java의 단일 메서드 인터페이스
button.setOnClickListener { view ->
    println("클릭: ${view.id}")
}

// Kotlin의 함수형 인터페이스 (fun interface)
fun interface Validator {
    fun validate(input: String): Boolean
}

val notBlank: Validator = Validator { it.isNotBlank() }
val minLength: Validator = Validator { it.length >= 8 }

3. SAM 변환 — Java 인터페이스와 lambda

Java의 단일 추상 메서드(SAM) 인터페이스는 Kotlin에서 lambda로 사용할 수 있습니다.

// Java: Runnable, Comparator, OnClickListener 등
val thread = Thread { println("백그라운드 실행") }
thread.start()

// Comparator (Java SAM)
val comparator = Comparator<String> { a, b -> a.length - b.length }

// Java OnClickListener
button.setOnClickListener { view ->
    // OnClickListener.onClick(view) 구현
}

Kotlin 인터페이스는 SAM 변환이 기본 지원되지 않습니다.
fun interface로 선언해야 lambda 사용이 가능합니다.

// Kotlin 일반 interface — SAM 변환 안 됨
interface KotlinCallback {
    fun onResult(result: String)
}
// val cb: KotlinCallback = { println(it) }  // ❌ 컴파일 에러

// fun interface — SAM 변환 가능
fun interface KotlinCallback {
    fun onResult(result: String)
}
val cb: KotlinCallback = { println(it) }  // ✅

4. 메서드 수에 따른 선택

// 메서드 1개 → lambda (또는 fun interface)
fun interface SingleMethod {
    fun execute(): String
}
val single: SingleMethod = { "결과" }

// 메서드 2개 이상 → object expression 필수
interface MultiMethod {
    fun onStart()
    fun onEnd()
    fun onError(e: Exception)
}
val handler = object : MultiMethod {
    override fun onStart() = println("시작")
    override fun onEnd() = println("종료")
    override fun onError(e: Exception) = println("에러: ${e.message}")
}

5. 외부 변수 캡처 비교

var count = 0

// lambda: 외부 var 캡처 가능
val incrementer = Runnable { count++ }
incrementer.run()
println(count)  // 1

// object expression: 외부 var 캡처 가능
val counter = object : Runnable {
    override fun run() { count++ }
}
counter.run()
println(count)  // 2

// object expression만의 특권: 자기 자신 참조
val selfReferencing = object : Runnable {
    var runCount = 0  // 자체 상태 보유
    override fun run() {
        runCount++
        println("${runCount}번째 실행")
    }
}
selfReferencing.run()  // 1번째 실행
selfReferencing.run()  // 2번째 실행

6. 실전 패턴 — Android Listener

// 메서드 1개: lambda 사용
binding.button.setOnClickListener {
    viewModel.onButtonClicked()
}

// 메서드 여러 개: object expression
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            viewModel.onScrollStopped()
        }
    }
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        if (!recyclerView.canScrollVertically(1)) {
            viewModel.loadNextPage()
        }
    }
})

// TextWatcher: 메서드 3개
binding.editText.addTextChangedListener(object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        viewModel.onQueryChanged(s?.toString() ?: "")
    }
    override fun afterTextChanged(s: Editable?) {}
})

7. 실전 패턴 — 전략 패턴에서 선택

fun interface SortStrategy {
    fun sort(list: MutableList<Int>)
}

class DataProcessor(private val strategy: SortStrategy) {
    fun process(data: MutableList<Int>) {
        strategy.sort(data)
    }
}

// lambda로 전략 주입
val bubbleSort = DataProcessor { list ->
    for (i in list.indices) {
        for (j in 0 until list.size - i - 1) {
            if (list[j] > list[j + 1]) {
                val temp = list[j]; list[j] = list[j + 1]; list[j + 1] = temp
            }
        }
    }
}

// object expression으로 상태 있는 전략
val trackedSort = DataProcessor(object : SortStrategy {
    var sortCount = 0
    override fun sort(list: MutableList<Int>) {
        sortCount++
        println("${sortCount}번째 정렬")
        list.sort()
    }
})

8. object expression vs object 선언

// object expression: 매번 새 인스턴스 생성
val a = object : Runnable { override fun run() = println("A") }
val b = object : Runnable { override fun run() = println("A") }
println(a === b)  // false (다른 인스턴스)

// object 선언: 싱글턴
object Singleton : Runnable {
    override fun run() = println("Singleton")
}
println(Singleton === Singleton)  // true

9. 선택 가이드

메서드가 1개인가?
    ↓ Yes → lambda 또는 fun interface
    ↓ No  → object expression

자체 상태(필드)가 필요한가?
    ↓ Yes → object expression
    ↓ No  → lambda

Java 인터페이스인가?
    ↓ Yes → SAM 변환으로 lambda 사용
    ↓ No (Kotlin) → fun interface 여부 확인

여러 곳에서 재사용하는가?
    ↓ Yes → 별도 클래스 또는 fun interface 인스턴스
    ↓ No  → 인라인 lambda / object expression

10. 정리



Related Posts