싱글톤이란?
하나! 오직 하나!
싱글톤 패턴은 클래스의 인스턴스가 오직 하나임을 보장하는 디자인 패턴이다.
어떤 클래스의 인스턴스를 생성하고 소멸하는 과정이 빈번하게 발생한다면, 고민해도 될 만한 패턴
대표적인 형태 - Eager initialization
public class Singleton{
// 인스턴스를 Static 생성한다.
private static final Singleton instance = new Singleton();
// private로 선언된 생성자
private Singleton(){
System.out.println("Create Instance");
}
// 인스턴스를 사용할 때는 이렇게 호출한다.
public static Singleton getInstance(){
return instance;
}
}
싱글톤 패턴을 사용하는 코드에서 나타나는 특징은 크게 세 가지 입니다.
- private로 선언된 생성자
단 하나의 인스턴스를 보장하기 위해서, 클래스 외부에서 새로운 인스턴스의 생성을 막아야 하므로 private로 접근을 제어한다.
- Static으로 선언된 인스턴스 멤버 변수
일반적으로 우리가 new
를 사용하여 클래스의 인스턴스를 선언하는 방법을 동적 생성이라고 하는데, 이 때는 메모리 구조상 Heap에 위치하게 된다.
Heap에 있는 인스턴스는 메모리 할당과 해제를 거치게 되는데, 이 연산이 많은 사용자들이 사용하는 서버에서 동시에 이루어 진다면, 부하가 될 수 있다.
반면 Static으로 선언된 인스턴스는 최초 클래스 로드시에 메모리에 적재된 후, 계속 메모리에 남게 된다.
- 오직 getInstance() 함수로 Instence 접근
이 클래스의 인스턴스를 접근할 수 있는 곳은 오로지 getInstance() 입니다.
위의 세 가지 조건을 만족한다면 클래스의 인스턴스가 1개인 클래스를 생성가능합니다.
이 방법이 가지는 단점은 이 클래스를 사용하든 안하든 항상 메모리 상에 상주한다는 겁니다.
getInstance()
가 실행 되기 전부터 메모리에 상주하여 낭비를 한다는 것이 마음에 썩 들지는 않습니다.
그래서 클래스를 사용할 때 메모리에 인스턴스를 로드하는 방법을 고안합니다.
Lazy Initialization
public class Singleton{
// 미리 생성하지 않는다.
// private static final Singleton instance = new Singleton();
private static Singleton instance = null;
// private로 선언된 생성자
private Singleton(){
System.out.println("Create Instance");
}
// 인스턴스를 사용할 때는 이렇게 호출한다.
public static Singleton getInstance(){
// 추가!! 최초로 사용할 때 인스턴스를 생성한다.
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
이전 코드와 비교 하기 위해서 주석으로 이전 코드를 남겨 두었다.
- 미리 생성하지 않는다.
클래스의 인스턴스를 사용하기 전부터 메모리에 상주하던 방법이 아닌 최초로 사용하는 곳에서 인스턴스를 생성합니다.
getInstance()
함수가 한 번이라도 실행되어야 이 클래스를
- 단 하나의 Instance 보장 문제
우연히 같은 시간에 여럭 개의 쓰레드가 getInstance()
를 호출된다면, 하나의 인스턴스를
이 경우에 단 하나의 Instance를 보장 가능하다고 말 할 수 없습니다.
아래의 코드는 Thread-Safe 를 붕괴해보는(?) 소스코드 입니다.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Runner implements Runnable{
public static final CyclicBarrier gate = new CyclicBarrier(20);
@Override
public void run() {
try {
gate.await();
Singleton s = Singleton.getInstance();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for(int i=0; i<20; i++) {
Runner broker = new Runner();
Thread worker = new Thread(broker);
worker.start();
}
}
}
이 소스 코드는 20개의 쓰레드가 동시에 getInstance()
를 실행하는 소스 코드입니다.
실행 결과를 보면 여러 개의 인스턴스가 생성되는 것을 확인 할 수 있습니다.
Lock을 걸어버리자
public class Singleton{
private static Singleton instance = null;
// private로 선언된 생성자
private Singleton(){
System.out.println("Create Instance");
}
// 추가!! Lock을 걸어보자
public static synchronized Singleton getInstance(){
// public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
synchronized
로 선언된 영역은 Lock을 걸어 주는 역할을 수행하므로 하나의 쓰레드씩 이 함수를 수행하게 되므로 Thread-Safe 문제를 보완해 줍니다.
하지만 우리가 병렬로 수행하기 위한 목적으로 멀티쓰레드를 쓰는데, synchronized
구간에서 병렬성이 무너지기 때문에 성능저하가 발생할 수 있습니다.
holder에 의한 초기화
public class Singleton{
// private로 선언된 생성자
private Singleton(){
System.out.println("Create Instance");
}
// Holder Class 추가
private static class Holder {
static{
System.out.println("Holder Class");
} // Holder 클래스를 메모리에 로드할 때 메세지를 보여주기 위함
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return Holder.instance;
}
// getInstance() 말고 다른 함수를 호출하였을 때 인스턴스 생성 여부 확인용
public static void print(){
System.out.println("Singleton.print()");
}
}
Singleton 클래스 내부에 Holder 클래스를 작성하고 그의 멤버 변수로 Singleton 클래스의 인스턴스를 생성합니다.
앞서 소개한 두 가지 방법에서 발생한 문제점을 어느 정도 보완하는지 체크해 보았습니다.
- 사용 전에 미리 생성하지 말 것
getInstance()
호출 전 Singleton 클래스의 인스턴스가 static 맴버 변수로 작성되지 않았으므로 인스턴스가 생성되지 않습니다.
위의 소스코드에서getInstance()
를 실행하지 않고 print()
함수만 실행했을 때 결과를 보면 쉽게 이해하실 수 있을 겁니다.
- Thread-Safe 보장
getInstance()
함수가 실행되면 Static으로 선언된 Holder 클래스는 JVM의 Class Loader 에 의해 메모리에 적재됩니다.
Holder는 Singleton 클래스의 인스턴스를 가지고 있으니 Thread-Safe를 붕괴할 수 있는 경우는 Holder 클래스가 Class Loader에 의해 메모리에 적재될 때 중첩되어 Load되는 경우 뿐입니다.
하지만 Class가 초기화되는 과정은 Java Language Specification (JLS)에 의해 시퀀스하게 진행됩니다.
그래서 synchronized
로 실행했을 때처럼 Lock을 제공하지만 더 좋은 성능을 보장하는 방법을 제공합니다.
긴 글 읽어 주셔서 감사합니다.
공감 버튼은 작성자에게 큰 동기부여가 됩니다.