본문 바로가기
자바

[Java] Enum과 싱글톤(Singleton)

by 책 읽는 개발자_테드 2021. 8. 31.
반응형

싱글톤이란?

 · 하나의 JVM 당 하나의 인스턴스만 존재하는 클래스

 · 동일한 싱글턴 인스턴스가 여러 개의 스레드에서 재사용됨

https://dzone.com/articles/java-singletons-using-enum

 

싱글톤을 만드는 기존의 방법

1. public static final field

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

2. public static factory method

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){};
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

3. lazy itialization

public class Singleton {
    private static Singleton INSTANCE = null;
    private Singleton(){};
    public static Singleton getInstance(){
        if(INSTANCE == null){
            synchronized (Singleton.class){
                if(INSTANCE == null)
                    INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

 

기존 방법의 장단점

 · 위 방법 모두 코드 블럭에서 하는 일이 없어도 private 생성자를 만들어야 한다 (기본 생성자가 생성되는 걸 피하기 위해)

 · 2, 3번 방법은 api를 변경하지 않고 클래스를 비싱글톤으로 만들수 있다는 (사소한) 장점이 있다.

public static Singleton getInstance() {
    return new Singleton ();
}

 

 · Static 필드는 클래스 로딩 시간에 초기화되므로 1, 2번 방법은 런타임에 해당 클래스를 사용하지 않아도 싱글톤 인스턴스가 생성 (자원 낭비)

 · 3번 방법은 이러한 자원 낭비를 막고, 싱글톤 객체에 처음 액세스할 때 인스턴스가 생성된다. 또한 synchronized 예약어를 통해 멀티 스레드 환경에서 객체가 두 개 이상 생성되지 않도록 막는다.

 

추가적인 문제1 - 직렬화와 역직렬화 (Serialization & Deserialization)

 · 클래스를 역직렬화할 때 새로운 인스턴스가 생성되어 싱글턴 속성을 위반한다.

public class Singleton implements Serializable {
    public static Singleton INSTANCE = new Singleton();
    private int value;

    private Singleton(){};

    public void setValue(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }
}
public class SerializeDemo {

    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.setValue(1);

        // Serialize
        try {
            FileOutputStream fileOut = new FileOutputStream("out.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(singleton);
            out.close();
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        singleton.setValue(2);

        // Deserialize
        Singleton singleton2 = null;
        try {
            FileInputStream fileIn = new FileInputStream("out.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            singleton2 = (Singleton) in.readObject();
            in.close();
            fileIn.close();
        } catch (IOException i) {
            i.printStackTrace();
        } catch (ClassNotFoundException c) {
            System.out.println("singletons.SingletonEnum class not found");
            c.printStackTrace();
        }

        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }

        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());
    }
}

main 메서드 실행 결과

 

해결방법

 · 싱글톤 클래스에 readResolve 메서드를 구현한다.

더보기

Serializable 및 Externalizable 클래스에서, readResolve 메서드는 클래스가 스트림에서 읽은 객체가 호출자에게 반환되전에 교체될 수 있도록 한다. readResolve 메서드를 구현함으로써 클래스는 역직렬화되는 인스턴스를 직접 제어할 수 있다.

 

readResolve 메서드는 ObjectInputStream이 스트림에서 객체를 읽고 이를 호출자에게 반환할 준비를 할 때 호출된다.

 

 

public class Singleton implements Serializable {
    public static Singleton INSTANCE = new Singleton();
    private int value;

    private Singleton(){};

    protected Object readResolve() {
        return INSTANCE;
    }

    public void setValue(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }
}

싱글톤 클래스에서 readResolve 메서드 구현 후 main 메서드 실행 결과

 

추가적인 문제2 - 리플렉션 (Reflectiom)

 · 리플렉션을 이용하면 런타임에 private 생성자에 접근하여 새로운 인스턴스를 생성할 수 있다.

public class ReflectionDemo {

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;

        Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);

        Singleton singleton2 = (Singleton) constructor.newInstance();

        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }

        singleton.setValue(1);
        singleton2.setValue(2);

        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());
    }
}

 

Enum을 통해 싱글톤 만들기

 · 위의 모든 문제는 enum 타입을 사용하여 싱글톤을 만들면 쉽게 해결 가능

public enum Singleton {
    INSTANCE;
}

 · enum 타입은 기본적으로 직렬화 가능하므로 Serializable 인터페이스를 구현할 필요가 없고, 리플렉션 문제도 발생하지 않는다.

 · 인스턴스가 JVM 내에 하나만 존재한다는 것이 100% 보장 되므로, Java에서 싱글톤을 만드는 가장 좋은 방법으로 권장된다.

 

사용법

public enum SingletonEnum {
    INSTANCE;
    int value;

    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}
public class EnumDemo {

    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.INSTANCE;

        System.out.println(singleton.getValue());
        singleton.setValue(2);
        System.out.println(singleton.getValue());
    }
}

실행 결과

 

주의사항!

열거형을 직렬화할 때 필드 변수는 소실된다. 즉, 위 코드에서 value 변수는 직렬화되지 않고, 소실된다.

 

출처

https://dzone.com/articles/java-singletons-using-enum

 

반응형

댓글