Effective Java | #3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

보람·2022년 5월 7일
1

Effective-Java

목록 보기
4/25

싱글턴(singleton)

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • ex) 함수(item-24)와 같은 무상태 객체, 유일해야 하는 시스템 컴포넌트

싱글턴 생성 방식

첫 번째, public static final 필드 방식의 싱글턴

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { .. } // `Elvis.INSTANCE` 초기화할 때 딱 한번만 호출

	public void leaveTheBuilding() {..}
}
  • 사용 : Elvis elvis = Elvis.INSTANCE;
  • public or protected 생성자가 없으므로 싱글턴임을 보장
    • 예외 리플렉션 API(item-65) - AccessibleObject.setAccessible ==> 재호출시 예외를 발생시켜 방어
  • 해당 방식의 장점
    • 싱글턴임이 명백하게 드러나면서 간결하다.

두 번째, 정적 팩터리 방식의 싱글턴

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

    public void leaveTheBuilding() { ... }
}
  • 사용 : Elvis elvis = Elvis.getInstance();
  • API를 바꾸지 않고도 싱글턴이 아니게 변경 가능
    • 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있음
  • 제네릭 싱글턴 팩터리로 만들 수 있음(item-30)
  • 정적 팩터리 메서드의 참조를 공급자(supplier)로 사용 가능(item-43, item-44)
Supplier<Elvis> elvisSupplier = Elvis::getInstance;
Elvis elvis1 = elvisSupplier.get();

System.out.println(elvis.equals(elvis1)); //true

위 장점이 필요X? -> 첫 번째 방식(public 필드 방식) 사용할 것

위 둘 중 하나의 방식으로 만들어진 싱글턴 클래스를 직렬화하여 싱글턴임을 보장하기 위해(인스턴스가 한번만 생성되기 위해)서는 readResolve()를 제공 해야한다.(item-89)

/**
 * 싱글턴임을 보장해주는 readResolve 메서드
 * @return
 */
private Object readResolve() {
	//'진짜' Elvis 를 반환하고, 가짜 Elvis 는 가비지 컬렉션에 맡긴다.
	return INSTANCE;
}

이게 무슨 의미인지 이해가 안돼서 직접 예제 작성해봤다. 궁금하다면 아래내용으로 이동🌝

세 번째, 열거 타입 방식의 싱글턴 - 바람직한 방법

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}
  • 사용 : Elvis elvis = Elvis.INSTANCE;
  • public 필드 방식과 비슷하지만 훨씬 간결하고 추가 노력 없이(readResolve() 필요 X) 직렬화 가능, 리플렉션 공격도 방어 가능
  • 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법

위까지가 책 내용이공 아래는 궁금해서 찾아본 것들😙

Serialize, readResolve

  • 싱글턴 클래스 직렬화할 때 인스턴스가 한번 더 생성되는 경우를 방지하는 것

코드 돌려보기 전에

직렬화란

자바 내부 시스템에서 사용되는 객체나 데이터를 외부에서 사용하기 위해서는 외부에 해당 객체나 데이터 형식으로 값을 전달할 수 없고 Byte 형태로 전달해야 한다. Byte 형태로 만들어주는 것을 Serialize(직렬화)라고 하며 이를 다시 사용 가능한 데이터나 객체 형식으로 만들어주는 것을 Deserialize(역직렬화)라고 한다.

실제 코드를 돌려보자!!
아래와 같이 readResolve() 추가하지 않은 Serialize를 구현하는 클래스가 있다고 하자.

  • Serializable 를 구현한 클래스이거나 그런 클래스를 상속받은 클래스를 직렬화할 수 있다. 모든 것을 직렬화 할 수 있는 것이 X
public class ElvisSerialize implements Serializable {
    private static final ElvisSerialize INSTANCE = new ElvisSerialize();
    private ElvisSerialize() { }
    public static ElvisSerialize getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {
        ..
    }
}

해당 직렬화를 테스트하는 코드가 아래와 같다.(요런 직렬화&역직렬화 예제는 인터넷에 많당)

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ElvisSerializeTest {

    public byte[] serialize(Object instance) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (bos; ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(instance);
        } catch (Exception e) {}
        return bos.toByteArray(); //외부에서 읽을 수 있는 바이트 형식 반환
    }

    public Object deserialize(byte[] serializedData) {
        ByteArrayInputStream bis = new ByteArrayInputStream(serializedData);
        try (bis; ObjectInputStream ois = new ObjectInputStream(bis)) {
            return ois.readObject(); //사용가능한 데이터 반환
        } catch (Exception e) {}
        return null;
    }

    public static void main(String[] args) {
        ElvisSerialize instance = ElvisSerialize.getInstance();
        ElvisSerializeTest serializationTester = new ElvisSerializeTest();
        byte[] serializedData = serializationTester.serialize(instance);
        ElvisSerialize result = (ElvisSerialize) serializationTester.deserialize(serializedData);
        System.out.println("instance == result : " + (instance == result)); 
        //instance == result : false
        System.out.println("instance.equals(result) : " + (instance.equals(result))); 
        //instance.equals(result) : false
    }
}

싱글턴이기 때문에 두 객체가 같다라고 나오는 것을 예상했지만 false가 떨어짐을 알 수 있다.
직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다.
이를 해결하기 위해서는 Serialize를 구현하는 ElvisSerialize 클래스에서 readResolve()를 추가하면 된다.

public class ElvisSerializeResolve implements Serializable {
    private static final ElvisSerializeResolve INSTANCE = new ElvisSerializeResolve();
    private ElvisSerializeResolve() { }
    public static ElvisSerializeResolve getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { ... }

    /**
     * 싱글턴임을 보장해주는 readResolve 메서드
     * @return
     */
    private Object readResolve() {
        //'진짜' Elvis 를 반환하고, 가짜 Elvis 는 가비지 컬렉션에 맡긴다.
        return INSTANCE;
    }
}

readResolve()를 추가후 다시 직렬화 예제 코드를 돌려본다.

public static void main(String[] args) {
        ElvisSerializeResolve instance = ElvisSerializeResolve.getInstance();
        ElvisSerializeResolveTest serializationTester = new ElvisSerializeResolveTest();
        byte[] serializedData = serializationTester.serialize(instance);
        ElvisSerializeResolve result = (ElvisSerializeResolve) serializationTester.deserialize(serializedData);
        System.out.println("instance == result : " + (instance == result)); 
        //instance == result : true
        System.out.println("instance.equals(result) : " + (instance.equals(result))); 
        //instance.equals(result) : true
    }

같은 인스턴스라는 true가 떨어지면서 싱글턴임을 보장됨을 확인했다.🥺

profile
백엔드 개발자

0개의 댓글