DEV Community

Cover image for 코틀린 object & companion object
jaeyeong
jaeyeong

Posted on

코틀린 object & companion object

object 키워드

코틀린은 object 키워드를 통해 싱글턴 패턴을 언어단에서 기본적으로 지원한다. 이말인 즉슨 object 키워드로 객체를 선언한다는 것은 해당 클래스를 정의함과 동시에 해당 클래스의 단일 인스턴스 생성을 동시에 한다는 것과 같다.

object CustomClassByObject {
    var customValue: Int = 20

    fun printCustomValue() {
        println("customValue is : $customValue")
    }
}
Enter fullscreen mode Exit fullscreen mode

코틀린에서 위처럼 object 로 클래스를 정의한 후 위 코드를 자바로 디컴파일 해보면 아래와 같다.

public final class CustomClassByObject {
   private static int customValue;
   @NotNull
   public static final CustomClassByObject INSTANCE;

   public final int getCustomValue() {
      return customValue;
   }

   public final void setCustomValue(int var1) {
      customValue = var1;
   }

   public final void printCustomValue() {
      System.out.println("customValue is : " + customValue);
   }

   private CustomClassByObject() {
   }

   static {
      CustomClassByObject var0 = new CustomClassByObject();
      INSTANCE = var0;
      customValue = 20;
   }
}
Enter fullscreen mode Exit fullscreen mode

우리가 익히 알고있는 싱턴 패턴이 보인다.

  • 클래스의 생성자를 private 으로 선언하여 외부에서 호출할 수 없도록 제한
  • 클래스의 인스턴스를 담은 변수를 static final 로 선언
  • 인스턴스의 생성과 변수의 초기화는 static 초기화 블록에서 수행 (static 초기화 블록은 클래스가 메모리에 로딩될 때 자동으로 수행된다.) (static 블록은 synchronized 키워드 없이도 synchronized 블록처럼 동작하기 때문에 동시성 문제는 걱정하지 않아도 된다. -> thread-safe)

자바였다면 싱글턴 패턴을 구현하기 위해 위 같은 코드를 손수 작성해야 했겠지만 코틀린에서는 object 키워드 하나로 해결할 수 있다.

object 키워드의 활용은 여기서 끝나지 않는다.

interface TestInterface {
    val a: Int
    fun calculate(): Int
}

fun main(args: Array<String>) {
    val someObject = object: TestInterface {
        override val a: Int = 5

        override fun calculate(): Int {
            return a * a + 12
        }
    }

    println(someObject.calculate())
}
Enter fullscreen mode Exit fullscreen mode

위 코드에서 object 키워드가 어떤 기능을 하는지 바로 감이 오는가? 바로 익명 객체이다. 위 코드도 자바로 디컴파일 해보자.

public static final void main(@NotNull String[] args) {
    <undefinedtype> someObject = new TestInterface() {
        private final int a = 5;

        public int getA() {
            return this.a;
        }

        public int calculate() {
            return this.getA() * this.getA() + 12;
        }
    };

    int var2 = someObject.calculate();
    System.out.println(var2);
}
Enter fullscreen mode Exit fullscreen mode

TestInterface 를 구현한 익명 클래스를 생성하는 것을 볼 수 있다. 다만 조금 특이한 점은 익명 클래스의 타입이 TestInterface 가 아니라 컴파일러가 임의로 정의한 “undefinedtype” 이라는 것인데, 이는 아무래도 런타임시에 상속으로 인해 발생할 수 있는 문제들을 방지하는 안전장치 인것으로 보인다.

코틀린으로 안드로이드 프로그래밍을 해봤다면 아래와 비슷한 코드를 본 적이 있을 것이다.

someButton.setOnClickListener(object : OnClickListener() {
    override fun onClick(e: Event) { /*...*/ }
})
Enter fullscreen mode Exit fullscreen mode

object 키워드로 OnClickListener 인터페이스의 구현체를 익명 클래스로 생성하여 setOnClickListener 에 넘겨주는 코드이다. 코틀린을 배우면서 object 키워드를 처음 접한게 안드로이드의 setOnClickListener 였던 것 같다. 그 때는 object 키워드가 어떤 역할을 하는지 제대로 학습하지도 않은채로 마구잡이로 사용했던 기억이 있다.

companion object 키워드

코틀린은 자바에 존재하는 static 키워드가 없다. 이를 모두 object 키워드로 대체하기로 한것이다. 위에서 말했듯 object는 단독으로 사용할 시 singleton 패턴을 구현하는 코드를 자동으로 생성해주는데, 이를 활용하면 개발자가 static 이라는 키워드를 직접 사용할 필요가 없다고 생각한 것이다. 한 클래스 내부에 object 키워드로 클래스를 생성하고 해당 클래스 내부에 static 으로 사용하고 싶은 변수나 메소드를 모두 집어넣는 방식으로 말이다.

class ClassForTest {
    // for static field
    object SomeObjectClass {
        var someValue: Int = 24

        fun printSomeValue() {
            println("SomeValue is $someValue")
        }
    }
}

fun main(args: Array<String>) {

    ClassForTest.SomeObjectClass.printSomeValue()
    // → 24
}
Enter fullscreen mode Exit fullscreen mode

위 코드처럼 static 으로 사용하고 싶은 변수나 메소드를 object 클래스안에 넣고 클래스.object클래스.변수/메소드 처럼 사용하는 것이다. 하지만 이 방식도 마냥 좋아보이지만은 않는다. static 필드에 접근할 때 마다 object 클래스이름을 반복적으로 타이핑해야하는 것이다. 너무 번거롭다.

그래서 이마저도 없애주고자 코틀린 디자이너들이 고안한 syntax sugar 가 companion object 이다.

class ClassForTest {
    companion object {
        val someValueInCompanionObject = 24

        fun printSomeValue() {
            println(someValueInCompanionObject)
        }
    }
}

fun main(args: Array<String>) {

    ClassForTest.printSomeValue()
    // → 24
}
Enter fullscreen mode Exit fullscreen mode

static 필드에 훨씬 자연스럽고 간결한 코드로 접근할 수 있다! 매번 object 클래스에 접근해서 사용하지 않고 해당 변수나 메소드에 직접 접근해서 사용하는 것처럼 보인다.

기존 object 키워드와 어떤점이 다르기에 이런 동작이 가능한걸까?

class ClassForTest {
    companion object {
        val someValueInCompanionObject = 24

        fun printSomeValue() {
            println(someValueInCompanionObject)
        }
    }

    object ObjectForTest {
        val someValueInObject = 24

        fun printSomeValue() {
            println(someValueInObject)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

위 처럼 한 클래스 내부에 companion object 와 object 를 각각 선언했을 때 기능과 그 구현이 어떻게 다른지 자바로 디컴파일한 코드를 통해 알아보자. 가독성을 위해 부모 클래스인 ClassForTest 는 생략하고 하위 2개의 object 클래스만 기술했다.

  • object ObjectForTest (decompiled)
public static final class ObjectForTest {
      private static final int someValueInObject;

      @NotNull
      public static final ClassForTest.ObjectForTest INSTANCE;

      public final int getSomeValueInObject() {
         return someValueInObject;
      }

      public final void printSomeValue() {
         int var1 = someValueInObject;
         System.out.println(var1);
      }

      private ObjectForTest() {
      }

      static {
         ClassForTest.ObjectForTest var0 = new ClassForTest.ObjectForTest();
         INSTANCE = var0;
         someValueInObject = 24;
      }
   }
Enter fullscreen mode Exit fullscreen mode
  • companion object (decompiled)
private static final int someValueInCompanionObject = 24;

   @NotNull
   public static final ClassForTest.Companion Companion 
        = new ClassForTest.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      public final int getSomeValueInCompanionObject() {
         return ClassForTest.someValueInCompanionObject;
      }

      public final void printSomeValue() {
         int var1 = ((ClassForTest.Companion)this).getSomeValueInCompanionObject();
         System.out.println(var1);
      }

      private Companion() {
      }

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

둘의 구현방식은 부모 클래스 내부에 static final 로 선언된다는 점에서는 동일하다.
차이점이라면 companion object 는 해당 클래스 외부(부모 클래스 내부)에 선언되고 초기화되지만 object 는 해당 클래스 내부에 static 블록을 통해 초기화된다는 점,
그리고 companion object 는 우리가 따로 이름을 정해주지 않아도 Companion 이라는 static class를 자동으로 생성해줌과 동시에 우리가 코틀린 코드에서 사용할 때 ClassForTest.Companion.printSomeValue() 와 같이 사용하지 않고 ClassForTest.printSomeValue() 처럼 클래스이름을 생략하여 자연스럽고 간결한 코드로 사용할 수 있게 해준다는 점이다.


summary

object 키워드는 코틀린에서 singleton 객체를 thread-safe 하게 생성하는 가장 간편한 문법이다.
코틀린은 이를 활용해 개발자가 object 혹은 companion object 키워드로 자바의 static 키워드와 동일한 경험을 할 수 있도록 코드를 자동으로 생성해준다.
마지막으로, object 키워드로 익명 클래스를 boilerplate 코드 없이 손쉽게 구현할 수 있다.

Top comments (0)