싱글톤 패턴(Singleton Pattern)
싱글톤 패턴은 특정 클래스가 단 하나만의 인스턴스를 생성하고, 그 인스턴스에 접근 할 수 있는 전역적인 접근 방법을 제공하는 디자인 패턴 입니다.
생성자를 여러번 호출 하더라도 하나의 인스턴스만 존재하도록 보장하며 어플리케이션에서 동일한 인스턴스에 접근하도록 합니다.
싱글톤 패턴을 사용하는 이유
[예시 1]
1. 1대의 정수기를 여러 사람이 공유하며 함께 사용한다.
2. 여러대의 정수기를 여러 사람이 각각 사용한다.
이 2가지의 상황에서의 일반적인 경우는 1대의 정수기를 여러 사람이 공유해서 사용하는 것이 일반적인 사용 방법이라고 할 수 있겠죠?
일반적으로 사무실에는 1대의 정수기만 존재 할 것이므로, 2번째 방법은 비효율적이며 불가능한 방법 일지도 모릅니다.
시스템 내에서 단 하나의 객체 인스턴스만 존재하여야 하며, 그 인스턴스를 여러 곳에서 필요로하는 경우가 있을 것 입니다.
[예시 2]
1. 사용자가 킥보드를 이용하여 하루 5km 이상을 이동하였다면 아이템을 지급하는 A라는 스케줄링 이벤트가 존재한다.
만약 A 이벤트가 발생 할 때 마다 인스턴스 객체를 생성하여 스케줄링 큐를 따로 생성한다면 매우 비효율적입니다.
이러한 경우에 싱글톤 패턴을 적용하여 단 하나의 인스턴스로 A 이벤트를 전역적으로 관리하며 멀티 스레드 환경에서의 동시성 문제까지 해결 한다면 효율적인 적용사례가 된다고 생각합니다.
싱글톤 패턴을 구현하는 방법
- 객체를 참조할 정적 참조 변수가 필요
- new 키워드를 다른 곳에서 사용할 수 없도록 private 생성자를 생성
- 객체를 반환 할 정적 메서드가 필요
이제부터 싱글톤 패턴을 구현하는 6가지 방법에 대해 알아보겠습니다.
1. 기본적인 싱글톤 패턴 구현
public class Singleton() {
// 정적 참조 변수
private static Singleton instance;
// private 생성자로 외부 접근 차단
private Singleton() {}
// 외부에서는 getInstance() 메서드로 instance를 반환
public static Singleton getInstance() {
// instance가 null인 경우에만 생성
if(instance == null) { // 여러개의 스레드가 동시에 진입 가능(!)
instance = new Singleton();
}
return instance;
}
}
다음은 가장 일반적인 싱글톤 패턴의 구현 방법입니다.
객체를 참조할 static 변수를 생성하고, private한 생성자를 만들어 외부 접근을 차단합니다.
외부에서는 getInstace() 메서드를 호출하여 instance를 반환받게 됩니다.
최초 호출시에 instance는 null이므로 새로운 instance가 생성될 것이고 이후의 호출에서는 이미 생성된 instance가 반환 될 것 입니다.
하지만 위의 로직에서는 멀티스레드 환경의 Thread-safe를 보장하지 않습니다.
if(instance == null) 부분에서 스레드 A와 스레드 B가 동시에 진입하여 여러개의 instance를 생성하게 될 수 있기 때문입니다.
그렇다면 Thread-safe한 싱글톤을 생성하기 위한 방법은 어떤게 있을까요?
이를 해결하기 위한 방법은 synchronized 입니다.
2. Synchronized를 사용한 Thread-safe한 싱글톤 패턴 구현
public class Singleton() {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singletone();
}
return instance;
}
}
위와 같이 synchronized를 사용하여 멀티스레드 환경에서 발생하던 문제를 해결 할 수 있습니다.
Thread-safe 안전성은 보완했지만 여기서 발생하는 또 다른 문제가 있습니다.
getInstance() 메서드를 호출 시 동기화작업(Lock)문제 때문에 성능저하가 발생합니다.
이러한 문제를 해결하기 위해 이른 초기화(Eager initializedtion)방법을 적용 할 수 있습니다.
3. 이른 초기화(Eager initializetion) 싱글톤 패턴 구현
public class Singleton() {
// 이른 초기화
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
이른 초기화 방법으로 Thread-safe한 싱글톤을 구현했고 성능문제도 보완을 했습니다.
하지만 이 방법을 미리 객체를 생성하기 때문에 불필요한 메모리를 사용하여 자원을 낭비하는 단점이 있습니다.
4. Double Check를 사용한 Thread-safe한 싱글톤 패턴 구현
public class Singleton() {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) { // 1) 처음 체크
synchronized(Singleton.class) {
if(instance == null) { // 2) synchronized 때문에 lock이 걸림
instance = new Singleton();
}
}
}
return instance;
}
}
Double Check 방법은 getInstance() 메서드를 호출 할 때 2개의 스레드가 if문에 진입하더라도, synchronized에 의해서 두번째 if문에서 Lock이 걸리기 때문에 Thread-safe한 싱글톤을 구현 할 수 있습니다.
그러나 이 방법은 volatile을 사용하기 위해 JVM 1.5 버전 이상이 요구되기에 JVM 버전에 따라 Thread-safe 하지 않을 수 있습니다.
마지막으로 알아 볼 방법은 Java에서 싱글톤 패턴을 구현하는 방법으로 권장되는 Bill Pugh Solution입니다.
5. Bill Pugh Solution을 적용한 싱글톤 패턴 구현(권장)
public class Singleton() {
private Singleton(){}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder instance;
}
}
이러한 구현 로직에서 getInstance() 메서드가 호출 되면 SingletonHolder의 instance에 접근하게 됩니다.
그 과정에서 JVM은 클래스를 초기화 하게 됩니다. 그로 인해 원자성을 보장하게 됩니다.
따라서 final로 선언한 instance는 getInstance() 호출 시 SingletonHolder 클래스의 초기화가 이루어지면서 원자성이 보장된 상태로 instance가 생성되고, final 변수로 선언하였기에 또 다른 instance 생성되는 것을 막을 수 있습니다.
다음과 같이 Thread-safe한 싱글톤 패턴을 단순하게 구현 할 수 있기에 권장되는 방법입니다.
싱글톤 패턴 구현 테스트 코드
public class SingletonTest() {
private final Map<String, JSONObject> dataMap = new ConcurrentHasMap<String, JSONObject>();
// private한 기본 생성자
private Singleton() {}
// 싱글톤 클래스홀더
public static class SingletonHolder {
private static final SingletonTest instance = new Singleton();
}
// 싱글톤을 반환하는 메서드
public static Singleton getInstance() {
return SingletonHolder.instnace;
}
// setData 메서드
public void setData(String key, JSONObject value) {
dataMap.put(key, value);
}
// getData 메서드
public JSONObject getData(String key) {
return dataMap.get(key);
}
}
====================== 호출 결과 ======================
// SingletonTest.getInstance());
Singleton TEST 1 = com.test.core.SingletonTest@34ce7dc
Singleton TEST 2 = com.test.core.SingletonTest@34ce7dc
Singleton TEST 3 = com.test.core.SingletonTest@34ce7dc
긴 글을 읽어주셔서 감사합니다.
'Java' 카테고리의 다른 글
[Java] 멀티쓰레드 Callable, Executor, ExecutorService 알아보기 (0) | 2024.11.30 |
---|---|
[Java] Thread와 Runnable의 개념 및 사용법 (1) | 2024.11.28 |
[Java] 람다식(Lambda) 개념 및 사용 예제 (0) | 2024.10.16 |
[Java] 스트림(Stream) 특징, 사용 예제 (0) | 2024.10.11 |
[Java] 자료구조 HashMap의 특징 및 핵심 원리 (0) | 2024.09.18 |