싱글톤 패턴이란?
- 클래스의 인스턴스를 오직 하나만 두고 사용하는 패턴이다.
- 1개 외에 추가적인 인스턴스 생성을 의도적으로 막아야 한다.
용례
- 애플리케이션의 구성(configuration) 정보와 같이 런타임 내에 공유되어야 하는 정적인 정보를 가지고 있는 클래스의 인스턴스를 만들 때 이 패턴을 사용할 수 있다.
- 생성 비용이 큰 인스턴스가 있을 때, 이 인스턴스를 한번 만들고 계속 재활용이 가능하다면 이 인스턴스를 싱글톤으로 공유하는 것을 생각해볼 수 있다.
자바에서의 나이브한 싱글톤 구현
public class SingletonClass {
private static instance;
private SingletonClass() {
// 생성자를 private 으로 만들면, 외부에서 생성자 호출이 불가능하다.
}
public static SingletonClass getInstance() {
// 1번이라도 호출되어야 인스턴스를 생성한다. 처음부터 싱글톤 인스턴스가 만들어져있는 것이 아니다.
if (instance == null) {
instance = new SingletonClass();
}
// static 메서드에서 static 필드를 반환하는식으로 구현한다.
return instance;
}
}
static
키워드를 활용하여 싱글톤 클래스를 작성할 수 있다.static
키워드가 붙은 데이터는Method Area(Static Area)
에 프로그램의 시작부터 상주한다.- 인스턴스 자체는
new
키워드를 통해 생성된 시점에Heap Memory
에 존재하게 된다.private static instance
필드는 프로그램 종료 시까지 유지되므로 싱글톤 인스턴스는 프로그램 종료시까지 가비지 컬렉팅되지 않는다.
나이브한 코드의 문제
- 멀티 스레드 환경에서 안전하지 않다.
- 다중 스레드가 동시에
if(instance == null)
에 접근하게 된다면, 여러 개의SingletonClass
가 생성될 수도 있다.
멀티스레드를 고려하지 않은 스프링 개발자라면 큰 실수로 이어질 수도 있다.
흔히 보이는 스프링에서는 이용자에게 스레드 풀에서 스레드를 할당하는 방식으로 작업을 하므로 멀티스레드 작업이 자연스레 일어나니 주의해야 한다.
문제 해결 1: synchronized
public class SingletonClass {
private static instance;
private SingletonClass() {
// 생성자를 private 으로 만들면, 외부에서 생성자 호출이 불가능하다.
}
public static synchronized SingletonClass getInstance() {
// 1번이라도 호출되어야 인스턴스를 생성한다. 처음부터 싱글톤 인스턴스가 만들어져있는 것이 아니다.
if (instance == null) {
instance = new SingletonClass();
}
// static 메서드에서 static 필드를 반환하는식으로 구현한다.
return instance;
}
}
synchronized
키워드는 해당 메서드에 하나의 스레드만 접근함을 보장해준다.lock
을 이용한다.
- 다만, 동기로 실행되기 때문에 앞서 해당 메서드를 점유한 스레드의 작업이 끝나기 전까지는 메서드를 사용할 수 없다.
- 이 과정에서 약간의 성능 저하가 생겨날 수 있다.
문제 해결 2: 미리 만들기
public class SingletonClass {
private static final INSTANCE = new SingletonClass();
private SingletonClass() {
// 생성자를 private 으로 만들면, 외부에서 생성자 호출이 불가능하다.
}
public static SingletonClass getInstance() {
return INSTANCE;
}
}
- 인스턴스의 초기 생성비용이 그렇게 신경쓰이지 않는다면, 이 방법도 좋은 방법이다.
- 다만, 이렇게 만들어놓고 사용하지 않는다면 낭비인 것은 반드시 알아두어야 한다.
- 성능 문제나 멀티스레드 문제 없이 싱글톤 패턴을 구현할 수 있다.
문제 해결 3: double checked locking
이용하기
public class SingletonClass {
private static volatile instance;
private SingletonClass() {}
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (Settings.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
}
위 코드는 자바 1.5 이상에서만 동작한다.
설마 지금 1.4 이하를 쓰는 사람이 있을까?
- 인스턴스가 만들어져있는지 한번 먼저 확인 후에
synchronized
블럭에서 2차로 확인하는 방식이다.- 장점은 매번
synchronized
가 걸리지 않는다. 인스턴스 생성 시 한번만.
- 장점은 매번
- 여러 스레드가 동시에 진입하더라도 단 하나의 스레드만 인스턴스를 만드는 것을 보장할 수 있다.
- 이전에 메서드 자체에
synchronized
키워드를 사용한 것과 다르게 일단 한번 인스턴스화 시키면 성능상의 문제도 없다. volatile
키워드까지 적어주어야 서로 다른 스레드라도 같은 메모리를 참조한다는 보장을 할 수 있다.instance
를 읽어올 때 CPU 캐시에서 읽어오는 것이 아닌 메인 메모리에서 읽어옴을 보장해준다.
DCLP 를 사용하지 말아야 한다는 의견 도 있는데, 이 의견은
java.concurrency
패키지에서 제공하는mutex.lock()
때문에 일어나는 문제인 것 같다. 단순히instance == null
까지 블로킹하는 것이 아니라 블록에서 일어나는 일이 모두 끝날 때 (초기화 작업이 모두 끝날 때) 까지 블록을 블로킹할 필요가 있다.
문제 해결 4: static inner class 사용하기
public class SingletonClass {
private SingletonClass() {
// 생성자를 private 으로 만들면, 외부에서 생성자 호출이 불가능하다.
}
private static class SingletonClassHolder {
private static final SingletonClass INSTANCE = new SingletonClass();
}
public static SingletonClass getInstance() {
return SingletonClassHolder.INSTANCE;
}
}
- 이 방식을 통해 간단히
getinstance()
를 실제로 호출했을 때만 생성되는 싱글톤 클래스를 만들 수 있다. static inner class
는 구조상 애플리케이션 시작 시에 클래스 로더에서 초기화되지 않고,getInstance()
가 호출되었을 때 JVM 메모리에 로드되고 객체를 생성한다.
싱글톤을 깨는 방법
협업하는 개발자들이 대부분은 싱글톤 클래스가 만들어진 의도대로 사용할테지만, 때때로 흑마법을 사용하는 개발자들도 있다.
방법 1: Reflection
이용하기
public class App {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
SingletonClass singletonClass = SingletonClass.getInstance();
Constructor<SingletonClass> constructor = SingletonClass.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonClass singletonClass1 = constructor.newInstance();
System.out.println(singletonClass == singletonClass1); // false
}
}
방법 2: 직렬화와 역직렬화 사용하기
- 직렬화와 역직렬화는 보통 자바 클래스를 파일로 떨궜다가 다시 읽어들이는 기능을 제공한다.
- 다른 언어와 정보를 주고받을 때도 사용할 수 있다. JSON 도 하나의 직렬화 역직렬화 기법이다.
Serializable
을 상속하면 된다.
public class App {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
SingletonClass singletonClass = SingletonClass.getInstance();
SingletonClass singletonClass1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
out.writeObject(singletonClass);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
singletonClass1 = (SingletonClass) in.readObject();
}
System.out.println(singletonClass == singletonClass1); // false
}
}
그런데 이러한 역직렬화는 readResolve()
를 직접 구현해주는 것으로 막을 수 있다.
public class SingletonClass implements Serializable {
// 이전과 동일
@Serial
protected Object readResolve() {
return getInstance();
}
}
- 역직렬화 과정에서는 무조건
readResolve()
를 호출하게 되는데, 그 반환값이getInstance()
여서 싱글톤을 유지할 수 있다.
절대 깰 수 없는 싱글톤을 만드는 방법
public enum SingletonEnum {
INSTANCE;
// 기본 private 생성자가 존재하여 따로 오버라이드하지 않아도 된다.
}
enum
을 이용한 방법은 가장 짧지만 가장 강력하다.
enum
싱글톤의 장점
Reflection
으로 생성자를 찾아서 억지로 생성할 수 없다.- 코드에는 보이지 않지만,
enum
은 내부적으로Enum
이라는 클래스를 상속하여, 직렬화 역직렬화가 가능하지만 안전장치를 해놓았다.- 바이트코드를 보면 보인다.
public final enum singleton/SingletonEnum extends java/lang/Enum {
// ...
}
- 상속하는
Enum
클래스가 비록 직렬화와 역직렬화를 제공하지만 역직렬화 시에도 자동으로 싱글톤을 보장하도록 설계되어 있다.
enum
싱글톤의 단점
enum
은 lazy load 가 되지 않는다는 점이 단점이다.Enum
외에 다른 클래스를 상속하지 못한다.
현실세계 싱글톤의 예
자바의 Runtime
인스턴스
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class {@code Runtime} are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the {@code Runtime} object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
}
- 자바 애플리케이션 시작 시에 초기화하도록 구현되어 있다.
public class App {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
System.out.println(runtime.maxMemory());
System.out.println(runtime.freeMemory());
}
}
- 간단하게 메모리 등을 조회해볼 수 있다.
스프링 빈의 스코프 중 싱글톤 스코프
사실상 스프링 빈에는 singleton
, prototype
, request
, session
, application
, websocket
등의 스코프가 있는데, 가장 많이 선택되는 스코프는 singleton
이다.
@Configuration
public class SpringConfig {
@Bean
public String singleton() {
return "singleton";
}
}
@SpringBootTest
class DesignPatternsWithJavaApplicationTests {
@Test
void contextLoads() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
String hello = applicationContext.getBean("singleton", String.class);
String hello1 = applicationContext.getBean("singleton", String.class);
Assertions.assertThat(hello).isEqualTo(hello1);
}
}
테스트를 잘 통과한다.
레퍼런스
'Java > 자바 디자인 패턴' 카테고리의 다른 글
추상 팩토리 패턴 (Abstract Factory Pattern) 이란? (0) | 2023.01.24 |
---|---|
팩토리 메서드 패턴 (Factory Method Pattern) 이란? (0) | 2023.01.23 |
자바 믹스인(mixins)이란? (0) | 2021.12.30 |
제네릭 싱글턴 팩토리 (0) | 2021.12.24 |
플라이 웨이트 패턴 (0) | 2021.12.22 |