Kotlin과 Java의 Nested and Inner Classes를 알아보고, Nested classes를 왜 사용해야 하는지 알아본다.



개인 광고 영역

Kotlin은 class 안에 class(중첩 클래스)를 정의할 때 기본 Nested classes 룰을 사용한다.

inner classes가 아닌 이유를 찾기엔 자바를 이해하는 편이 가장 좋다. 이 글에서는 Java의 inner classes와 Nested classes를 통해 코틀린의 Nested classes 이유를 알아본다.


이 글에서 알아볼 내용

  • Java Inner classes와 Nested classes를 알아본다.
  • Kotlin의 Inner classes와 Nested classes를 알아본다.
  • Java의 어떤 문제로 코틀린은 Nested classes가 기본인지를 살펴본다.


자바와 코틀린의 서로 다른 Inner Classes와 Nested Classes

자바든 코틀린이든 중첩 클래스를 활용하는 경우는 매우 흔하다.

내가 가진 멤버 클래스를 정의할 때 주로 사용하는데 정의하는 방법은 2가지가 있고, Java와 Kotlin은 전혀 반대로 정의한다.

Java는 명시하지 않으면 inner classes를 나타내고, Kotlin은 명시하지 않으면 Nested classes로 정의한다.

Nested classes와 Inner classes를 살펴보자.


Java의 Nested and Inner Classes

먼저 자바는 중첩 class를 정의하면 기본 Inner classes로 정의한다.

다음과 같이 별도로 명시하지 않아도 InnerClass의 바깥쪽 class인 Outer에 포함된 String outer에 자유롭게 접근하는 게 가능하다.

class Outer {

    private String outer = "Outer";

    class InnerClass {

        public InnerClass() {
            System.out.println(outer);
        }
    }
}

자바는 기본 Inner classes로 정의하며, Inner classes에는 내부적으로 Outer class에 대한 정보를 묵시적으로 참조를 포함하고 있고, 이를 통해 String outer에 접근할 수 있다.

이 코드에 Nested classes로 변경하려면 static을 추가해야 한다. 이제 InnerClass는 묵시적으로 참조하고있던 Outer class 정보가 사라지게 되고, 문법 오류가 발생하게 된다.

Non-static field ‘outer’ cannot be referenced from a static context

class Outer {

    private String outer = "Outer";

    static class InnerClass {

        public InnerClass() {
            System.out.println(outer);
        }
    }
}

이를 해결하기 위해서는 다시 inner classes로 돌리거나, InnerClass의 생성자에 Outer 객체를 넘겨주는 형태로 작성해 해결할 수 있다.

class Outer {

    private final String outer = "Outer";

    static class InnerClass {

        // 생성자에 parameter Outer를 넘겨준다.
        public InnerClass(Outer out) {
            System.out.println(out.outer);
        }
    }
}


Kotlin의 Nested and Inner Classes

Kotlin은 Java와 완전히 반대로 동작하는데, Nested classes로 정의한다. 처음부터 묵시적으로 Outer를 들고 있지 않도록 설계했다.

그렇기 때문에 다음의 코드의 print(outer)는 즉시 오류가 발생한다.

Unresolved reference: outer

class Outer {

    private val outer = "Outer"

    class InnerClass {

        init {
            print(outer)
        }
    }
}

이를 해결하는 방법은 2가지가 있다. Java의 static처럼 InnerClass 생성자에 parameter Outer를 추가하거나, inner 키워드를 추가해 묵시적으로 Outer를 참조하도록 만드는 것이다.

class Outer {

    private val outer = "Outer"

    inner class InnerClass {

        init {
            print(outer)
        }
    }
}

상황에 따라 아래와 같이 작성할 수 있다.

class Outer {

    private val outer = "Outer"

    class InnerClass(
        private val out: Outer
    ) {

        init {
            print(out.outer)
        }
    }
}


정리하면

위에서 작성한 코드를 표로 정리하면 아래와 같다.

class Outer 안에 정의한 InnerClass Java Kotlin
Nested classes static class InnerClass class InnerClass
inner classes class InnerClass inner class InnerClass

모든 Nested classes는 Outer에 접근할 수 없는데, Nested classes는 묵시적인 참조를 제공하지 않기 때문이다. 결국 서로 다른 클래스로 취급받을 수 있게 된다.

결국 아래 코드처럼 Outer.InnerClass()를 별도로 생성할 수 있고, 사용할 수 있게 독립적인 취급을 받을 수 있다.

Outer.InnerClass innerClass = new Outer.InnerClass();

코틀린에서도 아래와 같이 생성한다.

val innerClass = Outer.InnerClass()

Inner classes는 항상 밖에 있는 Outer가 생성되어야 안에 있는 InnerClas도 생성할 수 있다.

이렇게 Outer가 생성되고, InnerClas가 생성되면, InnerClas가 Outer의 멤버를 사용치 않아도 불필요하게 Outer는 항상 생성되어야 한다.

val innerClass = Outer().InnerClass()


코틀린은 왜 기본 Nested classes를 사용할까? - 책을 통해 알아보자.

이펙티브 자바와 코틀린 인 액션 책을 참고해보면, 자바의 Inner Classes에는 크게 3 가지 문제가 있음을 알 수 있다.

  • 직렬화에서 Inner classes를 사용할 경우 직렬화에 문제가 있다.
  • Inner classes 내부에 숨겨진 Outer class 정보를 보관하게 되고, 결국 참조를 해지하지 못하는 경우가 생기면 메모리 누수가 생길 수도 있고, 코드를 분석하더라도 이를 찾기 쉽지 않아 해결하지 못하는 경우도 생긴다.
  • Inner classes를 허용하는 자바는 Outer를 참조하지 않아도 기본 inner classes이기 때문에 불필요한 메모리 낭비와 성능 이슈를 야기한다.

위와 같이 3 가지 문제가 있을 수 있다고 나오니 자세한 내용은 두 권의 책을 모두 참고하면 좋다.

자바는 이러한 문제를 해결하기 위해 이펙티브 자바라는 책을 통해 가이드하고 있고, Kotlin은 처음부터 이를 배제하고 기본 Nested classes를 따르도록 설계했다고 보아도 좋을 것 같다.

결국 코틀린에서는 inner를 명시함으로써 Inner classes 임을 알 수 있고, 반대로 자바는 static을 붙여야 Nested classes임을 알 수 있다.

결국 문제가 하나라도 있는 걸 배제한 게 코틀린이라고 보면 되겠다.


그래서

Java는 중첩 class 사용 시 기본이 Inner class 임을 알아보았다. Inner class도 밖에서 생성하는게 가능한데 다음과 같았다.

Outer.InnerClass innerClass = new Outer().new InnerClass();

이 코드의 문제를 좀 더 보면 Outer에는 수많은 멤버가 포함되어 있을 수 있고, InnerClas는 Outer가 필요로 하는 데이터 만 가질 뿐 실제 Outer와 무관한 코드로 구성되었을 수 있다. 이 경우는 보통 Builder 패턴에 해당한다.

class AlertDialog(context: Context) : AppCompatDialog(context) {

    // 생략

    class Builder(private val context: Context) {
        private var title: String = ""

        fun setTitle(title: String): Builder {
            this.title = title
            return this
        }

        fun create(): AlertDialog =
            AlertDialog(context).apply {
                setTitle(title)
            }
    }
}

이러한 패턴에서 static을 붙이지 않으면 초기화할 때 다음과 같이 접근하니 사실 Builder의 의미가 없게 된다. 거기에 AlertDialog를 직접 접근할 수 있는 묵시적 참조도 만들어지니 더더욱 빌더 패턴을 하는 의미는 없어지게 된다.

AlertDialog.Builder(context).setTitle("").create()

결국 이 코드의 의미도 사라지게 되고, 불필요한 메모리 낭비와 생성 비용만 발생하게 된다.


안드로이드 RecyclerView 내부 코드를 통한 설계 살펴보기.

Android에서 매우 흔하게 사용하는 RecyclerView의 내부 class 정의를 통해 어떤 식으로 설계를 하면 좋을지 조금 알아보자.

RecyclerView에는 아래와 같이 3가지 종류의 class 정의를 가지고 있다.

  • private class 정의
  • public static class 정의
  • public inner class 정의
public class RecyclerView extends ViewGroup {

    // inner class로 정의하고 private 처리
    private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {
    }

    // Nested classes로 정의하고 public으로 정의
    public abstract static class ViewHolder {
    }

    // inner class 이지만 외부에서 생성 가능
    public final class Recycler {
    }
}


ItemAnimatorRestoreListener

ItemAnimatorRestoreListener의 경우 내부 private inner 클래스로 정의했다. 명확하게 RecyclerView 내부에서 inner class로써 클래스를 분리함을 뜻한다. 외부에서 바라볼 땐 이 클래스가 있는지조차 알 필요 없고, RecyclerView의 입장에선 이 클래스가 밖에서 수정 당하는 걱정도 없다.

참고로 리플렉션에 대한 이야기는 제외한다.


ViewHolder

ViewHolder는 static class로 정의되어 있는데, Inner classes가 아님을 명시했다. 결국 이 클래스는 독립적으로 운용 가능하다는 의미이고, 여기서의 코드는 abstract이기 때문에 상속을 받아 재정의를 강제로 해야 한다.

이 클래스는 RecyclerView 없이 단독 사용이 가능하기 때문에 ViewHolder 밖의 RecyclerView도 신경 쓸 필요가 없고, 단독으로 수정이 일어나더라도 서로 영향을 미치지 않는다.

val recyclerView = object : RecyclerView.ViewHolder(view) {
    // 상속 구현
}


Recycler

Recycler는 public으로 접근할 수 있고, 다음과 같이 초기화해야 한다.

val recycler = RecyclerView(this).Recycler()


그래서 기본은 Nested classes를 사용하는 걸 추천한다.

상황에 따른 Inner classes 정의와 Nested classes 정의를 사용해야 한다. 그게 어렵다면 코틀린에서는 기본 Nested classes를 활용하는 게 맘 편하다.

굳이 inner를 명시해 사용할 필요는 없다. 더더욱 Outer를 참조할 필요가 없다면 굳이 inner를 명시할 필요는 없고, 불필요한 메모리 낭비를 사전에 막을 수 있다.

코틀린에서 사용할 경우 몇 가지 룰을 만든다면 다음과 같을 수 있다.

  • Outer의 멤버를 참조할 필요가 없다면 굳이 inner 키워드를 사용치 않아야 한다.
  • Inner classes를 보호하고 싶다면 private를 명시해라.
  • 수정 가능한 상태로 두고 싶다면 open 키워드를 명시할 수 있다.

결국 자바와 다르게 코틀린에서는 수정이 가능한지를 지정하고, Inner classes 임을 명시할 수 있는 반대의 권한을 제공하는데, 이를 코드를 통해 확인해볼 수 있다.

class Outer {
    class Nested {
    }
}

위 코드를 디컴파일 하면 아래와 같다.

public final class Outer {
   public static final class Nested {
   }
}

이를 통해 알 수 있듯 이미 자바에서 생겼던 문제를 코틀린이 어느 정도 방어하고, 이펙티브 자바에서도 가이드하고 있으니, 굳이 필요치 않다면 명시하지 않는 방법을 활용하고, 정말 필요하다면 키워드를 잘 활용하길 바란다.

inner, private, open을 적절하게 활용하는 게 가장 좋은 구조를 만들 수 있고, 수정 가능성을 최소화시킬 수 있다.


안드로이드에서의 활용은?

RecyclerView.ViewHolder는 Inner classes보다는 Nested classes 또는 top level로 만드는 게 좋다고 생각한다.

  • 항상 하나의 어댑터에서 만 활용하는 건 아니다.
  • RecyclerView는 실제로 화면 사이즈보다 많이 onCreateViewHolder가 호출되고 만들어진다.

화면 사이즈 보다 많이 만들어지는데, Inner classes로 정의한다면 생성되는 객체마다 묵시적으로 Outer class를 항상 참조하게 된다.

하지만 안드로이드는 Activity/Fragment에서 RecyclerView를 사용하고 있고, 이들이 onDestroy 호출되면 Adapter 역시 함께 사라지고, 그럼 ViewHolder들도 함께 사라지기 때문에 큰 문제는 없다.

꼭 필요하지 않다면 코틀린 기본 값인 Nested classes인 기본 상태를 굳이 inner를 추가할 필요는 없다고 생각하고, 좀 더 좋은 접근법인 top level로 작성하는 것도 고려하는 게 좋다고 생각한다.


마무리

Kotlin은 왜 Nested classes를 기본으로 활용하는지를 살펴보았다. 결국 문제는 다 자바라고 할 수 있다. 자바에서는 제공하지만 코틀린에서는 제공하지 않는 이유 역시 자바 때문으로 해석할 수 있다.

사실 이 내용은 Effective Kotlin에서는 Inner classes 정의만 알면 되기 때문에 이런 내용을 굳이 다루지 않고, Kotlin in action에서는 다루고 있는 내용이다.

이번 글 요약

  • Inner classes는 정말 필요치 않으면 쓰지 않는 게 좋다.
  • 해당 클래스에서 만 사용함이 명확하다면 private을 걸어두어야 한다.
  • 참조를 어떻게 접근할 것인지에 따라 inner 키워드가 유용할 수 있다.
  • 자바를 잘 알면 Kotlin의 Nested classes가 왜 기본인지 이해하기 쉽다.
  • 꼭 필요한 게 아니라면 inner 키워드와 open을 필요할 때만 허용하자.


이 글만 보기 아쉽죠. Effective Kotlin 더 보기

지금까지 작성한 EffectiveKotlin 관련 글은 태그로 모아 링크를 추가한다.

더 보기 - Effective Kotlin 관련 글



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments