개발일지

Design Pattern in A..Z - Singleton (싱글톤) 본문

Design Pattern (디자인 패턴)

Design Pattern in A..Z - Singleton (싱글톤)

강태종 2021. 10. 4. 23:40

Singleton Pattern

객체를 생성할 때 생성자가 호출되고 메모리에 올라가는 등 비용이 발생한다. 만약 객체를 생성할 때 비용이 크다면 객체를 자주 생성하는 일은 시스템에 부담이 클 것이다. 싱글톤 패턴은 객체를 한번만 생성하고 생성된 객체를 재사용하면서 객체의 재생성 비용을 줄이는 디자인 패턴이다.

 

Database를 연결하고 접근하는 객체를 예로 들어 생각하면 DB를 연결할 때 드는 비용은 매우 비싸다. 하지만 연결을 한번 하고 생성된 객체를 재사용한다면 연결 비용을 줄일 수 있을 것이다.

 

* 인스턴스화 하는 비용을 줄일 수 있다는 장점을 가지지만, 한번 생성한 인스턴스를 반납하지 않기 때문에 무분별한 싱글톤 패턴 사용은 오히려 메모리 낭비를 일으킨다.

* 싱글톤 패턴은 객체간 결합도를 높이기 때문에 테스트가 어렵고, 멀티 쓰레드 환경에서 동기화 문제를 해결해야한다.


Static vs Singleton

Static을 통해 정적으로 생성된 객체나 함수를 접근할 수 있지만 상속을 통한 확장이 불가능하다.

Singleton 패턴은 상속을 통해 확장할 수 있고, 상속될 수 있어 확장성이 크다.


Eager Initialization (이른 초기화)

static 키워드를 통해 instance를 초기화하여 메모리에 등록해서 사용하는 방식입니다.

static 키워드의 특징은 클래스 로더가 로딩되는 최초 시점에 객체를 생성하고, 이러한 방식은 Thread-Safe합니다. 하지만 클래스 로드할 때 Singleton을 사용 유무에 상관없이 무조건 생성되는 단점이 있습니다.

class MySingleton private constructor() {
    companion object {
        private val instance = MySingleton()
        
        fun getInstance(): MySingleton {
            return instance
        }
    }
}

Lazy initialization (늦은 초기화)

인스턴스가 null인지 체크하여 필요할 때 한번만 초기화 합니다.

하지만 Multi-Thread 환경에서 getInstace를 동시에 접근할 경우 여러번 생성될 수 있다는 단점을 가지고 있습니다.

class MySingleton private constructor() {
    companion object {
        private var instance: MySingleton? = null

        fun getInstance(): MySingleton {
            return instance ?: MySingleton().also {
                instance = it
            }
        }
    }
}

Lazy Initialization with synchronized (동기화를 이용한 늦은 초기화)

synchronized를 통해 Locking하여 Thread-Safe를 보장할 수 있다.

하지만 Locking하는 비용이 발생하기 때문에 getInstance()를 호출할 때Multi-Thread 환경에서 성능 저하가 발생할 수 있습니다.

class MySingleton private constructor() {
    companion object {
        private var instance: MySingleton? = null

        @Synchronized
        fun getInstance(): MySingleton {
            return instance ?: MySingleton().also {
                instance = it
            }
        }
    }
}

Lazy Initialization with Double Check Locking (이중 체크를 통한 동기화)

getInstance를 Locking하는 방법이 아닌, instance가 null일 때 Singleton을 Locking하고, instance를 다시 확인하여 객체를 생성한다.

getInstance를 Locking하지 않았기 때문에 Singleton을 한번 생성하면 Locking이 걸리지 않아 성능을 향상 시킬 수 있다.

class MySingleton private constructor() {
    companion object {
        private var instance: MySingleton? = null

        fun getInstance(): MySingleton {
            return instance ?: synchronized(this) {
                instance ?: MySingleton().also {
                    instance = it
                }
            }
        }
    }
}

Lazy Initialization with Double Check Locking And Volatile

다중 프로세서 환경에서 CPU마다 캐쉬를 가지고 있고 서로 공유되지 않는다. 그렇게 때문에 같은 변수를 접근할 때 어떤 CPU는 Cache Hit가 발생하고 어떤 CPU는 Cache Miss가 발생할 수 있다.

Volatile은 CPU에 캐쉬를 저장하지 않고, Main Memory에 저장하도록 강제하는 방법이다.

class MySingleton private constructor() {
    companion object {
    	@Volatile
        private var instance: MySingleton? = null

        fun getInstance(): MySingleton {
            return instance ?: synchronized(this) {
                instance ?: MySingleton().also {
                    instance = it
                }
            }
        }
    }
}

Lazy Initialization with Holder

Synchronized나 Volatile을 사용하지 않기 때문에 성능면에서 유리하며, Class Loader와 static 특성을 이용하여 instace를 호출하지 않는 이상 instance를 생성하지 않으며 Thread-Safe를 보장한다. 제일 많이 사용하는 방식이다.

* static은 Class Loader가 Class를 Load할 때 초기화 된다. instance를 Holder로 감싸면서 Holder를 접근하지 않는 이상 Intance는 생성되지 않으며 Holder를 private으로 선언하여 getInstace를 호출할 때만 접근할 수 있도록 했다.

class MySingleton private constructor() {
    private object Holder {
        val instance = MySingleton()
    }

    companion object {
        fun getInstance(): MySingleton {
            return Holder.instance
        }
    }
}

'Design Pattern (디자인 패턴)' 카테고리의 다른 글

Design Pattern in A..Z - Command  (0) 2021.10.05
Design Pattern in A..Z - MVVM  (0) 2021.10.05
Design Pattern in A..Z - MVP  (0) 2021.10.05
Design Pattern in A..Z - MVC  (0) 2021.10.05
Comments