[Java] Singleton Parttern

sunnyboy·2025년 3월 29일

Java

목록 보기
4/4
post-thumbnail

What is Singleton Pattern??

클래스가 오직 하나의 인스턴스만을 갖도록 보장하고,

해당 인스턴스에 대한 전역적 접근을 가능하게 하는 디자인 패턴

즉, 클래스가 한 번만 인스턴스화 될 수 있도록 하고

이후에 인스턴스 요청 시 언제나 같은 객체(메모리 주소가 같은)를 반환

💡 Java에서 싱글톤 패턴 구현?
\quad
클래스 생성자 메서드에 private 키워드 붙이기 !!

\quad

어디에 사용할까? - 장점

  • 리소스를 많이 차지하는 객체인 경우 > 생성 비용 절감
  • 라이선스가 제한된 라이브러리인 경우 > 인스턴스 당 비용을 지불해야 하는 구조
  • 로그, 캐시 등 애플리케이션 전체에서 공유해야 하는 경우 > 중앙화

등등 중복/일관성 문제를 방지하는 등의 장점이 있음

\quad

그냥 일반 클래스에서 인스턴스를 한 번만 만들면 안되나?
\quad
그렇게 생각할 수 있지만, 혹시 모를 실수나 변수를 위해 단 한 번의 생성만 가능하도록
구조적인 제한을 두는 것이 싱글톤 패턴이다.

\quad

기본 구현 예시 - Lazy Initialization

아래의 예시는 싱글톤 패턴을 구현하는 방법 중 하나인 Lazy initialization이다.

외부에서 getInstance() 메서드를 호출해야 초기화를 진행하기 때문에 붙은 이름인데,

덕분에 당장 객체를 사용하지 않을 경우 메모리 공간을 절약할 수 있다.

수업시간에 주로 다뤘던 싱글톤 패턴이므로

public class Singleton {
    // 1. 단 하나의 인스턴스를 보관할 private static 필드
    private static Singleton instance;

    // 2. 외부에서 인스턴스를 생성하지 못하도록 private 생성자
    private Singleton() { }

    // 3. 단일 인스턴스를 제공하기 위한 public static 메서드
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. private static 필드 선언
  2. 생성자의 접근 제한자를 private으로 지정하여 클래스 외부에서 인스턴스 생성이 불가능.
  3. getInstance 메서드를 통해 미리 선언해둔 Singleton 인스턴스 제공

\quad

JDMC에서의 예시

마찬가지로 아래는 Lazy Initialization 예시이다.

public class JdbcTemplate {

    private String url;
    private String user;
    private String password;

    private static JdbcTemplate instance; // 1. private static 필드 선언
    
    
    // 2. 생성자를 private으로 지정
    private JdbcTemplate(String url, String user, String password) {
        this.url = url;
        this.user = user;
        this.password = password;

        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new JdbcInitializeException(e.getMessage(), e);
        }
    }
    
    // 3. init 메서드로 처음 한 번만 인스턴스 생성
    public static JdbcTemplate init(String url, String user, String password) {
        if (instance == null) {
            instance = new JdbcTemplate(url, user, password);
        }

        return instance;
    }
		
		// 4. 이후 getInstance 메서드로 생성된 인스턴스 반환
    public static JdbcTemplate getInstance(){ 
        if (instance == null) {
            throw new JdbcInitializeException("JdbcTemplate not initialized, please call init()");
        }

        return instance;
    }
    
    (.....생략.....)
}

DB의 연결 정보(url, user_id, password) 등을 담은 JdbcTemplate 인스턴스를 만들어둠

동일한 JdbcTemplate 인스턴스를 전역적으로 하나만 사용한다.

\quad

싱글톤의 단점

  • 멀티 스레드(Multi-Thread) 환경에서 안전하지 않다. 여러 스레드가 동시에 공유되고 있는 상황에서 아래 코드의 조건문이 동시에 두 번 돌 수 있기 때문에 하나의 인스턴스가 아닌 두 개 이상의 인스턴스가 생성될 위험이 있다.
      public static Singleton getInstance() {
          if (instance == null) {
              instance = new Singleton();
          }
          return instance;
      }
  • 의존성 문제
    • 싱글톤 인스턴스 변경 시 참조하는 모든 모듈을 수정해야 함
    • 단위 테스트 시에는 각 테스트가 독립적이어야만 하지만 그럴 수 없음.

\quad

그래도 멀티 스레드는 해결할 수 있다 - Thread safe initialization

멀티 스레드 문제를 해결할 방법으로 우선 수업시간에 다뤘던 동기화(Synchronized)가 있다.

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

이렇게 메서드 앞에 synchronized 키워드를 넣으면 두 개 이상의 스레드가 하나의 변수에

동시에 접근하지 못하도록 lock을 건다.

하지만 이렇게 매번 메서드에 synchronized를 넣으면 Overhead가 발생할 수 있다.

👆오버헤드란 ?
\\

  • wait, 스레드 관리 등에 들어가는 비용
  • 대기시간도 들고, 접근 권한 부여 및 회수 등 연산이 들어감

그래서 등장한 Double-Checked Locking 이다.

\quad

Double-Checked Locking

인스턴스 필드 선언 시에 volatile 키워드를 붙여주면 된다.

다만 명령어에 대한 이해가 필요하고 JVM 버전에 따라 지원하지 않는 경우가 있다고 한다.

public class DoubleCheckSingleton {
    private static volatile DoubleCheckSingleton instance;

    private DoubleCheckSingleton() {}

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

\quad

LazyHolder

JVM 에서는 내부 클래스라도 static으로 선언되면, 바깥 클래스와 별개의 독립적인 클래스로 취급됨.

따라서 그 내부 클래스가 처음 사용될 때 로딩이 일어난다는 점을 이용한 것.

public class LazyHolderSingleton {

    private LazyHolderSingleton() {}

    private static class SingleInstanceHolder {
        private static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
    }

    public static LazyHolderSingleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
}

즉, getInstance가 호출되어야 싱글톤 객체를 최초로 생성 및 리턴하게 됨.

그 이후에는 이미 로딩이 끝났으므로 두 번째 호출부터는 생성된 인스턴스를 반환한다.

리플렉션과 같은 방법을 이용해 클라이언트가 싱글톤을 깰 수 있다는 것이 단점.
\quad

👆리플렉션 이란?
\\
클래스의 메서드, 필드와 같은 구성요소를 조사하고 호출, 수정할 수 있는 매커니즘.
예) Java의 java.lang.reflect

😝 응 리플렉션으로 인스턴스 또 만들면 그만이야~
\quad

Enum Singleton

Enum 상수는 처음 단 한 번만 생성하므로, 오직 1개의 인스턴스가 만들어짐.
Java에서 100% 보장하는 JVM에 하나만 존재하는 인스턴스.

단, 다른 클래스를 상속받을 수 없음.

public enum EnumSingleton {
    INSTANCE;

    // 예시로 필드를 하나 두고
    private int value;

    // 필요한 메서드
    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void doSomething() {
        System.out.println("EnumSingleton : " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        EnumSingleton.INSTANCE.setValue(42); // 이 시점에서 생성
        EnumSingleton.INSTANCE.doSomething(); // "EnumSingleton : 42"
    }
}

\quad

결론

싱글톤 패턴은 분명한 Trade-Off 가 존재하는 패턴이므로, 적절히 상황을 고려해서 적용해야 한다.

특히 멀티 스레드 환경이라면 싱글톤 패턴이 정말 필요한지에 대해 신중히 체크하자.

profile
Data Analysis, ML, Backend 이것저것 합니다

0개의 댓글