디자인 패턴 - Singleton
singleton VS static
어차피 하나만 생성되는 객체라면 ``java static`` 메서드만 가진 클래스로 만들어도 똑같은거 아닌가 싶을 수도 있겠지만, 다음과 같은 장점 이 있다.
- OOP 패러다임 : 싱글턴은 OOP 패러다임을 따르는 객체이지만, static은 객체가 아니므로 OOP 패러다임과는 거리가 멀다.
- 상속 : 싱글턴은 인터페이스를 구현하거나, 클래스를 상속받거나, 상속해줄 수 있음. (반면 static은...)
- 인스턴스화 : 싱글턴은 static class와 달리 인스턴스화가 가능하다. (static은 인스턴스화가 의미가 없다) 인스턴스화가 가능하다는 것은 필드, 매개변수로 전달, 리턴 가능하다는 것이다.
- 상속 & 인스턴스화 가능하다는 것은? == 다형성을 사용할 수 있다.
- 다형성이 가능하다는 것은? == singleton을 DI 할 수 있다. (DI하는 의미가 있다.)
- 의존성 주입(DI, Dependency Injection)이란?
- Lazy initialization
- 사실 static 변수도 해당 클래스에 최초 접근이 일어날 때 초기화 되므로, 필요한 순간 까지 초기화를 뒤로 미루게 된다는 점에서 큰 차이는 없다.
- 그러나 static 변수는 해당 변수를 감싸고 있는 클래스의 다른 부분에 접근이 일어날 때에 무조건 같이 초기화되는 반면(== 간접접근해도 초기화됨)
- 싱글턴은 구현하기에 따라 해당 static 싱글턴 변수에 직접 접근 할 때만 초기화 되도록 만들 수 있다 (== 직접 싱글턴 변수에 접근 할 때 on demand 초기화)
단점 도 있기 때문에 싱글턴을 사용하는 것이 항상 적합한 것은 아니다.
static도 모두 가지고 있는 단점이며 singleton 자체 단점을 얘기한다.
- 싱글턴 인스턴스는 하나만 존재하기 때문에 mock 객체로 대체할 수 없다.
- 그래서 싱글턴 인스턴스를 사용하는 부분을 테스트하기 어렵다.
- 소프트웨어 시스템의 설정이 달라질 때 객체를 대체하거나, 의존관계를 바꿀 수 없다.
=> 하지만 이런 단점은 싱글턴 + DI 프레임워크로 보완할 수 있다는 것이 중요하다. (e.g. spring)
싱글턴 패턴
가장 기본적인 형태
public class EagerSingleton {
private EagerSingleton() {
System.out.println("EagerSingleton : init");
}
private static final EagerSingleton INSTANCE = new EagerSingleton();
public static EagerSingleton getInstance() {
return INSTANCE;
}
public static String access() { return "accessed"; }
}
private static void eagerTest() {
System.out.println("main : " + EagerSingleton.access());
}
---
EagerSingleton : init
main : accessed
- static field 초기화 시점은 해당 클래스에 최초 접근이 일어나는 시점이므로, `` EagerSingleton.access()`` 만 해도 INSTANCE 초기화가 일어난다는게 단점. (on demand 초기화 불가)
- Class Loader에 의한 class load, initialization은 thread-safe하므로, thread-safe.
initialization on demand holder idiom (Bill Pugh)
public class BillPughSingleton {
private BillPughSingleton() {
System.out.println("BillPughSingleton : init");
}
private static class LazyHolder {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return LazyHolder.INSTANCE;
}
public static String access() { return "accessed"; }
}
private static void billPughTest() {
System.out.println("main : " + BillPughSingleton.access());
System.out.println("main : getInstance 해야 초기화");
System.out.println("main : " + BillPughSingleton.getInstance());
}
---
main : accessed
main : getInstance 해야 초기화
BillPughSingleton : init
main : com.company.BillPughSingleton@1b6d3586
- outer class에 최초 접근이 발생해도, inner class 초기화가 일어나는 것은 아니므로, inner class의 static field로 INSTANCE를 구성해 명시적으로 `` BillPughSingleton.getInstance()`` 할 때만 초기화가 일어나도록 보완한 방식. (on demand 초기화)
- EagerSingleton과 같은 이유로 Thread-safe 함.
- reflection(`` AccessibleObject.setAccessible``)을 이용하면 새로운 객체를 반환 받을 수는 있음.
Enum Singleton
public enum EnumSingleton {
INSTANCE;
private int field1;
public void doSomething() { ... }
static {
System.out.println("EnumSingleton : init");
}
public static String access() { return "accessed"; }
}
private static void enumTest() {
System.out.println("main : " + EnumSingleton.class);
System.out.println("main : start initialization");
System.out.println("main : " + EnumSingleton.access());
}
---
main : class com.company.EnumSingleton
main : start initialization
EnumSingleton : init
main : accessed
- Enum 상수 INSTANCE는 기본적으로 ``java public static final INSTANCE``로 가지고 있는 것과 동일하므로, `` EnumSingleton.access()``로 간접 접근 하면 초기화 된다. (on demand 초기화 불가)
- 다른 항목과 같은 이유로 Thread-Safe 함.
- reflection 막을 수 있음.
- deserialize 시점의 공격을 막을 수 있음.
- 실무에서도 많이 쓴다.
EnumSingleton이 lazy initialization이 불가하다는 얘기가 많은데, 정확히는 on demand 초기화가 불가능한 것이다.
기본적으로 JVM에서 compile-time 상수가 아닌 모든 static field는 해당 class 최초 접근 시에 비로소 초기화되므로 lazy init이다.
'System Design > Method design' 카테고리의 다른 글
External Client class에서 Exception을 던지는게 좋을까? (0) | 2020.08.21 |
---|---|
공통 비즈니스 로직 분리(제휴사 인터페이스 통합 및 클래스 설계) (0) | 2019.08.21 |
Exception 처리, 어떻게 하는게 좋을까? (0) | 2019.05.29 |
상속 vs 컴포지션 구분 : delegation, decorator, wrapper (0) | 2019.02.04 |
Design Pattern, 디자인 패턴 (0) | 2018.05.14 |