singleton VS static
어차피 하나만 생성되는 객체라면 static
메서드만 가진 클래스로 만들어도 똑같은거 아닌가 싶을 수도 있겠지만, 다음과 같은 장점이 있다.
- OOP 패러다임 : 싱글턴은 OOP 패러다임을 따르는 객체이지만, static은 객체가 아니므로 OOP 패러다임과는 거리가 멀다.
- 상속 : 싱글턴은 인터페이스를 구현하거나, 클래스를 상속받거나, 상속해줄 수 있음.
- 어떤 base 기능을 9개가 사용하고 나머지 1개만 예외가 있을 때? 싱글턴은
override
처리가 가능하지만, static은 상속이 불가능해서, 확장이 어렵다.
- 인스턴스화 : 싱글턴은 static class와 달리 인스턴스화가 가능하다. (static은 인스턴스화가 의미가 없다) 인스턴스화가 가능하다는 것은 필드, 매개변수로 전달, 리턴 가능하다는 것이다.
- 상속 & 인스턴스화 가능하다는 것은? == 다형성을 사용할 수 있다.
- 다형성이 가능하다는 것은? == singleton을 DI 할 수 있다. (DI하는 의미가 있다.)
- 의존성 주입(DI, Dependency Injection)이란?
- Lazy initialization
- 사실 static 변수도 해당 클래스에 최초 접근이 일어날 때 초기화 되므로, 필요한 순간 까지 초기화를 뒤로 미루게 된다는 점에서 큰 차이는 없다.
- 그러나 static 변수는 해당 변수를 감싸고 있는 클래스의 다른 부분에 접근이 일어날 때에 무조건 같이 초기화되는 반면(== 간접접근해도 초기화됨)
- 싱글턴은 구현하기에 따라 해당 static 싱글턴 변수에 직접 접근 할 때만 초기화 되도록 만들 수 있다 (== 직접 싱글턴 변수에 접근 할 때 on demand 초기화)
단점도 있기 때문에 싱글턴을 사용하는 것이 항상 적합한 것은 아니다.
static도 모두 가지고 있는 단점이며 singleton 자체 단점을 얘기한다.
- 싱글턴 인스턴스는 하나만 존재하기 때문에 mock 객체로 대체할 수 없다.
- 그래서 싱글턴 인스턴스를 사용하는 부분을 테스트하기 어렵다.
- 소프트웨어 시스템의 설정이 달라질 때 객체를 대체하거나, 의존관계를 바꿀 수 없다.
=> 하지만 이런 단점은 (싱글턴+DI 프레임워크)로 보완할 수 있다는 것이 중요하다. (e.g. spring)
싱글턴 패턴
가장 기본적인 형태
1
2
3
4
5
6
7
8
9
10
| 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"; }
}
|
1
2
3
4
5
6
| private static void eagerTest() {
System.out.println("main : " + EagerSingleton.access());
}
---
EagerSingleton : init
main : accessed
|
initialization on demand holder idiom (Bill Pugh)
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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"; }
}
|
1
2
3
4
5
6
7
8
9
10
| 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
1
2
3
4
5
6
7
8
9
10
11
12
| public enum EnumSingleton {
INSTANCE;
private int field1;
public void doSomething() { ... }
static {
System.out.println("EnumSingleton : init");
}
public static String access() { return "accessed"; }
}
|
1
2
3
4
5
6
7
8
9
10
| 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는 기본적으로
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이다.