본문 바로가기

Dev.BackEnd/JAVA

[DP] 1. 싱글턴 패턴(Singleton pattern)

#1. 싱글턴 패턴(Singleton Pattern)
싱글턴 패턴이란 인스턴스를 하나만 만들어 사용하기 위한 패턴이다. 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의 경우, 인스턴스를 여러 개 만들게 되면 자원을 낭비하게 되거나 버그를 발생시킬 수 있으므로 오직 하나만 생성하고 그 인스턴스를 사용하도록 하는 것이 이 패턴의 목적이다.
하나의 인스턴스만을 유지하기 위해 인스턴스 생성에 특별한 제약을 걸어둬야 한다. new를 실행할 수 없도록 생성자에 private 접근 제어자를 지정하고, 유일한 단일 객체를 반환할 수 있도록 정적 메소드를 지원해야 한다. 또한 유일한 단일 객체를 참조할 정적 참조변수가 필요하다.

Singleton.java >> ver.1

public class Singleton {
private static Singleton singletonObject;

private Singleton() {}

public static Singleton getSingletonObject() {
if (singletonObject == null) {
singletonObject = new Singleton();
}
return singletonObject;
}
}


이 코드는 정말 위험하다.
멀티스레딩 환경에서 싱글턴 패턴을 적용하다보면 문제가 발생할 수 있다.
동시에 접근하다가 하나만 생성되어야 하는 인스턴스가 두 개 생성될 수 있는 것이다.
그렇게 때문에 getSingletonObject() 메소드를 동기화시켜야 멀티스레딩 문제가 해결된다.

Singleton.java >> ver.2

public class Singleton {
private static Singleton singletonObject;

private Singleton() {}

public static synchronized Singleton getSingletonObject() {
if (singletonObject == null) {
singletonObject = new Singleton();
}
return singletonObject;
}
}


synchronized 키워드만 보면 느려진다는 느낌이 든다.(실제로 약 100배 정도 느려진다고 한다.)
`singletonObject`를 더 효율적으로 제어할 수는 없을까?

Singleton.java >> ver.3

public class Singleton {
private static volatile Singleton singletonObject;

private Singleton() {}

public static Singleton getSingletonObject() {
if (singletonObject == null) {
synchronized (Singleton.class) {
if(singletonObject == null) {
singletonObject = new Singleton();
}
}
}
return singletonObject;
}
}


DCL(Double Checking Locking)을 써서 getSingletonObject()에서 동기화 되는 영역을 줄일 수 있다. 초기에 객체를 생성하지 않으면서도 동기화하는 부분을 작게 만들었다그러나 이 코드는 멀티코어 환경에서 동작할 때, 하나의 CPU를 제외하고는 다른 CPU가 lock이 걸리게 된다. 그렇기 때문에 다른 방법이 필요하다.

Singleton.java >> ver.4

public class Singleton {
private static volatile Singleton singletonObject = new Singleton();

private Singleton() {}

public static Singleton getSingletonObject() {
return singletonObject;
}
}


ver.1, 2, 3 은 `getSingletonObject()`라는 메소드를 호출하면 그 때서야 객체를 생성하고 생성한 객체를 반환한다. 그에 반해 ver.3 은 클래스가 로딩되는 시점에 미리 객체를 생성해두고 그 객체를 반환한다. Eager initialization 해주는 것이다. 하지만 이렇게 되면 프로그램이 실행되고 나서 처음부터 끝까지 객체가 메모리에 있게 된다. 이 인스턴스를 사용하지 않을 경우에도 말이다.

그리고 volatile 이라는 키워드가 붙어있다.
잠깐 volatile 이라는 키워드에 대해 알아보고 가자.

Voliate
사전적 정의로는 ‘변덕스러운'이란 뜻이다. 멀티 스레딩환경에서 동기화를 해주는 키워드이다. 좀 더 구체적으로는 컴파일러가 특정 변수에 대해 옵티마이져가 캐싱을 적용하지 못하도록 하는 키워드이다.

멀티 스레드에서 for나 while 문이 옵티마이져에 의해 캐싱(caching)을 사용하는데, 이 때 동기화 문제가 발생할 수 있다. 한 스레드에서 다른 스레드의 작업이 마치기를 기다린다고 가정했을 때, 최신의 변수를 읽어오지 못한다면 무한루프에 빠질 수 있다. 이러한 문제 발생 상황을 volatile 키워드를 사용하여 모든 스레드에 대해 항상 최신의 값을 읽을 수 있게 해주는 것이다.

그렇다면 syncronized 키워드와의 차이점은 무엇인가?
syncronized는 작업 자체를 원자화해버린다. volatile은 특정 변수에 대해서 최신 값을 제공한다.



Singleton.java >> ver.5

public class Singleton {
private Singleton() {}

private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}

public static Singleton getSingletonObject() {
return SingletonHolder.INSTANCE;
}
}

이 방법은 중첩 클래스를 이용한 Holder를 사용하는 방법이다. getInstance 메서드가 호출되기 전까지는 Singleton 인스턴스는 생성되지 않는다. 그리고 getInstace 메소드가 호출되는 시점에 SingletonHolder가 참조되고 그 때 Singleton 객체가 생성된다. 지연된 초기화(lazy Initialization) 기법을 사용하기 때문에 메모리 점유율 면에서 유리하고, synchronized 키워드를 사용하지 않았기 때문에 어떠한 성능 문제도 보이지 않는다. 그러나 아직 인스턴스가 생성되지 않은 초기의 시점에서 두 스레드가 동시에 getInstance를 실행하면 인스턴스가 두 개 생성될 것 같은 기분이 든다. 하지만 최신 VM은 클래스를 초기화하기 위한 필드 접근은 동기화한다. 초기화되고 나면 코드를 바꿔서 앞으로의 필드 접근에는 어떤 동기화나 검사도 필요하지 않게 된다. 그러므로 초기화 이후 다시 getInstace 메소드가 호출된다고 하더라도 new Singleton() 은 호출되지 않는다.

이제 main()메소드에서 싱글톤 객체를 생성해보자.

Main.java code>>

public static void main(String[] args) {
//생성자에 private 키워드가 있기 때문에 new를 통해 인스턴스를 생성할 수 없다
//Singleton singleton = new Singleton();
Singleton s1 = Singleton.getSingletonObject();
Singleton s2 = Singleton.getSingletonObject();
System.out.println(s1);//com.algorithm.singletonpattern.Singleton@39ed3c8d
System.out.println(s2);//com.algorithm.singletonpattern.Singleton@39ed3c8d
}


같은 주소값이 console 창에 출력되고 있다. 동일한 객체임을 알 수 있다.



1. singleton pattern end