[DesignPattern] 싱글톤 (Singleton)
09 Sep 2023싱글톤 (Singleton)
오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공합니다.
활용 방안
싱글톤은 다음과 같은 상황에 사용할 수 있다.
- 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점(accesspoint)으로 모든 사용자가 접근할 수 있도록 해야 할 때
- 유일한 인스턴스가 서브 클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때
구조
- Singleton: Instance() 연산을 정의하여, 유일한 인스턴스로 접근할 수 있도록 합니다. 유일한 인스턴스를 생성하는 책임을 맡습니다.
장점
- 유일하게 존재하는 인스턴스로의 접근을 통제합니다.
- 무분별한 전역 변수 사용으로 인한 Class 식별을 위한 이름 공간(name space)을 좁힙니다.
- 연산 및 표현의 정제를 허용합니다. 상속된 서브 클래스를 통해 새로운 인스턴스를 만들 수 있습니다.
- 인스턴스의 개수를 변경하기가 자유룝습니다.
- 클래스 연산을 사용하는 것보다 훨씬 유연한 방법입니다.
구현
Singleton 기본 구조
Singleton 패턴의 기본 구조는 아래와 같다.
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {}
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
static
으로 객체의 인스턴스 변수를 생성한 뒤 생성자를 private
접근자로 설정해 외부에서 해당 클래스의 접근을 막아 줍니다.
이후 getInstance()
static 메서드를 이용해 기존에 생성된 인스턴스만 가져올 수 있도록 구현합니다.
외부에선 아래와 같이 호출 할 수 있습니다.
public class Main {
public static void main(String[] args) {
Singleton1 singleton1 = Singleton1.getInstance();
}
}
또한 Singleton에는 두가지 초기화 방법이 있다.
앞서 보여준 기본 구조는 느린 초기화(Lazy Initialization)로 실제 인스턴스를 사용하기 위해 호출할 때 인스턴스를 생성하는 방법이다.
반대되는 방식으로 이른 초기화(Early Initialization)가 있다.
이른 초기화(Early Initialization)
static final 변수에 미리 new를 통해 인스턴스화 해서 인스턴스를 반환한다.
Thread-safe 하지만 인스턴스를 생성할 때 메모리를 많이 사용하는 클래스가 사용한다면, 해당 클래스를 사용하지 않을때 메모리 낭비가 된다.
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return instance;
}
}
하지만 기본 구조는 멀티쓰레드 환경에서 안전하지 못하다. 인스턴스가 생성되지 않은 시점에 두개 이상의 쓰레드가 동시에 if (instance == null)
조건에 접근한다면 하나 이상의 인스턴스가 생성될 것이고, 이 패턴은 깨지게 됩니다.
멀티 쓰레드 환경의 Singleton 구조
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
getInstance()
메서드에 synchronized
키워드를 추가해 동기화 메서드로 만들어 주면 Thread-safe 한 Singleton 클래스를 만들 수 있다.
하지만 불필요한 동기화 때문에 성능이 저하 될 수 있다. 그래서 아래와 같은 방법들을 이용해 개선할 수 있다.
double checked locking 방법 사용
public class Singleton3 {
private static volatile Singleton3 instance;
private Singleton3() {}
public static synchronized Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null)
instance = new Singleton3();
}
}
return instance;
}
}
JDK 1.5 이상부터 사용 가능한 키워드인 volatile
를 추가하여 instance 변수를 메인 메모리로 올려준 다음, 동기화 이전과 동기화 이후에 if (instance == null)
의 조건을 두번 사용함으로써, 두개 이상의 쓰레드가 접근할 때만 동기화 블럭으로 진입함으로 무분별한 동기화를 막을 수 있다.
만약 JDK 1.5 미만의 버전을 사용하여 volatile
키워드를 사용할 수 없다면 static inner Class
를 사용해 Singleton
을 구현할 수 있다.
public class Singleton4 {
private Singleton4() { }
private static class Singleton4Holder {
private static final Singleton4 INSTANCE = new Singleton4();
}
public static Singleton4 getInstance() {
return Singleton4Holder.INSTANCE;
}
}
이 구조는 Singleton4
클래스의 정적 메서드인 getInstance()
가 호출될 때 static inner Class
로 선언된 Singleton4Holder
클래스가 초기화 하여 정적필드인 INSTANCE
변수의 초기화가 일어나면서 인스턴스가 생성되고 반환합니다.
열거형을 이용한 Singleton 구현
앞서 구현한 Singleton 패턴은 리플렉션 API와 직렬화 & 역직렬화를 이용해서 쉽게 깨트릴 수 있다.
보통 리플렉션 API를 사용한다면 막을 수 있는 방법은 거의 없다. 하지만 enum을 사용하면 안전하게 방어를 할 수 있게 된다. 단점이 있다면 이른 초기화로 인스턴스가 미리 만들어진다는 것과 상속을 사용할 수 없다는 점이다.
public enum Singleton5 {
INSTANCE;
/* 이하 구현 */
...
}
자바와 스프링의 Singleton
자바 Runtime
자바의 기본 클래스 Runtime
의 경우 이른 초기화를 이용해 미리 인스턴스를 생성하고 반환한다.
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
스프링의 Bean
스프링에서 사용하는 Bean객체는 정확하게 Singleton은 아니지만 내부의 ApplicationContext
에서 인스턴스를 하나씩 생성하여 관리하고 있습니다.
getBean
을 이용해 생성된 Bean을 호출하면 자체적으로 관리하고 있는 인스턴스를 반환해 프레임워크에서 사용합니다.
public class SpringExample {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
String hello = applicationContext.getBean("hello", String.class);
String hello2 = applicationContext.getBean("hello", String.class);
System.out.println(hello == hello2);
}
}