[Design Pattern] 싱글톤 패턴 (Singleton Pattern)

히힣👅·2021년 8월 20일
0

DesignPattern

목록 보기
1/1
post-thumbnail

Singleton Pattern 이란?

싱글톤 패턴은 앱이 시작될때 어떤 최초 한번만 메모리를 할당하고(static), 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴이다.

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나고, 최초 생성이후에 호출된 생성자는 최초에 생성한 객체를 반환한다.

즉, 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인패턴이다.

인스턴스가 필요할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 기존 인스턴스를 사용하게 한다.


Singleton Pattern 을 사용하는 이유

① 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할수 있다.

② 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이데이터를 공유하기 쉽다.
Ex. DB Manager 처럼 공통된 객체를 여러개 생성해서 사용해야 하는 상황에서 많이 사용된다.

③ 안드로이드 앱 같은 경우 각 Activity나 클래스별로 주요 클래스들을 일일이 전달하기 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는 것이 편하다.

즉, 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을때 사용한다.


Singleton Pattern 의 문제점

멀티쓰레드 환경에서 동기화 처리를 하지않으면 인스턴스가 두개가 생성된다던지 하는 경우가 발생할 수 있다.


Singleton Pattern 의 종류

(1) Eager Initialization (이른 초기화 방식)

public class TestMaanger {
   // private static으로 선언하기
   private static TestManager instance = new TestManager();

   // 생성자
   private TestMaanger() { }

   // 인스턴스 리턴하기
   public static TestManager getInstance() {
     return instance;
   }
}

싱글톤 패턴의 가장 기본적인 유형인 Eager Initialization 방식이다.

먼저 클래스 내에 전역변수로 instance 변수를 생성하고 private static을 사용하여 인스턴스화에 상관없이 접근이 가능하면서, 동시에 private 접근 제어 키워드를 사용해 TestManager.instance 에 바로 접근할 수 없도록 한다.

또 생성자에도 private 접근 제어 키워드를 붙여 다른 클래스에서 new TestManager(); 방식으로 새로운 인스턴스를 생성하는 것을 방지한다.

오로지 정적메서드인 getInstance()메서드를 이용해서 인스턴스를 접근하도록 하여, 동일 인스턴스를 사용하는 기본 싱글톤 원칙을 지키게 한다.

Eager Initialization 방식은 싱글톤 객체를 미리 생성해놓는 방식이다.

장점은 static으로 생성된 변수에 싱글톤 객체를 선언했기 때문에, 클래스 로더에 의해 클래스가 로딩될 때 싱글톤 객체가 생성된다.

또 클래스 로더에 의해 클래스가 최초 로딩될 때 객체가 생성됨으로 Tread-safe 하다.

단점은 싱글톤 객체 사용유무와 관계없이 클래스가 로딩되는 시점에 항상 싱글톤 객체가 생성되고, 메모리를 잡고 있기 때문에 비효율적일 수 있다.


(2) Lazy Initialization (늦은 초기화 방식)

public class TestManager {
   // private static으로 선언하기
   private static TestManager instance;

   // 생성자
   private TestManager() { }

   // 인스턴스 리턴하기
   public static TestManager getInstance() {
      if(instance == null) {
         instance = new TestManager();
      }
      return instance;
   }
}

Eager Initialization과 정반대로 클래스가 로딩되는 시점이 아닌, 클래스의 인스턴스가 사용되는 시점에서 싱글톤 인스턴스를 생성한다.

즉, 사용 시점까지 싱글톤 객체 생성을 미루기 때문에 사용전까지 메모리를 점유하지 않는다.

장점은 싱글톤 객체가 필요할 때 인스턴스를 얻을 수 있어 메모리 누수를 방지할 수 있다.
(Eager Initialization 방식의 단점 보완)

단점은 만약 멀티 쓰레드 환경에서 여러곳에서 동시에 getInstance()를 호출할 경우, 인스턴스가 두번 생성될 여지가 있다. 즉, 멀티 쓰레드 환경에서는 싱글톤 철학이 깨질 수 있다.


(3) Thread safe Lazy Initialization (스레드 안전한 늦은 초기화 방식)

public class TestManager {
   // private static으로 선언하기
   private static TestManager instance;

   // 생성자
   private TestManager() { }

   // 인스턴스 리턴하기
   public static synchronized TestManager getInstance() {
      if (instance == null) {
          instance = new TestManager(0;
      }
      return instance;
   }
}

Lazy Initialization 방식에서 Thread-Safe 하지 않다는 단점을 보완하기 위해, 멀티 쓰레드 환경에서 쓰레드들이 동시 접근하는 동시성을 synchronized 키워드를 이용해 해결한다.

장점은 Lazy Initialization 방식에서 Thread-Safe 하지 않은 점을 보완한다.

단점은 synchronized 키워드를 사용할 경우, 자바 내부적으로 해당 영역이나 메서드를 lock, unlock 처리하기 때문에 내부적으로 많은 비용이 발생한다.

따라서 많은 쓰레드들이 getInstance()를 호출하게 되면 프로그램 전반적인 성능저하가 발생한다.

(4) Thread safe Lzy Initialization + Double-Checked locking 방식

public class TestManager {
   // private static으로 선언하기
   private static TestManager instance;

   // 생성자
   private TestManager() { }

   // 인스턴스 리턴하기
   public static TestManager getInstance() {
      if (instance == null) {
          synchronized (TestManager.class) {
              if (instance == null) {
                  instance = new TestManager();
              }
          }
      }
      return instance;
   }
}

Thread safe Lzy Initialization 방식은 많은 쓰레드들이 동시에 synchronized 처리된 메소드에 접근하면 성능저하가 발생한다.

이를 완화하기 위해 Double-Checked locking 기법을 사용한다.

첫번째 if문에서 instance가 null인 경우 synchronized 블럭에 접근하고 한번 더 if문으로 instance가 null인지를 체크한다.

두번 모두 다 instance가 null인 경우에만 new를 통해 인스턴스화 시킨다.

그 후에는 instance가 null이 아니기 때문에 synchronized 블럭을 타지 않을 것이다.

이런 Double-Checked locking 기법을 통해 성능저하를 보완할 수 있다.

(5) Initialization on demand holder idiom (holder에 의한 초기화)

public class TestManager {
   // 생성자
   private TestManager() { }
   
   // 인스턴스를 리턴할 Holder 클래스 생성하기
   private static class TestManagerHolder {
      private static final TestManager instance = new TestManager();
   }

   public static TestManager getInstance() {
      return TestManagerHolder.instance;
   }
}

이 방법은 클래스안에 클래스(Holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법이다.

Lazy Initialization 방식을 가져가면서 Thread간 동기화 문제를 동시에 해결할 수 있다.

중첩클래스 Holder는 getInstance()메서드가 호출되기 전에는 참조되지 않으며, 최초로 getInstance()메서드가 호출될 때, 클래스 로더에 의해 싱글톤 객체를 생성하여 리턴한다.

우리가 알아야 할 것은 Holder 안에 선언된 instance가 static이기 때문에, 클래스 로딩 시점에 한번만 호출된다는 점을 이용한 것이다. 또 final을 사용해서 다시 값이 할당되지 않도록 한다.

현재까지 가장 많이 사용되는 방법이다.




profile
⭐️조금씩이라도 꾸준히⭐️

0개의 댓글