싱글톤 패턴(Singleton Pattern)은 객체의 생성에 관련된 패턴 중 하나로 한 Class의 객체를 하나만 존재하도록 하는 방법입니다.
일반적으로 getInstance 라는 메소드를 구현하여 해당 메소드를 통해서만 instance를 생성하고 받을 수 있도록 하는 형태로 구현됩니다.
싱글톤 패턴은 객체의 생성과 관련된 패턴으로 생각보다 그 구조는 단순합니다. 간단한 예제를 통해서 보도록 하죠.
현재 사운드 드라이버에 대한 객체를 디자인 한다고 생각해 보도록 하죠. 이때 여러 프로그램에서 해당 사운드 드라이버를 호출하여 소리를 출력하려고 하는 상황입니다. 이럴때 사운드 드라이버 객체는 하나의 같은 객체에 접근해야 합니다. 어떻게 해야할까요? 밑의 Code 부분을 통해서 확인합시다.
SoundDriver class
public class SoundDriver{
SoundDriver(){
System.out.println("SoundDriver has been activated");
}
public void makeSound(){
System.out.println("Dong Dong");
}
}
그런데 이렇게 디자인을 하고 객체를 생성할때 new 키워드를 통해서 생성하면 여러곳에서 해당 객체를 호출하면 다 각기 다른 Instance를 호출하게 되겠죠? 이때 Singleton Pattern을 적용시켜 봅시다.
SoundDirver class ( Singleton implemented )
public class SoundDriver{
private static SoundDriver instance = null;
private SoundDriver(){
System.out.println("SoundDriver has been activated");
}
public SoundDriver getInstance(){
if(instance == null) instance = new SoundDriver();
return instance;
}
public void makeSound(){
System.out.println("Dong Dong");
}
}
이렇게 Singleton Pattern을 적용했습니다. 하나하나 설명은 아래와 같습니다.
이와 같이 생성하면 Singleton Pattern이 적용되어 class당 하나의 인스턴스만 생성하여 사용하도록 구현되었습니다. 이렇게 구현하면 일반적인 경우에는 문제가 되지 않습니다. 하지만 세상은 그렇게 녹록치 않더라구요… 이렇게 구현한 싱글톤 패턴도 멀티 쓰레드 환경에서는 문제가 됩니다. 이유는 다음과 같습니다.
getInstance 메소드의 if문의 조건 판별 부분을 통과한 후에 context switch가 이루어 진다면 오류가 생길 수 있습니다.
이러한 문제를 해결하기 위한 방법을 2가지 정도 제시하고자 합니다.
방법 1.) instance 속성에 바로 객체생성 및 할당.
위의 코드에서 instance 속성과 getInstance를 수정하여 적용가능한 방법입니다.
private static final SoundDriver instance = new SoundDriver();
public SoundDriver getInstance(){
return instance;
}
이렇게 하면 우선 멀티 스레드 환경에서의 instance 생성시의 문제를 해결가능한데 이러한 방법은 객체를 사용하지 않더라도 일단 객체를 생성하기에 이러한 방법으로 다수의 객체를 사용시 메모리가 불필요하게 많이 사용됩니다.
방법 2.) synchronized & volatile 사용. Double-Checked Locking 방법.
public class SoundDriver{
private volatile static SoundDriver instance;
private SoundDriver(){
System.out.println("SoundDriver has been activated");
}
public static SoundDriver getInstance(){
if(instance == null) {
synchronized(this){
if(instance == null){
instance = new SoundDriver();
}
}
}
return instance;
}
public void makeSound(){
System.out.println("Dong Dong");
}
}
우선 이를 이해하기 위해 새로운 2가지 키워드에 대해 소개하고자 합니다.(사실 저도 생소한 문법이긴합니다.)
전체적으로 보면 해당 코드는 “멀티 스레드 환경에서 Thread-safe한 코드를 작성하기 위해 synchronized라는 키워드를 사용하여 lock을 한다” 라는 부분까지는 얼추 이해가 쉬울 것이라 보는데 왜 하필이면 if(instance == null)
부분 후에 synchronized를 사용하고 다시 if(instance == null)
을 사용하는가? 하는 부분이 의문이 들수 있다. 사실 해당 static 메소드 전체에 sychronized 키워드를 걸수도 있지만 그렇게 한다면 성능의 저하가 심하다. 성능의 비약적인 상승을 위해서 lock을 거는 부분은 문제가 생길수 있는 가장 작은 부분에 걸어주는 것이 좋은데 해당 코드에서는 if문 부분이다. 그렇다면 synchrozied를 if문 전에 걸어주고 if문을 한번만 사용하면 되는것이 아닌가? 라는 의문이 들 수 있는데 한가지 경우를 생각해보자. 예를들어 instance는 생성되어있고 해당 if문의 조건을 확인하다가 context switch가 일어났다고 생각해보자. 그렇다면 다음 스레드가 getInstance를 호출한다면 if문 앞에서 lock이 걸리는 것이 맞을까? 그렇지 않다. 그렇다면 if문 안에 synchronized를 사용하고 synchroznied 안에 if문을 제거한다면? 이부분은 생각해 볼 필요도 없다고 생각한다.
너무 이야기가 길어진것 같은데 아직 와닿지 않는다면 곰곰히 생각해보길 바란다.
(스스로 생각해서 해답을 찾는것이 중요하다고 생각한다.)
오늘은 한 클래스당 하나의 instance만을 가지도록 하는 Singleton pattern에 대해서 알아보았습니다. 또한 추가적으로 Multi-Thread환경에서 thread-safe한 방법으로 singleton 패턴을 구현하는 것 또한 알아보았는데 이를 위해 synchronized 키워드와 volatile에 대해 추가적으로 알아보았습니다. 사실 가장 간단한 방법은 instance를 저장하는 속성 자체를 static final 로 선언한 후 바로 instance를 생성해서 할당해 주는 방법이 있는데 이는 해당 클래스를 아예 사용하지 않더라도 메모리 공간을 차지한다는 메모리 누수적인 단점이 있기에 언급하지 않았습니다. 아무튼! 해당 싱글톤 패턴을 잘 사용한다면 하나의 인스턴스만이 존재해야 하는 클래스를 만들어 사용하고 관리할 수 있다는 점 기억하고 넘어가도록 하면 좋을 것 같습니다.