2023. 11. 14. 22:52ㆍBook/이펙티브 자바
객체 생성과 파괴
인스턴스를 오직 하나만 생성해야 할 경우 싱글톤으로 만드는 방법
1) private 생성자 + public static final 필드
장점 : 간결하고 싱글턴임을 API에 들어낼 수 있다.
public class Elvis implements IElvis, Serializable {
/**
* 싱글톤 오브젝트
*/
public static final Elvis INSTANCE = new Elvis();
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public void sing() {
System.out.println("sing~ sing! sing~");
}
}
단점1 : 싱글톤을 사용하는 클라이언트를 테스트하기 어렵다.
class ConcertTest {
@Test
void perform() {
Concert concert = new Concert(Elvis.INSTANCE);
concert.perform();
assertTrue(concert.isLightsOn());
assertTrue(concert.isMainStateOpen());
}
}
Elvis 클래스를 생성하는데 비용이 아주 크다고 가정했을 때 Mock 구현으로 대체 할 수 없기 때문에 테스트하기 어렵다.
==> 대안 타입을 인터페이스로 정의하고 싱글턴이 그 인스턴스를 구현하면 가능
public interface IElvis {
void leaveTheBuilding();
void sing();
}
================================================================================
public class Elvis implements IElvis {
/**
* 싱글톤 오브젝트
*/
public static final Elvis INSTANCE = new Elvis();
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public void sing() {
System.out.println("sing~ sing! sing~");
}
}
==================================================================================
/**
* 테스트
*/
==================================================================================
public class MockElvis implements IElvis{
@Override
public void leaveTheBuilding() {
}
@Override
public void sing() {
System.out.println("Mock sing~");
}
}
===================================================================================
class ConcertTest {
@Test
void perform() {
Concert concert = new Concert(new MockElvis());
concert.perform();
assertTrue(concert.isLightsOn());
assertTrue(concert.isMainStateOpen());
}
}
단점2 : 리플렉션으로 private 생성자를 호출할 수 있다. (싱글톤이 깨질 수 있다.)
public class ElvisReflection {
public static void main(String[] args) {
try {
//리플렉션을 이용해 기본 생성자 접근
Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();
//private 한 생성자에 접근 허용
defaultConstructor.setAccessible(true);
Elvis elvis1 = defaultConstructor.newInstance();
Elvis elvis2 = defaultConstructor.newInstance();
System.out.println(elvis1 == elvis2);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
==> false
권한이 있는 클라이언트는 리플렉션API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
==> 대안
public class Elvis implements IElvis {
/**
* 싱글톤 오브젝트
*/
public static final Elvis INSTANCE = new Elvis();
private static boolean created;
private Elvis() {
if(created) {
throw new UnsupportedOperationException("Can't be created by constructor.");
}
created = true;
}
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public void sing() {
System.out.println("sing~ sing! sing~");
}
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
boolean 타입으로 필드를 만들고 플래그를 세워 인스턴스가 하나만 생성됨을 보장하면 된다.
단점3 : 역질렬화 할 때 새로운 인스턴스가 생길 수 있다.
public class ElvisSerialization {
public static void main(String[] args) throws FileNotFoundException {
//저장
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
out.writeObject(Elvis.INSTANCE);
} catch (IOException e) {
e.printStackTrace();
}
//읽기
try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))){
Elvis elvis3 = (Elvis) in.readObject();
System.out.println(elvis3 == Elvis.INSTANCE);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
==> false
직렬화를 통해 데이터를 저장하고, in.readObject() 를 통해 데이터를 읽어들일 때 새로운 인스턴스가 생성된다.
==> 대안
public class Elvis implements IElvis, Serializable {
/**
* 싱글톤 오브젝트
*/
public static final Elvis INSTANCE = new Elvis();
private static boolean created;
private Elvis() {
if(created) {
throw new UnsupportedOperationException("Can't be created by constructor.");
}
created = true;
}
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public void sing() {
System.out.println("sing~ sing! sing~");
}
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
private Object readResolve() {
return INSTANCE;
}
}
Serializable을 구현한다고 선언하고 readResolve() 메서드를 제공해야 한다. 역직렬화 과정에서 호출되는 readObject 메서드가 있더라도 readResolve 메서드에서 반환한 인스턴스로 대체된다. readObject 메서드를 통해 만들어진 인스턴스는 가비지 컬렉션 대상이 된다.
2) private 생성자 + 정적 팩토리 메서드
단점 : 1) private 생성자 + public static final 필드 와 동일
장점1 : API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() {
return INSTANCE;
// Or new Elvis => 클라이언트에 영향을 주지 않고 싱글톤이 아니게 할 수 있음
}
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
정적 팩토리 메서드의 return 값을 new 연산자를 통해 생성해서 반환하면 클라이언트 코드에 영향을 주지 않고 싱글톤이 아니게 바꿀 수 있다.
장점2 : 정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다.
public class MetaElvis<T> {
private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();
private MetaElvis() {}
@SuppressWarnings("unchecked")
public static <T> MetaElvis<T> getInstance() {
return (MetaElvis<T>) INSTANCE;
}
public void say(T t) {
System.out.println(t);
}
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public static void main(String[] args) {
MetaElvis<String> elvis1 = MetaElvis.getInstance();
MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
elvis1.say("hello");
elvis2.say(100);
}
}
동일한 인스턴스를 각자 다른 타입으로 사용할 수 있다.
장점3 : 정적 팩토리의 메서드 참조를 공급자로 사용할 수 있다.
public class Concert {
public void start(Supplier<Singer> singerSupplier) {
Singer singer = singerSupplier.get();
singer.sing();
}
public static void main(String[] args) {
Concert concert = new Concert();
concert.start(Elvis::getInstance);
}
}
public interface Singer {
void sing();
}
=============================================================================================
public class Elvis implements Singer{
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("leave the building");
}
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
@Override
public void sing() {
System.out.println("sing!! sing~~");
}
}
Supplier.get() 메서드에 Elvis.getInstance()로 재정의 해 참조를 공급자로 사용할 수 있다.
3) 열거 타입 방식 - 추천!
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("빌딩 나가기");
}
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
대부분 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는데 가장 좋은 방법이다.
역질렬화나 리플렉션을 걱정할 필요 없이 안전하다 테스트의 어려움 또한 인터페이스를 구현하면 된다.
'Book > 이펙티브 자바' 카테고리의 다른 글
Item6 - 불필요한 객체 생성을 피하라 (0) | 2023.11.17 |
---|---|
Item5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.11.16 |
Item4 - 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.11.15 |
Item2 - 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.11.12 |
Item1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.11.08 |