오늘도 더 나은 코드를 작성하였습니까?

Singleton pattern 싱글톤 패턴(feat. kotlin) 본문

디자인패턴/생성패턴

Singleton pattern 싱글톤 패턴(feat. kotlin)

hik14 2024. 5. 30. 04:27

정의

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.

 

어플리케이션 전반에 걸쳐 어디에서든 접근 가능하며, 유일한 객체를 필요로 할때 사용한다.

1번 생성할때, 많은 비용이 소모되며, 앱의 여러군데에서 동일한 데이터에 접근을 할 필요성이 있다면, 싱글턴 패턴을 사용하면된다.

 

대표적으로 DataBase를 추상화하여 접근하는 객체를 싱글턴 패턴으로 만들면 이점이 많다.

 

구현 (kotlin 구현)

- private 접근자를 통해 외부에서 생성자를 통한 객체생성을 막는다.

- 단, 1개의 객체만을 참조 하는 companion 변수 instance를 생성.

- 객체를 참조 하기 위한 companion 함수 getInstance를 정의한다. 

class Singleton private constructor(){

    companion object{
        private var instance: Singleton? = null

        fun getInstance(data: String): Singleton {
            if (instance == null){
                instance = Singleton()
            }
            return instance!!
        }
    }

    private lateinit var data: String
}

 

하지만 Multi -Thread 환경에서는 getInstance() 를 서로 다른 Thread에서 거의 동시에 호출하게 된다면, 서로 다른 객체를 생성하여 참조할 가능성이 있다.

 

@Synchronized - 함수 전체에 Lock을 걸어 한개의 Thread만 함수 블록을 실행하도록 한다.

class Singleton private constructor(){

    companion object{
        private var instance: Singleton? = null
		
        @Synchronized
        fun getInstance(data: String): Singleton {
            if (instance == null){
                instance = Singleton()
            }
            return instance!!
        }
    }

    private lateinit var data: String
}

 

위 코드로 쓰레드의 문제는 해결이 되었지만, 싱글톤 객체에 접근을 하기 위해 모든 Thread들이 차례로 기다리게 되는 OverHead가 커지게 된다. 또한 이미 싱글턴 객체가 생성되어 있음에도 불구하고 모든 Thread들이 참조를 얻기 위해 기다려야 한다.

 

함수 전체가 아닌 instance가 null일 때만, lock을 걸어 객체를 생성하고, 생성이 완료된 이후에는 2중의 null check를 통해 모든 Thread들이 기다리지 않고 참조를 얻을수 있다.

class Singleton private constructor() {

    companion object {
        private var instance: Singleton? = null

        fun getInstance(data: String): Singleton {
            if (instance == null) {
                synchronized(this) {
                    if (instance == null) {
                        instance = Singleton()
                    }
                }
            }
            return instance!!
        }
    }

    private lateinit var data: String
}

 

이제 위 코드는 Thread-safe 하며, OverHead도 줄어들었다. 하지만, 여전히 문제가 남아있다.

Thread-A, Thread-B가 있다고 가정을 하고, 거의 동시에 getInstance를 호출했다고 하자!

Thread-A가 조금더 먼저 sycrhonized{ ... }을 접근하여 lock을 얻고 객체를 초기화 했다.

이때, complie에서 부분적으로 생성된 singleton객체를 cache메모리에 올려 놓는다. 즉 객체 생성이 완료되기 전에 Thread-B가 참조를 할 수있다.

 

@Volatile - 변수 선언시 volatile을 지정하면  CPU의 cache 메모리가 아닌 값을 메인 메모리(Ram)에만 적재한다.

이렇게 되면, CPU의 cache 메모리를 사용하지 못하기 때문에  result라는 local 변수를 통해 미세한 조정을 해준다.

 

class Singleton private constructor(val data: String) {

    companion object {
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(data: String): Singleton {
            var result = instance
            if (result == null) {
                synchronized(this) {
                    if (result == null) {
                        result = Singleton(data)
                        instance = result
                    }
                }
            }
            return result!!
        }
    }
}

 

Kotlin 객체선언를 사용하여 구현한 Singleton 

 

객체선언

- 클래스 선언과 객체 생성을 통한 단일 객체를 생성한다

- 생성자(주생성자, 부생성자)를 사용할 수없다

- 프로퍼티, 함수, 초기화 블록을 사용할 수있다

- 클래스 및 인터페이스를 확장 및 구현 가능하다

fun main() {

    val db1 = Database
    val db2 = Database

    if (db1 === db2)
        println("동일한 데이터베이스 입니다.")
    else
        println("서로 다른 데이터 베이스 입니다.")
}

class Dao() {

    fun get() {}

    fun insert() {}

    fun delete() {}

    fun upDate() {}
}

object Database{
    val dao = Dao()
}