다양한 Singleton 객체 만들기

kms·2024년 4월 25일
post-thumbnail

본 글 이외에 다양하게 싱글톤을 만드는 방법은 존재합니다. 아래의 글은 본인이 공부하면서 필요하다고 생각되는 부분만 적었습니다.

싱글톤 패턴은 클래스의 인스턴스 객체가 메모리상에 딱 하나만 존재하도록 하는 패턴이다.

만드는 방법

  1. 해당 클래스로 매번 new 를 통해 새로은 인스턴스를 못만들도록 해야한다..
  2. 반.드.시 딱 하나만 만들도록 보.장 해야한다. (thread safe)
  3. 다른 곳에서 사용 시 이미 생성된 인스턴스를 사용할 수 있도록 해야한다.

1. 프로그램 시작할 때 부터 존재하도록 하기

public class SingletonObject {

  private static final SingletonObject singletonObject = new SingletonObject();

  private SingletonObject() {

  }

  public static SingletonObject getInstance() {
    return singletonObject;
  }
}
  • 내부 클래스 변수에 new 를 통해 생성한 인스턴스를 저장한다
  • 생성자의 접근제어자는 private 으로 한다. → 외부에서 new 를 통한 인스턴스 생성 불가능 하도록 만듦
  • 해당 클래스의 인스턴스를 사용할 때는 getInstance() 를 통해 이미 생성된 인스턴스를 리턴하도록 한다.

장점

  • 구현하기 쉽다.
  • 프로그램 시작과 동시에 인스턴스를 만들기 때문에 Thread-Safe 하다.(아래에서 언급하게 되는 방법은 동기화 작업이 필요하다)

단점

  • 사용하지 않으면 메모리가 낭비
    • 클래스 변수이기 때문에 스택 메모리 영역에 저장되어 프로그램이 종료될 때 까지 메모리에 상주하게 된다.

2. 싱글톤 객체가 필요할 때 생성하기

public class SingletonObject2 {

  private static SingletonObject2 singleObject2 = null;

  private SingletonObject2() {

  }

  public static SingletonObject2 getInstance() {
    if (singleObject2 == null) {
      singleObject2 = new SingletonObject2();
    }
    return singleObject2;
  }

} 
  • 클래스 변수에 담는 것은 똑같다. 단, 필요할 때 객체를 할당할 것이기 때문에 처음에는 null 로 선언한다.
  • 그렇기 때문에 final 키워드는 제거한다. ( 변수에 final 을 쓰면 재할당이 불가능하다. )
  • singleObject2 인스턴스를 사용할 때는 getInstance() 메서드를 사용한다.
  • 이미 만들어 놓은 클래스 변수를 리턴한다.
  • 단, 이때 singleObject2 가 null 인 경우에는 new 를 통해 새로운 객체를 생성하여 클래스 변수에 할당한 후 리턴한다.

위의 구현 방식은 Thread-Safe 하지 않다.

package thread.singleton;

import java.util.ArrayList;
import java.util.List;

public class SingletonTest {

  public static void main(String[] args) {
    List<Thread> threadPool = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
      threadPool.add(new Thread(() -> {
        try {
          System.out.println(Thread.currentThread().getName() + " : " + SingletonObject2.getInstance().hashCode());
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }));
    }

    for (int i = 0; i < threadPool.size(); i++) {
      threadPool.get(i).start();
    }
  }

}

1000개의 쓰레드가 동시에 getInstance() 를 호출했을 때 같은 객체를 바라봐야하지만 결과는 그렇지 않았다.

  1. 스레드 A: getInstance() 메서드가 호출됨.
    • singleObject2 변수가 null이므로 if 문의 조건을 만족하여 객체 생성을 시도.
    • 스레드 A는 new SingletonObject2()를 통해 객체를 생성하고 singleObject2 변수에 할당.
  2. 스레드 B: getInstance() 메서드가 호출됨.
    • singleObject2 변수가 null이 아님을 확인하고 if 문의 조건을 통과하지 못하므로 객체를 생성하지 않고 기존 객체를 반환하려 함.
    • 그러나 스레드 A는 이미 객체를 생성하고 singleObject2 변수에 할당했으므로, 기존 객체를 반환하는 대신 새로운 객체를 다시 생성하고 singleObject2 변수에 할당.

공유자원에 서로 다른 스레드가 접근하려고 할 때 문제가 생기고 있음.

이를 해결하기 위해 상호 배제 사용, 그 중에서 자바에서는 synchronized 키워드를 제공함. 대표적인 monitor 기법

public static synchronized SingletonObject2 getInstance() throws InterruptedException {
    if (singleObject2 == null) {
      singleObject2 = new SingletonObject2();
    }
    return singleObject2;
  }
  • 장점 : Thread Safe 하다
  • 단점 : 인스턴스 생성 여부와 상관 없는 동기화 때문에 성능이 떨어질 수 있다.

Lazy initialization , Double Checked Locking 방법

private static volatile SingletonObject2 instance;

public static SingletonObject2 getInstance() throws InterruptedException {
    if (instance == null) {
	    synchronized(SingletonObject2.class){
			   if (instance == null) {
			   	   instance = new SingletonObject2();
			   }
	    }
    }
    return singleObject2;
  }

Double Checked Locking : 잠금을 획득하기 전에 잠금 기준("잠금 힌트")을 테스트하여 잠금 획득의 오버헤드를 줄이는 데 사용되는 소프트웨어 디자인 패턴

장점

  • null 이하의 코드를 통과한 스레드에 대해서만 잠금을 흭득하기 때문에 오버헤드가 적다.(이후에는 null 이 아니기 때문에 락을 흭득하는 코드는 통과된다)

단점

  • 구현이 복잡하다.
  • 코드 가독성이 떨어진다.

0개의 댓글