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

Bobby·2022년 5월 15일
0

이펙티브 자바

목록 보기
3/7
post-thumbnail

싱글턴(singleton)이란?

  • 싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

단점

  1. 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워진다.
  2. 리플렉션 API를 사용해 private 생성자를 호출 할 수 있다. (AccessibleObject.setAccessible)
  3. 역질렬화 할 때 새로운 인스턴스가 생길 수 있다.

싱글턴을 만드는 방식

1. public static final 필드 방식의 싱글턴

  • private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화 할 때 딱 한번만 호출된다.
  • public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화 될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.

장점

  • 해당 클래스가 싱글턴임이 API에 명백히 드러난다.(final 필드)
  • 간결하다.
public class Elvis {

    public static final Elvis INSTANCE = new Elvis(); // 싱글톤

    private Elvis() {}

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }

}
  • 하지만 예외상황이 있다. 리플렉션 API를 사용해 private 생성자를 호출 할 수 있다.

public class ReflectionTest {

    public static void main(String[] args) {
        try {
            Constructor<Elvis> constructor = Elvis.class.getDeclaredConstructor(); 
            defaultConstructor.setAccessible(true); // private 접근 허용
            Elvis elvis1 = constructor.newInstance();
            Elvis elvis2 = constructor.newInstance();
            
 			System.out.println(elvis1);
            System.out.println(elvis2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
  • 주소가 다른 것을 확인 할 수 있다.

  • 리플렉션 API를 통한 인스턴스 생성을 막으려면 생성자를 수정하여 두번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.

public class Elvis {

    public static final Elvis INSTANCE = new Elvis(); // 싱글톤
	
    private static boolean created; // 생성 여부 판단

    private Elvis() {
    	// 이미 생성된 경우 예외 발생
    	if(create) throw new RuntimeException("Instance already exists.")
        create = true;
    }

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }

}
  • 또 다른 문제점으로 역질렬화 시 새로운 인스턴스가 생성된다.
  • Serializable을 상속 받고 readResolve 메서드를 제공하면 같은 인스턴스를 반환할 수 있다.
public class Elvis implements Serializable {

    public static final Elvis INSTANCE = new Elvis(); // 싱글톤
	
    private static boolean created; // 생성 여부 판단

    private Elvis() {
    	// 이미 생성된 경우 예외 발생
    	if(create) throw new RuntimeException("Instance already exists.")
        create = true;
    }

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }
    
    @Serial
    private Object readResolve() {
        return INSTANCE;
    }

}

2. 정적 팩터리 메서드를 pubilc static 멤버로 제공한다.

  • getInstance()는 항상 같은 객체의 참조를 반환한다.
  • 이 방식 또한 방법 1 처럼 리플렉션 API를 사용하여 인스턴스를 생성 할 수 있는 문제가 있다.
  • 이 방식 또한 방법 1 처럼 역직렬화 할 때 새로운 인스턴스가 생성된다.
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }
}

장점

  • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수있다.
	public static Elvis getInstance() { return new Elvis; }
  • 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
public class Elvis<T> {
    private static final Elvis<Object> INSTANCE = new Elvis<>();
    private Elvis() { }
    public static <T> Elvis<T> getInstance() { return (Elvis<T>) INSTANCE; }

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }
}
  • 팩터리 메서드 참조를 공급자(supplier)로 사용할 수 있다.
	Supplier<Elvis> supplier = Elvis::getInstance;

3. 열거 타입 방식의 싱글턴

장점

  • 가장 간결하다.
  • 리플렉션을 사용해도 객체를 생성할 수 없다.
  • 역직렬화 시에도 같은 인스턴스를 반환한다.
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }
}
  • 대부분의 상황에서는 이 방법이 가장 좋다. 하지만 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다 (열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.)

싱글턴 클래스 테스트

  • 싱글턴 클래스의 경우에는 인스턴스를 하나만 생성하므로 가짜(Mock) 객체를 만들 수가 없기 때문이다.
  • 진짜 객체로 테스트 해도 문제가 없지만 만약 메서드 동작이 오래 걸리거나 혹은 외부 API 호출, DB 접근 등의 작업을 해야한다면 문제가 생길 수 있다.
  • 인터페이스로 정의하여 가짜 객체로 교체하여 테스트 하면 된다.
public interface IElvis {

    void leaveTheBuilding();

}
public class Elvis implement IElvis {

    public static final Elvis INSTANCE = new Elvis(); // 싱글톤

    private Elvis() {}

	@Override
    public void leaveTheBuilding() { 
    	System.out.println("leave the building");
    }

}
  • Mock 클래스 생성하여 테스트시 사용한다.
public class MockElvis implement IElvis {

	@Override
    public void leaveTheBuilding() { 
    	System.out.println("Mock leave the building");
    }

}
profile
물흐르듯 개발하다 대박나기

0개의 댓글