6. 싱글톤 패턴

싱글톤 패턴

인스턴스가 하나 뿐인 객체를 만드는 패턴!
어디서든 그 인스턴스에 접근할 수 있게하는 패턴!

왜 쓰는가?

굳이 여러개가 필요 없는 클래스의 객체가 여러개 생기면 메모리를 불필요하게 차지하게 된다.
이런 클래스들의 객체를 하나로 유지하도록 설계해서 메모리를 효율적으로 사용하게 한다.

혹은 설정이나 보안같이 중요한 내용을 담는 클래스는 여러개의 객체를 만들면 안정성을 해친다.

  • 전역변수 쓰면 되잖아요
    • 전역변수는 시작하면서 생성(JVM마다 다름). 필요하지 않을 때 생성될 수 있음.

고전적인 싱글톤 패턴

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton uniqueObject;
private Singleton(){}
public static Singleton getInstance(){
if(uniqueObject == null){
uniqueObject =new Singleton();
}
return uniqueObject;
}
}
  1. 유일한 객체는 private static으로 정적으로 만들자.
  2. 객체에 대한 접근은 public static한 메소드로 하자.
    • 메소드가 현 객체의 유무를 파악해서 객체를 만들어 반환.

멀티 스레드 문제

만약 여러개의 스레드가 getInstance메소드에 접근하면 어떻게 될까?

(1번 스레드와 2번 스레드가 getInstace를 호출한 상황…)
1번 getInstance -> 2번 getInstance ->
1번 객체 유무 판별(없다고 판단) -> 2번 객체 유무 판별(없다고 판단) ->
1번 객체 생성 -> 2번 객체 생성(문제 발생!!!!!!) ->
1번 객체 반환(1번 객체 반환) -> 2번 객체 반환(2번 객체 반환)

즉 두 스레드가 반환한 객체는 서로 다른 객체다!!!!

스레드 문제 해결하기

  1. getInstance를 synchronized하기.

    • getInstance 호출 시 끝날 때까지 다른 스레드 정지.
    • 하지만 매번 객체에 접근할 때마다 동기화하는건 속도 문제 발생 가능
    • getInstance의 속도가 중요하지 않은 경우 사용.
  2. 인스턴스를 필요할 때 만들지 말고 처음부터 만들기

    1
    2
    3
    4
    5
    6
    public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton();
    public static Singleton getInstance(){return uniqueInstance;}
    }

    이 방법은 JVM에서 클래스가 로딩될 때 인스턴스를 생성!

  3. 게으른 홀더 방식
    내부 클래스를 따로 선언해서 그 안에 객체를 저장하는 방식이다.
    클래스 로더가 이 클래스를 읽을 때 객체 생성된다.

    1
    2
    3
    4
    5
    6
    7
    public class Singleton2 {
    private Singleton2(){}
    public static class LazyHolder{
    private static final Singleton2 uniqueInstance = new Singleton2();
    }
    public static Singleton2 getInstance(){return LazyHolder.uniqueInstance;}
    }

    synchronized를 사용하지 않아 성능도 좋고, 다른 키워드를 사용하지 않아도 되서 직관적이다.

주의 할 점

만약 여러개의 클래스 로더를 사용할 경우 여러개의 객체가 생성될 수 있다!
구체 클래스에 의존하여 인스턴스를 찾게 되므로, DIP, OCP를 위반할 가능성이 있다.

싱글톤의 비용

  • 소멸이 정의되어 있지 않음 : 싱글톤을 없애거나 중지하는 좋은 방법이 없다. 인스턴스를 저장하는 레퍼런스 변수를 Null로 바꾸는 메서드를 추가해도 다른 모듈의 그 싱글톤 인스턴스에 대한 참조값을 계속 유지할 수 있음.
  • 상속 불가 : 싱글톤에서 파생된 클래스는 싱글톤이 아니다.
  • 효율성 : 인스턴스 호출 때마다 if 문 실행한다. 대부분의 경우는 이 if문이 필요없긴 하다.
  • 비투명성 : 싱글톤 사용자는 getInstance 메서드를 사용해야 한다. 이는 사용자가 해당 객체가 싱글톤임을 알아야 한다는 의미다.

모노스테이트 패턴

모노스테이트 패턴은 하나의 객체만 만들도록 유도하는 또 다른 패턴이다. 여러 인스턴스가 하나인 것처럼 작동한다.

1
2
3
4
5
6
7
8
9
@Test
void testInstancesBehaveAsOne() {
final MonoState m1 = new MonoState();
final MonoState m2 = new MonoState();
for (int x = 0; x < 10; x++) {
m1.setX(x);
assertThat(x).isEqualTo(m2.getX());
}
}

왜냐면 2개의 객체가 같은 변수를 공유하기 때문이다. 모든 변수를 정적으로 만들어서 쉽게 구현할 수 있다. 그리고 어떤 메서드도 정적이어서는 안된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MonoState {
private static int itsX = 0;

public MonoState() {
}

public void setX(final int x) {
itsX = x;
}

public int getX() {
return itsX;
}
}

모노스테이트와 싱글톤의 차이
싱글톤은 단일 객체 구조를 강요한다. 싱글톤은 둘 이상의 인스턴스를 생성하지 못하게 한다.
모노스테이트는 단일 객체 행동을 강요한다. 모노스테이트는 여러 인스턴스를 만들 수 있되, 그 인스턴스들이 단일 객체처럼 행동을 한다.

모노스테이트의 이점

  • 투명성 : 모노스테이트 사용자는 일반 객체처럼 사용해도 상관없다.
  • 파생 가능성 : 모노스테이트의 파생 클래스는 모노스테이트다.
  • 다형성 : 모노스테이트의 메서드가 정적이지 않기 때문에 파생 클래스에서 오버라이딩해서 서로 다르게 구현할 수 있다.
  • 잘 정의된 생성과 소멸 : 정적인 모노스테이트의 변수는 생성과 소멸 시기가 잘 정의되어있다.

모노스테이트의 비용

  • 변환 불가 : 보통 클래스를 모노스테이트로 변환하기 힘들다.
  • 효율성 : 하나의 모노스테이트는 실체 객체이기 때문에 많은 생성과 소멸을 겪을 수 있다.
  • 실재함 : 모노스테이트 변수는 모노스테이트가 사용되지 않아도 공간을 차지한다. (정적)
  • 플랫폼 한정 : 한 모노스테이트가 여러 JVM 인스턴스나 여러 개의 플랫폼에서 동작하게 만들 수 없다.
Share