A:'싱글톤 패턴은 무엇인가요?'
B: '객체를 단 하나의 인스턴스로 생성하여 하나의 인스턴스를 프로세스가 돌아가는 동안 전역에서 접근 할 수 있는 디자인 패턴입니다.'
A: '이 패턴의 위험성에 대해 말씀해주세요.'
B: '...'
개발자라면 싱글톤 패턴(Singletone Pattern)을 사용해봤는지 물어봤을 때 '네'라는 말을 하겠지만,
싱글톤 패턴의 위험성을 고민해봤는지 물어봤을 때 모두 '네'라는 말을 하진 않습니다.
해당 글은 싱글톤에 대한 개인적인 고찰을 기록하기 위해 남겨둡니다.
싱글톤 패턴은 언어별 구현 형태가 다르지만서도 정의는 동일합니다.
'Head First:Design Pattern'에서는 싱글톤을 아래와 같이 정의를 합니다.
싱글톤 패턴(Singletone pattern)은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.
싱글톤 패턴의 다이어 그램
위 내용에서 정의한대로 싱글톤 패턴은 전역 상태로 메모리(Data) 영역에 저장되어 있는 객체의 인스턴스입니다.
** 왜 개발자들은 싱글톤 패턴을 쓰는걸까요?
생성 코스트 절감
객체를 생성하게 되면 메모리(Heap) 영역에서 리소스를 할당합니다. 동일 리소스를 반복적으로 여러 객체에서 접근해서 사용해야 하는 경우나 한번 생성할 때 발생하는 코스트가 높은 경우, 싱글톤 패턴을 이용해서 필요한 공유 객체를 메모리(Data) 영역에 한번만 생성하여 성능상 이점을 얻을 수 있습니다.
공유 리소스 관리
싱글톤 패턴은 여러 객체가 공유하는 리소스를 효율적으로 관리할 수 있습니다. 데이터베이스 연결, 로깅, 설정 등 전역으로 선언하여 여러 객체에서 접근이 가능하고 일관된 상태를 유지할 수 있습니다.
공유 리소스 접근
여러 객체에서 동시에 접근하여 사용할 때, 실제 인스턴스화된 객체는 하나이기 때문에 별다른 동기화 작업 없이 데이터의 무결성을 유지할 수 있습니다.(첫 생성 경우 제외)
싱글톤 패턴은 위와 같은 장점이 있지만, 과도한 사용은 의존성과 결합도를 높일 수 있고 테스트하기 어려워질 수 있습니다. 아래에서 싱글톤의 단점과 위험성에 대해 이야기를 하겠습니다.
싱글톤 패턴은 언뜻 사용하기에는 편리성이 좋습니다. 그렇지만 사용하는 방법과 호출하는 방법 등에서 오남용으로 발생할 수 있는 문제들이 있습니다.
다른 클래스에서 전역 객체를 호출한다는 것은 클래스의 인스턴스 간 결합도가 높아져서 '개발-폐쇄 원칙(OCP, Open Close Principle)'에 위배가 됩니다.
그리고, 멀티 스레드 환경에서 동기화 처리가 되지 않을 때, 싱글톤 패턴의 정의와 같이 하나가 아닌 n개의 인스턴스가 생성될 수 있습니다. 그렇게 되었을 때 데이터의 무결성이 무너지고 메모리 관리에도 치명적인 이슈가 발생하게 됩니다.
class Singleton private constructor() {
init {
println("Singleton 인스턴스 생성")
}
companion object {
// getInstance를 호출 할 때마다 새로운 객체를 인스턴스화함.
fun getInstance(): Singleton {
return Singleton()
}
}
}
fun main() {
// 신규 인스턴스가 호출하는 만큼 생성
val singleton1 = Singleton.getInstance()
val singleton2 = Singleton.getInstance()
if (singleton1 == singleton2) {
println("같은 인스턴스입니다.")
} else {
println("다른 인스턴스입니다.")
}
}
synchronized
키워드를 이용해서 thread-safety를 보장합니다.class SynchonizedSingleton private constructor() {
init {
println("SynchonizedSingleton 인스턴스 생성")
}
companion object {
@Volatile
private var instance: SynchonizedSingleton? = null
@Synchronized
fun getInstance(): SynchonizedSingleton {
if (instance == null) {
instance = Singleton()
}
return instance!!
}
}
}
class LazySingleton private constructor() {
init {
println("LazySingleton 인스턴스 생성")
}
companion object {
val instance: LazySingleton by lazy {
LazySingleton()
}
}
}
class HolderSingleton private constructor() {
init {
println("HolderSingleton 인스턴스 생성")
}
companion object {
private val instanceHolder = Holder.instance
val instance: HolderSingleton
get() = instanceHolder
}
private object Holder {
val instance = HolderSingleton()
}
}