Kotlin 중첩 클래스를 다루는 방법

코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 중첩 클래스를 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의에 소금을 조금 친 내용이다

클래스 안에 또 다른 클래스를 정의하는 것을 중첩 클래스(Nested Class)라고 한다. 중첩 클래스는 크게 두 가지 종류로 나눌 수 있으며 Kotlin은 Java와는 다른 기본 동작을 가지고 있어 이를 명확히 이해하는 것이 중요하다

중첩 클래스의 종류

정적 중첩 클래스 (Static Nested Class)

  • 특징: 외부 클래스와 독립적으로 존재하며, 외부 클래스의 인스턴스에 대한 참조를 가지지 않는다. 따라서 외부 클래스 멤버에 직접 접근할 수 없다
  • 사용 시점: 외부 클래스와 논리적으로 연관되어 있지만 외부 클래스의 인스턴스 데이터가 필요 없는 헬퍼 클래스나 유틸리티 클래스 등으로 재사용 가능한 경우에 적합하다
  • Kotlin에서의 구현: Kotlin에서는 inner 키워드를 붙이지 않으면 기본적으로 정적 중첩 클래스로 동작한다

내부 클래스 (Inner Class)

  • 특징: 외부 클래스의 인스턴스에 대한 숨겨진 참조를 가진다. 따라서 외부 클래스의 멤버(프로퍼티, 메서드)에 직접 접근할 수 있다
  • 사용 시점: 외부 클래스의 인스턴스 없이는 존재 의미가 없는 경우. 즉, 외부 클래스의 특정 인스턴스와 밀접하게 연관된 구현체일 때 사용한다
  • Kotlin에서의 구현: Kotlin에서는 inner 키워드를 클래스 선언 앞에 붙여야 내부 클래스로 동작한다

지역 클래스 (Local Class) 및 익명 클래스 (Anonymous Class)

  • 특징: 이 두 종류의 클래스는 특정 메서드 내부나 표현식 내에서 정의되며, 해당 스포크 내에서만 사용된다
  • 현재의 경향: 최근에는 람다(Lambda)식이나 메서드 참조(Method Reference)를 통해 대부분 대체 가능하므로 실무에서는 사용 빈도가 매우 낮다. Kotlin은 함수형 프로그래밍을 적극적으로 지원하여 람다 사용이 더욱 편리하다
이펙티브 자바의 권고사항과 Kotlin의 접근

이펙티브 자바에서는 “클래스 안에 클래스를 만들 때는 되도록 정적 중첩 클래스를 사용하라”고 권장한다. 그 이유는 내부 클래스가 외부 클래스의 인스턴스를 참조하면서 발생할 수 있는 여러 문제점 때문이다

  • 메모리 누수: 내부 클래스가 외부 클래스 인스턴스에 대한 숨겨진 참조를 가지고 있어, 내부 클래스 인스턴스가 계속 살아있으면 외부 클래스 인스턴스가 GC(가비지 컬렉션)되지 못하고 메모리 누수가 발생할 수 있다. 이를 디버깅 하기가 어렵다
  • 직렬화의 한계: 내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 있어 문제가 발생할 수 있다

Kotlin은 이러한 이펙티브 자바의 가이드를 충실히 따르고 있다. Kotlin에서는 기본적으로 inner 키워드를 사용하지 않으면 외부 클래스를 참조하지 않는 정적 중첩 클래스로 간주한다. 외부 클래스의 인스턴스를 참조해야 할 필요가 있을 때만 명시적으로 inner 키워드를 사용하도록 설계되어 있다

Java에서 LivingRoom을 House의 중첩 클래스로 정의하는 두 가지 방법

public class House {
    private String address;
    private LivingRoom livingRoom; // static 중첩 클래스 참조

    public House(String address) {
        this.address = address;
        this.livingRoom = new LivingRoom(10);
    }


    // 권장되는 static 중첩 클래스 (외부 클래스 참조 없음)
    public static class LivingRoom {
        private double area;

        public LivingRoom(double area) {
            this.area = area;
        }

        // static 중첩 클래스는 외부 클래스의 'address'에 직접 접근할 수 없다
        // public String getAddress() {
        //     return House.this.address;
        // }
    }


    // 권장되지 않는 내부 클래스 (외부 클래스 참조 있음)
    public Class InnerLivingRoom {
        private double area;

        public InnerLivingRoom(double area) {
            this.area = area;
        }

        public String getAddress() {
            return House.this.address; // 외부 클래스의 address에 직접 접근 가능
        }
    }
}
  • public static class LivingRoom: static 키워드가 붙어 외부 House 클래스와 독립적이다. House.this.address와 같이 외부 클래스 인스턴스의 멤버에 직접 접근할 수 없다 (권장)
  • public class InnerLivingRoom: static 키워드가 없어 내부 클래스로 동작하며, 외부 House 인스턴스에 대한 숨겨진 참조를 가진다. House.this.address를 통해 외부 클래스 멤버에 직접 접근할 수 있다 (권장되지 않음)

Kotlin House와 LivingRoom

// 코틀린 기본 중첩 클래스 (외부 클래스 참조 없음, 권장되는 유형)
class House(
    private val address: String,
    private val livingRoom: LivingRoom, // LivingRoom은 House의 중첩 클래스
) {
    LivingRoom( // 'inner' 키워드가 없어 기본적으로 정적 중첩 클래스처럼 동작
        private val area: Double
    ) {
        // 이 LivingRoom은 외부 House의 'address'에 직접 접근할 수 없다
        // val houseAddress: String = this@House.address // 컴파일 에러 발생
    }
}

// 외부 클래스 참조하는 내부 클래스 (권장되지 않는 유형)
class HouseWithInner(
    private val address: String,
    private val livingRoom: InnerLivingRoom,
) {
    inner class InnerLivingRoom(
        private val area: Double
    ) {
        // 'inner'클래스이므로 외부 HouseWithInner의 'address'에 접근 가능
        val houseAddress: String
            get() = this@HouseWithInner.address
    }
}
  • class LivingRoom: inner 키워드가 없으므로 외부 House 클래스의 address에 직접 접근할 수 없다. 이는 Java의 static class와 동일하게 동작하며, 권장되는 방식이다
  • inner class InnerLivingRoom: inner 키워드가 있으므로 외부 HouseWithInner 인스턴스에 대한 참조를 가진다. this@HouseWithInner.address를 통해 외부 클래스의 address에 접근할 수 있다. 이는 Java의 일반 중첩 클래스(static이 없는)와 동일하게 동작하며, 위에서 언급한 문제점들 때문에 권장되지 않는다

Kotlin에서 클래스 내부에 다른 클래스를 정의할 때

  • 기본적으로 inner 키워드를 사용하면 안 된다. Kotlin은 inner 키워드가 없으면 외부 클래스의 참조 없이 독립적인 중첩 클래스(Java의 static nested class와 동일)를 생성하며, 이것이 대부분의 권장되는 방식이다
  • 외부 클래스의 인스턴스 멤버에 접근해야만 하는 극히 예외적인 경우에만 inner 키워드를 사용해는 것을 권장한다. 하지만 이때 메모리 누수나 직렬화 문제 등 잠재적인 부작용을 항상 염두에 두어야 한다

출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)