singleton VS static

어차피 하나만 생성되는 객체라면 ``java static`` 메서드만 가진 클래스로 만들어도 똑같은거 아닌가 싶을 수도 있겠지만, 다음과 같은 장점 이 있다.

  1. OOP 패러다임 : 싱글턴은 OOP 패러다임을 따르는 객체이지만, static은 객체가 아니므로 OOP 패러다임과는 거리가 멀다.
    • 상속 : 싱글턴은 인터페이스를 구현하거나, 클래스를 상속받거나, 상속해줄 수 있음. (반면 static은...)
    • 인스턴스화 : 싱글턴은 static class와 달리 인스턴스화가 가능하다. (static은 인스턴스화가 의미가 없다) 인스턴스화가 가능하다는 것은 필드, 매개변수로 전달, 리턴 가능하다는 것이다.
    • 상속 & 인스턴스화 가능하다는 것은? == 다형성을 사용할 수 있다.
    • 다형성이 가능하다는 것은? == singleton을 DI 할 수 있다. (DI하는 의미가 있다.)
    • 의존성 주입(DI, Dependency Injection)이란?
  2. Lazy initialization
    1. 사실 static 변수도 해당 클래스에 최초 접근이 일어날 때 초기화 되므로, 필요한 순간 까지 초기화를 뒤로 미루게 된다는 점에서 큰 차이는 없다.
    2. 그러나 static 변수는 해당 변수를 감싸고 있는 클래스의 다른 부분에 접근이 일어날 때에 무조건 같이 초기화되는 반면(== 간접접근해도 초기화됨)
    3. 싱글턴은 구현하기에 따라 해당 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

 

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이다.