1. Singleton
Github : https://github.com/junghun9102/DesignPatternTemplate
branch : feature/singleton
싱글톤은 가장 쉽게 접할 수 있는 디자인 패턴이라고 생각한다.
1. 인스턴스를 하나만 가져야 할 때
2. 인스턴스를 하나를 초과해 만들 필요가 없을 때
사용한다.
예시 프로젝트로 세계관을 가진 게임을 만드려고 한다.
이 세계에는 유일신만이 존재한다.
그렇기 때문에 God이라는 클래스의 인스턴스는 하나만 존재해야한다.
Commit c5019208
class God private constructor(private val index: Int) {
companion object {
private var uniqueInstance: God? = null
fun getInstance(): God {
uniqueInstance ?: run {
uniqueInstance = God(count++)
}
return uniqueInstance!!
}
private var count = 0
}
fun bless(townName: String) {
println("$index God bless $townName")
}
}
짠! 가장 기본적으로 많이 보는 싱글톤 코드라고 생각한다.
나만 이렇게 사용했을 수도 있지만...
가장 기본적인 원칙
1. 여러개의 인스턴스를 만들 수 없게 생성자를 제한한다.
2. 인스턴스를 받을 수 있는 메소드에서 하나의 인스턴스만 만들어 리턴해준다.
전혀 문제될 부분이 없어 보인다.
물론 나도 그런줄 알고 이렇게 사용해 왔고..
하지만 이 코드는 인스턴스를 둘 이상 만들 수 있다.
class World {
private val aTown = Town("ATown")
private val bTown = Town("BTown")
fun run() {
Thread {
aTown.pray()
}.start()
Thread {
bTown.pray()
}.start()
}
}
class Town(private val name: String) {
fun pray() {
God.getInstance().bless(name)
}
}
두 개 이상의 스레드에서 동시에 getInstance를 호출하면
와 같이 두개의 인스턴스가 만들어 지는 경우가 있다.
두 스레드가 동시에 null 체크를 통과할 수 있기 때문이다.
물론 비동기를 사용하지 않는다면 발생하지 않는다.
Commit 25874942
class God private constructor(private val index: Int) {
companion object {
private var uniqueInstance: God? = null
@Synchronized
fun getInstance(): God {
uniqueInstance ?: run {
uniqueInstance = God(count++)
}
return uniqueInstance!!
}
private var count = 0
}
fun bless(townName: String) {
println("$index God bless $townName")
}
}
딱 한 줄이 늘었다.
Synchronized 키워드를 통해
동시 접근할 수 없게 만들었기 때문에 인스턴스가 두개 만들어질 걱정은 없다.
하지만 동기화 키워드의 값을 비싸다.
그리고 처음 초기화를 위해 동기화 키워드가 필요로 하지만
초기화 이후에도 동기화로 인해 손해를 보고 있다.
Commit 18eff495
class God private constructor(private val index: Int) {
companion object {
@Volatile
private var uniqueInstance: God? = null
fun getInstance() = uniqueInstance ?: run {
synchronized(God) {
uniqueInstance ?: run {
uniqueInstance = God(count++)
}
uniqueInstance!!
}
}
private var count = 0
}
fun bless(townName: String) {
println("$index God bless $townName")
}
}
null일 때만 동기화 블록에서 동시에 초기화할 수 없게 막아두었다.
이를 DCL(Double-Checking Locking)이라고 한다.
여기까지가 Head First Design Pattern에 나오는 Singleton이다.
하지만 싱글톤에 대해 찾아보다 DCL 역시 자바에서 문제가 생길 수 있다는 포스팅을 보았다.
uniqueInstance = God(count++)
uniqueInstance 에는 주소값이 할당되었지만 God 생성자가 돌지 않은 상태에서
다른 스레드가 getInstance를 호출해 not-null 상태인 uniqueInstance를 리턴하지만 제대로 된 객체를 받지 못한다는 이야기였다.
이에 관해 깊은 이해가 없어 마지막에 링크를 첨부했다.
Commit 42282b09
class God private constructor(private val index: Int) {
companion object {
private var count = 0
private var uniqueInstance: God = God(count++)
fun getInstance() = uniqueInstance
}
fun bless(townName: String) {
println("$index God bless $townName")
}
}
그렇기 때문에 getInstance 자체에 동기화를 거는 방식을 택하거나
위의 코드와 같이 전역변수를 선언하고 선언과 함께 초기화하는 방식으로 사용하면 된다.
사실 멀티스레드에서 싱글톤 객체를 경쟁적으로 호출하거나
늦은 초기화를 하는게 이점이 될만큼 무거운 싱글톤 객체를 사용해본 적이 없어
어떤 방식이든 상관이 없긴했다.
자바에서 DCL에서 생기는 문제점 포스팅
https://gampol.tistory.com/entry/Double-checked-locking%EA%B3%BC-Singleton-%ED%8C%A8%ED%84%B4
Volatile 포스팅
https://nesoy.github.io/articles/2018-06/Java-volatile