PPAK

[Java/Kotlin] Java 와 Kotlin 의 Nested Class(중첩 클래스) 선언 방식의 차이에 대한 생각 (feat. Singleton instance) 본문

jvm

[Java/Kotlin] Java 와 Kotlin 의 Nested Class(중첩 클래스) 선언 방식의 차이에 대한 생각 (feat. Singleton instance)

PPakSang 2022. 9. 18. 16:32

코틀린과 자바 언어가 비슷한 듯 다른 점에 대해  지난 포스팅 을 통해서 이야기 했습니다.

 

이번에도 역시 Java 와 Kotlin 에서 중첩 클래스를 선언하는 여러가지 방법과 그 차이에서 오는 시사점이 무엇인가에 대한 제 생각을 밝힙니다.

 

Nested Class(중첩 클래스) 라는 것은 기본 적으로 둘 이상의 클래스가 연관 관계(주로 계층 관계) 가 있을 때 이를 논리적인 그룹으로 묶어 코드의 가독성을 높이기 위해 존재합니다.

 

다만 이러한 중첩 클래스의 선언 방식에 따라서 JVM 이 인스턴스를 메모리에 다른 형태로 저장한다는 것을 알아야 했습니다.

 

 

JAVA

우선 아래는 Java 에서 선언 가능한 중첩 클래스의 종류입니다.

아래 예시 외에도 Method Local Inner Class, Anonymous Inner Class 두 가지가 더 있지만 본 포스팅의 주제와는 다소 관련이 없어서 생략했습니다.

class Outer {
    class Inner1 {}
    private class Inner2 {}
    static class Inner3 {}
    private static class Inner4 {}
    public static class Inner5 {}
    public class Inner6{}
}

위에서 볼 수 있는 가장 큰 차이점은 static 의 유무입니다. static 유무에 따라서 Static Nested Class / Inner Class 로 나뉩니다. 편하게 중첩 클래스 / 내부 클래스로 구분하겠습니다.

 

자바로 코드를 작성하다보면 한번쯤은 마주했을 법한 상황입니다.

Intellij 를 포함한 다수의 IDE 에서는 내부 클래스를 중첩 클래스로 바꾸어 사용할 것을 권장하고 있습니다.

 

그렇다면 왜 내부 클래스가 아니라 중첩 클래스로 변경할 것을 권장할까요?

기본적으로 저희가 알고 있는 사실은 내부 클래스를 생성하기 위해서는 항상 외부 클래스의 인스턴스가 필요하다는 사실 입니다. 즉 내부 클래스 인스턴스가 외부 클래스의 인스턴스를 참조하고 있다는 사실입니다.

 

내부 클래스의 인스턴스가 외부 클래스의 인스턴스를 참조하고 있기 때문에 발생할 수 있는 문제는 간단하지만 심각합니다.

 

우선 모든 내부 클래스 인스턴스는 외부 클래스의 참조 정보를 저장할 메모리 공간을 별도로 마련합니다. 이는 실제 클래스 내에서 외부 클래스 속성값을 사용하고 있지 않을 때에도 생성되는데, 쉽게 알 수 있듯 리소스 낭비로 이어질 수 있습니다.

 

실제로 외부 클래스의 속성값을 내부 클래스에서 사용하고 있다면 더 이상 static클래스로(선언할 수 없을 뿐더러) 변경하도록 권장하지 않습니다.

조금 더 심각한 문제는 GC(Garbage Collector) 가 외부 클래스에게 할당 된 메모리를 해제하지 않는다는 것에 있습니다. 위와 같은 방식으로 내부 클래스가 외부 클래스를 참조하고 있는 상황에서는 두 인스턴스 모두 수거 대상에서 제외되는 메모리 누수 문제가 생깁니다.

 

Kotlin

그렇다면 코틀린에서는 어떻게 중첩 클래스와 내부 클래스를 정의하는지에 대해서 알아봅시다.

 

코틀린에서는 inner 라는 내부 클래스 선언 키워드가 별도로 존재합니다. 그리고 자바에서는 Inner Class 로 선언되었던 방식이 코틀린에서는 Static Nested Class 로 선언된 것과 같이 동작합니다. 

class Outer {
    inner class Inner1
    private inner class Inner2
    class Inner3
    private class Inner4
    class Inner5
    inner class Inner6
}

 

코틀린에서는 static, final 키워드가 존재하지 않습니다. 대신 companion object 와 같은 키워드를 통해 공유 메모리 속성을 선언하고, val, inner 등등의 키워드로 기존의 자바 코드를 대치합니다.

 

자바 언어에서는 내/외부 클래스의 기본 관계를 내부 클래스가 외부 클래스를 참조하는 형태로 정의했습니다. 하지만 코틀린에서는 기본 형태로 중첩 클래스를 선언하는 방식을 택했습니다.

 

위의 예시에서 시사하는 바를 생각해보자면 다음과 같습니다.

 

내부 클래스는 확실히 특정한 목적이 있을 때, 가령 내부 클래스에서 외부 클래스의 속성값들을 사용해야하는 경우에 편리하게 사용하기 좋습니다. 다만 위에서 언급한 메모리 누수의 문제가 생길 수 있기 때문에 특별한 이유가 없다면 선언하지 않는 것이 좋습니다. 하지만 많은 개발자들이 이러한 사실을 개발 타임라인 간 고려하기란 쉽지 않을 수 있습니다(특히나 저 같은 코린이들에겐 더더욱). 사실 저도 IDE 차원에서 경고 메세지를 띄워주지 않았다면 무심결에 넘어갔을 것 같다는 생각도 합니다.

 

따라서 기본 형태를 중첩 클래스로하고 개발자가 특수한 목적이 있을 때 별도로 inner 를 명시하도록 합니다. 그 결과 기존의 리스크가 존재하는 선언 방식을 기본으로 하기 보다는 개발자 스스로 의도에 맞는 코드를 작성할 수 있도록 유도합니다.

 

실제로 코틀린은 개발자가 목적에 맞게 사용할 수 있는 여러가지 키워드를 자바보다 더 세분화하여 제시해줍니다. 그리고 개발 간에 피로할 수 있는 중복코드의 생성을 최소한으로 할 수 있는 대안 또한 존재합니다.

 

대표적으로 코틀린에서는 싱글톤 객체를 생성하기 위한 별도의 키워드를 제공합니다.

아래는 싱글톤 객체를 생성하는 두 가지 방법입니다.

object Singleton {}

class Signleton private constructor(){
    companion object {
        val instance: Singleton by lazy(LazyThreadSafetyMode.PUBLICATION) { Singleton() }
    }
}

위의 object 키워드를 사용한 싱글톤 선언은 아주 간단합니다. 여러 블로그에서 object 키워드는 프로세스가 생성됨과 동시에 자원이 할당되어 메모리가 낭비된다는 이야기가 많았는데, 언어 차원에서 제공하는 별도의 키워드가 그렇게 간단하게 짜여져 있는가 싶어서 코틀린 문서를 찾아보니 첫 참조가 이루어질 때 초기화되는 lazy initialization 방식을 사용한다고 한다고 합니다.(as static objects are lazy initialized)

 

절대 아래의 코드와 동일한 의미가 아닙니다!!

class Outer {
	val instance: Outer = Outer()
}

 

그 다음 예시는 compainon object 를 이용해서 기존의 자바에서 사용한 Holder 패턴과 어느정도? 유사한 방식으로 선언 가능한 싱글톤 패턴입니다.

 

한 가지 차이점은 자바에서는 class 가 로드되고 static 멤버의 값이 할당되는 과정이 thread-safe 함을 이용한 방식이였다면, 위의 방식은 최초의 인스턴스가 생성되는 시점에 로드되는 companion object 을 이용하여  lazy Initialization 방식을 thread-safe 하게 유지하여 메모리에 싱글톤 객체를 올리는 시점도 미루고 thread-safe 함도 보장하는 요구사항을 만족시킬 수 있었습니다.

 

이 외에도 Double Checked Locking 과 같은 대안도 존재하지만 Lock 을 해제하는 시점이 참조값을 할당하는 시점보다 미세하게 빨라 발생하는 문제점이 존재한다고 알고 있어 보편적으로 사용되는 위 두 가지 방식만을 제시하겠습니다.

 

코틀린을 학습한지 얼마 지나지 않아 정확하지 않은 부분이 있을 수 있습니다. 혹시라도 잘못된 정보가 있다면 지적해주시면 감사하겠습니다.

 

 

Comments