본문 바로가기
Kotlin

[Kotlin] Object 키워드 (with companion object)

by 태크민 2024. 11. 2.

이번 포스팅에서는 Kotlin에서 object 키워드가 무엇을 의미하고  class와 object는 어떤 차이가 있는지에 대해 알아보겠습니다.

object 키워드의 의미와 사용

코틀린에서 object 키워드는 클래스를 정의하는 동시에 객체를 생성하는 것이라고 볼 수 있습니다.

Kotlin에는 Java에서 쓰이는 static 키워드가 존재하지 않기 때문에 object를 사용해 static의 개념을 표현합니다.

 

object 키워드는 주로 다음과 같은 경우에 사용됩니다.

1. 싱글톤(Singleton) 클래스 정의

2. 동반객체(companion object) 생성

3. 익명 클래스 생성

 

1. 싱글톤(Singleton) 클래스 정의

싱글톤(Singleton)은 프로젝트에서 어떤 객체를 매번 생성하지 않고 하나의 객체만 생성해 하나의 객체를 재사용하는 방식으로 싱글톤 패턴이라고도 합니다.

 

싱글톤으로 생성된 객체는 전역에서 접근할 수 있고, 객체를 단 한 번만 생성하기 때문에 메모리를 효율적으로 사용할 수 있다는 장점이 있습니다.

object RetrofitInstance {
    const val BASE_URL = "BASE_URL"

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build()
    }
}

위 코드에서 RetrofitInstance는 class가 아닌 object로 생성되었기 때문에 싱글톤으로 동작하며 한 번의 생성으로 프로젝트 내의 모든 클래스에서 접근이 가능합니다.

 

2. 동반객체(companion object) 생성

코틀린에는 static 키워드를 제공하지 않지만 static처럼 표현하는 방법으로 companion object(동반 객체)라는 개념이 존재합니다. 

companion object는 클래스 내부에 존재하며, 클래스 내부의 companion object는 해당 클래스가 공유하는 메소드나 프로퍼티를 정의할 수 있습니다.

companion object 또한 하나의 객체이므로 객체의 이름을 지정하거나 상속이 가능하고, 하나의 클래스는 최대 한 개의 companion object를 정의할 수 있습니다.

 

또한, Companion obect를 통해 top-level function을 통해 같은 효과를 낼 수 있습니다. top-level function은 class 내부에 선언된 private property에는 접근할 수 없는 제한을 받지만, Companion object는 class 내부에 접근이 가능합니다.

class Number {
    companion object {
        const val MAX_NUMBER: Int = 1000
        fun bar() {
            println("Companion object called")
        }
    }
}

fun main() {
    println(Number.MAX_NUMBER)    // 1000
    A.bar() // "Companion object called"
}

3. 익명 클래스 생성

익명 클래스는 정의 그대로 이름이 없는 익명의 객체입니다.

한 번만 사용하고 재사용하지 않기 때문에 별도로 이름을 부여하지 않지만 싱글톤과 달리 매번 객체를 생성한다는 점에서 객체를 한 번만 생성하는 싱글톤과 차이가 있습니다.

productListAdapter.setItemClickListener(object : ProductListAdapter.OnItemClickListener {
    override fun onClick(v: View, position: Int) {
        // 클릭 시 실행
        ...
    }
})

private lateinit var itemClickListener: OnItemClickListener

interface OnItemClickListener {
    fun onClick(v: View, position: Int)
}

fun setItemClickListener(onItemClickListener: OnItemClickListener) {
    this.itemClickListener = onItemClickListener

 


 

Object vs CompanionObject

Object

Object는 클래스 전체가 하나의 Singleton 객체로 선언됩니다.

앱의 여러 곳에서 사용될 수 있는 상수나 유틸리티 메서드가 필요할 때 사용합니다. 특히, 싱글턴 패턴이 필요할 경우에 적합합니다.

  • Singleton 형태
  • thread-safe하게 생성이 가능하다.
  • lazy initialized : 실제로 사용될 때 초기화(initialized) 된다
  • var, val, const val로 선언된 상수는 static 변수

위와 같은 특징 중에서도 Object declaration의 장점은 thread-safe와 lazy initiallized라고 볼 수 있습니다.

  • Thread-safe란? 멀티 스레드 환경에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램 실행에 문제가 없는 것
  • lazy initialization란? Object 키워드가 선언된 클래스는 외부에서 객체가 사용되는 시점에 초기화가 이루어진다.

참고로 Object 키워드가 선언된 클래스는 주/부 생성자를 사용할 수 없습니다. 객체 생성과 동시에 생성자 호출 없이 바로 만들어지기 때문입니다.

또한 중첩 object 선언이 가능하며, 클래스나 인터페이스를 상속할 수 있습니다.

Object는 왜 Thread-Safe한걸까?

: 코틀린에서 obejct를 decompile해보면 인스턴스의 생성과 변수의 초기화는 static 초기화 블록에서 수행(static 초기화 블록은 클래스가 메모리에 로딩될 때 자동으로 수행)되는데, static block은 synchronized키워드 없이도 synchronized 블록 처럼 동작하기 때문에 thread-safe한 것이다.

 

CompanionObject

Companion object는 클래스가 메모리에 적재되면서 함께 생성되는 동반(companion)되는 객체이며, 클래스 내부의 객체 선언을 위한 object키워드입니다. 

한마디로 클래스 내부에서 Singleton패턴을 구현하기 위해 사용하며, Object와 달리 일부분이 Singleton 객체로 선언되는 것입니다.

일반적으로 클래스에 연관된 유틸리티 함수나 팩토리 메소드를 정의할 때 사용됩니다.

  • 해당 클래스(companion obejct 는 클래스 내부에 들어가는 블럭이므로) 자체가 static 이 아님.
  • 즉, CompanionObjectTest() 로 생성할 때 마다 객체의 주소값은 다름.
  • 해당 클래스가 로드될 때 초기화(initialized) 된다.
  • val, const val로 선언된 상수는 static 변수

 

 

초기화가 언제될까?

이해를 돕기 위해 Object와 CompanionObject로 된 클래스를 각각 실행하는 예제를 작성해보자.

 

CompanionObject부터 살펴봅시다.

ComanionObjectTest.kt

class CompanionObjectTest{
    companion object{
        const val cObjectConstVal=0
        val cObjectVal = 1
        init {
            println("companion object init!")
        }
    }
}

fun main(){
    CompanionObjectTest
}

실제 위 코드를 실행해보면 아래와 같이 "companion object init!"이 찍히며, 클래스 로드 시점에 init이 되는것을 확인하실 수 있습니다. 즉, companion object를 실제 사용하지 않고, 이를 내재하고 있는 클래스만 main()함수에서 호출을 해도 init이 되는 것입니다. 

 

실행 결과

 

이제 Object를 살펴봅시다.

class CompanionObjectTest{
    companion object{
        const val cObjectConstVal=0
        val cObjectVal = 1
        init {
            println("companion object init!")
        }
    }

    object ObjectTest{
        const val objectConstVal=0
        val objectVal = 1
        init {
            println("object init!")
        }
    }
}

fun main(){
    CompanionObjectTest
}

위에서 사용했던 클래스 안에 Object 클래스를 추가하였습니다.

main()함수를 실행해볼까요?

실행 결과

companion object의 init()만 호출된 것을 알 수 있는데요.

실제로 object 같은 경우, Object를 실제 사용을 해야 Init이 됩니다.

 

아래 코드를 확인해보시면 이해가 되실 겁니다.

fun main(){
    CompanionObjectTest.ObjectTest
}

 

main함수에서 ObjectTest를 사용하도록 변경하고 실행해보았습니다.

실행결과

Object의 init이 호출이 되는 것을 확인할 수 있습니다.

 

 

 

Java 코드에서는 Object와 CompanionObject가 어떻게 표현될까?

위에서 작성한 코드에 대해 디 컴파일을 해보았습니다.

 

CompanionObject

ComanionObjectTest.java

public final class CompanionObjectTest {
   public static final int cObjectConstVal = 0;
   private static final int cObjectVal = 1;
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   static {
      String var0 = "companion object init!";
      System.out.println(var0);
   }

   public static final class Companion {
      public final int getCObjectVal() {
         return CompanionObjectTest.cObjectVal;
      }

      private Companion() {
      }
   }
}

 

코틀린에서 선언했던 const val, val 로 선언했던 변수들이 static으로 변환되어 상단에서 초기화 되는것을 확인할 수 있었습니다. 따라서, 클래스 로드 시점에 초기화가 진행되며, 컴파일 타임에 값이 결정됩니다. 이들은 JVM의 Method Area의 Constant Pool에 저장이 되게 됩니다.

단, val로 선언된 부분은 private로 선언되었기 때문에 getter를 통해 호출을 해야하는 차이가 있습니다.

 

Object

자 그럼 Object도 Java클래스로 디컴파일을 해봐야겠지요?

   
   public final class CompanionObjectTest {
   	..
	..
       public static final class ObjectTest {
          public static final int objectConstVal = 0;
          private static final int objectVal;
          @NotNull
          public static final ObjectTest INSTANCE;

          public final int getObjectVal() {
             return objectVal;
          }

          private ObjectTest() {
          }

          static {
             ObjectTest var0 = new ObjectTest();
             INSTANCE = var0;
             objectVal = 1;
             String var1 = "object init!";
             System.out.println(var1);
          }
       }
 }

companion object와 동일하게 object도 코틀린에서 선언했던 const val, val 로 선언했던 변수들이 static으로 변환이 되는 것을 확인할 수 있었습니다. 하지만 companion object와 달리 val로 선언했던 부분은 상단에서 초기화가 되지 않고 static 블록에서 초기화를 해주고 있네요. 

 

 

이 둘은 static 변수이기 때문에 JVM 메모리 영역 중 Method Area에 저장이 되지만 아래와 같은 차이가 있습니다.

  • const val: public static final로 선언되어 초기값인 0으로 상수 풀에 저장됩니다. 이는 변경 불가능하며, 클래스가 로드될 때 초기화가 이루어지므로, 컴파일 타임에 값이 결정 됩니다.
  • val은 private static final로 선언되어 있지만, 정적 초기화 블록에서 초기화가 이루어지므로, 클래스가 로드된 후 결정 됩니다. (런타임에 값이 결정)
    초기값이 지정되기 전까지는 메모리에 저장되지 않습니다. (클래스가 로드된 후, 정적 초기화 블록이 실행됩니다.)

또한, Object를 디컴파일 했을 때 Instance 객체가 생성되는 것을 확인할 수 있습니다. 이는 static 블록에서 초기화가 되며, 인스턴스를 생성하여 메모리의 힙 영역에 객체를 할당합니다. 이 인스턴스를 통해 private로 선언된 멤버 변수에 접근을 할 수 있습니다. (위 코드에서는 objectVal)

 

멤버 변수가 val, var이 아닌 객체라면 ?

위 예제를 통해 companion object와 object에서의 val, var로 선언한 멤버변수가 실제 자바 코드로 어떻게 표현이 되는지 에 대해 알아 보았습니다.

그럼 객체는 어떻게 표현이 될까요? 

data class Dclass(
        val member1: Int,
        val member2: Int
)
class CompanionObjectTest{
    companion object{
        val dClass= Dclass(0,1)
        init {
            println("companion object init!")
        }
    }

    object ObjectTest{
        val dClass= Dclass(0,1)
        init {
            println("object init!")
        }
    }
}

 

위 코드는 아래와 같이 디컴파일 됩니다.

public final class CompanionObjectTest {
   @NotNull
   private static final Dclass dClass = new Dclass(0, 1);

   static {
      String var0 = "companion object init!";
      System.out.println(var0);
   }

   public static final class ObjectTest {
      @NotNull
      private static final Dclass dClass;
      @NotNull
      public static final ObjectTest INSTANCE;

      @NotNull
      public final Dclass getDClass() {
         return dClass;
      }

      private ObjectTest() {
      }

      static {
         ObjectTest var0 = new ObjectTest();
         INSTANCE = var0;
         objectVal = 1;
         dClass = new Dclass(0, 1);
         String var1 = "object init!";
         System.out.println(var1);
      }
   }
}

앞서 설명드린 바와 같이 객체도 동일한 위치에서 초기화가 진행이 되는 것 같습니다.

  • companion object: 클래스 로딩 시점에 객체가 힙 영역에 생성되고, 이 객체의 참조가 정적 변수에 할당됩니다
  • object: 클래스가 로드될 때, dClass는 초기값이 할당되지 않은 상태로 메모리에 존재합니다.
    정적 초기화 블록이 실행될 때 new Dclass(0, 1)이 호출되면서 dClass가 초기화됩니다. 이 초기화는 클래스가 로드된 후, 정적 초기화 블록이 실행될 때 이루어집니다.

 

번외: Top Level Property

const val topLevelConstVal:Int = 0
val topLevelVal: Int = 1
val dClass= Dclass(0,1)

fun main(){

}

 

Top Level Property에 대해서도 디컴파일을 해보자.

public final class JtmKt {
	public static final int topLevelConstVal = 0;
	private static final int topLevelVal = 1;
	@NotNull
	private static final Dclass dClass = new Dclass(0, 1);
}

클래스 이름은 최상위 함수가 들어있던 코틀린의 소스 파일의 이름으로 자동 생성된다.

companion obejct와 동일하게 최상단에서 정적으로 초기화가 됨을 확인할 수 있었다!

따라서, 아래와 같이 정리해볼 수 있다.

Top Level Property

  • 해당 클래스가 로드될 때 초기화(initialized)가 되며, 컴파일 타임에 값이 결정
  • JVM의 Method Area의 Constant Pool에 저장 (객체 제외)

 

 


참고자료

https://develop-oj.tistory.com/47

 

[안드로이드] 코틀린에서 object의 정의와 사용

이번 포스팅에서는 Kotlin에서 object 키워드가 무엇을 의미하고 class와 object는 어떤 차이가 있는지에 대해 알아보겠습니다. object 키워드의 의미와 사용 코틀린에서 object 키워드는 클래스를 정의하

develop-oj.tistory.com

https://tourspace.tistory.com/109

 

[Kotlin] 코틀린 object

이 글은 Kotlin In Action을 참고 하였습니다.더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다코틀린은 object란 키워드를 사용합니다.자바에는 이 키워드가 없죠.약간 생소할

tourspace.tistory.com

https://sikdroid.tistory.com/17

 

코틀린 Object와 Companion Object 차이? - kotlin

오늘은 비슷한거 같으면서 다른 Object declaration와 Companion object의 차이에 대해서 알아보려한다. 보통 Kotlin에서 Java의 static과 같은 정적 변수 및 메서드를 사용하기 위해 보통 object나 Companion object

sikdroid.tistory.com

https://jaeyeong951.medium.com/%EC%BD%94%ED%8B%80%EB%A6%B0-object-companion-object-3237ecea8df4

 

코틀린 object & companion object

object 키워드는 코틀린에서 singleton 객체를 thread-safe 하게 생성하는 가장 간편한 문법이며, companion object 키워드로 자바의 static 키워드와 동일한 경험을 할 수 있다.

jaeyeong951.medium.com

 

'Kotlin' 카테고리의 다른 글

[Kotlin] open class와 abstract class  (0) 2024.11.02
[Kotlin] 깊은 복사 (Deep Copy) 3가지 방법  (0) 2024.11.02
[Kotlin] Data Class란?  (3) 2024.11.02
[Kotlin] Sealed Class란?  (2) 2024.11.02
[Kotlin] 늦은 초기화 (lateinit, by lazy)  (0) 2023.08.22