본문 바로가기
개발/JAVA

[effective java 3/E] private 생성자나 열거타입으로 싱글턴임을 보증하라

by 상용최 2020. 6. 26.
반응형

싱글톤이란 ?

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

new 연산자를 이용하여 SingTon 인스턴스를 생성한다면 1개를 만들면 1개 10개를 만들면 10개의 인스턴스가 생성될 것이다. 또한 생성할때마다 메모리에 추가될것이며

모두가 다른 인스턴스일것이다.

반면 싱글톤패턴이란 최초 실행할때 1회만 생성하며 이후부터는 만들어진 객체를 할당하여 사용하는 방식이다.

 

아래와 같은 SingTon 클래스가 있다고 가정해보자.

생성자를 private 으로 제한하고 유일하게 인스턴스에 접근할 수 있는 수단으로 public static 변수를 하나 생성한다.

public class SingleTon {
    int firstValue = 0;
    public static final SingleTon SINGLE_TON = new SingleTon();

    private SingleTon(){
        firstValue = 5;
    }

    public int getFirstValue() {
        return firstValue;
    }

    public void setFirstValue(int firstValue) {
        this.firstValue = firstValue;
    }
}

SingleTon Class를 선언하고 main 메소드에서 아래와 같이 작성해보았다.

결과는 어떻게 나올것 같은가 ?

public final class main {

    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.SINGLE_TON;
        SingleTon singleTon2 = SingleTon.SINGLE_TON;

        System.out.println(singleTon.getFirstValue());
        System.out.println(singleTon2.getFirstValue());

        singleTon.setFirstValue(6);

        System.out.println(singleTon.getFirstValue());
        System.out.println(singleTon2.getFirstValue());
        System.out.println(singleTon.equals(singleTon2));
    }
}

하나의 값을 바꾸면 두개의 객체의 값이 바뀐다.

싱글톤을 사용하면 동일한 객체를 사용하게 되기때문이다.

 

그렇다면 무조건 싱글톤으로 사용해야 하나요?

그것은 아닙니다.

예를 들어 아래와 같이 좌표를 가지는 Point Class가 있고 이 클래스는 배달 지점을 나타낸다고 가정해보자.

손님1 - (1,1) 좌표에 배달

손님2 - (2,2) 좌표에 배달

위와 같이 사용하는곳마다 어떠한 데이터가 독립적으로 사용되어야 하는경우에는 싱글톤을 사용하면 안됩니다.

같은 인스턴스를 공유하기 때문에 손님1이 (1,1)로 주문하고 그 후에 손님2가 (2,2)로 주문을하면 손님1의 좌표도 (2,2)로 변경되기 때문입니다.

 

즉, 클래스가 내부적으로 하나 이상에 자원에 의존하고 있으며, 의존하는 자원이 클래스 동작에 영향을 준다면 싱글톤을 사용하면 안됩니다.

(싱글톤 뿐만이 아니라 정적 유틸리티 클래스도 자제해야 합니다.)

public class Point {
    int x;
    int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

그렇다면 언제 사용해야 하나요? 

  • 변경되는 데이터가 없을 때 혹은 변경되어도 영향이 없는 데이터만 가졌을 때
  • 공통된 객체를 여러군데에서 사용할 때 (예를들면 DB Connection)

 

싱글톤 패턴의 장점

  • 단 1개의 인스턴스만 생성하기 때문에 메모리 소비가 적음
  • 싱글톤으로 만들어진 인스턴스는 전역인스턴스기때문에 다른 클래스에서 참조하기가 쉬움

싱글톤 패턴의 단점

  • 잘못 작성하면 멀티 스레드 환경에서 문제가 발생할 수 있다.
    ex) 멀티스레드에서 독립적으로 사용해야하는 필드가 있는 클래스를 싱글톤으로 사용하면 필드의 값이 꼬여서 의도하지 않은 값으로 변할 수 있다.

 

싱글톤이 무엇인지 아주 간단하게 알아보았다.

그렇다면 이제 싱글톤을 어떠한 방식으로 작성할 수 있는지 알아보도록 한다.

 

첫번째

위에서 보았듯이 생성자를 private로 제한하고 public static 변수를 하나 선언하고 해당 변수를 사용하는 방법이다.

생성자가 private밖에 없으며 public static 변수를 사용하면 인스턴스가 하나임이 보장된다.

하지만, 예외는 존재한다.

권한이 있는 클라이언트가 리플렉션을 사용하여 private 생성자를 호출하는 경우이다.

이러한 공격을 방어하려면 생성자를 수정하여 두번째 객체가 생성되려 할 때 예외를 발생시켜야한다.

public class SingleTon {
    public static final SingleTon SINGLE_TON = new SingleTon();

    private SingleTon(){
    }
}

 

두번째

두번째 방법은 첫번째 방법과 크게 다르지않다.

public static 변수의 접근권한을 private 으로 바꿔준 후 SingTon 객체를 반환하는 정적 팩토리 메소드를 제공해주는 방법이다.

두번째 방법의 첫번째 장점은 API를 바꾸지 않고 싱글턴이 아니게 변경 할 수 있다는 점이 있다.

두번째 장점은 정적 팩토리를 제네릭 싱글톤 팩터리로 만들 수 있다는 점이 있다.

세번째 장점은 정적 팩터리의 메소드 참조를 공급자로 사용할 수 있다는 점이다.

하지만 이러한 장점이 굳이 필요없다면 public 필드방식이 명확해서 좋을때가 많다.

하지만 두번째 방법 역시 리플렉션을 통해 객체를 생성할 수 있다.

public class SingleTon {
    private static final SingleTon SINGLE_TON = new SingleTon();
    private SingleTon(){
    }
    public static SingleTon getInstance(){
        return SINGLE_TON;
    }
}

 

첫번째, 두번째 방법 모두 직렬화를 할때 추가적인 작업이 필요하다.

모든 인스턴스 필드를 transient라고 선언하고 readResolve 메소드를 제공해주어야 한다.

이렇지 않으면 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다. (싱글톤이 깨지게된다)

public final class main {

    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        byte[] serializedMember = null;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(singleTon);
                // serializedMember -> 직렬화된 singl 객체
                serializedMember = baos.toByteArray();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
        System.out.println(Base64.getEncoder().encodeToString(serializedMember));

        try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
            try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                // 역직렬화된 Member 객체를 읽어온다.
                Object objectMember = ois.readObject();
                SingleTon decodeSingleTon = (SingleTon) objectMember;
                System.out.println(decodeSingleTon);

                System.out.println(singleTon.equals(decodeSingleTon));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

위에는 getInstance를 통해 받은 인스턴스를 직렬화 한 후 결과값을 이용하여 역직렬화 했을 때 같은 인스턴스인지 검사하는 간단한 예제이다. 

결과는 아래와 같이 false가 나온다.

해결법은 간단하다 모든 인스턴스를 transient라고 선언하고 readResolve 메소드를 제공하면된다.

SingleTon Class를 아래와 같이 바꾼 후에 main 메소드를 재실행하게 되면 true가 나올것이다.

public class SingleTon implements Serializable {
    private static final SingleTon SINGLE_TON = new SingleTon();
    private static final long serialVersionUID = 1L;
    private SingleTon(){
    }
    public static SingleTon getInstance(){
        return SINGLE_TON;
    }

    private Object readResolve() {
        return SINGLE_TON;
    }

    @Override
    public String toString() {
        return "SingleTon{}";
    }
}

 

세번째

세번째 방법은 간단하다.

원소가 하나인 Enum을 선언하는것이다.

Enum으로 만드는 장점은 직렬화를 추가작업없이 수행 가능하다는 것과 리플렉션 공격에서도 제2의 인스턴스가 생기는것을 방지해준다.

public enum  EnumSingleTon {
    ENUM_SINGLE_TON;

    public void print(){
        System.out.println("ENUM SINGLETON!!");
    }
}

 

 

세가지 방법을 알아봤다.

대부분의 상황에서는 원소가 하나뿐인 Enum을 사용하여 싱글턴을 만드는것이 가장 좋은방법이다.

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

혹시나 Enum으로 싱글톤이 어떻게 되는지 헷갈리시는 분들은 아래문서를 통해 Enum의 특성을 공부하는것도 좋을 것 같다.

https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-EnumConstantList

간단하게 설명하자면 아래와 같은 특징을 가지고있다.

  • enum은 간단한 형태의 class
  • enum은 런타임이 아닌 컴파일단계에서 모든 값을 알고 있어야한다 -> 생성자도 private
  • enum 클래스 내에서도 인스턴스 생성이 불가능하다.

 

 

 

출처 : 이펙티브자바 3판 (effective java 3/E)

반응형

댓글