본문 바로가기
2023년 이전/kotlin

kotlin - object 키워드

by JeongUPark 2020. 2. 25.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 

 

코틀린에서 object 키워드는 다양하게 사용되지만 모든 경우 클래스를 정의하면서 동시에 인스턴스를 생성한다는 고통점이 있습니다. (이 object 키워드 때문에 Object 객체가 없고 대신 Any가 있습니다.)

 

객체 선언 : 싱글턴 만들기

객체지향 시스템을 만들다보면 인스턴스가 하나만 필요한 클래스가 필요할 때가 있습니다. 자바에서는 보통 다음과 같이 작성하여 싱글턴 클래스를 만듭니다.

public class SingleTon {
    private static SingleTon mSingleton;

    public static SingleTon getInstance() {
        if (mSingleton == null) {
            mSingleton = new SingleTon();
        }
        return mSingleton;
    }
}

(위와 같이 만들 경우 일부 문제가 생기는데 이에 대한 설명은 여기서 확인 하실 수 있습니다.)

하지만 코틀린에서는 객체 선언 기능을 통해 싱클턴을 언어에서 기본 지원합니다. (객체 선언 : 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선업을 합친 선언)

객체 선언은 object로 키워드로 시작합니다. 그래서 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어서 변수에 저장하는 모든 작업을 단 한 문장으로 처리합니다. 그리고 객체 선언 안에도 프로퍼티, 메소드 초기화 블록등이 들어갈 수 있지만 생성자는 쓸 수 없습니다.

fun main(args: Array<String>) {

    Singleton.checkName("No jeongu")
}
object Singleton {
    val name : String= "jeongu"
    fun checkName(checkName : String): Boolean = name == checkName
} 

이렇게 사용할 수 있습니다.

그리고 클래스 안에서 객체 선언을 할 수도 있습니다.

import java.util.Comparator

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

fun main(args: Array<String>) {
    val persons = listOf(Person("Bob"), Person("Alice"))
    println(persons.sortedWith(Person.NameComparator))
}

위의 코드를 보면 Person안에 object NameComparator를 만들었습니다. 혹시 Person 인스턴스을 생성할 떄마다 새로운 NameComparator가 생기는게 아니냐고 생각할 수 있지만 , Persion 인스턴스마다 객체 선언에 해당하는 인스턴스가 생성되는게 아니라 단 하나의 객체선언 인스턴스만 생성됩니다.

그래서 위의 코드에서 Person.NameComparator으로 객체 선언된 NameComparator를 사용할 수 있습니다.

 

동반 객체 

코틀린은 자바의 static 키워드를 지원하지 않는다. 대신 코틀린에는 패키지 수준의 최상위 함수(자바의 static 메소드 역할을 대신 할 수 있다.)와 객체 선언(자바의 static 메소드 역할 중 코틀린 최상위 함수가 대신 할 수 없는 역할이나 정적 필드를 대신 할 수 있다.)을 활용합니다. 보통은 최상위 함수를 활용하는 편을 더 권장합니다.

Java

class Foo {

    public static final String foo = "foo";

    public static void doSomething() {
        // Do something
    }
}

Kotlin

@file:JvmName("Foo")
package juparkTest

const val FOO = "foo"

fun doSomething() {
    // Do something
}

 

 

그리고 클래스 안에 companion object로 선언하면 동반객체가 생성 되는데 이렇게 생성된 동반객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용합니다.  그래서 코틀린의 동반 객체는 자바의 static 구문들과 사용이 같아 집니다.

class Foo{
    companion object{
        const val FOO = "foo"
        fun doSomething() {
            // Do something
        }
    }
}
fun main(args: Array<String>) {
    Foo.doSomething()
}

 그리고 클래스 안에 private 로 선언된 프로퍼티나 메소드 혹은 생성자가 있을 경우 그 클래스 외부에서는 접근할 수 없습니다. 하지만 위의 companion object 로 만들어진 동반 객체는 바깥쪽 클래스의 private 데이터에도 접근이 가능합니다. 그리고 이 동반객체를 호출하면 private로 선언된 데이터를 사용할 수 있습니다.

다음 코드를 보면

fun getFacebookName(accountId: Int) = "fb:$accountId"

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

fun main(args: Array<String>) {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    println(subscribingUser.nickname)
    println(facebookUser.nickname)
}

 

User 클래스의 생성자(constructor)가 private로 선언되었습니다. 그래서 외부에서는 접근할 수 없습니다. 하지만 companion object로 선언된 동반 객체에 있는 메소드  newSubscribingUser / newFacebookUser을통하여 생성자에 접근하여 생성자를 쓸수 있게 됩니다. 그리고 그 결과는

bob
fb:4

이렇게 나타납니다.

만일 위 코드가 잘 이해안가면 다음과 같이 코드를 수정해서 이해할 수 도 있습니다. (기능은 똑같습니다.)

class User {
    val nickname : String
    private constructor(email: String){
        nickname = email
    }
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}

그리고 위의 compaion object 안에서 각 경우에 따라 User 클래스를 인스턴스화 하는데 이같은 패턴을 팩토리 메서트 패턴이라고 합니다. (더 자세한 내용은 여기)

 

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

 

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

 

이름 붙이기

class Person(val name : String){
   companion object Loader{
       fun fromJSON(jsonText : String) : Person {
         // 내용
       }
   }
}

위와 같이 작성하면

Person.Loader.fromJSON("{name: jeongu}") 등의 식으로 사용할수 있습니다.

 

인터페이스 확장

interface JSONFactory<T>{
    fun fromJSON(josnText:String) : T
}
class Person(val name:String){
	companion object : JSONFactory<Person>{
       override fun fromJSON(josntext:String) : Person{
        // do Something
       }
    }
}

이렇게 인터페이스를 확장하여 사용할 수 도 있습니다.

 

Java에서 호출

Java에서 위의 동반객체를 호출하게 된다면 우선 package를 import하고 Person.Companion.fromJSON("...")으로 사용할 수 있습니다. (companion object가 이름을 가지고 있으면 Person.Loader.fromJSON("...") 으로 호출 됩니다.)

 

확장함수

 

class Person(val name : String){
   companion object{
    //비어있는 동반객체 선언
   }
}

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

val p = Person.fromJSON(json)

위와 같이 Person 클래스 안에 동반객체가 있고 그 동반객체에 대한 확장 함수를 만들 수 있습니다. 이렇게 확장 함수를 만들면 위와 Person 클래스에 대한 객체를 따로 만들지 않고 바로 Person.으로 클래스 이름으로 그 확장 함수를 사용할 수 있습니다.

 

객체 식 

object 키워드는 싱글턴을 만들 때만 사용하되는 것이 아니라 무명객체를 정의할 때도 사용 됩니다. 무명 객체는 자바의 무명 내부 클래스(혹은 익명클래스)를 대신합니다. ( 무명 내부 클래스는 구글링하면 자세한 설명들이 있습니다. ㅎㅎ)

다음은 view에 대한 이벤트 리스너를 객체식으로 구현한 것입니다.

view.setOnClikcListener(object : ClickListenr{
    override fun onClick() {
    }
    override fun onLongClick() {
    }
})

객체식은 클래스를 정의하고 그 클래스에 속한 인스턴스를 생성하지만, 그 클래스나 인스턴스의 이름을 붙이지 않습니다. 만일 붙이고 싶다면 변수에 무명 객체를 대입하면 됩니다.

val mLinstener = object : ClickListenr{
    override fun onClick() {
    }
    override fun onLongClick() {
    }
}

그리고 객체식은 싱글턴이 아니라 객체 식이 쓰일떄마다 인스턴스를 생성합니다.

위 객체 식에 대한 전체 코드는

 

class View{
    constructor()
}
interface ClickListenr{
    fun onClick()
    fun onLongClick()
}
fun main(){
    val view :View = View()
    var isClick = false
    view.setOnClickListener(object : ClickListenr{
        override fun onClick() {
            isClick = !isClick
        }
        override fun onLongClick() {
        }
    })
}


fun View.setOnClickListener(clickListenr: ClickListenr){
    clickListenr.onClick()
}

이렇게 됩니다. View에 대한 setOnClickListener 확장함수를 만들고 ClickListener를 파라미터로 받습니다. 그리고 메인에서 View에 대한 확장함수 setOnClickListener를 사용하고 파마리터에 ClickListenr에 대한 객체 식을 넣습니다. 그럼 아래 clickListenr.onClick() 이 호출되면 위의 객체 식의 onClick() 메소드가 호출 됩니다. 그리고 로컬 변수를 객체 식 안에서 사용 할 수도 있습니다. (위에서는 isClick)

 

사실 이 방식은 Android App 개발할때 kotlin을 쓰게되면 handler나 각 Listener 작업시 많이 사용 되므로 Android App 개발을 kotlin으로 하신다면 기억하는 것이 좋습니다.

반응형