본문 바로가기

Java/Java

(Effective Java) 아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱글턴이란?

인스턴스를 오직 하나만 생성할 수 있는 클래스를 뜻함.

 

장점

  • 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 공유하기 때문에 메모리 낭비를 방지할 수 있다.
  • 두 번째 사용부터는 객체 로딩 시간이 줄어들어 성능이 좋아진다.

단점 

  • 싱글턴 인스턴스가 너무 많은 일을 하면 인스턴스의 간의 결합도가 높아진다. (OCP의 원칙에 위배 )
  • 디버깅이 어려움이 있다.
  • 테스트 코드의 작성의 어려움이 있다.

생성 방법 1. public static final 방식의 싱글턴

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {

    }

    public void leaveTheBuilding() {

    }

}

 

Elvis elvis = Elvis.INSTANCE;

 

문제점 

리플렉션을 이용하여 private 한 생성자에 접근이 가능하게 할 수 있다.

        Class class1 = Class.forName("me.choi.chaper03.version01.Elvis");

        Constructor constructor = class1.getDeclaredConstructor();
        System.out.println(constructor);
        constructor.setAccessible(true);
        Elvis elvis = (Elvis) constructor.newInstance();

 

생성 방법 2. 정적 팩토리 방식의 싱글턴 

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

    public static Elvis2 getInstance() {
        return INSTANCE;
    }

    private Elvis2 () {

    }
}

Elvis2.getInstance()는 항상 같은 객체의 참조를 반환하므로 제2의 인스턴스는 만들어지지 않는다.

 

하지만 이 또한 리플렉션에 대한 방어는 할 수 없다.

 

하지만 첫 번째 방식 보다는 나은 세 가지 장점이 존재한다.

  1. API를 바꾸지 않고 싱글턴이 아니게 변경할 수 있다. - 호출하는 스레드 별로 다른 인스턴스를 넘겨줄 수 있게 가공 가능
  2. 해당 메서드를 제네릭으로 사용할 수있다.
  3. 참조 공급자로 사용 가능하다.  (Elvis2::getInstance)

직렬화 가능 클래스로 만들 때의 문제점

public class SingletonSerialize {

    public static void main(String[] args) {
        Elvis3 elvis3 = Elvis3.getInstance();
        serializing(elvis3);

        Elvis3 elvis31 = deserializing();

        System.out.printf("Foo1(%s): (%s, %s)\n", elvis3, elvis3.getName(), elvis3.getAge());
        System.out.printf("Foo2(%s): (%s, %s)\n", elvis31, elvis31.getName(), elvis31.getAge());
        System.out.println(elvis3 == elvis31);
        System.out.println(elvis3.equals(elvis31));

    }

    public static void serializing(Elvis3 elvis3) {
        try (FileOutputStream fos = new FileOutputStream("test");
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             ObjectOutputStream out = new ObjectOutputStream(bos)) {
            out.writeObject(elvis3);
        } catch(Exception e) {
            e.printStackTrace();
        }
    }


    public static Elvis3 deserializing() {
        try (FileInputStream fis = new FileInputStream("test");
             BufferedInputStream bis = new BufferedInputStream(fis);
             var in = new ObjectInputStream(bis)) {

            Elvis3 elvis3 = (Elvis3) in.readObject();

            return elvis3;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

다른객체가 출력된다.

역 직렬화를 했을 경우 다른 객체가 출력됨을 볼 수 있다.

 

이를 방지하기 위해서는 모든 필드를 transient로 선언하고 readResolve 메서드를 추가해야 한다.


생성 방법 3. Enum 싱글턴 패턴

public enum Elvis4 {
    INSTANCE("choi",29);

    private String name;
    private int age;

    Elvis4(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static Elvis4 getInstance() {
        return INSTANCE;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

1. 간단하다
Enum 인스턴스의 생성은 기본적으로 thread safe 하다. 따라서 스레드 관련된 코드가 없어져서 코드가 간단해진다. double checked locking처럼 스레드 관련 코드를 고민할 필요가 없다. 하지만 Enum 내의 다른 메서드에 대해서는 프로그래머가 thread safe를 책임져야 한다. 

2. Enum 싱글톤은 Serialization을 스스로 해결한다
일반적인 싱글톤에서는 직렬 화할 때, 싱글톤이 싱글톤이 아니게 되는 문제가 발생한다.
왜냐하면 직렬화 하려면 readObject()를 구현해야 하는데, readObject()는 매번 새로운 인스턴스를 리턴하기 때문이다. 이러한 문제를 해결하기 위해서 몇 가지 단계가 필요하다.

1) 싱글톤 오브젝트를 직렬화 하기 위해 implements Serializable을 추가한다.
2) 모든 필드를 transient로 선언한다
3) readResolve() 메서드를 구현한다. 

 

public class SingletonSerialize {
    public static void main(String[] args) {
        Elvis4 elvis4 = Elvis4.getInstance();
        serializing(elvis4);
        Elvis4 elvis41 = deserializing();

        System.out.printf("Foo1(%s): (%s, %s)\n", elvis4, elvis4.getName(), elvis4.getAge());
        System.out.printf("Foo2(%s): (%s, %s)\n", elvis41, elvis41.getName(), elvis41.getAge());
        System.out.println(elvis4 == elvis41);
        System.out.println(elvis4.equals(elvis41));
    }

    public static void serializing(Elvis4 elvis4) {
        try (FileOutputStream fos = new FileOutputStream("test");
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             ObjectOutputStream out = new ObjectOutputStream(bos)) {
            out.writeObject(elvis4);
        } catch(Exception e) {
            e.printStackTrace();
        }
    }


    public static Elvis4 deserializing() {
        try (FileInputStream fis = new FileInputStream("test");
             BufferedInputStream bis = new BufferedInputStream(fis);
             var in = new ObjectInputStream(bis)) {

            Elvis4 elvis4 = (Elvis4) in.readObject();

            return elvis4;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

똑같은 객체

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

하지만 만들려는 싱클턴이 Enum 이외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.

 


Code Link

https://github.com/mike6321/PURE_JAVA/tree/master/TheJava

 

mike6321/PURE_JAVA

Contribute to mike6321/PURE_JAVA development by creating an account on GitHub.

github.com