[Android] Object vs Companion object

이승우·2023년 5월 9일
1

Object는 Kotlin에서 싱글톤 객체를 사용하기 위해 사용할 수 있으며, Companion object는 클래스 내에서 함수나 상수들을 정의해서 Java에서 Static 하게 사용 가능한 것으로 알고 있었고 둘의 차이에 대해서 깊게 생각해보지 않았던 것 같아서 정리해보려고 한다.

kotlin의 object 선언과 companion object는 java의 static과 동일하지 않다.

🤔 둘의 차이점은 무엇일까?

1) Object 선언

object ClassA {
	fun testA(){
    
    }
}
  • Singleton 형태이다.
  • Thread Safe 하다. 즉, 멀티 스레드 환경에서 함수나 변수에 여러 스레드가 동시 접근하여도 안전하다는 것이다.
  • Lazy initialization을 지원한다. 즉, Object 로 선언된 클래스는 외부에서 객체가 사용되는 시점에 초기화가 이루어진다. (객체는 클래스를 정의하면서 이미 선언되었다.)
  • const val로 선언된 상수는 static 변수이다.
  • object 내부에 선언된 변수와 함수들은 java의 static이 아니다. 단, 아래 케이스는 static이다.
    • const val로 선언한 상수들
    • @JVMStatic or @JVMField의 어노테이션이 붙은 변수 및 함수들

Object 키워드로 선언된 클래스는 주 / 부 생성자를 사용할 수 없다. 객체 생성과 동시에 생성자 호출 없이 바로 만들어지기 때문이다. 또한, 중첩 object 선언이 가능하며, 클래스나 인터페이스를 상속이 가능하다.

2) companion object

class ClassB{
	companion object {
    	fun create()
    }
}
  • 해당 클래스 자체가 static이 아니다. 즉, ClassB()로 생성할 때마다 객체의 주소값은 다르다.
  • 해당 클래스가 로드될 때 초기화된다.
  • const val로 선언된 상수는 static 변수이다.
  • companion object 내부에 선언된 변수와 함수들은 java의 static이 아니다. 단, 아래 케이스는 static
    • const val로 선언된 상수들
    • @JVMStatic or @JVMField의 어노테이션이 붙은 변수 및 함수들
  • 클래스 내부에 객체 선언을 위한 object 키워드이다. 쉽게 설명하면, 클래스 내부에 싱글톤 패턴을 구현하기 위해 사용하는 것이라고 볼 수 있다.
  • 생성자 없이 companion object 클래스의 이름을 사용하여 호출할 수 있다.

companion object는 클래스 인스턴스 없이 어떤 클래스 내부에 접근하고 싶을 때, 선언한다. 클래스당 하나만 사용할 수 있고, Object 선언과 같이 생성자를 가질 수 없으며, static으로 선언되는 것이 아니라 런타임시에 실제 객체의 인스턴스로 실행된다.

😀 Conclusion

Object 선언은 클래스 전체가 하나의 싱글톤 객체로 선언되지만,

companion object는 클래스 내에 일부분이 싱글톤 객체로 선언되는 것이다.

  • Object 선언
    • 클래스 전체가 하나의 싱글톤 객체로 선언된다.
    • 해당 클래스가 사용될 때 초기화된다. (이미 싱글톤이기 때문에 클래스 로드는 이루어진 다음이다.)
    • 아래 코드에서 ObjectTest.nonStaticFun()을 호출할 때, 초기화가 된다.

ex)

object ObjectTest {

    const val CONST_STRING = "1"

    val nonStaticField = "2"
    @JvmField
    val staticField = "2"

    fun nonStaticFun() {
        println("test nonStaticFun()")
    }

    @JvmStatic
    fun staticFun() {
        println("test staticFun()")
    }
}
  • companion object
    • 클래스 내에 일부분이 싱글톤 객체로 선언된다.
    • 해당 클래스가 속한 클래스가 load 될 때 초기화된다. (아직 메모리에 로드된 적이 없기 때문에 로드 될 때 초기화된다.)
    • 아래 코드에서 val test = CompanionObjectTest() 시 초기화가 된다.
class CompanionObjectTest {

    companion object {
        const val CONST_TEST = 2

        fun test() { }
    }
}

실제 코드로 비교

1) Object 코드

object ObjectTest {

    const val CONST_STRING = "1"

    val nonStaticField = "2"
    @JvmField
    val staticField = "2"

    fun nonStaticFun() {
        println("test nonStaticFun()")
    }

    @JvmStatic
    fun staticFun() {
        println("test staticFun()")
    }
}

java로 변환된 코드를 보자. (kotlin bytecode를 디컴파일해서 얻은 코드이다.)

public final class ObjectTest {
   @NotNull
   public static final String CONST_STRING = "1";
   @JvmField
   @NotNull
   public static final String staticField;
   @NotNull
   public static final ObjectTest INSTANCE;
   
   @NotNull
   private final String nonStaticField;

   @NotNull
   public final String getNonStaticField() {
      return nonStaticField;
   }

   public final void nonStaticFun() {
      String var1 = "test nonStaticFun()";
      boolean var2 = false;
      System.out.println(var1);
   }

   @JvmStatic
   public static final void staticFun() {
      String var0 = "test staticFun()";
      boolean var1 = false;
      System.out.println(var0);
   }

   private ObjectTest() {
   }

   static {
      ObjectTest var0 = new ObjectTest();
      INSTANCE = var0;
      nonStaticField = "2";
      staticField = "2";
   }
}

[정리]

  • java로 변환된 파일 내부에 INSTANCE라는 ObjectTest 타입의 static 객체가 생성된다. 그래서 ObjectTest는 싱글톤 형태가 맞다.
  • const로 선언한 변수들은 public static 필드로 변환된다. 그래서 상수가 맞다.
  • val 또는 일반 함수로 선언된 녀석들은 static 필드가 아니어서 INSTANCE 객체로 접근 가능하다.
  • 단, 함수에 @JVMStatic을 선언한 경우에는 static 함수로 변환된다. @JVMField로 선언된 경우 static 필드가 된다.
// 아래 3가지는 static 필드 처리된 것. 
String s = ObjectTest.CONST_STRING;
String s2 = ObjectTest.staticField;
ObjectTest.staticFun();

// 아래 2가지는 static 이 아니기 때문에, static 변수인 INSTANCE 로 접근해야 함.
ObjectTest.INSTANCE.getNonStaticField();
ObjectTest.INSTANCE.nonStaticFun();

2) companion object 코드

class CompanionObjectTest {

    companion object {
        const val CONST_TEST = 2

        val valTest = 1

        fun nonStaticMethod() {
            println("test nonStaticMethod()")
        }

        @JvmStatic
        fun staticMethod() {
            println("test staticMethod()")
        }
    }
}

java로 변환된 코드를 보자. (kotlin bytecode를 디컴파일해서 얻은 코드이다.)

public final class CompanionObjectTest {
   @NotNull
   public static final String CONST_STRING = "1";
   @JvmField
   @NotNull
   public static final String staticField = "2";
   @NotNull
   public static final CompanionObjectTest.Companion Companion = new CompanionObjectTest.Companion((DefaultConstructorMarker)null);

   @NotNull
   private static final String nonStaticField = "2";
   
   @JvmStatic
   public static final void staticFun() {
      Companion.staticFun();
   }

   public static final class Companion {
      @NotNull
      public final String getNonStaticField() {
         return CompanionObjectTest.nonStaticField;
      }

      public final void nonStaticMethod() {
         String var1 = "test nonStaticMethod()";
         boolean var2 = false;
         System.out.println(var1);
      }

      @JvmStatic
      public static final void staticMethod() {
         String var1 = "test staticMethod()";
         boolean var2 = false;
         System.out.println(var1);
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

[정리]

  • CompanionObjectTest 클래스 자체가 static이 아니다. 그래서 class 자체가 싱글톤은 아니다. 즉, CompanionObjectTest()로 생성할 때마다 객체의 주소값은 다르다.
  • const val로 선언된 상수는 static 변수이다.
  • companion object 내에서 선언된 일반 변수 또는 함수는 static이 아니다. static class인 Companion을 통해 접근이 가능하다.
  • 단, companion object 내에서 @JVMStatic or @JVMField를 붙이면 static
String s3 = CompanionObjectTest.CONST_STRING;
String s4 = CompanionObjectTest.staticField;
CompanionObjectTest.staticFun();
CompanionObjectTest.nonStaticFun(); // ❌ error: nonStaticMethod() 는 static method 아님!!

CompanionObjectTest.Companion.getNonStaticField();
CompanionObjectTest.Companion.nonStaticFun();

클래스 로드 vs 클래스 사용

[클래스 로드]

JVM이 클래스 파일을 읽어들이고, 메모리에 로딩하는 과정을 말한다. 클래스는 처음 사용될 때가 아니라 필요할 때마다 로드된다. 클래스 로드는 아래와 같은 경우에 발생할 수 있다.

  • 프로그램이 실행될 때
  • 클래스가 처음 사용될 때
  • 클래스가 로드될 때, 그 클래스가 의존하는 클래스가 로드되지 않았다면 의존하는 클래스도 함께 로드된다.

[클래스 사용]

클래스 사용은 이미 로드된 클래스에서 메모리를 사용하는 것을 말한다. 클래스가 사용될 때는 해당 클래스의 인스턴스를 생성하거나, 정적 메소드나 변수를 호출하거나, 상속 구조에서 서브 클래스에서 상위 클래스를 참조하는 등의 경우가 있다.

즉, 클래스 로드는 메모리에 로딩하는 과정이고 클래스 사용은 메모리에 로드된 클래스를 실제로 사용하는 것이다. 클래스 로드는 클래스가 사용되기 전에 한 번만 발생하며, 클래스 사용은 필요에 따라 여러 번 발생할 수 있다.

Ref

profile
Android Developer

0개의 댓글