Kotlin singletons with argument


본 포스팅은 Kotlin singletons with argument 을 번역하여 작성했습니다.


객체는 한계점을 지니고 있습니다.

코틀린에서, 싱글톤 패턴은 해당 프로그래밍 언어로 존재하지 않는 정적멤버와 필드를 대체하는 데 사용되곤 합니다. 싱글톤은 object를 간단하게 선언함으로서 생성됩니다.

object SomeSingleton

class와는 반대로, object는 어떤 생성자도 갖을 수 없지만, 만약 초기화가 필요한 경우 init block이 허용됩니다.

object SomeSingleton {
    init {
        println("init complete")
    }
}

object에 최초로 접근했을 때 lazy하게 thread-safe한 방식으로 init 블록은 실행되고 인스턴스화 될 것 입니다. 이를 위해, 코틀린 object는 Java static initialization 블럭에 의존합니다. 위의 코틀린 object는 다음과 같은 자바 코드로 컴파일 될 것입니다:

public final class SomeSingleton {
   public static final SomeSingleton INSTANCE;

   private SomeSingleton() {
      INSTANCE = (SomeSingleton)this;
      System.out.println("init complete");
   }

   static {
      new SomeSingleton();
   }
}

이는 복잡한 double-checked locking같은 locking 알고리즘에 의존하는 것 없이 thread-safe하고 lazy한 초기화를 가능하게 해주기 때문에 JVM에서 싱글톤을 구현하는 방법으로 선호됩니다. 코틀린에서 간단하게 object를 선언하면, 안전하고 효율적인 싱글톤 구현을 보장받을 수 있습니다.


cryingkotlin


인수 전달

만약 초기화 코드가 추가적인 인수를 필요로 한다면? 코틀린 object는 어떤 생성자도 가질 수 없기 때문에, 인수를 전달할 수 없습니다.

싱글톤 초기화 블럭에 인수를 전달하는 유효한 케이스가 있는데, 추천하는 패턴입니다. 대안책은 싱글톤 클래스가 인수를 가져올 수 있도록 관심사 분리의 법칙을 위반하고 코드의 재사용성을 줄어지더라도 외부 컴포넌트를 인식할 수 있어야 함을 요합니다. 이 이슈를 완화하기 위한 외부 컴포넌트로 의존성 주입 시스템을 도입할 수 있습니다. 유효한 해결책이지만, 항상 그런 종류의 라이브러리를 사용하기를 원하지 않을 것이며 다음에 나오는 안드로이드 예제처럼 사용할 수 없는 경우도 있습니다.

코틀린에서 싱글톤을 관리하는 다른 방식에 의존해야 하는 또다른 시나리오는 외부(Retrofit, Room, …)라이브러리 혹은 툴에 의해 생성되는 싱글톤의 구현체가 생성되고 그것의 인스턴스가 커스텀 빌더 혹은 팩토리 메서드를 사용하여 가져오게 되는 경우입니다. 이 경우에, 우리는 abstract class나 interface로 싱글톤을 선언하고 이는 객체가 될 수 없습니다.

안드로이드 use case

안드로이드 플랫폼에서, 당신은 싱글톤 컴포넌트가 Context로 파일 경로를 가져오거나 세팅을 읽거나 혹은 서비스에 접근할 수 있도록 초기화 블럭에 Context 인스턴스를 넘긴 경우가 종종 있을테지만, 당신은 Context에 대한 정적 참조를 유지하는 것 을 피하고 싶어 합니다. (applcation Context에 대한 정적 참조는 기술적으로 안전하지만). 이를 위한 두가지 방법이 있습니다:

LocalBroadcastManager.getInstance(context).sendBroadcast(intent)

재사용 가능한 Kotlin 구현

우리는 싱글톤을 lazy하게 생성하고 초기화하기 위해서 인수와 함께 로직을 SingletonHolder 클래스 안에 캡슐화할 수 있습니다. 로직을 thread-safe하게 만들기 위해서, 우리는 synchronized 알고리즘을 구현 할 필요가 있고 가장 효율적인 것은 - 또한 올바르게 사용하는 것이 가장 어렵긴 하지만 - double-checked locking 알고리즘입니다.

open class SingletonHolder<out T, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg: A): T {
        val i = instance
        if (i != null) {
            return i
        }

        return synchronized(this) {
            val i2 = instance
            if (i2 != null) {
                i2
            } else {
                val created = creator!!(arg)
                instance = created
                creator = null
                created
            }
        }
    }
}

알고리즘이 제대로 동작하려면 instance field에 @Volatile 어노테이션을 추가해야 합니다. 이는 가장 콤팩트하거나 우아한 코틀린 코드는 아니지만, double-checked locking 알고리즘을 위한 가장 효율적인 바이트 코드를 생성하는 코드입니다. 이에 관한 코틀린 저자의 말을 믿으세요: 이 코드는 실제로 기본적으로 동기화 되어있는 코틀린 표준 라이브러리의 lazy()함수 구현으로부터 직접 차용된 것입니다. 생성자 함수에 인수를 넘기는 것을 허용하도록 수정되어 왔습니다.

이의 상대적 복잡성에 고려해 볼 때, 한 번 이상 쓰고 싶은 종류(혹은 읽고 싶은) 종류의 코드가 아니기 때문에, 목표는 당신이 인수를 가진 싱글톤을 구현해야 할 때마다 SingletonHolder 클래스를 재사용하는 것입니다.

getInstance() 함수를 선언하는 논리적인 위치는 singleton 클래스의 companion 객체 안이며, 이는 Java의 정적 메서드와 비슷하게, 싱글톤 클래스를 qualifier로서 사용하여 호출될 수 있도록 합니다. 코틀린 companion 객체가 제공하는 한가지 강력한 특징은 다른 객체와 같이 base 클래스로 부터 상속할 수 있다는 것이고, 이는 static-only 상속과 비교할 만한 것을 가능하게 해줍니다. 이 경우에, 우리는 재사용하고 자동으로 singleton 클래스의 getInstance() 함수를 노출(expose)하기 위해서, SingletonHolder를 싱글톤의 companion 객체에 대한 base class로 사용하기를 원합니다.

생성자 함수를 인수로서 SingletonHolder 생성자에 전달하면, 커스텀 람다는 inline으로 선언될 수 있지만 일반적인 해결책은 singleton 클래스의 private 생성자에 레퍼런스를 전달하는 것 입니다. 인수를 가진 싱글톤에 대한 마지막 코드는 다음과 같습니다:

class Manager private constructor(context: Context) {
    init {
        // Init using context argument
    }

    companion object : SingletonHolder<Manager, Context>(::Manager)
}

object 표기법처럼 짧지는 않지만, 다음으로 가장 좋은 것일 겁니다. 싱글톤은 아마 다음과 같은 문법으로 호출될 것이고 이의 초기화는 lazy하고 thread-safe할 것입니다:

Manager.getInstance(context).doStuff()

싱글톤 구현체가 외부 라이브러리에 의해 생성되고 빌더에 인수가 필요한 경우 또한 이 방법을 사용할 수 있습니다. Android Room Persistence Library를 사용한 예제 입니다:

@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object : SingletonHolder<UsersDatabase, Context>({
        Room.databaseBuilder(it.applicationContext,
                UsersDatabase::class.java, "Sample.db")
                .build()
    })
}

빌더가 인수를 필요로 하지 않을 경우, 대신에 간단하게 lazy 위임된 프로퍼티를 사용할 수 있습니다:

interface GitHubService {

    companion object {
        val instance: GitHubService by lazy {
            val retrofit = Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .build()
            retrofit.create(GitHubService::class.java)
        }
    }
}

유용하였기를 바랍니다. 만약 이 문제를 해결하기 위한 다른 방법을 제안하고 싶거나 질문이 있다면, 주저하지 말고 코멘트를 달아주세요. 읽어주셔서 감사합니다!







추가



static block은 클래스의 객체가 생성되었을 경우나 스테틱 멤버변수에 접근했을 경우에만 딱 한번만 실행됩니다.

// filename: Main.java
class Test {
    static int i;
    int j;

    // start of static block
    static {
        i = 10;
        System.out.println("static block called ");
    }
    // end of static block
}

class Main {
    public static void main(String args[]) {

        // Although we don't have an object of Test, static block is
        // called because i is being accessed in following statement.
        System.out.println(Test.i);
    }
}

Output: static block called 10


// filename: Main.java
class Test {
    static int i;
    int j;
    static {
        i = 10;
        System.out.println("static block called ");
    }
    Test(){
        System.out.println("Constructor called");
    }
}

class Main {
    public static void main(String args[]) {

       // Although we have two objects, static block is executed only once.
       Test t1 = new Test();
       Test t2 = new Test();
    }
}

Output: static block called Constructor called Constructor called

(참고 : https://www.geeksforgeeks.org/g-fact-79/)



Kotlin에는 static keyword가 존재하지 않습니다. 대신에, companion 키워드를 통해서 static하게 접근할 수 있도록 합니다.

// 생성자 private 처리
class ClassName private constructor() {

  // 외부에서 static 형태로 접근 가능
  companion object {
    fun getInstance() = ClassName()
  }
}

// Use kotlin
val className = ClassName().getInstance()
// Use java
ClassName className = ClassName.Companion.getInstance();

(참고 : https://thdev.tech/kotlin/2016/10/09/Kotlin-Class.html)