본문 바로가기

코틀린

코틀린(Kotlin) - object 키워드 : 싱글톤, static 멤버, 객체 식 선언

코틀린에서는 "object" 키워드를 다양한 상황에서 사용하지만 그 상황마다의 공통점이, 클래스를 정의하면서 동시에 인스턴스(객체)를 생성한다는 점입니다.

이러한 object 키워드를 사용하는 여러 상황을 살펴보도록 하겠습니다.

객체 선언 : 싱글톤 쉽게 만들기 (object)

객체지향 프로그래밍을 설계하다 보면, 인스턴스가 하나만 필요한 클래스의 유용한 경우가 많습니다.

자바에서는 다음과 같이, 보통 클래스의 생성자를 private으로 선언하고, static 변수에 클래스 객체를 저장하는 패턴으로 구현합니다.

 

/** 자바에서의 일반적인 싱글톤 패턴 */
public class DataRepository {
    private static DataRepository INSTANCE;

    private DataRepository() { }

    public static DataRepository getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new DataRepository();
        }
        return INSTANCE;
    }
}

 

코틀린은 "객체 선언(object)" 기능을 통해 싱글톤을 언어 자체에서 기본 지원해줍니다!^^

 

/** object 키워드로 싱글톤 구현하기 */
object DataRepository {}

//코틀린에서 사용
>>> val dataRepository = DataRepository

//자바에서 사용
>>> DataRepository dataRepository = DataRepository.INSTANCE;

 

싱글톤 선언은 "object" 키워드로 시작하면 되고, 이로써 싱글톤 작업이 단 한 문장으로 끝납니다!

코틀린에서 사용 시엔 객체명만으로 사용하고, 자바에서 사용 시엔 자동으로 생성된 INSTANCE 필드를 호출하여 사용할 수 있습니다.

 

싱글톤과 의존관계 주입

싱글톤 패턴과 마찬가지 이유로, 대규모 소프트웨어 시스템에서는 객체 선언이 항상 적합하지는 않습니다.
의존관계가 별로 많지 않은 소규모 소프트웨어에서는 싱글톤이나 객체 선언이 유용하지만, 대규모 프로젝트에서는 객체 생성을 제어할 수 없고, 생성자 파라미터를 지정할 수 없으므로, 단위 테스트를 하거나 프로젝트의 설정이 달라질 때, 객체를 대체하거나 객체의 의존관계를 바꾸기가 쉽지 않습니다.
그렇기 때문에, 의존관계 주입을 사용해야 합니다.

static 멤버 선언

코틀린 언어는 자바의 static을 지원하지 않기 때문에, 클래스 안에는 static 멤버가 없습니다. 

그 대신, 코틀린에서는 클래스 바깥에 선언하는 최상위 함수(자바의 정적 메소드 역할)와 객체 선언을 활용합니다.

대부분의 경우에는 최상위 함수를 활용하는 편을 더 권장하기는 합니다. 

하지만, 최상위 함수는 아래 그림처럼 private 멤버에 접근할 수가 없습니다

 

클래스 바깥에 있는 최상위 함수는 private 멤버를 호출할 수 없다.

 

클래스 안에 "companion object"를 정의하면, 그 클래스의 프로퍼티나 메소드에 접근할 수가 있습니다.

"companion object" 의 사용은, 자바의 static 호출 사용 구문과 동일합니다.

 

이러한 companion object 객체 선언은, private 생성자를 호출하기 좋은 위치인데요, 그 이유는 자신을 포함한 클래스의 모든 private 멤버에 접근할 수 있기 때문에, 팩토리 패턴을 구현하기 가장 적합한 위치입니다.

 

간단한 예제로 부 생성자가 두 개 있는 클래스를, companion object를 활용해서 팩토리 클래스로 정의하는 방식으로 변경해보도록 하겠습니다.

 

/** 부 생성자가 두개 있는 클래스 정의 */
class User {
    val name: String
    
    constructor(email: String) { //부 생성자
        name = email.substringBefore('@')
    }
    
    constructor(facebookAccountId: Int) { //부 생성자
        name = getFacebookName(facebookAccountId)
    }
}

 

이러한 로직을 더 유용한 방법으로 클래스의 인스턴스를 생성하는 팩토리 메소드로 바꿔보도록 하겠습니다.

 

class User private constructor(val name: String) { //주 생성자를 private으로 선언
    companion object {
        fun newEmailUser(email: String) = User(email.substringBefore('@'))

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

>>> val emailUser = User.newEmailUser("kotlin@gmail.com") //companion object 메소드 호출
>>> println(emailUser.name)
kotlin

 

어떤가요? 팩토리 패턴을 적용하니, 훨씬 더 유용해 보이지 않으시나요?

이렇게 정의된 팩토리 메소드는 생성할 필요가 없는 객체를 생성하지 않을 수도 있는데, 예를 들어 이메일 주소별로 유일한 User 인스턴스를 만드는 경우, 팩토리 메소드가 이미 존재하는 인스턴스에 해당하는 이메일 주소를 전달받으면 새 인스턴스를 만들지 않고, 캐시에 있는 기존 인스턴스를 반환할 수 있습니다.

이와 같이 팩토리 패턴은 굉장히 유용합니다.

 

companion object를 일반 객체처럼 사용하기

companion object는 클래스 안에 정의된 일반 객체입니다.

그렇기 때문에 이름을 붙이거나, 인터페이스를 상속하거나, companion object 안에 확장 함수와 프로퍼티를 정의할 수 있습니다.

 

class User private constructor(val name: String) { //주 생성자를 private으로 선언
    companion object IdType {
        fun newEmailUser(email: String) = User(email.substringBefore('@'))

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

>>> val emailUser = User.newEmailUser("kotlin@gmail.com") 
>>> println(emailUser.name)
kotlin

>>> val emailUser = User.IdType.newEmailUser("kotlin@gmail.com") //이름붙인 companion object 메소드 호출
>>> println(emailUser.name)
kotlin

 

companion object에서 인터페이스 구현

일반적인 객체 선언과 마찬가지로, companion object도 인터페이스를 구현할 수 있습니다.

 

/** companion object에서 인터페이스 구현 */

interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class User(val name: String) {
   companion object : JSONFactory<User> { //User클래스 사용 가능
      override fun fromJSON(jsonText: String): User = ...
   }
}
코틀린 companion object와 static 멤버

클래스의 companion object는 일반 객체와 비슷한 방식으로, 클래스에 정의된 인스턴스를 가리키는 static 필드로 컴파일됩니다.
companion object에 따로 이름을 붙이지 않았다면, 자바에서 Companion이라는 이름으로 접근할 수 있습니다.

/* 자바에서 사용 */
User.Companion.fromJSON("...");

때로는, 자바에서 사용하기 위해 코틀린 클래스의 멤버를 static 멤버로 만들어야 할 필요가 있습니다.
그런 경우에는 @JvmStatic 어노테이션을 코틀린 멤버에 붙이면 되고,
static 필드가 필요하다면, @JvmField 어노테이션을 최상위 프로퍼티나 객체에서 선언된 프로퍼티 앞에 붙이면 됩니다.

자바에선 어떻게 쓰나요? : @JvmStatic

companion object를 자바에서 사용하려면 속성 및 함수가 자바의 필드/메소드로 해석되도록 알려주어야 합니다.

 

const 키워드는 Primitive type과 String에만 사용 가능하며, const 선언이 되어 있는 속성은 별도 처리가 필요 없이 자바에서도 동일하게 사용 가능합니다.

함수는 @JvmStatic 어노테이션을 붙여주어 자바에서 정적 메소드로 사용할 수 있게 해 줍니다.

 

class User(val name: String) {
   companion object : JSONFactory<User> { 
   
      //자바에서도 동일하게 User.AGE로 접근 가능
      const val AGE = 30
   
      //자바에서 정적 메소드(static method)처럼 사용할 수 있도록 해줌
      @JvmStatic
      override fun fromJSON(jsonText: String): User = ...
   }
}

Primitive type이나 String이 아닌 객체 타입은 어떻게 처리하나요? : @JvmField

Primitive type과 String에 해당하지 않는 타입의 객체를 자바에서 정적 필드처럼 사용하려면 @JvmField 어노테이션을 사용하면 됩니다.

 

class Car {
    companion object {
        @JvmField MANUFACTURE = Manufacture()
    }
}

class Manufacture { }

//자바에서 접근
>>> Manufacture manufacture = Car.MANUFACTURE

익명 객체 정의 (e.g. Listener...)

object 키워드는 익명 객체(annoymous object)를 정의할 때도 사용됩니다.

흔히 익명 클래스로 구현하는 이벤트 리스너들의 경우라고 생각해주시면 됩니다.

 

/** 익명 객체로 리스너 구현하기 */
binding.floatingBtton.setOnClickListener(
      object : View.OnClickListener { //익명 객체로 클릭 리스너 선언
            override fun onClick(v: View?) {
                //...
            }
      }
)

 

단, 여기서 익명 클래스는 싱글톤이 아닙니다.

호출 시마다 매번 새로운 인스턴스가 생성되는 점과, 익명 클래스 안에서 자신이 포함된 함수의 로컬 변수에 접근할 수 있습니다.

자바에서는 무조건 final이어야만 접근이 가능한데, 코틀린에서는 final이 아닌 변수도 객체 식 안에서 사용할 수 있는 것입니다.

 

/** 익명 객체 안에서 로컬 변수 사용하기 */
fun countClick() {
    var clickCount = 0 //로컬 변수
    
    binding.floatingBtton.setOnClickListener(object : View.OnClickListener {
        override fun onClick(v: View?) {
            clickCount++ //로컬 변수의 값을 변경한다.
        }
    })
}

 

추가로, object 익명 객체 사용은 여러 메소드를 오버라이드 해야 하는 경우에 훨씬 더 유용하고,

오버라이드 메소드가 하나뿐인 경우에는, 다음과 같이 람다식을 활용하는 편이 낫습니다. ^^

 

fun countClick() {
    var clickCount = 0
    binding.floatingBtton.setOnClickListener { clickCount++ }
}