싱글톤 패턴

반응형
반응형

싱글톤 패턴은 내가 생각하기에 가장 만들기 쉬우면서
가장 위험한? 패턴이라 생각이 든다.
원래 블로그를 작성하지 않고 머릿속에 정리할 생각이 었지만 아무리 생각해도 답이 나오지 않다는 생각에 이렇게 작성하게 되었다.
일단 싱글톤 패턴이 무엇인지 부터 생각 해야 된다.

싱글톤 패턴은 싱글, 즉 객체가 하나로 나오는 패턴이다.
우리가 알기로는 일반적으로 객체는 생성할때마다 다른 객체가 나오는 것이 당연한이야기다.
다시말해 사람이라는 클래스가 존재한다면, 각자 다른 사람인것 처럼
객체도 마찬가지로 생성할때마다 다르게 나오는게 정상이다.

싱글톤 패턴은 이 정상적인 행동을 제약하는 패턴이라 생각이 든다.
위에서 언급했듯이 싱글톤 패턴을 이용하게 되면 아무리 객체를 많이 만들어도 같은 객체가 나온다는 뜻이다.
근데 과연 이러한 행동이 좋을까?

항상 객체가 하나만 나온다면 많은 장점이 온다고 생각한다.
가장 큰 이유는 메모리 낭비인데 객체를 자꾸 생성해버리면 그 많큼 메모리가 객체를 생성하는 많큼 든다는 점이다.

아무튼 코드를 대충 작성해보면 다음과 같은 코드를 볼 수 있다.

public class Template {

  public static Template instance;

  private Template() {
  }

  public static Template getInstance() {
    if(instance == null) {
      return new Template();
    }
    return instance;
  }


}



이게 왜 객체가 하나만 존재하는 이유는 객체를 생성하는 부분이 private처리가 되어있다.
이는 다른 곳에서 이 객체의 생성을 막는다는 뜻이 된다.
오로지 getInstance라는 메소드를 통해서만 객체가 생성이 된다. 
뭐 이렇게 생각할 수 도 있다.
getInsance를 사용해도 계속적으로 new Template()를 사용하면 다른 객체가 나오는게 아니냐고.
코드를 자세히 살펴보면 instance가 null인 경우에는 객체를 만들고 그렇지 않는 경우에는 생성한 객체를 사용한다는 의미다.

하지만 멀티 쓰레드 환경에서는 치명적인 문제점을 가지고 있다.

1번 환경에서 instance가 null확인후 instance를 생성했다고 해서
2번 환경에서 instance가 null이 아니라는 보장은 없다.
즉, 2번 환경에서도 instance가 null인지 체크를 해야 된다는 뜻이다.
결국 두 환경의 주소값은 서로 다를 수 도 있다는 뜻이 된다.

이것을 해결하는 방법은 여러가지가 있는데 그 중에서 가장 간단한 방법은

synchronized 

synchronized를 붙이는 행위다.
이것을 동기화라고 부르는데 코드가 동기화가 진행이되면 locking이 되어지는 형태다.
하지만 이 방법같은 경우 성능에 치명적인 단점이 존재한다고 한다.
들리는 말로는 100배가량 성능이 떨어진다고 한다.

public static synchronized Template getInstance() {
...
}

double checked locking

이 방법같은 경우는 checking을 2번해서 확실하게 instance가 null인지 확인하는 방법이다.

public class Template {

  public static volatile Template instance;

  private Template() {
  }

  public static Template getInstance() {
    if (instance == null) {
      synchronized (Template.class) {
        if (instance == null) {
          instance = new Template();
        }
      }
    }
    return instance;
  }

}

위 그림을 보면서 코드를 설명해보면
1번환경에서 instance를 체크하고 동기화가 되었는지 확인한뒤, 동기화된 instance가 null인지 확인한다.
그리고 만약 null인 경우에 instance를 생성하는 방식이다.
이제 2번 환경에서는 첫 번째 instance에서는 null이 발생할 수 도 있겠지만,
동기화가 된 상태의 instance는 null이 아니기 때문에 이미 존재하고 있는 instance를 리턴해준다.

하지만 이방법 같은 경우는 jdk1.5이상에서만 사용이 가능하다는 제약사항을 가지고 있다.

Eager Initialization

public class Template {

  public static Template instance = new Template();

  private Template() {
  }

  public static Template getInstance() {
    return instance;
  }


}

이 코드는 이른 생성 방식이라고 한다.
왜냐하면 객체를 생성할때 미리 생성해버리기 때문이다.
뭐 final staic을 붙여서 상수로 만들어 버려도 되겠지만, 그냥 귀찮아서 안했다. 중요한건 상수로 만드는게 아니라 싱글톤이니까
참고로 위 방법들을 lazy initialization이라고 한다. 

static inner class

public class Template {

  private Template() {
  }

  static class TemplateHolder {
    public static final Template INSTANCE = new Template();
  }

  public static Template getInstance() {
    return TemplateHolder.INSTANCE;
  }

}

이 방법같은 경우, 위 Eager initialization를 응용한 방법이라 할 수 있다.
다른 점은 인스턴스를 만들때 내부의 클래스를 접근해서 만들어야 된다고 한다.
근데 이게 어째서 멀티 쓰레드 환경에서 안전한지 생각해보자.

getInstance()에서 TemplateHolder를 호출을 하게 된다.
만약에, 이미 instance를 만들어졌다면, TemplateHolder의 인스턴스를 사용하면 된다.
근데 이게 왜 멀티 쓰레드 환경에서 안전한 걸까?
그건 바로 static에 존재한다. 애초에 static이라는건 메모리상에 미리 만들어둔다.
그러니까 이미 TemplateHolder는 메모리상에 올라져 있기 때문에 멀티 쓰레드 환경에서도 안전한 것이다.

근데 생각해보자. 이렇게 코딩해도 되지 않을까?

public class Template {

  public static final Template INSTANCE;

  static {
    INSTANCE = new Template();
  }

  private Template() {
  }

  public static Template getInstance() {
    return Template.INSTANCE;
  }

}

static블럭을 만들어서 메모리상에 올리는걸 생각해봤는데
생각해보니 이건 위에서 언급한 이른 초기화 방식과 유사하다는 걸 느꼈다. 왜냐하면 static블럭이 존재하는 것말고 다른 점이 없기 때문이다.
아무튼 TemplateHolder가 메모리상에 미리 올라가 있지만, 실질적으로 Template를 호출하기 위해서는 TemplateHolder의 정보를 확인해야 되기 때문에 Lazy 초기화 방식이다.

사실 위 방법들은 특정 방법들로 싱글톤을 깨뜨릴 수 있다.

싱글톤 깨뜨리기

리플렉션

가장 생각하기쉬운 방법으로 리플렉션이라는 기술을 이용하게 되면 싱글톤을 깨뜨릴 수 있다.
리플렉션에 대해 설명하는 건 너무 길어질 것 같고 리플렉션 기능에 대해 하나 말해보면 private한 메소드도 접근이 가능하다.

public class App {

  public static void main(String[] args) throws Exception {
    Template instance1 = Template.getInstance();
    Constructor<Template> constructor = Template.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Template instance2 = constructor.newInstance();
    System.out.println(instance1 == instance2);
  }
}

instance1은 일반적인 방법으로 생성하고 2번째 방법은 리플렉션을 이용해서 생성자를 호출하였다. 원래 private라 불가능하지만,
constructor.set.. 에서 private도 접근 가능하게 했다.
그리고 그 안에서 인스턴스를 만드는 방법이다.
근데 이게 왜 싱글톤을 깨뜨리냐면 싱글톤 자체가 생성자를 사용못하게 함으로써 유일한 객체를 만들기 위함인데 위 처럼 사용하게 되면
Template template = new Tempate()과 다를게 없기 때문이다.

여기서 궁금한게 생겼는데 메소드를 호출해서 한다면 일치할까? 아님 다를까?
내 가설로 따진다면 일치할것 같긴하다.
해보자.

public class App {
  public static void main(String[] args) throws Exception {
    Template instance1 = Template.getInstance();
    Template instance2 = (Template) Template.class.getMethod("getInstance").invoke(instance1);
    System.out.println(instance1 == instance2);
  }
}

내가 리플랙션을 재대로 학습하지 않아서 이게 맞는지는 잘 모르겠지만, invoke라는 걸 이용해서 메소드를 사용하는 것 같다.
아무튼 실행결과는 true가 나왔다. 
내 예상이 맞았다.
결론은 리플렉션을 사용한다고 해서 싱글톤이 깨지는 건 아니라는 소리다.

직렬화/ 역직렬화

직렬화와 역직렬화는 네트워크 환경에서 데이터를 보내기 위한 방법이다.
이것에 대해서는 나중에 정리하면 좋을 것 같다.
아무튼

public class Template implements Serializable {
...
}

이렇게 수정한뒤,

public class App {

  public static void main(String[] args) throws Exception {
    Template instance1 = Template.getInstance();
    Template instance2;
    // 직렬화
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("Template.obj"))) {
      out.writeObject(instance1);
    }

    // 역 직렬화
    try (ObjectInput in = new ObjectInputStream(new FileInputStream("Template.obj"))) {
      instance2 = (Template) in.readObject();
    }

    System.out.println(instance1 == instance2);
  }
}

이런식으로 직렬화와 역직렬화를 사용했다.
이거는 리플렉션과 달리 무조건 객체를 사용해야 하기 때문에
내가 어떻게 해도 싱글톤이 깨지는 것 같다. 물론 객체에 Serializable를 입력해야 된다는 문제가 있긴하지만,
이거 안하면 직렬화를 할 수 없다.

그러면 싱글톤의 미래는 없는 걸까?
사실 이 모든 방법을 무마시킬 방법이 존재한다.
그건 바로 enum이다.

public enum Template {
  INSTANCE
}


요게 끝이라고 한다. 놀랍다. 이걸로 싱글톤을 만들 수 있게 되었다.
근데 뭔가 찝찝한게 이걸로 가능하다는게 솔직히 조금 그렇다.
그래서 바이트 코드를 열어봤다.

  // access flags 0x2
  // signature ()V
  // declaration: void <init>()
  private <init>(Ljava/lang/String;I)V
    // parameter synthetic  $enum$name
    // parameter synthetic  $enum$ordinal
   L0
    LINENUMBER 5 L0
    ALOAD 0
    ALOAD 1
    ILOAD 2
    INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/design_pettern/object_instance/_singleton/Template; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 3

생성자가 private가 되있는 것으로 봐서 enum을 사용하면 싱글톤을 지킬 수 있다는 사실을 알게 되었다.
일단 직렬화를 enum으로 돌린 결과 true가 나왔다. 즉, 직렬화로는 enum의 싱글톤을 막을 수 없다는 뜻이 된다.
리플렉션을 돌려면 결과 에러가 나온다.
즉, 리플랙션 상태에서도 싱글톤을 깨뜨리는 것은 불가능했다.

그러면 enum은 완벽한걸까?
전혀 그렇지 않다. 왜냐하면 enum자체는 다른 클래스로 상속이 불가하기 때문이다.
또한, enum도 상속받을 수 없다. 유일하게 받을 수 있는건 interface밖에 존재하지 않는다.

만약에 상속을 받아야 되는 상황이라면 enum을 사용하기에는 선택하기 어려울 것 같다.

지금까지 싱글톤을 만드는 방법과 그것을 깨뜨리는 방법 또 그것을 해결하는 방법에 대해 학습해봤다.

 

참고 자료 : 코딩으로 학습하는 GoF의 디자인 패턴 (인프런)

반응형

'디자인패턴' 카테고리의 다른 글

전략 패턴  (0) 2021.11.19
팩토리 메소드 패턴 vs 추상 팩토리 패턴  (0) 2021.11.11
탬플릿 패턴  (0) 2020.06.05
빌더 패턴  (0) 2020.05.27
프록시 패턴  (0) 2020.05.18

댓글

Designed by JB FACTORY