싱글톤 패턴
싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하게됩니다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool) 과 같은 상황에서 많이 사용합니다.
사용하는 주된 이유는 리소스를 많이 차지하는 무거운 클래스를 한 개의 객체로 관리하면서 메모리 절약을 할 수 있기 때문입니다.
싱글톤 패턴 구현
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
Singleton instance3 = Singleton.getInstance();
System.out.println(instance1); // Singleton@27d6c5e0
System.out.println(instance2); // Singleton@27d6c5e0
System.out.println(instance3); // Singleton@27d6c5e0
System.out.println(instance1 == instance2 && instance2 == instance3); // true
}
싱글톤 패턴 구현은 어렵지 않습니다.
생성자의 접근제한자를 private로 선언해서 new 생성자를 통한 객체생성을 제한하고, static 을 붙혀주면됩니다.
세번의 getInstance()로 불러와도 같은 주소를 참조하고있다는걸 확인할 수 있습니다.
다만 이 방식은 멀티쓰레드 환경에서 안전하지 않습니다. if 문에 다수의 사람이 동시에 들어간다면 객체라 여러번 생성될것입니다.
이러한 싱글톤 패턴의 종류는 여러가지가 있습니다. 검증된 싱글톤 패턴을 확인하시려면 6번과 7번으로 넘어가주세요.
1. Eager Initialization
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
미리 만들어주는 방법입니다. static 영역에 재할당이 불가능한 final 이기때문에 멀티쓰레드 환경에서 안전합니다. 다만 예외처리를 할 수 없고, 리소스가 크다면 static 영억에서 자원을 사용하지 않더라도 메모리에 적재하기 때문에 자원낭비가 될 수 있습니다.
2. Static block Initialization
public class Singleton {
private static Singleton instance;
private Singleton() { }
static {
try {
instance = new Singleton();
} catch (Exception e) {
e.printStackTrace();
}
}
public static Singleton getInstance() {
return instance;
}
}
static block ( 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행된느 블록)
static block을 통해 예외를 처리할 수 있습니다.
3. Lazy Initialization
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
처음 싱글톤 패턴을 구현했던 방식입니다.
4. Thread Safe Initialization
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized 키워드를 사용하여 쓰레드의 접근을 한개로 제한시켜 동기화시킬 수 있습니다. 하지만 매번 사용하는것은 성능하락이 될 수 있습니다.
5. Double Checked Locking
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 키워드를 사용해서 캐시가 아닌 메인메모리에서 읽도록 지정해줍니다.
synchronized를 클래스에 동기화를 걸어 최초 초기화만 동기화 작업을 진행하여 리소스 낭비를 최소화 할 수 있습니다.
하지만 volatile 은 JVM 1.5이상이 되어야하고 JVM에 따라서 쓰레드 세이프하지 않는 문제가 발생할 수 있기때문에 사용하지 않는다고 합니다.
6. Bill Pugh Solution (LazyHolder)
public class Singleton {
private Singleton() { }
private static class SingleInstanceHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
클래스 내부에 static 클래스를 만들어 사용합니다. 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됩니다. 이 방식은 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법이라고 합니다.
하지만 Reflection API, 직렬화 / 역직렬화를 통해 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점이 존재합니다.
Reflection API 우회
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton); // Singleton@4f3f5b24
try {
Class<?> singletonClass = Class.forName("blog.design.싱글톤패턴.Singleton");
Constructor<?> constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = (Singleton) constructor.newInstance();
System.out.println(newSingleton); // Singleton@15aeb7ab
System.out.println(singleton == newSingleton); // false
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private 생성자를 우회하고 private 필드에 접근할 수 있습니다.
직렬화 / 역직렬화 우회
public class Singleton implements Serializable { ... }
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
System.out.println(instance); // Singleton@4f3f5b24
// 직렬화
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("blog.design.싱글톤패턴.Singleton"))) {
out.writeObject(instance);
} catch (IOException e) {
e.printStackTrace();
}
// 역직렬화
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("blog.design.싱글톤패턴.Singleton"))) {
Singleton newInstance = (Singleton) in.readObject();
System.out.println(newInstance); // Singleton@4c98385c
System.out.println(instance == newInstance); // false
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
직렬화 / 역직렬화를 사용하는 경우 readResolve 메서드를 사용하여 싱글톤을 유지할 수 있습니다.
public class Singleton implements Serializable {
private Singleton() { }
private static class SingleInstanceHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
private Object readResolve() {
return SingleInstanceHolder.INSTANCE;
}
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
System.out.println(instance); // Singleton@4f3f5b24
// 직렬화
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("blog.design.싱글톤패턴.Singleton"))) {
out.writeObject(instance);
} catch (IOException e) {
e.printStackTrace();
}
// 역직렬화
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("blog.design.싱글톤패턴.Singleton"))) {
Singleton newInstance = (Singleton) in.readObject();
System.out.println(newInstance); // Singleton@4f3f5b24
System.out.println(instance == newInstance); // true
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
readResolve 메서드는 Serializable 인터페이스를 구현한 클래스에서 제공할 수 있는 메서드로 역직렬화시에 호출됩니다.
이 메서드를 사용하여 역직렬화된 객체 대신에 기존의 인스턴스를 반환하도록 지정하면 싱글톤 패턴을 사용할 수 있습니다.
7. Enum
enum SingletonEnum {
INSTANCE;
private final Client client;
SingletonEnum() {
dbClient = Database.getClient();
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public Client getClient() {
return client;
}
}
public class Main {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.getInstance();
singleton.getClient();
}
}
enum은 thread safe하고 상수뿐만 아니라 변수나 메서드를 싱글톤클래스처럼 사용이 가능합니다.
위의 Bill Pugh Solution과 달리 Reflection API 우회에도 안전합니다. 또한 위에 직렬화 / 역직렬화에서 readResolve() 메서드를 구현한것과 달리 enum은 그 과전이 불필요하고 JVM에서 Serialization을 보장해준다고합니다.
싱글톤의 문제
- 싱글톤의 전역상태로 인해 코드의 복잡성이 증가할 수 있고 디버깅이 어려워진다.
- 테스트 시 mock 객체나 다른 객체로 대체하기 어려워 테스트 작성을 어렵게한다.
- 싱글톤의 의존성때문에 클래스간의 결합도를 높힐 수 잇고 유연성을 감소시킬 수 있다.
- SOLID 원칙에 위배될 소지가 있다.
'디자인패턴 > 생성' 카테고리의 다른 글
자바(JAVA) - 프로토타입 패턴(Prototype Pattern) (0) | 2024.02.01 |
---|---|
자바(JAVA) - 팩토리 메소드 패턴(Factory Method Pattern) (0) | 2024.01.30 |
자바(JAVA) - 빌더 패턴(Builder Pattern) (0) | 2024.01.29 |
자바(JAVA) - 추상 팩토리 패턴(Abstract Factory Pattern) (0) | 2024.01.22 |