본문 바로가기
안드로이드/Kotlin

[Kotlin] 객체 선언(Object Declaration) , 동반 객체(Companion Object)

by jinwo_o 2024. 12. 9.
  • 코틀린 클래스 안에는 정적인 멤버가 없다. 자바 static 키워드를 지원하지 않는다.
  • 그 대신 코틀린에서는 패키지 수준의 최상위 함수와 객체 선언을 활용한다.
  • 대부분의 경우 최상위 함수를 활용하는 편을 더 권장한다. 
    • 그러나 최상위 함수는 특정 클래스의 private 멤버에 접근할 수 없기 때문에, 클래스 내부에 정의된 static 멤버가 다른 클래스 멤버와 상호작용해야 하는 경우에는 적합하지 않다.
    • static 을 최상위 함수로 대체할 수 없는 경우에는 object 를 고려한다.

 

객체 선언(Object Declaration) : 싱글톤

  • 클래스 선언과 그 클래스에 속한 단일 인스턴스의 생성을 동시에 처리해주기 때문에, 싱글톤에 사용하기 적합하다.
  • 클래스 전체가 하나의 싱글톤 객체로 선언되고, 처음 사용될 때까지 초기화가 지연된다.
  • 클래스와 마찬가지로 객체 선언 안에도 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있다. 하지만 생성자는(주 생성자와 부 생성자 모두) 객체 선언에 쓸 수 없다.
    • 일반 클래스 인스턴스와 달리, 싱글톤 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.
    • 인터페이스를 구현해야 하는데, 그 구현 내부에 다른 상태가 필요하지 않은 경우에 유용하다.
  • 클래스 안에서 객체 선언을 사용하더라도 객체 선언의 인스턴스는 단 하나만 생성된다.
  • 대규모 소프트웨어 시스템에서는 객체 생성을 제어할 방법이 없고 생성자 파라미터를 지정할 수 없어 객체 선언이 항상 적합하지는 않다. 
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}

val persons = listOf(Person("B"), Person("C"), Person("A"))
println(persons.sortedWith(Person.NameComparator)) // [Person(name=A), Person(name=B), Person(name=C)]
  • 함수가 여러 객체와 관계를 맺지 않고 하나의 특정 객체(Person)와만 관계를 맺는다면, 최상위 함수보다는 클래스 내부에 정의하는 것이 좋다. (캡슐화)
data class Person(val name: String) : Comparator<Person> {
    override fun compare(p1: Person, p2: Person): Int {
        return p1.name.compareTo(p2.name)
    }
}

val persons = listOf(Person("B"), Person("C"), Person("A"))
println(persons.sortedWith(Person("A"))) // [Person(name='A'), Person(name='B'), Person(name='C')]
  • Comparator 를 직접 상속받으면 sortedWith(Person("A"))와 같이 사용해야 하는데, 이는 매우 부자연스럽다.

 

동반 객체(Companion Object)

  • 객체 선언처럼 클래스가 로드되는 시점에 인스턴스가 단 하나만 생성된다.
  • 클래스 내에 일부분이 싱글톤 객체로 선언되고, 클래스의 인스턴스가 로드될 때 즉시 초기화 된다.
  • 동반 객체 선언도 일반 객체 선언처럼 상속이나 함수, 프로퍼티를 가질 수 있다.
  • 객체 선언은 한 클래스 내에 여러개 존재할 수 있지만, 동반 객체는 단 하나만 존재할 수 있다.
  • 동반 객체를 포함하는 클래스를 확장해야하는 경우에는 동반 객체 멤버를 하위 클래스에서 오버라이드할 수 없으므로 부 생성자를 사용하는 편이 더 낫다.
  • 동반 객체에 이름을 지정하지 않는 경우 자동으로 Companion 이 이름이 된다.
  • 이름을 붙이건 안붙이건 동반 객체 내부의 메소드는 그냥 호출할 수 있다.
    • 객체 선언에서는 Person.NameComparator.compareTo()로 내부 객체를 명시해 주어야 하지만, 동반 객체는 그냥 Person.fromJSON()으로 사용했다는 점에서 차이가 있다.
  • 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. 따라서 동반 객체는 바깥쪽 클래스의 private 생성자도 호출할 수 있다. (팩토리 메서드 패턴)
class User private constructor(val nickname: String) { // 주 생성자를 비공개로 만든다.
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))
        
        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookUser = User.newFacebookUser(4)
println(subscribingUser.nickname) // bob
  • 동반 객체는 클래스 안에 정의된 일반 객체로서 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person {
            TODO("not implemented")
        }
    }
}

fun loadFromJSON<T>(factory: JSONFactory<T>): T {
    . . .
}

val p = Person.fromJSON(json)
loadfromJSON(p)
  • 동반 객체의 메소드도 확장 함수로 정의할 수 있기 때문에 함수의 정의를 바깥쪽 클래스와 분리할 수 있다.
// Person 은 비즈니스 로직 모듈이므로, JSON 역직렬화 함수는 서버/클라이언트 통신 모듈에 두고싶다면
class Person(val name: String) {
    companion object {}  // 빈 동반 객체
}

fun Person.Companion.fromJSON(json: String): Person {
    TODO()
}

val p = Person.fromJSON(json)

 

Kotlin In Action 4장

클래스와 인터페이스. 뻔하지 않은 생성자와 프로퍼티. 데이터 클래스. 클래스 위임. object 키워드 사용. 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메소드도 정의할 수 있다.

velog.io

 

(Kotlin) 싱글턴 object(static) - 객체 선언, 동반 객체, 객체 식 / 익명, 무명

클래스를 정의하면서 동시에 인스턴스를 생성한다.

umbum.dev

 

Kotlin object의 초기화 시점과 companion object의 초기화 시점 차이 알아보기

object와 companion object의 초기화 시점 object는 싱글톤 인스턴스를 간편하게 만들기 위해 Kotlin에서 제공해주는 방법이다. object를 작성하면 싱글톤 패턴을 구현하기 위한 긴 코드를 작성할 필요 없이

kotlinworld.com