Companion Object

sumi Yoo·2022년 10월 7일
0

Companion Object

자바의 static 키워드는 클래스 멤버임을 지정하기 위해 사용한다. static이 붙은 멤버는 클래스가 메모리에 적재될 때 자동으로 함께 생성되므로 인스턴스 생성 없이도 클래스명 다음에 점(.)을 쓰면 바로 참조할 수 있습니다.

하지만 코틀린에는 static이 없고, 대신에 companion object라는 키워드와 함께 블록안에 멤버를 구성한다.

선언 위치에 따른 멤버 종류 (변수, 메소드)

public class test {

	int iv; // 인스턴스 변수
	static int cv; // 클래스 변수
	
	void method() {
		int lv; // 지역 변수
	}
}
  • 클래스 변수
    클래스 변수는 인스턴스 변수에 static만 붙여주면 됩니다. 인스턴스 변수는 각각 고유한 값을 가지지만 클래스 변수는 모든 인스턴스가 공통된 값을 공유하게 됩니다. 한 클래스의 모든 인스턴스들이 공통적인 값을 가져야할 때 클래스 변수로 선언합니다. 클래스가 로딩될 때 생성되어(그러므로 메모리에 딱 한번만 올라갑니다.) 종료 될 때 까지 유지되는 클래스 변수는 public 을 붙이면 같은 프로그램 내에서 어디서든 접근할 수 있는 전역 변수가 됩니다. 또한 인스턴스 변수의 접근법과 다르게 인스턴스를 생성하지 않고 클래스이름.클래스변수명 을 통해서 접근할 수 있습니다.
  • 인스턴스 변수
    인스턴스 변수는 인스턴스가 생성될 때 생성됩니다. 그렇기 때문에 인스턴스 변수의 값을 읽어오거나 저장하려면 인스턴스를 먼저 생성해야합니다. 인스턴스 별로 다른 값을 가질 수 있으므로, 각각의 인스턴스마다 고유의 값을 가져야할 때는 인스턴스 변수로 선언합니다.
  • 지역 변수
    메서드 내에서 선언되며 메서드 내에서만 사용할 수 있는 변수입니다. 메서드가 실행될 때 메모리를 할당 받으며 메서드가 끝나면 소멸되어 사용할 수 없게 됩니다.

object 키워드

코틀린에는 자바에 없는 독특한 싱글턴(singleton: 인스턴스가 하나만 생성되는 클래스) 선언 방법이 있습니다. class 대신 object 키워드를 사용하면 됩니다.

object MySingleton{
    val prop = "나는 MySingleton의 속성이다."
    fun method() = "나는 MySingleton의 메소드다."
}
fun main(args: Array<String>){
    println(MySingleton.prop);    //나는 MySingleton의 속성이다.
    println(MySingleton.method());   //나는 MySingleton의 메소드다.
}

Companion object는 static이 아니다.

사실 코틀린 companion object는 static이 아니며 사용하는 입장에서 static으로 동작하는 것처럼 보일 뿐이다.

class MyClass2{
    companion object{
        val prop = "나는 Companion object의 속성이다."
        fun method() = "나는 Companion object의 메소드다."
    }
}
fun main(args: Array<String>) {
    //사실은 MyClass2.맴버는 MyClass2.Companion.맴버의 축약표현이다.
    println(MyClass2.Companion.prop)
    println(MyClass2.Companion.method())
}

companion object{}는 MyClass2 클래스가 메모리에 적재되면서 함께 생성되는 동반되는 객체이고 이 동반 객체는 클래스명.Companion으로 접근할 수 있다는 점이다. MyClass2.Companion.prop 과 MyClass2.prop 는 동일한 결과를 보입니다. 축약 표현일 뿐이다. 언어적으로 지원하는 축약하는 표현 때문에 companion object가 static으로 착각이 드는 것이다.

Companion object는 객체이다.

class MyClass2{
    companion object{
        val prop = "나는 Companion object의 속성이다."
        fun method() = "나는 Companion object의 메소드다."
    }
}
fun main(args: Array<String>) {
    println(MyClass2.Companion.prop)
    println(MyClass2.Companion.method())

    val comp1 = MyClass2.Companion  //--(1)
    println(comp1.prop)
    println(comp1.method())

    val comp2 = MyClass2  //--(2)
    println(comp2.prop)
    println(comp2.method())
}

companion object는 객체이므로 변수에 할당할 수 있다. 그리고 할당한 변수에서 점으로 MyClass2에 정의된 companion object의 멤버에 접근할 수 있다. 이렇게 변수에 할당하는 것은 자바의 클래스에서 static 키워드로 정의된 멤버로는 불가능한 방법이다.

위 코드에서 .Companion을 빼고 직접 MyClass2로 할당했다. 이것도 또한 MyClass2에 정의된 companion object이다.

static 키워드만으로는 클래스 멤버를 companion object처럼 하나의 독립된 객체로 여겨질 수 없다.

Companion object에 이름을 지을 수 있다.

companion object의 기본 이름은 companion인데, 바꿀 수 있다.

class MyClass3{
    companion object MyCompanion{  // -- (1)
        val prop = "나는 Companion object의 속성이다."
        fun method() = "나는 Companion object의 메소드다."
    }
}
fun main(args: Array<String>) {
    println(MyClass3.MyCompanion.prop) // -- (2)
    println(MyClass3.MyCompanion.method())

    val comp1 = MyClass3.MyCompanion // -- (3)
    println(comp1.prop)
    println(comp1.method())

    val comp2 = MyClass3 // -- (4)
    println(comp2.prop)
    println(comp2.method())

    val comp3 = MyClass3.Companion // -- (5) 에러발생!!!
    println(comp3.prop)
    println(comp3.method())
}

하지만, 이름을 지었을 경우 더이상 .Companion으로 접근할 수 없다.

클래스 내 Companion object는 딱 하나만 쓸 수 있다.

클래스 명만으로 companion object 객체를 참조할 수 있기 때문에 한 번에 2개를 참조하는 것은 불가능하다.

class MyClass5{
    companion object MyCompanion1{
        val prop1 = "나는 Companion object의 속성이다."
        fun method1() = "나는 Companion object의 메소드다."
    }
    companion object MyCompanion2{ // --  에러발생!! Only one companion object is allowed per class
        val prop2 = "나는 Companion object의 속성이다."
        fun method2() = "나는 Companion object의 메소드다."
    }
}

이름을 별도로 부여해도 마찬가지로 불가능하다.

덕분에 자바에서 static 멤버를 클래스에 아무데나 쓰는게 제약이 없었다면 코틀린에서는 자동으로 한곳에 모이게 된다.

인터페이스 내에도 Companion object를 정의할 수 있다.

인터페이스 수준에서 상수항을 정의할 수 있고, 관련된 중요 로직을 이곳에 기술할 수 있다.

상속 관계에서 Companion object 멤버는 같은 이름일 경우 가려진다. (shadowing)

open class Parent{
    companion object{
        val parentProp = "나는 부모값"
    }
    fun method0() = parentProp
}
class Child:Parent(){
    companion object{
        val childProp = "나는 자식값"
    }
    fun method1() = childProp
    fun method2() = parentProp
}
fun main(args: Array<String>) {
    val child = Child()
    println(child.method0()) //나는 부모값
    println(child.method1()) //나는 자식값
    println(child.method2()) //나는 부모값
}

다른 이름이라면 문제가 없다. 하지만 같은 이름이면 어떻게 될까?

open class Parent{
    companion object{
        val prop = "나는 부모"
    }
    fun method0() = prop //Companion.prop과 동일
}
class Child:Parent(){
    companion object{
        val prop = "나는 자식"
    }
    fun method1() = prop //Companion.prop 과 동일
}
fun main(args: Array<String>) {
    println(Parent().method0()) //나는 부모
    println(Child().method0()) //나는 부모
    println(Child().method1()) //나는 자식

    println(Parent.prop) //나는 부모
    println(Child.prop) //나는 자식

    println(Parent.Companion.prop) //나는 부모
    println(Child.Companion.prop) //나는 자식
}

prop이 부모에서 정의 되어 있지만 가려져서 무시되는 것을 볼 수 있다.

Child().method0() 메소드는 Parent 클래스의 것이기 때문에 부모의 companion object의 prop 값인 "나는 부모"가 출력된다.

open class Parent{
    companion object{
        val prop = "나는 부모"
    }
    fun method0() = prop
}
class Child:Parent(){
    companion object ChildCompanion{ // -- (1) ChildCompanion로 이름을 부여했어요.
        val prop = "나는 자식"
    }
    fun method1() = prop
    fun method2() = ChildCompanion.prop
    fun method3() = Companion.prop
}
fun main(args: Array<String>) {
    val child = Child()
    println(child.method0()) //나는 부모
    println(child.method1()) //나는 자식
    println(child.method2()) //나는 자식
    println(child.method3()) // -- (2)
}

주석 2는 "나는 부모"가 출력된다. 자식의 이름을 바꿨기 때문에 Companion은 부모가 된다.

open class Parent{
    companion object ParentCompanion{ // -- (1) ParentCompanion로 이름을 부여했어요.
        val prop = "나는 부모"
    }
    fun method0() = prop
}
class Child:Parent(){
    companion object ChildCompanion{
        val prop = "나는 자식"
    }
    fun method1() = prop
    fun method2() = ChildCompanion.prop
    fun method3() = Companion.prop // -- (2) Unresolved reference: Companion 에러!!
}

주석이 에러가 난다. ParentCompanion.prop 로 바꿔야 한다.

부모/자식의 companion object에 정의된 멤버는 자식 입장에서 접근할 수 있지만, 같은 이름을 쓰면 섀도잉 되어 감춰진다는 점을 알 수 있다.

다형성 문제

open class Parent{
    companion object{
        val prop = "나는 부모"
    }
    open fun method() = prop //Companion.prop과 동일
}
class Child:Parent(){
    companion object{
        val prop = "나는 자식"
    }
    override fun method() = prop //Companion.prop 과 동일
}
fun main(args: Array<String>) {
    println(Parent().method()) //나는 부모
    println(Child().method()) //나는 자식

    val p:Parent = Child()
    println(p.method()) // -- (1)
}

다형성은 두 가지를 만족해야 한다.

  1. 대체 가능성(substitution) - 어떤 형을 요구한다면 그 형의 자식형으로 그 자리를 대신할 수 있다.
  2. 내적 동질성(internal identity) - 객체는 그 객체를 참조하는 방식에 따라 변화하지 않는다. 즉 업다운 캐스팅해도 여전히 최초 생성한 그 객체라는 것이다.

이 두 가지 조건을 만족하면 다형성을 만족한다고 볼 수 있고, 다형성을 만족한다면 객체지향 언어라고 볼 수 있다. 따라서 p.method()의 결과는 Child().method() 결과와 같습니다. 그러므로 결과는 "나는 자식"입니다.

private 접근

companion object는 클래스 안에 존재하므로 private 선언된 생성자에 접근 가능하다

class User {
    private val name = "User1"
    companion object {
        val name2 = User().name
    }
}
fun main() {
    println(User.name2)
}

Companion object?

0개의 댓글

관련 채용 정보