프로그래밍 노트/Kotlin

[Kotlin] 코틀린 object/companion 클래스 (동반객체)

깡냉쓰 2020. 11. 30. 23:01
728x90
반응형

object 키워드를 사용하는 여러 상황

  • 객체 선언(object declaration)은 싱글턴을 정의하는 방법 중 하나다.
  • 동반 객체(companion object)는 인스턴스 메소드는 아니지만 어떤 클래스와 관련 있는 메소드와 팩토리 메소드를 담을 때 쓰인다. 동반 객체 메소드에 접근할 때는 동반객체가 포함된 클래스의 이름을 사용할 수 있다.
  • 객체 식은 자바의 무명 내부 클래스(anonymous inner class)대신 쓰인다.

객체선언: 싱글턴을 쉽게 만들기

코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스 의 선언을 합친 선언이다.

한 회사에 여러 급여 대장이 필요하지 않을테니 회사 급여 대장은 싱글턴으로 쓰는게 정당하다.

object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary(){
        for (person in allEmployees) {
            ...
        }
    }
}

객체 선언은 object 키워드로 시작하며, 클래스와 마찬가지로 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있다. 하지만 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어지기 때문에 생성자를 쓸 수 없다. 따라서 객체 선언에는 생성자 정의가 필요 없다.

Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()

클래스나 인터페이스를 상속할 수 있다. 두 파일 경로를 대소문자 관계없이 비교해주는 Comparator를 구현해보자.

object CaseInsensitiveFileComparator: Comparator<File> {
    override fun compare(file1: File, file2: File): Int{
        return file1.path.compareTo(file2.path, ignoreCase =true)
    }
}
>>> println(CaseInsensitiveFileComparator(File("/User"), File("/user"))
0
// 일반 객체를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.
>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[/a, /Z]

클래스 안에서 객체를 선언할 수도 있는데 그런 객체도 인스턴스는 단 하나뿐이다. (바깥 클래스의 인스턴스마다 중첩 객체 선언에 해당하는 인스턴스가 하나씩 따로 생기는 것이 아님)

ex. 어떤 클래스의 인스턴스를 비교하는 Comparator를 클래스 내부에 정의하는게 더 바람직하다.

data class Person(val name: String){
    object NameComparator: Comparator<Person>{
        override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
    }
}

>>> var persons = listOf(Person("Corn"), Person("ACorn"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name=ACorn), Person(name=Corn)]

자바에서 코틀린 객체를 사용하는 방법

// 자바 코드에서 코틀린 싱글턴 객체를 사용하려면 정적인 INSTANCE 필드를 통하면 된다.
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);

동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소

코틀린 언어에서는 static 키워드를 지원하지 않는다. 대신 패키지 수준의 최상위 함수(자바의 정적 메소드 역할을 거의 대신할 수 있음)와 객체 선언(자바의 정적 메소드 역할 중 코틀린 최상위 함수가 대신할 수 없는 역할이나 정적 필드를 대신할 수 있다)를 활용한다.

대부분의 경우 최상위 함수를 활용하는 것을 권장하지만 private 으로 표시된 클래스 비공개 멤버에 접근할 수 없다. 그래서 클래스의 인스턴스와 관계없이 호출해야 하지만, 클래스 내부 정보에 접근해야 하는 함수가 필요할 때는 클래스에 중첩된 객체 선언의 멤버 함수로 정의해야 한다. 그런 함수의 대표적인 예로 팩토리 메소드를 들 수 있다.

클래스 안에 정의된 객체 중 하나에 companion이라는 특별한 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다. 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다.

class A{
    companion object{
        fun bar(){
            println("Companion object called")
        }
    }
}

>>> A.bar()
Companion object called

동반 객체가 private 생성자를 호출하기 좋은 위치다. 동반 객체는 자신을 둘러싼 클래스의 모든 pirvate 멤버에 접근할 수 있다. 따라서 동반 객체는 바깥쪽 클래스의 private 생성자도 호출할 수 있다. 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치다.

// 부 생성자가 2개 있는 클래스
class User{
    var nickname: String
    // 부 생성자
    constructor(email: String){
        nickname = email.substringBefore('@')
    }
    constructor(facebookAccountId: Int){
        nickname = getFacebookName(facebookAccountId)
    }
}

더 유용한 방법으로 클래스의 인스턴스를 생성하는 팩토리 메소드가 있다.

// 동반 객체안에서 팩토리 클래스를 정의하는 방식
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

동반 객체를 일반 객체처럼 사용

동반 객체는 클래스 안에 정의된 일반 객체다. 따라서 동반 객체에 이름을 붙이거나, 동반 객체가 인터페이스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.

// 동반 객체에 이름 붙이기
class Person(val name: String){
    companion object Loader{
        fun fromJSON(jsonText: String): Preseon = ...
    }
}
// 두 방법 모두 호출 가능하다.
>>> person = Person.Loader.fromJSON("{name:'corn'}")
>>> person.name
corn
>>> person2 = Person.Loader.fromJSON("{name:'corn'}")
>>> person2.name
Corn

동반 객체에서 인터페이스 구현

동반 개체도 인터페이스를 구현할 수 있다. 인터페이스를 구현하는 동반 객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있다.

⇒ JSON을 역직렬화하는 JSONFactory 인터페이스 존재

// 동반 객체에서 인터페이스 구현하기
interface JSONFactory<T>{
    fun fromJSON(jsonText: String): T
}
class Person(val name: String){
    companion object: JSONFactory<Person>{
        // 동반객체가 인터페이스 구현
        override fun fromJSON(jsonText: String): Person = ...
    }
}

JSON으로부터 각 원소를 다시 만들어내는 추상 팩토리가 있다면 Person 객체를 그 팩토리에게 넘길 수 있다.

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

동반 객체가 구현한 JSONFacotry인스턴스를 넘길 때 Person 클래스의 이름을 사용했다.

동반 객체 확장

C 라는 클래스 안에 동반 객체가 있고 그 동반 객체(C.Companion)안에 func를 정의하면 외부에서는 func()를 C.func()로 호출할 수 있다.

Person의 관심사를 분리해보자. 역직렬화 함수를 비즈니스 모듈이 아니라 클라이언트/서버 통신을 담당하는 모듈 안에 포함시키고 싶다면 아래와 같이 코드를 변경한다.

// 동반 객체에 대한 확장 함수 정의하기
// 비즈니스 로직 모듈

class Pserson(val firstName: String, val lastName: String){
    companion object{
        // 비어있는 동반 객체 선언
    }
}

// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String): Person{
    // 확장 함수 선언
    ...
}
val p = Person.fromJSON(json)

마치 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 fromJSON을 호출할 수 있다. (실제로는 클래스 밖에서 정의한 확장 함수)

728x90
반응형