Post

디자인 패턴 - Singleton

singleton VS static

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

  1. OOP 패러다임 : 싱글턴은 OOP 패러다임을 따르는 객체이지만, static은 객체가 아니므로 OOP 패러다임과는 거리가 멀다.
    • 상속 : 싱글턴은 인터페이스를 구현하거나, 클래스를 상속받거나, 상속해줄 수 있음.
      • 어떤 base 기능을 9개가 사용하고 나머지 1개만 예외가 있을 때? 싱글턴은 override 처리가 가능하지만, static은 상속이 불가능해서, 확장이 어렵다.
    • 인스턴스화 : 싱글턴은 static class와 달리 인스턴스화가 가능하다. (static은 인스턴스화가 의미가 없다) 인스턴스화가 가능하다는 것은 필드, 매개변수로 전달, 리턴 가능하다는 것이다.
    • 상속 & 인스턴스화 가능하다는 것은? == 다형성을 사용할 수 있다.
    • 다형성이 가능하다는 것은? == singleton을 DI 할 수 있다. (DI하는 의미가 있다.)
    • 의존성 주입(DI, Dependency Injection)이란?
  2. 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이다.

This post is licensed under CC BY 4.0 by the author.