JAVA

[JAVA] 싱글톤 구현 방식

경딩 2024. 11. 21. 23:07

싱글톤 패턴이란?

인스턴스를 오직 한 개만 제공하는 클래스입니다.

 

시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 경우가 있다. 

인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요하다.

 

싱글톤 구현 방식 1. private 생성자 및 static 메서드 사용

public class Settings {
    private static Settings instance;


    // 클래스 밖에서 인스턴스 생성 방지
    private Settings(){}
    public static Settings getInstance(){
        if(instance == null) {
            return instance = new Settings();
        }
        return instance;
    }
    
}

 

해당 방식은 멀티 쓰레드 환경에서 안전하지 않다.

동시에 두개의 스레드가 접근해 new를 동시에 실행한다면 두 스레드가 가진 인스턴스는 달라질 것이다.

 

싱글톤 구현 방식 2. private 생성자 및 static 메서드 사용 + synchronized 

    private static Settings instance;


    // 클래스 밖에서 인스턴스 생성 방지
    private Settings(){}
    public static synchronized Settings getInstance(){
        if(instance == null) {
            return instance = new Settings();
        }
        return instance;
    }

 

synchronized 키워드를 사용하면 멀티쓰레드 환경에서 안전하다.

하지만 쓰레드가 기다리게 되는 병목현상이 나타날 우려도 있다.

자세힌 알고 싶으면 synchronized에

 

 

싱글톤 구현 방식 3. 이른 초기화(eager initalization) 사용하기

public class Settings {
    private static final Settings INSTANCE = new Settings();


    // 클래스 밖에서 인스턴스 생성 방지
    private Settings(){}
    public static Settings getInstance(){
        return INSTANCE;
    }
   
 }

 

이른 초기화는 클래스가 로딩되는 시점에 미리 만들어 놓은 객체를 리턴해주면 되기 때문에 멀티 스레드 환경에서 안전한다.

대부분의 상황에서는 일반적인 초기화가 지연 초기화보다 낫다. [effective java 참고]

만약 해당 인스턴스를 만드는 과정이 굉장히 길고 오래 걸리고 메모리를 많이 사용함에도 불구하고 해당 객체가 잘 쓰이지 않는 상황일 때 지연초기화를 쓰는 것이 좋다.

 

싱글톤 구현 방식 4. double checked locking 으로 효율적인 동기화 블록 만들기

public class Settings {
    private static volatile Settings instance;


    // 클래스 밖에서 인스턴스 생성 방지
    private Settings(){}
    public static Settings getInstance(){
        if (instance == null) {
            synchronized (Settings.class) {
                if (instance == null) {
                    instance = new Settings();
                }
            }
        }
        return instance;
    }
  }

 

객체 생성을 나중에 하고 싶지만 Synchronized block 이 비용이 신경쓰인다면 다음과 같이 구현하면 된다.

인스턴스가 이미 있는 경우 Synchronized 를 skip 하게 되어 Synchronized block을

동시에 멀티스레드가 들어오는 경우에만 실행된다.

모든 스레드가 Synchronized에

그리고 인스턴스를 필요로 하는 시점에서만 만들 수 있다.

 

싱글톤 구현 방식 5. static inner 클래스 사용하기

public class Settings {

    // 클래스 밖에서 인스턴스 생성 방지
    private Settings(){}

    
    static class SettingsHolder {
        public static final Settings INSTANCE = new Settings();
    }


    public static Settings getInstance(){
        return SettingsHolder.INSTANCE;
    }
}

 

해당 코드는 멀티스레드 환경에서 안전하고 getInstance 호출 시 SettingsHolder 가 로딩이 되며 인스턴스를 생성하기 때문에  lazy loading 이 된다는 장점이 있다.

 

더보기

static inner 클래스 사용하기 

이 방식으로 싱글턴 객체를 생성할 땐 어떻게 하나의 객체만 생성된다는걸 보장할 수 있을까?

 

클래스 참조시 클래스 로딩이 발생하며 클래스 초기화 과정이 이루어집니다.
(jvm 스펙에 따르면 ) 클래스는 로딩 및 초기화 과정이 딱 한번만 수행됩니다. 따라서 스레드 세이프하여 하나의 객체만 생성된다는 것을 보장할 수 있습니다.
또한 final 키워드가 있어 참조를 변경할 수 없기에 불변입니다.


싱글톤 패턴 구현을 깨트리는 방법

1. 리플렉션으로 싱글톤 깨뜨리기

 

package singleton;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class App {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings settings1 = Settings.getInstance();


        Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings settings = constructor.newInstance();

        System.out.println(settings1 == settings);
    }
}

리플렉션을 통해 new을 활용한 것과 같은 인스턴스를 생성해 새로운 인스턴스를 만드는 것을 확인할 수 있다.

리플렉션으로 꺼내온 객체는 싱글톤이 깨지는 것을 확인할 수 있다.

 

 

2. 직렬화 , 역직렬화 활용

자바에는 오브젝트를 파일 형태로 디스크에 저장해 놨다가 다시 읽어드리는 직렬화, 역직렬화를 지원한다.

public class Settings implements Serializable { // 직렬화를 위해 Serializable 구현

 

package singleton;

import java.io.*;

public class App {
    public static void main(String[] args) throws Exception {
        Settings settings1 = Settings.getInstance();
        Settings settings2 = null;

        // 직렬화 작업
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("setting.obj"))) {
            out.writeObject(settings1);
        }

        // 역직렬과 작업
        try (ObjectInput in = new ObjectInputStream(new FileInputStream("setting.obj"))) {
            settings2 = (Settings)in.readObject();
        }

        System.out.println(settings1);
        System.out.println(settings2);

        System.out.println(settings1 == settings2);
    }
}

 

직렬화 역직렬화를 통해서도 싱글톤을 깨뜨릴 수 있다.

 

역직렬화시 싱글톤을 보장하기 위해 역직렬화 시 쓰이는 readResolve 메서드를 다음과 같이 정의해 주면 싱글톤을 보장할 수 있다.

 

    protected Object readResolve() {
        return getInstance();
    }
package generic.sigleton;

import java.io.Serializable;

public class Settings implements Serializable {

    private static final Settings INSTANCE = new Settings();


    private static class SettingsHoler {
        private static final Settings INSTANCE = new Settings();
    }
    public static Settings getInstance(){
        return SettingsHoler.INSTANCE;
    }

    protected Object readResolve() {
        return getInstance();
    }

}

 

안전하고 단순하게 싱글톤 구현하기

 

리플렉션을 통해 싱글톤을 깨지는 것을 방지하는 방법은 없을까?

 

 

enum 타입 방식의 싱글턴 - 바람직한 방법

package singleton;

public enum Settings {

    INSTACE;
}

enum 클래스는 내부 코드적으로 리플렉션 생성을 막아놓았다.

단점은 클래스 로딩 순간 해당 인스턴스가 미리 만들어진다는 것이다. 객체 생성 비용이 비싸지 않는다면 가장 안전한 방법이다. 

enum 은 기본적으로 Serializable 를 상속받고 있고 별다른 조치를 취하지 않아도 안전하게 동일한 인스턴스로 역직렬화가 된다.

 

 

 

  • 자바에서 enum 을 사용하지 않고 싱글톤을 구현하는 방법은?
    • private 생성자를 만들고 static 메서드를 사용하여 싱글톤을 구현한다.
  • private 생성자와 static 메서드를 사용하는 방법의 단점은?
    • 지연 로딩 시 멀티 스레드에 안전하기 위해 추가 작업이 필요하다.
    • 리플렉션으로 싱글톤을 깨뜨릴 수 있다.
  • enum을 사용해 싱글톤 패턴을 구현하는 방법의 장점과 단점은?
    • 리플렉션, 역직렬화로  싱글톤을 파괴하는 객체 생성을 방지할 수 있다.
    • 객체가 미리 생성된다.
  • static inner 클래스를 사용해 싱글톤 패턴을 구현하라.

 

참고 자료

GoF 의 디자인 패턴, 이팩티브 자바