싱글톤 패턴(Singleton Pattern) 처음부터 끝까지
오늘도 평화롭게 프리코스 주차 미션을 하고있던 와중.. 코드리뷰에서 다른 참가자가 작성한 코드리뷰가 떠올랐다.
이건 싱글톤 패턴을 사용하기 적합한 클래스네요!
싱글톤에 대해 아무것도 몰랐던 당시의 나는 그냥 넘어갔지만 현재 작성중인 클래스가 말로만 듣던 싱글톤 패턴을 쓰기 좋은 예제라고 생각이 들었다!
하지만 난 싱글톤 패턴에 대해 아는것이 없었다.

그렇다고 모른채로 넘어갈수 없었기에 이번 기회에 제대로 알아보고 정리해보기로 했다! 나처럼 싱글톤 패턴을 처음 들어본 사람도 쉽게 알수 있도록 최대한 가볍게 적어보려고 해보겠다.

주의사항
본 작성자는 해당 정보가 오류가 있을수도 있다는것을 인지하고 있습니다.
혹시 글을 읽다가 잘못된 정보가 있다면 언제든지 알려주시면 감사하겠습니다!
그래서? 싱글톤 패턴이 뭔데?
싱글톤 패턴(Singleton Pattern)은 디자인 패턴중에 하나로, 클래스의 객체를 한개만 생성해서 그 객체만을 사용하는 패턴이다.
왜 객체를 한개만 생성하는거야?
예를 들면 우리가 간단한 계산기 기능을 가진 클래스를 만든다고 가정해보자! 이 클래스에서는 계산만 하고 결과값만 반환하는 메서드만 있을것이다. 즉 상태값이 없고 기능만을 수행하는 클래스라고 볼수있다.
이를 사용하려고 여러곳에서 객체를 생성하면 낭비라는 생각이 들지 않는가? 이런 경우에 싱글톤 패턴을 통해 객체를 한개만 생성하고 이를 돌려쓴다는 거다. 즉 메모리적으로 효율이 좋아진다는 거다!
오 좋은데? 그래서 어떻게 쓰는데?
가장 기본적인 싱글톤 패턴의 예시를 보여주겠다.
public class Singleton {
private static Singleton singleton = null;
private Singleton(){} //생성자 사용 안함!
public static Singleton getInstance(){ //객체 생성시에는 이걸로!
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
//호출할때는 이런식으로
Singleton single=SingleTon.getInstance();
이런 식으로 기존의 클래스처럼 생성자로 객체를 만드는것이 아니라 static 메서드인 getInstance()로 객체를 만드는거다.
동작 과정
1. 최초 getInstance() 호출때는 클래스 변수인 singleton이 null이니까 객체를 생성하고 이를 반환해준다
2.이후 getInstance() 호출때는 singleton은 null이 아니니까 만들지 않고 그대로 반환해준다.
이런 식으로 최초 호출때 객체를 생성하고 이후에는 그 객체만 호출이 되서 사용되는거다!
오! 완전 신기하다! 이제 쓰러 가볼게요 ㅂㅂ

잠깐만 기다려봐라! 이 싱글톤 패턴에는 아주 중요한 문제가 있다. 이대로 가면 후회할거다!
엥.. 뭔데요..
싱글톤패턴의 가장 큰 문제는 바로 동시성의 문제이다. 동시성에 대해 처음 들어본사람이 있을것이다.
(나도 이번에 처음 들었다)
간단하게 말하면 멀티쓰레드 환경에서 우리가 의도한 것과는 다르게 객체가 여러개 생길수도 있는거라는 거다!
멀티쓰레드 환경..? 그게 뭔데요?? 처음 들어보는데..
우리가 짜고 돌리는 프로그램은 크게 프로세스와 쓰레드로 구성이 되어있다!!
(CS 전공자들은 자주 들어봤을 면접 질문 프로세스와 쓰레드의 차이점이 뭔가요?)
간단하게 말하면 프로세스라는 거대한 공장안에 쓰레드라는 인부들이 일을 하고 있다고 보면된다.
이 공장은 시설이 안좋아서 일을 할수있는 작업대가 하나밖에 없다! 그래서 각자 일을 해야하는 인부들은 순서대로 작업대를 사용하면서 자신들이 해야할 일을 하는거다!
그래서 결국 동시성이 뭔데요?
예를 들면 두개의 쓰레드가 각자 싱글턴 패턴을 이용한 클래스를 사용하려고 getInstance() 메서드를 호출했다고 가정해보자.
public class Singleton {
private static Singleton singleton = null;
private Singleton(){} //생성자 사용 안함!
public static Singleton getInstance(){ //객체 생성시에는 이걸로!
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
//호출할때는 이런식으로
Singleton single=SingleTon.getInstance();
1번 쓰레드는 처음에 null을 확인하기 위해 메모리를 볼것이다.(정확히는 캐시지만 이는 뒤에 차근차근 설명해보겠다)
1번 쓰레드는 다음과 같이 생각할것이다.
'어? null이네! 그러니까 if문 내부를 실행해야지.' 즉, 객체를 만들어야지가 되는것이다.
이제 만들려고 하는데 옆에서 기다리고 있던 2번 쓰레드가 이렇게 말한다!
"작업대 사용시간 끝났어! 이제 내차례야 비켜!"
그렇게 1번쓰레드는 작업대에서 쫓겨나고(?) 2번쓰레드가 작업을 시작한다. 이 쓰레드도 메모리를 보는데 아직 객체가 만들어지기 전이기에 null일것이다.
null이니까 if문 내부를 실행할것이고 이번에는 시간이 여유로웟는지(?) 객체를 만들고 이를 반환까지 했다. 그렇게 일을 하고있던 중에 기다리고 있던 1번 쓰레드가 이렇게 말한다.
"시간 끝!끝! 이제 내 차례야!"
그렇게 1번쓰레드가 다시 작업대를 차지하고 아까 못한 것부터 수행할것이다.
'어디까지 했지..? 아 if문 내부부터 수행하면 되지!' 이렇게 1번쓰레드도 객체를 생성하고 이를 반환할것이다.
이렇게 싱글톤 패턴에서 객체가 의도치 않게 두개가 생겨버렸다!!
우리는 이를 동시성의 문제라고 부르기로 했다.
그럼 멀티스레드 환경에서는 싱글톤 패턴을 못쓰는거에요?
아니다! 사용할수 있다. 기본 구조에 몇가지를 추가하면 사용할수 있다.
지금부터 멀티스레드 환경에서 싱글톤 패턴을 사용하기 위한 방법을 발전순서대로 작성하겠다. 즉, 뒤로갈수록 좋은것이라고 볼수 있다.
(지금도 문서가 길지만 이제 시작이다.. 다들 좀만 참고 봐보자)
1. getInstance()때 말고 바로 만들어버리자! (Eager Initialization)
class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
바로 클래스 멤버에 직접 객체를 초기화 해버리는 방법이다! 상당히 무식한 방법이다.
이는 클래스 로더(Class Loader)가 클래스를 로딩할때 객체를 생성해주게 된다.
무식한 방법에 맞게 단점도 많이 존재한다.
단점
1. 무조건적인 객체 생성
이는 해당 객체를 사용하지 않아도 이미 로딩할때 객체가 생성되고 이게 메모리에 저장되게 된다. 즉, 자원 낭비가 될수 있다는것이다.
2. Exception 처리 불가능
만약 객체를 생성하는 과정에서 예상치 못한 오류로 Exception이 발생했다고 가정하자!
해당 방법에서는 이를 처리하는 로직을 작성할수 없기에 문제가 발생할수 있다.
2. static 블록에 넣어버리자! (Static Block Initialization)
class Singleton {
private static Singleton singleton;
private Singleton() {}
static{
try{
singleton = new Singleton();
}catch(Exception e){
throw new Exception("객체 생성과정에서 Exception이 발생했습니다.");
}
}
public static Singleton getInstance() {
return singleton;
}
}
1번의 발전형이라고 볼수 있다. 객체 초기화를 직접적으로 하는것이 아니라 static 블록에 넣는것이다.
static 블록은 클래스가 최초 로딩되는 1회에만 수행이 되는 블록을 뜻한다.
해당 블록에서 보이는것처럼 Exception에 대한 처리로직을 구현할수 있게되어 처리가 가능해졌다.
단점
1. 무조건적인 객체 생성
하지만 아직도 로딩시에 무조건적으로 객체가 생성되서 메모리 낭비가 존재한다.
3. Synchronized를 사용해버릴거야! (Thread safe Lazy initialization)
public class Singleton {
private static Singleton singleton = null;
private Singleton(){} //생성자 사용 안함!
public static synchronized Singleton getInstance(){ //객체 생성시에는 이걸로!
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
//호출할때는 이런식으로
Singleton single=SingleTon.getInstance();
기본 코드에서 달라진점은 getInstance() 메서드에 synchronized 라는 키워드가 붙었다는 것이다.
Synchronized? 처음보는데? 저게 뭐지..?
해당 키워드는 동기화를 시켜주는 효과를 지닌다.
어렵게 말하면 어느 순간에는 하나의 스레드만이 임계 영역(Critical Section) 안에서 실행하는 것이 보장해준다는것이다.
임계영역은 뭔데?
쉽게 말하면 쓰레드끼리 공유하는 자원(쉽게 변수)에 대해서, 한 쓰레드만 접근해야하는 영역을 의미한다.
앞의 작업대의 예처럼 여러 쓰레드가 하나의 자원에 대해 동시에 접근해서 값을 변경하려고 하면 접근 순서에 따라 해당 자원의 값이 바뀔수 있다. 이를 막기위해서 한쓰레드가 공유되는 자원에대한 처리를 하고있는 로직코드를 진행중이라면 다른 쓰레드들은 해당 로직코드에 접근할수 없다. 여기서 이 로직코드의 영역을 임계영역 이라고한다
쉽게 공유되는 값의 처리를 할때 이를 일치시켜준다는 의미이다.
따라서 앞에서 의미했던 객체의 중복생성을 막을수 있게되는 것이다!
오! 이제 해결됐네? 이만 가볼게요!

잠깐만 기다려봐라! 아직 이 형태도 단점이 존재한다.
저런 고급 매커니즘을 가진 기능의 경우는 늘 단점이 하나씩 존재할 가능성이 있다... 바로 비용이 적지 않다는것이다.
예를 들어서 수십,수백,수천번을 getInstance()를 사용한다고 생각해보자.
그럼 그때마다 synchronized는 열일을 하면서 동기화를 해줄것이다. 하지만 이는 비용이 적지 않고, 이것이 쌓이고 쌓이다보면 자연스럽게 성능이 저하되는 결과를 초래할수 있다.
단점
1. 적지 않은 비용
계속된 synchronized 사용으로 인해 성능이 저하될수 있다.
4. 그럼 난 두번 검사할래! (Double Checked Locking)
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){ //객체 생성시에는 이걸로!
if(singleton == null){
synchronized (Singleton.class) {
if(singleton == null) singleton = new Singleton();
}
}
return singleton;
}
}
이는 synchronized의 무분별한 사용을 막기위해 if문이라는 관문을 하나 설치하는것이다.
이렇게 되면 synchronized의 사용이 줄어들게 되어서 성능 저하를 억제할수 있게된다.
하지만 이 방법에도 문제점이 존재한다.
자바 메모리모델은 부분적으로 초기화된 객체에 대한 접근을 허용하는 특성을 가진다.
singleton = new Singleton(); 이라는 로직을 수행할때 먼저 해당 객체의 공간을 먼저 할당한후에 값을 넣게 되는 과정을 거치게 된다.
그런데 여기서 적은 확률로 공간만 할당하고 쓰레드가 넘어갔다면? 그런데 다른쓰레드가 getInstance() 메서드를 사용했다면? 그 쓰레드에서는 메모리 공간을 보고 '아! 객체가 있네? 그럼 반환만 해야지!' 하고 생성이 되지 않은 객체를 반환하게 된다. 즉 다른 객체가 생기게되는 문제가 발생하게 된다.
자세하게 설명해보자면 쓰레드는 기본적으로 메모리를 직접 데이터를 가져오는것이 아닌 캐시에서 데이터를 가져오게 된다. (이는 컴퓨터의 성능 향상을 위함인데 자세한건 CS 지식을 설명해야 하기에 넘어간다.)
쓰레드는 실제로는 캐시와 데이터를 주고받으면서 캐시의 값이 주기적으로 메모리에 넘어간다. (이를 flush라고 합니다)
그런데 만약 서로 다른 두 쓰레드가 다른 캐시를 사용하게 될 경우 위와같은 문제가 발생할수 있다. 분명 같은 객체를 보고 있는데 값이 서로 달라지는 결과가 나오게 되는것이다.!
그걸 Visibility 문제라고 부른다고 하더라. (한 쓰레드에서 업데이트 된 값을 다른 쓰레드가 확인할수 없는 문제)
4-1. 동기화를 다시 한번 막아보자! (Double Checked Locking + volatile 메서드 사용)
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){ //객체 생성시에는 이걸로!
if(singleton == null){
synchronized (Singleton.class) {
if(singleton == null) singleton = new Singleton();
}
}
return singleton;
}
}
Volatile은 또 뭐야.. 진짜 처음보는거 투성이네..
나도 그랬다. 그래도 최대한 이해한 범주에서 쉽게 표현해보겠다.
앞에서 쓰레드는 캐시와 데이터를 주고받는다고 말했다. 그런데 volatile은 다음과 같은 의미를 가진다.
'나는 이 데이터는 캐시가 아니라 메모리에서 직접 받을거야!'
즉 volatile을 통해 앞에서 언급된 캐시로 인한 문제가 해결될수 있다는것이다!!
하지만 이는 너무 불편하지 않은가? 뒤에 나올 두가지 방법은 더 간단한 방법이다.
(좀만 더 버텨주길 바란다.. 거의 다왔다..)
5. 클래스안의 클래스 (Bill Pugh Singleton Implementation a.k.a lazyholder)
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
public static final Singleton INSTANCE = new Singleton();
}
}
아마 검색창에 '싱글톤 패턴 동시성 해결' 이라고 검색하면 많이 나올 두가지 방법중 하나일것이다.
이 방법은 inner class의 특징을 활용한 방식이라고 볼수있다.
LazyHolder 클래스는 Inner Class이기에 Singleton클래스가 클래스로더에 의해 로딩이 될때는 로딩이 되지 않는다.
이 클래스는 getInstacne() 메서드를 호출할때 로딩이 되고 객체가 생성되게 된다. 자세히 보면 1번 방법의 로직이 어느정도 들어갔다고 볼수있다. 따라서 멀티쓰레드 환경에서도 안전하고, 사용될때만 객체가 생성되니 메모리 낭비도 없다!
즉 완벽한 방법이라고 볼수도 있다.
하지만! 이 방법도 특정 로직을 이용하게되면 싱글톤 패턴을 무너트릴수 있게된다.
바로 리플렉션(Reflection)이다.
간단하게 설명하면 Reflection을 사용하면 외부에서도 private 생성자,메서드에 접근이 가능해진다!
즉 객체를 만들어 낼수 있다는것이고 이를 통해 싱글톤 패턴이 무너질 수 있다.
6. 이 모든걸 해결할 방식 Enum ( Enum Singleton )
public enum Singleton {
INSTANCE;
}
최고의 방식답게 코드마저 간결하다!
Enum 인스턴스는 기본적으로 멀티쓰레드 환경에서 나올수 있는 문제를 해결해준다.
이 방식은 이런 Enum의 특성을 이용한 방식이라고 볼수 있다.
하지만 이 방식도 완벽한 방식이라고 할수는 없다고 한다.
해당 방식은 앞의 방식처럼 Lazy Loading 즉, 사용시에 메모리에 올라가는 방식이 아니기에 자원의 효율성을 해결하지 못한다는 한계가 존재하고, 인스턴스 이외의 멀티쓰레드 환경의 안정성을 보장하려면 개발자가 직접 구현을 해야한다는 점이 있다.
이렇게 길게 써놓고 결국 뭘 쓰라는건데요?
이에 대해서는 한가지 짤로 표현이 가능할것 같다.

무엇이 정답이라는 것은 없고 각자의 개발 상황에 맞게 적절하게 쓰는것이 가장 좋은 답이라고 생각한다.
글을 마무리 지으면서
분명 가벼운 마음으로 찾아본 정보였는데 파고파다보니 너무나도 깊은 문제였고, 결론은 알잘딱깔센이었다..!
하지만 글을 적으면서 여러모로 머리속에 어느정도 정립이 된 느낌이다.
이 못난 글을 통해 누군가 도움이 되었으면 좋겠다
참고 자료
1. https://ynzu-dev.tistory.com/entry/JAVA-싱글톤-패턴Singleton-Pattern-멀티-스레드-환경에서의-문제점
2. https://superfelix.tistory.com/83
3. https://sorjfkrh5078.tistory.com/108
5.https://ttl-blog.tistory.com/238#%F0%9F%A7%90%20Mutual%20Exclusion%20%26%20Visibility-1
6.https://seunghyunson.tistory.com/28