티스토리 뷰

728x90

 

 

자바처럼 코틀린에서도 클래스 안에 다른 클래스를 선언할 수 있다.

도우미 클래스(?)를 캡슐화하거나 코드를 사용하는 곳에 가까이 두고 싶을 때 유용하다.

 

코틀린에서는 클래스 내부에 다른 클래스를 정의할 수 있는 두 가지 방법이 있다.

nested class와 inner class인데, 먼저 nested class에 대해 알아보자.

 

nested class

sealed class Error(val message: String) {
    class NetworkError : Error("Network failure")
    class DatabaseError : Error("Database cannot be reached")
    class UnknownError : Error("An unknown error has occurred")
}
class RecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    class ViewHolder(private val binding: ...Binding) : RecyclerView.ViewHolder(binding.root) {
        ...
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        ...
    }
}
  • class 내에 class 키워드를 통해 생성한 일반적인 클래스를 nested class(내포된 클래스)라고 한다.
  • nested class는 단순히 외부 클래스의 공간 안에 정의된 클래스일 뿐이다. 외부 클래스와 어떤 것도 공유하지 않는다.
 
class Person {
    class Id(private val name: String)
}

fun main() {
    val id = Person.Id("olive")
    val person = Person()
}
 
  • 외부 클래스 밖에서 nested class에 접근할 때는, "외부클래스명.nested클래스명"을 사용한다. 
    • ex) Person.Id("John")
  • "Person().Id("olive")처럼 외부 클래스의 인스턴스를 통해 nested class에 접근하지 않는다.
  • 즉, 외부 클래스와 nested class는 별도의 공간을 가진다.

 

package test

import test.Person.Id

class Person {
    class Id(private val name: String) { ... }
}

fun main() {
    val id = Id("olive")
}
  • "패키지명.외부클래스명.nested클래스명"을 import 하면 외부 클래스명 없이도 인스턴스를 생성할 수 있다.
    • ex) Person.Id("olive") -> Id("olive")

 

class Person(private val id: Id) {
    class Id(private val name: String)
   
    fun showMe() = println(id.name) // Cannot access 'name': it is private in 'Id'
}
  • 외부 클래스에서는 nested class의 private 멤버에 접근할 수 없다.

 

class Person(private val id: Id) {
    class Id(val name: String)
   
    fun showMe() = println(id.name) // 가능
}
  • private을 public으로 변경하면 Id의 name에 접근 가능!

 

class Person(private val age: Int) {
    class Id(private val name: String) {
        fun showAge(person: Person) = println(person.age) // 가능
    }
}
  • 반면에 nested class는 외부 클래스의 private 멤버에 접근할 수 있다.

 

 

inner class

class Person(private val age: Int) {
    inner class Id(private val name: String) { ... }
}

fun main() {
    val person = Person(15)
    val id = person.Id("olive")
}
  • class 내에 inner class 키워드를 통해 클래스를 생성할 수 있다.
  • 이러한 inner class는 외부 클래스의 인스턴스에 대한 참조를 가지고 있다.
  • inner class는 외부 클래스의 인스턴스와 관련이 있기 때문에, inner class에 접근할 때는 "외부 클래스의 인스턴스명.inner클래스명"을 사용한다.

 

class Person(private val age: Int) {
    inner class Id(private val name: String) {
        fun showAge() = println(age)
    }
}
  • inner class에서 외부 클래스의 private 멤버에 접근할 수 있다.
  • 아까 말했듯이 외부 클래스의 인스턴스와 연관이 있기 때문에, 아무런 한정자 없이 외부 클래스의 멤버에 접근할 수 있다.
    • ex) person.age가 아닌 age

 

class Person(private val age: Int) {
    inner class Id(private val name: String) {
        val 그냥적절한변수명 = this@Person.어떤함수()
    }

    fun 어떤함수()
}
  • inner class 내에서 외부 클래스의 인스턴스를 가리켜야 한다면 "this@외부 클래스명"을 사용한다.
    • ex) this@Person 
    • this는 항상 가장 내부 클래스의 인스턴스를 가리킨다.
  • 이러한 방식을 "한정된 this"(Qualified this)라고 한다. this 뒤에 @를 붙이고 대상 클래스 이름을 적는 것이다.

 

 

local class

fun main() {
    var x = 1

    class Counter {
      fun increment() {
          x++
      }
    }

    Counter().increment()
    println(x) // 2
}
  • 함수 내에 선언된 클래스를 local class라고 한다.
  • local class는 자신을 둘러싼 코드 블록 내에서만 접근할 수 있다. (  접근 제한자를 붙일 수 없다.)
  • 자신을 둘러싼 코드 블록의 값을 접근하거나 변경할 수 있다.
  • local class는 항상 inner class가 된다.
    • local class는 자신을 둘러싼 블록의 값에 접근하거나 값을 변경할 수 있다고 했다.
    • 그런데 nested class는 외부 값에 접근할 수 없다. local class로 선언된 nested class는 외부 값에 접근이 가능하다면 가시성 규칙에 어긋난다.

 

fun interface Counter {
    fun next(): Int
}

object CounterFactory {
    private var count = 0
    
    fun new1(): Counter { // Anonymous Object
        return object : Counter {
            override fun next(): Int {
                return count++
            }
        }
    }
    
    fun new2() = Counter { count++ } // SAM
}

fun main() {
    println(CounterFactory.new1().next()) // 0
    println(CounterFactory.new2().next()) // 1
}
  • 우리가 자주 사용하는 익명 클래스도 inner class에 포함된다.
  • 외부의 값을 접근/변경 할 수 있기 때문이다.

 

 

nested class ↔ inner class 

  • nested class
    • 외부 클래스와 독립적으로 존재한다. 외부 클래스의 인스턴스와 관련이 없다.
    • 외부 클래스의 멤버에 접근할 수 없다.
    • 인스턴스를 생성할 때 외부 클래스와 독립적인 메모리 공간이 할당된다. 
  • inner class
    • 외부 클래스의 인스턴스와 연관되어 있다.
    • 외부 클래스의 멤버 함수나 프로퍼티에 접근할 수 있다.
    • 외부 클래스의 인스턴스가 생성되어야만 inner class의 인스턴스를 생성할 수 있다.
    • inner class의 인스턴스는 외부 클래스 인스턴스의 참조를 가진다.

 

 

자바와의 비교

  자바 코틀린
nested class static class A class A
inner class class A inner class A

자바에서는 기본적으로 inner class이다. (아무런 키워드 없이 클래스 내에 클래스를 생성하면)

반면에 코틀린에서는 기본적으로 nested class이다.

코틀린은 왜 기본적으로 nested class를 생성할 수 있도록 했을까?

 

 

inner class의 위험성

코틀린이 기본적으로 nested class를 생성하도록 한 이유는, inner class는 메모리 누수의 위험이 있기 때문이다.

 

객체가 삭제되는 시점은 객체가 더 이상 사용되지 않을 때이다.

그런데 inner class를 사용하면 항상 외부 클래스의 인스턴스를 참조한다.

때문에 객체가 적절한 시점에 삭제되지 못하고 메모리 누수가 발생한다.

 

이러한 메모리 누수는 명시적인 것이 아니라 암묵적이기 때문에 위험하다.

(명시적: 컴파일 오류 등 알 수 있는 것 / 암묵적: 프로그래머가 발견하기 전까지는 알 수 없는 것)

 

class Outer {
    inner class Inner
}

fun main() {
    val `inner` = Outer().Inner()
}

메모리 누수의 이해를 돕기 위해 예시 코드를 작성해보았다.

inner class인 Inner 인스턴스를 가지기 위해서는 위와 같은 코드가 필요하다.

그런데 Outer에는 매우 많은 멤버들이 있을 수 있다.

Inner만을 사용하기 위해 Outer의 모든 멤버를 계속해서 가지고 있다면, 불필요한 메모리 낭비가 된다.

따라서 inner class의 사용을 최대한 지양해야 한다.

 

 

 

 

참고 자료

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/01   »
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
글 보관함