(Kotlin/코틀린) inner class vs nested class 완전 비교


1. Java vs Kotlin 기본 동작 차이

Java에서 내부 클래스(inner class)는 기본이 non-static, 즉 외부 클래스 인스턴스를 암묵적으로 참조합니다.
Kotlin은 반대로 기본이 static(nested) 이며, 외부 참조가 필요한 경우에만 inner 키워드를 명시합니다.

구분 Java Kotlin
기본 내부 클래스 non-static (외부 참조 O) nested (외부 참조 X)
외부 참조 포함 명시 없음 inner 키워드 필요
static 내부 클래스 static 명시 기본값 (nested class)

2. nested class (기본)

외부 클래스 인스턴스와 독립적입니다. 외부 클래스의 멤버에 접근할 수 없습니다.

class Outer {
    private val secret = "비밀"

    class Nested {
        fun greet() = "안녕!"
        // fun reveal() = secret  // 컴파일 에러: Outer 인스턴스 없음
    }
}

// Outer 인스턴스 없이 바로 생성 가능
val nested = Outer.Nested()
println(nested.greet())  // 안녕!

3. inner class

inner 키워드를 붙이면 외부 클래스 인스턴스를 암묵적으로 참조합니다.

class Outer {
    private val secret = "비밀"

    inner class Inner {
        fun reveal() = secret  // 외부 클래스 멤버 접근 가능
        fun outerRef() = this@Outer  // 외부 클래스 인스턴스 명시적 참조
    }
}

// 반드시 Outer 인스턴스를 통해 생성
val outer = Outer()
val inner = outer.Inner()
println(inner.reveal())  // 비밀

4. 핵심 차이 비교

구분 nested class inner class
키워드 없음 (기본) inner
외부 클래스 참조 없음 있음 (암묵적)
생성 방법 Outer.Nested() outer.Inner()
외부 멤버 접근 불가 가능
메모리 누수 위험 없음 있음 (주의 필요)
주요 용도 독립 헬퍼 클래스 외부 상태 참조 필요 시

5. 메모리 누수 주의 — inner class

inner class는 외부 클래스 인스턴스를 강한 참조로 보유합니다.
외부 클래스가 GC되어야 하는 상황에서도 inner class 인스턴스가 살아있으면 외부도 해제되지 않습니다.

class Activity {
    inner class ClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            // Activity의 멤버에 접근 가능하지만
            // 이 리스너가 View에 등록된 채로 남으면 Activity 메모리 누수!
            updateUI()
        }
    }
}

Android에서 Handler, Runnable, 콜백 등에 inner class를 사용할 때 특히 주의해야 합니다.
WeakReference 사용 또는 nested class로 전환 권장


6. 실전 사용 패턴

nested class — Builder 패턴

class HttpRequest private constructor(
    val url: String,
    val method: String,
    val headers: Map<String, String>
) {
    class Builder {  // nested: HttpRequest 인스턴스 불필요
        private var url = ""
        private var method = "GET"
        private val headers = mutableMapOf<String, String>()

        fun url(url: String) = apply { this.url = url }
        fun method(method: String) = apply { this.method = method }
        fun header(key: String, value: String) = apply { headers[key] = value }

        fun build() = HttpRequest(url, method, headers.toMap())
    }
}

val request = HttpRequest.Builder()
    .url("https://api.example.com")
    .header("Authorization", "Bearer token")
    .build()

inner class — Iterator 구현

class LinkedList<T> {
    private var head: Node<T>? = null

    private data class Node<T>(val value: T, var next: Node<T>? = null)

    inner class ListIterator : Iterator<T> {  // inner: head 접근 필요
        private var current = head

        override fun hasNext() = current != null
        override fun next(): T {
            val value = current!!.value
            current = current!!.next
            return value
        }
    }
}

7. 익명 객체와의 관계

익명 객체(object : Interface { })는 inner class처럼 외부 스코프를 캡처합니다.

class Counter {
    private var count = 0

    fun getIncrementer() = object : Runnable {
        override fun run() {
            count++  // 외부 count 캡처 — inner class와 동일한 누수 위험
        }
    }
}

8. 정리



Related Posts